milestone 13 discrepancies: redact credentials from urls in logs, metrics and cli output
This commit is contained in:
@@ -59,11 +59,35 @@ time you look at the UI, rather than "my edit was applied and then quietly
|
||||
undone next Tuesday".
|
||||
|
||||
The emptiness check is a real query over the content tables, not a flag: the
|
||||
database counts as configured when any content table has rows, or when the
|
||||
default group has been altered. One consequence is worth knowing, because it
|
||||
is not obvious: clients are auto-materialised when they first send a query, so
|
||||
a server that has answered even one query is "configured" and will ignore a
|
||||
seed file placed there afterwards.
|
||||
database counts as configured when a content table holds rows, or when the
|
||||
default group has been altered. What it measures is intent, not activity, and
|
||||
the client table is where those two come apart. Clients are auto-materialised
|
||||
when they first send a query — the DNS path writes a row per device it sees —
|
||||
and such a row records what the network did, not what an operator decided. It
|
||||
carries `hand_edited = 0`, the emptiness check counts only the `hand_edited = 1`
|
||||
rows, and `export` omits them. So answering queries never turns an unconfigured
|
||||
database into a configured one; naming a device does.
|
||||
|
||||
Counting traffic here would have been a quiet trap: a server that resolved one
|
||||
name would have declared itself configured and ignored a seed file placed
|
||||
afterwards, and the operator would have had no line of output saying why.
|
||||
|
||||
The same distinction survives an import. `import` replaces the content in one
|
||||
transaction, which empties `clients` along with every other table, so every
|
||||
client row is saved before the wipe. The materialised ones are put back after it
|
||||
unchanged, and restoring a backup does not make the server forget the devices it
|
||||
has met.
|
||||
|
||||
An address the imported file names belongs to the file — the operator's
|
||||
statement wins over the discovered row — with one carve-out. First-seen and
|
||||
last-seen are not configuration: they record when a device was heard from, the
|
||||
configuration model has no field for either, and an import is not a query. So
|
||||
they follow the address rather than the row. If the database already knew that
|
||||
address, its two timestamps are carried onto the new row; only an address the
|
||||
database has never seen takes the import's clock. Without that, re-importing a
|
||||
backup would stamp every device the operator had bothered to name as though it
|
||||
had just arrived — and those are exactly the devices whose history is worth
|
||||
something.
|
||||
|
||||
## The round trip
|
||||
|
||||
@@ -90,11 +114,17 @@ difference is a configuration change, never noise from when the export ran.
|
||||
|
||||
`nxdns import` replaces the whole database content in one transaction. Not a
|
||||
merge, not a patch: delete every content table in foreign-key-safe order, then
|
||||
insert what the file says. A failed import — bad syntax, failed validation, a
|
||||
constraint violation halfway through — leaves the database exactly as it was,
|
||||
because everything happens inside a single `BEGIN IMMEDIATE`.
|
||||
insert what the file says — with the one exception described above. Every client
|
||||
row is lifted out first. The materialised ones are put back rather than
|
||||
recreated from a file that never held them, and the observed timestamps of an
|
||||
address the file *does* name are merged onto its new row. A client the file
|
||||
leaves out is gone, history included: nothing puts a `hand_edited = 1` row back.
|
||||
A failed import — bad syntax, failed
|
||||
validation, a constraint violation halfway through — leaves the database exactly
|
||||
as it was, because everything happens inside a single `BEGIN IMMEDIATE`.
|
||||
|
||||
Without `--force`, import refuses a database that already has content. That
|
||||
Without `--force`, import refuses a database that already holds configuration.
|
||||
That
|
||||
check runs *inside* the transaction, after the lock is taken, so it cannot be
|
||||
raced by a concurrent write. The effect is that a plain `import` can never
|
||||
clobber a configured server by accident, and clobbering it deliberately takes
|
||||
|
||||
@@ -15,8 +15,8 @@ PLAN §18 sets five:
|
||||
- sustained ≥ 100 qps on a Raspberry Pi 5;
|
||||
- blocklist lookup p95 < 1 ms;
|
||||
- cached response p95 < 5 ms;
|
||||
- memory with ~1M blocked domains < 100 MB;
|
||||
- stripped static binary < 10 MB per arch, < 15 MB with the embedded frontend.
|
||||
- memory with ~1M blocked domains < 100 MiB;
|
||||
- stripped static binary < 10 MiB per arch, < 15 MiB with the embedded frontend.
|
||||
|
||||
They are household-scale numbers, and they are deliberately unambitious. 100
|
||||
qps is far more than a house generates; the point of the target is not speed
|
||||
|
||||
@@ -88,8 +88,8 @@ before writing to it.
|
||||
|
||||
## Restore over an existing database
|
||||
|
||||
`import` refuses a database that already has content, so a plain `import` can
|
||||
never clobber a configured server by accident:
|
||||
`import` refuses a database that already holds configuration, so a plain
|
||||
`import` can never clobber a configured server by accident:
|
||||
|
||||
```sh
|
||||
nxdns import /tmp/nxdns-lab/backup.zon --data-dir /tmp/nxdns-lab/data
|
||||
@@ -112,6 +112,13 @@ imported /tmp/nxdns-lab/backup.zon
|
||||
Stop the server first. `import` replaces the whole configuration underneath a
|
||||
process that has already read it, and a running server will not notice.
|
||||
|
||||
What `--force` does to the client list is worth knowing before you restore an
|
||||
old backup. A client the backup does not name is removed, and its first-seen
|
||||
and last-seen go with it — restoring a month-old file drops the devices you
|
||||
named since. Devices the server discovered from traffic are kept, and a device
|
||||
the backup does name keeps the first-seen and last-seen the database already
|
||||
held, so a restore does not restamp your whole network as newly arrived.
|
||||
|
||||
On a real install that is the systemd unit:
|
||||
|
||||
```sh
|
||||
@@ -185,8 +192,9 @@ The data directory layout is in
|
||||
## A backup before every upgrade
|
||||
|
||||
There is no downgrade path. Schema migrations run forward automatically at
|
||||
startup and before `check`, `export` and `import`; nothing walks them back. Take
|
||||
an export before installing a new binary — see [upgrade](upgrade.md).
|
||||
startup and before `export` and `import`; nothing walks them back, and `nxdns
|
||||
check` does not run them at all. Take an export before installing a new binary —
|
||||
see [upgrade](upgrade.md).
|
||||
|
||||
Every command on this page was executed on this host as written, except the
|
||||
`systemctl` block marked **Not verified on this host** above.
|
||||
|
||||
@@ -75,23 +75,40 @@ nxdns check --data-dir /tmp/nxdns-lab/data --config /tmp/nxdns-lab/etc/config.zo
|
||||
|
||||
```
|
||||
checking configuration file /tmp/nxdns-lab/etc/config.zon
|
||||
OK https://cloudflare-dns.com/dns-query
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no problems found
|
||||
```
|
||||
|
||||
`check` reads both endpoints' certificate and key. An unreadable file is a
|
||||
failure and exits 2:
|
||||
`check` loads both endpoints' certificate and key the same way the listeners do,
|
||||
so what passes here will start. An unreadable file is a failure and exits 2:
|
||||
|
||||
```
|
||||
FAIL doh_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem' is not readable
|
||||
FAIL dot_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem' is not readable
|
||||
FAIL doh_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem': certificate file is not readable
|
||||
FAIL dot_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem': certificate file is not readable
|
||||
```
|
||||
|
||||
A key readable by anyone but its owner is a warning, and does not change the
|
||||
exit code, because the service still starts:
|
||||
So is a key that does not belong to the certificate, which is the mistake worth
|
||||
catching before a restart — the two files are individually valid and only their
|
||||
pairing is wrong. mbedTLS writes its own line to stderr as it rejects the pair:
|
||||
|
||||
```
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
checking configuration file /tmp/nxdns-lab/etc/mismatch.zon
|
||||
FAIL doh_server.key_path: '/tmp/nxdns-lab/etc/other.pem': private key does not belong to the certificate
|
||||
FAIL dot_server.key_path: '/tmp/nxdns-lab/etc/other.pem': private key does not belong to the certificate
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
```
|
||||
|
||||
A key readable by anyone but its owner is a warning instead. It does not change
|
||||
the exit code, because the service still starts, and the summary line counts it
|
||||
rather than claiming nothing was found:
|
||||
|
||||
```
|
||||
WARN doh_server.key_path: '/tmp/nxdns-lab/etc/key.pem' is mode 644; a TLS key must be readable by its owner only
|
||||
WARN dot_server.key_path: '/tmp/nxdns-lab/etc/key.pem' is mode 644; a TLS key must be readable by its owner only
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no failures found, 2 warnings
|
||||
```
|
||||
|
||||
## 4. Start and confirm the listeners
|
||||
|
||||
@@ -73,13 +73,28 @@ database has nothing to forward to. The log is
|
||||
`no configuration file at '/etc/nxdns/config.zon'; using the database as it is`
|
||||
followed by `nxdns run failed: NoUsableUpstreams`.
|
||||
|
||||
A file that is present but rejected is a different failure and a different exit
|
||||
A file that is present but rejected is a different failure with the same exit
|
||||
code. No `default` group, no enabled upstream, a syntax error — `run` prints the
|
||||
diagnostic and exits **1**, for example `nxdns run failed: MissingDefaultGroup`.
|
||||
Both were run here against this image: a seed file whose only group was named
|
||||
`other` exited 1, and an empty `/etc/nxdns` exited 2 with `NoUsableUpstreams`.
|
||||
Under `restart: unless-stopped` either one is a restart loop, so read the exit
|
||||
code from `docker inspect` to tell them apart; see
|
||||
diagnostic and exits 2 as well. Both were run here against this image. A seed
|
||||
file whose only group was named `other`:
|
||||
|
||||
```
|
||||
FAIL groups: no group named 'default'; every unknown client is assigned to it
|
||||
nxdns run failed: MissingDefaultGroup
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
and an empty `/etc/nxdns`:
|
||||
|
||||
```
|
||||
info(config_bootstrap): no configuration file at '/etc/nxdns/config.zon'; using the database as it is
|
||||
nxdns run failed: NoUsableUpstreams
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
Under `restart: unless-stopped` either one is a restart loop, and the exit code
|
||||
alone no longer tells them apart: read the lines above the failure, which either
|
||||
name the diagnostic in the file or say there was no file at all. See
|
||||
[Troubleshoot nxdns](troubleshoot.md).
|
||||
|
||||
The container runs as uid 65532, and the mount is read-only, so the container
|
||||
|
||||
@@ -152,7 +152,7 @@ A good file prints the source it checked, one `OK` line per upstream, and
|
||||
|
||||
```
|
||||
checking configuration file /etc/nxdns/config.zon
|
||||
OK https://cloudflare-dns.com/dns-query
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no problems found
|
||||
```
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ nxdns export --data-dir /tmp/nxdns-lab/data | grep password
|
||||
.password_hash = "$argon2id$v=19$m=19456,t=2,p=1$kvlRj1tdGul3MlfbvzLncLKWirNpJRJ3howFA9/ysgg$7elW7PPQ3WXHwI4YOmOpZ/1KNEQo7ZDLRJhnYOPMjqw",
|
||||
```
|
||||
|
||||
`--force` is required because the database already has content. See
|
||||
`--force` is required because the database already holds configuration. See
|
||||
[back up and restore](back-up-and-restore.md).
|
||||
|
||||
## What happens with no password set
|
||||
|
||||
+74
-29
@@ -21,8 +21,9 @@ nxdns run failed: NoUsableUpstreams
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
Exit 2 is reserved for a small set of faults `run` raises itself:
|
||||
`NoUsableUpstreams`, `BadBindAddress`, `BadRateLimit` and `BadCertificate`.
|
||||
Exit 2 means the configuration is wrong and you can fix it. Every subcommand
|
||||
uses the same definition, so a file `run` exits 2 on exits 2 from `check` and
|
||||
`import` too.
|
||||
|
||||
**Diagnosis.**
|
||||
|
||||
@@ -45,18 +46,18 @@ checked on its first line.
|
||||
certificate. `run` names both paths before it exits:
|
||||
`doh_server: '<cert>' + '<key>': certificate file is not readable`.
|
||||
|
||||
**`check` does not catch most of this.** It tests only that each file is
|
||||
readable, and warns when the key is readable beyond its owner; it never opens
|
||||
the PEM. Parsing and the key/certificate pairing happen when `run` builds the
|
||||
TLS context, so `check` can print `OK: no problems found` on a configuration
|
||||
`run` then refuses. Reproduced here with a self-signed pair and the key from a
|
||||
second, unrelated pair:
|
||||
`check` catches this without starting a listener. It loads both PEM files and
|
||||
tests the key against the certificate through the same code `run` uses, so it
|
||||
fails on exactly what `run` would fail on. Reproduced here with a self-signed
|
||||
pair and the key from a second, unrelated pair:
|
||||
|
||||
```
|
||||
$ nxdns check --config config.zon
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
checking configuration file config.zon
|
||||
OK https://cloudflare-dns.com/dns-query
|
||||
OK: no problems found # exit 0
|
||||
FAIL doh_server.key_path: 'mismatched-key.pem': private key does not belong to the certificate
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
# exit 2
|
||||
|
||||
$ nxdns run --config config.zon --data-dir ./data
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
@@ -64,59 +65,103 @@ checked on its first line.
|
||||
nxdns run failed: BadCertificate # exit 2
|
||||
```
|
||||
|
||||
A cert file containing `not a certificate` behaves the same way — `check`
|
||||
exits 0, `run` exits 2 with `certificate PEM could not be parsed`. So a
|
||||
successful `check` means the paths and permissions are right, not that the
|
||||
certificate is usable; the only test of that is starting the service. Fix the
|
||||
path, the ownership, or the pair; see
|
||||
The `warning(tls_server)` line comes from mbedTLS on stderr and can appear
|
||||
before the `checking` line, which is on stdout. A cert file containing
|
||||
`not a certificate` fails the same way, with
|
||||
`FAIL doh_server.cert_path: 'junk.pem': certificate PEM could not be parsed`.
|
||||
An unreadable file reads
|
||||
`FAIL doh_server.cert_path: '<path>': certificate file is not readable`.
|
||||
|
||||
Fix the path, the ownership, or the pair; see
|
||||
[Enable DoH and DoT](enable-doh-and-dot.md).
|
||||
- `BadRateLimit` — a rate limit or window is zero. `import` refuses such a
|
||||
configuration, so this only reaches a database that was edited by hand.
|
||||
- `BadBindAddress` — `dns.bind_ipv4` or `dns.bind_ipv6` is not an address of
|
||||
that family.
|
||||
|
||||
## The service exits with code 1 on a seed file you just wrote
|
||||
## A seed file you just wrote is rejected
|
||||
|
||||
**Symptom.** A first start against an empty database prints the validation
|
||||
problem and stops, but with exit code 1, not 2:
|
||||
problem and stops with exit 2:
|
||||
|
||||
```
|
||||
groups: no group named 'default'; every unknown client is assigned to it
|
||||
FAIL groups: no group named 'default'; every unknown client is assigned to it
|
||||
nxdns run failed: MissingDefaultGroup
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
A syntax error behaves the same way:
|
||||
|
||||
```
|
||||
config: 2:42: error: expected ',' after initializer
|
||||
FAIL config: 3:16: error: expected ',' after initializer
|
||||
nxdns run failed: ParseZon
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
So does a seed file whose upstream list is empty or all disabled:
|
||||
|
||||
```
|
||||
upstreams: at least one upstream must be enabled
|
||||
FAIL upstreams: at least one upstream must be enabled
|
||||
nxdns run failed: NoUpstreams
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
`NoUpstreams` from a seed file is not the same fault as `NoUsableUpstreams`
|
||||
above: the first is a file `run` refused, the second is a database `run`
|
||||
accepted and found empty. Only the second is exit 2.
|
||||
accepted and found empty. Both are exit 2.
|
||||
|
||||
**Diagnosis.** Run the same file through `check`, which reports it as a
|
||||
configuration problem and exits 2:
|
||||
**Diagnosis.** Run the same file through `check`, which reports the same
|
||||
problems and exits 2:
|
||||
|
||||
```sh
|
||||
nxdns check --config /etc/nxdns/config.zon
|
||||
```
|
||||
|
||||
**Fix.** Correct the file the diagnostics name and start again. The database is
|
||||
still empty after a failed seed, so the next start re-reads the file. Note that
|
||||
`nxdns check` and `nxdns import` of the same bad file exit 2 while `nxdns run`
|
||||
exits 1 — the exit code differs by command, the diagnostics do not. All three
|
||||
commands were run here against a file missing its `default` group, one with a
|
||||
syntax error and one with no enabled upstream, and every pair came out that
|
||||
way.
|
||||
still empty after a failed seed, so the next start re-reads the file. The exit
|
||||
code no longer depends on which command read the file: all three of these files
|
||||
were run through `run`, `check` and `import` here, and every one of the nine
|
||||
combinations exited 2 with the same diagnostic.
|
||||
|
||||
## `nxdns check` fails on a server that is running fine
|
||||
|
||||
**Symptom.** The service is up and answering, but `nxdns check` on the same
|
||||
machine exits 2 with one long line about a write-ahead log:
|
||||
|
||||
```
|
||||
checking database /var/lib/nxdns/config.db
|
||||
FAIL /var/lib/nxdns/config.db: uncheckpointed changes are waiting in /var/lib/nxdns/config.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.
|
||||
```
|
||||
|
||||
Nothing is damaged. `check` opens `config.db` immutable so that it can never
|
||||
write to it, and an immutable open ignores the write-ahead log. When that log
|
||||
holds bytes, the newest settings are in it and the main file holds older ones,
|
||||
so `check` refuses rather than grade stale values.
|
||||
|
||||
The log holds bytes after a configuration write that has not been checkpointed
|
||||
yet, which on a running server means someone changed something through the web
|
||||
interface or the API. A server that has only been answering queries has an empty
|
||||
`config.db-wal` and `check` reads it normally — so this line comes and goes, and
|
||||
its absence is not proof that nothing is running.
|
||||
|
||||
**Fix.** Check the exported configuration instead of the live file:
|
||||
|
||||
```sh
|
||||
nxdns export --data-dir /var/lib/nxdns --out /tmp/current.zon
|
||||
nxdns check --config /tmp/current.zon
|
||||
```
|
||||
|
||||
`export` opens the database read/write and does see the log, so it renders the
|
||||
settings that are actually in force. Stopping the service and checking again
|
||||
works too: a clean shutdown checkpoints the log away.
|
||||
|
||||
> Reproduced here on a scratch data directory rather than `/var/lib/nxdns` —
|
||||
> that path is the only substitution in the output above. nxdns was started on
|
||||
> unprivileged ports; `config.db-wal` was 0 bytes and `check` exited 0; one
|
||||
> `POST /api/blocklists` took it to 8272 bytes and `check` then printed the line
|
||||
> above and exited 2; `export` from the same live directory succeeded and its
|
||||
> output checked clean; and after a clean shutdown `check --data-dir` exited 0
|
||||
> again.
|
||||
|
||||
## Port 53 is already taken
|
||||
|
||||
|
||||
+44
-22
@@ -108,43 +108,63 @@ configuration.
|
||||
|
||||
```sh
|
||||
nxdns version
|
||||
nxdns check
|
||||
nxdns export --out /tmp/after-upgrade.zon
|
||||
nxdns check --config /tmp/after-upgrade.zon
|
||||
dig @127.0.0.1 example.com A +short
|
||||
```
|
||||
|
||||
`nxdns check` with no `--config` checks the database, which is what you want
|
||||
after an upgrade — it names its source on the first line and migrates a
|
||||
database that is one schema version behind before checking it:
|
||||
The restart in step 3 is what migrated the database, so by now the schema is
|
||||
current and the service is answering. Confirming with `nxdns check` alone would
|
||||
not work here, and the reason is worth knowing: `check` opens `config.db`
|
||||
immutable so it can never write to it, and the migration you just performed is
|
||||
sitting in `config.db-wal` waiting to be checkpointed. Rather than read around
|
||||
the log and grade older settings, `check` reports it:
|
||||
|
||||
```
|
||||
checking database /var/lib/nxdns/config.db
|
||||
OK https://cloudflare-dns.com/dns-query
|
||||
FAIL /var/lib/nxdns/config.db: uncheckpointed changes are waiting in /var/lib/nxdns/config.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.
|
||||
```
|
||||
|
||||
`export` opens the database read/write and does see the log, so exporting and
|
||||
then checking the export validates what is actually in force:
|
||||
|
||||
```
|
||||
checking configuration file /tmp/after-upgrade.zon
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no problems found
|
||||
```
|
||||
|
||||
> Verified on this host for the first two commands, with `--data-dir` pointing
|
||||
> at the scratch data directory instead of `/var/lib/nxdns`:
|
||||
> Verified on this host for the first three commands, with `--data-dir`
|
||||
> pointing at a scratch data directory instead of `/var/lib/nxdns` — that path
|
||||
> is the only difference from the blocks above:
|
||||
>
|
||||
> ```
|
||||
> $ nxdns version
|
||||
> nxdns 0.1.0-dev (unknown)
|
||||
> zig 0.16.0
|
||||
> $ nxdns check --data-dir $SCRATCH/data
|
||||
> checking database /…/scratchpad/data/config.db
|
||||
> OK https://cloudflare-dns.com/dns-query
|
||||
> OK: no problems found
|
||||
> ```
|
||||
>
|
||||
> The database path on the first line is the only difference from the block
|
||||
> above. The `dig` line was not run in this round: nothing is listening on
|
||||
> 127.0.0.1:53 here, and port 53 needs root.
|
||||
> Against a running server whose database had just been migrated and seeded,
|
||||
> `nxdns check --data-dir` printed the uncheckpointed-log line above and exited
|
||||
> 2, while `nxdns export --out` followed by `nxdns check --config` on the result
|
||||
> exited 0 with `OK: no problems found`. The `dig` line was not run in this
|
||||
> round: nothing is listening on 127.0.0.1:53 here, and port 53 needs root.
|
||||
|
||||
## What happens to the database
|
||||
|
||||
Migrations run at startup, and also before `check`, `export` and `import`, so
|
||||
whichever of those you run first performs the upgrade. A fresh database is
|
||||
created at the current schema version; an older one is stepped up to it. The
|
||||
log line names both versions:
|
||||
Migrations run at startup, and also before `export` and `import`, so whichever
|
||||
of those you run first performs the upgrade. `nxdns check` is the exception: it
|
||||
opens the database immutable and never migrates, so on a database still one
|
||||
version behind it reports the mismatch and exits 2 rather than fixing it:
|
||||
|
||||
```
|
||||
FAIL /var/lib/nxdns/config.db: schema version 0, this nxdns expects 2; `nxdns run` migrates it, `check` will not
|
||||
```
|
||||
|
||||
That line was reproduced here against a database stamped at version 0; the path
|
||||
and the version numbers are what vary.
|
||||
|
||||
A fresh database is created at the current schema version; an older one is
|
||||
stepped up to it. The log line names both versions:
|
||||
|
||||
```
|
||||
info(migrations): config.db migrated from schema version 0 to 2
|
||||
@@ -194,9 +214,11 @@ nxdns import config-backup.zon --force
|
||||
systemctl start nxdns
|
||||
```
|
||||
|
||||
`--force` is required here. A plain `import` into a database that already has
|
||||
content fails with `import failed: DatabaseNotEmpty` and exits 2, so it cannot
|
||||
clobber a configured server by accident.
|
||||
`--force` is required here. A plain `import` into a database that already holds
|
||||
configuration fails with `import failed: DatabaseNotEmpty` and exits 2, so it
|
||||
cannot clobber a configured server by accident. What counts is what an operator
|
||||
set: client rows the DNS path materialised from traffic never trigger the
|
||||
refusal on their own.
|
||||
|
||||
> Verified on this host for the two `nxdns` lines, against a populated scratch
|
||||
> data directory:
|
||||
|
||||
+105
-42
@@ -23,21 +23,24 @@ Serves DNS until SIGINT or SIGTERM.
|
||||
| `--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).
|
||||
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](#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:
|
||||
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](#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).
|
||||
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
|
||||
`upstream.total_timeout_ms` as the per-attempt deadline. A FAIL line names
|
||||
@@ -50,13 +53,28 @@ checks, in order:
|
||||
| `--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 warnings` — `1 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. Pending
|
||||
schema migrations are applied first, so a `check` immediately after an upgrade
|
||||
works.
|
||||
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](#what-check-does-not-do).
|
||||
- Otherwise, if the default config file path exists: check it.
|
||||
- Otherwise: "nothing to check", exit 2.
|
||||
|
||||
@@ -64,10 +82,42 @@ 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.
|
||||
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`
|
||||
|
||||
@@ -92,17 +142,24 @@ 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.
|
||||
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 has content. |
|
||||
| `--force` | Replace a database that already holds configuration. |
|
||||
|
||||
## `version`
|
||||
|
||||
@@ -120,31 +177,37 @@ the same. A usage error prints the same text to stderr and exits 64.
|
||||
| --- | --- |
|
||||
| 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. |
|
||||
| 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 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.
|
||||
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.
|
||||
|
||||
`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: <Error>` and exits 1.
|
||||
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:
|
||||
|
||||
`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 `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
`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.
|
||||
`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](../how-to/troubleshoot.md).
|
||||
|
||||
@@ -115,7 +115,7 @@ The DNS-over-HTTPS listener (server side, for clients on the LAN). See
|
||||
| `doh_server.enabled` | bool | false | — | — | gates the DoH listener (`src/server/doh_server.zig`) |
|
||||
| `doh_server.bind` | string | `"0.0.0.0"` | IP address | must parse as an IP address of either family | DoH listener bind |
|
||||
| `doh_server.port` | u16 | 443 | port | 1–65535 (0 is refused, enabled or not) | DoH listener port |
|
||||
| `doh_server.cert_path` | string | `"/etc/nxdns/cert.pem"` | path | non-empty when enabled | certificate loaded into the hot-reloading `CertStore`; readability is checked by `nxdns check`, not by the validator |
|
||||
| `doh_server.cert_path` | string | `"/etc/nxdns/cert.pem"` | path | non-empty when enabled | certificate loaded into the hot-reloading `CertStore`; the file itself is loaded and paired with the key by `nxdns check`, not by the validator |
|
||||
| `doh_server.key_path` | string | `"/etc/nxdns/key.pem"` | path | non-empty when enabled | private key for the same; `nxdns check` warns when it is readable beyond its owner |
|
||||
|
||||
### dot_server
|
||||
@@ -169,7 +169,7 @@ the client tracker and the blocklist scheduler are throttled
|
||||
| Key | Type | Default | Unit | Validation | Consumed by |
|
||||
|---|---|---|---|---|---|
|
||||
| `blocklist_update.enabled` | bool | true | — | — | blocklist refresh scheduler (`src/filter/manager.zig`); when false the scheduler stops after the startup pass and only a manual refresh runs |
|
||||
| `blocklist_update.interval_hours` | u16 | 24 | hours | at least 1 | sleep between refresh passes and the per-source staleness test |
|
||||
| `blocklist_update.interval_hours` | u16 | 24 | hours | at least 1 | sleep between refresh passes and the per-source staleness test; each pass is preceded by the [orphan sweep](files-and-directories.md#the-orphan-sweep), which runs even when the pass itself is skipped for disk space |
|
||||
|
||||
## Collections
|
||||
|
||||
@@ -329,9 +329,35 @@ authentication](../how-to/set-up-admin-authentication.md).
|
||||
|
||||
## Validation errors
|
||||
|
||||
`nxdns check` and `nxdns import` print one `path: message` line per problem and
|
||||
report every problem, not just the first. The error set is
|
||||
`validate.ValidateError` in `src/config/validate.zig`:
|
||||
`nxdns check`, `nxdns import` and the `nxdns run` that seeds a database from the
|
||||
file all print one `FAIL path: message` line per problem, and report every
|
||||
problem rather than the first. A finding that is legal but almost certainly
|
||||
unintended is prefixed `WARN` instead: it does not change the exit code, and it
|
||||
is printed by all three even when nothing failed, so an accepted configuration
|
||||
still says what is odd about it.
|
||||
|
||||
A url in a diagnostic is redacted to its scheme, host and port. The userinfo,
|
||||
the path, the query and the fragment are dropped, and control characters are
|
||||
escaped. These lines reach the journal, and every one of those parts can carry a
|
||||
credential: a NextDNS DoH upstream is `https://dns.nextdns.io/abcd12`, where the
|
||||
path segment is the whole account identifier. The field path beside the message
|
||||
names the entry, so `blocklist_sources[1].url` still says which one to go and
|
||||
fix.
|
||||
|
||||
One credential this cannot remove: a NextDNS **DoT** upstream is
|
||||
`tls://abcd12.dns.nextdns.io`, which carries the same identifier in the hostname.
|
||||
Stripping it would leave no host at all and no line worth reading. So if you use
|
||||
NextDNS over DoT, your profile id appears in the log.
|
||||
|
||||
The host is kept because a hostname is not a secret in the general case — it is
|
||||
resolved publicly and offered as SNI on every connection — and dropping it would
|
||||
cost every operator a diagnostic to cover one vendor's choice. If that trade is
|
||||
wrong for you, it is yours to make rather than ours: NextDNS also publishes a DoH
|
||||
endpoint, `https://dns.nextdns.io/abcd12`, whose identifier sits in the path and
|
||||
is redacted in full. Configure that form instead and nothing identifying reaches
|
||||
the log.
|
||||
|
||||
The error set is `validate.ValidateError` in `src/config/validate.zig`:
|
||||
|
||||
| Error | Raised by |
|
||||
| --- | --- |
|
||||
@@ -363,6 +389,13 @@ report every problem, not just the first. The error set is
|
||||
| `MissingLogPath` | `logging.output = .file` with an empty or relative `file_path` |
|
||||
| `PasswordAndHashBothSet` | both `web.password` and `web.password_hash` are set |
|
||||
|
||||
Warnings are a separate set, outside `ValidateError` because they are not
|
||||
failures. There is one:
|
||||
|
||||
| Warning | Raised by |
|
||||
| --- | --- |
|
||||
| `SourceInNoGroup` | a `blocklist_sources` entry that no `group_sources` link names; it downloads and blocks nothing until a group uses it |
|
||||
|
||||
## Minimal working example
|
||||
|
||||
The smallest file that passes validation: a `default` group and one enabled
|
||||
|
||||
@@ -16,19 +16,31 @@ never looks at the directory, and without one `check` falls back to the default
|
||||
configuration file, or prints "nothing to check" and exits 2 when neither source
|
||||
exists.
|
||||
|
||||
No subcommand opens the directory read-only. `run`, `import`, `export` and a
|
||||
`check` that resolved to the database all go through the same
|
||||
`DataDir.openConfigDb`, which opens `config.db` read/write, chmods it to 0600,
|
||||
enables WAL — creating `config.db-wal` and `config.db-shm` — and then runs any
|
||||
pending schema migrations. So `nxdns export` and `nxdns check` write to the data
|
||||
directory, and on a database one schema version behind they migrate it. `export`
|
||||
additionally creates an empty `config.db` if the directory exists without one;
|
||||
`check` reaches the database branch only when `config.db` is already there.
|
||||
`run`, `import` and `export` go through `DataDir.openConfigDb`, which opens
|
||||
`config.db` read/write, chmods it to 0600, enables WAL — creating
|
||||
`config.db-wal` and `config.db-shm` — and then runs any pending schema
|
||||
migrations. So `nxdns export` writes to the data directory, and on a database
|
||||
one schema version behind it migrates it. `export` also creates an empty
|
||||
`config.db` if the directory exists without one.
|
||||
|
||||
`nxdns check` is the exception: it does not use that path at all. It opens
|
||||
`config.db` immutable, which is `SQLITE_OPEN_READONLY` plus `immutable=1`, so
|
||||
SQLite refuses every statement that would write and builds no wal-index. No
|
||||
`config.db-wal` and no `config.db-shm` appear beside the file, the mode is left
|
||||
alone, and no migration runs — a database behind this binary's schema is
|
||||
reported as a failure naming `nxdns run` as the fix. A `check` against a data
|
||||
directory leaves it byte-identical, and `check` reaches the database branch only
|
||||
when `config.db` is already there.
|
||||
|
||||
`immutable=1` ignores any `-wal` file, so it is refused rather than used when
|
||||
one holds bytes: the newest settings would be invisible and `check` would grade
|
||||
older ones from the main file. That is the "uncheckpointed changes" failure in
|
||||
[the CLI reference](cli.md#what-check-does-not-do).
|
||||
|
||||
| Path | What it is | Mode |
|
||||
| --- | --- | --- |
|
||||
| `config.db` | The configuration database — the single source of truth, including `web.password_hash`. | 0600 |
|
||||
| `config.db-wal`, `config.db-shm` | SQLite write-ahead log and shared-memory index for `config.db`. Created when WAL is enabled, inheriting the main file's permissions. | 0600 |
|
||||
| `config.db-wal`, `config.db-shm` | SQLite write-ahead log and shared-memory index for `config.db`. Created by `run`, `import` and `export` when WAL is enabled, inheriting the main file's permissions. `check` creates neither. | 0600 |
|
||||
| `querylog.db` | The query log: every domain every client asked for. Expendable — if it is missing or unusable it is recreated empty. | 0600 |
|
||||
| `querylog.db-wal`, `querylog.db-shm` | WAL sidecars for `querylog.db`. | 0600 |
|
||||
| `querylog.db.corrupt-<unix-seconds>` | A `querylog.db` that could not be used, moved aside before an empty one was created in its place. Kept, never overwritten. | Whatever the renamed file had — no chmod reaches it |
|
||||
@@ -38,11 +50,45 @@ additionally creates an empty `config.db` if the directory exists without one;
|
||||
| `blocklists/<id>.wild` | Wildcard entries for the same source. | 0600 |
|
||||
| `blocklists/<id>.raw.tmp`, `<id>.list.tmp`, `<id>.wild.tmp` | Transient refresh state: the downloaded body and the two compile outputs before they are published by rename. | 0600 |
|
||||
|
||||
`<id>` is the `blocklist_sources` row id. A `.list` or `.wild` file whose id is
|
||||
no longer a `blocklist_sources` row is deleted by the orphan sweep; files of a
|
||||
live source are left alone whatever their state, and the sweep matches only the
|
||||
two published suffixes, so a `.tmp` file left by an interrupted refresh is not
|
||||
swept — the next refresh of that source overwrites it.
|
||||
`<id>` is the `blocklist_sources` row id.
|
||||
|
||||
### The orphan sweep
|
||||
|
||||
The sweep decides by id, not by suffix. It matches all five names above and
|
||||
deletes those whose `<id>` is no longer a `blocklist_sources` row, so the
|
||||
compiled `.list` and `.wild` of a removed source go, and so do a `.raw.tmp`,
|
||||
`.list.tmp` or `.wild.tmp` left behind by a refresh that was killed before it
|
||||
could clean up. Files belonging to a source that still has a row are never
|
||||
touched, whatever state they are in: the sweep holds the same lock every refresh
|
||||
takes, so it never reads the directory while a refresh is part-way through.
|
||||
|
||||
It runs at three moments:
|
||||
|
||||
- at startup, before the first refresh pass — this is what collects what a
|
||||
killed process left behind, and the compiled files of a source deleted while
|
||||
the server was down;
|
||||
- before each scheduled update pass, ahead of the disk-space gate: the sweep
|
||||
only unlinks, so it is the one step here that can give a critically full disk
|
||||
room back, and gating it would keep the residue that helped fill the disk;
|
||||
- immediately after `DELETE /api/blocklists/{id}`, which is when an orphan is
|
||||
actually created in normal operation. Without it a deleted list would keep its
|
||||
megabytes until the next scheduled pass.
|
||||
|
||||
With `blocklist_update.enabled = false` there are no scheduled passes, so only
|
||||
the first and the last of those three happen.
|
||||
|
||||
Each deletion is logged:
|
||||
|
||||
```
|
||||
info(blocklist_manager): pruned orphaned blocklist file 9999.list
|
||||
```
|
||||
|
||||
A failed sweep is a warning, not an outage — leftover bytes do not justify
|
||||
losing the refresh pass behind them, let alone the server.
|
||||
|
||||
The temporaries of a source that still exists are cleaned by the refresh that
|
||||
owns them rather than by the sweep: each refresh deletes its own `.raw.tmp`,
|
||||
`.list.tmp` and `.wild.tmp` as it finishes, successfully or not.
|
||||
|
||||
A `querylog.db` is moved aside when it is missing nothing but usability:
|
||||
SQLite reports it corrupt or not a database, `PRAGMA quick_check` does not
|
||||
@@ -111,7 +157,9 @@ no file; under systemd the journal captures it.
|
||||
|
||||
`doh_server.cert_path` / `key_path` and `dot_server.cert_path` / `key_path`,
|
||||
conventionally under `/etc/nxdns`. nxdns reads them, never writes or creates
|
||||
them. Both must be readable by the user nxdns runs as; the key should be
|
||||
readable by its owner only, which `nxdns check` warns about when it is not. A
|
||||
them. Both must be readable by the user nxdns runs as, and the key must belong
|
||||
to the certificate: `nxdns check` loads the pair and fails when it does not. The
|
||||
key should also be readable by its owner only, which `check` warns about when it
|
||||
is not. A
|
||||
watcher polls both files and swaps a renewed pair in without a restart. See
|
||||
[enable DoH and DoT](../how-to/enable-doh-and-dot.md).
|
||||
|
||||
@@ -12,8 +12,8 @@ see [measure performance](../how-to/measure-performance.md).
|
||||
| Sustained ≥ 100 qps on Raspberry Pi 5 | End-to-end against the real binary on the Pi; not a harness number |
|
||||
| Blocklist lookup p95 < 1 ms | `bench filter`: `matcher.normalize` + `Snapshot.evaluate` per op |
|
||||
| Cached response p95 < 5 ms | `bench cache`: `buildKey` + `DnsCache.get` + `packet.setId` per op |
|
||||
| Memory with ~1M blocked domains < 100 MB | `bench filter`: VmRSS with the 1M-domain snapshot loaded |
|
||||
| Stripped static binary < 10 MB per arch (< 15 MB with the embedded frontend) | CI size assert on the `cross` artifacts |
|
||||
| Memory with ~1M blocked domains < 100 MiB | `bench filter`: VmRSS with the 1M-domain snapshot loaded |
|
||||
| Stripped static binary < 10 MiB per arch (< 15 MiB with the embedded frontend) | CI size assert on the `cross` artifacts |
|
||||
|
||||
The harness is `tools/bench.zig`. It measures the three targets that are
|
||||
measurable in process; the qps target is end to end and the binary-size target
|
||||
@@ -61,7 +61,7 @@ VmRSS is lower because the filter suite's snapshot has been freed by then.
|
||||
| --- | --- |
|
||||
| Blocklist lookup p95 < 1 ms | to be measured on hardware |
|
||||
| Cached response p95 < 5 ms | to be measured on hardware |
|
||||
| Memory with ~1M blocked domains < 100 MB | to be measured on hardware |
|
||||
| Memory with ~1M blocked domains < 100 MiB | to be measured on hardware |
|
||||
| Sustained ≥ 100 qps | to be measured on hardware, end to end |
|
||||
|
||||
The qps target belongs to the real binary rather than the harness: it means
|
||||
|
||||
@@ -71,9 +71,9 @@ an install decision, not a first-run decision. A real install is covered in
|
||||
The `default` group and one enabled upstream are the two things nxdns will not
|
||||
start without. Every client that nxdns has never seen is assigned to `default`,
|
||||
and with no usable upstream there is nowhere to send a query it cannot answer
|
||||
itself, so a configuration missing either one is rejected. Seeding happens
|
||||
inside `run`, so `run` prints the problem and exits 1; `nxdns check` and `nxdns
|
||||
import` reject the same file with exit code 2.
|
||||
itself, so a configuration missing either one is rejected. Whichever command
|
||||
reads the file says the same thing and stops the same way: `run`, `nxdns check`
|
||||
and `nxdns import` all print the problem and exit 2.
|
||||
|
||||
## 4. Check the configuration before starting
|
||||
|
||||
@@ -83,13 +83,14 @@ zig-out/bin/nxdns check --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutori
|
||||
|
||||
```
|
||||
checking configuration file /home/you/nxdns-tutorial/config.zon
|
||||
OK https://cloudflare-dns.com/dns-query
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no problems found
|
||||
```
|
||||
|
||||
`check` parses the file, validates it, and contacts each upstream to confirm it
|
||||
answers. It exits 0 when there is nothing to fix and 2 when there is. It does
|
||||
not start any listener, so you can run it as often as you like.
|
||||
answers. It exits 2 when it found something that has to be fixed and 0
|
||||
otherwise. It writes nothing and starts no listener, so you can run it as often
|
||||
as you like.
|
||||
|
||||
## 5. Start the server
|
||||
|
||||
@@ -158,6 +159,13 @@ separate decision, which is what lets one group get a strict list and another
|
||||
get none. A source that is attached to no group is downloaded and then filters
|
||||
nothing.
|
||||
|
||||
You are in that state right now, between the previous step and this one, and it
|
||||
is legal rather than wrong — creating a source and attaching it afterwards is
|
||||
the normal order, which is why the API accepted it without complaint. On a
|
||||
stopped server `nxdns check` names such a source in a `WARN` line and still
|
||||
exits 0, so a list that silently blocks nothing is something you can find out
|
||||
about later. Attaching it now is what makes it take effect.
|
||||
|
||||
Ask which groups exist:
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user