Authoritative + recursive DNS server in Zig (PowerDNS port)
  • Zig 95.2%
  • Shell 2.6%
  • TLA 1.5%
  • Roff 0.3%
  • Makefile 0.2%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-08 13:45:39 +00:00
corpus Add fuzz testing and automated lint checks 2026-04-01 19:12:43 +02:00
docker Add Docker / Linux build, run, and test harness 2026-06-12 22:36:32 +02:00
examples release: v0.1.8 — --version/-V on every binary 2026-07-01 22:28:04 +02:00
packaging release: v0.1.10 — authoritative systemd service (zdns@.service) 2026-07-01 23:10:55 +02:00
src fix(config): parseListenAddr rejects empty address (was addr[0] OOB) 2026-07-08 15:39:28 +02:00
test Make weighted-LB integration test robust to sampling variance 2026-06-12 22:36:17 +02:00
tla Add TLA+ model of the server shutdown handshake 2026-06-12 21:24:52 +02:00
.dockerignore Add Docker / Linux build, run, and test harness 2026-06-12 22:36:32 +02:00
.gitattributes fix(cicd): auto-resolve CHANGELOG land conflicts via .gitattributes merge=union 2026-07-05 12:14:44 +00:00
.gitignore Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench savegame 2026-04-04 22:57:37 +02:00
bench-network.sh Add SQLite and PostgreSQL backends with Docker testing 2026-04-06 17:23:12 +02:00
bench.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_authoritative_cache.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_backends.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_control_socket.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_dns_comparison.sh Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
bench_dnssec.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_generic.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_metrics_http.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_pdns_comparison.sh Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
bench_recursor_comparison.sh Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
bench_recursor_workers.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_reload.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_results_summary.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
bench_socket_comparison.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
bench_zone_load.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
benchmark_test.zone Replace custom benchmark with industry-standard dnsperf 2026-04-06 19:48:22 +02:00
build.zig release: v0.1.8 — --version/-V on every binary 2026-07-01 22:28:04 +02:00
build.zig.zon release: v0.1.11 (auto-cadence) 2026-07-08 13:45:39 +00:00
CHANGELOG.md release: v0.1.11 (auto-cadence) 2026-07-08 13:45:39 +00:00
docker-compose.yml Add Docker / Linux build, run, and test harness 2026-06-12 22:36:32 +02:00
fuzz_authoritative.zone Split recursor into focused modules and harden test isolation 2026-06-12 15:39:38 +02:00
KNOT_DNS_BUG.md Add comprehensive DNS server benchmark comparison 2026-04-07 01:55:02 +02:00
lint.sh Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
Makefile Add Docker / Linux build, run, and test harness 2026-06-12 22:36:32 +02:00
NOT_IMPLEMENTED.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
OPTIMIZATION_IDEAS.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
OPTIMIZATION_RESULTS.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
pdns-bench.conf Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
pdns_bind.conf Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
PERFORMANCE_ANALYSIS.md Add hardware CRC32 optimization for DNS name hashing 2026-04-07 02:04:36 +02:00
README.md docs(readme): align example listen port with docker quick-start (5354) 2026-07-03 22:57:11 +02:00
RECORD_TYPES.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
RECURSOR_BASELINE.md Split recursor into focused modules and harden test isolation 2026-06-12 15:39:38 +02:00
RFC_COMPLIANCE.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
stress_test.zig Port to Zig 0.16 2026-06-12 20:27:49 +02:00
test-with-postgres.sh Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
TEST_COVERAGE.md Add comprehensive DNS coverage documentation and missing tests 2026-04-06 18:40:03 +02:00
zdist.conf checkpoint: commit remaining zig-dns work 2026-04-04 09:38:02 +02:00
zdist.conf.example checkpoint: commit remaining zig-dns work 2026-04-04 09:38:02 +02:00
zdns.conf.example Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00
ZIG.md Rename zig-dns references to zdns 2026-06-12 15:45:46 +02:00

zdns

zdns is the Zig PowerDNS port in this repository.

Goal:

  • reach broader PowerDNS feature parity, not just authoritative-server parity
  • become a drop-in binary replacement for PowerDNS
  • get there with better performance and maintainability through idiomatic Zig

The most mature supported subset today is the typed authoritative stack built around DnsServer(BackendT) and MemoryBackend.

Current maturity: the authoritative path is mature, and the recursor now has a supported UDP server binary with DNSSEC validation, control socket support, and HTTP metrics.

Status

Supported:

  • core DNS parsing and packet generation
  • authoritative serving with four backends:
    • MemoryBackend - in-memory HashMap (fastest, fully featured)
    • BindBackend - BIND zone files with binary index (read-only)
    • SqliteBackend - SQLite persistent storage (ACID, mutations supported)
    • PostgresBackend - PostgreSQL networked database
  • canonical serve handles authoritative UDP and TCP
  • zone load and reload on all backends
  • AXFR and IXFR client paths
  • typed AXFR, IXFR, and UPDATE server APIs
  • TSIG support in transfer and UPDATE codepaths
  • control socket and HTTP metrics services
  • compile-time backend selection (-Dbackend=memory|bind|sqlite|postgres)
  • authoritative DNS Cookies via --cookie-secret-hex
  • UDP ANY mitigation returns TC=1; TCP ANY still returns the full answer
  • control socket is local-user only by default, with 0600 pathname permissions

Example server config:

[server]
listen=127.0.0.1:5354
tcp_timeout_ms=5000
tcp_max_connections=100
tcp_max_queries_per_connection=1000
tcp_max_message_size=65535
control_socket_path=/tmp/zdns.sock
control_socket_mode=0600
control_socket_read_timeout_ms=1000
control_socket_write_timeout_ms=1000
control_socket_max_connections=10

control_socket_path must be set for the control socket to be enabled; the mode/timeout/connection keys above only take effect once it is. The parser fails closed on unknown keys.

Incomplete or unsupported:

  • BindBackend mutations (read-only, no addRecord/deleteRecord support)
  • UPDATE is not transactional
  • recursor is UDP-only for now; TCP query serving is still not supported
  • benchmark claims for the recursor are not yet published here
  • full operational and end-to-end coverage
  • verified benchmark accuracy
  • recursor operator commands are PING, STATS, and SHUTDOWN; STATS returns a raw JSON body after the OK line
  • recursor HTTP monitoring exposes /metrics, /stats, and /health; /zones returns 501 Not Implemented on the recursor

Canonical status and roadmap: ./ZIG.md.

Formal models for the authoritative path live in tla/auth/; the listening-server shutdown handshake (the macOS accept/join deadlock the Zig 0.16 port fixed) is modeled in tla/server/.

Architecture

Prefer typed backends and typed service composition. Under Zig 0.16 every init takes a std.Io; in a binary, io and the allocator come from the process init struct in main:

pub fn main(init: std.process.Init) !void {
    const allocator = init.gpa;
    const io = init.io;

    // Memory backend (HashMap-based, fastest)
    const Server = dns.server.DnsServer(dns.backends.memory.MemoryBackend);
    var backend = dns.backends.memory.MemoryBackend.init(allocator, io);
    var server = try Server.init(allocator, io, &backend, .{});
    defer server.deinit();
    // ... serve ...
}

Swap the backend by changing the two backend lines (the Server.init call is identical for all four):

// BIND backend (zone file with binary index, read-only; 100-entry LRU cache)
const Server = dns.server.DnsServer(dns.backends.bind.BindBackend);
var backend = dns.backends.bind.BindBackend.init(allocator, io, 100);

// SQLite backend (persistent storage, ACID transactions)
const Server = dns.server.DnsServer(dns.backends.sqlite.SqliteBackend);
var backend = try dns.backends.sqlite.SqliteBackend.init(allocator, io, "zones.db");

// PostgreSQL backend (networked database)
const Server = dns.server.DnsServer(dns.backends.postgres.PostgresBackend);
var backend = try dns.backends.postgres.PostgresBackend.init(allocator, io, "host=localhost dbname=pdns");
  • treat the runtime-erased backend layer as compatibility glue, not the main path
  • use -Dbackend=memory|bind|sqlite|postgres to select backend at build time

Build And Test

On macOS, if Zig cannot find openssl/rsa.h, set OPENSSL_PREFIX to the Homebrew OpenSSL install before building:

export OPENSSL_PREFIX="$(brew --prefix openssl@3)"

The default zig build installs all binaries to zig-out/bin, including serve, serve_tcp, zdist, and recursor. There is no separate recursor step — plain zig build produces zig-out/bin/recursor.

zig build
zig build -Dbackend=memory
zig build -Dbackend=bind
zig build -Dbackend=sqlite
zig build -Dbackend=postgres
zig build test
zig build test-integration
zig build test-integration-lb
zig build test-fuzz
zig build stress-test

Recursor unit tests run as part of zig build test (via src/recursor.zig); there is no dedicated recursor test step.

Docker / Linux

The host dev build and tests run natively (so on macOS, here). docker/Dockerfile is a multi-stage build (Debian + Zig 0.16) that builds and runs zdns on Linux, driven by a few make targets:

make docker-test    # build on Linux, run test + integration + integration-lb + fuzz
make docker-serve   # run `serve` as a DNS container on 5354 (UDP+TCP)
make docker-e2e     # compose: server + a dig client that asserts a real answer

make docker-test is the one that earns its keep: the integration suites bind real sockets, so running them on Linux exercises the platform branch of the accept/shutdown handshake that a macOS host never runs (shutdown() wakes a blocked accept() on Linux but not macOS — see tla/server/).

make docker-serve then dig +short @127.0.0.1 -p 5354 www.example.com A returns 93.184.216.34 from test/example.com.zone. The server image uses the memory backend; Postgres-backend tests skip unless wired up — make docker-test-pg runs the suite with PGCONNECT pointed at the compose Postgres so they execute. The zdns/e2e compose services live behind the e2e profile, so ./test-with-postgres.sh (which uses only the postgres service) is unaffected.

Running Integration Tests

The easiest way to run PostgreSQL integration tests:

# Run all tests with PostgreSQL (starts container, runs tests, cleans up)
./test-with-postgres.sh

# Or keep container running for repeated test runs
./test-with-postgres.sh --keep
zig build test  # Run again without restart
docker-compose down  # When done

PostgreSQL Tests with Local Install

If you have PostgreSQL installed locally:

# Create test database
createdb pdns_test
psql pdns_test -c "CREATE USER test WITH PASSWORD 'test';"
psql pdns_test -c "GRANT ALL PRIVILEGES ON DATABASE pdns_test TO test;"
psql pdns_test -c "GRANT CREATE ON SCHEMA public TO test;"

# Run tests
export PGCONNECT="host=localhost port=5432 dbname=pdns_test user=test password=test"
zig build test

Manual Docker Container Management

# Start PostgreSQL container
docker-compose up -d postgres

# Run tests
export PGCONNECT="host=localhost port=5432 dbname=pdns_test user=test password=test"
zig build test

# Stop PostgreSQL container
docker-compose down

PostgreSQL Backend for Server

To run the server with the PostgreSQL backend, build with -Dbackend=postgres and provide the connection string in PGCONNECT:

# Install PostgreSQL
brew install postgresql@16  # macOS
# or
sudo apt-get install postgresql libpq-dev  # Ubuntu

# Create production database
createdb powerdns
psql powerdns -c "CREATE USER pdns WITH PASSWORD 'secret';"
psql powerdns -c "GRANT ALL PRIVILEGES ON DATABASE powerdns TO pdns;"

# Build with the postgres backend and run
zig build -Dbackend=postgres
export PGCONNECT="host=localhost port=5432 dbname=powerdns user=pdns password=secret"
./zig-out/bin/serve test/example.com.zone example.com.

serve reads PGCONNECT from the environment, falling back to host=localhost port=5432 dbname=powerdns user=pdns when it is unset.

Examples:

  • examples/serve.zig - canonical UDP and TCP authoritative server
  • examples/serve_tcp.zig - TCP-only variant
  • examples/zdist.zig - DNS load balancer

DNS Cookies

Set a cookie secret to enable stateless authoritative DNS Cookies:

./zig-out/bin/serve test/example.com.zone example.com. --cookie-secret-hex=0123456789abcdef

You can also set server.cookie_secret_hex in a config file. The DNS_COOKIE_SECRET_HEX env var is only consulted when a config file is loaded (via --config/-c); it is not read for a standalone invocation.

Benchmarks

Runnable commands:

zig build bench-dnssec
zig build bench-generic
zig build bench-socket
zig build bench-zone-load
zig build bench-reload
zig build bench-control-socket
zig build bench-metrics-http
zig build bench-backends

Treat benchmark conclusions as unverified unless you rerun them yourself.

Documentation

  • ./ZIG.md: canonical status and roadmap

License

See the repository license.