mokhtar bcd9bce16c
Gates / frontend (push) Successful in 2m34s
Gates / test (push) Successful in 2m39s
Gates / test-aarch64 (push) Successful in 8m16s
Gates / package (push) Successful in 4m21s
Gates / container (push) Successful in 16s
CI / gates (push) Successful in 15m39s
Release / guard (push) Successful in 35s
Gates / frontend (push) Successful in 2m1s
Gates / test (push) Successful in 2m28s
Gates / test-aarch64 (push) Successful in 7m24s
Gates / package (push) Successful in 52s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m59s
Release / publish (push) Successful in 5m11s
build: bump version to 0.0.18
2026-09-09 19:25:08 +02:00
2026-09-09 19:25:08 +02:00
2026-09-09 19:23:07 +02:00
2026-09-09 19:25:08 +02:00

nxdns

nxdns is a DNS sinkhole for your LAN. It blocks the names you do not want, and forwards the rest over an encrypted connection.

flowchart LR
    devices["Your devices"] -- "DNS query" --> nxdns["nxdns"]
    nxdns -- "answer" --> devices
    nxdns -- "allowed" --> upstream["Upstream resolvers<br/>DoH / DoT"]
    upstream -- "answer" --> nxdns
    nxdns -- "blocked" --> sink["0.0.0.0 / NXDOMAIN"]

One static Zig binary. SQLite holds the state and the query log, which the web UI, the REST API and /metrics read.

Features

  • Blocklist filtering: subscribe to hosts, domain and Adblock Plus lists — whose @@ exception lines are honoured — plus your own allow and block rules, exact, wildcard (*.example.com) or regular expression
  • Two configuration modes: a database the web UI edits, or a ZON file you keep in git and converge onto at every start
  • 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)

Write 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 compose file runs nxdns run --config=/etc/nxdns/config.zon, which makes that file the configuration: every start reconciles the database onto it, and the UI refuses configuration edits. Edit the file and restart to change anything. Drop the command: line to run bare nxdns run instead, where the database is the configuration and changes go through the UI, the API, or nxdns export / nxdns import — the packaged systemd unit does that. Which mode is live is printed at every start (authority: database / authority: file (<path>)); see docs/explanation/configuration-model.md.

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 admin && npm ci && npm run build)          # web UI -> admin/dist
zig build -Dadmin-dist=admin/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 admin && 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" \
    -Dadmin-dist=admin/dist -Doptimize=ReleaseSafe     # tarballs -> zig-out/dist/
zig build verify-dist -Dversion-string="$VERSION" \
    -Dadmin-dist=admin/dist -Doptimize=ReleaseSafe     # the release checks

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

On the toolchain versions pinned at the top of .gitea/workflows/gates.yml, your tarballs hash the same as the published ones of that version. docs/how-to/verify-a-release.md has the rebuild recipe and what to check when a hash differs.

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
6.9 MiB
v0.0.21
Latest
2026-09-12 19:12:55 +00:00
Languages
Zig 82.2%
TypeScript 17.2%
JavaScript 0.3%