Repository navigation
A maintenance release with two operational changes worth reading before you upgrade: SIGHUP now reloads the configuration, not only the TLS certificate, and the answer rate limit (ratelimit) works per cached answer and replies with a truncated answer instead of silence. It also brings three opt-in features from a contributor (AAAA suppression, owner-authoritative views and hosts file filtering), a fix for DoH from browsers, a shutdown that waits out querytimeout, and a memory warning for oversized caches. Recommended for all deployments, and especially for any that set ratelimit.
Reload the configuration without a restart (#720)
On SIGHUP sdns reads its configuration file again and passes it through the same gate as sdns -t, unknown keys included. A file that fails changes nothing: the error is logged and the running configuration stays. The TLS certificate is still reloaded on the same signal, and the packaged systemd unit now has ExecReload, so systemctl reload sdns sends it.
These keys take effect without a restart:
| Key | On reload |
|---|---|
loglevel |
The new level applies at once |
accesslist |
The new list replaces the old one |
clientratelimit |
Every client starts the new rate with a full quota |
whitelist |
The new list replaces the old one |
blocklist |
Added entries are blocked. An entry removed from the file stays blocked, since an API write may have saved it too; remove it through the API |
[[views]] |
The new views replace the old ones, and views can be added to a server that started with none |
rpz.mode |
shadow and enforce switch at once; a query in flight finishes in the mode it started in |
- All or nothing. Every handler whose key changed prepares its new settings first, and only when all of them succeed do they take effect. One refusal leaves everything as it was.
- Restart-only keys. Any other changed key, such as listeners, forwarders, the RPZ zone list, cache and resolver settings or the hosts file options, is listed as needing a restart. It is listed again on every reload until the server restarts or the file goes back.
- Generated values stay. A cookie secret or IPv6 access that sdns generated or probed at startup is kept when the file does not name it.
- Record. Each reload is logged in one line and counted in
dns_config_reloads_total{outcome}, with outcomeapplied,restart_needed,unchangedorrejected.
If you already send SIGHUP, from a certificate renewal hook for example, it now also applies whatever the configuration file on disk says. Check the file with sdns -t before the next renewal, or the reload log line after it.
A side effect of the same work: a file with ipv6access = true no longer pays a two-second IPv6 probe at startup.
The answer rate limit, per answer (#716, closes #715)
ratelimit limits how often each cached answer is served to sources that have not proved their address, against reflection. Its state used to live in a fixed pool of 997 buckets that answers hashed into, so a flood on one answer silenced every answer sharing its bucket. A reflection flood of root DNSKEY queries left unrelated names unanswerable this way.
- Each answer keeps its own limit. It costs one machine word in the cache entry, with no allocation and no shared state, so a flood of one question never slows the answer to another. A refresh keeps the bucket its predecessor left, so refreshing a flooded answer does not hand it a fresh burst.
- Over the rate, UDP gets a truncated reply instead of silence. The reply is empty: the question, the OPT with the server cookie, no records and no NSID. It is no larger than the query, apart from the 16-byte server cookie for a query that sent a cookie, so it reflects nothing worth sending. A real client retries over TCP, or over UDP with the cookie the reply carried. Other unproved transports are dropped over the rate, as before.
- Proved sources are never limited. A TCP or QUIC handshake, a valid server cookie or a loopback address proves the source, so the retry a truncated reply asks for is always answered.
- Counted. Refusals are counted in the new
dns_cache_answers_limited_total{action="truncated|dropped"}. Before, a refused cache hit was silent and counted as a hit.
New opt-in features
All off by default. Thanks to @mrpops2ko for all three, and for the guide below.
block_aaaa(#699). For intentionally IPv4-only networks, answers clientIN AAAAqueries with a local NODATA, ahead of hosts files, views, blocklists, RPZ, the cache and DNS64. Internal resolution and IPv6 transport are unaffected. Replies clear AA and AD and carry EDE 0 for EDNS clients. New metricdns_aaaa_blocked_total.- Owner-authoritative views (#702).
mode = "authoritative-owner"on a[[views]]entry makes the view own its configured names across record types: a missing type is NODATA rather than a lookup upstream, and a configured CNAME is returned without following its target. Overlay remains the default. A new guide (#704) uses it to issue certificates with ACME DNS-01 for services that only exist on a private network. - Hosts file filters (#700).
hostsfilechecknamesskips names that are not valid hostnames, with a warning, andhostsfilezonespublishes only names under the listed zones. Both apply on every reload of the file.
Fixes
- DoH from browsers (#697). A page that POSTs
application/dns-messagesends a CORS preflight first, which got 405, so the browser never sent the query.OPTIONSnow gets 204 and never reaches the DNS pipeline. - Shutdown waits out
querytimeout(#698, closes #693). The listeners' drain used a fixed ten seconds. It now waitsquerytimeout, ten seconds when unset, so a stop takes up toquerytimeoutplus five seconds whencache_persistsaves the cache. Docker stop timeouts and systemdTimeoutStopSechave to cover that sum. Thanks to @mvanhorn. - Cache size warning (#711, closes #696). Startup and
sdns -twarn whencachesize, full, would not fit the machine's memory budget; the estimate is in the cache docs. Thanks to @SomSamantray. dns_cache_evictions_totalcounts (#692). It was registered and documented but never raised, so it read zero, and so did the API's evictions figure. It now counts answers evicted for capacity.- DNS64 clears AD on a filtered reply (#692). When every AAAA was excluded and the A lookup yielded nothing, the stripped reply kept the AD bit of the unfiltered answer. It is cleared, with EDE 4 when the upstream had vouched for the answer.
- Homebrew service (#692). The service ran
sdns -config, which the command line reads as-c onfig, so it exited at once. sdns is now in homebrew-core. - Snap (#692). The snap writes the standard generated configuration with its state under
SNAP_COMMON, which survives refreshes, and every release is published to the stable channel. dns_localroot_answers_totalexports all five kinds from startup, at zero, where each used to appear on its first count.
Configuration
- No version line (#718). New configuration files carry no
versionline. One in an existing file is ignored and can be deleted; it no longer logs a warning when it differs from the running release. - Documentation audit (#692). Every page of the documentation and the generated configuration was checked against the code and corrected where they disagreed.
Under the hood
- The two remaining query-path counters,
dns_localroot_answers_totalandrpz_action_total, go through the sharded metric shim like the rest (#719); names and labels are unchanged. - Seventeen flaky tests were fixed, each in the test (#712).
- Release binaries are built with Go 1.27.3.
- Dependencies: quic-go 0.64.1, with better congestion control on paths that reorder packets, for DoQ and DoH3; prometheus client_golang 1.25.0; golang.org/x/sys 0.49.0; golang.org/x/sync 0.24.0; lz4 4.1.33.
Upgrade notes
SIGHUPreloads the configuration. See above.ratelimit. UDP queries over an answer's rate now get a truncated reply instead of being dropped, and proved sources are exempt. The new counter replaces nothing;dns_ratelimit_exceeded_totalstill counts onlyclientratelimit.- Stop time. Up to
querytimeoutplus five seconds withcache_persist. - Go API, for embedders.
config.Parsereads and judges a file without side effects, andconfig.LoadisParseplus them. A loaded configuration now has the defaults handlers used to fill in at construction: an emptybindreads[":53"], an emptyaccesslistreads the two allow-all ranges, andloglevel,blocklistdir,maxdepthandquerytimeoutare settled. Handlers that apply settings live implementmiddleware.Reloader.cache.CacheEntry.GetRateLimiteris gone with the shared limiter.