Files
2026-04-15 11:11:44 +03:30

191 lines
6.4 KiB
Python

"""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