first init
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
"""Bypass strategy implementations."""
|
||||
|
||||
from .base import BypassStrategy
|
||||
from .fragment import FragmentBypass
|
||||
from .fake_sni import FakeSNIBypass
|
||||
from .combined import CombinedBypass
|
||||
from .raw_injector import RawInjector, is_raw_available
|
||||
|
||||
__all__ = [
|
||||
"BypassStrategy",
|
||||
"FragmentBypass",
|
||||
"FakeSNIBypass",
|
||||
"CombinedBypass",
|
||||
"RawInjector",
|
||||
"is_raw_available",
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Base class for bypass strategies."""
|
||||
|
||||
import abc
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BypassStrategy(abc.ABC):
|
||||
"""Abstract base for DPI bypass strategies.
|
||||
|
||||
Each strategy implements a different technique for evading
|
||||
Deep Packet Inspection when forwarding TCP connections.
|
||||
"""
|
||||
|
||||
name: str = "base"
|
||||
|
||||
@abc.abstractmethod
|
||||
async def apply(
|
||||
self,
|
||||
client_sock: socket.socket,
|
||||
server_sock: socket.socket,
|
||||
fake_sni: str,
|
||||
first_data: bytes,
|
||||
loop=None,
|
||||
) -> bool:
|
||||
"""Apply the bypass strategy to an outgoing connection.
|
||||
|
||||
This method is called after the TCP connection to the server
|
||||
is established but before any real data is forwarded.
|
||||
|
||||
Args:
|
||||
client_sock: The incoming client socket
|
||||
server_sock: The outgoing socket to the real server
|
||||
fake_sni: The fake SNI hostname to use
|
||||
first_data: First data received from the client
|
||||
loop: asyncio event loop
|
||||
|
||||
Returns:
|
||||
True if strategy was applied successfully, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} strategy='{self.name}'>"
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Combined bypass strategy.
|
||||
|
||||
Combines multiple bypass techniques for maximum effectiveness.
|
||||
|
||||
With raw sockets (Linux + root):
|
||||
1. The raw injector sends a fake ClientHello with an out-of-window
|
||||
seq number during the TCP handshake (DPI parses it, server drops it)
|
||||
2. Then the real ClientHello is fragmented at the SNI boundary
|
||||
Both techniques hit DPI at once.
|
||||
|
||||
Without raw sockets (fallback):
|
||||
Uses fragmentation only (with optional TTL trick for the fake).
|
||||
The fake_sni prefix method is NOT used on the real TCP stream
|
||||
because it corrupts the TLS handshake.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .base import BypassStrategy
|
||||
from ..tls import ClientHelloBuilder
|
||||
from ..tls.fragment import fragment_client_hello, fragment_data
|
||||
|
||||
logger = logging.getLogger("snispf")
|
||||
|
||||
|
||||
class CombinedBypass(BypassStrategy):
|
||||
"""Combined DPI bypass using multiple techniques simultaneously.
|
||||
|
||||
With raw injector available:
|
||||
1. Fake ClientHello injected out-of-window (by the sniffer/injector)
|
||||
2. Real ClientHello fragmented at SNI boundary
|
||||
3. Small inter-fragment delays
|
||||
|
||||
Without raw injector:
|
||||
1. (Optional) TTL trick to send fake ClientHello that expires
|
||||
before reaching the server
|
||||
2. Real ClientHello fragmented at SNI boundary
|
||||
3. Small inter-fragment delays
|
||||
"""
|
||||
|
||||
name = "combined"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fragment_strategy: str = "sni_split",
|
||||
use_ttl_trick: bool = False,
|
||||
fragment_delay: float = 0.1,
|
||||
fake_first: bool = True,
|
||||
raw_injector=None,
|
||||
):
|
||||
self.fragment_strategy = fragment_strategy
|
||||
self.use_ttl_trick = use_ttl_trick
|
||||
self.fragment_delay = fragment_delay
|
||||
self.fake_first = fake_first
|
||||
self.raw_injector = raw_injector
|
||||
|
||||
async def apply(
|
||||
self,
|
||||
client_sock: socket.socket,
|
||||
server_sock: socket.socket,
|
||||
fake_sni: str,
|
||||
first_data: bytes,
|
||||
loop=None,
|
||||
) -> bool:
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
try:
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
# Step 1: Handle fake ClientHello
|
||||
if self.raw_injector is not None:
|
||||
# Raw injector already sent the fake out-of-window during
|
||||
# the TCP handshake. Wait for server confirmation.
|
||||
local_port = server_sock.getsockname()[1]
|
||||
confirmed = await loop.run_in_executor(
|
||||
None,
|
||||
self.raw_injector.wait_for_confirmation,
|
||||
local_port,
|
||||
2.0,
|
||||
)
|
||||
if not confirmed:
|
||||
logger.warning(
|
||||
f"port={local_port}: no confirmation that server "
|
||||
f"ignored the fake packet (timeout)"
|
||||
)
|
||||
|
||||
elif self.fake_first and self.use_ttl_trick:
|
||||
# TTL trick: send fake with low TTL so it reaches DPI
|
||||
# but expires before the server
|
||||
fake_hello = ClientHelloBuilder.build_client_hello(sni=fake_sni)
|
||||
try:
|
||||
original_ttl = server_sock.getsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_TTL
|
||||
)
|
||||
server_sock.setsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_TTL, 3
|
||||
)
|
||||
await loop.sock_sendall(server_sock, fake_hello)
|
||||
await asyncio.sleep(0.05)
|
||||
server_sock.setsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_TTL, original_ttl
|
||||
)
|
||||
except OSError:
|
||||
# TTL trick not available, skip the fake entirely
|
||||
pass
|
||||
|
||||
await asyncio.sleep(0.001)
|
||||
|
||||
# NOTE: Without raw sockets or TTL trick, we do NOT send a fake
|
||||
# ClientHello on the real TCP stream. It would corrupt the
|
||||
# handshake because the server receives it as real data.
|
||||
|
||||
# Step 2: Fragment and send the real ClientHello
|
||||
fragments = fragment_client_hello(first_data, self.fragment_strategy)
|
||||
|
||||
for i, fragment in enumerate(fragments):
|
||||
await loop.sock_sendall(server_sock, fragment)
|
||||
if i < len(fragments) - 1 and self.fragment_delay > 0:
|
||||
await asyncio.sleep(self.fragment_delay)
|
||||
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Fake SNI bypass strategy.
|
||||
|
||||
Sends a fake TLS ClientHello with an allowed SNI that DPI will parse
|
||||
and whitelist, but the real server will ignore.
|
||||
|
||||
Two operating modes:
|
||||
- With raw sockets (Linux + root): Uses the seq_id trick from the
|
||||
original tool. Injects a fake ClientHello with an out-of-window
|
||||
TCP sequence number. DPI parses it, server drops it.
|
||||
- Without raw sockets (fallback): Sends the real ClientHello in
|
||||
fragments so DPI cannot read the SNI from any single packet.
|
||||
The fake_sni prefix method does NOT work without raw sockets
|
||||
because sending the fake on the same TCP stream corrupts the
|
||||
TLS handshake.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from .base import BypassStrategy
|
||||
from ..tls import ClientHelloBuilder
|
||||
from ..tls.fragment import fragment_client_hello
|
||||
|
||||
logger = logging.getLogger("snispf")
|
||||
|
||||
|
||||
class FakeSNIBypass(BypassStrategy):
|
||||
"""Bypass DPI by injecting a fake TLS ClientHello with spoofed SNI.
|
||||
|
||||
The only reliable way to do this is with raw socket injection
|
||||
(out-of-window seq trick). When raw sockets are not available,
|
||||
this falls back to TLS fragmentation which hides the SNI by
|
||||
splitting it across TCP segments.
|
||||
|
||||
Methods:
|
||||
- "raw_inject" - Inject fake ClientHello with wrong seq number
|
||||
via AF_PACKET. DPI sees it, server drops it. (Linux + root)
|
||||
- "ttl_trick" - Send fake with low IP TTL. May reach DPI but
|
||||
expire before the server. Unreliable, platform-dependent.
|
||||
- "fragment_fallback" - Falls back to fragmenting the real
|
||||
ClientHello. No fake is sent on the real stream.
|
||||
"""
|
||||
|
||||
name = "fake_sni"
|
||||
|
||||
def __init__(self, method: str = "prefix_fake", raw_injector=None):
|
||||
self.method = method
|
||||
self.raw_injector = raw_injector
|
||||
|
||||
async def apply(
|
||||
self,
|
||||
client_sock: socket.socket,
|
||||
server_sock: socket.socket,
|
||||
fake_sni: str,
|
||||
first_data: bytes,
|
||||
loop=None,
|
||||
) -> bool:
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# If we have a raw injector running, the fake was already injected
|
||||
# during the TCP handshake. Just send the real data and go.
|
||||
if self.raw_injector is not None:
|
||||
return await self._raw_inject_send(
|
||||
server_sock, first_data, loop
|
||||
)
|
||||
|
||||
# Without raw sockets, the old "prefix_fake" method of sending
|
||||
# a fake ClientHello on the same TCP stream is broken - the server
|
||||
# receives both and the TLS handshake fails. Fall back to
|
||||
# TTL trick if requested, otherwise just fragment the real hello.
|
||||
if self.method == "ttl_trick":
|
||||
return await self._ttl_trick(
|
||||
server_sock, fake_sni, first_data, loop
|
||||
)
|
||||
else:
|
||||
# Fragment fallback: split the real ClientHello so DPI can't
|
||||
# read the SNI from any single packet.
|
||||
return await self._fragment_fallback(
|
||||
server_sock, first_data, loop
|
||||
)
|
||||
|
||||
async def _raw_inject_send(
|
||||
self,
|
||||
server_sock: socket.socket,
|
||||
first_data: bytes,
|
||||
loop,
|
||||
) -> bool:
|
||||
"""With raw injection, the fake was already sent out-of-window.
|
||||
Just send the real ClientHello normally."""
|
||||
try:
|
||||
local_port = server_sock.getsockname()[1]
|
||||
|
||||
# Wait for the sniffer to confirm the server ignored the fake.
|
||||
confirmed = await loop.run_in_executor(
|
||||
None,
|
||||
self.raw_injector.wait_for_confirmation,
|
||||
local_port,
|
||||
2.0,
|
||||
)
|
||||
|
||||
if not confirmed:
|
||||
logger.warning(
|
||||
f"port={local_port}: server did not confirm fake was "
|
||||
f"ignored (timeout). Sending real data anyway."
|
||||
)
|
||||
|
||||
# Send the real ClientHello (untouched)
|
||||
await loop.sock_sendall(server_sock, first_data)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _ttl_trick(
|
||||
self,
|
||||
server_sock: socket.socket,
|
||||
fake_sni: str,
|
||||
first_data: bytes,
|
||||
loop,
|
||||
) -> bool:
|
||||
"""Send fake ClientHello with low TTL, then real data normally.
|
||||
|
||||
The fake packet has a TTL low enough to expire before reaching
|
||||
the server, but the DPI middlebox (typically 1-3 hops away)
|
||||
will see it. This is unreliable depending on network topology.
|
||||
"""
|
||||
try:
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
fake_hello = ClientHelloBuilder.build_client_hello(sni=fake_sni)
|
||||
|
||||
# Save original TTL
|
||||
original_ttl = server_sock.getsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_TTL
|
||||
)
|
||||
|
||||
# Low TTL: reaches DPI (1-5 hops) but expires before server
|
||||
server_sock.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, 3)
|
||||
|
||||
try:
|
||||
await loop.sock_sendall(server_sock, fake_hello)
|
||||
except OSError:
|
||||
pass # May get ICMP TTL exceeded
|
||||
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
# Restore normal TTL
|
||||
server_sock.setsockopt(
|
||||
socket.IPPROTO_IP, socket.IP_TTL, original_ttl
|
||||
)
|
||||
|
||||
# Send real ClientHello normally
|
||||
await loop.sock_sendall(server_sock, first_data)
|
||||
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _fragment_fallback(
|
||||
self,
|
||||
server_sock: socket.socket,
|
||||
first_data: bytes,
|
||||
loop,
|
||||
) -> bool:
|
||||
"""Fallback: fragment the real ClientHello at the SNI boundary.
|
||||
|
||||
Without raw sockets we cannot safely send a fake ClientHello
|
||||
(it would corrupt the TLS stream). Instead, fragment the real
|
||||
ClientHello so DPI cannot read the full SNI from a single packet.
|
||||
"""
|
||||
try:
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
fragments = fragment_client_hello(first_data, "sni_split")
|
||||
|
||||
for i, fragment in enumerate(fragments):
|
||||
await loop.sock_sendall(server_sock, fragment)
|
||||
if i < len(fragments) - 1:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Fragment-based DPI bypass strategy.
|
||||
|
||||
Splits the real TLS ClientHello into fragments so that DPI systems
|
||||
that only inspect the first packet or don't reassemble TCP streams
|
||||
cannot read the SNI.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import socket
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .base import BypassStrategy
|
||||
from ..tls.fragment import fragment_client_hello
|
||||
|
||||
|
||||
class FragmentBypass(BypassStrategy):
|
||||
"""Bypass DPI by fragmenting the TLS ClientHello.
|
||||
|
||||
This is the most compatible cross-platform bypass method.
|
||||
It works by splitting the ClientHello into multiple TCP segments,
|
||||
with the split point strategically placed in the middle of the
|
||||
SNI extension value.
|
||||
|
||||
DPI systems that don't reassemble TCP streams will see an incomplete
|
||||
SNI in the first packet and won't be able to filter it.
|
||||
"""
|
||||
|
||||
name = "fragment"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
strategy: str = "sni_split",
|
||||
fragment_delay: float = 0.1,
|
||||
tcp_nodelay: bool = True,
|
||||
):
|
||||
"""Initialize fragment bypass.
|
||||
|
||||
Args:
|
||||
strategy: Fragmentation strategy (sni_split, half, multi, tls_record_frag)
|
||||
fragment_delay: Delay between fragments in seconds
|
||||
tcp_nodelay: Enable TCP_NODELAY to send fragments immediately
|
||||
"""
|
||||
self.strategy = strategy
|
||||
self.fragment_delay = fragment_delay
|
||||
self.tcp_nodelay = tcp_nodelay
|
||||
|
||||
async def apply(
|
||||
self,
|
||||
client_sock: socket.socket,
|
||||
server_sock: socket.socket,
|
||||
fake_sni: str,
|
||||
first_data: bytes,
|
||||
loop=None,
|
||||
) -> bool:
|
||||
"""Apply fragmentation to the first TLS record.
|
||||
|
||||
The first_data from the client (usually a TLS ClientHello) is
|
||||
fragmented and sent to the server in multiple TCP segments.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
try:
|
||||
# Enable TCP_NODELAY so each send() becomes its own segment
|
||||
if self.tcp_nodelay:
|
||||
server_sock.setsockopt(
|
||||
socket.IPPROTO_TCP, socket.TCP_NODELAY, 1
|
||||
)
|
||||
|
||||
# Fragment the ClientHello
|
||||
fragments = fragment_client_hello(first_data, self.strategy)
|
||||
|
||||
# Send each fragment as a separate TCP segment
|
||||
for i, fragment in enumerate(fragments):
|
||||
await loop.sock_sendall(server_sock, fragment)
|
||||
if i < len(fragments) - 1 and self.fragment_delay > 0:
|
||||
await asyncio.sleep(self.fragment_delay)
|
||||
|
||||
# Disable TCP_NODELAY after fragments are sent (optional)
|
||||
if self.tcp_nodelay:
|
||||
server_sock.setsockopt(
|
||||
socket.IPPROTO_TCP, socket.TCP_NODELAY, 0
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
@@ -0,0 +1,418 @@
|
||||
"""Raw socket packet injection for out-of-window fake SNI.
|
||||
|
||||
Implements the seq_id trick from the Go reference:
|
||||
1. Sniff the outbound SYN to record the ISN (Initial Sequence Number)
|
||||
2. Sniff the outbound 3rd ACK (handshake complete)
|
||||
3. Inject a fake TLS ClientHello with seq = ISN+1 - len(fake)
|
||||
This puts it BEFORE the server's receive window, so the server drops it,
|
||||
but DPI sees and parses the fake SNI.
|
||||
4. Wait for the server to ACK with ack == ISN+1, confirming the fake was
|
||||
ignored and the server still expects the real data.
|
||||
|
||||
Linux only. Requires CAP_NET_RAW (run as root).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, Dict
|
||||
|
||||
logger = logging.getLogger("snispf")
|
||||
|
||||
ETH_P_IP = 0x0800
|
||||
ETH_P_ALL = 0x0003
|
||||
IPPROTO_TCP = 6
|
||||
|
||||
# TCP flags
|
||||
FIN = 0x01
|
||||
SYN = 0x02
|
||||
RST = 0x04
|
||||
PSH = 0x08
|
||||
ACK = 0x10
|
||||
|
||||
|
||||
def _htons(v):
|
||||
return socket.htons(v)
|
||||
|
||||
|
||||
def _ip_hdr_len(ip_bytes):
|
||||
return (ip_bytes[0] & 0x0F) * 4
|
||||
|
||||
|
||||
def _checksum_fold(s):
|
||||
while s >> 16:
|
||||
s = (s & 0xFFFF) + (s >> 16)
|
||||
return (~s) & 0xFFFF
|
||||
|
||||
|
||||
def _sum16(data):
|
||||
s = 0
|
||||
for i in range(0, len(data) - 1, 2):
|
||||
s += (data[i] << 8) | data[i + 1]
|
||||
if len(data) % 2 == 1:
|
||||
s += data[-1] << 8
|
||||
while s >> 16:
|
||||
s = (s & 0xFFFF) + (s >> 16)
|
||||
return s
|
||||
|
||||
|
||||
def _ip_checksum(iph):
|
||||
return _checksum_fold(_sum16(iph))
|
||||
|
||||
|
||||
def _tcp_checksum(iph, tcp_with_payload):
|
||||
ihl = _ip_hdr_len(iph)
|
||||
pseudo = bytearray(12)
|
||||
pseudo[0:4] = iph[12:16] # src IP
|
||||
pseudo[4:8] = iph[16:20] # dst IP
|
||||
pseudo[9] = 6 # TCP protocol
|
||||
struct.pack_into("!H", pseudo, 10, len(tcp_with_payload))
|
||||
return _checksum_fold(_sum16(pseudo) + _sum16(tcp_with_payload))
|
||||
|
||||
|
||||
def _build_fake_frame(template_pkt, isn, fake_payload):
|
||||
"""Build the injection frame from a captured 3rd-ACK packet template.
|
||||
|
||||
Takes the captured Ethernet+IP+TCP headers from the 3rd handshake ACK,
|
||||
appends the fake TLS ClientHello as payload, and sets:
|
||||
- seq = ISN + 1 - len(fake_payload) (out of window for the server)
|
||||
- PSH flag added
|
||||
- Proper IP and TCP checksums recalculated
|
||||
"""
|
||||
ip_off = 14 # Ethernet header is 14 bytes
|
||||
ihl = _ip_hdr_len(template_pkt[ip_off:])
|
||||
tcp_off = ip_off + ihl
|
||||
tcp_hdr_len = (template_pkt[tcp_off + 12] >> 4) * 4
|
||||
|
||||
# Copy headers (Ethernet + IP + TCP) and append fake payload
|
||||
headers = bytearray(template_pkt[:tcp_off + tcp_hdr_len])
|
||||
out = headers + fake_payload
|
||||
|
||||
# Update IP total length
|
||||
struct.pack_into("!H", out, ip_off + 2, len(out) - ip_off)
|
||||
|
||||
# Increment IP ID
|
||||
old_id = struct.unpack("!H", out[ip_off + 4:ip_off + 6])[0]
|
||||
struct.pack_into("!H", out, ip_off + 4, (old_id + 1) & 0xFFFF)
|
||||
|
||||
# Recalculate IP checksum
|
||||
out[ip_off + 10] = 0
|
||||
out[ip_off + 11] = 0
|
||||
ip_cksum = _ip_checksum(out[ip_off:ip_off + ihl])
|
||||
struct.pack_into("!H", out, ip_off + 10, ip_cksum)
|
||||
|
||||
# Set PSH flag
|
||||
out[tcp_off + 13] |= PSH
|
||||
|
||||
# Set out-of-window sequence number: ISN + 1 - len(fake)
|
||||
seq = (isn + 1 - len(fake_payload)) & 0xFFFFFFFF
|
||||
struct.pack_into("!I", out, tcp_off + 4, seq)
|
||||
|
||||
# Recalculate TCP checksum
|
||||
out[tcp_off + 16] = 0
|
||||
out[tcp_off + 17] = 0
|
||||
tcp_cksum = _tcp_checksum(
|
||||
out[ip_off:ip_off + ihl],
|
||||
bytes(out[tcp_off:]),
|
||||
)
|
||||
struct.pack_into("!H", out, tcp_off + 16, tcp_cksum)
|
||||
|
||||
return bytes(out)
|
||||
|
||||
|
||||
class PortState:
|
||||
"""Per-connection state tracked by the sniffer."""
|
||||
|
||||
def __init__(self, syn_seq, fake_hello):
|
||||
self.syn_seq = syn_seq
|
||||
self.fake_hello = fake_hello
|
||||
self.fake_sent = False
|
||||
self.confirmed = threading.Event()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
|
||||
class RawInjector:
|
||||
"""Raw socket sniffer and injector for out-of-window fake SNI.
|
||||
|
||||
This is the core mechanism that makes the seq_id trick work:
|
||||
- Monitors all TCP traffic between local and target IPs
|
||||
- When a new outbound SYN is detected, records the ISN
|
||||
- When the 3rd handshake ACK is seen, injects the fake ClientHello
|
||||
- Waits for server confirmation (ACK with ack == ISN+1)
|
||||
"""
|
||||
|
||||
def __init__(self, local_ip, remote_ip, remote_port, fake_sni_builder):
|
||||
self.local_ip = socket.inet_aton(local_ip)
|
||||
self.remote_ip = socket.inet_aton(remote_ip)
|
||||
self.remote_port = remote_port
|
||||
self.fake_sni_builder = fake_sni_builder
|
||||
|
||||
self.ports: Dict[int, PortState] = {}
|
||||
self.ports_lock = threading.Lock()
|
||||
|
||||
self.raw_fd = None
|
||||
self.iface_idx = None
|
||||
self.iface_name = None
|
||||
self.running = False
|
||||
self._sniffer_thread = None
|
||||
|
||||
def start(self):
|
||||
"""Open the raw socket and start the sniffer loop."""
|
||||
try:
|
||||
self.raw_fd = socket.socket(
|
||||
socket.AF_PACKET,
|
||||
socket.SOCK_RAW,
|
||||
socket.htons(ETH_P_ALL),
|
||||
)
|
||||
except (PermissionError, OSError) as e:
|
||||
logger.warning(f"Cannot open AF_PACKET socket: {e}")
|
||||
logger.warning("Raw injection unavailable - need root/CAP_NET_RAW")
|
||||
return False
|
||||
|
||||
# Find the interface
|
||||
iface_info = self._find_interface()
|
||||
if iface_info is None:
|
||||
logger.warning("Cannot determine outgoing interface for raw injection")
|
||||
self.raw_fd.close()
|
||||
self.raw_fd = None
|
||||
return False
|
||||
|
||||
self.iface_name, self.iface_idx = iface_info
|
||||
self.raw_fd.bind((self.iface_name, ETH_P_ALL))
|
||||
|
||||
self.running = True
|
||||
self._sniffer_thread = threading.Thread(
|
||||
target=self._sniff_loop, daemon=True
|
||||
)
|
||||
self._sniffer_thread.start()
|
||||
logger.info("Raw packet injector started")
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
"""Stop the sniffer."""
|
||||
self.running = False
|
||||
if self.raw_fd:
|
||||
try:
|
||||
self.raw_fd.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _find_interface(self):
|
||||
"""Find the network interface name and index for the target IP.
|
||||
|
||||
Returns:
|
||||
Tuple of (interface_name, interface_index) or None if not found.
|
||||
"""
|
||||
import fcntl
|
||||
import array
|
||||
|
||||
try:
|
||||
# Use a UDP connect to find which interface is used
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect((socket.inet_ntoa(self.remote_ip), 53))
|
||||
local_addr = s.getsockname()[0]
|
||||
s.close()
|
||||
|
||||
# Get all interfaces and find the matching one
|
||||
# Using SIOCGIFCONF
|
||||
max_bytes = 8096
|
||||
buf = array.array("B", b"\0" * max_bytes)
|
||||
ifconf = struct.pack("iL", max_bytes, buf.buffer_info()[0])
|
||||
result = fcntl.ioctl(
|
||||
self.raw_fd.fileno(), 0x8912, ifconf # SIOCGIFCONF
|
||||
)
|
||||
out_bytes = struct.unpack("iL", result)[0]
|
||||
|
||||
offset = 0
|
||||
while offset < out_bytes:
|
||||
name = buf[offset:offset + 16].tobytes().split(b"\0", 1)[0]
|
||||
ip_bytes = buf[offset + 20:offset + 24].tobytes()
|
||||
ip_str = socket.inet_ntoa(ip_bytes)
|
||||
if ip_str == local_addr:
|
||||
iface_name = name.decode("ascii", errors="replace")
|
||||
# Get interface index
|
||||
ifreq = struct.pack("16sI", name, 0)
|
||||
result = fcntl.ioctl(
|
||||
self.raw_fd.fileno(), 0x8933, ifreq # SIOCGIFINDEX
|
||||
)
|
||||
idx = struct.unpack("16sI", result)[1]
|
||||
logger.debug(f"Using interface {iface_name} (index {idx})")
|
||||
return (iface_name, idx)
|
||||
offset += 40 # struct ifreq size
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Interface detection error: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def register_port(self, local_port, fake_hello):
|
||||
"""Register a port for monitoring (called before connect)."""
|
||||
with self.ports_lock:
|
||||
self.ports[local_port] = PortState(0, fake_hello)
|
||||
|
||||
def wait_for_confirmation(self, local_port, timeout=2.0):
|
||||
"""Wait for the server to confirm it ignored the fake packet.
|
||||
|
||||
Returns True if confirmed, False on timeout.
|
||||
"""
|
||||
with self.ports_lock:
|
||||
ps = self.ports.get(local_port)
|
||||
|
||||
if ps is None:
|
||||
return False
|
||||
|
||||
return ps.confirmed.wait(timeout=timeout)
|
||||
|
||||
def cleanup_port(self, local_port):
|
||||
"""Clean up state for a port."""
|
||||
with self.ports_lock:
|
||||
self.ports.pop(local_port, None)
|
||||
|
||||
def _inject_frame(self, frame):
|
||||
"""Inject a raw Ethernet frame."""
|
||||
try:
|
||||
addr = (
|
||||
self.iface_name or "", # interface name
|
||||
ETH_P_IP,
|
||||
0, # packet type
|
||||
0, # arp hardware type
|
||||
frame[0:6], # destination MAC
|
||||
)
|
||||
self.raw_fd.sendto(frame, addr)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"Inject error: {e}")
|
||||
# Fallback: try sendto with sockaddr_ll style
|
||||
try:
|
||||
sll = struct.pack(
|
||||
"HH I BB 8s",
|
||||
socket.htons(ETH_P_IP), # protocol
|
||||
self.iface_idx, # ifindex
|
||||
0, # pkttype
|
||||
6, # halen
|
||||
0,
|
||||
frame[0:8], # addr
|
||||
)
|
||||
os.write(self.raw_fd.fileno(), frame)
|
||||
return True
|
||||
except Exception as e2:
|
||||
logger.debug(f"Inject fallback error: {e2}")
|
||||
return False
|
||||
|
||||
def _sniff_loop(self):
|
||||
"""Main sniffer loop - watches TCP handshakes and injects fake packets."""
|
||||
while self.running:
|
||||
try:
|
||||
pkt, _ = self.raw_fd.recvfrom(65536)
|
||||
except (OSError, socket.error):
|
||||
if not self.running:
|
||||
break
|
||||
continue
|
||||
|
||||
if len(pkt) < 14 + 20 + 20:
|
||||
continue
|
||||
|
||||
# Check Ethernet type is IPv4
|
||||
eth_type = struct.unpack("!H", pkt[12:14])[0]
|
||||
if eth_type != ETH_P_IP:
|
||||
continue
|
||||
|
||||
ip = pkt[14:]
|
||||
if (ip[0] >> 4) != 4 or ip[9] != IPPROTO_TCP:
|
||||
continue
|
||||
|
||||
ihl = _ip_hdr_len(ip)
|
||||
src_ip = ip[12:16]
|
||||
dst_ip = ip[16:20]
|
||||
tcp = ip[ihl:]
|
||||
if len(tcp) < 20:
|
||||
continue
|
||||
|
||||
flags = tcp[13]
|
||||
tcp_hdr_len = (tcp[12] >> 4) * 4
|
||||
payload_len = len(tcp) - tcp_hdr_len
|
||||
|
||||
outbound = (src_ip == self.local_ip and dst_ip == self.remote_ip)
|
||||
inbound = (src_ip == self.remote_ip and dst_ip == self.local_ip)
|
||||
|
||||
if outbound:
|
||||
src_port = struct.unpack("!H", tcp[0:2])[0]
|
||||
seq = struct.unpack("!I", tcp[4:8])[0]
|
||||
|
||||
# SYN (no ACK): new outbound connection
|
||||
if (flags & SYN) and not (flags & ACK):
|
||||
with self.ports_lock:
|
||||
ps = self.ports.get(src_port)
|
||||
if ps is not None:
|
||||
with ps.lock:
|
||||
ps.syn_seq = seq
|
||||
logger.debug(
|
||||
f"[sniff] SYN port={src_port} isn={seq}"
|
||||
)
|
||||
continue
|
||||
|
||||
# 3rd-handshake ACK: ACK only, no payload
|
||||
if (flags & ACK) and not (flags & (SYN | FIN | RST)) and payload_len == 0:
|
||||
with self.ports_lock:
|
||||
ps = self.ports.get(src_port)
|
||||
if ps is None:
|
||||
continue
|
||||
|
||||
with ps.lock:
|
||||
if ps.fake_sent:
|
||||
continue
|
||||
ps.fake_sent = True
|
||||
syn_seq = ps.syn_seq
|
||||
fake = ps.fake_hello
|
||||
|
||||
# Inject after a tiny delay (like the Go version's 1ms)
|
||||
tpl_copy = bytearray(pkt)
|
||||
|
||||
def _do_inject(tpl=tpl_copy, isn=syn_seq, payload=fake, port=src_port):
|
||||
time.sleep(0.001)
|
||||
frame = _build_fake_frame(bytes(tpl), isn, payload)
|
||||
if self._inject_frame(frame):
|
||||
out_seq = (isn + 1 - len(payload)) & 0xFFFFFFFF
|
||||
logger.debug(
|
||||
f"[inject] port={port} fake seq={out_seq} "
|
||||
f"(ISN={isn}, fake_len={len(payload)})"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"[inject] port={port} injection failed")
|
||||
|
||||
threading.Thread(target=_do_inject, daemon=True).start()
|
||||
|
||||
if inbound:
|
||||
dst_port = struct.unpack("!H", tcp[2:4])[0]
|
||||
ack_num = struct.unpack("!I", tcp[8:12])[0]
|
||||
|
||||
# Server's ACK confirming fake was ignored
|
||||
if (flags & ACK) and not (flags & (SYN | FIN | RST)) and payload_len == 0:
|
||||
with self.ports_lock:
|
||||
ps = self.ports.get(dst_port)
|
||||
if ps is None:
|
||||
continue
|
||||
|
||||
with ps.lock:
|
||||
if ps.fake_sent and ack_num == (ps.syn_seq + 1) & 0xFFFFFFFF:
|
||||
if not ps.confirmed.is_set():
|
||||
ps.confirmed.set()
|
||||
logger.debug(
|
||||
f"[sniff] port={dst_port} CONFIRMED "
|
||||
f"server acked ISN+1={ack_num}"
|
||||
)
|
||||
|
||||
|
||||
def is_raw_available():
|
||||
"""Check if raw socket injection is available on this system."""
|
||||
try:
|
||||
s = socket.socket(
|
||||
socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL)
|
||||
)
|
||||
s.close()
|
||||
return True
|
||||
except (PermissionError, OSError, AttributeError):
|
||||
return False
|
||||
Reference in New Issue
Block a user