WeSearch

Bypassing DPI with eBPF Sock_ops

Bora Tanrikulu· ·9 min read · 0 reactions · 0 comments · 0 views
Bypassing DPI with eBPF Sock_ops

eBPF sock_ops, fake TLS packets, and why every platform needs a different hack

Original article
Bora · Bora Tanrikulu
Read full at Bora →
Full article excerpt tap to expand

The problemI wanted to open a website. The TLS handshake never completed. A middlebox somewhere was reading the SNI field in my ClientHello and killing the connection. The other half: DNS poisoning. Even if you somehow get past the DPI, the resolver returns a fake IP pointing to a block page. So you need to solve both, the SNI inspection and the DNS.Most solutions involve a VPN or a proxy. I didn’t want either. A VPN routes all your traffic through a remote server, which is overkill for this problem. A proxy requires app configuration, and some apps ignore proxy settings entirely. I wanted something that works at the system level, transparently, with one command: sudo gecit run.The ideaThe core trick is simple. Before the real TLS ClientHello reaches the DPI, send a fake one.The fake ClientHello carries a different SNI (www.google.com) and has a low TTL. The TTL is set just high enough to reach the DPI middlebox but low enough to expire before reaching the actual server. The DPI processes the fake, records “google.com”, and lets the connection through. The server never sees the fake because it expired in transit.Then the real ClientHello passes through. The DPI already made its decision based on the fake. It’s desynchronized.App connects to target:443 | gecit intercepts the connection Linux: eBPF sock_ops fires (inside kernel, before app sends data) macOS: TUN device captures packet, gVisor netstack terminates TCP | Fake ClientHello with SNI "www.google.com" sent with low TTL | Fake reaches DPI -> DPI records "google.com" -> allows connection Fake expires before server (low TTL) -> server never sees it | Real ClientHello passes through -> DPI already desynchronized On top of that, the eBPF program clamps the TCP MSS to a small value (88 bytes). This forces the kernel to fragment the real ClientHello into tiny segments. Some DPI systems only inspect the first TCP segment, so if the SNI spans multiple segments, they can’t read it.For DNS, gecit runs a local DoH (DNS-over-HTTPS) server on 127.0.0.1:53 and redirects system DNS to it. DNS queries go through encrypted HTTPS to Cloudflare, Google, or whichever upstream you choose. The plaintext DNS poisoning doesn’t work anymore.Linux: eBPF sock_opsThis is the interesting part.I wanted the fake packet to be sent before the application sends any data. Not after, not concurrently. Before. If the app’s real ClientHello reaches the DPI before the fake, the game is over.eBPF sock_ops gives you exactly this. You attach a BPF program to a cgroup, and the kernel calls it at specific points in the TCP lifecycle. The one I care about is BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB, which fires when an outgoing TCP connection completes the three-way handshake. At that moment, the connection is established but the application hasn’t sent any data yet. Perfect timing.Here’s the BPF program:SEC("sockops") int gecit_sockops(struct bpf_sock_ops *skops) { __u32 key = 0; struct gecit_config_t *cfg = bpf_map_lookup_elem(&gecit_config, &key); if (!cfg || !cfg->enabled) return 1; switch (skops->op) { case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: return handle_established(skops, cfg); case BPF_SOCK_OPS_HDR_OPT_LEN_CB: return handle_hdr_opt_len(skops); case BPF_SOCK_OPS_WRITE_HDR_OPT_CB: return handle_write_hdr_opt(skops, cfg); } return 1; } When a new connection to port 443 is established, handle_established does two things:static __always_inline int handle_established(struct bpf_sock_ops *skops, struct gecit_config_t *cfg) {…

This excerpt is published under fair use for community discussion. Read the full article at Bora.

Anonymous · no account needed
Share 𝕏 Facebook Reddit LinkedIn Email

Discussion

0 comments

More from Bora