Install the static binary
Each release on GitHub carries static binaries named
opdns-node_<version>_<os>_<arch> for linux_amd64, linux_arm64 and
darwin_arm64, with a SHA256SUMS file. The binary has no dependencies
except Unbound, which it starts and supervises; it refuses to start without
one.
Install on Linux with systemd
Section titled “Install on Linux with systemd”For Debian, Ubuntu and Raspberry Pi OS. The repository ships the unit,
deploy/systemd/opdns-node.service,
and the same steps are in
cmd/opdns-node/README.md.
-
Install Unbound, and stop the distribution’s own Unbound service, which would take port 53 (the node runs its own instance on
127.0.0.1:5353with a generated configuration):Terminal window sudo apt install unboundsudo systemctl disable --now unboundTerminal window sudo apk add unboundAlpine has no systemd: run the node from your init system with the same user and data directory.
The node looks for
unboundon thePATHand in/usr/sbin,/usr/local/sbin,/opt/homebrew/sbinand/sbin; setunbound.binaryotherwise. -
Download and verify the binary:
Terminal window V=<version>; A=linux_arm64curl -fLO https://github.com/operomtl/opdns/releases/download/v$V/opdns-node_v${V}_$Acurl -fLO https://github.com/operomtl/opdns/releases/download/v$V/SHA256SUMSsha256sum --ignore-missing -c SHA256SUMSsudo install -m 0755 opdns-node_v${V}_$A /usr/local/bin/opdns-node -
Create the
opdnsuser and its data directory, and install the unit:Terminal window sudo useradd --system --user-group --home-dir /var/lib/opdns --shell /usr/sbin/nologin opdnssudo install -d -o opdns -g opdns -m 0700 /var/lib/opdnscurl -fLO https://raw.githubusercontent.com/operomtl/opdns/v$V/deploy/systemd/opdns-node.servicesudo install -m 0644 opdns-node.service /etc/systemd/system/opdns-node.servicesudo systemctl daemon-reload -
Free port 53 from systemd-resolved’s stub listener. The host keeps resolving through systemd-resolved’s upstreams:
Terminal window sudo mkdir -p /etc/systemd/resolved.conf.dprintf '[Resolve]\nDNSStubListener=no\n' | sudo tee /etc/systemd/resolved.conf.d/opdns.confsudo systemctl restart systemd-resolvedsudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf -
Enrol. In the dashboard open Nodes → Add a node, create a code, then:
Terminal window sudo -u opdns opdns-node enrol --token <enrolment-code>Or put
OPDNS_ENROL_TOKEN=<enrolment-code>in/etc/opdns/node.env(mode 0600): the node enrols on its first start and ignores the variable afterwards. See Enrol a node, or standalone mode to run without an account. -
Start it and check:
Terminal window sudo systemctl enable --now opdns-nodeopdns-node statusjournalctl -u opdns-node -fThe status page is on port 8053.
No configuration file is needed for an enrolled node: the list signing key
comes from the cloud (list signing keys).
Settings go in /etc/opdns/node.yaml, or as OPDNS_<SECTION>_<KEY>
variables in /etc/opdns/node.env; every one is on the
configuration reference.
What the unit does
Section titled “What the unit does”| Runs as | the opdns user, with only CAP_NET_BIND_SERVICE (ports 53, 443, 853), no new privileges |
| Files | reads /etc/opdns/node.yaml and /etc/opdns/node.env if they exist; writes only /var/lib/opdns (ProtectSystem=strict, private /tmp, no home directories) |
| Restarts | on failure after 5 seconds, except on exit status 2 |
systemctl reload opdns-node |
sends SIGHUP: applies the reloadable keys of the configuration, pulls the profile and lists again and reloads the TLS certificate |
| Stops | on SIGTERM, within 20 seconds: drains listeners, flushes pending log records, closes the link, stops Unbound |
Exit status 2 means the node is not set up (no identity, no enrolment code and no standalone profile) or its configuration is invalid. It prints what to do, and systemd does not restart it: fix the cause and start it. A node never serves unfiltered DNS because it was not set up.
Certificates for encrypted DNS
Section titled “Certificates for encrypted DNS”With listen.dot, listen.doh, listen.doq or listen.doh3 set, the
node needs listen.tls_cert and listen.tls_key. It checks both files
every second and loads a renewed pair as soon as it appears, with no
restart; a pair that fails to load is logged and the previous certificate
stays. So a renewal hook only has to write the files where the opdns user
can read them (for example under /etc/opdns). systemctl reload opdns-node forces a reload. See also
Encrypted DNS on the node.
macOS and other systems
Section titled “macOS and other systems”On macOS, brew install unbound, then run opdns-node run from a
terminal or your own launchd job. There is no packaged service yet. The
node needs to bind port 53, so run it as root or on another port
(listen.udp, listen.tcp).
Commands
Section titled “Commands”opdns-node run [--config FILE] [--data-dir DIR] [--log-level L] [--enrol-token CODE] [--profile-file FILE] [--lists-url URL] [--print-config]opdns-node enrol --token CODE|TOKEN [--node-id ID] [--replace] [--keep-cloud-profile] [--config FILE] [--data-dir DIR]opdns-node unenrol [--purge-logs] [--local-only] [--config FILE] [--data-dir DIR]opdns-node status [--config FILE] [--addr HOST:PORT] [--json]opdns-node config check [--config FILE | FILE]opdns-node config referenceopdns-node backup [--config FILE] [--data-dir DIR] ARCHIVEopdns-node restore [--force] [--config FILE] [--data-dir DIR] ARCHIVEopdns-node hash-passwordopdns-node versionrun is the default. config check and config reference are on
Configuration, backup and restore
on Upgrade and back up,
hash-password on Network access.
status asks the running node’s status page, or reads the data directory
if the node is not running (exit status 3).