the response reader was constructed over transfer_buf and then read into that same buffer, so readSliceShort memcpy'd the reader's buffer onto itself and panicked on any read that found bytes already buffered. stream the body straight into the caller's writer instead: no second copy, no aliasing.
nxdns
A self-hosted DNS sinkhole for a household LAN, written in Zig 0.16. One static musl binary, SQLite for state, a Raspberry Pi 5 as the reference target. It answers your network's DNS, blocks what you tell it to, and shows you what asked for what.
Features
- Blocklist filtering: subscribe to hosts/domain lists, plus your own allow
and block rules with wildcard support (
*.example.com) - Per-client policy groups: different filtering for the kids' tablet and your workstation
- Local DNS records and conditional forwarding for internal zones
- Encrypted upstreams: DNS-over-HTTPS and DNS-over-TLS with failover
- Built-in DoH and DoT server endpoints, with certificate hot-reload
- Bounded in-memory DNS cache with TTL-respecting expiry
- Query log with retention limits, live-streamed over SSE
- Web UI (embedded in the binary) and a REST API with a served OpenAPI spec
- Prometheus-style
/metrics, per-client rate limiting, disk-full self-protection
Quickstart (docker compose)
Build the binary and image, seed a minimal configuration, start it:
(cd web && npm ci && npm run build)
zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
cd deploy/docker
mkdir -p etc-nxdns
cat > etc-nxdns/config.zon <<'EOF'
.{
.groups = .{ .{ .name = "default" } },
.upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
.web = .{ .password = "choose-a-real-password" },
}
EOF
docker compose up -d
DNS is on port 53, the web UI on http://localhost:8080. The config file
seeds the database on first boot only; from then on the database is the
truth and changes go through the UI, the API, or nxdns export /
nxdns import. Full install instructions, including the systemd path and
the Pi 5 recipe, are in docs/operator.md.
Building from source
Requires Zig 0.16.0 and Node.js 24 (for
the web UI). C dependencies (SQLite, mbedTLS) are vendored and built by
zig build.
(cd web && npm ci && npm run build) # web UI -> web/dist
zig build -Dweb-dist=web/dist # native binary -> zig-out/bin/nxdns
zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
# static x86_64 + aarch64 musl binaries
zig build test --summary all # unit tests
Documentation
- docs/operator.md — install, configure, back up, upgrade, troubleshoot
- docs/architecture.md — module map and design
- docs/config-reference.md — every configuration field
- docs/api.md — REST API, auth and SSE
- PLAN.md and specs/ — scope, design decisions and per-milestone contracts