Files
nxdns/docs/reference/configuration.md
T

20 KiB
Raw Blame History

Configuration reference

Every setting and list nxdns accepts, with its type, default and allowed values.

The configuration model explains how the file, the database and the web interface relate. The CLI reference covers the commands that read and write configuration.

File format

The configuration file is ZON, Zig's literal syntax: one .{ … } block whose fields are the sections and lists below. Strings are in double quotes, and choices are written with a leading dot (.response = .nxdomain). Comments start with //. The file may be at most 4 MiB, and a syntax error is reported with its line and column.

Anything the file leaves out keeps its default.

The smallest file that starts is a default group and one upstream:

.{
    .groups = .{ .{ .name = "default" } },
    .upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
}

Settings

Settings are grouped in sections. The Key column gives a setting's full name, as the web interface and nxdns check show it; in the file it sits inside its section, so dns.port is written .dns = .{ .port = 53 }.

Some settings take effect only after a restart; the web interface marks them needs restart.

upstream

How long nxdns waits for upstream resolvers.

Key Type Default Allowed values What it does
upstream.attempt_timeout_ms integer 2500 100–120000 ms, and not above total_timeout_ms Time allowed for one try against one upstream, connecting included. When it runs out, nxdns tries the next upstream.
upstream.total_timeout_ms integer 5000 100–120000 ms Time allowed for a whole query across every upstream it tries. When it runs out, the client gets SERVFAIL. Also the time nxdns check gives each upstream.
upstream.read_timeout_ms integer 3000 100–120000 ms Time allowed for a query sent to a forward zone's resolver, including a retry over TCP.

A query against several unreachable upstreams costs total_timeout_ms once, not one attempt_timeout_ms per upstream. An attempt that the total cut short is not held against that upstream's health.

dns

The plain DNS listener, over UDP and TCP.

Key Type Default Allowed values What it does
dns.bind_ipv4 string "0.0.0.0" an IPv4 address The IPv4 address to listen on.
dns.bind_ipv6 string "::" an IPv6 address The IPv6 address to listen on.
dns.port integer 53 1–65535 The port to listen on.
dns.rate_limit integer 1000 at least 1 How many queries one client may send per window.
dns.rate_window_seconds integer 60 1–3600 s The length of that window.

Each bind address must belong to its own family: an IPv4 address in dns.bind_ipv6 is refused.

blocking

What a blocked query gets back.

Key Type Default Allowed values What it does
blocking.response .zero or .nxdomain .zero .zero answers 0.0.0.0 or ::; .nxdomain answers that the name does not exist.
blocking.ttl integer 5 0–86400 s How long a client may cache a blocked answer.

cache

Key Type Default Allowed values What it does
cache.size integer 10000 1–1000000 entries How many answers nxdns caches. The memory for the cached answers grows with it, and when that memory is full, answers nobody has asked for lately make room for new ones.
cache.negative_ttl_max integer 3600 0–86400 s The longest nxdns caches an answer that a name does not exist; 0 turns that off.

The cache cannot be turned off; the smallest is cache.size = 1.

web

The web interface.

Key Type Default Allowed values What it does
web.enabled bool true Runs the web interface.
web.bind string "0.0.0.0" an IP address The address it listens on.
web.port integer 8080 1–65535 The port it listens on.
web.password string none not empty; not together with password_hash Sets the admin password. Only its hash is stored; see Passwords.
web.password_hash string none an argon2id hash, or "" The stored password hash. "" removes the password.
web.session_ttl_hours integer 24 at least 1 How long a login lasts.
web.api_rate_limit_per_min integer 300 at least 1 How many requests one client may make to the web interface per minute.
web.api_localhost_exempt bool true Requests from the nxdns machine itself skip that limit.
web.sse_max_connections_per_ip integer 3 at least 1 How many live views (such as Activity's Live tab) one client may keep open.
web.trusted_proxies string "" IP addresses separated by commas Reverse proxies whose X-Forwarded-For header nxdns believes; see Behind a reverse proxy.

doh_server

DNS over HTTPS for clients on your network. See Enable DoH and DoT.

Key Type Default Allowed values What it does
doh_server.enabled bool false Runs the DoH listener.
doh_server.bind string "0.0.0.0" an IP address The address it listens on.
doh_server.port integer 443 1–65535 The port it listens on.
doh_server.cert_path string "/etc/nxdns/cert.pem" not empty when enabled The certificate, as PEM. A renewed file is picked up within 30 seconds.
doh_server.key_path string "/etc/nxdns/key.pem" not empty when enabled Its private key, as PEM. nxdns check warns when others can read it.

dot_server

DNS over TLS for clients on your network.

Key Type Default Allowed values What it does
dot_server.enabled bool false Runs the DoT listener.
dot_server.bind string "0.0.0.0" an IP address The address it listens on.
dot_server.port integer 853 1–65535 The port it listens on.
dot_server.cert_path string "/etc/nxdns/cert.pem" not empty when enabled The certificate, as PEM.
dot_server.key_path string "/etc/nxdns/key.pem" not empty when enabled Its private key, as PEM.

edns

Key Type Default Allowed values What it does
edns.ecs_mode .strip or .forward .strip .strip removes the client's subnet (EDNS Client Subnet) from queries before they go upstream; .forward passes it on.

logging

The process log and the query history.

Key Type Default Allowed values What it does
logging.level .err, .warn, .info or .debug .info The least important log line nxdns writes. The file says .err; the web interface says error.
logging.retention_days integer 30 at least 1 How long the query history is kept, and how long a device that stopped asking stays in Clients.
logging.query_log_buffer_max integer 1024 1–37449 How many queries nxdns holds in memory while it waits to write them, each about 1.8 KiB. The default covers over a minute and a half of queries at 10 a second, far more than a household asks while a write is in progress. When the buffer is full, the oldest waiting query is dropped from the history and counted.
logging.query_log_flush_interval_s integer 60 0–3600 s How often the query history is written to disk; see below.
logging.hide_domains bool false Stores a placeholder instead of the name in the query history.
logging.hide_client_ips bool false Stores a placeholder instead of the client's address in the query history.
logging.output .stderr, .syslog or .file .stderr Where the process log goes. .stderr and .syslog both write to standard error, which journald and Docker collect. .file writes to file_path and rotates it.
logging.file_path string "/var/log/nxdns/nxdns.log" an absolute path, when output is .file The log file. nxdns does not create its directory.
logging.max_size_mb integer 50 at least 1 The size at which the log file is rotated.
logging.max_files integer 5 at least 1 How many log files are kept, the current one included.

Why the query history is written once a minute

Writing each query to disk as it happens wears out an SD card for no benefit, so nxdns writes the queries of the last query_log_flush_interval_s seconds together. Pi-hole's default is the same minute, for the same reason.

The cost is that the History tab, Overview and the charts run up to a minute behind, and a crash loses up to that minute of history. Activity → Live is not delayed: it shows each query the moment it is answered. A burst of 100 queries is written at once without waiting, and stopping nxdns writes what it holds first.

0 writes queries as they arrive, if you want History current to the second and do not mind the extra writes.

disk

Free-space thresholds for the data directory. DNS keeps working whatever the free space.

Key Type Default Allowed values What it does
disk.min_free_mb integer 200 at least 1, and not above warn_free_mb Below this, nxdns stops writing query history and device records and stops downloading blocklists, until space comes back.
disk.warn_free_mb integer 500 at least 1 Below this, nxdns logs a warning.

blocklist_update

Key Type Default Allowed values What it does
blocklist_update.enabled bool true Refreshes blocklists on a schedule. When false, they refresh only at startup and when you press Update now.
blocklist_update.interval_hours integer 24 at least 1 How often blocklists are refreshed.

Lists

The rest of the configuration is lists of entries. Fields with no default are required.

groups

Groups of clients that share blocklists and rules. A group named default is required: every client not assigned to another group is in it.

Field Type Default Allowed values
name string required not empty, at most 64 bytes, unique
safe_search bool false

safe_search makes search engines that support it return their safe results to the group's clients.

upstreams

The resolvers nxdns forwards queries to, over DoH (https://) or DoT (tls://). At least one must be enabled.

Field Type Default Allowed values
url string required an https:// or tls:// URL; unique
priority integer 100 lower is tried first
enabled bool true
tls_name string "" tls:// only: the name the resolver's certificate must carry

nxdns tries the upstreams in order of priority and moves to the next when one fails.

A tls:// URL must use an IP address, because nxdns would need DNS to look up its own resolver's name. Put the name in tls_name so the certificate can still be checked:

.{ .url = "tls://9.9.9.9:853", .tls_name = "dns.quad9.net" },

An https:// URL takes a hostname as usual.

nxdns speaks DoH over HTTP/1.1, so a DoH server that requires HTTP/2 does not work. Quad9's DoH endpoint is one: use Quad9 over DoT, as above.

clients

Devices with a fixed group, by address.

Field Type Default Allowed values
ip string required an IP address; unique
name string "" the name shown in the web interface
group string "default" a declared group

Names nxdns learns

A device you have not named can still show a name. If a forward zone covers your network's reverse names, nxdns asks that resolver, usually your router, for each unnamed device's name once a day, and shows the answer:

.forward_zones = .{
    .{ .zone = "168.192.in-addr.arpa", .resolver = "udp://192.168.1.1:53" },
},

A name you type always wins. Learned names are never exported, and a renamed device can show its old name for up to a day.

client_prefixes

Groups for whole address ranges, for devices without a clients entry.

Field Type Default Allowed values
prefix string required a CIDR range such as 192.168.2.0/24 or fd00:abcd::/48; unique
group string "default" a declared group
priority integer 100 breaks a tie between equally specific ranges; lower wins

A clients entry beats any range, and a narrower range beats a wider one.

blocklist_sources

Blocklists nxdns downloads.

Field Type Default Allowed values
url string required an http:// or https:// URL; unique
name string required not empty, at most 64 bytes
enabled bool true
is_suggested bool false marks the list as a suggestion in the web interface

A source blocks nothing until a group uses it through group_sources; nxdns check warns about one that no group uses.

nxdns reads hosts files, plain lists of domains, and Adblock Plus lists. From an Adblock Plus list it takes the rules that name a domain, ||example.com^, which block the domain and everything under it, and exception rules, @@||example.com^, which unblock a name that another list blocks. Your own rules always win over both.

It skips what does not translate into a DNS answer, and Protection → Sources counts it for each list:

  • Skipped regex: regex lines, which nxdns takes only from your own rules. Copy the ones you trust into a regex rule.
  • Skipped unsupported: page-element hiding (##), rules with $ options, and other browser-only syntax. A list with many of these and few domains is written for browser extensions; its DNS or hosts version blocks more.

group_sources

Which group uses which blocklist.

Field Type Default Allowed values
group string required a declared group
source_url string required the url of a declared blocklist source; each pair once

rules

Your own allow and block rules for a group. They are checked before any blocklist.

Field Type Default Allowed values
group string required a declared group
pattern string required see below
kind .exact, .wildcard or .regex required
action .allow or .block required
  • .exact matches one name, such as ads.example.com.
  • .wildcard has one or more labels that are exactly *: *.example.com matches every name under example.com, but not example.com itself. A partial label such as ads*.example.com is not a wildcard; write a regex for that.
  • .regex is a regular expression matched against the lowercase name. It matches anywhere in the name unless you anchor it with ^ or $.

Exact rules are checked first, then wildcards, then regexes; within each kind, allow rules win over block rules.

A regex may use literal characters, ., classes such as [a-z0-9] and [^0-9], \d, \w, \ before punctuation to make it literal, *, +, ?, {n}, {n,m}, {n,}, |, ( … ), ^ and $. Anything else is refused with a message, rather than quietly read differently from what another engine would do: lookaround, backreferences, \s, \b, (?…), an empty alternative such as ads|, and a quantifier directly after another, such as a+?. A pattern is at most 256 bytes, and a group holds at most 256 regex rules. Matching takes time in proportion to the pattern and the name, so no pattern can slow resolution down.

local_records

Answers nxdns gives itself, without asking an upstream.

Field Type Default Allowed values
name string required a domain name
rtype .a, .aaaa or .cname required
value string required an IPv4 address for .a, an IPv6 address for .aaaa, a domain name for .cname
ttl integer 300 1–604800 s

Each name, type and value together appear once.

forward_zones

Domains that go to a resolver of your choice instead of the upstreams, such as your router's local names.

Field Type Default Allowed values
zone string required a domain name; unique
resolver string required udp://IP:port or tcp://IP:port, with an IP address and a port

The most specific zone wins.

.forward_zones = .{
    .{ .zone = "lan", .resolver = "udp://192.168.1.1:53" },
    .{ .zone = "168.192.in-addr.arpa", .resolver = "udp://192.168.1.1:53" },
},

Passwords

web.password and web.password_hash are both optional, and leaving one out is not the same as setting it empty.

The file says What happens to the stored password
Neither field Nothing. It stays as it was.
.password = "…" Sets that password.
.password = "" Refused.
.password_hash = "$argon2id$…" Sets the password that hash belongs to, for example from an export.
.password_hash = "" Removes the password, which turns authentication off.
Both fields Refused.

The plaintext password is never stored: nxdns keeps an argon2id hash, and nxdns export writes .password = null with the hash. Applying the same file twice changes nothing.

Errors and warnings

nxdns check, nxdns import and a start in file mode print one line per problem, and report all of them, not just the first:

FAIL groups: no group named 'default'; every unknown client is assigned to it
FAIL rules[0].pattern: '(' is not a valid regex pattern

A FAIL stops the command with exit code 2. A WARN marks something allowed but probably unintended, such as a blocklist no group uses, and does not stop it.

URLs in these lines are shortened to their scheme, host and port, because the rest can hold an account identifier: a NextDNS URL such as https://dns.nextdns.io/abcd12 appears as https://dns.nextdns.io. A hostname is shown as it is, so prefer NextDNS's form with the identifier in the path over abcd12.dns.nextdns.io.

A fuller example

.{
    .dns = .{ .port = 53, .rate_limit = 1000 },

    // Blocked names answer 0.0.0.0 or :: with a 5 second TTL.
    .blocking = .{ .response = .zero, .ttl = 5 },

    .web = .{ .password = "correct horse battery staple" },

    // Serve DoT to the network.
    .dot_server = .{
        .enabled = true,
        .cert_path = "/etc/nxdns/cert.pem",
        .key_path = "/etc/nxdns/key.pem",
    },

    .logging = .{ .level = .warn, .retention_days = 14 },

    .groups = .{
        .{ .name = "default" },
        .{ .name = "kids", .safe_search = true },
    },

    // Cloudflare first, Quad9 if it fails.
    .upstreams = .{
        .{ .url = "https://cloudflare-dns.com/dns-query", .priority = 10 },
        .{ .url = "tls://9.9.9.9:853", .priority = 20, .tls_name = "dns.quad9.net" },
    },

    .clients = .{
        .{ .ip = "192.168.1.20", .name = "Kids' tablet", .group = "kids" },
    },
    .client_prefixes = .{
        .{ .prefix = "192.168.2.0/24", .group = "kids" },
    },

    .blocklist_sources = .{
        .{ .url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", .name = "StevenBlack hosts" },
    },
    .group_sources = .{
        .{ .group = "default", .source_url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" },
        .{ .group = "kids", .source_url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" },
    },

    .rules = .{
        .{ .group = "default", .pattern = "allowed.example.com", .kind = .exact, .action = .allow },
        .{ .group = "kids", .pattern = "*.tracker.example", .kind = .wildcard, .action = .block },
        .{ .group = "kids", .pattern = "^ad[0-9]+-", .kind = .regex, .action = .block },
    },

    .local_records = .{
        .{ .name = "nas.lan", .rtype = .a, .value = "192.168.1.5" },
        .{ .name = "files.lan", .rtype = .cname, .value = "nas.lan" },
    },

    // Names under lan, and reverse lookups, go to the router.
    .forward_zones = .{
        .{ .zone = "lan", .resolver = "udp://192.168.1.1:53" },
        .{ .zone = "168.192.in-addr.arpa", .resolver = "udp://192.168.1.1:53" },
    },
}