Derive the NSEC next name for QNAMEs at the 255-octet ceiling - #360
Merged
Conversation
RFC 9824 3.1 builds the compact denial-of-existence NSEC Next Domain Name by prepending a label of one zero octet to the QNAME. That costs two octets on the wire, which a QNAME already at the 255-octet maximum cannot afford, so dns_domain:to_wire/1 raised name_too_long while the synthesized NSEC was being canonicalised for its RRSIG. erldns_pipeline caught the exception and carried on with the message untouched, so the response went out as a bare NXDOMAIN plus SOA with no NSEC, no RRSIG and no AD bit. A validating resolver that asked with DO=1 against a signed zone and got an unsigned negative answer treats it as bogus, so every such query became a SERVFAIL downstream, plus an error log carrying a full message dump and a telemetry event. Any name of 254 octets or more triggered it, on any signed zone. Names at the ceiling now fall back to the successor ladder of RFC 4471 3.1.2: append a zero octet inside the left-most label where the label and the name both have room, otherwise increment the right-most octet below 0xff and drop everything to its right, otherwise drop the label and start again one level up. Uppercase US-ASCII is skipped when incrementing, because canonical order compares names lowercased (RFC 4034 6.1) and 0x41-0x5a therefore never occur in a canonical name. Step 4 deviates from the RFC on purpose. It loops back to step 2, whose guard cannot hold once a 64-octet label has been dropped, so the RFC falls through to step 3 and yields a name past the true successor. An NSEC asserts nothing exists between its owner and its next name, and RFC 8198 resolvers cache that span and answer NXDOMAIN from it for names they never queried, so a span wider than the truth denies names that exist. Both PowerDNS and BIND ship that behaviour, BIND with synth-from-dnssec enabled by default, and neither recognises these fallback names as synthesized: PowerDNS matches only next names of the form \000.<owner>, BIND only type bitmaps of exactly RRSIG+NSEC. Applying step 2's action in the recursion keeps the span empty instead. Tested with a case per rung of the ladder, each asserting the exact successor plus the two invariants every rung owes, that the name encodes at all and that it sorts after its input, and an end-to-end case that reproduces the original crash through handle/7. Full ct suite at 331 passing, plus fmt, lint, xref and dialyzer.
Both exception logs in erldns_pipeline named the caught reason `error`.
Every other exception log in the library, and this module's own docs for
the corresponding telemetry event, call it `reason`, alongside `class`
and `stacktrace`.
That matters beyond tidiness: error reporters key off the shape of a
structured log to decide whether it describes an exception, and they
match on #{class, reason, stacktrace}. A report carrying `error` where
`reason` belongs matches nothing and is dropped without a trace, so
these two logs — the ones covering every exception a pipe can raise on
the query path — never reached the error reporter. The NSEC crash fixed
earlier in this branch is a worked example: it ran in production, logged
on every occurrence, and was never reported.
The telemetry metadata built two lines above each log already uses
kind/reason/stacktrace and was unaffected.
pipe_raises now asserts the shape of the report, not just that the
telemetry event fires, so a rename cannot silently cost this visibility
again. The handler is keyed by test case name, as the pipelines are,
because pipe_calls is a parallel group and a shared key lets a sibling's
end_per_testcase tear it down mid-case. Checked against the old key
first: the case fails with "No log report carrying an exception shape".
The async site takes the same rename. Its test drives a peer node, whose
logs a handler on the test node cannot see, so it is covered by the
rename alone.
DXTimer
approved these changes
Aug 3, 2026
DXTimer
reviewed
Aug 3, 2026
The ladder walked the QNAME's labels with no notion of where the zone began. Its ascent drops a label whose every octet is 0xff, and if the label directly below the apex is such a label, the next step grew the apex's own left-most label instead — naming a sibling of the zone. The Next Domain Name is a name in this zone (RFC 4034 4.1.1), so that record was malformed. Reproduced with an apex of 190 octets and a QNAME of <0xff x63>.<apex>: the successor came back as a sibling of the apex. 190 is the threshold, since the ascent needs one saturated label between the apex and the 254-octet floor where the ladder starts; for ordinary zone names no arrangement of saturated labels reaches the apex, which is why this went unnoticed. The denial itself was never wrong. The ascent only passes a maximal label, so nothing sorts between owner and next either way. What was wrong was naming something outside the zone. RFC 4034 4.1.1 also supplies the answer: the last NSEC of a zone puts the apex in that field. The ascent now stops there and returns the apex, which is what dnssec:gen_nsec's add_next_dname/3 does for the last record of a real chain. It stays truthful, too: the owner is the maximal representable name in the zone, so wrapping to the apex asserts nothing exists after it. The depth bound also covers a QNAME that is the apex itself, which needed an apex of 254 octets or more to reach and would otherwise have grown the apex the same way. Reported in review by DXTimer.
DXTimer
approved these changes
Aug 3, 2026
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.
Compact denial of existence builds the NSEC Next Domain Name by prepending a label of one zero octet to the QNAME (RFC 9824 §3.1). That costs two octets on the wire, which a QNAME already sitting at the 255-octet maximum cannot afford, so
dns_domain:to_wire/1raisedname_too_longwhile the synthesized NSEC was being canonicalised for its RRSIG.Observed in production: https://app.datadoghq.com/logs?query=service%3Aerldnsimple%20%40what%3Apipe_failed_with_exception&agg_m=count&agg_m_source=base&agg_t=count&clustering_pattern_field_path=message&cols=host%2Cservice%2C%40what%2C%40reason&event=AwAAAZ-tDrLB14qdvAAAABhBWi10RHNRcEFBRHNRTUtEdkVucFVnQUEAAAAkMDE5ZmFkMTMtNzEyZS00OTI3LWFmMzEtNTg0OTE4MmEwZTc4AAF6KQ&fromUser=true&messageDisplay=inline&refresh_mode=sliding&storage=hot&stream_sort=desc&viz=stream&from_ts=1785292156911&to_ts=1785306556911&live=true
erldns_pipelinecaught the exception and carried on with the message untouched, so the server did not fall over — but the response went out as a bare NXDOMAIN plus SOA, with no NSEC, no RRSIG and no AD bit. A validating resolver that asked with DO=1 against a signed zone and received an unsigned negative answer treats it as bogus, so every such query became a SERVFAIL for the end user.🔍 QA
N/A
📋 Deployment Pre/Post tasks
N/A
what=pipe_failed_with_exceptioncarryingerror=name_too_longfromerldns_dnssecin the logs after rollout.[erldns, pipeline, error]telemetry counter returns to its pre-incident baseline.delv, ordig +sigchase).