# CLI reference ``` nxdns [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. | Seeding failures are not treated as configuration faults here: a seed file that is unparseable, oversized or invalid prints its diagnostics and exits 1, not 2. See [exit codes](#exit-codes). ## `check` Validates the configuration and probes the upstreams. Exit 0 when clean, 2 when it found problems. It always reports every problem, not just the first. What it checks, in order: 1. Which source to check (see [source selection](#source-selection)). 2. Full validation — the same rules `import` enforces. 3. For each enabled DoH/DoT listener: certificate and key are readable (FAIL if not), and the key's permissions are owner-only (WARN if any group or other bit is set; the WARN 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 `upstream.total_timeout_ms` as the per-attempt deadline. 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. | ### Source selection - `--config FILE` given explicitly: check that file, nothing else. - Otherwise, if `/config.db` exists: check the database — the right default, since the database is the truth on a configured server. Pending schema migrations are applied first, so a `check` immediately after an upgrade works. - 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 missing file is treated differently depending on how the path was reached. An explicit `--config FILE` that does not exist is an I/O failure: `check failed: FileNotFound` on stderr, exit 1. A default path that does not exist is one of the cases above and produces "nothing to check", exit 2. ## `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 `export` → `import` → `export` 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](../how-to/back-up-and-restore.md). ## `import FILE` Validates FILE and replaces the whole configuration with it in one transaction. Prints every validation problem on failure; a failed import leaves the database untouched. Refuses a database that already has content unless `--force` is given (`DatabaseNotEmpty`, exit 2). Creates the data directory at mode 0700 if it is missing. `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 has content. | ## `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. Which faults qualify differs per subcommand; see below. | | 64 | Usage error — unknown command or flag, a flag without its value, a missing or extra argument. | Code 2 is not a single rule shared by every subcommand. `check` and `import` classify configuration faults; `run` maps only four errors to it and lets everything else out as a runtime failure. `check` exits 2 when validation reported at least one problem, when a certificate or key named by an enabled listener is unreadable, when a probed upstream failed, when the file is larger than 4 MiB, when the file has a ZON syntax error, and when there was nothing to check. Any other error escaping the run — a missing explicit `--config` file, an unreadable database — prints `check failed: ` and exits 1. `import` exits 2 when validation recorded at least one problem, and for `DatabaseNotEmpty`, `ConfigTooLarge`, `ParseZon` and `PasswordAndHashBothSet`. `OutOfMemory` is 1 even when problems were recorded, because the report is then incomplete. Every other error is 1. `run` exits 2 for `NoUsableUpstreams`, `BadBindAddress`, `BadRateLimit` and `BadCertificate`, and points the operator at `nxdns check` on stderr when it does. Nothing else is remapped. In particular, seeding the database from `--config` on first start happens before that classification applies to it: a seed file that is unparseable (`ParseZon`), larger than 4 MiB (`ConfigTooLarge`), or rejected by validation prints its diagnostics to stderr and exits 1. The same file given to `check` or `import` exits 2. Where an exit code sends you next: [troubleshoot](../how-to/troubleshoot.md).