esc
move open

Server and listeners

Bind addresses, encrypted transports, outbound source IPs, the API and logging.

Listeners

bind    = ":53"       # UDP and TCP
bindtls = ":853"      # DNS over TLS
binddoh = ":443"      # DNS over HTTPS
binddoq = ":853"      # DNS over QUIC

bind opens both UDP and TCP. A bare ":53" means every address on both families; give an address to narrow it ("192.0.2.10:53", "[2001:db8::1]:53"). Leaving a key unset means that listener is not started — the encrypted transports are all unset by default.

bindtls and binddoq can share port 853 because one is TCP and the other UDP.

TLS material

tlscertificate = "/etc/sdns/server.crt"
tlsprivatekey  = "/etc/sdns/server.key"

Both are PEM files, and both are required before DoT, DoH or DoQ will start. sdns -t opens them, so a path typo or a key the process cannot read is caught before a restart rather than after it.

Outbound source addresses

outboundips  = ["192.0.2.10", "192.0.2.11"]
outboundip6s = ["2001:db8::10"]

Addresses sdns sends its own queries from. With more than one, a source is picked per request, which spreads queries across them. Leave both empty to let the operating system choose.

These must be addresses the host actually holds. A source address that is not local fails at bind time when the query goes out, not at startup.

HTTP API

api         = "127.0.0.1:8080"
bearertoken = ""

Serves /metrics in Prometheus format plus the blocklist and cache-purge endpoints. Set api = "" to disable it entirely.

bearertoken, when set, requires Authorization: Bearer <token> on the blocklist, purge and metrics routes.

It is not sufficient protection on its own. This listener is plain HTTP with no TLS, so a token sent to a reachable address crosses the network in the clear. And with SDNS_PPROF=true the /debug/pprof routes skip the token check entirely, because pprof tooling sends no Authorization header.

Keep it on loopback. If it must be reachable, put it behind a TLS-terminating authenticating proxy, a VPN, or a source-restricted firewall, and treat the token as a second layer rather than the first. See Monitoring for what the endpoints do.

Logging

loglevel  = "info"     # error, warn, info, debug
accesslog = ""         # path; empty disables

accesslog writes one line per query in Common Log Format. It is off by default because on a busy resolver it is the largest thing the process writes.

For structured, machine-readable query logging, use dnstap instead:

dnstapsocket        = "/var/run/sdns/dnstap.sock"
dnstapidentity      = "sdns"
dnstapversion       = "1.0"
dnstaplogqueries    = true
dnstaplogresponses  = true
dnstapflushinterval = 5

Identification

nsid  = ""      # RFC 5001; empty disables
chaos = true

nsid returns a server identifier in an EDNS option, which is how you tell which member of an anycast set answered you.

chaos answers version.bind, version.server, hostname.bind and id.server in the CHAOS class. It is on by default and is the usual way to confirm which build a node is running:

dig @resolver version.bind TXT CHAOS +short

Turn it off if you would rather not publish the version.

Server resources

The worker pool, the in-flight query cap and the TCP/DoT connection cap are derived at startup from the machine’s memory, CPU count and file-descriptor limit, and each is logged as its listener starts. The overrides exist, but leave them unset unless a measurement on your own hardware says otherwise:

# ingressworkers  = 256    # handler workers per listener
# ingressqueue    = 64     # ready-queue depth before a query gets its own goroutine
# ingresstcpconns = 1024   # concurrent inbound TCP/DoT connections
# memorytrim      = true   # return burst memory to the OS after a long idle

memorytrim runs one synchronous garbage collection over the whole process after several quiet minutes. It is meant for memory-constrained devices — containers on routers, small VPSes — where returning a traffic burst’s memory matters more than the pause. On a busy server it is the wrong trade.