403 lines
20 KiB
Markdown
403 lines
20 KiB
Markdown
# Configuration reference
|
||
|
||
Every setting and list nxdns accepts, with its type, default and allowed values.
|
||
|
||
[The configuration model](../explanation/configuration-model.md) explains how the file, the database and the web interface relate. [The CLI reference](cli.md) 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:
|
||
|
||
```zon
|
||
.{
|
||
.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](#forward_zones)'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 is reserved at startup. |
|
||
| `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](#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](../how-to/set-up-admin-authentication.md#behind-a-reverse-proxy). |
|
||
|
||
### doh_server
|
||
|
||
DNS over HTTPS for clients on your network. See [Enable DoH and DoT](../how-to/enable-doh-and-dot.md).
|
||
|
||
| 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:
|
||
|
||
```zon
|
||
.{ .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](#forward_zones) 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:
|
||
|
||
```zon
|
||
.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.
|
||
|
||
```zon
|
||
.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
|
||
|
||
```zon
|
||
.{
|
||
.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" },
|
||
},
|
||
}
|
||
```
|