Repository navigation
v1.8.1
The resolution release: 1.8.0 rebuilt how sdns serves, 1.8.1 rebuilds how it resolves. QNAME minimisation now follows RFC 9156's actual mechanism at the RFC's recommended values, upstream selection is rebuilt around honest evidence, the walk stops waiting on nameserver bookkeeping it doesn't need, and two served-TTL bugs found in production are fixed — one of which affects every deployment. Recommended for all deployments.
RFC 9156 QNAME minimisation, at the RFC's values (#578)
- The budget counts queries, not delegation depth. The old
qname_min_levelcapped the label depth walked minimised, and depth counts labels the resolver never had to expose: once a delegation was cached, the budget arrived already spent and the full name went out on the first query — the warmer the cache, the less a resolution minimised, and the labels still hidden at that point are the private ones. The budget now counts the minimised queries the request actually sends, which is what RFC 9156 §2.3 bounds. - Both RFC parameters, at the recommended values.
qname_max_minimize_count(MAX_MINIMISE_COUNT) andqname_minimize_one_label(MINIMISE_ONE_LAB) ship at 10 and 4. The old degenerate pair had no grouping phase — at the cap the whole name went out; the RFC keeps minimising and widens the step instead, and so does sdns now. Deprecatedqname_min_levelis still read when the new key is unset (with a startup warning), and an explicitqname_max_minimize_count = 0now really disables minimisation even with the old key still in the file — zero used to read as "unset." - Underscore labels ride along (
_dmarc,_25._tcp, …): a run of service labels is taken in one step — no zone cut hides behind_tcp, so probing it alone spends a query to hide nothing. Probes go out as QTYPE A per §2.1, restoring a 2020 behaviour a 2023 refactor silently lost: a client asking DS or NSEC no longer has that type put on a name whose zone doesn't answer for it, and one cached probe serves clients of every type. Internal DS/DNSKEY lookups stop minimising — they carry the resolver's own bookkeeping, and minimising them let deep names exhaust query budgets on walks that hid nothing. - A deliberate, documented departure from the RFC's own algorithm: a minimised denial that cannot be proven to cover the subtree — an unsigned zone, an NSEC3 opt-out span, a CD query — used to cost one query per hidden label. The walk now asks for the full name at once: one query, from the delegation already reached, since the client's answer depends only on the full name anyway. A validated, non-opt-out NXDOMAIN keeps its early RFC 8020 cut; an alias-bearing NXDOMAIN never cuts (it denies the end of the chain, not the asked name — RFC 2308 §2.1); and the decision sits on one RCODE gate ahead of any section-shape reading, so a REFUSED dressed with an SOA cannot masquerade as a walkable answer.
Choosing which server to ask (#576)
Upstream selection was rebuilt around a simple principle: rank on evidence, and don't let silence or refusal manufacture any.
- An unmeasured server is priced as a guess (300 ms), not as instant. A server nobody had reached carried zero accumulated RTT — the ranking read that as the fastest option and sent one of every miss's two parallel queries to the one server whose speed nobody had established.
- Only an answer is scored by the clock. A refused connection returns in microseconds — faster than any authority on earth — and a stale glue record pointing at a host that is up but not serving DNS could take the head of a delegation and never lose it. Transport errors, silence, and non-answering rcodes are priced at the timeout, which is what not answering is worth.
- Evidence ages per server instead of being amnesiated. The old scheme cleared every server's statistics every thousandth sort, putting the whole set back into the state it ranked first. A measurement now drifts halfway back toward a guess after five minutes unrefreshed, and each new sample blends half-and-half — one bad sample moves the ranking at once, where the old all-time average kept feeding a degrading authority for dozens of queries.
- Ties rotate and settled delegations still explore. The second racing slot rotated onto the same address forever by sort stability; it now rotates among tied candidates, and a settled delegation occasionally spends the slot on its stalest address — at a rate proportional to how much of the delegation is out of date, so a freshly met zone explores on most lookups and a settled one almost never.
- An exploration probe outlives the winner. The leader's answer used to cancel the second query before it could report, so an address could only ever be measured by winning outright — and addresses genuinely slower than the leader stayed unmeasured for good. Probes now finish on a detached context (pooled, capped, one exchange, no retries or fallbacks), so every candidate eventually gets measured at its true latency.
- Operators can see it: a server whose last exchange went unanswered reports FAILING in the delegation health line — priced at a timeout for the ranking, named for the human.
- The ranking pass allocates nothing, and is measurably faster than the sort it replaced at every delegation size.
The walk stops waiting (#578)
- One address is enough to take the next step. The walk used to resolve every glue-less nameserver of a delegation inline — up to a dozen full recursions, sequentially — before moving. It now resolves synchronously only until the delegation holds two distinct endpoints (counted on the deduped address list, so two names glued to one address don't fake a fallback), then queues the rest of the roster onto fixed process-wide worker pools, one lane per address family. Jobs are self-contained — they retain nothing of the originating request, so a backlog cannot anchor request state through an outage — and a full lane sheds. The IPv6 side moves onto the same lane, retiring the goroutine-per-referral scheme.
- Glue caches honour record TTLs. Cached nameserver addresses used to live until eviction pressure; a renumbered nameserver's old address could be handed out indefinitely. Entries carry the smallest TTL of the records they came from (floored at 30 s, capped at 6 h), a read past the horizon deletes exactly what it saw — and a cache hit no longer renews the horizon, so a frequently read address can't outlive its records through the floor.
Served-TTL correctness (#579, #580)
- Answers serve their own TTL, not the oldest key consulted on the walk. Internal DS/DNSKEY cache consults folded each consulted entry's remaining lifetime into the request, and the client answer inherited it as its serving bound — a fresh 3600-second answer could be served with 60 seconds, varying by name and time of day, depending on which infrastructure key the validation happened to read. Positive consults no longer bind the request; validated denials still do (a delegation held insecure by a cached DS denial must not outlive that proof).
- A provisional delegation entry can no longer displace the real lease. The short-lived stub written before a nameserver address lookup could — via the new enrichment queue — overwrite an hours-long lease with its one-minute cap, collapsing served TTLs under whole delegations. The store is atomic insert-if-absent-or-expired now; a live lease always wins the race.
- The delegation lease caps the first response too. Cache entries were always bounded by the parent-granted delegation lease (the ghost-domain protection, GHSA-mqfw-f48p-2vc8) — but the first response, served straight from resolution, advertised the records' full TTL, and a downstream cache could keep a withdrawn delegation's answer long past the lease sdns itself enforced. Every response now advertises min(record TTL, lease remaining): the first answer and every hit make the same promise.
DNSSEC validation
- Empty-non-terminal NODATA proofs validate. A signed zone answers NODATA for a name with descendants but no RRsets using the covering NSEC alone, whose NextDomain lies strictly below the query name. The validator knew the exact-owner and wildcard proofs and nothing in between, so this shape — constant in reverse trees and service names — was rejected as incomplete on live signed zones, each rejection a wasted validation and a spurious warning. The strict check is in place: a NextDomain equal to or beside the query name still requires the wildcard proof.
- ECDSA verification now parses the public key as a curve point rather than assembling it from coordinates — stricter in the direction a validator wants, rejecting off-curve points at the parse.
Cold-cache resolution, measured (#574, #577)
BENCHMARKS.md measured the cached-answer serving ceiling and said plainly that it wasn't a prediction of miss-path behaviour. That half is now measured: 50,000 names against an empty cache, four resolvers alternating over three rounds on one host, dual-stack shipped defaults, file descriptors and timeouts equalised, in both minimisation modes. Medians: minimisation off (the engine comparison) — sdns 905 q/s, PowerDNS Recursor 799, Knot Resolver 534, Unbound 399; as shipped — meaningful for the first time, now that sdns runs the same 10/4 the others do — sdns 658, PowerDNS 636, Knot 436, Unbound 243. The document records the methodology corrections that mattered most (address-family pins, fd limits — each moved the answer by more than the gaps being measured) and why the unanswered column (SERVFAIL + lost together) is what makes the throughput column readable.
Housekeeping (#581, dependency bumps)
- The contrib config template is verified byte-identical to what the binary generates for a missing config; version marks move to 1.8.1.
- The last RFC 7816 reference (obsoleted by RFC 9156 in 2021) is gone from the docs.
Upgrading
Drop-in. Configs still on qname_min_level keep working with a deprecation warning; moving to qname_max_minimize_count = 10 / qname_minimize_one_label = 4 (the generated template's values) is recommended — the legacy key's mapping is a weaker privacy setting than the shipped default. Operators with TTL-sensitive automation downstream should note that served TTLs are now consistently bounded by the delegation lease from the first response onward.