first init
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user