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.
Configure
Section titled “Configure”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:
opdns-node config check /etc/opdns/node.yamlIt validates the file (unknown keys are errors, including in the profile) and prints the effective configuration with the source of every value (Configuration).
Edit the profile file
Section titled “Edit the profile file”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.
What is different from enrolled mode
Section titled “What is different from enrolled mode”- 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).
Local page
Section titled “Local page”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.
Local API
Section titled “Local API”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.
TOKEN=$(sudo cat /var/lib/opdns/secrets/api_token)curl -H "Authorization: Bearer $TOKEN" http://192.168.1.2:8053/api/profileWrites 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.