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

# 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/2.0.0/config#cfg-interface) · [[[link]]required](https://nexusatlas.dev/docs/2.0.0/config#cfg-link) · [[[link_group]]](https://nexusatlas.dev/docs/2.0.0/config#cfg-link_group) · [[[peer]]](https://nexusatlas.dev/docs/2.0.0/config#cfg-peer) · [[scheduler]](https://nexusatlas.dev/docs/2.0.0/config#cfg-scheduler) · [[link_monitor]](https://nexusatlas.dev/docs/2.0.0/config#cfg-link_monitor) · [[crypto]](https://nexusatlas.dev/docs/2.0.0/config#cfg-crypto) · [[auth]](https://nexusatlas.dev/docs/2.0.0/config#cfg-auth) · [[routing]](https://nexusatlas.dev/docs/2.0.0/config#cfg-routing) · [[multicast]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-multicast) · [[mesh]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-mesh) · [[terrain]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-terrain) · [[dns]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-dns) · [[traversal]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-traversal) · [[qos]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-qos) · [[telemetry]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-telemetry) · [[metrics]on by default](https://nexusatlas.dev/docs/2.0.0/config#cfg-metrics) · [[journal]on by default](https://nexusatlas.dev/docs/2.0.0/config#cfg-journal) · [[storage]](https://nexusatlas.dev/docs/2.0.0/config#cfg-storage) · [[syslog]opt-in · default off](https://nexusatlas.dev/docs/2.0.0/config#cfg-syslog) · [[reporting]on by default](https://nexusatlas.dev/docs/2.0.0/config#cfg-reporting) · [[logging]](https://nexusatlas.dev/docs/2.0.0/config#cfg-logging) · [[config_mgmt]](https://nexusatlas.dev/docs/2.0.0/config#cfg-config_mgmt) · [[rotation]](https://nexusatlas.dev/docs/2.0.0/config#cfg-rotation)

## [interface]
required

### `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.

### `address` — string
default `""`
Tunnel address of this node as IPv4 CIDR, e.g. `10.0.100.1/24`. Bare addresses imply /24. May be left out on a node joining by a pairing code (`[auth] invite_token`): the code's `i` with the prefix length of its `h`, or — a pool code, which carries no `i` — the address the inviting node assigns at admission (the engine starts with the interface up and unaddressed until then).

### `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 `1416`
TUN device MTU. Default 1416: a full-size packet relayed through another node (56 B of Atlas headers) plus UDP/IPv4 (28 B) is exactly 1500 bytes.

### `next_private_key` — string
default `unset`
The next transport private key during a rotation overlap. The node answers handshakes on either key and initiates with `private_key`; activating the rotation swaps the two.

### `node_id` — string
default `unset`
This node's record id (a UUID minted by `atlasd keygen --identity`). Only meaningful in a record fleet (`[auth] provider = "fleet"`), where it must match the fleet-list record that holds this node's key; the LSDB then knows the node by this id, so rotating a key changes nothing anyone routes on. Absent = the transport key is the node's id, as in a fleet without records.

### `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.

### `signing_key` — string
default `unset`
Base64 Ed25519 seed of this node's signing key (beside `private_key`, same 0600 file). Signs the node-bound dashboard read token. Its public half belongs in the record's `[[node.keys.signing]]`.

## [[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); `type = "serial"` puts Atlas frames straight onto a serial radio (`device`, no socket, no PPP).

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

### `address` — integer

Serial link only: this node's address on a channel shared by several nodes (RFD900 multipoint firmware with the radios broadcasting). Set, every frame carries destination and source addresses (3 bytes); unset, the link is point to point and frames carry none.

### `air_rate` — number | string

Serial link only: the radio's air data rate (SiK `ATS2`), unit-tagged or a bare number in Mbps. With no `capacity`, the link's capacity is the lower of this and the line rate (baud × 0.8).

### `bandwidth_mbps` — number

Capacity as a plain number, always in Mbps. Overridden by `capacity` when both are set.

### `baud` — enum
default `57600`
Serial link only: line rate, 8N1. Must match the radio's serial speed (SiK `ATS1`). A mismatch shows as framing and CRC errors in the link's counters.

`9600``19200``38400``57600``115200``230400``460800``500000``921600`

### `bind_address` — string

Local IP the socket binds. `0.0.0.0` = any IPv4; a bare IPv6 address selects the v6 family.

### `bind_device` — string

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

### `buffer_pacing` — boolean
default `true`
Serial link only: write to the port only while the serial line and the radio's buffer hold less than `[qos] low_band_target_ms` at the link's rate, so Atlas's own queues decide what goes on air next (the high band first). False writes as fast as the port takes it, as pppd's path does.

### `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`.

### `device` — string

Serial link only (required there): the serial device of the radio. A `/dev/serial/by-id/…` path keeps naming the same radio across replugs. The link reopens it by itself after an unplug or a radio reset.

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

### `flow_control` — enum
default `"none"`
Serial link only: `rtscts` when the radio and the cable carry RTS/CTS (SiK `RTSCTS=1`); the radio then stops the line instead of dropping bytes when its buffer is full.

`none``rtscts`

### `heartbeat_ms` — integer

Serial link only: interval of the MAVLink heartbeat written to the radio so it reports RADIO_STATUS (17 bytes each, also sent on air). Default 250 for `rfd900`/`sik`, 0 (never) for `generic`.

### `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_fragment_airtime_ms` — integer
default `20`
Longest one fragment may occupy this link, in ms (default 20, one voice frame): packets sent over it are fragmented at capacity × airtime, clamped to [256 B, the global payload]. 0 = the global payload.

### `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`.

### `peer_addresses` — table

Serial link with `address` only: peer tunnel address → that node's address on the channel (1–254). A node not listed is learned from its first authenticated frame.

### `peers` — array of string

Serial link only, point to point: tunnel addresses of the node(s) at the other end of the radio. Empty = every configured peer may be reached over this link.

### `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.

### `radio` — enum
default `"generic"`
Serial link only: the radio on the port. `rfd900` and `sik` write a MAVLink heartbeat so the radio injects RADIO_STATUS (needs MAVLink framing, `ATS6=1`), whose RSSI, noise and transmit-buffer fill feed the link score and the pacer. `generic` = any byte pipe.

`rfd900``sik``generic`

### `radio_buffer_bytes` — integer
default `2048`
Serial link only: the radio's transmit buffer in bytes, to turn RADIO_STATUS `txbuf` (percent free) into bytes for the pacer. Default 2048.

### `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), `relay`, `local` or `serial`. A serial link frames Atlas datagrams straight onto a serial port (`device`, `baud`) — no PPP, no IP — and takes no bind_device/bind_address. A relay link carries peer traffic through a Nexus Atlas relay server and needs `relay_server` + `relay_pubkey` (or traversal discovery). A local link binds to loopback only and joins this daemon to another Atlas daemon on the same host — the seam of an Atlas Interconnect (see `[[peer]] interconnect`); it never carries link-state adverts.

`direct``relay``local``serial`

### `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.

## [[link_group]]

`[[link_group]]`: a named set of links.

### `links` — array of string
required
`[[link]] name`s; a link belongs to at most one group.

### `name` — string
required

## [[peer]]

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

### `allowed_ips` — array of string
default `[]`
Tunnel IPv4 CIDRs routed to this peer (WireGuard semantics), e.g. `["10.0.100.2/32"]`. Bare addresses imply /32. May be left out for the inviting node of `[auth] invite_token`: the code's `h` as a /32.

### `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), endpoints are matched to links by position: `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"]
```

### `interconnect` — InterconnectConfig | null

Atlas Interconnect: this peer is a daemon of ANOTHER Atlas network on the same host (reached over a `type = "local"` link). Two modes, which may be combined in one block:
- Mapped mode (`map`): how single hosts of that network appear inside this one. Each `local` address is advertised into this network as a served prefix; traffic to it is translated to the real `remote` address and sent across the seam, and traffic from a `remote` address arrives with its `local` source. Members need nothing. - Prefix mode (`route`): whole prefixes of that network are imported with their real addresses, no rewriting. Each `prefix` is advertised into this network as an imported route; members' daemons install a kernel route for it on their TUN (`[routing] install_learned_routes`), so a member reaches a host behind the seam by its real address. The other daemon must list the prefix in its own `export` (and import ours with its own `route`), or nothing crosses.
`export` limits which of this network's addresses take part — reachable from the other side, and allowed to send across. Optional for mapped mode (empty = every address); required for prefix mode. LSAs never cross the seam, and nothing learned through one seam is ever passed on through another.

```
[peer.interconnect]
export = ["10.0.101.0/24"]

[[peer.interconnect.map]]
remote = "10.0.9.249/32"
local = "10.0.101.249/32"

[[peer.interconnect.route]]
prefix = "10.0.9.0/25"
```

Keys of `[peer.interconnect]`:

- `export` array of string — This network's addresses that take part in the interconnect: the other side may reach them, and only they may send across. Empty = every address in this network (mapped mode only — a block with a `route` must list them).

- `map` array of tables — How the other network's hosts appear in this one. `remote` is a real address/prefix in the other network (inside this peer's `allowed_ips`); `local` is where it appears here (inside this network's subnet, same prefix length).
- `local` string required — Where it appears in this network, e.g. `"10.0.101.249/32"`.

- `remote` string required — Real address or prefix in the other network, e.g. `"10.0.9.249/32"`.

- `route` array of tables — Prefixes of the other network imported with their real addresses (prefix mode). Each must lie inside this peer's `allowed_ips`, outside this network's subnet, and overlap no mapped address and no other route; IPv4 only.
- `prefix` string required — A prefix of the other network, e.g. `"10.0.9.0/24"`, reached by its real addresses through this seam.

### `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.

### `egress_queue_sample_ms` — integer
default `200`
How often the kernel egress queues below each link (socket send buffer, root qdisc backlog) are sampled, in ms. 0 disables sampling.

### `load_window_secs` — integer
default `2`
Window over which each link's utilisation is measured for the load entry of the link-state advert (headroom-aware routing), in seconds: bytes sent over the window ÷ the link's capacity (its declared `capacity`, or the queue's measured drain rate when that is lower). 1–60.

### `position_advert_distance_m` — number
default `25`
Distance in metres from the last advertised position that triggers an immediate position advert. Default 25 (under one 30 m elevation cell, and about a second of flight for a fixed-wing aircraft); 0 turns the trigger off.
Range prediction and the SPF's terrain weights use the positions other nodes advertise, so this is how stale they can be about a moving node.

### `position_advert_interval_secs` — integer
default `10`
Longest a moving node goes between position adverts, in seconds. A node with a fix that has moved at all since its last link-state advert re-advertises its position at least this often. Default 10; 0 turns the period off; at most 30 is useful (the advert refresh already carries the position every 30 s).
A position advert is an ordinary flood under the bearer-aware pacing: a link with its own `route_advert_interval_secs` never carries it faster than that, and no link faster than `position_advert_min_interval_secs`.

### `position_advert_min_interval_secs` — number
default `1`
Fewest seconds between two position adverts, node-wide (a float). Default 1.0; at least 0.1. However fast a node moves, an unpaced link carries its position at most this often, and a link with its own `route_advert_interval_secs` at the longer of the two. Position adverts are not held to `[mesh] route_advert_interval_secs`. Longer than `position_advert_interval_secs` is warned: the period trigger would be held to it.

### `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`
Shorthand switch for RSSI prediction: `true` behaves as `prediction_source = "rssi"` when prediction_source is unset.

### `standing_queue` — boolean
default `true`
Let the scheduler use each link's queue delay (standing-queue estimator and kernel egress queues). The figures stay visible in the snapshot either way; this only decides whether delivery time and the escalation guard react to them.

### `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.

### `e2e` — boolean
default `false`
End-to-end payload encryption (opt-in, default off). When true, unicast inner IP packets are sealed under a source-to-destination AEAD *before* fragmentation, so relaying nodes forward an opaque blob and cannot read the payload — while still routing on the cleartext relay header, so un-upgraded relays forward it unchanged. The per-hop AEAD is unaffected. Broadcast, multicast and `Fec`-class traffic are e2e-exempt in this version. See `crate::e2e`.

### `e2e_require` — boolean
default `false`
Require inbound unicast to be e2e-sealed: drop any unicast packet that arrives un-sealed. A hard posture for a node that must never accept relay-injected plaintext. Default off. Implies opening is active even when this node does not itself seal egress.

### `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`.

### `accept_forwarded` — boolean
default `true`
On a config author: sign records that other nodes admitted and forwarded (invitations and approvals on nodes that cannot sign the list) into the next fleet list at once, after checking them — never `config_author`, no flag the forwarding node lacks, no collision, no revoked key. `false` holds them for an operator (`atlasd fleet pending list|approve|deny`, the Pending panel).

### `block_secs` — integer
default `3600`
How long a denied key stays blocked (dropped without an answer).

### `cert_file` — string
default `unset`
Joining side: this node's fleet certificate (`atlasd fleet cert`), presented in its handshakes so any node trusting the fleet authority admits it without a list entry.

### `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.

### `fleet_list` — string
default `unset`
Bootstrap copy of the signed fleet list for `provider = "fleet"` (TOML authored with `atlasd fleet …`, or the JSON envelope). A newer list received over a session supersedes it by sequence number. Absent on a node joining by invitation or certificate: it learns the list from the node that admits it.

### `invite_secret_file` — string
default `unset`
Secret behind invitation tokens (`atlasd fleet invite`): 32 random bytes, base64, mode 0600; created on the first invitation. Default: `<state dir>/<iface>.invite-secret`. Replacing it voids every open invitation.

### `invite_token` — string
default `unset`
Joining side: the whole `v=3` pairing code this node was invited with (`atlas:pair?v=3&k=…&a=…&h=…&t=…`). Its handshakes present the token and the code's binding; the code's `i` fills a missing `[interface] address` (with `h`'s prefix length), and `k` names the inviting node — its `[[peer]]` gets `h/32` as `allowed_ips` when they are left out, and is added (endpoint from `e`, if any) when missing. The anchors (`a`) and authors (`c`) it carries are trusted for fleet lists once the inviting node confirms the code. Keep it after joining: it is where those anchors come from.

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

### `pending` — boolean
default `false`
Pending-approval queue: an unknown key that fails every other check is parked (key, source, claimed name, first/last seen, count) for an operator to approve — minting its record with chosen flags — or deny. Works with any provider.

### `pending_max` — integer
default `200`
Most keys the pending queue holds; further knocks are dropped.

### `provider` — enum
default `"static"`
`static` (default, unknown keys rejected), `keyfile`, `command`, or `fleet` (the signed fleet list: a key without a record gets no session, and every record's capability flags are enforced). Misconfiguration refuses to start.

`static``keyfile``command``fleet`

### `psk` — string
default `unset`
Fleet-wide pre-shared key (base64 of 32 random bytes) — admission by key + PSK. Every handshake then carries a PSK tag and mixes the key into the session keys (Noise IKpsk2); an init without the right tag is dropped without an answer. Both ends of every pair need it.

### `psk_file` — string
default `unset`
Named pre-shared keys for records that carry a `psk_id`: a TOML file of `id = "<base64 32 bytes>"` lines (mode 0600). A pair whose record names a `psk_id` uses that key instead of `psk`.

## [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.
`headroom_classes` is independent of both: it names the QoS classes that route by the headroom map — a second shortest-path computation whose edge cost adds each link's advertised queue delay and a utilisation penalty to its RTT, so those classes steer around loaded hops while everything else keeps the default path. Applied at the origin and at every transit hop from the packet's class byte; traffic from a sender without QoS is unclassified and never takes it. With `path_redundancy` ≥ 2 the top-K paths are taken on whichever map the class selects.

### `headroom_classes` — array of string
default `["Control"]`
QoS classes routed by the headroom map (headroom-aware routing): class names as the QoS policy spells them (`Control`, `Voice`, `Position`, `Telemetry`, `Video`, `Bulk`), or `"*"` for every class. Empty disables the second shortest-path computation. Unclassified traffic (a sender without QoS) always keeps the default map.

### `install_learned_routes` — boolean
default `true`
Install a kernel route on this node's TUN for every prefix of another network that a seam host of this network imports (Atlas Interconnect prefix mode), and remove it when no seam host advertises the prefix any more — so a member reaches hosts behind the seam by their real addresses with no host configuration. A prefix that overlaps a route already in the main table (other than the default route) is skipped and logged, never installed over it. Linux only, and only on a TUN this daemon created (an embedder that owns its interface, such as the Android app, reads the learned prefixes instead). Default on; restart-required.

```
[routing]
install_learned_routes = false
```

### `max_hops` — integer
default `8`
The cap on Atlas's own Layer-3 relay hops: the hop budget this node stamps into the relay header of every packet it originates, counted in Atlas links from origin to destination (8 = up to seven relays in between). Default 8. Eight is a chosen default, not a technical ceiling: the header field that carries the budget is one byte, so any value from 1 to 255 is accepted, and 0 or anything above 255 is refused. 1 leaves only direct peers reachable.
The count is Atlas's own hops between Atlas nodes. Hops inside an underlying Layer-2 mesh (a MANET radio, 802.11s) that one Atlas link rides are invisible to Atlas and are not counted.
The budget belongs to the packet: a transit node forwards on what the origin stamped, whatever its own setting, so nodes with different values still agree on every forwarding decision. Raise it on the nodes that originate traffic across long chains.
Restart-required, like the rest of `[routing]`.

```
[routing]
max_hops = 12
```

### `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.

### `redundant_parents` — integer
default `1`
Tree parents to be fed by, per subscribed group: 1 (default) or 2. With 2, a node whose record says `sink = true` (a public feed gateway with links to two mesh nodes) also takes a copy from a second neighbour that carries the stream anyway, so losing the tree parent costs no re-root gap; the multipath dedup window drops the duplicate. The extra copy rides only that neighbour's link to the sink. Ignored on a node that is not a sink, and by every peer for such a node.

### `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 v1 plaintext (truncated-hash) gossip bodies beside v2. 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).

## [terrain]
opt-in · default off
`[terrain]`: elevation tiles and the radio parameters terrain line-of-sight is judged with. Off by default.

### `antenna_height_m` — number
default `2`
Antenna height above each node's reported position, metres.

### `clamp_to_ground` — boolean
default `true`
Lift a position reported below the ground up to the ground before adding the antenna height. GPS altitude is often on the ellipsoid while tiles are on the geoid (tens of metres apart); without this a ground node would sit "underground" and every path would read blocked.

### `enabled` — boolean
default `false`
Load the tiles and assess every positioned neighbour's path.

### `frequency_mhz` — number
default `5180`
Radio frequency for the first Fresnel zone, MHz, on links with no entry in `link_frequency_mhz` and on paths between other nodes.

### `fresnel_clearance` — number
default `0.6`
Share of the first Fresnel zone that must stay clear for a path to count as healthy (risk 0). The classic rule is 0.6.

### `interval_ms` — integer
default `1000`
How often paths are re-assessed, ms (a moved position triggers an assessment sooner).

### `k_factor` — number
default `1.3333333333333333`
Effective-earth-radius factor for refraction: 4/3 is the standard atmosphere; lower is more pessimistic.

### `link_frequency_mhz` — table

Per-link frequency, MHz, keyed by `[[link]] name` (`{ "wifi" = 5180, "rfd900" = 915 }`).

### `max_paths` — integer
default `512`
Most paths assessed per pass: this node's neighbours first, then the links between other nodes.

### `max_tiles_mb` — integer
default `256`
Memory the tiles may take, MB. Tiles past the budget are skipped.

### `prediction` — boolean
default `true`
Let terrain steer: inflate the scheduler's view of a shadowed link and raise its SPF cost. `false` keeps the analysis (snapshot, journal, 3D view) without steering anything. Applies live.

### `samples` — integer
default `256`
Terrain samples along each path.

### `tiles` — array of string
default `[]`
Baked `.nadt` tiles (`nexus-atlas-terrain/tools/dem_bake.py`), as files or directories (every `*.nadt` inside). A relative entry is read from `<config dir>/terrain/` (`/etc/atlas/terrain`).

## [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 and none is gated.

### `directory` — boolean
default `unset`
Directory-assisted rendezvous. While this node holds a relay session it posts, for every static `[[peer]]`, a sealed presence record to the control plane naming the relays it is on — readable only by that peer, stored under an id only the pair can compute, so the control plane never learns which key is on which relay. A peer absent from every relay we hold is looked up there and its relay added as a runtime relay link. Unset = the value of `auto_relay`.

### `directory_slots` — integer
default `2`
How many relay links are reserved at start for relays learned from the directory (each bound on the first relay link's device with a port of its own). A slot whose relay has shown no present peer for 10 minutes is released, and a lookup naming another relay re-arms an idle slot. Default 2; 0 turns lookups off while records are still published — unless `federation` is on, which looks peers up for its via hint.

### `directory_url` — string
default `unset`
Override the control-plane base URL presence records are posted to and fetched from (`{base}/v1/presence`). `None` → `relay_map_url`, else the baked-in default.

### `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.

### `federation` — boolean
default `false`
One-hop relay federation. When a peer is not on the relay we hold but its presence record names a relay we do not hold, frames for it are sent to our relay with a `via` hint naming the peer's relay, which our relay forwards one hop to (both relays must run `[federation] enabled`). The election also ranks such federated paths (cost: our leg + the relay-to-relay RTT from the signed map + the peer's leg) against relays both ends hold. A second relay is a second observer of relayed traffic, so a federated path is used only when `[crypto] e2e` seals egress, or with `federation_allow_plain`. Requires `enabled` and the directory. Default off.

### `federation_allow_plain` — boolean
default `false`
Use federated paths even when `[crypto] e2e` is off (both relays then see the Atlas datagrams they forward — still Noise-encrypted between the peers, but metadata and timing are visible to two operators). Default off.

### `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.

### `punch_links` — array of string
default `[]`
Which (non-relay) links take part in hole punching, by name: offered as host candidates to peers and tried for direct paths. Empty (the default) = every non-relay link, the historical behaviour. Set it on a node whose other links can never reach a traversal peer — a ground station's point-to-point radios — so their private addresses are not advertised to a phone on cellular, which would only waste its trials.

### `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 packets take the plain outbound/inbound paths.
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.

### `classes` — table

Per-class settings by class name (`[qos.classes.Video]`); the policy file's value wins when it sets one.

Keys of each `[qos.classes.<name>]`:

- `budget_by_level_ms` array — Latency budget per precedence level in ms.

- `companion` string — Ride with this class: its link plan, its stripe unit and, at the receiver, its release buffer — so the two arrive in step.

- `redundancy` string — `none` | `previous_packet` (each packet also carries the previous one; default for Voice).

- `retain` boolean — Journal every packet of this class so destinations can ask for what they missed (complete footage; bounds in `[qos.retain]`). Refused on a store-carry-forward class (Bulk). The policy file's `retain` turns it on as well.

### `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`.

### `drop_event` — table

When a class's late and shed drops are reported as an event (journal code `qos.drop.burst`, and syslog/CEF when export is on).

Keys of `[qos.drop_event]`:

- `cooldown_secs` integer default `60` —

- `pct` number default `1` —

- `threshold` integer default `20` —

- `window_secs` integer default `10` —

### `enabled` — boolean
default `false`
Master switch. `false` (default) = the plain data path: no classification, no class queue, the class byte is not acted on.

### `feasibility` — boolean
default `true`
Offer a packet with a deadline only to links that can still deliver it in time (half the RTT + serialisation + queue delay ≤ time left). A packet no link can deliver in time is dropped at planning.

### `feasibility_margin_ms` — integer
default `5`
Subtracted from the time left before the feasibility comparison.

### `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.

### `flow` — array of tables
default `[]`
Operator flow rules (`[[qos.flow]]`), tried before the DSCP and port rules: an explicit rule wins.

Each entry:

- `class` string required — Service class name.

- `dst_port` integer — Destination port (UDP or TCP).

- `level` integer — Precedence inside the class: 0 primary, 1 secondary, 2 tertiary.

- `src` string — IPv4 source prefix, e.g. `10.0.9.20/32`.

### `group` — table

Per link group (`[[link_group]]` name, or `default`): per class name, whether the class may ride the group and the multipath policy used when planning on it. Everything else about a class stays node-wide.

Keys of each `[qos.group.<name>.<class>]`:

- `allowed` boolean — `false` keeps the class off this group's links.

- `multipath` GroupMultipath | null — Multipath policy when the class is planned on this group.

### `low_band_pacing` — boolean
default `true`
Pace each link's low band (everything but Control, Voice and Position) to the measured drain rate of the kernel queue below it, so a voice frame waits behind at most `low_band_target_ms` of video there. Applies while QoS is enabled, on links whose device queue can be read (not a `noqueue` device such as veth or most Wi-Fi drivers, nor a netem root that emulates delay).

### `low_band_target_ms` — integer
default `5`
Airtime the kernel queue below a link may hold before low-band datagrams wait in Atlas's queue, in ms at the measured drain rate (2–100).

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

### `repeat_budget_pct` — integer
default `20`
Ceiling on time-diversity repeats per class, as a percentage of the sole link's capacity; beyond it copies degrade to 2, then 1.

### `retain` — table

Retention and backfill (`[qos.classes.<C>] retain = true`): bounds of the sender's journal, the receiver's request pacing and the backfill pace. See `RetainConfig`.

Keys of `[qos.retain]`:

- `backfill_max_kbps` integer default `0` — Ceiling on the backfill rate in kbit/s; `0` = no ceiling beyond the Bulk lane (backfill only uses capacity no live class is using).

- `gap_request_delay_ms` integer default `2000` — How long a hole stays open at the receiver before it is requested, so reordering and FEC repair get their chance first.

- `gap_request_interval_ms` integer default `2000` — At most one request per (origin, class) this often; doubled while the origin answers nothing.

- `max_bytes` integer | string default `2147483648` — Journal size bound across every retained stream (a ring: the oldest segment is dropped first). Also the bound on the receiver's captures, which stop growing (counted) instead of deleting footage. Unit-tagged string (`"2 GiB"`, `"500 MB"`) or a bare number of bytes.

- `max_inflight` integer default `1024` — Backfill packets queued per destination at the origin (more requested packets wait for the next request).

- `max_secs` integer default `86400` — Journal age bound in seconds: segments whose newest packet is older are dropped. `atlasd retain clear` empties the journal at the end of a flight.

- `path` string default `""` — Directory for journals (`<iface>.retain/`) and captures (`<iface>.capture/`); empty = the state directory.

### `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) = received frames are handed on as they arrive, with no release buffer. 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. 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`.

## [storage]

`[storage]`: encrypt every daemon-owned file at rest.

### `at_rest` — string
default `"off"`
`"off"` (default: files exactly as before) or `"on"`.

### `key_source` — string
default `"node_key"`
Where the store key comes from: `node_key` (default; derived from the interface private key — protects deleted data and casual reads, not an attacker holding the config file), `file:<path>` (32 bytes on a separate volume), `boot_secret` ($NEXUS_ATLAS_STORE_SECRET or the systemd credential `atlas-store`), `tpm` (reserved).

## [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.

## [config_mgmt]

Fleet pushes (`[config_mgmt]`). Update classes live on the node records of the fleet list; these are the timings the pushing node and the left-behind nodes use.

```
[config_mgmt]
release_watch_secs = 600      # 0 = 3 x confirm_secs + 120
left_behind_grace_secs = 600
```

### `left_behind_grace_secs` — integer
default `600`
A left-behind node that is not pinned takes a new epoch this many seconds after it first saw it, so it catches up after the release watch rather than during it. A pinned node never takes it.

### `release_watch_secs` — integer
default `0`
How long a released rollout is watched before a required node that has not confirmed the epoch aborts it. 0 (the default) derives the window from the rollout: 3 x its `confirm_secs` + 120 s. A rollout started with an explicit `release_watch_secs` uses that instead.

## [rotation]

Partition-safe rotation (`[rotation]`): node keys and the mesh secret change on a publish → activate → retire schedule carried in the signed fleet list. The windows here are the defaults `atlasd fleet rotate plan` and the dashboard use on the operator host; the lifeline window and the clock rule are read by every node.

```
[rotation]
default_publish_to_activate_secs = 3600
default_activate_to_retire_secs = 604800
lifeline_secs = 86400
require_trusted_clock_for_scheduled_activation = true
```

### `default_activate_to_retire_secs` — integer
default `604800`
Default gap between activation and the earliest retire the operator is offered. Retire is always a signed step of its own, gated on every required node reporting the rotation active.

### `default_publish_to_activate_secs` — integer
default `3600`
Default gap between publishing a rotation (every node accepts the new key or secret) and activating it (the subject sends with it).

### `lifeline_secs` — integer
default `86400`
Nodes whose record says `lifeline = true` (bootstrap peers) keep accepting the retired key or secret this much longer, so a straggler still finds a node that speaks its material. A fleet list's own `lifeline_secs` overrides this.

### `require_trusted_clock_for_scheduled_activation` — boolean
default `true`
Act on a rotation's `activate_at` only while the system clock is synchronised (NTP/PTP/GPS discipline, as the kernel reports it); without a synchronised clock the node activates on receipt and says so in its journal. `false` trusts the clock as it is.
