· 7 min read ·

Injecting Fake TLS Hellos from the Kernel: How gecit Uses eBPF to Fool DPI

Source: lobsters

Deep packet inspection systems that filter HTTPS traffic almost always key on one thing: the Server Name Indication field in the TLS ClientHello. SNI is sent in plaintext before the encrypted channel is established, and it was designed that way intentionally so that a single server can host multiple TLS certificates. ISPs, corporate firewalls, and national filtering infrastructure all learned to exploit this. The SNI is the most legible thing in an HTTPS connection, and it arrives before any key material has been exchanged.

A recent project called gecit takes a genuinely interesting approach to defeating this: it hooks into the kernel’s socket operations layer via eBPF, detects new outgoing TLS connections the moment they are established, then fires a synthetic ClientHello with a fabricated SNI and a deliberately low TTL through a raw socket before the real handshake begins. The fake packet fools the filtering device. The real one goes on to finish the handshake with the actual server. Neither side ends up confused, because the low TTL ensures the decoy expires before it reaches the destination.

What DPI Systems Actually See

TLS 1.3 encrypts almost everything, but the ClientHello is the unavoidable exception. The record layer starts with a plaintext 5-byte header (content type 0x16, version 0x0301, length), followed by the handshake header, and then the ClientHello body. Inside that body, the server_name extension (type 0x0000) contains the hostname the client intends to reach. A DPI box sitting inline or as a passive tap reads this field to determine whether the connection is permitted.

The structure in hex looks roughly like:

16 03 01 [length]       ; TLS record header
01 [length]             ; ClientHello handshake header
03 03 [random 32 bytes] ; version + random
[session id]
[cipher suites]
[compression methods]
[extensions]
  00 00 [length]        ; extension type: server_name
    [server name list]
      00 [hostname length] [hostname bytes]

The hostname bytes are ASCII and completely readable to anyone on the path. Filtering systems match them against blocklists with essentially zero overhead.

The eBPF Hook That Fires at the Right Moment

gebcit’s kernel-side component uses BPF_PROG_TYPE_SOCK_OPS with the BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB callback. This callback fires on the active (client) side of a TCP connection when the three-way handshake completes: the SYN-ACK has been received and acknowledged. At this point, the kernel has a fully established socket but no application data has been sent yet. For a TLS connection to port 443, this is the precise window before the ClientHello goes out.

The bpf_sock_ops structure passed to the program contains the four-tuple: source address, source port, destination address, destination port. The program can inspect these to decide whether to act, and it can use helper functions like bpf_sock_ops_cb_flags_set() to request additional callbacks on subsequent events. It can also call into a BPF map to communicate with a userspace process that will do the actual packet construction.

This is cleaner than the alternatives. Netfilter hooks require the packet to be in flight; you either modify it in place (which has constraints on packet headroom) or you drop and reinject it (which adds latency and complexity). The sock_ops hook fires before any send call, so the eBPF program can signal userspace to construct and dispatch the fake packet while the application layer is still preparing the real ClientHello.

The TTL Trick and Why It Works

The fake ClientHello is sent via a raw socket with a manually specified IP TTL. The value is set low, typically in the range of 3 to 8, calibrated to expire before the packet reaches the destination server while still passing through the filtering device.

This works because of a geographic and topological assumption: the DPI infrastructure is close to the subscriber, often co-located with the ISP’s access equipment. The actual destination server, a CDN edge node or a remote host, is further away in terms of hop count. A packet with TTL 5 might pass through the ISP’s inspection equipment (which decrements the TTL but lets the packet through if the SNI looks acceptable), then expire a few hops later and generate an ICMP Time Exceeded back to the sender. The server never sees the fake ClientHello at all.

The real ClientHello follows immediately after, with the correct TTL and the actual SNI. By the time it reaches the DPI box, the connection has already been classified as permitted based on the decoy. Some DPI systems perform stateful tracking and will not re-examine the SNI on subsequent packets once a flow has been allowed. Others are stateless and would inspect the second ClientHello, but by then the fake one has already arrived first and set the classification.

This approach is not new in concept. Tools like GoodbyeDPI for Windows and zapret for Linux have used TTL-based decoy packets as one technique among several. SpoofDPI, written in Go, sends a fake ClientHello via a regular connection before proxying the real one. What gecit adds is the kernel integration: there is no proxy, no redirection, no iptables rule sending traffic to a local port. The hook is inside the kernel’s socket path itself.

MSS Clamping and Fragmentation

gecit also implements MSS clamping to force fragmentation of the ClientHello across multiple TCP segments. The Maximum Segment Size is advertised in the TCP SYN and SYN-ACK, and it controls the largest chunk of data that can be sent in a single segment. By clamping the MSS to a value smaller than the ClientHello, gecit forces the TLS record to be split.

Many DPI systems perform reassembly only up to a limited buffer size, or they inspect only the first segment of a flow. If the SNI field happens to land in the second segment, a non-reassembling inspector will never see it. This is a complementary technique to the fake ClientHello: even if the decoy packet approach fails, the SNI fragmentation may independently prevent inspection.

Fragmentation-based bypass has been documented in academic literature and is implemented by most serious DPI evasion tools. The Geneva project from the University of Maryland, which uses genetic algorithms to discover censorship evasion strategies, independently found fragmentation to be one of the most reliably effective techniques across multiple national filtering systems.

DNS-over-HTTPS Integration

Bypassing SNI inspection does not help if the DNS query for the destination is still visible. gecit includes a built-in DoH resolver. DNS queries sent to port 53 in plaintext are trivially observable by the same infrastructure that performs DPI, and many filtering systems maintain blocklists at the DNS level as a first line of defense, with SNI inspection as the second.

DoH, standardized in RFC 8484, tunnels DNS queries over HTTPS to a resolver that supports it, such as Cloudflare’s 1.1.1.1 or Google’s 8.8.8.8. The query is indistinguishable from regular HTTPS traffic to a passive observer. Integrating DoH into the bypass tool means the entire name resolution and connection establishment chain is covered without requiring the user to configure a system-wide DNS resolver separately.

Comparison with the Userspace Approaches

GoodbyeDPI operates at the Windows network driver level using the WinDivert library, which lets userspace programs intercept and modify packets in the Windows network stack. It can send decoy fragments, modify TTLs, and split packets, but it requires a service running in userspace that adds processing overhead for every packet.

zapret takes a more modular approach on Linux, using nfqueue or nflog via netfilter to pass packets to a userspace process for modification. It supports a wide range of bypass strategies and is highly configurable, but the userspace round-trip for each packet adds measurable latency on high-throughput connections.

SpoofDPI acts as a local HTTP/HTTPS proxy. Applications must be configured to route traffic through it, either manually or via system proxy settings. It does not require kernel modifications, but it does require the proxy to be in the data path for all connections.

The eBPF approach in gecit avoids all of these indirections. The sock_ops program fires synchronously with connection establishment. The fake packet injection happens via a raw socket in a companion process, but the core detection mechanism has no per-packet overhead for established flows. Once the fake ClientHello has been sent, the real connection proceeds through the normal kernel network stack unmodified.

Limitations

The TTL assumption is the fundamental fragility of this approach. If the DPI system is further away than expected, or if the path to the server is unusually short (common with anycast CDN infrastructure like Cloudflare, which may terminate connections within a few hops of the user), the fake packet may reach the server before expiring. The server will see an unexpected ClientHello before the legitimate one, which will likely cause the handshake to fail or produce a confusing error. Calibrating the TTL requires knowing the network topology, and that topology varies by network and destination.

Stateful DPI systems that perform full flow reassembly will not be fooled by the decoy if they buffer and re-order packets before inspecting them. These systems effectively ignore TTL and inspect the semantically first ClientHello regardless of arrival order. They are less common than simpler inline inspection devices, but they exist.

The correct long-term answer to SNI-based filtering is Encrypted Client Hello, currently in the final stages of IETF standardization. ECH encrypts the inner ClientHello using a public key published in the DNS HTTPS record, making the SNI opaque to any on-path observer. Cloudflare has had partial ECH support in production for several years. The catch is that ECH itself can be blocked by refusing to serve the HTTPS DNS record or by dropping connections with the ECH extension, and some filtering systems already do this. The gecit approach and ECH solve the same problem from different angles; neither is a complete answer on its own.

What gecit demonstrates clearly is that eBPF has become expressive enough to implement non-trivial network manipulation entirely within the kernel’s existing hook points, without loadable kernel modules and without userspace proxies. The sock_ops interface in particular is underused relative to its capabilities. Hooking connection establishment to trigger side-channel packet injection is a creative application of a mechanism that was originally designed for things like setting TCP socket options based on connection metadata. The same hook point that lets you tune TCP_NODELAY for latency-sensitive applications turns out to be equally useful for injecting decoy traffic at precisely the right moment.

Was this interesting?