Skip to content

Repository files navigation

compendium

A small, self-contained authoritative DNS server for UDP, configured in Dhall and implemented in Zig as a small set of binaries built by dhake (Dhakefile.dhall).

It links the dhall-c interpreter core (compiled to zig-out/libdhall.so) to parse, typecheck and normalize a Dhall config file describing zones and records, then serves those records authoritatively over UDP (RFC 1035).

Try it live in your browserhttps://fixpointlinux.org/compendium/ (edit the Dhall config and query a frozen prebuilt wasm build of the server, entirely client-side; the wasm build path itself has been removed).

Why

This project started as a simple question: can a real, useful network server be written in C, configured in a typed language, and shipped as one self-contained binary?

Three decisions shaped the answer:

1. The config is code — so it's typechecked. dnsd doesn't parse a config file; it interprets one. Your zones are a Dhall program, evaluated at startup by the same interpreter core this project shares with dhall-c. A typo in a record isn't a silent runtime surprise — it's a type error before the server ever binds a port. Configuration as code means configuration that's verified.

2. One engine, many targets. The same interpreter core and wire codec compile to:

  • a native server binary (zig-out/dnsd, built by zig/build.sh) that runs on Linux — no runtime, no interpreter, just the file (+ the libdhall.so seated next to it);
  • WebAssembly (dnsd.wasm) that runs the actual server, client-side, in a browser tab (a frozen prebuilt demo artifact; the wasm build path was removed and is no longer regenerable from source).

You don't write DNS twice. You write it once and decide how to ship it.

3. Public servers demand conservative engineering. An authoritative nameserver on the open internet is a reflection/amplification target. So dnsd is deliberately boring: per-source and global rate limits, answers capped with proper TC truncation, no recursion, no EDNS0 large-response amplification, full bounds-checking on the wire path — and it runs unprivileged under MemoryDenyWriteExecute + a seccomp allowlist, with exactly one capability (cap_net_bind_service).

Boring is the feature. A DNS server that never needs a CVE is one you can forget about.

Features

  • Records: A, AAAA, CNAME, TXT, MX, NS, SOA, CAA (RFC 8659), PTR (reverse DNS)
  • Semantics: authoritative answer / NODATA / NXDOMAIN, ANY, name-compression
  • Config: Dhall, typechecked against its schema, evaluated at startup
  • Build: Zig 0.16 via dhake → zig-out/dnsd (+ check binaries + libdhall.so)
  • Hardened for public exposure:
    • per-source + global token-bucket rate limiting (DoS)
    • bounded answer count (MAX_ANSWERS) with truncation (TC) bit
    • bounded name-compression probe depth, cached A/AAAA rdata, capped lookup
    • full bounds-checking on the remote wire path

Build

Requires zig (0.16.x). The dhall-c interpreter core is a git submodule at ./dhall-c. The build is driven by dhake (Dhakefile.dhall), which replaces the former Makefile and uses verified builds — each target pins the expected sha256 of its output and every source dependency, so a build fails loudly if any input or the deterministic output hashes to something unexpected.

git submodule update --init   # fetch the dhall-c core (once after clone)
./vendor/dhake/dhake.com      # builds all Zig binaries into zig-out/ (default: all)
./vendor/dhake/dhake.com all  # same, explicit
./vendor/dhake/dhake.com test # runs the golden regression harness (zig/dnsd_diff.sh)
./vendor/dhake/dhake.com dist/index.html   # build the docs site (fixpointlinux.org/compendium)
./vendor/dhake/dhake.com clean             # remove built binaries

The engine sources live in zig/src/ (one module per former C file); see zig/README.md for the layout, the golden-test harness and the re-pinning workflow after a toolchain bump.

Usage

./zig-out/dnsd --config config.example.dhall --port 5353 --address 127.0.0.1

Options:

  • -c, --config <file> — Dhall config (required)
  • -p, --port <n> — listen port (default 5353)
  • -a, --address <ip> — listen address (default 127.0.0.1)

Config format

let Record = < A     : { name : Text, ttl : Natural, value : Text }
             | AAAA  : { name : Text, ttl : Natural, value : Text }
             | CNAME : { name : Text, ttl : Natural, value : Text }
             | TXT   : { name : Text, ttl : Natural, value : Text }
             | MX    : { name : Text, ttl : Natural, priority : Natural, exchange : Text }
             | NS    : { name : Text, ttl : Natural, value : Text }
             | SOA   : { name : Text, ttl : Natural, mname : Text, rname : Text
                       , serial : Natural, refresh : Natural, retry : Natural
                       , expire : Natural, minimum : Natural }
             | CAA   : { name : Text, ttl : Natural, flags : Natural, tag : Text, value : Text }
             | PTR   : { name : Text, ttl : Natural, value : Text } >
in  let Zone   = { name : Text, records : List Record }
in  let Config = { zones : List Zone }
in  { zones = [ { name = "example.com.", records = [ < A = { name = "@", ttl = 3600, value = "192.0.2.1" } > ] } ] } : Config

Owner names are relative to the zone (@ / "" = apex); rdata target names are absolute FQDNs with a trailing dot. See config.example.dhall for a full example.

Security / DoS notes

  • Records-only authoritative: no recursion, no forwarding, no AXFR.
  • EDNS0 is ignored (no large-response amplification); responses capped at 512 bytes.
  • Rate limits (per-source burst 100 / 20 q/s; global burst 500 / 100 q/s) are tunable in zig/src/dnsd.zig.

License

MIT

About

An authoritative, records-only UDP DNS server configured by typechecked Dhall, compiled to a portable APE binary with cosmocc (and to wasm for the browser). Live demo: https://jmars.github.io/compendium/

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages