> Source: https://nexusatlas.dev/docs/config — Nexus Atlas developer documentation (Reference). Converted from the HTML page; the page is canonical.

# Configuration reference

Every key the daemon parses from `/etc/atlas/config.toml`, with types, defaults and behavior. This page is generated from the same schema the daemon generates from its own configuration structs (`atlasd config schema`), so it cannot drift from what a release actually accepts. Add `#:schema` pointing at that schema file to your config and a TOML-aware editor validates as you type.

> **Validate before you start:** `atlasd config check -c /etc/atlas/config.toml` runs the daemon’s exact load pipeline — exit 0 means loadable, exit 1 means the daemon would refuse to start. Sections marked *opt-in* leave the data path byte-identical to a build without the feature until enabled.

[[interface]required](https://nexusatlas.dev/docs/config#cfg-interface) · [[[link]]required](https://nexusatlas.dev/docs/config#cfg-link) · [[[peer]]](https://nexusatlas.dev/docs/config#cfg-peer) · [[scheduler]](https://nexusatlas.dev/docs/config#cfg-scheduler) · [[link_monitor]](https://nexusatlas.dev/docs/config#cfg-link_monitor) · [[crypto]](https://nexusatlas.dev/docs/config#cfg-crypto) · [[auth]](https://nexusatlas.dev/docs/config#cfg-auth) · [[routing]](https://nexusatlas.dev/docs/config#cfg-routing) · [[multicast]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-multicast) · [[mesh]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-mesh) · [[dns]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-dns) · [[traversal]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-traversal) · [[qos]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-qos) · [[telemetry]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-telemetry) · [[metrics]on by default](https://nexusatlas.dev/docs/config#cfg-metrics) · [[journal]on by default](https://nexusatlas.dev/docs/config#cfg-journal) · [[syslog]opt-in · default off](https://nexusatlas.dev/docs/config#cfg-syslog) · [[reporting]on by default](https://nexusatlas.dev/docs/config#cfg-reporting) · [[logging]](https://nexusatlas.dev/docs/config#cfg-logging)

## [interface]
required

### `address` — string
required
Tunnel address of this node as IPv4 CIDR, e.g. `10.0.100.1/24`. Bare addresses imply /24.

### `listen_port` — integer
required
UDP source port every link binds by default (WireGuard-style single port). Per-link `listen_port` overrides it.

### `name` — string
required
TUN interface name (e.g. `nexus0`). Identifies this daemon instance in stats files, the web UI and the CLI.

### `private_key` — string
required
Base64 Curve25519 private key (from `atlasd keygen`). Keep the file mode 0600 — this is the node's identity.

### `description` — string
default `unset`
Brief note describing what this atlas instance is for, e.g. "office to vehicle" or "ground station to aircraft". Surfaced in the web UI header beside the interface name so an operator can tell multiple atlas instances apart at a glance. Local-only — not advertised in LSAs.

### `endpoint_roaming` — boolean
default `true`
Endpoint roaming (WireGuard semantics, per link): when an AUTHENTICATED packet from a peer arrives from a new source address, adopt it as that peer's endpoint for the receiving link. Lets a mobile node (drone re-associating to a different ground radio / mesh relay) keep the tunnel alive without restarts. On by default; static fleets are unaffected because their traffic already arrives from the configured endpoints. Probes/keepalives are unauthenticated and never steer endpoints.

### `mtu` — integer
default `1420`
TUN device MTU. Default 1420 (1500 minus tunnel overhead).

### `node_name` — string
default `unset`
Human-readable label for this node, advertised in our LSAs so peers can show it in their topology view. Optional — when omitted the daemon falls back to the system hostname.

### `node_role` — string
default `unset`
What this node physically is, advertised in our LSAs beside the position so peers can tell a drone from a rover from a mast without guessing from the name. Purely descriptive — nothing in the data path reads it; the topology views (atlas-sim, dashboard) use it to pick a symbol. Well-known values are listed in `KNOWN_NODE_ROLES` (`ground-station`, `uav`, `ugv`, `usv`, `dismount`, `relay`); anything else is passed through unchanged with a config-check warning, so a fleet can coin its own.

### `position` — array
default `unset`
Static position of this node as `[lat_deg, lon_deg, alt_m]` (WGS-84). Advertised in our LSAs for peers' topology prediction and relay planning. Fixed installations set it here; mobile nodes instead stream fixes through the telemetry socket (`node_position` JSON messages), which override this value.

## [[link]]
required
One underlay link of the bond: a UDP socket pinned to a physical device. `type = "relay"` makes it a relay link (peer traffic wrapped in encrypted envelopes through a Nexus Atlas relay server).

### `bind_address` — string
required
Local IP the socket binds. `0.0.0.0` = any IPv4; a bare IPv6 address selects the v6 family.

### `bind_device` — string
required
Network device the link's UDP socket is bound to (SO_BINDTODEVICE), e.g. `eth0`, `wlan1`, `ppp-sik`.

### `name` — string
required
Link name, unique within this config. Referenced by scheduler `priority_order`, peer `endpoint_per_link` and telemetry events.

### `bandwidth_mbps` — number

Legacy capacity field, always in Mbps. Overridden by `capacity` when both are set.

### `capacity` — number | string

Nominal capacity of the physical link — the reference ceiling the scheduler's cost functions divide by. Unit-tagged string (`"64 kbps"`, `"100 Mbps"`, `"1.5 Gbps"`, case-insensitive) or a bare number in Mbps. Default 80 Mbps when neither this nor `bandwidth_mbps` is set.

### `check_interval_ms` — integer

Accepted alias for `probe_interval_ms`.

### `enabled` — boolean
default `true`
Set false to keep the link configured but unused. Default true.

### `listen_port` — integer

Per-link UDP source port overriding `[interface] listen_port`. REQUIRED when two links share bind_device+bind_address (SO_REUSEPORT would otherwise load-balance them against each other).

### `max_range_m` — number

Maximum usable radio range in meters. Only meaningful with `[link_monitor] prediction_source = "position"`/"both": scheduler-facing loss inflates as the projected peer distance approaches it. Default: the global `range_full_m`.

### `probe_interval_ms` — integer

Per-link probe (health-check) interval in ms, overriding the global `[link_monitor] probe_interval_ms` for this link only.

### `quota` — integer | string

Metered-link budget: wire bytes this link may carry per `quota_period`. Unit-tagged string (kB/MB/GB/TB are decimal, KiB/MiB/GiB/TiB binary) or a bare number of bytes. Omit for an unmetered link. An over-budget link is not promoted by the scheduler while another link is usable, but is still used when it is the last path.

### `quota_period` — string
default `"monthly"`
Window the `quota` applies to: `monthly` (default), `weekly`, `daily`, or a sliding `rolling-<N>d` window.

### `quota_reset_day` — integer
default `1`
Anchor for the calendar periods: day-of-month for `monthly` (1-31, clamped to the month's length), ISO day-of-week for `weekly` (1 = Monday … 7 = Sunday). Ignored by `daily` and `rolling-<N>d`.

### `quota_warn_pct` — integer
default `80`
Percent of the budget at which the link is reported as warning (journal event `link.quota_warn`, `warning` in the stats snapshot). 0 disables the warning.

### `range_warn_m` — number

Distance in meters where this link's range penalty starts ramping. Default 60% of `max_range_m` when that is set, else the global `range_warn_m`.

### `relay_pubkey` — string

Base64 static public key of that relay, pinned — authenticates the relay itself.

### `relay_server` — string

`ip:port` of the relay server (its STUN/relay port, conventionally 3478). Required for `type = "relay"` unless traversal discovery resolves it.

### `route_advert_interval_secs` — integer

Link-state flooding interval for THIS link in seconds, overriding the global `[mesh] route_advert_interval_secs`. Lets a narrow UHF bearer flood slowly while a wideband one keeps the fast cadence. Clamped to 30 s so a peer can never age this node's LSA out.

### `type` — enum
default `"direct"`
`direct` (default) or `relay`. A relay link carries peer traffic through a Nexus Atlas relay server and needs `relay_server` + `relay_pubkey` (or traversal discovery).

`direct``relay`

### `unusable_rtt_ms` — number

Round-trip time in ms past which a returning probe on THIS link stops counting as evidence the link works. Overrides the global `[link_monitor] unusable_rtt_ms`; 0 disables the rule for this link.

### `weight` — integer
default `1`
Relative weight for the `weighted_round_robin` strategy. Default 1.

## [[peer]]

### `allowed_ips` — array of string
required
Tunnel IPv4 CIDRs routed to this peer (WireGuard semantics), e.g. `["10.0.100.2/32"]`. Bare addresses imply /32.

### `public_key` — string
required
Base64 Curve25519 public key of the peer — its identity.

### `endpoint` — array of string
default `[]`
Known endpoints, positionally per link. **Optional**: a peer reached through a relay — or one that will only ever be reached by hole punching — has no endpoint at all. Its identity is its public key, and its address is discovered at runtime (from the relay envelope, or from a punched candidate). Requiring an endpoint here would make the whole traversal stack unusable, since "I do not know where this peer is" is precisely the problem it exists to solve.

### `endpoint_per_link` — table

Explicit link-name → endpoint mapping. When non-empty, atlasd uses *only* this map for outbound sends to this peer: a link whose name has no entry here is not used to reach the peer at all. Useful when each radio has its own point-to-point IP pair (e.g. a multi-modem setup) so a packet that's bound to one link's interface can't be routed to the *other* link's remote IP without bypassing kernel iface pinning.
When empty (default), the legacy positional behaviour applies: `endpoint[i]` is used for link `i`, with fallback to `endpoint[0]` when the index is out of range.

```
[[peer]]
public_key = "..."
endpoint = []  # not used when endpoint_per_link is set
endpoint_per_link = { "sik-hb" = "10.0.1.2:51820",
                      "sik-rfd" = "10.0.2.2:51820" }
allowed_ips = ["10.0.100.2/32"]
```

### `persistent_keepalive` — integer
default `unset`
Send a keepalive to this peer every N seconds so NAT/stateful firewall mappings stay open. Omit to disable (default).

## [scheduler]

### `adaptive_dwell_ms` — integer
default `5000`
Minimum dwell (ms) before the adaptive strategies de-escalate to fewer links; escalation is immediate. Default 5000.

### `adaptive_mirror_enter_ratio` — number
default `0.9`
Enter `n=2` when primary_cost / next_cost ≥ this. Default 0.9.

### `adaptive_mirror_exit_ratio` — number
default `0.5`
Drop back to `n=1` only when primary_cost / next_cost ≤ this. Default 0.5 (Schmitt gap of ~2× prevents flapping).

### `adaptive_triplicate_enter_ratio` — number
default `0.5`
Enter `n=3` when primary_cost / third_cost ≥ this. Default 0.5.

### `adaptive_triplicate_exit_ratio` — number
default `0.25`
Drop from `n=3` to `n=2`/`n=1` only when primary_cost / third_cost ≤ this. Default 0.25.

### `data_loss_probe_every_n` — integer
default `10`
Piggyback a loss-probe sequence on every Nth real fragment so the peer can measure data-fragment loss directly (instead of just probe loss, which underestimates burst-loss damage on full-size fragments). Set to 0 to disable. Only takes effect when `strategy = "adaptive"`. Default 10.

### `latency_mirror_ms` — number
default `40`
Engage `n=2` when the best link's RTT ≥ this many ms. Default 40.

### `latency_triplicate_ms` — number
default `100`
Engage `n=3` when the best link's RTT ≥ this many ms. Default 100.

### `loss_mirror_pct` — number
default `20`
Engage `n=2` when the best link's loss ≥ this percent. Default 20.

### `loss_triplicate_pct` — number
default `50`
Engage `n=3` when the best link's loss ≥ this percent. Default 50.

### `priority_order` — array of string
default `[]`
Link names in priority order, highest-priority first. Only honoured when `strategy = "priority"`. Names are matched against `[[link]].name` entries; unknown names are ignored with a warning at engine startup.

### `redundancy_factor` — integer
default `1`
Send every fragment over up to this many of the best alive links at once. 1 (default) = single path. The adaptive strategies manage their own redundancy and ignore this.

### `strategy` — enum
default `"lowest_delivery_time"`
Multipath scheduling strategy. Default `lowest_delivery_time`.

`lowest_delivery_time``weighted_round_robin``broadcast``priority``shortest_path``adaptive``loss_adaptive``latency_adaptive`

## [link_monitor]

### `dead_probe_count` — integer
default `5`
Consecutive missed probes before a link is declared dead. Default 5.

### `prediction_source` — enum
default `unset`
What feeds predictive scheduling/topology decisions: "none" (default), "rssi", "position", or "both". - rssi:     local RSSI trend inflates scheduler-facing loss (see rssi_floor_dbm/rssi_lookahead_secs), and LSA-advertised per-link RSSI weights SPF costs. - position: LSA-advertised node positions/velocities are projected ahead; stretched links get expensive in SPF and the local scheduler (range_warn_m/range_full_m). - both:     max of the two risks per link. Prediction inflates costs/loss only — probes remain the sole authority on link death, and dashboards keep measured values.

`none``rssi``position``both`

### `probe_interval_ms` — integer
default `250`
Probe (health-check) interval per link, in milliseconds. Default 250. Per-link `probe_interval_ms` overrides it.

### `range_full_m` — number
default `1000`
Projected inter-node distance (m) where the position-based penalty reaches its maximum.

### `range_warn_m` — number
default `600`
Projected inter-node distance (m) where the position-based penalty starts ramping.

### `rssi_floor_dbm` — number
default `-88`
RSSI (dBm) below which a link is considered effectively dead for prediction purposes.

### `rssi_lookahead_secs` — number
default `2`
How far ahead (seconds) the RSSI trend is projected.

### `rssi_predictive` — boolean
default `false`
Legacy switch (predates `prediction_source`): `true` behaves as `prediction_source = "rssi"` when prediction_source is unset.

### `unusable_rtt_ms` — number
default `10000`
Round-trip time past which a probe that DOES come back stops counting as evidence the link works, in milliseconds. Default 10000; 0 turns the rule off.
Without it, liveness means "a round trip completed eventually", which a deeply queued link satisfies forever: probes return tens of seconds late, probe loss reads 0%, the link reads `up`, and traffic through it measures 100% loss. Probes past this ceiling are counted as misses, so the ordinary `dead_probe_count` rule declares the link dead — no separate state machine, and one late probe still cannot take a link down. Per-link `[[link]] unusable_rtt_ms` overrides it.

## [crypto]

### `disable_encryption` — boolean
default `false`
When true, payloads are sent in plaintext (no AEAD). Useful for performance testing. Both peers must have this set identically.

### `rekey_data_limit` — integer
default `1073741824`
Also rekey after this many payload bytes on a session. Default 1073741824 (1 GiB).

### `rekey_interval_secs` — integer
default `120`
Re-run the Noise handshake with every peer after this many seconds. Default 120.

## [auth]

Pluggable peer authentication (`[auth]`).
Controls what happens when a HandshakeInit arrives from a static public key that is NOT in the `[[peer]]` list. Configured peers are always accepted (they carry explicit operator intent) — the provider only ever *extends* trust to unknown keys, so the default is exactly today's behaviour: drop them.

```
[auth]
provider = "keyfile"
keyfile = "/etc/atlas/authorized_peers.toml"
```

Providers: - `"static"` (default): unknown keys are rejected. Identical to a config with no `[auth]` table. - `"keyfile"`: unknown keys are looked up in `keyfile`, a TOML allowlist of `[[peer]] public_key = "...", allowed_ips = [...]` entries. The file is re-read when its mtime changes, so keys can be granted/revoked without restarting the daemon (revocation stops NEW sessions; established sessions live until rekey). - `"command"`: unknown keys are passed to an external hook — `command <base64-pubkey> <source-addr>` — which decides. Exit 0 authorizes; each stdout line is an allowed-IPs CIDR for the peer. Any other exit status denies. This is the integration point for an external control plane (console enrollment DB, LDAP, …).
Library embedders can bypass all of this and inject their own implementation of `auth::PeerAuthenticator` via `Engine::with_authenticator`.

### `command` — string
default `unset`
Hook executable for `provider = "command"`.

### `command_timeout_ms` — integer
default `5000`
Hard cap on how long the `command` hook may run before the handshake is dropped (the hook keeps running; its verdict is simply too late).

### `deny_cache_secs` — integer
default `30`
A denied key is not re-checked for this long — keeps a handshake flood from an unauthorized key from hammering the file/hook.

### `keyfile` — string
default `unset`
Allowlist file for `provider = "keyfile"`.

### `provider` — enum
default `"static"`
`static` (default, unknown keys rejected), `keyfile`, or `command`. Misconfiguration refuses to start.

`static``keyfile``command`

## [routing]

Routing knobs.
- `path_redundancy` drives the 4.2 "dual-SPF" scenario: when K = 1 (default) atlas behaves as before, SPF picks one shortest path per destination; when K ≥ 2 atlas computes top-K next-hops and fans every outgoing/forwarded fragment across all of them with a receive-side dedup window. Killing any single link in the diamond becomes packet-loss-free because the alternate path is already carrying live traffic.
- `static_path` drives the 4.3 "static multipath" scenario: when one or more entries are present, atlas stops consulting SPF for forwarding entirely and routes purely off the operator-declared path table. Each path is a full hop list of tunnel IPs and carries a stable `path_id` that is stamped into the relay header so every intermediate node can pick the right next-hop without a database lookup. SPF/probing still runs (for telemetry); only the forwarding decision is overridden.
The two knobs are mutually exclusive: when `static_path` is non-empty the static FIB wins and `path_redundancy` is ignored.

### `path_redundancy` — integer
default `1`
Top-K next-hops to fan out across. 1 = single-path (default, unchanged behaviour). 2+ = dual/multi-SPF.

### `static_path` — array of tables
default `[]`
Operator-declared static paths. Empty (default) = SPF / dual-SPF routing. Non-empty = static-multipath: forwarding uses these entries exclusively.

Each entry:

- `hops` array of string required — Full path as a sequence of tunnel IPv4 addresses (origin first, final destination last). At least two entries required.

- `name` string required — Human-readable label, surfaced in stats/logs.

- `path_id` integer required — Stable u16 path identifier, stamped into `RelayHeader.path_id`.

## [multicast]
opt-in · default off
Subscriber-aware multicast distribution trees (`[multicast]`). **Opt-in. Default OFF.** Off, inner multicast keeps the flat-mesh semantics: a packet to `224.0.0.0/4` is replicated at the source, once per peer. On, traffic to a group listed in `groups` is carried as a source-rooted SPF distribution tree instead — one copy per shared segment, forked only where subscriber paths diverge. Subscriptions are advertised in the LSA flood (additive, wire-compatible field); every node that sources or forwards a tree group should carry the same `groups` list.

```
[multicast]
enabled = true
groups = ["239.10.10.10"]        # tree-distributed groups (fleet-wide)
subscribe = ["239.10.10.10"]     # deliver these to THIS node's TUN
```

### `enabled` — boolean
default `false`
Master switch.

### `groups` — array of string
default `[]`
Multicast groups (IPv4, `224.0.0.0/4`) distributed as trees. Groups not listed here keep the flat per-peer fan-out.

### `subscribe` — array of string
default `[]`
Groups this node subscribes to: advertised to the mesh and delivered to the local TUN (the kernel still filters by the application's own group joins, exactly as with flat fan-out).

## [mesh]
opt-in · default off
Mesh control plane configuration.

### `bootstrap_peers` — array of string
default `[]`
Bootstrap peer endpoints for initial join.

### `config_propagation` — boolean
default `true`
Whether mesh-wide config changes propagate to all nodes.

### `enabled` — boolean
default `false`
Enable mesh mode (gossip-based CRDT peer discovery).

### `expiry_secs` — integer
default `60`
Seconds after which a non-heartbeating node is considered dead.

### `gossip_full_sync_secs` — integer
default `60`
How long a gossip target may go without a full roster re-send. Between full syncs a target receives only members whose clock moved since we last spoke to it, so this is the bound on repairing a document that was lost in flight. Default 60 s.

### `gossip_interval_secs` — integer
default `5`
Gossip interval in seconds (how often to sync state with a random peer).

### `gossip_v1_compat` — boolean
default `false`
Accept AND emit legacy plaintext (truncated-hash) gossip bodies for mixed fleets still running pre-v2 daemons. Default false: v2 (encrypted NAG2) only, and inbound v1 is dropped with a warning.

### `heartbeat_interval_secs` — integer
default `10`
Heartbeat interval in seconds.

### `max_gossip_bytes` — integer
default `1330`
Ceiling on one gossip document, in serialized JSON bytes.
The document goes out as a single UDP datagram: oversize it and the IP layer fragments it, at which point ONE lost fragment discards the entire membership update. Capping it turns that into a page — the members that did not fit lead the next document to the same target. Default 3000 (~config block + 3 members). Set 0 to disable the cap and send the whole roster in one datagram (pre-paging behaviour).

### `mesh_secret` — string
default `""`
Shared secret for mesh authorization (base64-encoded).

### `node_token_reads` — boolean
default `true`
Let mesh members read this node's `/api/config` and `/api/stats` with a token derived from `mesh_secret` (`atlasd config node-token`), even under `[web] protect = "all"`. Read-only and window-limited; it never creates a session and never authorises a write. Default true — `protect = "all"` is a deliberate lockdown, so set this false to close machine reads as well (the fleet compare then shows this node as unauthorized, as before).

### `role` — string
default `unset`
This node's role for signed config epochs: an epoch's `[roles.<name>.<section>]` overlay applies only to nodes whose role matches, and `target_roles` gates whether an epoch is applied here at all. Optional; unset = untargeted by role-scoped epochs.

### `route_advert_interval_secs` — integer
default `2`
Route advertisement interval in seconds.

### `route_expiry_secs` — integer
default `90`
Route expiry in seconds (advertised routes removed after this).

### `seed_refresh_secs` — integer
default `60`
How often to re-contact `bootstrap_peers` once every seed is in the roster, in seconds.
Joining is not a one-shot: two halves of a mesh that have each converged internally share no member, so nothing but the seed list spans the split and gossip alone can never merge them. Re-sending a MeshJoin to the seeds on a slow timer is what makes a partition heal. Default 60 s; 0 disables the refresh (join once, as before).

## [dns]
opt-in · default off
Mesh DNS (`[dns]`) — a per-node local resolver answering mesh names straight from the LSDB.
**Opt-in. Default OFF.** With `enabled = false` no socket is opened and no task is spawned. When on, the daemon binds a small DNS server (UDP + TCP) to the tunnel address and answers A/AAAA/PTR for `<node_name>.<domain>` from its own link-state database — no server anywhere in the mesh, no coordination plane, and each partition keeps resolving its own half's names because each half has its own LSDB.
DNS is a tenant of the tunnel, never a prerequisite for it: peer endpoints remain IP literals and nothing in tunnel establishment ever performs a lookup.

```
[dns]
enabled = true
domain = "atlas"
mode = "split"

[[dns.override]]
name = "video.acme.com"
a = ["10.0.100.7"]

[[dns.forward]]
suffix = "corp.acme.com"
servers = ["10.0.100.53"]
```

### `domain` — string
default `"atlas"`
Zone suffix; a node's FQDN is `<node_name>.<domain>`. Must already be DNS-safe (lowercase `[a-z0-9-.]`).

### `enabled` — boolean
default `false`
Master switch.

### `forward` — array of tables
default `[]`
Per-suffix forwards (`[[dns.forward]]`): a customer's internal resolver, typically reachable only over the tunnel.

Each entry:

- `servers` array of string required — Resolvers for the suffix, as IP or `IP:port` (port defaults to 53).

- `suffix` string required — Domain suffix (e.g. `corp.acme.com`) — matches the suffix itself and everything under it.

### `listen` — array of string
default `[]`
Listen addresses as `IP:port` (a bare IP gets port 53). Empty (the default) means the `[interface]` tunnel address on port 53. `config check` HARD-REFUSES wildcards (`0.0.0.0`/`[::]`) — a mesh resolver on a public interface is an open-resolver / amplification hazard — and warns when an address is neither the tunnel address nor loopback.

### `mode` — enum
default `"split"`
`split` (default): mesh zone answered locally, everything else relayed to `upstream`. `mesh-only` (EMCON posture): out-of-zone queries answered REFUSED — no query ever leaves on an uplink.

`split``mesh-only`

### `override` — array of tables
default `[]`
Split-horizon overrides (`[[dns.override]]`): names answered authoritatively with tunnel-side addresses when queried over the tunnel, regardless of zone — outside, public DNS answers.

Each entry:

- `a` array of string — IPv4 answers.

- `aaaa` array of string — IPv6 answers.

- `cname` string — CNAME target. Exclusive of `a`/`aaaa`.

- `name` string required — Exact name to answer for (e.g. `video.acme.com`).

### `serve_reverse` — boolean
default `true`
Serve PTR (reverse) zones over the mesh's tunnel prefixes.

### `upstream` — array of string
default `["9.9.9.9","149.112.112.112"]`
Upstream resolvers for out-of-zone queries in `split` mode, as IP or `IP:port` (port defaults to 53). Default: Quad9.

### `upstream_timeout_ms` — integer
default `800`
Hard deadline for an upstream answer, milliseconds. Expiry answers SERVFAIL — a dead upstream must never become a hang, and a contested uplink never blocks in-mesh resolution.

## [traversal]
opt-in · default off
NAT traversal (the ".net" connectivity stack). **Opt-in. Default OFF.** With `enabled = false` (or no `[traversal]` table at all) no STUN demux runs, no netcheck task is spawned, and the data path is byte-identical to a build without traversal.
Phase 0 scope: netcheck — per-link STUN probes against two-IP observation points (our relays double as STUN servers), NAT mapping classification, and a JSON report at `/tmp/atlas/<iface>.netcheck.json` for the web UI / status CLI.

### `auto_relay` — boolean
default `false`
Auto-election of the shared relay set. When enabled (traversal on), the daemon periodically STUN-probes every configured relay link, advertises that RTT vector to each peer over the relay control channel, and — from the deterministic joint ranking both ends independently compute — keeps exactly `relay_paths` relay sessions **active**, gating the rest. A pair thus auto-converges on the same shared relay(s) with no negotiation, and re-elects the same next-best relay on failure. Off (default) = every configured relay link stays active, i.e. the legacy static behaviour.

### `discover` — boolean
default `true`
Runtime relay discovery. When enabled (and traversal is on), a relay link that omits `relay_server`/`relay_pubkey` resolves its relay from the signed relay map fetched from the control plane at startup. Static per-link config always wins when present. A no-op if every relay link is statically configured.

### `enabled` — boolean
default `false`
Master switch.

### `netcheck_interval_secs` — integer
default `300`
Netcheck re-run cadence in seconds (it always runs once shortly after startup). 0 disables the periodic re-run.

### `netcheck_timeout_ms` — integer
default `1500`
Per-request STUN response timeout in milliseconds.

### `punch` — boolean
default `true`
Hole punching: exchange candidates with peers over the relay and try to bring up a DIRECT path alongside the relayed one. On by default when traversal is enabled — a failed punch costs nothing (the relay keeps carrying the traffic), and a successful one removes a hop and the relay's bandwidth bill.

### `region` — string
default `unset`
Home-region hint for relay selection, e.g. `"eu-central"`. `None` → pick the lowest-RTT relay across all regions.

### `relay_map_url` — string
default `unset`
Override the control-plane base URL the signed relay map is fetched from (`{base}/v1/relays`). `None` → the baked-in default.

### `relay_paths` — integer
default `1`
How many relays to hold a session to **concurrently** for a peer, from the deterministically-ranked preference list (best-first). `1` (default) = one shared relay per pair, re-elected on failure. `2` = the best two relays simultaneously for redundancy, etc. A punched DIRECT path (if any) is always additional to these. Clamped to at least 1.

### `stun_servers` — array of string
default `[]`
STUN observation points as `IP:port`. Two entries with DIFFERENT IPs are required for mapping classification (a single relay's address pair is exactly that). Later phases populate this from the signed relay map; static config always wins when set.

## [qos]
opt-in · default off
QoS / transport-class data-path configuration (features K1–K4 + K3 store-carry-forward).
**Opt-in. Default OFF.** When `enabled = false` (the default, and the state for any config that has no `[qos]` table at all), the data path behaves byte-identically to a build without QoS: classification, the class queue, per-class multipath and the bundle store are never constructed and the legacy outbound/inbound paths run unchanged.
The active policy is loaded at startup from `/tmp/atlas/<iface>.qos.json` (the same file the web UI / daemon writes) and re-read every `reload_secs` so an operator's "Apply" takes effect live. An absent or corrupt file falls back to `PolicyTable::tactical_default`.

### `backlog_bytes` — integer
default `4000000`
ClassQueue AQM byte budget across all classes.

### `bundle_backend` — enum
default `"memory"`
Bundle durability backend: `"memory"` | `"file"`.

`memory``file`

### `bundle_enabled` — boolean
default `true`
K3 store-carry-forward for the StoreCarryForward (Bulk) class.

### `bundle_lifetime_ms` — integer
default `600000`
Per-bundle lifetime in milliseconds before it expires unsent.

### `bundle_max_bytes` — integer
default `16000000`
Total bundle payload byte cap.

### `bundle_max_count` — integer
default `10000`
Total bundle count cap.

### `bundle_path` — string
default `""`
Write-ahead-log path when `bundle_backend = "file"`. Per-iface OK.

### `deadline_ceil_ms` — integer
default `1000`
Upper clamp on the derived deadline budget, milliseconds.

### `deadline_floor_ms` — integer
default `80`
Lower clamp on the derived deadline budget, milliseconds.

### `deadline_k` — number
default `3.5`
Deadline budget multiplier: `budget = k * worst alive-link RTT`.

### `enabled` — boolean
default `false`
Master switch. `false` (default) = legacy data path, no QoS.

### `fec_block_packets` — integer
default `8`
FEC block size `k`: number of data packets batched before parity shards are appended and the block is shipped.

### `fec_block_timeout_ms` — integer
default `200`
FEC RX expiry: give up on an incomplete block after this many milliseconds (deliver the data shards that arrived, drop the rest).

### `fec_enabled` — boolean
default `false`
Forward-error-correction master switch. Opt-in; `false` (default) = no FEC even for classes whose policy is `Fec`. When `true`, any class whose `MultipathPolicy` is `Fec { overhead_pct }` has its packets batched into blocks, parity shards generated, and lost packets reconstructed on the receiver with NO retransmission. Doubly gated: this switch AND the per-class `Fec` policy must both be set.

### `fec_flush_ms` — integer
default `60`
FEC TX flush: emit a partial block after this many milliseconds of idle so latency stays bounded when fewer than `k` packets are in flight.

### `reload_secs` — integer
default `2`
How often to re-read the `<iface>.qos.json` policy file, seconds. `0` = never reload (load once at startup).

### `rx_hold_ms` — integer
default `120`
Relative arrival-based hold time for the RX release buffer, in milliseconds. Each Video packet is held ~this long from local arrival then released in sequence order. `0` falls back to the default (120); never unbounded.

### `rx_max_items` — integer
default `1024`
Per-peer cap on buffered Video packets in the RX release buffer. On overflow the lowest sequence is force-released early.

### `rx_release_buffer` — boolean
default `false`
Receiver-side release buffer (jitter / reorder buffer with late-drop). Opt-in master switch; `false` (default) = legacy RX path. Only ever applied to the `Video` (DeadlineBounded) class — never to any other class or reliable traffic.

## [telemetry]
opt-in · default off
Out-of-band telemetry feed from per-radio adapter processes (e.g. `atlas-sik`). Adapters open a Unix domain socket here and push newline-delimited JSON `LinkTelemetry` events. Each event names a link (matching `[[link]] name`) and carries radio-native signals — RSSI, noise floor, TX buffer free space — that the OS can't see.
Strictly opt-in. Disabled by default; the scheduler doesn't depend on any of this for correctness.

### `console_report` — boolean
default `true`
Report link telemetry to the customer console (console.nexusatlas.net) every ~30 s. Opt-out — `true` by default, but a node that was never enrolled sends at most one token-mint request (the console answers 404 `not_enrolled` and the reporter stops for the daemon's lifetime). Independent of `enabled`, which gates only the local adapter socket.

### `console_url` — string
default `unset`
Console base URL override. Resolution order (highest wins): `NEXUS_ATLAS_CONSOLE_URL` env var, this key, the built-in default (`https://console.nexusatlas.net`) — same chain the enroll flow and the web dashboard use.

### `enabled` — boolean
default `false`
Accept adapter connections on `socket_path`. Default false.

### `legacy_socket_compat` — boolean
default `true`
Also answer to the pre-rename socket path (`/run/nexus-atlas/telemetry.sock`) by symlinking it onto whatever `socket_path` bound, so adapters whose units still name it keep connecting during a staged fleet upgrade. Default true. Never clobbers a real socket already sitting there.

### `socket_path` — string
default `"/run/atlas/telemetry.sock"`
UDS path the adapter connects to. atlasd creates the parent directory and removes any stale socket file at startup.

## [metrics]
on by default
Prometheus / OpenMetrics exposition (`GET /metrics` on the web server).
**On by default.** "Does it feed our NMS?" is the first question a systems integrator asks, and the answer has to be yes without an operator finding a switch first. It costs nothing when nobody scrapes: the endpoint renders from the snapshot documents the daemon already writes, in the `atlasd web` process, so the forwarding daemon is not on the path at all.
The endpoint is a plain READ and the node's existing `[web] protect` posture governs it — open on a `protect = "writes"` node, session- or token-gated on a `protect = "all"` node, exactly like `/api/stats`.

### `enabled` — boolean
default `true`
Master switch. `false` makes the endpoint answer 404.

### `path` — string
default `"/metrics"`
Exposition path. Change it only when something else on the node already owns `/metrics`; scrapers assume the default.

## [journal]
on by default
Persistent event journal (`/var/lib/atlas/journal/<iface>.jsonl`).
**On by default.** Every other view of the node is a live value: once a link flaps back up, nothing on disk remembers that it went down. The journal is what makes "what happened at 14:32?" answerable after the fact, so it has to be on for the incident nobody planned for.
The two size knobs are a hard ceiling, not a target: `max_files x max_file_bytes` is the most disk one interface's journal can ever use (default 4 x 2 MiB = 8 MiB). `max_age_days` expires rotated files sooner on a quiet node. See `crate::journal` for the retention math.

### `enabled` — boolean
default `true`
Master switch. `false` stops the writer thread from starting; the emit sites become a single atomic load and nothing is written.

### `max_age_days` — integer
default `30`
Delete rotated files older than this. `0` disables the age bound and leaves the size cap as the only limit.

### `max_file_bytes` — integer
default `2097152`
Rotate the live file once it would exceed this many bytes.

### `max_files` — integer
default `4`
Files kept INCLUDING the live one. Total disk = this x `max_file_bytes`.

## [syslog]
opt-in · default off
Syslog / CEF event export to a collector (the SIEM half of the integrator question).
**Opt-in. Default OFF.** With `enabled = false` no socket is opened, no thread is spawned and no event is built. When on, events go to a bounded queue drained by one writer thread — the daemon never blocks on a collector that is down, and the events it had to drop are reported into the stream once the collector returns.

### `enabled` — boolean
default `false`
Master switch.

### `facility` — string
default `"local0"`
Syslog facility by name (`local0`…`local7`, `daemon`, …) or number.

### `format` — enum
default `"rfc5424"`
Wire format: `rfc5424` (structured syslog) or `cef` (ArcSight Common Event Format).

`rfc5424``cef`

### `hostname` — string
default `unset`
HOSTNAME field override. `None` uses the kernel hostname.

### `min_severity` — string
default `"info"`
Least severe level that is exported: `emerg`, `alert`, `crit`, `err`, `warning`, `notice`, `info`, `debug`.

### `poll_interval_ms` — integer
default `1000`
How often the interim snapshot watcher looks for state changes, milliseconds. Irrelevant once the event journal feeds the exporter.

### `queue_depth` — integer
default `1024`
Bounded queue depth. Deeper rides out a longer collector outage; beyond it, events are dropped and counted rather than blocking.

### `sd_id` — string
default `"atlas@32473"`
RFC 5424 SD-ID for the structured-data block. The default carries the enterprise number IANA reserves for documentation; set a real PEN if the collector validates it.

### `target` — string
default `"unix:///dev/log"`
Collector: `udp://host:port`, `tcp://host:port` or `unix:///dev/log`. A bare `host:port` means UDP; the port defaults to 514.

## [reporting]
on by default
Crash reporting and the watchdog (`[reporting]`, ).
**Capture is on; sending is not.** The product's positioning is no-phone-home , so the defaults here capture a crash locally, redact it, and stop. With `auto_send = false` (the default) and `relay_peers = false` the daemon starts no upload thread and opens no socket to `hub_url`; a report leaves only when an operator runs `atlasd crash export` or `atlasd crash send <id>`, one report at a time. `send_mode` decides whether such a send names this node.
`coredump` is off for a harder reason: a coredump of this daemon holds a Curve25519 private key, live Noise session keys and the mesh secret. It cannot be turned on without `coredump_pubkey`, because the only acceptable core is one encrypted to the hub at capture time.

### `auto_send` — boolean
default `false`
opt-in: upload spooled reports in the background instead of waiting for `atlasd crash send`. Off by default — per-report consent is the default posture, and this is the switch that trades it away deliberately.

### `coredump` — boolean
default `false`
Let `systemd-coredump` capture a full core and attach it encrypted. Requires `coredump_pubkey`; see the struct docs.

### `coredump_pubkey` — string

Base64 X25519 hub key a core is encrypted to at capture time.

### `enabled` — boolean
default `true`
Master switch for crash capture. `false` installs no panic hook and no signal handlers, and writes no markers.

### `full_peer_keys` — boolean
default `false`
Carry whole peer public keys instead of 8-character fingerprints.

### `hub_url` — string
default `"https://console.nexusatlas.net"`
Collector base URL. Sovereign customers self-host; nothing in the tree hardcodes a collector beyond this default.

### `journal_tail` — integer
default `200`
Journal entries attached to a report (the tail). Capped at the hub's 200.

### `max_bytes` — integer
default `8388608`
Total spool size. Whichever bound binds first evicts the oldest.

### `max_reports` — integer
default `20`
Reports kept in the spool.

### `redact_local` — boolean
default `false`
Same, for the copy kept for local export.

### `redact_uploads` — boolean
default `true`
Remove addresses, endpoints, hostnames and node names from the form of the report that would be uploaded.

### `relay_peers` — boolean
default `false`
Pull crash reports from mesh members that have no route to the hub and forward them. Requires `[mesh]`; off by default, because relaying another node's diagnostics is a decision.

### `send_mode` — string
default `"attributed"`
`"attributed"` (default) or `"anonymous"` — two modes.
Attributed carries this node's public key and presents the ingest token, so the report lands in the owning account's console. Anonymous carries no identity and no credential: the fingerprint still groups the crash with everyone else's, but nothing says whose it was. Anonymous also **forces `redact_uploads`** — a report that still named an endpoint would not be anonymous.

### `watchdog` — boolean
default `true`
Feed systemd's watchdog from the data-path liveness counters. Harmless when the unit has no `WatchdogSec=`: with no `$WATCHDOG_USEC` in the environment nothing is sent.

### `watchdog_grace` — integer
default `3`
Consecutive failed liveness checks before the watchdog stops feeding systemd. One missed check is a scheduling hiccup; three in a row is a wedged data path.

## [logging]

### `level` — string
default `"info"`
Log level / tracing filter: `trace`, `debug`, `info` (default), `warn`, `error`, or any `RUST_LOG`-style directive string.
