PackageTrack
Sign in Get early access

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 2026
2019 2020 2021 2022 2023 2024 2025 2026
Release Pre-release

Releases

latest 60 of 90
  1. v1.8.0 18 Aug 2026
    Release notes

    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/dns server layer is retired. UDP runs on preallocated job slabs, fixed workers behind a ready ring, and batched kernel I/O (recvmmsg/sendmmsg on 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_trim setting 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.Msg being 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.UnpackDomainName is 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 netip parity (#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 against example.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 sendmmsg per 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-case emptyzones entry 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} and dns_tcp_ingress_drops_total{reason} — every shed packet or connection has a named reason; dns_udp_ingress_overflow_total counts 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).
    • testify is 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.Request is 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 with ctx, 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 marked ch.Replay(). A handler with once-per-query entry effects (tokens, scores, counters, log lines) should skip them when ch.Replay() is true, and a handler that must block — network calls, contended locks — should decline an ch.InlineOnly() pass with ch.MarkHandoff() and return, exactly as the cache does. For transport and module authors: custom transports implement middleware.Transport (the dns.ResponseWriter-based contract is gone), though *Server remains a dns.Handler through retained delegating shims; WriteMsg no 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 optional ResponseSizer capability rather than a widened writer interface; and the DoQ server's Handler is 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), and memorytrim (opt-in burst-memory return on quiescence).
    • The legacy sdns.toml fallback is removed. 1.7.x silently loaded a working-directory sdns.toml when sdns.conf was missing; 1.8.0 generates a fresh config at the requested path and logs a warning if a leftover sdns.toml is found — migrate those settings manually, or the process runs on defaults. Relatedly, -c with a custom path now generates a missing config at that path instead of failing to load.
    • /metrics scrapes are uncompressed and the promhttp_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.
    • domainmetricslimit default 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. 0 still 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 [::]:53 alongside 0.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 in bind behaves exactly as before.
    • No toolchain change for source builds: the Go version requirement is the same as 1.7.4.
    Open source →
    Release notes

    v1.8.0

    Compare

    Choose a tag to compare

    Open source →
  2. v1.7.4 09 Aug 2026
    Release notes

    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 / enforce via [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 the dnssec_work_per_request and dns_recursion_fanout_ratio histograms 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. querytimeout now 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 standard context.WithTimeout at 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

    • querytimeout is now end-to-end: cache dedup waits, DNS64 subqueries, and failover all count against it, and fallbackservers are not tried once a blackholed upstream has consumed the window.
    • The legacy negative answer cache is retired: the full cachesize now 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 size maxconcurrentqueries accordingly.

    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

    Open source →
    Release notes

    v1.7.4

    Compare

    Choose a tag to compare

    Open source →
  3. v1.7.3 19 Jul 2026
    Release notes

    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 at docs/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

    Open source →
    Release notes

    v1.7.3

    Compare

    Choose a tag to compare

    Open source →
  4. v1.7.2 25 Jun 2026
    Release notes

    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 .tr TLD 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

    Open source →
    Release notes

    v1.7.2

    Compare

    Choose a tag to compare

    Open source →
  5. v1.7.1 24 Jun 2026
    Release notes

    A security and hardening patch.

    Security

    • DNSSEC: validate RSA keys whose public exponent exceeds Go's crypto/rsa ceiling (2³¹-1) — restores DNSSEC validation for zones anchored on such keys, including the entire .lv (Latvia) TLD and mailbox.org. (#495)
    • Dependencies: pinned the build toolchain to go1.26.4, clearing 5 reachable standard-library CVEs (govulncheck clean); 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: --config path 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.md now supports the 1.7.x line and directs vulnerability reports to GitHub's private reporting.

    Full changelog: v1.7.0...v1.7.1

    Open source →
    Release notes

    v1.7.1

    Compare

    Choose a tag to compare

    Open source →
  6. v1.7.0 31 May 2026
    Release notes

    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_SUBNET option, 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_v6 ceilings (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
      ```

    • 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

    • `golang.org/x/sys` → v0.45.0 (#481)
    • `goreleaser/goreleaser-action` → v7.2.2 (#480)

    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

    Open source →
    Release notes

    v1.7.0

    Compare

    Choose a tag to compare

    Open source →
  7. v1.6.7 18 May 2026
    Release notes

    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/dns64 synthesises AAAA records from A records for IPv6-only clients reaching IPv4-only services. Sits between kubernetes and cache; 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/24 per 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::/96 is the runtime default when none is configured.
      • §5.3.1 PTR translation via CNAME to in-addr.arpa with optional best-effort chase.
      • §5.3.2 — only Answer-section AAAAs are synthesised; Authority/Additional pass through unmodified.
      • §5.5 DNSSEC safetyCD=1 requests 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 had AD=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/authority
      

      Since 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

    Open source →
    Release notes

    v1.6.7

    Compare

    Choose a tag to compare

    Open source →
  8. v1.6.6 07 May 2026
    Release notes

    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 for attacker.example. with a message whose question section was victim.example. — and because the cache is keyed on the response's question, the unrelated answer was stored under victim.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, and Qclass match 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 a zone label, a list of networks (CIDR), and a list of answers (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 hostsfile and blocklist, so a view-curated answer wins over a global blocklist rule for that name. See the example block in contrib/linux/sdns.conf and 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 / Remove held b.mu (mutually exclusive with the RLock that ServeDNS takes on every query) for the full duration of the synchronous disk write in save(). Large blocklists turned that into multi-millisecond stalls of every in-flight query.

      Fixes:

      • Mutate maps under b.mu, snapshot, release b.mu, then persist outside the lock. ServeDNS readers no longer wait on disk I/O.
      • A new saveMu serializes concurrent persists; the os.Rename of 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 / RemoveBatch perform 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/batch
      • POST /api/v1/block/remove/batch

      A new contract test (Test_BlockList_NoStallDuringSave) holds saveMu from a goroutine and asserts that a concurrent ServeDNS-style RLock returns within 2s, so a future regression that re-introduces disk I/O inside the map lock fails loudly.

    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 / RemoveEndpointSlice plus a worker-coalesced MaterialiseHeadless, 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 EndpointSlice events from a deleted Service via tombstone tracking and ownerRef.UID matching, plus dirty-replay on AddService so the synthetic seed handover doesn't drop other slices.
    • onEndpointSliceUpdate retracts the slice from the old service on a service-name relabel.
    • cluster_domain is normalised (trailing dot, mixed case) at construction and at Registry.SetClusterDomain.
    • Anonymous headless endpoints get distinct dashed-IP SRV targets (10-0-0-1.svc...) instead of collapsing to one record.
    • buildConfig defers to clientcmd's default loading rules so multi-file KUBECONFIG entries merge correctly.
    • Skip-if-equal guard in applyEndpointSlice eliminates rebuilds for resourceVersion-only update events.
    • SRV port-number edits invalidate the cached *dns.SRV pointer; SRV glue refresh allocates a new answerSet rather than mutating the published one in place.
    • Run waits on per-handler HasSynced (not just informer.HasSynced) and flushes pending rebuilds before publishing synced=true.
    • DeleteService order is now tombstone → flush → DeleteService, preventing a worker rebuild from re-populating the registry after wipe.

    config.KubernetesConfig.killer_mode is 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/dnssec subpackage. The generic DNS RR helpers (ExtractRRSet, FilterRRsToZone, NameInZone, DnameTarget) and the EDEError type moved into util/, where both resolver and dnssec can share them without a circular import. Resolver-side network errors keep their identities but now use *util.EDEError instead of the resolver-local ValidationError type.

    (*Resolver).lookup() was split in place: the per-server query goroutine moved to a queryServer method, the adaptive RTT-based timeout became adaptiveServerTimeout, and the trailing fallback-picker became pickFallbackResponse. Behaviour is unchanged; lookup() drops from ~250 lines to ~140 and the goroutine entry no longer captures state via closure. Net diff: −2016 / +265 in middleware/resolver/, ~1100 lines under middleware/resolver/dnssec/.

    Config

    • Update B-root to current IANA addresses. ICANN/Verisign re-numbered B-root in late 2023 (IPv4 199.9.14.201170.247.170.2; IPv6 2001:500:200::b2801: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 canonical named.root list.

    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: configver bumps to 1.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 deprecated kubernetes.killer_mode key 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

    Open source →
    Release notes

    v1.6.6

    Compare

    Choose a tag to compare

    Open source →
  9. v1.6.6-0.20260425193455-35432ae01f86 25 Apr 2026 pre-release

    Nothing published for this version

  10. v1.6.5 25 Apr 2026
    Release notes

    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_amd64 because of the reuseport_* 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.go to bring the resolver into alignment with RFC 5011 §2 / §4 and to harden persistence against partial failures. Highlights:

    • verifyFetchedKeys is 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 kskCurrent into 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.RootKeys snapshot 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 StateRevoked marker so a tombstone-write failure survives across retries; selective fail-closed only when an actual contraction would otherwise be lost on disk.
    • New errTrustAnchorsUnavailable gates answer / authority / validateDelegation and the two delegation-cache Set sites, 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 on r.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 || dragonfly was ANDed against that, so freebsd/netbsd/openbsd/dragonfly all failed to link with undefined: defaultUDPWorkers / kernelLoadBalances / reusePortControl. Renamed to reuseport_bsds.go so 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.Open doesn'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.Dialer and bypass DialContext on 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 authcacheauthority (split into server.go + cache.go). Type renames: AuthServer/AuthServersServer/Servers; NSCacheCache; NSDelegation; DSRRDSSet; VersionIPVersion.
    • parentDSRR/parentdsrrparentDS.
    • accesslist.AccessListaccesslist.List; accesslog.AccessLogaccesslog.Log (config field names preserved).
    • r.ncacher.delegations; nameservers map type → hostSet; nameserverInfodelegationInfo with hosts field.
    • rootservers/rootkeys (smashed lowercase) → rootServers/rootKeys.
    • ipv4cache/ipv6cacheglueV4/glueV6.
    • Internal sub-pipeline now flows through the Queryer interface; util.ExchangeInternal retired.

    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

    Open source →
    Release notes

    v1.6.5

    Compare

    Choose a tag to compare

    Open source →
  11. v1.6.4 25 Apr 2026

    Nothing published for this version

  12. v1.6.4-0.20260423153249-9d6ed12c55ba 23 Apr 2026 pre-release

    Nothing published for this version

  13. v1.6.3 20 Apr 2026

    Nothing published for this version

  14. v1.6.3-0.20260419204212-728e86b0617d 19 Apr 2026 pre-release

    Nothing published for this version

  15. v1.6.2 19 Apr 2026

    Nothing published for this version

  16. v1.6.1 28 Nov 2025

    Nothing published for this version

  17. v1.6.1-0.20250812075348-5c88ac2d1c69 12 Aug 2025 pre-release

    Nothing published for this version

  18. v1.6.0 06 Jul 2025

    Nothing published for this version

  19. v1.5.3 08 Jun 2025

    Nothing published for this version

  20. v1.5.3-0.20250608081226-45a5e076d4fb 08 Jun 2025 pre-release

    Nothing published for this version

  21. v1.5.3-0.20250607221635-e3b939fbc6bd 07 Jun 2025 pre-release

    Nothing published for this version

  22. v1.5.2 07 Jun 2025

    Nothing published for this version

  23. v1.5.1 07 Jun 2025

    Nothing published for this version

  24. v1.5.1-0.20250607213458-034e31d68e0a 07 Jun 2025 pre-release

    Nothing published for this version

  25. v1.5.1-0.20250607181944-07860e902b2d 07 Jun 2025 pre-release

    Nothing published for this version

  26. v1.5.1-0.20250606105039-eca06a65326f 06 Jun 2025 pre-release

    Nothing published for this version

  27. v1.5.0 04 Jun 2025

    Nothing published for this version

  28. v1.4.1-0.20250217132115-86830209f555 17 Feb 2025 pre-release

    Nothing published for this version

  29. v1.4.0 14 Feb 2025

    Nothing published for this version

  30. v1.3.7 23 Jun 2024

    Nothing published for this version

  31. v1.3.7-0.20240623093653-80408dd1623c 23 Jun 2024 pre-release

    Nothing published for this version

  32. v1.3.7-0.20240520123848-e6ae8639c5ca 20 May 2024 pre-release

    Nothing published for this version

  33. v1.3.7-0.20240403151222-05cba1426203 03 Apr 2024 pre-release

    Nothing published for this version

  34. v1.3.7-0.20240228160045-eaaed2a7ad96 28 Feb 2024 pre-release

    Nothing published for this version

  35. v1.3.7-0.20240217115504-c5b879388cf5 17 Feb 2024 pre-release

    Nothing published for this version

  36. v1.3.6 02 Jan 2024

    Nothing published for this version

  37. v1.3.6-0.20240102112642-88568c535540 02 Jan 2024 pre-release

    Nothing published for this version

  38. v1.3.5 26 Aug 2023

    Nothing published for this version

  39. v1.3.5-0.20230826114320-3c3d7798fbc8 26 Aug 2023 pre-release

    Nothing published for this version

  40. v1.3.5-0.20230811122206-00d9f6702dfe 11 Aug 2023 pre-release

    Nothing published for this version

  41. v1.3.4 11 Aug 2023

    Nothing published for this version

  42. v1.3.3 06 Aug 2023

    Nothing published for this version

  43. v1.3.3-0.20230806085310-8888ef722926 06 Aug 2023 pre-release

    Nothing published for this version

  44. v1.3.2 26 Jul 2023

    Nothing published for this version

  45. v1.3.1-rc1.0.20230722151613-bdce4622e3c7 22 Jul 2023 pre-release

    Nothing published for this version

  46. v1.3.1-rc1.0.20230722142258-2f566be2a783 22 Jul 2023 pre-release

    Nothing published for this version

  47. v1.3.1-rc1.0.20230706214937-df3234c9ab14 06 Jul 2023 pre-release

    Nothing published for this version

  48. v1.3.1-rc1.0.20230706152244-4f79362bfb15 06 Jul 2023 pre-release

    Nothing published for this version

  49. v1.3.1-rc1 05 Jul 2023 pre-release

    Nothing published for this version

  50. v1.3.1-0.20230705115858-04513089c0ce 05 Jul 2023 pre-release

    Nothing published for this version

  51. v1.3.0 01 Jul 2023

    Nothing published for this version

  52. v1.2.5-0.20230701201342-6b2e769ae5d5 01 Jul 2023 pre-release

    Nothing published for this version

  53. v1.2.4 30 Apr 2023

    Nothing published for this version

  54. v1.2.3 30 Apr 2023

    Nothing published for this version

  55. v1.2.2 30 Apr 2023

    Nothing published for this version

  56. v1.2.1 04 Feb 2022

    Nothing published for this version

  57. v1.2.0 04 Feb 2022

    Nothing published for this version

  58. v1.1.9-0.20220204083142-bd53f679ac13 04 Feb 2022 pre-release

    Nothing published for this version

  59. v1.1.9-0.20220101103133-049b20e9239d 01 Jan 2022 pre-release

    Nothing published for this version

  60. v1.1.9-0.20211225142305-c06eef39b2be 25 Dec 2021 pre-release

    Nothing published for this version

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive