mokhtar 004092a7ef
Gates / test (push) Successful in 1m38s
Gates / test-aarch64 (push) Successful in 5m5s
Gates / frontend (push) Successful in 1m26s
Gates / package (push) Successful in 6m43s
Gates / container (push) Successful in 5m30s
CI / gates (push) Successful in 20m24s
delete TECH_DEBT.md: all 71 findings closed by milestones 15-19
2026-08-08 12:39:58 +02:00

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

Install

No release exists yet. This repository has no tags, nothing has been published to https://git.mial.net/mokhtar/nxdns/releases, and no container image has been pushed. Every release URL on this page and in the how-to guides is a 404 today, and docker pull finds nothing. Until the first tag ships, building from source is the only way to get nxdns.

What a tag will publish, once one exists: five assets — two static musl tarballs (nxdns-<version>-x86_64-linux-musl.tar.gz, nxdns-<version>-aarch64-linux-musl.tar.gz), IMAGE-DIGEST.txt naming the multi-architecture container image by digest, SHA256SUMS.txt covering those three files, and SHA256SUMS.txt.asc, a detached OpenPGP signature over the checksum file. Verify what you downloaded before you run it: docs/how-to/verify-a-release.md, which also says what that signature does and does not prove.

Quickstart (docker compose)

Seed a minimal configuration and start the published image. This is what the first release will make possible; it does not work today, because there is no image in the registry to pull:

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
NXDNS_VERSION=<version> docker compose up -d

The compose file defaults to :latest; pin a version for anything you intend to keep running. To run it before a release exists, build the image yourself and name it — NXDNS_IMAGE=nxdns docker compose up -d — as docs/how-to/install-with-docker.md describes. 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/how-to/install-with-systemd.md and docs/how-to/install-with-docker.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 test --summary all                 # unit tests

The release artifacts come out of the same build graph, so the whole release build runs on a laptop exactly as it runs on the CI runner:

(cd web && npm ci && npm run build)                # required: dist refuses the placeholder
VERSION=$(sed -n 's/^[[:space:]]*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' build.zig.zon)
zig build dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \
    -Dweb-dist=web/dist -Doptimize=ReleaseSafe     # tarballs -> zig-out/dist/
zig build verify-dist -Dversion-string="$VERSION" -Dgit-commit=$(git rev-parse HEAD) \
    -Dweb-dist=web/dist -Doptimize=ReleaseSafe     # the release checks

The version comes from build.zig.zon because verify-dist asserts the two agree; a tag sets both.

That is not a claim that your tarball will hash the same as a published one. Nothing in this project measures whether two builds of the same commit on two different machines land on the same bytes, so no document here describes the build as reproducible. The gate that would settle it is a recorded deferral — specs/milestone-14.md ruling 12 — and docs/how-to/verify-a-release.md explains what a matching or differing hash is worth in the meantime.

Documentation

Start at docs/README.md, which splits the documentation into a tutorial, how-to guides, reference and explanation.

Licence

Copyright (c) 2026 Mokhtar Mial. nxdns is licensed under the European Union Public Licence v. 1.2 (EUPL-1.2); the full text is in LICENSE.

Every released tarball and image carries a THIRD-PARTY-NOTICES file assembled from the reviewed inventory in licenses/, which covers what the artifacts actually contain: musl, the Zig runtime, SQLite, Mbed TLS and its vendored Everest and p256-m code, and the JavaScript and CSS bundled into the admin UI.

S
Description
No description provided
Readme EUPL-1.2
2.4 MiB
v0.0.1
Latest
2026-08-08 23:26:44 +00:00
Languages
Zig 89.5%
TypeScript 9.8%
JavaScript 0.4%
Dockerfile 0.1%