milestone 17: real deadlines, validator holes, upstream editor, trusted proxies, contract samples, badvers
This commit is contained in:
@@ -53,8 +53,20 @@ Timeouts for talking to upstream resolvers.
|
||||
|
||||
| Key | Type | Default | Unit | Validation | Consumed by |
|
||||
|---|---|---|---|---|---|
|
||||
| `upstream.attempt_timeout_ms` | u32 | 2500 | ms | 100–120000, and not above `total_timeout_ms` | deadline on one attempt against one upstream inside the pool's failover loop (`src/upstream/pool.zig`), the whole attempt including the connect |
|
||||
| `upstream.read_timeout_ms` | u32 | 3000 | ms | 100–120000 | read deadline on conditional-forward-zone exchanges (`src/local/forward_client.zig`) |
|
||||
| `upstream.total_timeout_ms` | u32 | 5000 | ms | 100–120000, and not below `read_timeout_ms` | per-query budget of the upstream pool (`src/upstream/pool.zig`), the whole attempt including the connect; also the `nxdns check` probe deadline |
|
||||
| `upstream.total_timeout_ms` | u32 | 5000 | ms | 100–120000 | per-query budget of the upstream pool (`src/upstream/pool.zig`): every failover attempt together, not one of them; also the `nxdns check` probe deadline |
|
||||
|
||||
The two pool budgets nest. `attempt_timeout_ms` bounds one try against one
|
||||
upstream; when it expires the pool records the failure and moves to the next
|
||||
candidate. `total_timeout_ms` bounds the whole loop, so a query against five
|
||||
unreachable upstreams costs the total budget once, not five attempt budgets in
|
||||
a row. When the total expires the in-flight attempt is canceled and the query
|
||||
fails with a timeout.
|
||||
|
||||
`read_timeout_ms` is unrelated to both. It bounds a different subsystem — the
|
||||
conditional-forward-zone client — so no cross-check relates it to the pool's
|
||||
budgets, and it is free to sit above either of them.
|
||||
|
||||
### dns
|
||||
|
||||
@@ -86,9 +98,14 @@ What a blocked query gets back.
|
||||
|
||||
| Key | Type | Default | Unit | Validation | Consumed by |
|
||||
|---|---|---|---|---|---|
|
||||
| `cache.size` | u32 | 10000 | entries | none; 0 disables caching (puts short-circuit) | DNS answer cache capacity (`src/cache/dns_cache.zig`) |
|
||||
| `cache.size` | u32 | 10000 | entries | 1–1000000 | DNS answer cache capacity (`src/cache/dns_cache.zig`) |
|
||||
| `cache.negative_ttl_max` | u32 | 3600 | seconds | at most 86400 | cap on cached negative answers; 0 disables negative caching |
|
||||
|
||||
The cache's slot array is allocated in full at startup, so `cache.size` carries
|
||||
a ceiling: it is a sanity bound against a typo, not a promise that the value
|
||||
fits in the box's memory. There is no "off" value — to run without a cache, set
|
||||
`cache.size` to 1.
|
||||
|
||||
### web
|
||||
|
||||
The web interface and REST API.
|
||||
@@ -104,6 +121,7 @@ The web interface and REST API.
|
||||
| `web.api_rate_limit_per_min` | u32 | 300 | requests per minute | at least 1 | API token-bucket limiter (`src/web/api_limiter.zig`) |
|
||||
| `web.api_localhost_exempt` | bool | true | — | — | loopback requests skip the API limiter |
|
||||
| `web.sse_max_connections_per_ip` | u16 | 3 | connections | at least 1 | cap on concurrent SSE streams per client address |
|
||||
| `web.trusted_proxies` | string | `""` | — | comma-separated IP literals | reverse proxies whose `X-Forwarded-For` nxdns believes (`src/web/server.zig`); empty trusts none |
|
||||
|
||||
### doh_server
|
||||
|
||||
@@ -145,7 +163,7 @@ Process log and query log behavior.
|
||||
|---|---|---|---|---|---|
|
||||
| `logging.level` | enum `.err` \| `.warn` \| `.info` \| `.debug` | `.info` | — | one of the four tags; stored as `"error"` / `"warn"` / `"info"` / `"debug"` | log threshold (`src/platform/logging.zig`) |
|
||||
| `logging.retention_days` | u16 | 30 | days | at least 1 | query-log pruning cutoff (`src/storage/retention.zig`) and the client tracker's last-seen cutoff (`src/server/clients.zig`) |
|
||||
| `logging.query_log_buffer_max` | u32 | 10000 | entries | at least 1 | in-memory query-log ring size and backpressure cap (`src/storage/logger.zig`) |
|
||||
| `logging.query_log_buffer_max` | u32 | 10000 | entries | 1–1000000 | in-memory query-log ring size and backpressure cap (`src/storage/logger.zig`) |
|
||||
| `logging.hide_domains` | bool | false | — | — | the query log stores a hidden marker instead of the domain |
|
||||
| `logging.hide_client_ips` | bool | false | — | — | the query log stores a hidden marker instead of the client address |
|
||||
| `logging.output` | enum `.stderr` \| `.syslog` \| `.file` | `.stderr` | — | one of the three tags | log sink selection (`src/platform/logging.zig`); `.stderr` and `.syslog` both write to stderr (journald captures it), `.file` rotates |
|
||||
@@ -197,7 +215,7 @@ Upstream resolvers. At least one enabled upstream is required.
|
||||
|
||||
| Field | Type | Default | Validation |
|
||||
|---|---|---|---|
|
||||
| `url` | string | required | an `https://` (DoH) or `tls://` (DoT) endpoint accepted by `transport.Endpoint.parse`; unique |
|
||||
| `url` | string | required | an `https://` (DoH) or `tls://` (DoT) endpoint accepted by `transport.Endpoint.parse`; unique; a `tls://` host must be an IP literal |
|
||||
| `priority` | i32 | 100 | — (lower is tried first) |
|
||||
| `enabled` | bool | true | — |
|
||||
| `tls_name` | string | `""` | DoT only — a `tls_name` on an `https://` upstream is an error; when set it must be a valid domain name |
|
||||
@@ -207,6 +225,19 @@ ascending priority and tried in order with failover. `tls_name` sets SNI and the
|
||||
certificate verification name for a `tls://` upstream written as an IP literal;
|
||||
empty means "verify by the URL host" (`src/upstream/dot_client.zig`).
|
||||
|
||||
A `tls://` upstream's host must be an IP literal. nxdns does not resolve an
|
||||
upstream's own name — that is a bootstrap problem, and the DoT client refuses a
|
||||
non-literal host on every dial — so the validator rejects the hostname form
|
||||
rather than letting it fail at query time as an unreachable upstream. Write the
|
||||
address and put the name in `tls_name`:
|
||||
|
||||
```zig
|
||||
.{ .url = "tls://9.9.9.9:853", .tls_name = "dns.quad9.net" },
|
||||
```
|
||||
|
||||
A DoH upstream is not affected: `https://` goes through the HTTP client, which
|
||||
resolves normally, so a hostname there is the usual form.
|
||||
|
||||
### clients
|
||||
|
||||
Known clients with a fixed group assignment.
|
||||
@@ -344,18 +375,19 @@ path segment is the whole account identifier. The field path beside the message
|
||||
names the entry, so `blocklist_sources[1].url` still says which one to go and
|
||||
fix.
|
||||
|
||||
One credential this cannot remove: a NextDNS **DoT** upstream is
|
||||
`tls://abcd12.dns.nextdns.io`, which carries the same identifier in the hostname.
|
||||
Stripping it would leave no host at all and no line worth reading. So if you use
|
||||
NextDNS over DoT, your profile id appears in the log.
|
||||
What redaction cannot remove is the host, because a hostname is not a secret in
|
||||
the general case — it is resolved publicly and offered as SNI on every
|
||||
connection — and dropping it would leave a diagnostic that names nothing worth
|
||||
reading. A vendor that puts an account identifier in the hostname therefore has
|
||||
that identifier appear in any line naming the entry. NextDNS is the example:
|
||||
`abcd12.dns.nextdns.io` is a profile id.
|
||||
|
||||
The host is kept because a hostname is not a secret in the general case — it is
|
||||
resolved publicly and offered as SNI on every connection — and dropping it would
|
||||
cost every operator a diagnostic to cover one vendor's choice. If that trade is
|
||||
wrong for you, it is yours to make rather than ours: NextDNS also publishes a DoH
|
||||
endpoint, `https://dns.nextdns.io/abcd12`, whose identifier sits in the path and
|
||||
is redacted in full. Configure that form instead and nothing identifying reaches
|
||||
the log.
|
||||
Two things limit the exposure. A hostname `tls://` upstream is rejected by the
|
||||
validator (see `upstreams` above), so the DoT form of that URL can only reach
|
||||
the log once, in the line rejecting it — never on every failover. And NextDNS
|
||||
publishes a DoH endpoint, `https://dns.nextdns.io/abcd12`, whose identifier sits
|
||||
in the path and is redacted in full; configure that form and nothing identifying
|
||||
reaches the log at all.
|
||||
|
||||
The error set is `validate.ValidateError` in `src/config/validate.zig`:
|
||||
|
||||
@@ -363,6 +395,7 @@ The error set is `validate.ValidateError` in `src/config/validate.zig`:
|
||||
| --- | --- |
|
||||
| `NoUpstreams` | no upstream has `enabled = true` |
|
||||
| `BadUpstreamUrl` | an upstream `url` is not an `https://` or `tls://` endpoint |
|
||||
| `UpstreamHostNotIpLiteral` | a `tls://` upstream `url` names a host instead of an IP literal |
|
||||
| `DuplicateUpstreamUrl` | two upstreams share a `url` |
|
||||
| `BadTlsName` | a `tls_name` is not a valid domain name |
|
||||
| `TlsNameOnNonTlsUpstream` | a `tls_name` on a non-`tls://` upstream |
|
||||
@@ -378,13 +411,15 @@ The error set is `validate.ValidateError` in `src/config/validate.zig`:
|
||||
| `BadLocalRecordName` / `BadLocalRecordValue` / `DuplicateLocalRecord` | local record fields |
|
||||
| `BadForwardZone` / `DuplicateForwardZone` / `BadResolverUrl` | forward zone fields |
|
||||
| `BadPort` | a port field is 0 |
|
||||
| `BadTimeout` | a timeout is outside 100–120000 ms, or `total` is below `read` |
|
||||
| `BadTimeout` | a timeout is outside 100–120000 ms, or `attempt` is above `total` |
|
||||
| `BadTtl` | `blocking.ttl`, `cache.negative_ttl_max`, a record `ttl`, `web.session_ttl_hours` or `blocklist_update.interval_hours` outside its range |
|
||||
| `BadRetention` | `logging.retention_days` or `logging.query_log_buffer_max` below 1 |
|
||||
| `BadCacheSize` | `cache.size` outside 1–1000000 |
|
||||
| `BadRetention` | `logging.retention_days` below 1, or `logging.query_log_buffer_max` outside 1–1000000 |
|
||||
| `BadLogRotation` | `logging.max_size_mb` or `logging.max_files` below 1 |
|
||||
| `BadDiskThresholds` | a threshold below 1, or `min_free_mb` above `warn_free_mb` |
|
||||
| `BadRateLimit` | `dns.rate_limit`, `dns.rate_window_seconds`, `web.api_rate_limit_per_min` or `web.sse_max_connections_per_ip` out of range |
|
||||
| `BadBindAddress` | a bind field is not an IP address, or not of the required family |
|
||||
| `BadTrustedProxy` | an element of `web.trusted_proxies` is not an IP literal |
|
||||
| `MissingCertPath` / `MissingKeyPath` | an enabled TLS listener with an empty path |
|
||||
| `MissingLogPath` | `logging.output = .file` with an empty or relative `file_path` |
|
||||
| `PasswordAndHashBothSet` | both `web.password` and `web.password_hash` are set |
|
||||
|
||||
Reference in New Issue
Block a user