Skip to content

Standalone mode

A standalone node has no account and no link. Its profile lives in its config file; it downloads the same public list artifact as everyone else.

/etc/opdns/node.yaml
mode: standalone
lists:
url: <public-list-latest-json-url>
poll: 6h
profile:
id: home01 # any 6 characters, [a-z0-9]
version: 1 # bump when you change the profile
name: Home
lists: [1, 10] # list ids from the catalogue (GET /v1/lists)
deny:
- pattern: "*.ads.example"
allow:
- pattern: "=cdn.example.net"
rewrites:
- {name: nas.home.arpa, type: A, value: 192.168.1.2}
security: {threat_intel: true, nrd: true}
parental: {safe_search: false}
settings:
block_mode: nxdomain
logs_enabled: true
log_client_ip: true
log_domains: true
retention_days: 30
cname_uncloak: true
linked_ips: []

The profile block uses the same field names as the profile document the cloud publishes (see the ProfileDocument schema in the API reference); unknown fields are errors. Instead of an inline profile:, you can point profile_file: (or --profile-file) at a JSON or YAML file with the same content.

lists.url can name an artifact (…/v<N>.bin) or the latest.json pointer next to it, which the node follows to the newest artifact on every poll. The artifact must verify with a trusted Ed25519 key. By default (lists.trust_cloud_key: true) the node trusts the pubkey published in that latest.json, fetched over HTTPS from the same place, and keeps it in lists_keys.json for offline restarts; on a key rotation the old key stays trusted until the next fetch confirms the new one. To pin the key yourself, set lists.public_keys and lists.trust_cloud_key: false.

Instead of writing the profile by hand, you can use the profile.json of any profile from an account export: it is the same format.

List ids come from the public catalogue: GET /v1/lists needs no account.

Check the result without starting the node:

Terminal window
opdns-node config check /etc/opdns/node.yaml

It validates the file (unknown keys are errors, including in the profile) and prints the effective configuration with the source of every value (Configuration).

With profile_file, the node watches the file and applies an edit within about two seconds, with no restart and no signal. It polls the file’s modification time, size and inode, so it also sees an editor that writes a new file and renames it over the old one, on bind mounts and network shares too. An edit that does not parse or validate changes nothing: the node keeps the last good profile and shows the error, with its line and field, on the local page.

An inline profile: in node.yaml is read at start only; change it and restart the node, or move it to a profile_file.

  • No dashboard: logs stay in the node’s SQLite file; read them with sqlite3. The local API does not serve logs yet.
  • The node re-fetches the list artifact every lists.poll (default 6 hours).
  • opdns sees only the artifact downloads (address and time).

http://<node>:8053/ shows health, versions, the loaded profile and lists, the config file path, the SQLite size, and the network guard. /api/status is the same as JSON, /healthz is the health check, and /metrics is Prometheus once you turn it on (Metrics). A password can protect the page. They answer only devices on your network; see Network access.

A standalone node has a small REST API on the same port, for scripts and home automation:

Request Does
GET /api/profile the profile in use, with its version as ETag
PUT /api/profile replace the profile (JSON or YAML, the same format as the file); the node validates it, rewrites the profile file (as JSON) and applies it at once, and answers with the stored profile and its new version
GET /api/lists the loaded list artifact: version, entries, when it was built and fetched, its source, the last error, and every list with its category and whether the profile applies it
POST /api/lists/refresh fetch the list artifact now instead of at the next lists.poll (202)

Every request needs Authorization: Bearer <token>. Set the token with web.api_token (at least 16 characters), or leave it empty: the node then generates one (opdnslocal_…) on its first run, keeps it in <data_dir>/secrets/api_token, and shows it once on the local page.

Terminal window
TOKEN=$(sudo cat /var/lib/opdns/secrets/api_token)
curl -H "Authorization: Bearer $TOKEN" http://192.168.1.2:8053/api/profile

Writes are guarded against lost updates: send the ETag of your GET back as If-Match (or leave the version you read in the body). If the profile changed in between, by another write or by hand in the file, the answer is 409 version_mismatch and nothing changes. A profile the node refuses is 422 validation_failed with the reason.

The API writes only a profile in its own file: with the profile inline in node.yaml, PUT /api/profile answers 409 and asks you to move it to a file named by profile_file. An enrolled node answers 404 on every /api/profile and /api/lists route: it is configured from the dashboard.