Installation
Packages, containers and building from source.
Pre-built binaries
Every release publishes archives, plus .deb and .rpm packages. The
architectures differ by platform:
| Platform | Architectures |
|---|---|
| Linux | amd64, arm64, armv5/6/7, mips, mipsle, mips64, mips64le (softfloat) |
| macOS | amd64, arm64 |
| Windows | amd64 |
| FreeBSD, OpenBSD, NetBSD | amd64 |
Asset names carry the version, so there is no version-less latest/download/
URL to fetch. Resolve the tag first:
TAG=$(curl -fsSL https://api.github.com/repos/semihalev/sdns/releases/latest \
| grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
curl -fsSL -o sdns.tar.gz \
"https://github.com/semihalev/sdns/releases/download/${TAG}/sdns-${TAG#v}_linux_amd64.tar.gz"
tar xzf sdns.tar.gz
./sdns-${TAG#v}_linux_amd64/sdns version
Replace linux_amd64 with the platform you want — the
releases page lists every
asset, and pinning a specific tag rather than resolving latest is the right
call in a deployment script.
Docker
# sdns.conf must set directory = "/var/lib/sdns" and narrow accesslist —
# see below for why each half of this command matters.
docker run -d --name sdns \
-p 127.0.0.1:53:53 -p 127.0.0.1:53:53/udp \
-v sdns-data:/var/lib/sdns \
-v "$PWD/sdns.conf:/etc/sdns.conf:ro" \
ghcr.io/semihalev/sdns:1.8.2 -c /etc/sdns.conf
Three parts of that are not decoration.
127.0.0.1: on both publishes. A bare -p 53:53 binds every interface on
the host. The shipped accesslist allows every client, so on a machine with a
public address that is an open recursive resolver, and open resolvers are found
and used for reflection attacks within hours. Publish to loopback until
accesslist says who may query.
A configuration file, mounted, and named with -c. Without one the
container writes a default config and uses it.
directory = "/var/lib/sdns" inside that file. The image is built
FROM scratch with no WORKDIR, so the process runs in / and the default
relative directory = "db" resolves to /db — not the volume. The trust
anchor state then lives in the container’s writable layer and is lost on the
next docker rm, which is exactly the failure the volume was meant to prevent
and which only surfaces at a root KSK rollover.
The volume is not optional: it holds the RFC 5011 trust-anchor state, and a container without it only reveals the problem at a root KSK rollover. A compose file and the rest of the container story are on the Containers page.
Package managers
brew install semihalev/tap/sdns # macOS, tracks releases
snap install sdns # Linux
yay -S sdns-git # Arch (AUR)
From source
Go 1.26 or newer is required; the toolchain pinned in go.mod is 1.27.
git clone https://github.com/semihalev/sdns
cd sdns
make all # generate, tidy, test, build
./sdns version
make all runs the test suite before it builds; use go build directly if you
only want the binary.
Verifying the install
Port 53 needs privilege and the shipped access list allows every client, so verify on a loopback high port rather than as root.
A partial file will not do: settings you leave out are not filled in from
the defaults, and a file without directory, rootservers and rootkeys is
rejected. Generate a complete one first — pointing -t at a path that does not
exist writes the full documented file and validates it:
./sdns -t -c check.conf
Then change three lines in check.conf:
bind = "127.0.0.1:5354" # 5353 is mDNS; pick something else
api = ""
accesslist = ["127.0.0.1/32"]
Check it again, start it, and ask it what it is:
./sdns -t -c check.conf
./sdns -c check.conf &
dig @127.0.0.1 -p 5354 version.bind TXT CHAOS +short
"SDNS v1.8.2" means it is up. A real query works too, but the first one is
slow while the resolver primes the root and fetches the trust anchor:
dig @127.0.0.1 -p 5354 example.com A +dnssec
An answer with the ad flag means the response was validated. If the first
query is slow, that is the resolver priming the root and fetching the trust
anchor — subsequent queries are served from cache.