Skip to content

fix(dns): stop the resolver putting a registry lookup in front of the whole web - #102

Merged
ralyodio merged 1 commit into
masterfrom
fix/dns-clearnet-passthrough
Aug 3, 2026
Merged

fix(dns): stop the resolver putting a registry lookup in front of the whole web#102
ralyodio merged 1 commit into
masterfrom
fix/dns-clearnet-passthrough

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Pointing a machine's DNS at the bridge (DNS=127.0.0.1:5354, Domains=~.) made ordinary browsing slow and broke features on sites that have nothing to do with Moshpit. Three causes, all in how a clearnet answer was read.

1. NODATA was read as "clearnet has no name here" — for every query type

clearnetAnswered returned false for NOERROR-with-no-records regardless of what was asked. A browser asks A, AAAA and HTTPS (RFC 9460) for every hostname it touches, and most real domains answer the last two with nothing. So two queries in three were read as clearnet coming up empty, and paid for a registry round trip plus a root probe before the browser got its answer.

Measured against the live bridge, cold:

host type before after
svelte.dev AAAA 119 ms 12 ms
remix.run AAAA 131 ms 10 ms
turso.tech AAAA 132 ms 9 ms
nixos.org.uk AAAA 177 ms 20 ms

Worse than slow where the registry happened to hold the name: a legitimate domain's AAAA was answered with the gateway's address, so a dual-stack client went to the pit instead of the site it asked for — and its HTTPS record came back as an authoritative NODATA, which is how ECH and the HTTP/3 upgrade stop working.

NODATA is a positive statement about a name: the zone exists and holds no record of this type. It now counts as an answer. The one case the original note was about is kept — an A query with no address, e.g. a clearnet name parked behind an MX — which is rare enough to cost a lookup almost nowhere.

2. The rate limiter throttled the whole machine

It prices an attack that needs a forged source address, and loopback cannot be forged from off the box. But a machine resolving through the bridge sends every query from 127.0.0.1, so the entire machine shared a single client's 50 qps. Over-budget queries are dropped rather than refused — right for a spoofing victim, and the worst possible answer locally: the stub waits out its full timeout, retries, and the page finishes with subresources that never resolved.

A burst of 150 concurrent queries against the live bridge lost 20; the control resolver lost none. After this, none.

3. co.uk was read as a Moshpit name

The Moshpit name in a query is its last two labels, so www.bbc.co.uk became co.uk. That was a registry lookup on every UK page load, and in moshpit mode whoever registered co.uk would have intercepted every site under it. Public suffixes are now excluded.

Unchanged

Moshpit resolution itself. Verified live against the patched build: chovy.hackerCNAME dev.profullstack.com, fuck.yeah → gateway, mosh.whatever → catch-all gateway, and clearweb resolves normally.

190 tests pass; tsc --noEmit clean.

Not in this PR — deployment, needs a decision

The deployed copy at ~/moshpit-dns binds 0.0.0.0, so it is reachable as an open public resolver; dns.log already shows queries arriving from 67.205.189.229. It should bind 127.0.0.1 if it is only this machine's resolver. Flagging rather than changing, since it is an infra call and restarts the box's DNS.

🤖 Generated with Claude Code

… whole web

Pointing a machine's DNS at the bridge made ordinary browsing slow and broke
features on sites that have nothing to do with Moshpit. Three causes, all in
how a clearnet answer was read.

`clearnetAnswered` treated NOERROR-with-no-records as "clearnet came up empty",
for every query type. A browser asks A, AAAA and HTTPS (RFC 9460) for every
hostname it touches, and most real domains answer the last two with nothing —
so two queries in three were read as clearnet having no name there, and paid
for a registry round trip plus a root probe before the browser got its answer.
Measured against the live bridge: 105-274ms on names that forward in 8-12ms.

Worse than slow where the registry happened to hold the name: a legitimate
domain's AAAA was answered with the *gateway's* address, so a dual-stack client
went to the pit instead of the site it asked for, and its HTTPS record came back
as an authoritative NODATA, which is how ECH and the HTTP/3 upgrade stop
working. NODATA is a positive statement about a name — the zone exists and
holds no record of this type — so it now counts as an answer. The one case the
original note was about is kept: an A query with no address, e.g. a clearnet
name parked behind an MX. That one is rare enough to cost a lookup nowhere.

The rate limiter was the second cause. It prices an attack that needs a forged
source address, and loopback cannot be forged from off the box — but a machine
resolving through the bridge sends every query from one address, so the whole
machine shared a single client's 50 qps. Over-budget queries are dropped rather
than refused, which is right for a spoofing victim and the worst possible answer
locally: the stub waits out its timeout, retries, and the page finishes with
subresources that never resolved. A burst of 150 concurrent queries lost 20 of
them; it now loses none.

Third, the Moshpit name in a query is its last two labels, which reads
`www.bbc.co.uk` as `co.uk`. That was a registry lookup on every UK page load,
and in moshpit mode whoever registered `co.uk` would have intercepted every site
under it. Public suffixes are excluded.

Moshpit resolution is unchanged — registered names, aliases, parked names and
the catch-all all still answer as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio marked this pull request as ready for review August 3, 2026 10:20
@ralyodio
ralyodio merged commit 4fc86e6 into master Aug 3, 2026
2 checks passed
@ralyodio
ralyodio deleted the fix/dns-clearnet-passthrough branch August 3, 2026 10:27
ralyodio added a commit that referenced this pull request Aug 3, 2026
)

The rate limit prices reflection: a forged source address turns our answers
into someone else's inbound traffic, so a client's budget is the most we will
ever send a victim who never asked. At 50 qps / 100 burst it priced ordinary
browsing too.

A client here is an address, not a person, and behind one address is a laptop
with tabs open — or a household, or an office. A single page asks A, AAAA and
HTTPS for every hostname it touches, so a few tabs clear 100 queries in a burst
without trying. The excess is dropped rather than refused, which is right for a
spoofing victim and the worst possible answer for a browser: nothing comes back,
the stub waits out its full timeout, retries, and the page finishes with
subresources that never resolved.

Loopback stopped paying this in #102, which covers a machine resolving through
the bridge locally. It does not cover the case this deployment actually serves —
a laptop pointed at dns.moshcode.sh is a remote client and still paid.

200/600 is still a bound, drawn around a browser instead of inside it. It lets
through ~200 answers/sec to a spoofed victim, well under 100KB/s and no kind of
amplifier: ANY is already refused, which keeps the amplification factor small
enough for this to be the right trade. Both knobs stay configurable via
MOSHPIT_DNS_QPS / MOSHPIT_DNS_BURST.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant