191 lines
6.4 KiB
Python
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
|