Files
nxdns/docs/reference/cli.md
T
mokhtar c50c6d285a
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 4m55s
CI / frontend (push) Successful in 39s
CI / cross (push) Successful in 7m57s
CI / docker (push) Failing after 1h10m42s
milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
2026-08-07 17:55:59 +02:00

9.7 KiB

CLI reference

nxdns <command> [options]

Six subcommands: run, check, export, import, version, help. Source of truth: src/cli.zig.

Every flag takes both spellings, --flag value and --flag=value. An attached value that is empty (--config=) is a missing value, not an empty path. --force is boolean and takes no value at all, so --force=1 is not a spelling of any flag this program has. A flag is rejected by the subcommand that has no use for it: --web-dev outside run is an unknown flag, not a no-op.

run

Serves DNS until SIGINT or SIGTERM.

Flag Meaning
--data-dir DIR Data directory (default /var/lib/nxdns). Created at mode 0700 if missing.
--config FILE Seed configuration file (default /etc/nxdns/config.zon). Read only when the database has never been configured.
--web-dev DIR Serve the web interface from DIR instead of the embedded assets, with no cache headers. Development only.

A seed file that is unparseable, oversized or invalid prints its diagnostics and exits 2 — the same code check and import give for the same file. See exit codes.

check

Validates the configuration and probes the upstreams. Exit 0 when it found no failures, 2 when it found one. It always reports every problem, not just the first. What it checks, in order:

  1. Which source to check (see source selection).
  2. Full validation — the same rules import enforces.
  3. For each enabled DoH/DoT listener: both PEM files are read and the key is tested against the certificate, through the same CertStore.init the listeners boot with. A file that is missing, unreadable, too large or unparseable, and a key that does not belong to the certificate, are all FAIL. The key's permissions are a separate finding: WARN when any group or other bit is set, which does not change the exit code.
  4. A live probe: one real A query for example.com through every enabled upstream, driving the same pool and failover machinery the server uses, with the same two deadlines: upstream.attempt_timeout_ms bounds one try against one upstream, upstream.total_timeout_ms the whole probe. A FAIL line names the upstream and the concrete cause recorded in its health. This probe leaves the machine, so check needs network access to pass. It runs from the command line but not from unit tests.
Flag Meaning
--data-dir DIR Data directory to look for config.db in.
--config FILE Check this file instead of the database.

Failures and warnings

Every finding carries a severity. FAIL is a problem that sets exit 2. WARN is legal configuration that is almost certainly not what was meant — a blocklist source no group links to, a TLS key readable beyond its owner — and never changes an exit code, because the service starts either way.

The last line is a summary, and it never contradicts the lines above it:

What was found Last line Exit
Nothing OK: no problems found 0
Warnings only OK: no failures found, 2 warnings1 warning in the singular 0
At least one failure No summary line; the FAIL lines are the report 2

Source selection

  • --config FILE given explicitly: check that file, nothing else.
  • Otherwise, if <data-dir>/config.db exists: check the database — the right default, since the database is the truth on a configured server. It is opened immutable, so check writes nothing to it; see what check does not do.
  • Otherwise, if the default config file path exists: check it.
  • Otherwise: "nothing to check", exit 2.

The first line of output always names which source was checked. A file larger than 4 MiB fails with larger than 4194304 bytes; a ZON syntax error is reported with its line and column.

A named file that is missing or unreadable is a finding like any other, not an I/O failure that escapes the run: FAIL <path>: no such file or FAIL <path>: not readable, exit 2. That is the same code the implicit path gives when there is nothing to check, so naming the file does not change what a missing file costs.

What check does not do

check never writes to config.db. It opens the file immutable, which means SQLite refuses every statement that would write and builds no write-ahead log, so no config.db-wal and no config.db-shm appear beside it. It does not chmod the file and it does not migrate the schema.

Two consequences are worth knowing before you read a FAIL line as damage:

  • A database behind this binary's schema is reported, not upgraded. Start the service to migrate it.

    FAIL <db>: schema version <n>, this nxdns expects <m>; `nxdns run` migrates it, `check` will not
    
  • A database with an unapplied write-ahead log cannot be graded without writing, because the newest settings are in the log and the main file holds older ones. check says so rather than reading the stale values:

    FAIL <db>: uncheckpointed changes are waiting in <db>-wal, and reading without writing would answer from the older settings in the main file; `nxdns run` applies them. A running nxdns normally holds this log, which is the usual reason to see this line.
    

    A running nxdns is the usual holder of that log, so this is a common answer when checking a live server rather than a sign of damage. It depends on what is in the log, not on whether a server is up: a configuration write that has not been checkpointed puts bytes there, and a server that has only been answering queries leaves config.db-wal empty and reads normally. Check an export with --config, or stop the service first.

export

Writes the configuration as ZON to stdout, or atomically at mode 0600 to --out FILE. --out paths are relative to the shell's working directory, not to the data directory. The output is canonical: every default emitted, deterministic ordering, no timestamps, so exportimportexport is byte-identical.

export does not create the data directory; it fails if the directory is not there. A directory that exists without a config.db is not that case: the database is opened with create semantics, so an empty config.db is created and migrated, and the export is of a default configuration.

Flag Meaning
--data-dir DIR Data directory holding config.db.
--out FILE Write to FILE instead of stdout.

See back up and restore.

import FILE

Validates FILE and replaces the whole configuration with it in one transaction. Prints every validation problem; a failed import leaves the database untouched. Refuses a database that already holds configuration unless --force is given (DatabaseNotEmpty, exit 2). The check measures what an operator set, not what the network did: client rows the DNS path materialised from traffic never trigger the refusal on their own. Creates the data directory at mode 0700 if it is missing.

Client history survives the replacement. An address the database already knew keeps its first-seen and last-seen even when FILE names it; only an address it has never seen takes the import's clock. A client FILE leaves out is removed, history included.

FILE is positional and may appear before or after the flags.

Flag Meaning
--data-dir DIR Data directory holding config.db (created if missing).
--force Replace a database that already holds configuration.

version

Prints two lines: the nxdns version with the git commit, then the Zig version the binary was built with. Takes no flags and no arguments.

help

Prints the usage text to stdout and exits 0. nxdns --help and nxdns -h do the same. A usage error prints the same text to stderr and exits 64.

Exit codes

Code Meaning
0 Success.
1 Runtime failure — I/O, database, out of memory. A partial diagnostic report caused by an allocation failure is a runtime failure, not a verdict on the configuration.
2 A configuration problem the operator can fix, or a check that found one.
64 Usage error — unknown command or flag, a flag without its value, a missing or extra argument.

Code 2 means the same thing from every subcommand. src/config/faults.zig holds the one list of errors that mean "the configuration the operator supplied is wrong", and run, check and import all ask it, so a rejected file exits 2 whichever command read it. The list is every error the validator raises, plus ParseZon, ConfigTooLarge, NoUsableUpstreams and BadCertificate. In practice that covers a seed file with a syntax error, one larger than 4 MiB, one with no default group (MissingDefaultGroup), one with no enabled upstream (NoUpstreams), a bad bind address, a bad rate limit, an unusable certificate, and password and password_hash set together.

When run exits 2 it points at the diagnosis on stderr, whether the fault came from the seed file or from the database it loaded:

run `nxdns check` to see the configuration in full

check exits 2 for those faults and also when a probed upstream failed, when a named configuration file is missing or unreadable, when the database cannot be read or is not at this binary's schema version, and when there was nothing to check. Warnings never contribute.

import exits 2 for those faults and for DatabaseNotEmpty. That last one is deliberately not a configuration fault — it reports the state of the database rather than the content of a file — and import decides it for itself; the answer to it is --force, not an edit.

OutOfMemory is exit 1 even when problems were recorded, because the report is then incomplete. Every other error is 1.

Where an exit code sends you next: troubleshoot.