Five kinds of DNS chapter · resolution

Every connection starts with a question.

Which makes DNS the first thing a censor breaks — and the last thing that still gets out.

01
the same three letters, five different problems

The five roles DNS plays

DNS shows up five separate times inside a circumvention client, and they are not variations on one problem — they have different threat models, different failure modes, and different code. Conflating any two of them is how you end up with a resolution loop that deadlocks the tunnel on its own first query.

role 01 · startup

Bootstrap

Before there is a tunnel, the client has to resolve its own control-plane names — where to fetch config, where the servers are. There is no proxy yet to hide behind, and this is the single most valuable lookup for a censor to poison.

role 02 · in-tunnel

The fake-IP server

Once the tunnel is up, every application's DNS lands inside it. The client answers those queries itself — with synthetic addresses — for a reason that has nothing to do with censorship. §03.

role 03 · per-flow

Flow resolution

Having answered with a synthetic address, the client must later turn the name back into a real one to actually dial it — and it cannot ask the operating system, because that question would come straight back to itself.

role 04 · search axis

A dimension of the search

In the proxyless path, how you resolve is one of the two axes being searched over — alongside how the handshake is shaped. §04.

role 05 · transport

DNS as the tunnel itself

On networks where almost nothing escapes, DNS often still does. So it becomes the carrier. §05.

02
the chicken-and-egg lookup

Bootstrap: resolving before a tunnel exists

A client that reaches its servers through a tunnel still has to find those servers before the tunnel exists. That first lookup happens naked, on whatever network the user is on, and everything downstream depends on it being truthful.

So it isn't one lookup. It's a race at two levels: an outer race across whole strategies — encrypted DNS, or asking through a proxy — and an inner race, within encrypted DNS, across a diverse pool of resolvers. Neither a blocked strategy nor a single blocked resolver can hold anything up, because the first validated answer wins and the rest are abandoned.

Validated is the load-bearing word. A race that accepts the first answer to arrive, rather than the first that validates, is worse than no race at all — the censor is usually the fastest responder on the network, because it doesn't have to do any actual work to make one up. Speed alone selects for the forgery.
03
the netstack surfaces an IP; the rules need a name

Fake IPs, and why the range matters

Here is a problem that isn't about censorship at all. Routing rules are written about names — this domain direct, that one blocked, this category proxied. But by the time traffic reaches the tunnel it is packets, and packets carry addresses. The name was resolved minutes ago and thrown away.

The fix is to intercept the resolution and never let the name go. The client answers each query with a synthetic address from a reserved range, remembers the mapping, and recovers the domain when the application connects to that address moments later.

app asks: example.com 28.1.4.9 app dials 28.1.4.9 → example.com rules · real resolve · dial

Which raises the only interesting question here: which synthetic range? The conventional pick is the benchmarking range reserved by RFC 2544, and it turns out to be actively harmful.

Browsers classify every reserved range as “local” — and then refuse to load a public page's images from a “local” address.

Chromium's Local Network Access rules block cross-origin subresource fetches from a public document down into local address space. Point fake IPs at a reserved range and you get a browser that mostly works, with images silently missing on some sites — a bug that looks like flaky censorship and is nothing of the kind.

So the range is a slice of a real, globally registered allocation that isn't announced on the public internet — space that exists on paper, is treated by browsers as ordinary public address space, and that no user will ever legitimately reach. That's an empirical claim about the routing table rather than a guarantee, and it's written down as one.

And the loop. Having answered a query with a fake address, the client must resolve the real one to dial it — and it must not ask the operating system, whose DNS now points back into the tunnel, at the client's own fake-IP server. That path is a deadlock, not a slow lookup. Flow resolution therefore runs on its own encrypted resolver over sockets explicitly excluded from the tunnel. The allocator helps too: it only ever hands out addresses from its own ranges, so a recovered flow's real dial can never re-enter the map.

The two per-flow seams differ by intent. A direct flow's domain is unblocked — that is why it's direct — so poisoning isn't the worry and the local resolver is used alone, because racing it against distant ones would throw away the whole point of getting a nearby answer. A proxied flow that has to be resolved client-side is the poisoning-risk case, so there the configured resolver races alongside the resilient pool.

04
a forged answer cannot survive the handshake

Why a poisonable resolver is safe to try

In the proxyless path, reaching a blocked site directly is a search over two independent axes: how the address is learned — encrypted DNS over HTTPS or TLS, plaintext over TCP or UDP, or just the system resolver — and how the opening handshake is shaped. Every pairing is a candidate.

Plaintext and system resolvers are exactly the ones a censor can forge an answer for. Including them looks reckless. It isn't, because of what the test for success actually is.

The certificate is the oracle. Not “did DNS return something,” not “did TCP connect” — did a TLS handshake to that address complete against a verified chain for the hostname we asked for.

A censor can drop your packets and can answer a plaintext query with anything it likes. What it cannot do is produce a valid certificate for someone else's domain. So a poisoned answer doesn't need to be detected and excluded — it loses the race, because the strategy built on it fails its own verification. A resolver that would be unsafe to trust on its own is safe to try, when success is defined this strictly.

The search order follows from cost rather than elegance: every resolver against no shaping at all first, and only once plain dials are exhausted does it start paying for shaping. Most of the time the answer is that the DNS was the only thing broken.

One caveat worth stating. This works because the client is only ever verifying a certificate it did not issue, on a connection it does not terminate. A tool that terminated TLS here — to inspect or rewrite it — would destroy the very property that makes the oracle sound.
05
the protocol a network cannot afford to block

DNS as a transport of last resort

Captive portals, hotel networks, aggressively filtered mobile carriers: places where almost every port is closed and every protocol is inspected — and DNS still resolves, because a network that breaks DNS breaks itself. That makes it a carrier of last resort.

It is a genuinely bad one. The payload has to be smuggled through query names, which are length-limited and must survive base32 encoding, so useful bytes per packet are pitiful. Resolvers cache, reorder, retry, and truncate. There is no connection to rely on, so reliability has to be rebuilt from scratch — sequencing, acknowledgements, retransmission timers, windows, all of it, on top of a request/response protocol that was never meant to carry a stream.

Which is why the protocol core is written as a library that does no I/O at all — no sockets, no async runtime. It transforms bytes and drives state machines; the caller owns the sockets and the timers. That constraint isn't tidiness. A reliability state machine with the network wired into it can only be tested against a network, and the failures being defended against here — a resolver that returns the same cached answer twice, a middlebox that truncates at exactly the wrong byte — are precisely the ones you cannot summon on demand. Pulled apart, the same core is exercised exhaustively by unit and fuzz tests and reused verbatim by both ends.

It is deliberately not compatible with anything. Wire compatibility with an existing DNS tunnel would mean inheriting a fingerprint that censors have already been trained on. The design borrows architecture from prior work and shares no bytes with it.