Skip to content

v1.9.1

Latest

Choose a tag to compare

@semihalev semihalev released this 22 Oct 20:34
· 1 commit to main since this release
48ee393

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 outcome applied, restart_needed, unchanged or rejected.

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 client IN AAAA queries 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 metric dns_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). hostsfilechecknames skips names that are not valid hostnames, with a warning, and hostsfilezones publishes 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-message sends a CORS preflight first, which got 405, so the browser never sent the query. OPTIONS now 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 waits querytimeout, ten seconds when unset, so a stop takes up to querytimeout plus five seconds when cache_persist saves the cache. Docker stop timeouts and systemd TimeoutStopSec have to cover that sum. Thanks to @mvanhorn.
  • Cache size warning (#711, closes #696). Startup and sdns -t warn when cachesize, full, would not fit the machine's memory budget; the estimate is in the cache docs. Thanks to @SomSamantray.
  • dns_cache_evictions_total counts (#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_total exports 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 version line. 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_total and rpz_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

  • SIGHUP reloads 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_total still counts only clientratelimit.
  • Stop time. Up to querytimeout plus five seconds with cache_persist.
  • Go API, for embedders. config.Parse reads and judges a file without side effects, and config.Load is Parse plus them. A loaded configuration now has the defaults handlers used to fill in at construction: an empty bind reads [":53"], an empty accesslist reads the two allow-all ranges, and loglevel, blocklistdir, maxdepth and querytimeout are settled. Handlers that apply settings live implement middleware.Reloader. cache.CacheEntry.GetRateLimiter is gone with the shared limiter.