fix(dns): answer the address a name actually has - #269
Merged
Conversation
Most of the registry could not be resolved by our own resolver. Every
name pointed at a host — seo.rank, chovy.hacker, alt.2600, all of them —
came back as an authoritative NOERROR with no answers, which a client is
entitled to treat as final.
That is the worst shape a failure can take. `dig` said the name existed,
nothing could reach it, and no log anywhere reported an error. google.com
resolved the whole time, so the machine looked healthy.
Two independent causes, both on the address path:
- RECORD_TYPES covered CNAME, MX and TXT only, so a published A or AAAA
record was never consulted for an address question. alt.2600 publishes
an AAAA in the registry and still answered nothing.
- targetAddress() returns null for a hostname, and nothing picked up
after it. The comment said turning a host into an address would mean
the bridge doing clearnet DNS — but it already forwards clearnet
queries upstream, so that reasoning had gone stale.
addressAnswer() replaces the old answerPolicy-then-look-for-a-CNAME pair
with one plan: target address, published A/AAAA, published CNAME, then a
CNAME synthesised from a hostname target. The cheap question is still
asked first, so a name pointed at a bare IP costs exactly one registry
call and no record fetch — the fast path every page load takes.
A bare CNAME would not have been enough. This bridge sets RA=0, so a stub
handed a dangling CNAME has been told in the same breath that nobody will
chase it; systemd-resolved reports that as a name with no address.
buildChainResponse() emits the leaf alongside it, best-effort, so a slow
upstream costs the extra record and never the answer.
DoH gets the same treatment. Its own comment said a name that resolves
over the bridge and not over DoH is the failure that endpoint exists to
remove, and it carried the identical gap.
Verified against the live registry before any test was written:
seo.rank. 30 IN CNAME dev.profullstack.com.
dev.profullstack.com. 30 IN A 67.205.189.229
alt.2600. 300 IN AAAA 2604:a880:400:d1:0:4:c3fe:1
One existing test changed rather than added to: it had recorded "a live
name pointed at a hostname is NODATA" as correct. It was the bug.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan87 finding(s) HIGH/CRITICAL: 50 | MEDIUM: 37
…and 37 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
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.
Our own resolver could not resolve most of the namespace.
Every name pointed at a host —
seo.rank,chovy.hacker,alt.2600, all of them — answered like this:Authoritative, NOERROR, no answers. A client is entitled to treat that as final, and does.
Why it stayed hidden
google.comresolved the whole time, because forwarded queries never touch this path. Sosystemctl statuswas clean,resolvectlwas clean, DNS on the machine looked fine, and only the namespace the bridge exists to serve was dead.digeven confirmed the name existed. There was no error to find anywhere.Two causes, both on the address path
A published A/AAAA record was never read.
RECORD_TYPEScovered CNAME, MX and TXT; addresses came only fromtarget.alt.2600publishesAAAA 2604:a880:400:d1:0:4:c3fe:1in the registry and answered nothing.A hostname target produced nothing.
targetAddress()returns null for a host and nothing picked up after it. The comment there said turning a host into an address would mean the bridge doing clearnet DNS — but it already forwards clearnet queries upstream. That reasoning had gone stale, and it was load-bearing for the wrong conclusion.Most of the registry points at a host, so this was not an edge case.
The change
addressAnswer()replaces the oldanswerPolicy-then-look-for-a-CNAME pair with a single plan: target address → published A/AAAA → published CNAME → a CNAME synthesised from a hostname target → NODATA.The cheap question is still asked first. A name pointed at a bare IP costs one registry call and no record fetch — that is the fast path every page load takes, and it stays intact. Only a name that had nothing to say pays for the second round trip, which is the same bargain the old CNAME lookup already struck.
A bare CNAME would not have been enough. This bridge sets
RA=0, so a stub handed a dangling CNAME has been told in the same breath that nobody will chase it —systemd-resolvedreports that as a name with no address.buildChainResponse()emits the leaf alongside it. Best-effort by design: the CNAME is already correct on its own, so a slow or silent upstream costs the extra record and never the answer.targetHostname()is the other half oftargetAddress(). It refuses anything a CNAME cannot carry — ports, paths, bare IPs, single labels. A target naming:8080stays NODATA rather than quietly sending the client to port 80 of the right host, which would be a wrong answer that looks right.DoH gets the same treatment. Its own comment said a name that resolves over the bridge and not over DoH is the failure that endpoint exists to remove; it carried the identical gap.
Verified against the live registry, before any test was written
That
alt.2600AAAA is the record the bridge had been ignoring.Tests
15 new in
test/dns-address-answer.test.mjs, reading answers back off the wire — a reply of the right shape with the wrong bytes is exactly the failure being fixed. They cover both owners in a chain, a leaf of the wrong family being dropped rather than encoded as garbage, the fast path not fetching records, and NXDOMAIN still surviving all of it.One existing test changed rather than added to:
dns-nodata.test.mjshad recorded "a live name pointed at a hostname is NODATA" as correct behaviour. It was the bug, written down.Not fixed here
The published
@moshcoder/moshpit-dns@0.3.0has the same bug — confirmed directly:RECORD_TYPESisCNAME,MX,TXT,targetAddress('dev.profullstack.com')isnull, no hostname path. That is the standalone resolver users install, in a separate repo.dns-drift.test.mjspins the shared surface between the two, so this fix is deliberately kept out of that list until the package catches up.The bridge needs ~15s after start before it answers. Until the TLD list pages in,
isOurs()says no and every Moshpit name forwards upstream to an NXDOMAIN. Worth a separate look — it is a second, independent reason a freshly-enabled resolver looks broken.