fix(dns): stop the resolver putting a registry lookup in front of the whole web - #102
Merged
Merged
Conversation
… 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>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
ralyodio
marked this pull request as ready for review
August 3, 2026 10:20
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
clearnetAnsweredreturned 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:
svelte.devremix.runturso.technixos.org.ukWorse 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
Aquery 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.ukwas read as a Moshpit nameThe Moshpit name in a query is its last two labels, so
www.bbc.co.ukbecameco.uk. That was a registry lookup on every UK page load, and inmoshpitmode whoever registeredco.ukwould have intercepted every site under it. Public suffixes are now excluded.Unchanged
Moshpit resolution itself. Verified live against the patched build:
chovy.hacker→CNAME dev.profullstack.com,fuck.yeah→ gateway,mosh.whatever→ catch-all gateway, and clearweb resolves normally.190 tests pass;
tsc --noEmitclean.Not in this PR — deployment, needs a decision
The deployed copy at
~/moshpit-dnsbinds0.0.0.0, so it is reachable as an open public resolver;dns.logalready shows queries arriving from67.205.189.229. It should bind127.0.0.1if 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