Skip to content

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.

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.

  1. 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:5353 with a generated configuration):

    Terminal window
    sudo apt install unbound
    sudo systemctl disable --now unbound

    The node looks for unbound on the PATH and in /usr/sbin, /usr/local/sbin, /opt/homebrew/sbin and /sbin; set unbound.binary otherwise.

  2. Download and verify the binary:

    Terminal window
    V=<version>; A=linux_arm64
    curl -fLO https://github.com/operomtl/opdns/releases/download/v$V/opdns-node_v${V}_$A
    curl -fLO https://github.com/operomtl/opdns/releases/download/v$V/SHA256SUMS
    sha256sum --ignore-missing -c SHA256SUMS
    sudo install -m 0755 opdns-node_v${V}_$A /usr/local/bin/opdns-node
  3. Create the opdns user and its data directory, and install the unit:

    Terminal window
    sudo useradd --system --user-group --home-dir /var/lib/opdns --shell /usr/sbin/nologin opdns
    sudo install -d -o opdns -g opdns -m 0700 /var/lib/opdns
    curl -fLO https://raw.githubusercontent.com/operomtl/opdns/v$V/deploy/systemd/opdns-node.service
    sudo install -m 0644 opdns-node.service /etc/systemd/system/opdns-node.service
    sudo systemctl daemon-reload
  4. 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.d
    printf '[Resolve]\nDNSStubListener=no\n' | sudo tee /etc/systemd/resolved.conf.d/opdns.conf
    sudo systemctl restart systemd-resolved
    sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
  5. 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.

  6. Start it and check:

    Terminal window
    sudo systemctl enable --now opdns-node
    opdns-node status
    journalctl -u opdns-node -f

    The 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.

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.

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.

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).

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 reference
opdns-node backup [--config FILE] [--data-dir DIR] ARCHIVE
opdns-node restore [--force] [--config FILE] [--data-dir DIR] ARCHIVE
opdns-node hash-password
opdns-node version

run 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).