Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Operations

Signals

SignalEffect
SIGTERM, SIGINTgraceful shutdown: listeners stop accepting, in-flight queries finish (5 s), the API closes, the query log is flushed, then the process exits 0
SIGHUPreload without a restart: zone files, TLS certificates for DoT/DoH, the DoT client CA bundle, blocklists and custom rules, the DNSSEC trust anchors (inline and trust_anchor_file, re-read from dnsy.toml) and a pass of DNSSEC re-signing

Zone files and certificates are also watched on disk, so SIGHUP is a “do it now”. Other changes to dnsy.toml (listen addresses, limits, logging, doh.path) need a restart; limits, block mode, cache and retention can also be changed through PATCH /settings without one. A reload that fails keeps the previous state and logs the reason.

Logging

See also Logging and metrics.

Logs go to stderr through tracing. [logging] level is trace to error; format = "json" writes one JSON object per line for log shippers. Tokens, Authorization headers, cookies, DoH path tokens, passwords and the DNSSEC key passphrase are never logged.

Health

GET /healthz answers while the process runs. GET /readyz answers 200 when the database works and every DNSSEC zone can be signed, and 503 otherwise; point load balancers and orchestrators at it.

Background work

serve runs these tasks for you: blocklist refresh, zone hot reload, re-signing of DNSSEC zones (every minute it checks for changed zones, expiring signatures and key state changes), the key rollover timer (advances key states per the zone policy), client-certificate allowlist refresh and query-log retention ([querylog] retention_days; batched, also removes expired sessions and folds days older than 48 hours into daily rollups, removes daily counter rollups older than 400 days, and runs PRAGMA optimize daily).

Query log, stats and cache

The query log and stats live in SQLite and are served by the API and UI. Flush the cache from the UI or the command line:

dnsy cache flush --token "$DNSY_TOKEN"                    # everything
dnsy cache flush example.com --subtree --token "$DNSY_TOKEN"

Dropped queries (ACL drop, UDP rate limiting) get no reply on UDP and TCP; over DoH they become 403. The exception is the UDP slip: one over-limit UDP query in limits.rate_limit_slip (default 2) gets an empty reply with TC=1 so that a real client retries over TCP.

Database and file modes

SQLite runs in WAL mode with synchronous = NORMAL (a power loss can lose the last moments of the query log, never corrupt the database) and journal_size_limit = 64 MiB. Reads use a pool of four connections, any idle one serves a request, so a slow query-log search does not queue the others behind it. The first start after an upgrade logs applying migration ... while it builds new indexes on a large query log.

data_dir is created with mode 0700 and dnsy.db (with -wal and -shm) with 0600. Existing ones are never changed. At start the server logs a warning for every TLS private key (api.tls_key, dot.key, doh.key) that group or others can read; fix it with chmod 600.

Backups

See Backup and restore for the procedure; in short:

Run dnsy db backup <file> (SQLite’s online backup; consistent and safe while the server runs), or stop the server and copy <data_dir>/dnsy.db with its -wal file. It holds users, tokens, rules, upstream groups and DNSSEC private keys; protect the copy like the keys themselves. Without DNSY_DNSSEC_PASSPHRASE or key_passphrase_file the keys are stored unencrypted. Zone files are plain JSON in [zones] dir; back them up too.

Database maintenance

dnsy db works on <data_dir>/dnsy.db without creating or migrating it:

CommandWhat it doesWhile the server runs
dnsy db check [--full]PRAGMA quick_check (--full: integrity_check); prints ok or the problems and exits non-zerosafe, read-only (a few seconds per 10 million query-log rows)
dnsy db backup <file>consistent copy via the online backup API, mode 0600, never overwritessafe
dnsy db vacuumrebuilds the file and returns free pages to the diskrefused: it blocks every writer and needs up to twice the database size in free disk, so stop the server first

vacuum detects a server (or any other process) holding the database and exits with an error instead of waiting. Retention purges free pages for reuse but do not shrink the file; vacuum only when disk space matters.

Upgrades and migrations

See Upgrade and migrations. In short:

dnsy serve and dnsy migrate apply pending migrations on start. dnsy migrate --status lists applied and pending migrations without touching the database; it opens the file with immutable=1, so it works on a read-only directory, but it ignores the -wal file and therefore shows the last checkpointed state of a database a server is writing. Migrations are never edited after release; a database newer than the binary is refused. Back up before upgrading.

Benchmarks

just bench builds the release dnsy, starts it against a fake upstream and prints a table of throughput and latency: UDP cache hit, UDP cache miss (every name unique), TCP (one persistent connection, 64 clients, 64 clients with 16 queries pipelined) and DoH (HTTP/1.1 and h2, POST). All 64 load clients connect from 127.0.0.1, so the runner raises the per-client listener limits (max_conns_per_client and udp_in_flight_per_client, see below) in its own config; against the defaults the TCP and DoH rows would be reset and UDP datagrams dropped. Every row must show 0 errors. Pin CPUs for stable numbers, for example taskset -c 0-5 just bench; the server and the load generator share the machine, so compare runs on the same host only. Reference numbers on a 24-thread workstation, unpinned: UDP hit about 310k QPS, TCP persistent connection p50 18 us, TCP pipeline 16 at 64 clients about 426k QPS.

TCP and DoT listeners set TCP_NODELAY and write each length-prefixed answer in a single write, so a client reusing a connection no longer waits for a delayed ACK (about 41 ms per answer before). just bench-micro runs the criterion micro-benchmarks (codec, cache lookup).

Threads

[server] worker_threads (default: available parallelism, which honours a CPU affinity mask) sizes the async runtime that runs the listeners and the pipeline. Argon2 and zone signing run on tokio’s blocking pool on top of that, and every SQLite write runs on one dedicated thread, dnsy-sqlite-writer, so the query-log writer queues behind nothing but other writes. A UDP datagram that the pipeline can answer without waiting (cache hit, blocked name, local zone) is answered in the receive loop of its socket; only one that waits for an upstream becomes a task.

Listener limits and UDP sockets

Admission caps are documented in Security. UDP sockets are created with a 4 MiB receive buffer (SO_RCVBUF; the kernel may clamp it to net.core.rmem_max, raise that sysctl for bursty traffic). On Linux [server] udp_sockets_per_address (default: available parallelism) binds that many sockets per UDP address with SO_REUSEPORT, each with its own receive loop; the kernel spreads clients across them by source address and port. Other platforms use one socket and ignore the key. Check for kernel drops with nstat -az UdpRcvbufErrors (or netstat -su, “receive buffer errors”): it should stay at zero under load. ss -ulnp shows the sockets.

Building with the UI

just build produces the release binary with the web UI embedded (see Install).