- Zig 94.7%
- Shell 3%
- TLA 1.5%
- Roff 0.4%
- Makefile 0.2%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| ci | ||
| corpus | ||
| docker | ||
| examples | ||
| packaging | ||
| src | ||
| test | ||
| tla | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| bench-network.sh | ||
| bench_backends.zig | ||
| bench_control_socket.zig | ||
| bench_dns_comparison.sh | ||
| bench_dnssec.zig | ||
| bench_generic.zig | ||
| bench_metrics_http.zig | ||
| bench_pdns_comparison.sh | ||
| bench_recursor_comparison.sh | ||
| bench_reload.zig | ||
| bench_results_summary.md | ||
| bench_socket_comparison.zig | ||
| bench_zone_load.zig | ||
| benchmark_test.zone | ||
| build.zig | ||
| build.zig.zon | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| fuzz_authoritative.zone | ||
| KNOT_DNS_BUG.md | ||
| lint.sh | ||
| Makefile | ||
| NOT_IMPLEMENTED.md | ||
| OPTIMIZATION_IDEAS.md | ||
| OPTIMIZATION_RESULTS.md | ||
| pdns-bench.conf | ||
| pdns_bind.conf | ||
| PERFORMANCE_ANALYSIS.md | ||
| README.md | ||
| RECORD_TYPES.md | ||
| RECURSOR_BASELINE.md | ||
| RFC_COMPLIANCE.md | ||
| stress_test.zig | ||
| test-with-postgres.sh | ||
| TEST_COVERAGE.md | ||
| zdist.conf | ||
| zdns.conf.example | ||
| ZIG.md | ||
zdns
zdns is the Zig PowerDNS port in this repository.
Binjovi Builds use ci/binjovi-package.sh as the one repository-owned package
entry point. The fixed worker supplies a validated revision, version, mode, and
output directory. A pull-request Build compiles and tests the source and creates
one tar archive. After Binjovi integrates the selected Builds into trunk, the
combined Release Build also creates a DEB package and an RPM package. A fixed
task stages one signed private APT and DNF repository. Separate Debian and Alma
tasks install through that repository. Their package-install containers receive
no storage credential. After both tests pass, the Release copies the same
package bytes and repository metadata to an immutable public release snapshot,
adds the production signatures, and verifies the public files.
Goal:
- Reach broader PowerDNS feature parity, not just authoritative-server parity.
- Become a drop-in binary replacement for PowerDNS.
- Use idiomatic Zig to reach this goal with better performance and maintainability.
The most mature supported subset today is the typed authoritative stack. It is
built around DnsServer(BackendT) and MemoryBackend.
Current maturity: the authoritative path is mature. 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
servehandles 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
ANYmitigation returnsTC=1. TCPANYstill returns the full answer - control socket is local-user only by default, with
0600pathname 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
You must set control_socket_path to enable the control socket. The mode,
timeout, and connection keys above take effect only after you set it. The
parser fails closed on unknown keys.
Incomplete or unsupported:
- BindBackend mutations: read-only. It has no addRecord or deleteRecord support.
- UPDATE is not transactional.
- The 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 is missing.
- Benchmark accuracy is not yet verified.
- Recursor operator commands are
PING,STATS, andSHUTDOWN.STATSreturns a raw JSON body after the OK line. - Recursor HTTP monitoring exposes
/metrics,/stats, and/health./zonesreturns501 Not Implementedon the recursor.
Canonical status and roadmap: ./ZIG.md.
Formal models for the authoritative path live in tla/auth/. The
listening-server shutdown handshake is modeled in tla/server/. This handshake
models the macOS accept-join deadlock that the Zig 0.16 port fixed.
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 ...
}
To swap the backend, change the two backend lines. The Server.init call
stays the same for all four backends:
// 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|postgresto select backend at build time
Build And Test
On macOS, Zig may not find openssl/rsa.h. If that happens, set
OPENSSL_PREFIX to the Homebrew OpenSSL install before you build:
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.
Binjovi validates pull requests and releases packages from trunk. The signed
Debian and Alma-compatible repository indexes and the immutable release
manifest are under https://pkg.sean.farm/zdns/. The package install tests run
against both distribution families before Binjovi can publish a Release.
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, through src/recursor.zig.
There is no dedicated recursor test step.
Docker / Linux
The host dev build and tests run natively. On this machine, that means macOS.
docker/Dockerfile is a multi-stage build using Debian and Zig 0.16. It builds
and runs zdns on Linux through 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 gives the most value. The integration suites bind real
sockets. Running them on Linux exercises the platform branch of the
accept-shutdown handshake that a macOS host never runs. On Linux, shutdown()
wakes a blocked accept(). On macOS, it does not. See tla/server/ for
details.
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 and e2e compose services live behind the e2e profile, so
./test-with-postgres.sh is unaffected. That script uses only the postgres
service.
Running Integration Tests
PostgreSQL Tests with Docker (Recommended)
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. If it is unset, serve falls
back to host=localhost port=5432 dbname=powerdns user=pdns.
Examples:
examples/serve.zig- canonical UDP and TCP authoritative serverexamples/serve_tcp.zig- TCP-only variantexamples/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 used only when a config file is loaded with
--config or -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.
Native pipeline timing
Native tasks build the packages and run the Debian and Alma install checks. The pipeline ends at the verified package Release. This project has no configured Deploy target.
Open the project dashboard and expand an execution attempt for the task timeline and available CPU, off-CPU, and syscall profiles. Cached BuildKit steps still appear as cached work; a missing sample does not prove that a task used no CPU.
Select an exact successful Build with
binjovictl release zdns --build BUILD_ID --wait.
Then use binjovictl status zdns to check its release and any
configured deployment. Telemetry explains time spent; immutable artifact and
revision evidence establish completion.