From fb6d395f0db84ca80bbc5a24211476a015e8f21b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 3 Aug 2026 07:46:19 +0000 Subject: [PATCH] fix(dns): resolve names under a numeric ending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The registry sells `.2600`. The resolver refused to resolve anything under it: alt.2600 28 forwarded -> NXDOMAIN from upstream seo.rank 28 moshpit(backfill) -> answered `moshpitCandidate` rejected every all-numeric ending outright, so a registered name was never even looked up. Buying an ending that cannot resolve is the worst possible failure: the registry takes the money and the name is dead, with nothing in any log to say why. The rule was protecting against address literals — `1.2.3.4` must never be read as `3.4` in this namespace, or whoever registered `.4` could intercept traffic meant for a machine. But the dotted-quad guard above it already covers that, and the blanket version caught a much larger class than it needed to. Narrowed to what is actually address-shaped: a name whose *every* label is numeric. `10.0.0.1` and `192.168` stay rejected; `alt.2600` resolves. The registry still decides whether a name exists — this is only the shape filter, and it should exclude what can never be a name rather than what merely looks unusual. Co-Authored-By: Claude Opus 5 --- lib/dns/policy.ts | 12 +++++++++++- tests/dns-policy.test.mjs | 24 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/dns/policy.ts b/lib/dns/policy.ts index 9e912db..2caadfa 100644 --- a/lib/dns/policy.ts +++ b/lib/dns/policy.ts @@ -62,7 +62,17 @@ export function moshpitCandidate(qname: string): { name: string; label: string; const tld = parts[parts.length - 1]; const label = parts[parts.length - 2]; if (NEVER_MOSHPIT.has(tld)) return null; - if (/^\d+$/.test(tld)) return null; + // A name whose every label is a number is address-shaped, and reading + // `10.0.0.1` or `192.168` as a name in this namespace would let anyone who + // registered `.1` intercept traffic meant for a machine. A *numeric ending* + // under a real label is a different thing: `.2600` is a registered ending, + // and `alt.2600` can only ever be a name. Rejecting the whole class here + // meant the registry sold endings the resolver then refused to resolve. + // + // The registry still decides whether the name exists. This is only the shape + // filter, and it should exclude what can never be a name rather than what + // merely looks unusual. + if (parts.every((part) => /^\d+$/.test(part))) return null; if (tld.length < 2) return null; if (!LABEL.test(tld) || !LABEL.test(label)) return null; // Underscore-prefixed service labels (`_dmarc`, `_acme-challenge`) are legal diff --git a/tests/dns-policy.test.mjs b/tests/dns-policy.test.mjs index 93f64bd..4739e9a 100644 --- a/tests/dns-policy.test.mjs +++ b/tests/dns-policy.test.mjs @@ -26,11 +26,33 @@ test("names the legacy root already owns are never sent to the registry", () => }); test("things that only look like names are not names", () => { - for (const name of ["", "localhost", "203.0.113.7", "eggs", "scrambled.e", "-bad.eggs", "scrambled.123"]) { + for (const name of ["", "localhost", "203.0.113.7", "eggs", "scrambled.e", "-bad.eggs"]) { assert.equal(moshpitCandidate(name), null, `${name} should not be a Moshpit candidate`); } }); +test("a numeric ending is a real ending", () => { + // `.2600` is registered, and the resolver used to refuse every name under + // it — the registry sold endings that could not resolve. A numeric ending + // under a real label can only ever be a name. + const candidate = moshpitCandidate("alt.2600"); + assert.equal(candidate?.name, "alt.2600"); + assert.equal(candidate?.tld, "2600"); + assert.equal(candidate?.label, "alt"); + assert.equal(moshpitCandidate("www.alt.2600")?.name, "alt.2600", "subdomains of it still work"); +}); + +test("an address is still never read as a name", () => { + // This is what the old blanket rule was protecting, and it has to keep + // holding: if `10.0.0.1` were a candidate, whoever registered `.1` could + // intercept traffic meant for a machine. + for (const address of ["10.0.0.1", "203.0.113.7", "192.168", "1.2.3.4", "8.8.8.8", "12.34"]) { + assert.equal(moshpitCandidate(address), null, `${address} is an address, not a name`); + } + // Colons are IPv6, rejected earlier and for the same reason. + assert.equal(moshpitCandidate("2604:a880:400:d1:0:4:c3fe:1"), null); +}); + test("clearnet mode forwards first and lets the registry backfill", () => { const plan = planQuery({ question: question("scrambled.eggs"), rd: true, mode: "clearnet" }); assert.equal(plan.action, "forward-first");