DNS · C11 · Dhall · one source, many targets

An authoritative DNS server,
configured by Dhall.

A small, self-contained records-only authoritative DNS server for UDP. Describe your zones in typechecked Dhall; serve them over the wire (RFC 1035), hardened for public exposure. The same C source ships two ways: a single portable APE binary and a WebAssembly build that runs right here in your browser.

One source · two targets

Compiled once for C.
Delivered two ways.

The same lexer, parser, typechecker, and wire codec (src/*.c) build into two artifacts — from your terminal to a browser tab.

Native binary — dnsd.com

APE · cosmocc

A single self-contained ~1 MB polyglot binary — an Actually Portable Executable built with cosmocc (Cosmopolitan libc). The same file runs natively, no VM, no runtime, no recompile:

  • Linux
  • macOS
  • Windows
  • FreeBSD
  • NetBSD
  • OpenBSD

…and any other x86-64 platform cosmocc targets.

A UDP listener with per-source + global token-bucket rate limiting, authoritative records-only semantics, and full bounds-checking on the remote wire path.

$ ./dnsd.com --config config.example.dhall --port 5353
$ make          # builds dnsd.com
Build & usage

In the browser — dnsd.wasm

WebAssembly

The real C server compiled to a small .wasm module (dnsd.js + dnsd.wasm). It runs 100% client-side — the actual config_load + dns_handle_query, no server, no build step. Your config and queries never leave the tab.

The demo below loads the exact config.example.dhall and answers real DNS queries — decoded into records and as raw wire-format hex.

$ make wasm     # → docs/dnsd.js + dnsd.wasm
$ node tests/wasm-smoke.js
Try the live demo ↓

WebAssembly · runs in this tab

Live demo

Edit the Dhall config (a real CodeMirror editor with Dhall highlighting), then ask a domain and record type. Press Run, Enter, or Ctrl/⌘+Enter. The actual C server (src/config.c + src/dns.c, compiled to wasm) loads the config, answers the query, and renders the decoded records plus the raw wire bytes.

compendium — in your browser wasm
loading wasm…
loading wasm…

What it does

A capable authoritative server

Records-only, authoritative, UDP — no recursion, no forwarding, no AXFR.

Record types

A, AAAA, CNAME, TXT, MX, NS, SOA, and CAA (RFC 8659).

Dhall config

Zones are described in Dhall, typechecked against their schema, evaluated at startup.

Semantics

Authoritative answer / NODATA / NXDOMAIN, ANY, and suffix name-compression.

Single binary

cosmocc → one portable dnsd.com APE binary for every major OS.

WebAssembly

The same C server compiles to wasm and runs fully client-side in this page.

Bounds-checked

Full bounds-checking on the remote wire path — no unchecked reads on attacker input.

Built for public exposure

Hardening

A DNS server on the open internet is a reflection / amplification target. compendium is deliberately conservative.

Rate limiting

Per-source token bucket (burst 100 / 20 q/s) and a global bucket (burst 500 / 100 q/s) that bounds total CPU even against a spoofed-source flood.

TC truncation

Answers capped at MAX_ANSWERS (16); the truncation (TC) bit is set when more records exist.

Response caps

Responses capped at 512 bytes (MAX_PKT); EDNS0 is ignored — no large-response amplification.

Strict rcodes

Malformed packets get FORMERR, non-query opcodes NOTIMP, non-IN class REFUSED; short/QR packets are dropped.

No recursion

Records-only and authoritative: no recursion, no forwarding, no AXFR, no open-resolver behavior.

Cached rdata

A/AAAA rdata parsed once at config load; bounded name-compression probe depth on the wire path.

Why

Why this exists

It 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.

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 is a type error before the server ever binds a port — configuration as code means configuration that's verified.

One C source, many targets

The same lexer, parser, typechecker, and wire codec compile to a single ~1 MB APE binary (dnsd.com) that runs on Linux, macOS, Windows, and the BSDs — no runtime, no interpreter — and to WebAssembly that runs the actual server, client-side, in a browser tab. You don't write DNS in C twice; you write it once and decide how to ship it.

Public servers demand conservative engineering

An authoritative nameserver on the open internet is a reflection/amplification target, so compendium is deliberately boring: rate limits, capped answers, no recursion, no EDNS0 amplification, full bounds-checking — running unprivileged under MemoryDenyWriteExecute + a seccomp allowlist with exactly one capability.

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

Build & run

Quickstart

Requires cosmocc (Cosmopolitan toolchain). The dhall-c interpreter core is a git submodule at ./dhall-c.

Native — make

git submodule update --init   # fetch dhall-c core (once)
make            # builds dnsd.com (APE) + dnsd.com.dbg (ELF)
make test       # config/lookup/query/wire/rl + live UDP

Browser — make wasm

make wasm                # → docs/dnsd.js + dnsd.wasm
node tests/wasm-smoke.js # headless smoke test

Needs emscripten clang lld llvm nodejs.

Usage

./dnsd.com --config config.example.dhall --port 5353 --address 127.0.0.1
# options: -c/--config (Dhall config, required) · -p/--port (default 5353) · -a/--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 } >
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.