Six stages turn one into the other. Exactly one of them is allowed to be clever.
Capture a device's traffic and the operating system hands you a firehose of raw IP packets — headers, sequence numbers, retransmissions, fragments, the lot. Hand those to a proxy transport and it has no idea what to do with them. A transport wants two things: where is this going, and what are the bytes.
Nothing bridges that gap for free. Something has to speak enough TCP/IP to terminate a connection locally — to watch a SYN go by, decide that a connection is being opened, complete the handshake on the application's behalf, reassemble the segments into an ordered byte stream, and remember where the application actually meant to send it. Only then is there a conversation to proxy.
That machinery is the least glamorous part of a circumvention tool and the part most likely to decide whether it is usable. Everything on the narrative page — the gambit, the genome, the discovery loop — happens in the last few inches of this pipeline. This chapter is the pipeline.
The whole client is one path. Each stage changes what the data is, and hands the next stage something narrower.
The device. A virtual interface the OS routes traffic into — utun on macOS, WinTun on Windows, a tun fd from VpnService on Android, a packet-flow object on iOS. Read gives you a packet; write puts one back into the OS as though it arrived from the network.
This is also the privilege boundary: creating one and editing the routing table needs root, CAP_NET_ADMIN, or Administrator.
The hard part. A TCP/IP implementation that terminates connections locally instead of forwarding them: completes handshakes, tracks windows, reassembles streams, and surfaces each connection as something you can read and write. Two very different implementations do this — §03.
The unit of work. A source, an original destination — the address the application actually dialed, which the netstack has to preserve because the packets were redirected — and a byte stream. Everything downstream is written against this and knows nothing about packets.
When DNS is being handled locally, the destination may be a synthetic address that maps back to a name. Carrying the name rather than an IP matters: it lets the exit resolve, and it's what the routing rules match on.
Per flow, one of four answers — proxy it, dial it directly, dial it directly with circumvention, or drop it. Matched on domain, IP, geography, port, and process. §04.
The pluggable slot. One trait — dial a target, return a stream — behind which sit a plain tunnel, AnyTLS, Shadowsocks, Hysteria2, a DNS tunnel, a domain-fronted meek path, and sandboxed WebAssembly modules. §05.
Where the book plays. The first few hundred bytes the censor will read: which extensions in what order, how the hello is cut into records, where the bytes split across segments, and how they're timed.
This is the only stage that is allowed to be clever. The five above it are deliberately boring — a bug there looks exactly like a censor, and you will misdiagnose it for a week.
The netstack stage has two implementations, and the choice between them is more interesting than it looks.
A TCP/IP stack compiled into the binary. Reads packets off the tun, runs the state machine itself, hands up streams. Works identically everywhere, including where there is no kernel tun at all.
Rewrite each packet's destination to a local listener and put it back. The OS's own TCP stack — decades of congestion control, zero-copy, hardware offload — does the termination. You only move headers.
The kernel stack should win, and on paper it does: no userspace reassembly, mature congestion control, less CPU per byte. The userspace stack has a specific pathology it fixes — under several concurrent downloads its throughput collapses, because a single task is doing all the dispatch. On a small two-core server that collapse takes it from ~1.5 Gb/s to 0.13 Gb/s. The kernel stack eliminates it outright.
Then we tried to reproduce the collapse on a laptop, and couldn't.
The pathology is CPU-bound, so its severity scales inversely with how fast the processor is. On Apple silicon the userspace stack held 0.268 Gb/s across four concurrent streams — twice the floor measured on the small server, at the fastest link we could put in front of it. The regime that justifies the kernel stack may simply not exist on a modern desktop.
Which inverts the usual instinct. The case for borrowing the kernel's TCP is strongest where CPUs are weakest — phones, cheap hardware, battery-sensitive devices — and weakest on exactly the developer laptops where this kind of work normally gets benchmarked and declared a win.
Routing every packet through a proxy is slow, expensive, and often counterproductive — it makes local services unreachable and pushes traffic through a distant exit that had no reason to go there. So each flow gets judged on its own.
Through the pool, to an exit. The default, and what everything else is measured against.
Dial it here, on a protected socket that skips the tunnel. Plain connection, no tricks — for traffic that was never blocked.
Dial it here, and still get through: un-poisoned resolution plus a shaped opening handshake. No proxy, no exit hop, no bandwidth cost — the censor is defeated on the direct path itself.
Drop it. Ads, malware, phishing.
Proxyless is the interesting one, and it's deliberately not a variant of "direct." Direct means a plain connection with no cleverness; quietly redefining it would change the behaviour of every rule already written. Proxyless is its own verdict: the site is blocked, but blocked in a way that opening-shaping alone can beat, so no exit server needs to carry the bytes at all. It is the same book from the narrative page, played without a proxy behind it.
The catch is that the right shaping is network-specific — what defeats one carrier's middlebox is noise on another. So a proven strategy is remembered against a fingerprint of the network it was proven on, and re-derived when that changes. The fingerprint costs nothing to take: open a UDP socket toward a documentation-only address that will never be routed, and read back which local address the kernel picked. Nothing is transmitted; it is a route lookup with a socket API.
Everything below the router is one interface: dial this target, return a stream. That narrowness is what makes the repertoire possible — a new wire protocol is a new implementation of one trait, not a change to the client.
Behind it today: a plain tunnel, AnyTLS, Shadowsocks, Hysteria2, a DNS tunnel for networks where little else escapes, a domain-fronted meek path, and the proxyless direct path. They are dialed through a pool that races and scores them, so a protocol that stops working stops being chosen.
One slot is not a Rust implementation at all: a sandboxed WebAssembly module, so a new obfuscation can be written, signed, and delivered without shipping a new client — and by someone who isn't us. That contract is a chapter of its own.
→ Write a module — the four exports, the calling convention, the host crypto menu, and a worked example.
A shipping client is two processes. The privileged one owns the tunnel device and the routing table. The unprivileged one is the user interface — large, frequently updated, exposed to whatever the user clicks. You do not want the second running with the powers of the first.
The packet path never crosses a process boundary. Hand over the file descriptor once, not the packets.
So the split is drawn along the control plane, not the data plane. What crosses between processes is commands, status, and logs — a few messages a second. What never crosses is traffic. Shipping packets over a socket to another process would double-copy every one of them and spend the entire performance budget on ceremony. Whichever process owns the tunnel also runs the netstack and the transports; the core is written so it doesn't know or care which process that is.
Two consequences worth stating plainly. The privileged side authenticates its caller rather than trusting whoever connects to the socket — peer credentials, pipe permissions, or code-signing identity, depending on the platform. And proxy secrets live only on the privileged side: the UI can ask for status, and cannot ask for keys.
Every platform hands you packets differently and disagrees about who is allowed to ask. Only the first stage of the spine changes.
| Platform | Tunnel primitive | What's different |
|---|---|---|
Linux | kernel tun | Privilege via a service process; the descriptor can be passed to a less-privileged one. |
macOS | utun | A system extension owns the device. Traffic to a local address never reaches the routing table, which makes same-machine testing impossible — benchmarks need a second box. |
Windows | WinTun | Different socket-binding semantics; the kernel-stack path needs its own implementation rather than a port. |
Android | VpnService → tun fd | It really is a Linux tun, so the kernel stack applies. Upstream sockets must be explicitly protected or they route back into the tunnel. |
iOS | packet-flow object | Not a kernel tun at all. Userspace stack only, permanently — and a hard memory ceiling that sizes every buffer in the system. |
A circumvention client is downloaded by people on metered connections, over links that are actively hostile, sometimes passed between phones by hand. Size is a feature. The target is under three megabytes stripped, and it is enforced rather than hoped for: aggressive optimization for size, fat link-time optimization, one codegen unit, symbols stripped.
That budget is also an architectural constraint, and it does real work. It rules out pulling in a second TLS implementation, or a heavyweight HTTP client, or the kind of transitive dependency tree that arrives with a C library attached. When a design decision is close, the smaller one usually wins — and the discipline of justifying every dependency is most of what keeps the attack surface of a security tool honest.