263 lines
9.7 KiB
Markdown
263 lines
9.7 KiB
Markdown
# Install nxdns with systemd
|
|
|
|
Installs nxdns as a system service on a Linux host with systemd, including a
|
|
Raspberry Pi 5. At the end the service answers DNS on port 53 and starts on
|
|
boot.
|
|
|
|
For what each flag does, see [the CLI reference](../reference/cli.md); for what
|
|
each configuration field means, see
|
|
[the configuration reference](../reference/configuration.md).
|
|
|
|
> Verification: the build steps and `systemd-analyze verify` were run on the
|
|
> machine that wrote this page. `nxdns check` and `nxdns run` were run there
|
|
> too, but against a scratch `--data-dir` and `--config` on an unprivileged
|
|
> port, because that machine is not a deploy target and has no `/etc/nxdns`,
|
|
> no `/var/lib/nxdns` and no root. The steps that need root on a target host —
|
|
> `install`, `systemd-sysusers`, `systemctl` — were not run; they are marked
|
|
> where they appear.
|
|
|
|
## 1. Build the binary
|
|
|
|
Requires Zig 0.16.0 and Node.js. From the repository root:
|
|
|
|
```sh
|
|
(cd web && npm ci && npm run build)
|
|
zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
```
|
|
|
|
The first command builds the admin interface into `web/dist`; the second
|
|
embeds that directory in the binary. Build the interface every time, before the
|
|
binary: a stale `web/dist` ships an admin UI that does not match the API it
|
|
talks to.
|
|
|
|
Two static musl binaries come out, one per deploy target:
|
|
|
|
- `zig-out/cross/x86_64-linux-musl/nxdns`
|
|
- `zig-out/cross/aarch64-linux-musl/nxdns`
|
|
|
|
Both are statically linked and need nothing installed on the target host.
|
|
|
|
## 2. Copy the files to the target
|
|
|
|
```sh
|
|
scp zig-out/cross/x86_64-linux-musl/nxdns target:/tmp/nxdns
|
|
scp deploy/systemd/nxdns.service deploy/systemd/sysusers.conf target:/tmp/
|
|
```
|
|
|
|
For a Raspberry Pi 5, copy `zig-out/cross/aarch64-linux-musl/nxdns` instead —
|
|
see [Raspberry Pi 5](#raspberry-pi-5) below.
|
|
|
|
> Not verified on this host: `target` is a placeholder for your server's
|
|
> hostname, and the machine that wrote this page has no second host to copy to.
|
|
> What was verified is that both source paths exist after step 1 and that the
|
|
> aarch64 file is a statically linked aarch64 ELF executable.
|
|
|
|
Before copying, you can confirm the unit file parses:
|
|
|
|
```sh
|
|
systemd-analyze verify deploy/systemd/nxdns.service
|
|
```
|
|
|
|
Off the target host this prints one complaint and exits 1:
|
|
|
|
```
|
|
nxdns.service: Command /usr/local/bin/nxdns is not executable: No such file or directory
|
|
```
|
|
|
|
That is the ExecStart path check finding no binary yet. Any other message is a
|
|
real problem with the unit. On the target, after step 3, the same command
|
|
should print nothing.
|
|
|
|
## 3. Install the binary, the user and the unit
|
|
|
|
Run as root on the target:
|
|
|
|
```sh
|
|
install -m 0755 /tmp/nxdns /usr/local/bin/nxdns
|
|
|
|
install -m 0644 /tmp/sysusers.conf /usr/lib/sysusers.d/nxdns.conf
|
|
systemd-sysusers
|
|
|
|
install -m 0644 /tmp/nxdns.service /etc/systemd/system/nxdns.service
|
|
systemctl daemon-reload
|
|
|
|
mkdir -p -m 0755 /etc/nxdns
|
|
```
|
|
|
|
> Not verified on this host: these commands need root on a target machine. The
|
|
> files they install were read at HEAD and the unit was checked with
|
|
> `systemd-analyze verify`.
|
|
|
|
The service user is a static one, not `DynamicUser`: a TLS key for the DoH or
|
|
DoT listener has to be chown-able to a uid that survives a restart.
|
|
|
|
Do not create `/var/lib/nxdns` or `/var/log/nxdns` by hand. The unit's
|
|
`StateDirectory` and `LogsDirectory` settings make systemd create them on first
|
|
start, `/var/lib/nxdns` at mode 0700 owned by `nxdns`.
|
|
|
|
`/etc/nxdns` is the one directory the `mkdir` above is for. The unit's
|
|
`ConfigurationDirectory=nxdns` also creates it, but not until the first start
|
|
in step 5, and step 4 has to write a file into it before then. systemd does not
|
|
mind finding the directory already there; it adjusts the mode and ownership to
|
|
what the unit asks for.
|
|
|
|
## 4. Write the seed configuration
|
|
|
|
nxdns starts from an empty database only if a configuration file tells it what
|
|
to forward to. Write `/etc/nxdns/config.zon`. The smallest file that starts is
|
|
one group named `default` and one enabled upstream:
|
|
|
|
```zon
|
|
.{
|
|
.groups = .{ .{ .name = "default" } },
|
|
.upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
|
|
.web = .{ .password = "choose-a-real-password" },
|
|
}
|
|
```
|
|
|
|
That file holds a password in plain text, so restrict it as soon as you have
|
|
written it:
|
|
|
|
```sh
|
|
chown root:nxdns /etc/nxdns/config.zon
|
|
chmod 0640 /etc/nxdns/config.zon
|
|
```
|
|
|
|
Root's umask is 022 on most distributions, so a freshly written
|
|
`/etc/nxdns/config.zon` is mode 0644 and every account on the host can read the
|
|
password out of it. The unit's `UMask=0077` does not help here: it applies to
|
|
files the service creates once it is running, and never re-chmods a file that
|
|
was written before the first start.
|
|
|
|
0640 with group `nxdns` rather than 0600: `/etc/nxdns` is a
|
|
`ConfigurationDirectory`, which systemd leaves owned by root, and the service
|
|
runs as `nxdns` and has to read this file on the first start. A root-owned 0600
|
|
file would be unreadable to it.
|
|
|
|
Do not expect `nxdns check` to catch a permissive mode here. Its only
|
|
permission warning is for a TLS private key
|
|
(`WARN doh_server.key_path: ... is mode 644; a TLS key must be readable by its
|
|
owner only`, from `checkTlsFiles` in `src/cli.zig`); it never stats the
|
|
configuration file. A mode 0644 `config.zon` passes `check` in silence, so the
|
|
`chmod` above is yours to remember.
|
|
|
|
Check it before you start the service:
|
|
|
|
```sh
|
|
nxdns check --config /etc/nxdns/config.zon
|
|
```
|
|
|
|
A good file prints the source it checked, one `OK` line per upstream, and
|
|
`OK: no problems found`:
|
|
|
|
```
|
|
checking configuration file /etc/nxdns/config.zon
|
|
OK https://cloudflare-dns.com/dns-query
|
|
OK: no problems found
|
|
```
|
|
|
|
The upstream probe sends a real query, so this needs working DNS on the host at
|
|
the time you run it. Exit 2 means `check` found something to fix and printed
|
|
every problem it found, not only the first.
|
|
|
|
The file seeds the database once. From the second start onwards it is ignored
|
|
and the database is the configuration; see
|
|
[the configuration model](../explanation/configuration-model.md) and
|
|
[Upgrade nxdns](upgrade.md) for how to change settings after that.
|
|
|
|
Once the seed has been consumed — after step 6 confirms you can log in — the
|
|
plaintext in it is dead weight that only carries risk. The seed's
|
|
`web.password` is hashed into `web.password_hash` at import time and the
|
|
plaintext is never stored; `nxdns export` writes `.password = ""` back out
|
|
alongside the hash. Nothing downstream ever reads the plaintext again, so
|
|
delete the file:
|
|
|
|
```sh
|
|
rm /etc/nxdns/config.zon
|
|
```
|
|
|
|
Keep it only if you want the seed as a record of the intended starting
|
|
configuration, and if you keep it, leave it at 0640 root:nxdns. Note that a
|
|
kept seed is not a backup — `nxdns export` is
|
|
(see [Back up and restore](back-up-and-restore.md)), and the export carries the
|
|
password hash rather than the password.
|
|
|
|
> Verified on this host, with a scratch `--config` and `--data-dir` in place of
|
|
> `/etc/nxdns` and `/var/lib/nxdns`: a seed written under umask 022 came out
|
|
> 0644, `nxdns check --config` on it printed `OK: no problems found` with no
|
|
> mode warning, and after `nxdns import` of that seed an `nxdns export` wrote
|
|
> `.password = ""` next to a populated `.password_hash =
|
|
> "$argon2id$v=19$..."`. The `chown`, `chmod` and `rm` lines above are the
|
|
> ordinary root-owned-file operations and were not run against a real
|
|
> `/etc/nxdns`, which this host does not have.
|
|
|
|
## 5. Start it
|
|
|
|
```sh
|
|
systemctl enable --now nxdns
|
|
journalctl -u nxdns -f
|
|
```
|
|
|
|
> Not verified on this host: needs root and an installed unit.
|
|
|
|
A healthy start logs a line naming every socket it bound:
|
|
|
|
```
|
|
info(nxdns): nxdns 0.1.0-dev serving on udp [::]:53 tcp [::]:53 tcp 0.0.0.0:53; 1 upstream(s); blocklist generation 1
|
|
```
|
|
|
|
nxdns writes to stderr and systemd captures that into the journal; logging
|
|
needs no further configuration. Port 53 is privileged, and the unit grants
|
|
`CAP_NET_BIND_SERVICE` through `AmbientCapabilities`.
|
|
|
|
If the start fails, read [Troubleshoot nxdns](troubleshoot.md). The two common
|
|
first-install failures are a port 53 already held by `systemd-resolved` and a
|
|
seed file that does not parse.
|
|
|
|
## 6. Confirm it answers
|
|
|
|
From another machine on the LAN:
|
|
|
|
```sh
|
|
dig @<server-ip> example.com A +short
|
|
```
|
|
|
|
The admin interface is on port 8080 by default; log in with the password from
|
|
the seed file. `http://<server-ip>:8080/api/health` reports upstream
|
|
availability and disk state without a login.
|
|
|
|
> Not verified on this host as written: `<server-ip>` is a placeholder, and a
|
|
> LAN client to run it from is a second machine this host does not have. What
|
|
> was verified is the same two checks against a local nxdns started from a
|
|
> scratch data directory on an unprivileged port — `dig @127.0.0.1 -p 15353
|
|
> example.com A +short` returned the A records, and `curl` against the web
|
|
> port returned 200. Only the address and the port differ from the lines
|
|
> above.
|
|
|
|
## Raspberry Pi 5
|
|
|
|
The Pi 5 is aarch64. Nothing about the procedure changes except which binary
|
|
you copy — the cross build needs no toolchain on the Pi and no toolchain beyond
|
|
Zig on the build machine:
|
|
|
|
```sh
|
|
(cd web && npm ci && npm run build)
|
|
zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
scp zig-out/cross/aarch64-linux-musl/nxdns pi:/tmp/nxdns
|
|
scp deploy/systemd/nxdns.service deploy/systemd/sysusers.conf pi:/tmp/
|
|
```
|
|
|
|
Then follow steps 3 to 6 on the Pi.
|
|
|
|
The build was run on the machine that wrote this page and
|
|
`zig-out/cross/aarch64-linux-musl/nxdns` is a statically linked aarch64 ELF
|
|
executable.
|
|
|
|
> Not verified on this host: the two `scp` lines. `pi` is a placeholder for
|
|
> your Pi's hostname, and this page was written on an x86_64 machine with no Pi
|
|
> attached. The build steps above it were run; the copy was not.
|
|
|
|
> Not verified on this host: the aarch64 binary was not executed. This host is
|
|
> x86_64 and has no `qemu-aarch64` to run it under. Running it needs a
|
|
> Raspberry Pi 5 or another aarch64 machine.
|