github.com/semihalev/sdns
v1.8.0
#1578 most downloaded on Go modules
semihalev/sdns
What this package is like to depend on
Last release 6 days ago
18 Aug 2026
Release timing varies
gaps range from 8 days to 5 months
Rarely documented
notes for 9 of 55 stable releases
Nothing withdrawn
no release was ever pulled
8 years old
90 releases · first in 2018
16 releases in the last 12 months
see the full history below
Release timeline
90 releases · Oct 2018 to Aug 2026Releases
latest 60 of 90-
v1.8.018 Aug 2026Release notes
Open source →The serving-engine release: sdns now owns its transport engines end to end, answers cached queries as stored bytes without building a message, and — measured on identical load against PowerDNS Recursor, Unbound, and Knot Resolver — outserves all three on both UDP and TCP. Recommended for all deployments; high-QPS resolvers benefit most.
The serving engine (#559)
- Owned UDP, TCP, and DoT engines. The
miekg/dnsserver layer is retired. UDP runs on preallocated job slabs, fixed workers behind a ready ring, and batched kernel I/O (recvmmsg/sendmmsgon Linux); TCP and DoT run an owned accept loop with prefix-first framing and syscall-batched streams. Every reply leaves as raw bytes from job-owned storage. - Admission instead of backpressure by accident. A token/lease system bounds slab memory explicitly: what the engine may hold at full saturation is arithmetic checked by tests, not an emergent property of load. Overflow beyond the worker pool serves on bounded spill goroutines, so miss concurrency survives bursts without unbounded growth.
- Traffic-following memory. Idle slabs are trimmable; the opt-in
memory_trimsetting returns burst memory to the OS once the engines quiesce — and quiescence is a real barrier the tests assert, not a heuristic. - Strict wire ingress. An eligible query enters the middleware chain as a parsed view over its own packet bytes: no decoded message, no per-request context allocation, no copies. Ineligible packets take the classic decoded path unchanged; header-level rejections (FORMERR/NOTIMP/ignore) mirror the library byte for byte.
Answers served as stored bytes (#530–#546, #550–#551, #560)
- The cache retains wire form (#531) and serves eligible hits straight from it (#534): plain hits, DO-stripped bodies for clients without DNSSEC (#544), additional-section and RRSIG-question shapes, fully cache-contained CNAME chases, NXDOMAIN subtree cuts, aggressive NSEC/NSEC3 synthesis, and cached-failure answers (#560) all leave without a
dns.Msgbeing built. - Byte-identical packing from pooled storage (#550) and direct pack onto declared sdns-owned transports (#551): when the writer is our own UDP/TCP/DoT sink, even the decoded path packs once into the job buffer instead of allocating through the library.
- Correctness carried across: derived and composed answers stay bound to their source entries' cache lineage in both directions — a chase target with one second of life is no longer re-published for the TTL floor's five, and a nearly-expired alias no longer truncates its freshly resolved target's lifetime (#544, #545); denial-proof zones publish once per bundle with precomputed canonical order (#530, #537, #541, #546); and every gate that turns a hit away from byte serving is a named counter (#543) — the diagnostic that later found real production bugs.
A wire-transparent middleware chain (#552, #562, #563, #566)
Middlewares no longer materialize a request just to look at it: metrics reads the domain from the wire (#562), ratelimit runs its token and cookie checks on parsed offsets (#563), hostsfile and as112 answer from wire-keyed lookups (#563, #566), and the reflex/dnstap writer wrappers pass the byte path through instead of pushing hits back onto the message path (#552). On a production node, the share of client traffic served on the byte path went from zero to over 80%.
The allocation war (#547–#549, #553–#558, #564, #567–#570)
dns.UnpackDomainNameis retired repo-wide (#553, #555, #564): purpose-built wire walkers present, fold, and canonicalize names from packet bytes with stack buffers and map-index lookups — zero allocations, parity-tested against the library on every vector including the hostile ones.- DNSSEC without scratch buffers: DS digests and signatures verify without the library's fixed buffers (#547), key tags sum without decoding the key (#549), cached NSEC names canonicalize once at admission instead of per lookup (#537), and the aggressive-denial set and hit bodies stopped being rebuilt per query (#558).
- Copies that know why they exist (#567): the resolver's per-attempt request views share immutable records and privatize exactly what the wire packer mutates — replacing whole-message deep copies with requirement-analyzed shells (51.9 ns/3 allocs → 26.3 ns/1 alloc per attempt).
- Small knives: message IDs from the runtime's per-core ChaCha8 CSPRNG with zero allocations (#568); hand-parsed PTR names for both address families with
netipparity (#570); the RFC 9520 attempt guard keyed by hash instead of composed strings (#570); endpoint identity kept, not re-derived per lookup (#548, #533, #532); five question formatters folded into one wire-reading helper with hot debug lines guarded (#556); per-query context plumbing trimmed (#557).
DNSSEC validation: correctness and hardening (#547, #549, #553)
The verification rewrite was driven by allocation profiles, but holding the library's semantics up to the RFCs fixed real validation outcomes along the way:
- RRset canonical ordering now sorts by RDATA as RFC 4034 §6.3 requires — the old comparator wrongly included RDLENGTH, so a TXT set the library signs and accepts could be rejected here as a bad signature. The bug predates this release.
- Escaped label dots no longer confuse zone containment:
foo\.example.com.is a two-label name and no longer authenticates againstexample.com.'s keys. - DS digest type 5 is not SHA-512: IANA assigns 5 to GOST R 34.11-2012 (RFC 9558); it was being computed as SHA-512 and is now refused like every other unimplemented digest type (1, 2, and 4 are admitted).
- The EDE tells the truth: a signature that fails to decode now reports Bad Signature rather than Missing Key — the verdict was already right; the explanation the client saw was not.
- Hostile-input hardening: attacker-sized DNSKEYs are refused on encoded length before anything decodes them; a crafted two-octet RSAMD5 key can no longer reach the library's slice-underflow panic, and RSAMD5 key tags follow RFC 4034 Appendix B.1 (errata 193), so a crafted key cannot collide a trust anchor at tag 0. ECDSA signatures must be the RFC 6605 fixed width, and RSA moduli above 4096 bits are refused.
- NSEC coverage for escaped names compares the octets a name encodes, not its presentation-form escape text (RFC 4034 §6.3).
The final mile: inline serving, and the engine uncontended (#572)
Profile-driven, each step A/B-measured on a 32-core host before the next was attempted:
- 16-way sharded slab caches — the single idle-slab mutex (~540k lock ops/s) leaves the profile entirely.
- Fetch-add lease admission — the CAS retry spin becomes add-and-rollback.
- Batch-slot persistence — received-but-unserved slots stay armed across reader cycles instead of churning through release/re-take.
- Inline wire-hit serving. The reader runs the full middleware chain on every packet with an inline-only mark; the cache — the pipeline's declared inline barrier — answers from its wire ladder or hands off unwritten. Hits never cross the ring: no worker wake, and the receive batch leaves as one transmit batch, one
sendmmsgper cycle. Misses replay on a worker under a chain-level replay mark that keeps entry effects (rate-limit tokens, reflex scores, dnstap query frames) exactly once per query while response observers still fire. - TCP unchained. The per-connection query budget forced a server-side close every 2048 queries — under pipelining, a reconnect storm every half second. Fairness was already enforced per-frame and per-connection elsewhere, so the cap is gone; sessions serve until the client leaves, per RFC 7766.
Shipped only after independent multi-pass adversarial review — every finding (in-flight accounting, the replay contract for dual-keyed middlewares, dnstap wire transparency, per-entry rate-limit double charges) closed with the contract under test, including a Linux end-to-end whose quiescence assertion fails on the broken accounting.
Measured on the same corpus and harness across the arc: UDP cached answers 268k → 424k qps median / 444k best; TCP ~100k → 226k median / 273k best.
Benchmarks (#573)
Head-to-head on one 32-core host, identical load, DNSSEC validation on everywhere — full method, configurations, spread bands, and caveats in BENCHMARKS.md:
resolver UDP median TCP median sdns 1.8.0 424k 226k PowerDNS Recursor 5.4.1 371k 56k Unbound 1.24.2 343k 136k Knot Resolver 6.2.0 191k 142k sdns runs its full middleware chain per query in those numbers, and the untouched default configuration measures in the same band as the tuned one. These are cached-answer serving ceilings on loopback, not production predictions — the document says so plainly.
Standards and correctness
- RFC 6891 §7 truncation: a response that cannot fit shrinks to the minimal truncated form — header, question, and (only when the request carried one) OPT — instead of shipping partial records (#571).
- RFC 7766: TCP sessions persist; the server no longer closes pipelined connections mid-conversation (#572).
- RFC 7828 EDNS TCP keepalive, client-facing (#559): a TCP or DoT client that sends the edns-tcp-keepalive option gets the server's idle-timeout advertisement (8 s) in its responses — on the byte-serving path too — an explicit invitation to hold the connection instead of reconnecting per query. The option stays hop-by-hop as the RFC requires: an upstream's keepalive answer is stripped before a response leaves, and the option never appears over UDP, where it is forbidden outright.
- NSEC3 admission at the root zone and a precise wildcard acceptance boundary in the denial walkers (#555).
- Exact UDP overflow measurement before the truncation decision (#542).
- Reverse-zone handling is case-correct (#566): uppercase spellings (
10.IN-ADDR.ARPA.) no longer slip past the as112 empty zones into recursion, and a mixed-caseemptyzonesentry no longer becomes a dead key that passes validation yet never serves. - PTR name parsing is exact (#570): IPv6 PTR accepts precisely the RFC 3596 32-nibble shape; malformed spellings the old split-and-rejoin parser incidentally accepted now fall through as ordinary misses.
- Blocklist async persistence can no longer roll the on-disk file backwards on shutdown races (#529), and a source URL carrying an explicit port now loads on Windows instead of failing on a file name the OS refuses (#539).
Observability
dns_udp_inline_total{outcome}— inline-served vs handed-off queries; the live health signal of the new fast path.dns_udp_ingress_drops_total{reason}anddns_tcp_ingress_drops_total{reason}— every shed packet or connection has a named reason;dns_udp_ingress_overflow_totalcounts queries served outside the fixed pool, the signal that the pool is undersized for the traffic.dns_cache_wire_*decline counters (#543) — which gate turned a hit away from byte serving.dns_blocklist_entries(#538), and domain metrics now read from the wire with a bounded default (domainmetricslimit = 1000) (#562).dns_ingress_plan— the engine's computed resource plan (slabs, workers, sockets, connection caps) as a labeled gauge, and the same bounds printed in the listeners' startup lines, so what the admission arithmetic decided for this host is visible instead of implied.- Access logs and observers now report the response's true wire length — no more decoded-size inflation, stream length prefixes excluded (#534). dnstap no longer logs a response twice when a wire write falls back to the message path, and reflex scores a source on actual response bytes instead of estimates (#552).
Testing and dependencies
- The test suite resolves nothing on the live internet: a signed loopback namespace stands in for the world (#539), and the forwarder harness heals its own port races (#554).
testifyis retired; the standard library says it plainly (#561).- Allocation gates in CI pin the zero-allocation hit classes so they cannot regress silently.
Upgrade and compatibility notes
- Middleware and plugin API — source-breaking.
Chain.Requestis no longer a*dns.Msg: it is a*middleware.Request, a zero-copy view over the query's wire bytes that decodes only on demand. External middlewares and plugins that read the message directly must now either take wire-level facts from the Request's accessors (qname, qtype, EDNS state — no decode, keeps the query on the byte-serving path) or fetch the decoded message withctx, req := ch.Materialize(ctx). Two new chain marks also matter for handler semantics: on the UDP fast path the chain can run twice per query — an inline pass on the transport reader and, when the cache hands off, a replay pass markedch.Replay(). A handler with once-per-query entry effects (tokens, scores, counters, log lines) should skip them whench.Replay()is true, and a handler that must block — network calls, contended locks — should decline anch.InlineOnly()pass withch.MarkHandoff()and return, exactly as the cache does. For transport and module authors: custom transports implementmiddleware.Transport(thedns.ResponseWriter-based contract is gone), though*Serverremains adns.Handlerthrough retained delegating shims;WriteMsgno longer mutates the message it is handed (extended rcodes land in the wire alone, so sharing a message across goroutines is sound); response sizes surface through the optionalResponseSizercapability rather than a widened writer interface; and the DoQ server'sHandleris its own small interface now. - Config schema is v1.8.0. No keys were removed or renamed; older files keep working, with defaults filling what they omit. Regenerate to see the new sections. New keys, all with derived defaults that reach the measured numbers untouched:
ingressworkers(engine workers per listener),ingressqueue(ready-queue depth),ingresstcpconns(TCP/DoT connection cap — an explicit value above what the descriptor limit can serve is clamped with a warning, because connections past the kernel's grant are EMFILE at accept, not capacity), andmemorytrim(opt-in burst-memory return on quiescence). - The legacy
sdns.tomlfallback is removed. 1.7.x silently loaded a working-directorysdns.tomlwhensdns.confwas missing; 1.8.0 generates a fresh config at the requested path and logs a warning if a leftoversdns.tomlis found — migrate those settings manually, or the process runs on defaults. Relatedly,-cwith a custom path now generates a missing config at that path instead of failing to load. /metricsscrapes are uncompressed and thepromhttp_metric_handler_*self-instrumentation series are gone (#570). A dashboard charting the scrape handler's own stats loses those series; every DNS metric is unchanged, and none were removed or relabeled.- Truncated UDP responses are now minimal (RFC 6891 §7): header, question, and — only when the request carried one — OPT. Previous versions shipped partial record sets under TC=1; a client that consumed those instead of retrying over TCP will now see none. Standards-conforming resolvers and stub libraries are unaffected.
domainmetricslimitdefault dropped from 10000 to 1000 (#562). An explicit value in your config is honored unchanged; only deployments relying on the old default track fewer domains.0still means unlimited.- TCP sessions persist (#572): clients are no longer disconnected after a per-connection query budget. Connection-count limits and idle timeouts are unchanged.
- dnstap frames now carry the client's original wire bytes for wire-born queries instead of a re-packed message — semantically identical, but a byte-level consumer may notice different name compression than 1.7.x produced. A connected tap no longer disables the byte-serving path.
- The default bind now serves IPv6 (#559):
bind = ":53"listens on[::]:53alongside0.0.0.0:53, so a dual-stack host answers over IPv6 with the stock configuration. Review firewall rules written for v4 only; an explicit address inbindbehaves exactly as before. - No toolchain change for source builds: the Go version requirement is the same as 1.7.4.
- Owned UDP, TCP, and DoT engines. The
-
v1.7.409 Aug 2026Release notes
Open source →A resilience release: request-level work budgets, standards-based negative caching, and the fixes from a production outage post-mortem — plus a measurably faster hot path. Recommended for all deployments; open resolvers exposed to untrusted query load benefit most.
Recursion firewall (#527)
- Request-tree work budgets. Outbound transport attempts, resolver-internal child queries, and DNSSEC operations (signature verifications, DS digests, NSEC3 hashes, per-RRset/candidate fan-out, process-wide crypto concurrency) are accounted against one ledger spanning the complete request tree — retries, UDP→TCP fallbacks, and nested DS/DNSKEY/NS lookups included — so one hostile query cannot amplify into unbounded upstream traffic or crypto work.
- Three modes, shadow by default.
off/shadow/enforcevia[recursion_firewall] mode. Shadow records budget crossings in metrics plus a rate-limited log line naming the query, without changing any response; enforce terminates over-budget trees with SERVFAIL and an RFC 8914 Extended DNS Error. Calibrate from thednssec_work_per_requestanddns_recursion_fanout_ratiohistograms before enforcing. - RFC 9520 §3.1 retry ceiling. At most three attempts per (question, server, transport) tuple per resolution, always on.
Negative caching (#527)
- RFC 9520 resolution-failure caching. SERVFAILs and failed-authority state live in a dedicated bounded cache with exponential backoff (5s→5m) and zone-wide reachability entries; after backoff expiry, concurrent retries elect a single upstream probe instead of stampeding. Request-local failures (budget rejections, attempt limits, cancellation) are never admitted to shared state. Cached failures answer with EDE 13. Kill switch:
rfc9520. First field hour on a public node: ~16 answers/s served from failure state — retry load that no longer reaches upstreams. - RFC 8020 NXDOMAIN subtree cuts and RFC 8198 aggressive NSEC/NSEC3 synthesis. Locally validated denial proofs answer later negative queries without upstream traffic. Admission requires this resolver's own validation — the AD bit is never trusted — with exact-response provenance and fingerprint sealing; NSEC3 Opt-Out is excluded by design. The proof indexes carry their own per-zone and global entry/byte bounds so hostile denial churn cannot evict ordinary answers. Kill switch:
rfc8198. (~4.8k upstream lookups/hour avoided on the same node.)
Outage containment (#527)
Root-caused from a 2026-07-28 production incident (1.43M goroutines, 93% blocked behind cache segment locks during a partial upstream outage):
- Proportional cache eviction. Segment-clearing eviction (dropping 10–60% of entries in bulk while writers queued uncancellably) is replaced by self-paying eviction: an over-capacity insert evicts at most two entries under the lock it already holds. Worst single-insert loss drops from 50,003 entries to 1; the eviction-heavy insert benchmark improves 378→94 ns/op.
- Ghost-entry fix. Backward-shift deletion stopped at the first unmovable entry, stranding later cluster entries — present and counted but unfindable until an incidental grow. Rewritten to the canonical linear-probing delete (Knuth 6.4R), which also removes an O(cluster²) cost from every delete.
- In-flight ceilings and per-zone fairness. A hard cap on concurrent zone-level lookups plus a per-zone quota (the analog of BIND's fetches-per-zone): a popular destination going dark sheds itself with a scoped EDE while every other zone keeps resolving at full speed, through every phase of an incident. Detached IPv6 NS enrichment draws from a bounded pool instead of growing at arrival-rate × timeout. Sheds are observable via
dns_resolution_shed_total{scope}.
DNSSEC hardening (#527)
- Opt-Out denial proofs no longer set AD (RFC 5155 §9.2). The proof still validates; the response is correctly marked insecure instead.
- NSEC3 evaluation on a prepared ring. Each response's NSEC3 set is bound to the RRSIG signer zone and one parameter tuple, then evaluated as a sorted ring — removing the per-record hash multiplier an attacker-authored zone could exploit. Responses mixing NSEC3 parameter tuples are rejected fail-closed per RFC 5155 §7.2/§8.2.
- Sharper validation plumbing. Canonical wire-label handling in wildcard next-closer derivation (Unicode case folding could alias distinct octet names), deterministic deduplicated candidate ordering in DS/RRSIG verification, and RFC 5011 trust-anchor refresh brought under the same work accounting.
Performance (#527)
- End-to-end request deadline, kept off the hot path.
querytimeoutnow bounds the whole pipeline from ingress on every transport (UDP/TCP/DoT/DoH/DoQ). A lazily-armed deadline context keeps its cost invisible: versus a standardcontext.WithTimeoutat ingress, pipeline overhead drops 481→200 ns (7→1 allocs) and a positive cache hit 760→471 ns (15→9 allocs). - Cold-cache parity with PDNS Recursor 5.4.1 over 50k live queries: 641 vs 627 qps, 0 lost (PDNS: 14), response-code distribution identical to within 0.05% — with every new accounting and validation layer active.
Operator notes
querytimeoutis now end-to-end: cache dedup waits, DNS64 subqueries, and failover all count against it, andfallbackserversare not tried once a blackholed upstream has consumed the window.- The legacy negative answer cache is retired: the full
cachesizenow backs positive/NXDOMAIN entries, and SERVFAILs use the RFC 9520 failure cache (failure_cache_*settings under[recursion_firewall]). - The config schema is now v1.7.4 (
rfc8198,rfc9520,[recursion_firewall]). Older config files get the out-of-version notice — regenerate to see the new sections, or keep running: omitted settings mean shadow mode with both switches on. - The per-zone in-flight quota defaults to
maxconcurrentqueries/16(min 16); very-high-QPS cold-cache deployments should sizemaxconcurrentqueriesaccordingly.
Build
- Dependencies: quic-go 0.61.0 (#523), prometheus/client_golang 1.24.1 (#522), k8s.io 0.36.3 (#521), CI action bumps (#524, #526).
Full changelog: v1.7.3...v1.7.4
-
v1.7.319 Jul 2026Release notes
Open source →A security release fixing two reported advisories and two additional DNSSEC forgery vectors. Recommended for all deployments — validating resolvers should upgrade promptly.
Security advisories
- CD-bit cache poisoning (GHSA-g5mh-6738-qgh9, High). A single CD=1 query for a Bogus (validation-failing) name cached the unvalidated answer and served it as NOERROR to validating CD=0 clients — a DNSSEC downgrade any client of a shared resolver could trigger. Cache state is now strictly isolated across the CD bit: delegations are keyed on the client's CD bit only and the cross-CD fallback that leaked unvalidated CD=1 results into CD=0 answers is gone. The same change fixes a prefetch path that could refresh a cached answer without re-validation and silently drop AD. (#505)
- Ghost/phoenix domain attack (GHSA-mqfw-f48p-2vc8, Medium). A withdrawn child zone could be kept resolving indefinitely: the delegation cache floored parent referral TTLs to one hour and prefetch kept renewing the lease, so the parent's NXDOMAIN was never observed. Fixed in three layers: the parent-granted delegation TTL is honoured verbatim as one absolute deadline that validation and NS-lookup time count against (#513); nested delegations inherit the shallowest ancestor deadline on the path, so a deep 12h referral cannot outlive the 3s ancestor lease that granted it (#514); and every cached answer is bound to its delegation cut — effective lifetime is
min(answer TTL, cut deadline)enforced at read time, with the prefetch write-back made CAS so a stale refresh cannot resurrect an expired lease (#515). The full design is committed atdocs/security/ghost-phoenix-durable-design.md.
DNSSEC hardening
- Require wildcard-denial proof on positive answers (RFC 4035 §5.3.4). A zone's legitimately-signed wildcard RRSIG could be replayed over a concrete name that really exists and returned with AD=1 — RRSIG verification alone accepts it. Wildcard-expanded answers now additionally require an NSEC/NSEC3 proving the next closer name does not exist. (#512)
- Reject exact-owner NSEC3 matches as denial coverage (RFC 5155). miekg/dns
NSEC3.Cover()accepts a hash equal to the owner inside an ordinary interval, so an NSEC3 proving a name exists was accepted as proof it doesn't — enabling forged authenticated NXDOMAINs and bypassing the wildcard next-closer check above. Coverage is now strict: exact owner matches are excluded at every NSEC3 coverage call site. (#516)
Fixes
- False SERVFAILs for insecure names served by multi-zone authorities (e.g.
tether.edge.apple, #506): when a server authoritative for several zones of the chain answers with no referral crossed, the insecure-delegation proof demanded an exact-match NSEC3 that opt-out zones cannot have by definition — and the resulting SERVFAIL latched in the negative cache. Opt-out delegations are now proven via the RFC 5155 §8.6 covering path. (#507)
Build
- Toolchain bumped to go1.26.5; routine dependency and CI-action bumps. (#504, #508, #509, #510, #511)
Thanks
Thanks to @MaciejTe for reporting and providing a clean PoC for the CD-bit cache poisoning, and to @Bubb1eGvm for reporting the ghost-domain attack — both reported responsibly through GitHub private vulnerability reporting.
Full changelog: v1.7.2...v1.7.3
-
v1.7.225 Jun 2026Release notes
Open source →A DNSSEC correctness release. Recommended for all validating deployments.
DNSSEC
- Validate insecure delegations served without a referral. When one server is authoritative for both a signed parent and an unsigned child delegated from it (e.g.
tr.+ns.tr.,comcast.net+tx.comcast.net), it answers the child name authoritatively — no referral is crossed. v1.7.1 mis-attributed the answer to the signed parent and SERVFAIL'd the child's legitimately-unsigned records, breaking every signed zone whose nameservers live in an unsigned in-bailiwick subzone (e.g. the whole.trTLD intermittently lost validation). Such data is now accepted as insecure only when cryptographically proven — an authenticated DS, or an exact NSEC3/NSEC delegation proof (NS set, DS/SOA clear; opt-out rejected for the no-referral case). The DS authentication is fail-closed and downgrade-safe: the DS lookup is validated explicitly before any conclusion, and a forged unsigned DS (even with an unsupported algorithm) cannot downgrade a signed child. (#501) - Clear the AD bit for CD=1 clients (RFC 4035 §3.2.3 / RFC 6840 §5.7). A client that sets CD=1 has opted out of trusting the resolver's validation, so AD must never be asserted to it — including an upstream's bit passed through by the forwarder. (#501)
Internal
- Own DNS exchange library (
internal/dnsclient) replacing the vendored miekg client copy — UDP/TCP, DoT, and DoH — with the resolver and forwarder client paths trimmed accordingly. (#500)
Full changelog: v1.7.1...v1.7.2
- Validate insecure delegations served without a referral. When one server is authoritative for both a signed parent and an unsigned child delegated from it (e.g.
-
v1.7.124 Jun 2026Release notes
Open source →A security and hardening patch.
Security
- DNSSEC: validate RSA keys whose public exponent exceeds Go's
crypto/rsaceiling (2³¹-1) — restores DNSSEC validation for zones anchored on such keys, including the entire.lv(Latvia) TLD andmailbox.org. (#495) - Dependencies: pinned the build toolchain to go1.26.4, clearing 5 reachable standard-library CVEs (
govulncheckclean); the snap build was on the long-stale Go 1.23.4. - Cache: full-question verification on every cache hit — defends against xxhash key collisions being used to poison the cache.
- DNS Cookies: generate a full-entropy (128-bit) server secret (the previous value was space-padded).
Fixes
- Resolver: the circuit-breaker failure map could grow unbounded; idle entries are now evicted regardless of failure count.
- Server: malformed queries (QDCOUNT ≠ 1) now return FORMERR instead of triggering a panic/recover cycle.
- Blocklist: the whitelist now matches across the domain hierarchy on both the lookup and add paths (a parent whitelist exempts subdomains).
- Config:
--configpath handling fixed for Windows.
Build / CI / Docs
- CI: coverage upload no longer fails the build on a Codecov outage or missing token.
- Rewrote the package documentation (
doc.go) to match the current architecture. - Routine dependency and CI-action bumps.
Security policy
SECURITY.mdnow supports the 1.7.x line and directs vulnerability reports to GitHub's private reporting.Full changelog: v1.7.0...v1.7.1
- DNSSEC: validate RSA keys whose public exponent exceeds Go's
-
v1.7.031 May 2026Release notes
Open source →A feature release: EDNS Client Subnet (RFC 7871) lands in two stages — opt-in upstream forwarding plus an ECS-aware cache that closes #417 (cache pollution across client subnets). DNS-over-HTTPS forwarder upstreams (RFC 8484) close #473. The metrics surface is rebuilt around a new sharded-counter shim that's roughly 20× faster than direct Prometheus on the hot path, and observability gains 23 new counters across the policy, resolver, server, and DNSSEC paths. Two correctness fixes for blocklist subdomain matching and a resolver context-key collision panic are included.
What's Changed
Features
-
EDNS Client Subnet — Stage 1: opt-in upstream forwarding (RFC 7871, #483). When a client sends an
EDNS0_SUBNEToption, SDNS now forwards a clamped form upstream — previously every ECS option was stripped per RFC 7871 §11 privacy guidance. Forwarding stays off by default; operators opt in per server.- Source-prefix clamp to
forward_v4/forward_v6ceilings (defaults 24 / 56) so a privacy-leaky client (e.g. one sending its full /32) can't widen the leak beyond the operator's policy. - Client allow-list via
client_networks = [...](CIDRs); empty = all clients. - Fail-closed config: a malformed CIDR or out-of-range knob disables the entire policy (logged at startup) so a typo can't silently re-open forwarding.
- The clamped ECS option is stripped from the client-facing response so it never round-trips to the client; the wire form only carries it upstream.
Configuration:
```toml
[ecs]
enabled = false # default off
forward_v4 = 24
forward_v6 = 56
client_networks = [] # CIDRs; [] = all clients
cache_limit_ttl = "5m" # ceiling on scoped-cache entries (Stage 2)
min_scope_v4 = 24 # cache-cardinality floor (Stage 2)
min_scope_v6 = 56
``` - Source-prefix clamp to
-
EDNS Client Subnet — Stage 2: cache partitions by ECS scope (closes #417, #484). The cache keys entries by the authority's response SCOPE so a geo-tailored answer for one client subnet is never served to a client in a different subnet — the original "cache pollution" report.
- Each `(qname, qtype, qclass, CD)` tuple can hold one shared-key entry (the historical behaviour, used when `SCOPE = 0` or for non-ECS traffic) plus any number of scoped entries.
- Lookup does longest-prefix-match from the client's source prefix down to `/1`, falling back to the shared key on a scoped miss so SCOPE=0 / pre-1.7.0 entries still hit.
- Dedup is scope-aware too — two clients in different subnets get separate upstream queries instead of sharing one.
- Scoped entries are prefetch-ineligible — the prefetch worker has no client IP to derive scope from, so refreshing a scoped entry would lose its scope and store the wrong-audience answer.
- `cache_limit_ttl` caps the lifetime of scoped entries (geo answers go stale faster than the resolver's normal `MaxTTL`).
- `min_scope_v4` / `min_scope_v6` refuse to cache scopes narrower than this — load-bearing safety knob against per-client cardinality blowup.
- New metric: `dns_cache_ecs_lookups_total{outcome}` with `outcome ∈ hit_scoped / hit_shared / miss`. Non-ECS lookups stay on the existing `dns_cache_hits_total` / `dns_cache_misses_total`.
Stage 1 is the prerequisite for Stage 2; Stage 2 cannot be enabled without Stage 1's forwarding. The split was deliberate so operators could validate ECS reaches their authorities first, then opt into the cache change separately.
-
DNS-over-HTTPS forwarder upstreams (RFC 8484, closes #473, #486). `forwarderservers` now accepts `https://` URLs alongside the existing UDP and `tls://` (DoT) forms — both IP-literal and hostname URLs supported.
```toml
forwarderservers = [
"1.1.1.1:53", # plain UDP
"tls://1.1.1.1:853", # DoT
"https://1.1.1.1/dns-query", # DoH, IP literal
"https://cloudflare-dns.com/dns-query" # DoH, hostname (system-resolver bootstrap)
]
```- Hostnames are bootstrapped once at startup through `net.DefaultResolver` (the system resolver). Resolved IPs are pinned for the process lifetime — no per-query DNS dependency. Bootstrap failure is logged and the entry is skipped without aborting startup.
- HTTP/2 per-server client with pinned-IP rotation. Each DoH server gets its own `*http.Client` with `MaxIdleConnsPerHost: 4` for connection reuse, and a custom `DialContext` that rotates through pinned IPs and caps each TCP dial at `cfg.Timeout` — a blackholed address (typical mixed-A/AAAA-with-broken-v6 case) bypasses to the next IP in ≤ 2 s instead of consuming the request budget.
- TLS `ServerName` is set to the original URL hostname so cert SAN validation works correctly when dialing IPs.
- POST `application/dns-message` per RFC 8484. Response body bounded at 64 KiB (oversized bodies rejected, not silently truncated). Response `Content-Type` parsed with `mime.ParseMediaType` so case-insensitive types and parameters are accepted.
- Response TXID validated (echo of `req.Id` or `0` per RFC 8484 §4.1 cache normalization). Matches the UDP/DoT path's behaviour.
- Shared query budget across upstreams. `cfg.QueryTimeout` is now also a forwarder-level constraint — `ServeDNS` wraps the chain ctx with `WithTimeout(queryTimeout)` once so three slow upstreams can't take ~3 × per-call timeout. Applies to UDP, DoT, and DoH alike.
-
`internal/metric`: sharded-counter shim + 23 new metrics (#485). A thin layer over Prometheus that trades a small amount of staleness for a much faster hot path. Hot-path costs measured on 8-core M5 contention:
```
ns/op allocs notes
─────────────────────────────────────────────────────────────────────
prometheus.CounterVec.WithLabelValues 128.3 0 previous SDNS pattern
metric.Counter unlabeled 1.6 0 per-CPU shard via procPin
metric.CounterVec single-label hot 6.5 0 atomic.Pointer[map] lookup
metric.CounterVec multi-label hot 12.1 0 length-prefix key, alloc-free
```A background goroutine flushes shard sums to Prometheus on a 1 s tick (configurable). The lag is invisible at the typical 15 s scrape since `rate()` operates over windows that dwarf the flush interval.
All existing per-query counters migrated to the new package — wire-compatible (names, labels, label values unchanged) so existing dashboards keep working. Gauges, `GaugeFunc`, and dynamic-cardinality metrics (`dns_domain_queries_total`) stay on direct Prometheus by design.
23 new counters filling previously-silent decision points:
Policy / security:
- `dns_blocklist_hits_total`
- `dns_accesslist_denied_total`
- `dns_ratelimit_exceeded_total`
- `dns_forwarder_failures_total`
- `dns_forwarder_response_mismatch_total` (potential poisoning signal)
- `dns_failover_attempts_total` / `dns_failover_success_total`
- `dns_edns_errors_total{reason}`
- `dns_recovery_panics_total`
Resolver / DNSSEC:
- `dns_resolver_failures_total{reason}` (timeout, no_reachable_auth, max_depth, network_error, other)
- `dns_resolver_dnssec_failures_total{reason}` (bogus, sig_expired, sig_not_yet_valid, dnskey_missing, rrsig_missing, nsec_missing, unsupported_algorithm, other)
- `dns_circuit_breaker_trips_total` / `dns_circuit_breaker_resets_total`
- `dns_trust_anchor_lifecycle_total{transition}` (new_pending, became_valid, revoked, missing, reappeared, deleted)
Server / transport:
- `dns_listener_errors_total{proto}`
- `dns_doh_http_errors_total{code}`
Existing internal-counter exports:
- `dns_hostsfile_lookups_total` / `dns_hostsfile_hits_total`
- `dns_kubernetes_queries_total` / `_answered_total` / `_errors_total` / `_write_errors_total`
Bug Fixes
- `blocklist`: bare domains now block subdomains too (#478). Previously a `blocklists` entry of `example.com` blocked only the exact name; the new semantics match Pi-hole / AdGuard / dnsmasq — a bare domain blocks the apex AND every subdomain. Wildcard `*.example.com` continues to block subdomains only (not the apex), unchanged.
- `resolver`: context-key collision no longer panics `checkLoop` (da884dc). A `A` query (qtype 1) used to land on the same context key as `contextKeyDnameDepth` and `checkLoop` would read an int as a `[]string`, panicking. The per-qtype NSList base key now sits far enough above the fixed keys (`1 << 16`) that adding a 16-bit qtype can never collide.
Dependencies
Upgrade Notes
- ECS is opt-in. `enabled = false` by default; existing deployments behave identically to 1.6.7 until you flip it. Stage 2's cache changes are a no-op for non-ECS traffic — pre-1.7.0 cache entries continue to hit on the shared key.
- DoH forwarder is fully transparent to existing configs. Add `https://...` entries to `forwarderservers` to opt in; existing `udp` / `tls://` entries are unchanged.
- `metric` package callers MUST invoke `metric.Stop()` at shutdown to drain the final flush — wired automatically in `sdns.go`. Operators embedding the middleware packages directly should do the same.
- Metric wire format is unchanged. All migrated counters keep their original names, label keys, and label values. Existing Prometheus scrapers, Grafana dashboards, and alert rules continue to work without modification. The 23 new counters are additive.
- No on-disk format changes to `trust-anchor.db` / `trust-anchor-tombstones.db` / blocklist persistence.
- Config compatibility: `configver` stays at `1.7.0` (already bumped during Stage 1 to keep ECS additions covered by one version bump). Existing configs continue to parse; regenerate `sdns.conf` (or copy from `contrib/linux/sdns.conf`) to pick up the new `[ecs]` block and the DoH forwarder examples.
Full Changelog: v1.6.7...v1.7.0
-
-
v1.6.718 May 2026Release notes
Open source →A feature release: DNS64 (RFC 6147) lands as a first-class middleware, and the private-package layout gets tightened with an
internal/move. No security fixes in this one.What's Changed
Features
-
DNS64 middleware (RFC 6147, #472). New
middleware/dns64synthesises AAAA records from A records for IPv6-only clients reaching IPv4-only services. Sits betweenkubernetesandcache; activates when a client's AAAA query has no usable answer and a secondary A lookup succeeds, embedding each IPv4 into a configured Pref64::/n per RFC 6052 §2.2.RFC 6147 coverage at a glance:
- §5.1.2 / §5.1.3 RCODE handling — NOERROR-NODATA / non-NXDOMAIN errors trigger synthesis; NXDOMAIN passes through.
- §5.1.4 exclusions — default AAAA exclusion
::ffff:0:0/96; default A exclusion under WKP follows the IANA Special-Purpose registry (incl.192.88.99.0/24per RFC 7526). - §5.1.5 CNAME / DNAME chains preserved on both synthesis and the no-A-records path.
- §5.1.6 — when the AAAA query yields empty/error, the A response's RCODE, Authority section, and chain become the basis for the client reply.
- §5.1.7 TTL =
min(A TTL, AAAA negative-cache TTL); 600 s ceiling when no SOA is present. - §5.2 multiple Pref64 prefixes synthesise in parallel; the well-known prefix
64:ff9b::/96is the runtime default when none is configured. - §5.3.1 PTR translation via CNAME to
in-addr.arpawith optional best-effort chase. - §5.3.2 — only Answer-section AAAAs are synthesised; Authority/Additional pass through unmodified.
- §5.5 DNSSEC safety —
CD=1requests and SERVFAILs carrying DNSSEC-validation EDE codes (1, 2, 5–12, 27) bypass synthesis entirely; on synthesised replies, AD is cleared and EDE 4 (Forged Answer) is attached when the upstream hadAD=1. - RD=0 clients skip DNS64 entirely.
Configuration:
[dns64] enabled = true prefixes = [\"64:ff9b::/96\"] client_networks = [] # empty = all clients exclude_zones = [] exclude_aaaa_networks = [\"::ffff:0:0/96\"] exclude_a_networks = [...] # IANA Special-Purpose default
Metrics: `dns64_synthesised_total`, `dns64_ptr_translated_total`, `dns64_passthrough_total{reason}`, `dns64_a_lookup_failures_total{reason}`.
Closes the last open item on the README TODO list.
Internal Refactor (#479)
Five packages that were never intended as public API move under `internal/`:
Old path New path `cache` `internal/cache` `util` `internal/dnsutil` (renamed) `waitgroup` `internal/waitgroup` `mock` `internal/mock` `authority` `internal/authority` `util` is renamed to `dnsutil` at the same time — the old name was the lowest-information identifier in Go and the package's actual contents (EDE, TTL, RRset construction, response classification) are entirely DNS-message helpers. The new name is self-documenting.
Pure rename + import-path updates, no behavioural changes. The module path is unchanged so the public binary and middleware-extension API (`middleware.Constructor` / `*config.Config`) keep working. External plugin authors who imported any of the five packages directly will need to either pin to an older version, vendor, or remove the dependency.
API Documentation
`api/README.md` rewritten end-to-end (139 → 105 lines). Fixes several inaccuracies in the previous draft:
- `/api/v1/block/exists/:key` returns `{"exists": }`, not the previously documented `{"success": true}`.
- `/api/v1/block/set/:key` and `/api/v1/block/remove/:key` can return `success: false` on duplicate / missing — not always `true`.
- 404 response shape for `/api/v1/block/get/:key` and 401 response shape are now documented explicitly.
- Bulk batch 400 cases (malformed JSON, unknown fields, oversized body, empty keys) documented.
- `/debug/pprof/*` routes documented with their auth-bypass caveat.
- `ReadHeaderTimeout`, `MaxBytesReader`, graceful-shutdown timeout, and async blocklist persistence semantics documented.
Conversational style with an endpoint table up top for quick scan and curl examples interleaved with prose where they help.
Dependencies
- `k8s.io/client-go`, `k8s.io/apimachinery` → v0.36.1 (#476, #477)
- `github.com/quic-go/quic-go` → v0.59.1 (#475)
- `golang.org/x/sys` → v0.44.0 (#474)
- `github.com/fsnotify/fsnotify` → v1.10.1 (#468)
Upgrade Notes
-
DNS64 is opt-in. `enabled = false` by default; existing deployments behave identically to 1.6.6 until you flip it.
-
Config compatibility: `configver` bumps to `1.6.7`. Existing configs continue to parse — you'll see a one-line "Config file is out of version" log warning until you regenerate. `contrib/linux/sdns.conf` has been refreshed and is the easiest reference for the new `[dns64]` block.
-
Plugin authors who imported moved packages need to update import paths. Migration cheat-sheet:
github.com/semihalev/sdns/cache → github.com/semihalev/sdns/internal/cache github.com/semihalev/sdns/util → github.com/semihalev/sdns/internal/dnsutil # also rename util.X → dnsutil.X github.com/semihalev/sdns/waitgroup → github.com/semihalev/sdns/internal/waitgroup github.com/semihalev/sdns/mock → github.com/semihalev/sdns/internal/mock github.com/semihalev/sdns/authority → github.com/semihalev/sdns/internal/authoritySince these are now `internal/`, the Go compiler will refuse to compile any out-of-tree code that imports them. The intended path forward for plugin authors is to depend only on the public middleware-extension surface (`config`, `middleware`, `ctx`, `server`).
-
No on-disk format changes to `trust-anchor.db` / `trust-anchor-tombstones.db` / blocklist persistence.
Full Changelog: v1.6.6...v1.6.7
-
-
v1.6.607 May 2026Release notes
Open source →Security release. Closes a cache-poisoning vulnerability in both forwarder and resolver paths (issue #469). Operators on 1.6.5 should upgrade.
CVE / advisory: the issue was reported and disclosed publicly via the issue tracker. A GHSA entry will follow.
What's Changed
Security
-
Drop upstream responses with mismatched question section (#470, #471). Both the forwarder (
middleware/forwarder/forwarder.go) and the resolver wire layer (middleware/resolver/client.go:Conn.Exchange) used to accept an upstream reply as long as the DNS transaction ID matched. A malicious or misbehaving upstream could answer a query forattacker.example.with a message whose question section wasvictim.example.— and because the cache is keyed on the response's question, the unrelated answer was stored undervictim.example.and served from cache to later clients.Both paths now require the response to contain exactly one question whose
Name(case-insensitively, per DNS wire rules),Qtype, andQclassmatch the outstanding request. Mismatches drop the response and fall through to the next upstream, with the existing retry path covering transient cases. New regression tests pin the contract at both layers.Closes #469.
Features
-
Per-client static-answer middleware ("views", #360). New
[[views]]config block returns different DNS answers based on the originating client's source IP — split-horizon resolution where*.example.lan.can resolve to one address for LAN clients and a different one for VPN clients without disturbing recursion for everyone else. Each view declares azonelabel, a list ofnetworks(CIDR), and a list ofanswers(zone-file format, wildcards allowed).Match precedence follows RFC 4592: exact owners override a covering wildcard (§3.2); among wildcards, the longest matching suffix (closest encloser, §2.2.1) wins. Views are evaluated in declaration order; the first whose networks contain the client IP wins. A matched-but-no-answer view falls through (CoreDNS-style "fallthrough" semantics). Internal sub-pipelines skip views entirely. Position in the chain: between
hostsfileandblocklist, so a view-curated answer wins over a global blocklist rule for that name. See the example block incontrib/linux/sdns.confand the README for usage. -
Non-blocking blocklist persistence + bulk import API. Reported issue: blocklist mutations via the HTTP API caused DNS to temporarily stop responding while changes were applied. Root cause:
Set/Removeheldb.mu(mutually exclusive with theRLockthatServeDNStakes on every query) for the full duration of the synchronous disk write insave(). Large blocklists turned that into multi-millisecond stalls of every in-flight query.Fixes:
- Mutate maps under
b.mu, snapshot, releaseb.mu, then persist outside the lock.ServeDNSreaders no longer wait on disk I/O. - A new
saveMuserializes concurrent persists; theos.Renameof a temp file (CreateTemp+Sync+Rename) is the linearisation point, so the on-disk file always matches some in-memory state and never a half-written intermediate. - New
SetBatch/RemoveBatchperform one map lock + one disk write for an entire batch instead of one disk write per entry.
Two new HTTP endpoints accept
{"keys":[...]}JSON bodies (8 MiB cap, unknown fields rejected), returning{requested, added/removed, skipped/missing}:POST /api/v1/block/set/batchPOST /api/v1/block/remove/batch
A new contract test (
Test_BlockList_NoStallDuringSave) holdssaveMufrom a goroutine and asserts that a concurrentServeDNS-styleRLockreturns within 2s, so a future regression that re-introduces disk I/O inside the map lock fails loudly. - Mutate maps under
Kubernetes Middleware Refactor
Collapses the dual-mode (killer/boring) implementation into one sharded registry with per-headless-service incremental state. Slice events go through
ApplyEndpointSlice/RemoveEndpointSliceplus a worker-coalescedMaterialiseHeadless, so a one-pod change in a 1000-pod headless service costs O(slice size) for state work and O(delta) RR allocations.Correctness fixes that came along with the refactor:
- SERVFAIL for cluster-domain queries when not synced — forward queries no longer leak to public DNS during initial informer warmup; reverse queries still fall through.
- UID guard rejects late
EndpointSliceevents from a deleted Service via tombstone tracking andownerRef.UIDmatching, plus dirty-replay onAddServiceso the synthetic seed handover doesn't drop other slices. onEndpointSliceUpdateretracts the slice from the old service on a service-name relabel.cluster_domainis normalised (trailing dot, mixed case) at construction and atRegistry.SetClusterDomain.- Anonymous headless endpoints get distinct dashed-IP SRV targets (
10-0-0-1.svc...) instead of collapsing to one record. buildConfigdefers toclientcmd's default loading rules so multi-fileKUBECONFIGentries merge correctly.- Skip-if-equal guard in
applyEndpointSliceeliminates rebuilds forresourceVersion-only update events. - SRV port-number edits invalidate the cached
*dns.SRVpointer; SRV glue refresh allocates a new answerSet rather than mutating the published one in place. Runwaits on per-handlerHasSynced(not justinformer.HasSynced) and flushes pending rebuilds before publishingsynced=true.DeleteServiceorder is now tombstone → flush → DeleteService, preventing a worker rebuild from re-populating the registry after wipe.
config.KubernetesConfig.killer_modeis dropped from the live API; existing configs still parse (the field is retained but ignored), but new configs should omit it.Resolver / DNSSEC Refactor
Pure DNSSEC verify functions (RRSIG, DS, NSEC, NSEC3 denial-of-existence proofs) and the EDE-coded sentinel errors that go with them moved into a new
middleware/resolver/dnssecsubpackage. The generic DNS RR helpers (ExtractRRSet,FilterRRsToZone,NameInZone,DnameTarget) and theEDEErrortype moved intoutil/, where both resolver and dnssec can share them without a circular import. Resolver-side network errors keep their identities but now use*util.EDEErrorinstead of the resolver-localValidationErrortype.(*Resolver).lookup()was split in place: the per-server query goroutine moved to aqueryServermethod, the adaptive RTT-based timeout becameadaptiveServerTimeout, and the trailing fallback-picker becamepickFallbackResponse. Behaviour is unchanged;lookup()drops from ~250 lines to ~140 and the goroutine entry no longer captures state via closure. Net diff: −2016 / +265 inmiddleware/resolver/, ~1100 lines undermiddleware/resolver/dnssec/.Config
- Update B-root to current IANA addresses. ICANN/Verisign re-numbered B-root in late 2023 (IPv4
199.9.14.201→170.247.170.2; IPv62001:500:200::b→2801:1b8:10::b). The old addresses still answer for transitional reasons and priming even discovers the new ones at runtime, but the embedded default config, the Linux packaging config, the benchmark fixtures, and the fuzz seed corpus now match the canonicalnamed.rootlist.
Dependencies
github.com/semihalev/zlog/v2→ v2.0.8 (v2.0.7 broke the variadic-KV signature; v2.0.8 restores it, so this is a no-op upgrade).github.com/fsnotify/fsnotify→ v1.10.0.goreleaser/goreleaser-action→ v7.2.1.
Upgrade Notes
- Recommended for everyone on 1.6.5. The cache-poisoning fix is the headline reason for this release.
- Config compatibility:
configverbumps to1.6.6; existing configs continue to parse, you'll just see a one-line "Config file is out of version" log warning until you regenerate. The deprecatedkubernetes.killer_modekey is now ignored. - No on-disk format changes to
trust-anchor.db/trust-anchor-tombstones.db/ blocklist persistence — the new blocklist save path is a strict superset of the old format.
Full Changelog: v1.6.5...v1.6.6
-
-
v1.6.6-0.20260425193455-35432ae01f8625 Apr 2026 pre-releaseNothing published for this version
-
v1.6.525 Apr 2026Release notes
Open source →Patch release for 1.6.3. Major focus on RFC 5011 trust-anchor correctness, DNSSEC validation hardening, and listener lifecycle. Also closes a build-tag bug that prevented 1.6.4 from releasing on FreeBSD/NetBSD/OpenBSD/DragonFly.
Note: 1.6.4 was tagged but never published — the goreleaser pipeline failed on
freebsd_amd64because of thereuseport_*build constraint bug fixed in this release. 1.6.5 is the first available shipping point that includes the trust-anchor work below.What's Changed
Trust Anchors (RFC 5011)
A full pass over
middleware/resolver/auto_trust_anchor.goto bring the resolver into alignment with RFC 5011 §2 / §4 and to harden persistence against partial failures. Highlights:verifyFetchedKeysis now correct under KSK rollover. At-least-one-trusted-anchor RRSIG semantics with a narrow revoked-bootstrap carve-out (RFC 5011 §2.1: a revoked key may authenticate the RRset that contains it, but only for the purpose of validating its own revocation). Returns a split-mode flag so a revoked-only proof can tombstone the matching key but cannot seed AddPend or mark other anchors missing.- Revocation requires a self-signed RRSIG and key-material match, not just key-tag arithmetic. Defends against 16-bit tag collisions where an unrelated self-signed key could otherwise be admitted as a revocation of the real anchor.
- Tombstones moved out of
kskCurrentinto a material-keyed store with its own state file. Tag collisions with future legitimate KSKs can no longer suppress them. - Missing keys remain valid trust-point keys until the remove hold-down expires (§4.2). Missing→Valid restoration on KeyPres. AddPend reset on KeyRem (the hold-down aborts cleanly instead of drifting through Missing). Missing-aged-out simply deletes (§2.4.2 is bookkeeping); only RevBit revocations tombstone permanently.
- Configured-merge uses an immutable
cfg.RootKeyssnapshot taken at startup, honours tombstones by key material, filters seeded entries on load, and refuses to resurrect a stale admin-config anchor that the root has revoked. - Atomic gob writes (CreateTemp + fsync + rename + parent dirsync) with tombstones-first ordering. New revocations dual-write to a
StateRevokedmarker so a tombstone-write failure survives across retries; selective fail-closed only when an actual contraction would otherwise be lost on disk. - New
errTrustAnchorsUnavailablegatesanswer/authority/validateDelegationand the two delegation-cacheSetsites, so an empty trust set fails closed with SERVFAIL instead of slipping into the "unsigned delegation" branch. AutoTA's own DNSKEY query runs CD=true so it doesn't depend onr.rootKeys.
Bug Fixes
- Build reuseport file on all BSDs, not just darwin. The file was named
reuseport_darwin.go, which Go treats as an implicit GOOS=darwin build constraint. The explicit//go:build darwin || freebsd || netbsd || openbsd || dragonflywas ANDed against that, so freebsd/netbsd/openbsd/dragonfly all failed to link withundefined: defaultUDPWorkers / kernelLoadBalances / reusePortControl. Renamed toreuseport_bsds.goso the explicit tag governs. - Windows compatibility for AutoTA persistence. Split the post-rename directory fsync into platform-specific helpers (POSIX does it; Windows is a no-op since FlushFileBuffers on a directory handle requires GENERIC_WRITE which
os.Opendoesn't grant). Tombstones-file open errors now distinguish Windows sharing violations from real corruption — only decode failures fail closed. - DNSSEC validation hardening + DNAME correctness. SERVFAIL when a signed zone omits RRSIG. Multiple correctness fixes around DNAME synthesis and parallel lookup paths.
- Five correctness bugs in the parallel lookup path.
- Listener lifecycle: explicit fail-fast binds. Listener startup now fails immediately if a bind cannot be established, instead of silently degrading.
Performance
- Pool
net.Dialerand bypassDialContexton UDP upstream. Per-query allocation cut on the recursive hot path. - Pre-build hostsfile answer RRs at load time instead of constructing them per query.
Refactor / Naming
- Package
authcache→authority(split intoserver.go+cache.go). Type renames:AuthServer/AuthServers→Server/Servers;NSCache→Cache;NS→Delegation;DSRR→DSSet;Version→IPVersion. parentDSRR/parentdsrr→parentDS.accesslist.AccessList→accesslist.List;accesslog.AccessLog→accesslog.Log(config field names preserved).r.ncache→r.delegations;nameserversmap type →hostSet;nameserverInfo→delegationInfowithhostsfield.rootservers/rootkeys(smashed lowercase) →rootServers/rootKeys.ipv4cache/ipv6cache→glueV4/glueV6.- Internal sub-pipeline now flows through the
Queryerinterface;util.ExchangeInternalretired.
Dependencies
github.com/semihalev/zlog/v2→ v2.0.6.k8s.io/apimachinery→ 0.36.0.k8s.io/client-go→ 0.36.0.codecov/codecov-action→ v6.
Full Changelog: v1.6.3...v1.6.5
-
v1.6.425 Apr 2026Nothing published for this version
-
v1.6.4-0.20260423153249-9d6ed12c55ba23 Apr 2026 pre-releaseNothing published for this version
-
v1.6.320 Apr 2026Nothing published for this version
-
v1.6.3-0.20260419204212-728e86b0617d19 Apr 2026 pre-releaseNothing published for this version
-
v1.6.219 Apr 2026Nothing published for this version
-
v1.6.128 Nov 2025Nothing published for this version
-
v1.6.1-0.20250812075348-5c88ac2d1c6912 Aug 2025 pre-releaseNothing published for this version
-
v1.6.006 Jul 2025Nothing published for this version
-
v1.5.308 Jun 2025Nothing published for this version
-
v1.5.3-0.20250608081226-45a5e076d4fb08 Jun 2025 pre-releaseNothing published for this version
-
v1.5.3-0.20250607221635-e3b939fbc6bd07 Jun 2025 pre-releaseNothing published for this version
-
v1.5.207 Jun 2025Nothing published for this version
-
v1.5.107 Jun 2025Nothing published for this version
-
v1.5.1-0.20250607213458-034e31d68e0a07 Jun 2025 pre-releaseNothing published for this version
-
v1.5.1-0.20250607181944-07860e902b2d07 Jun 2025 pre-releaseNothing published for this version
-
v1.5.1-0.20250606105039-eca06a65326f06 Jun 2025 pre-releaseNothing published for this version
-
v1.5.004 Jun 2025Nothing published for this version
-
v1.4.1-0.20250217132115-86830209f55517 Feb 2025 pre-releaseNothing published for this version
-
v1.4.014 Feb 2025Nothing published for this version
-
v1.3.723 Jun 2024Nothing published for this version
-
v1.3.7-0.20240623093653-80408dd1623c23 Jun 2024 pre-releaseNothing published for this version
-
v1.3.7-0.20240520123848-e6ae8639c5ca20 May 2024 pre-releaseNothing published for this version
-
v1.3.7-0.20240403151222-05cba142620303 Apr 2024 pre-releaseNothing published for this version
-
v1.3.7-0.20240228160045-eaaed2a7ad9628 Feb 2024 pre-releaseNothing published for this version
-
v1.3.7-0.20240217115504-c5b879388cf517 Feb 2024 pre-releaseNothing published for this version
-
v1.3.602 Jan 2024Nothing published for this version
-
v1.3.6-0.20240102112642-88568c53554002 Jan 2024 pre-releaseNothing published for this version
-
v1.3.526 Aug 2023Nothing published for this version
-
v1.3.5-0.20230826114320-3c3d7798fbc826 Aug 2023 pre-releaseNothing published for this version
-
v1.3.5-0.20230811122206-00d9f6702dfe11 Aug 2023 pre-releaseNothing published for this version
-
v1.3.411 Aug 2023Nothing published for this version
-
v1.3.306 Aug 2023Nothing published for this version
-
v1.3.3-0.20230806085310-8888ef72292606 Aug 2023 pre-releaseNothing published for this version
-
v1.3.226 Jul 2023Nothing published for this version
-
v1.3.1-rc1.0.20230722151613-bdce4622e3c722 Jul 2023 pre-releaseNothing published for this version
-
v1.3.1-rc1.0.20230722142258-2f566be2a78322 Jul 2023 pre-releaseNothing published for this version
-
v1.3.1-rc1.0.20230706214937-df3234c9ab1406 Jul 2023 pre-releaseNothing published for this version
-
v1.3.1-rc1.0.20230706152244-4f79362bfb1506 Jul 2023 pre-releaseNothing published for this version
-
v1.3.1-rc105 Jul 2023 pre-releaseNothing published for this version
-
v1.3.1-0.20230705115858-04513089c0ce05 Jul 2023 pre-releaseNothing published for this version
-
v1.3.001 Jul 2023Nothing published for this version
-
v1.2.5-0.20230701201342-6b2e769ae5d501 Jul 2023 pre-releaseNothing published for this version
-
v1.2.430 Apr 2023Nothing published for this version
-
v1.2.330 Apr 2023Nothing published for this version
-
v1.2.230 Apr 2023Nothing published for this version
-
v1.2.104 Feb 2022Nothing published for this version
-
v1.2.004 Feb 2022Nothing published for this version
-
v1.1.9-0.20220204083142-bd53f679ac1304 Feb 2022 pre-releaseNothing published for this version
-
v1.1.9-0.20220101103133-049b20e9239d01 Jan 2022 pre-releaseNothing published for this version
-
v1.1.9-0.20211225142305-c06eef39b2be25 Dec 2021 pre-releaseNothing published for this version