182 lines
10 KiB
Markdown
182 lines
10 KiB
Markdown
# Files and directories
|
|
|
|
Every path nxdns reads or writes, and the mode it is created with. Source of
|
|
truth: `src/cli.zig` (`DataDir`), `src/storage/querylog_schema.zig` (the
|
|
preserved query-log databases), `src/filter/manager.zig` (the blocklist
|
|
snapshots), `src/platform/logging.zig` (the log file).
|
|
|
|
## The data directory
|
|
|
|
Default `/var/lib/nxdns`, overridable with `--data-dir DIR`. `nxdns run` and
|
|
`nxdns import` create it and its parents at mode 0700 when it is missing;
|
|
`nxdns check` and `nxdns export` do not create it. `export` fails if it is not
|
|
there. `check` opens it only on the branch that resolved to the database, so an
|
|
absent data directory is not in itself a failure: an explicit `--config FILE`
|
|
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.
|
|
|
|
`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 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 |
|
|
| `querylog.db.corrupt-<unix-seconds>-<n>` | The same, when the plain name is taken — `<n>` counts from 1 and rises until the name is free. Two recreates within one second is the case it exists for. | The same |
|
|
| `blocklists/` | Compiled blocklist snapshots, one subdirectory of the data directory. | 0700 |
|
|
| `blocklists/<id>.list` | Exact domains for blocklist source `<id>`, one per line, behind a header. | 0600 |
|
|
| `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.
|
|
|
|
### 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
|
|
answer `ok`, or its `user_version` fingerprint does not match the schema. Only
|
|
the main file is renamed — its `-wal` and `-shm` are deleted, because a stale
|
|
WAL would be replayed into the fresh database. A missing `querylog.db` is
|
|
created without any aside file. The rename happens inside
|
|
`querylog_schema.open`, before the 0600 chmod, and that chmod names
|
|
`querylog.db` and its two sidecars only — so an aside file keeps the mode the
|
|
file had at rename time, which for a `querylog.db` nxdns itself created is 0600
|
|
and for one an operator put there is whatever they left it at. Nothing prunes
|
|
the aside files; they accumulate until an operator removes them, and each one
|
|
holds the same browsing history the live query log holds.
|
|
|
|
The 0600 modes are not cosmetic. `config.db` holds the argon2id password hash
|
|
and `querylog.db` holds the browsing history of every client on the LAN, so both
|
|
are as sensitive as each other, and a WAL file holds the same rows as the
|
|
database it belongs to. SQLite creates the main database at `0644 & ~umask`;
|
|
nxdns chmods it to 0600 before enabling WAL, so the sidecars inherit 0600 rather
|
|
than being created world-readable.
|
|
|
|
## The configuration file
|
|
|
|
Default `/etc/nxdns/config.zon`, overridable with `--config FILE`. nxdns reads
|
|
it and never writes it: it seeds an unconfigured database once and is ignored
|
|
afterwards. nxdns does not create the file or its directory.
|
|
|
|
`nxdns export --out FILE` writes a ZON file at mode 0600 through a temporary
|
|
file and a rename. That file carries `web.password_hash`, so treat exports as
|
|
secrets. Without `--out` the export goes to stdout, where permissions are the
|
|
redirect's business.
|
|
|
|
## The log file
|
|
|
|
Only when `logging.output = .file`. The path is `logging.file_path`, default
|
|
`/var/log/nxdns/nxdns.log`, and validation requires it to be absolute.
|
|
|
|
**nxdns does not create the log directory.** It must exist and be writable by
|
|
the user the service runs as before the process starts; under systemd the unit's
|
|
`LogsDirectory=nxdns` does that.
|
|
|
|
Rotation triggers at `logging.max_size_mb` MiB and keeps `logging.max_files`
|
|
files in total, the live one included, so the highest generation on disk is
|
|
`logging.max_files - 1`. With the default 5 that is `nxdns.log` plus `nxdns.log.1`
|
|
through `nxdns.log.4`. Rotation deletes the highest generation, renames each
|
|
remaining one up by one, then renames the live file to `.1`. A `max_files` of 1
|
|
or 0 deletes the live file instead of renaming it. The directory holding the log
|
|
file is also sampled by the disk monitor.
|
|
|
|
| Path | What it is | Mode |
|
|
| --- | --- | --- |
|
|
| `logging.file_path` (default `/var/log/nxdns/nxdns.log`) | The live process log. Opened write-only and written at a tracked offset; created when it does not exist. | `0666 & ~umask` — nxdns never chmods it |
|
|
| `<file_path>.1` … `<file_path>.<max_files - 1>` | Rotated generations, newest first. | Inherited from the live file they were renamed from |
|
|
|
|
**Do not point external logrotate at this file.** nxdns owns the rotation of
|
|
its own log. There is no append mode in Zig 0.16, so the writer reads the file
|
|
length once when it opens the file and then writes every line at an offset it
|
|
tracks in the process. A rotator that moves the file behind it breaks that
|
|
offset, and nxdns does not notice until the next restart:
|
|
|
|
- With `copytruncate` the offset survives the truncation, so the next line
|
|
lands where it would have without it. The file regrows with a sparse,
|
|
NUL-filled prefix as long as the log that was just rotated away.
|
|
- With rename-and-create nxdns keeps writing to the renamed inode. The new
|
|
file stays empty, the renamed one grows without bound, and
|
|
`logging.max_size_mb` bounds nothing on disk.
|
|
|
|
If an external rotator has to own the file, set `logging.output = .stderr` and
|
|
let the collector capture the stream instead.
|
|
|
|
The log file is the one path here nxdns does not set a mode on. `config.db` and
|
|
`querylog.db` are chmodded to 0600 whatever the umask; the log file is left at
|
|
whatever the umask gives it. Under the shipped unit that is 0600, because
|
|
`deploy/systemd/nxdns.service` sets `UMask=0077`. Run from a shell with the
|
|
usual `umask 022` it is 0644, and `LogsDirectory=nxdns` leaves the directory at
|
|
systemd's default 0755, so a log written there is readable by any local user.
|
|
|
|
With `logging.output = .stderr` or `.syslog` nxdns writes to stderr and creates
|
|
no file; under systemd the journal captures it.
|
|
|
|
## Certificate and key files
|
|
|
|
`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, 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).
|