383 lines
15 KiB
Markdown
383 lines
15 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.
|
|
|
|
The normal path is to download a released tarball, verify it, and install what
|
|
is inside it. Building from source is still supported and is the last section
|
|
of this page.
|
|
|
|
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: `systemd-analyze verify` was run on the machine that wrote this
|
|
> page. `nxdns check`, `nxdns import`, `nxdns export` 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.
|
|
>
|
|
> The download in step 1 could not be run at all: this repository has no tags
|
|
> and no published release, so every release URL on this page is a 404 today.
|
|
> Its commands are the ones [Verify a release](verify-a-release.md) covers in
|
|
> full, and the URL shapes — including the `releases/latest` redirect the
|
|
> version is read from — were probed there against `gitea.com`, a public
|
|
> instance running the same Gitea series.
|
|
>
|
|
> The `zig build dist` and `zig build verify-dist` blocks in the last section
|
|
> were run here, both to completion and both exiting 0; that section carries
|
|
> the detail.
|
|
|
|
## 1. Download and verify
|
|
|
|
Two static musl tarballs are published per release, one per architecture. Pick
|
|
`x86_64-linux-musl` for a normal PC or server and `aarch64-linux-musl` for a
|
|
Raspberry Pi 5.
|
|
|
|
```sh
|
|
BASE=https://git.mial.net/mokhtar/nxdns
|
|
VERSION=$(curl -fsS -o /dev/null -w '%{redirect_url}' "$BASE/releases/latest" |
|
|
sed 's#.*/releases/tag/v##')
|
|
mkdir -p ~/nxdns-release && cd ~/nxdns-release
|
|
curl -fLO "$BASE/releases/download/v$VERSION/nxdns-$VERSION-x86_64-linux-musl.tar.gz"
|
|
curl -fLO "$BASE/releases/download/v$VERSION/SHA256SUMS.txt"
|
|
curl -fLO "$BASE/releases/download/v$VERSION/SHA256SUMS.txt.asc"
|
|
```
|
|
|
|
The first line asks the server which release is current instead of hardcoding a
|
|
number that goes stale one release later — Gitea redirects `releases/latest` to
|
|
the newest published release's tag page. To install a particular version
|
|
instead, set `VERSION=<version>` yourself with the one you want; the tarball
|
|
filenames carry the version either way, so there is no version-free download
|
|
URL for them.
|
|
|
|
Verify before you extract. The signature is over `SHA256SUMS.txt`, and
|
|
`SHA256SUMS.txt` is over the tarballs:
|
|
|
|
```sh
|
|
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
|
|
sha256sum -c --ignore-missing SHA256SUMS.txt
|
|
tar -xzf "nxdns-$VERSION-x86_64-linux-musl.tar.gz"
|
|
```
|
|
|
|
[Verify a release](verify-a-release.md) has the whole procedure: where the
|
|
public key comes from, what fingerprint to expect, what each failure means, and
|
|
what the signature does and does not prove. Read it once before your first
|
|
install.
|
|
|
|
The extracted directory `nxdns-$VERSION-x86_64-linux-musl/` holds everything
|
|
this page installs:
|
|
|
|
| File | What it is |
|
|
| --- | --- |
|
|
| `nxdns` | The static binary, mode 0755 |
|
|
| `nxdns.service` | The systemd unit |
|
|
| `nxdns.conf` | The sysusers fragment that creates the `nxdns` user |
|
|
| `LICENSE` | EUPL-1.2 |
|
|
| `THIRD-PARTY-NOTICES` | Licences of everything compiled or bundled in |
|
|
| `INSTALL.md` | A short version of this page |
|
|
|
|
> Not verified on this host: no release exists yet, so none of these commands
|
|
> could be run against one — the `releases/latest` lookup returns 404 for this
|
|
> repository and leaves `VERSION` empty. The same lookup was run against
|
|
> `gitea.com/gitea/tea` on Gitea `1.27.0+dev` and printed `0.15.1`.
|
|
|
|
## 2. Copy the files to the target
|
|
|
|
```sh
|
|
cd "nxdns-$VERSION-x86_64-linux-musl"
|
|
scp nxdns nxdns.service nxdns.conf target:/tmp/
|
|
```
|
|
|
|
For a Raspberry Pi 5, extract the `aarch64-linux-musl` tarball 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.
|
|
|
|
Before copying, you can confirm the unit file parses:
|
|
|
|
```sh
|
|
systemd-analyze verify 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.
|
|
|
|
> Verified on this host against `deploy/systemd/nxdns.service` in a checkout,
|
|
> which is the same file the tarball ships — the path is the only difference.
|
|
|
|
## 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/nxdns.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
|
|
```
|
|
|
|
The sysusers fragment ships under the name it is installed as, so there is no
|
|
rename to get wrong.
|
|
|
|
> 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 upstreams[0] https://cloudflare-dns.com
|
|
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 <version> 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 tarball
|
|
you take:
|
|
|
|
```sh
|
|
curl -fLO "$BASE/releases/download/v$VERSION/nxdns-$VERSION-aarch64-linux-musl.tar.gz"
|
|
sha256sum -c --ignore-missing SHA256SUMS.txt
|
|
tar -xzf "nxdns-$VERSION-aarch64-linux-musl.tar.gz"
|
|
cd "nxdns-$VERSION-aarch64-linux-musl"
|
|
scp nxdns nxdns.service nxdns.conf pi:/tmp/
|
|
```
|
|
|
|
Then follow steps 3 to 6 on the Pi.
|
|
|
|
> Not verified on this host: no release exists to download, `pi` is a
|
|
> placeholder for your Pi's hostname, and this page was written on an x86_64
|
|
> machine with no Pi attached.
|
|
|
|
## Build from source instead
|
|
|
|
You do not need this to install nxdns, and it gets you a binary nobody has
|
|
signed. It is here for two cases: you want to run something other than a
|
|
tagged release, or you want to build the release yourself and compare it
|
|
against the published one. For the second case, follow
|
|
[Verify a release](verify-a-release.md) rather than this section — it says what
|
|
the comparison is and is not worth.
|
|
|
|
Requires Zig 0.16.0 and Node.js. From the repository root:
|
|
|
|
```sh
|
|
(cd web && npm ci && npm run build)
|
|
VERSION=$(sed -n 's/^[[:space:]]*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' build.zig.zon)
|
|
zig build dist -Dversion-string="$VERSION" -Dgit-commit="$(git rev-parse HEAD)" \
|
|
-Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
```
|
|
|
|
The first command builds the admin interface into `web/dist`; the last one
|
|
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. `dist` refuses to run against the `web/dist-placeholder` default for
|
|
exactly that reason, so there is no way to skip it by accident.
|
|
|
|
`-Dversion-string` is required and has no default. It is what `nxdns version`
|
|
prints. Take it from `build.zig.zon` rather than inventing one: `verify-dist`
|
|
asserts that the version under build equals `.version` there, so a made-up
|
|
string like `0.0.0-local` builds but then fails verification. `-Dgit-commit`
|
|
is what distinguishes your build from the published one of the same version.
|
|
|
|
What comes out under `zig-out/dist/` is the same set a release publishes,
|
|
minus the signature and the image digest:
|
|
|
|
- `bin/<triple>/nxdns` — the stripped static binary, one per target
|
|
- `stage/nxdns-<version>-<triple>/` — the staged payload, one per target
|
|
- `nxdns-<version>-<triple>.tar.gz` — one tarball per target
|
|
- `SHA256SUMS` — the two tarball hashes. The release publishes this as
|
|
`SHA256SUMS.txt`, with a third line for the image digest appended
|
|
|
|
The two targets are `x86_64-linux-musl` and `aarch64-linux-musl`. Both binaries
|
|
are statically linked and need nothing installed on the target host.
|
|
|
|
Check the result the same way the release pipeline does:
|
|
|
|
```sh
|
|
zig build verify-dist -Dversion-string="$VERSION" -Dgit-commit="$(git rev-parse HEAD)" \
|
|
-Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
|
```
|
|
|
|
`verify-dist` extracts each archive and asserts the ELF is static and within
|
|
the size budget, that the layout and file modes are exactly what step 1 lists,
|
|
and that `nxdns version` prints what was built. It exits non-zero on any
|
|
failure.
|
|
|
|
> Verified on this host: `zig build dist` and `zig build verify-dist` were both
|
|
> run to completion with the version taken from `build.zig.zon`. `dist`
|
|
> produced the two tarballs, `SHA256SUMS` and the staged payloads described
|
|
> above; `verify-dist` exited 0 with every check passing and the aarch64
|
|
> `nxdns version` check skipped for want of `-fqemu`. Passing a made-up
|
|
> `-Dversion-string=0.0.0-local` was also run: `dist` succeeded and
|
|
> `verify-dist` then failed with
|
|
> `FAIL zon-version: build.zig.zon says '0.0.1', the build says '0.0.0-local'`,
|
|
> which is why this section reads the version out of `build.zig.zon`.
|
|
|
|
From here, join the page at step 2 with the staged directory in place of the
|
|
extracted one:
|
|
|
|
```sh
|
|
cd "zig-out/dist/stage/nxdns-$VERSION-x86_64-linux-musl"
|
|
scp nxdns nxdns.service nxdns.conf target:/tmp/
|
|
```
|
|
|
|
The aarch64 binary is built by the same command and needs no toolchain on the
|
|
Pi.
|