esc
↑↓ move ↵ open

Running as a service

systemd, the unprivileged user, file layout and binding to port 53.

The .deb and .rpm packages (built for Linux amd64, x86_64, only) install the unit, the user and the configuration for you. This page describes what they set up, which is also what you want if you are installing from a tarball.

The unit

[Unit]
Description=SDNS - Fast DNS Resolver
ConditionPathExists=/var/lib/sdns
Wants=network.target
After=network.target

[Service]
Type=simple
User=sdns
Group=sdns
LimitNOFILE=131072
Restart=on-failure
RestartSec=10
Environment="SDNS_DEBUGNS=false"
Environment="SDNS_PPROF=false"
WorkingDirectory=/var/lib/sdns
ExecStart=/usr/bin/sdns --config=/etc/sdns.conf
PermissionsStartOnly=true
StandardOutput=syslog
StandardError=journal
SyslogIdentifier=sdns
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Three lines carry most of the weight.

AmbientCapabilities=CAP_NET_BIND_SERVICE is what lets an unprivileged process bind port 53. Do not run sdns as root to solve this.

LimitNOFILE=131072 matters more than it looks: the TCP/DoT connection cap is derived at startup partly from the file-descriptor limit, so a low limit silently gives you a smaller connection cap than the machine could handle.

ConditionPathExists=/var/lib/sdns makes systemd skip the start, without marking the unit failed, rather than start a resolver with nowhere to write.

Files

Path Contents
/usr/bin/sdns The binary
/etc/sdns.conf Configuration
/var/lib/sdns Working directory of the unit
/var/lib/sdns/db/ State, from the shipped directory = "db"

directory = "db" is relative, so it resolves against the unit’s WorkingDirectory and state lives in /var/lib/sdns/db/:

File Contents
trust-anchor.db RFC 5011 trust anchor database
trust-anchor-tombstones.db Revoked trust anchors
root.zone Local root zone copy, with hyperlocal_root
cache.snapshot Cache saved at shutdown, with cache_persist
blacklists/ Downloaded blocklists

The directory must be writable by the sdns user. It holds real state, the RFC 5011 trust anchor database in particular, so it belongs on persistent storage, not in a tmpfs.

Operating it

sudo systemctl enable --now sdns
sudo systemctl status sdns
sudo journalctl -u sdns -f

The listener bounds (worker pool, in-flight cap, TCP connection cap) are logged as each listener starts, which is the quickest way to confirm what the process actually derived from the machine.

Restarting safely

sudo -u sdns sh -c 'cd /var/lib/sdns && /usr/bin/sdns -t -c /etc/sdns.conf' && sudo systemctl restart sdns

Run the check as the service user. As root it reads files the sdns user cannot, so a TLS key with tight ownership passes the test and then fails at startup. Run it from /var/lib/sdns too: the relative directory = "db" is checked against the current directory, so from anywhere else the test validates the wrong path.

A stop waits for in-flight queries to drain for querytimeout (10 seconds when that is omitted or "0"), and a query still running then gets no reply. With cache_persist on, saving the snapshot can add up to 5 more, so a stop takes at most querytimeout plus 5 seconds (15 seconds at the default). The unit sets no TimeoutStopSec, so systemd’s DefaultTimeoutStopSec applies. Its usual 90 seconds covers the default; a querytimeout whose drain plus 5 seconds exceeds that needs TimeoutStopSec set above that sum.

Make the validation gate part of the restart, not a thing you remember to run. It reports every problem in the file at once and exits nonzero on any of them, so the && is doing real work.

Conflicting resolvers

On most distributions something already holds port 53: systemd-resolved, dnsmasq, or an existing recursor. Check before the first start:

sudo ss -lnup 'sport = :53'

For systemd-resolved, the usual approach is to turn off its stub listener (DNSStubListener=no in /etc/systemd/resolved.conf) rather than disabling the service, so /etc/resolv.conf handling stays intact.

Debug environment variables

The unit ships both off:

SDNS_PPROF=false     # serve /debug/pprof on the API listener
SDNS_DEBUGNS=false   # serve the CHAOS-class nameserver debug view

SDNS_PPROF=true exposes Go’s profiling endpoints on the API address, and bearertoken does not cover them: pprof tooling sends no Authorization header, so those routes stay open even when a token is set. Leave it off unless you are actively profiling, and keep the listener on loopback while it is on.

SDNS_DEBUGNS=true answers CHAOS HINFO queries with the delegation sdns holds for a name, its per-server RTT and its health. Both are covered on the diagnostics page.