NAT traversal & relays opt-in · default off
Two peers behind carrier-grade NAT cannot address each other at all — no endpoint to write in the config, because "I don't know where this peer is" is the problem itself. The traversal stack solves it Atlas-style: a relay link brings the connection up unconditionally, hole punching tries to add a direct path beside it, and the ordinary scheduler — not a reconnection — decides which one carries traffic. Everything here is opt-in: without [traversal] enabled = true the data path is byte-identical to a build without the feature.
The load-bearing idea
Atlas did not adopt the WebRTC stack wholesale, because it does not want what WebRTC wants — one winning path. It wants every working path in the bond:
| Standard | In Atlas | Why |
|---|---|---|
| STUN | Adopted. Binding requests, XOR-mapped addresses, NAT mapping classification — answered by Atlas relays themselves. | Small, correct, useful; no third-party server dependency. |
| TURN | Replaced by an Atlas-native relay server. | The relay forwards opaque, already-encrypted envelopes between clients that both pin its public key. It authenticates who may relay — it cannot read what it relays. |
| ICE | Replaced by rendezvous over the relay plus the existing scheduler. | A punched path is not a "new connection" — it's an existing link whose peer endpoint was discovered at runtime. The probes are the hole punch; the relay→direct switchover is a routine scheduling decision, with the relay staying in the bond as a warm standby. |
Consequence: failover between a relayed path and a punched direct path is the same gradual, probe-driven traffic shift as any other pair of links. Nothing reconnects, ever.
Netcheck: know your NAT before you fight it
With traversal on, the daemon STUN-probes out of each link's own socket — the same 5-tuple its data uses, so the discovered mapping is the mapping that matters — classifies the NAT per link, and writes the report to /tmp/atlas/<iface>.netcheck.json (also at GET /api/netcheck and in atlasd status). It re-runs every netcheck_interval_secs (default 300).
Relay links are ordinary links
A relay link is a [[link]] with type = "relay", the relay's address, and its pinned public key — hijacking the relay's IP achieves nothing. It gets its own socket, its own probes, its own RTT, and competes in the scheduler like everything else. A peer reached through a relay needs no endpoint at all: its identity is its public key, and its address is discovered at runtime.
[traversal]
enabled = true
stun_servers = [
"203.0.113.10:3478", # the relay's first public address
"203.0.113.11:3478", # its second — a DIFFERENT IP, required
]
[[link]]
name = "relay-eu"
type = "relay"
bind_device = "wwan0"
bind_address = "0.0.0.0"
relay_server = "203.0.113.10:3478"
relay_pubkey = "RELAY_PUBLIC_KEY" # pinned; printed by the relay's keygen
capacity = "20 Mbps" # relays are shared — cost them honestly
[[link]]
name = "direct-wwan"
bind_device = "wwan0" # same modem as the relay link →
bind_address = "0.0.0.0"
listen_port = 51821 # → a distinct port is REQUIRED
weight = 2
[[peer]]
public_key = "PEER_PUBLIC_KEY"
allowed_ips = ["10.0.100.2/32"] # no endpoint — discovered via the relay
Housekeeping the stack does for you: relay envelopes add 66 bytes, so configuring any relay link automatically shrinks the fragment budget to keep relayed datagrams inside path MTU; relay keepalives hold the NAT binding open; and the relay-link/direct-link pair on one modem above demonstrates the distinct-port rule, which config validation enforces.
Hole punching: paid for by information, not timers
With punch = true (the default once traversal is on), peers exchange address candidates over the relay they already share and probe toward each other simultaneously, sorted in the same order on both sides so the attempts actually cross. A successful punch adds a direct path beside the relayed one — one hop shorter, and off the relay's bandwidth bill. A failed punch costs nothing: the relay keeps carrying traffic.
Retry discipline is deliberate: a candidate set is tried for one full lap and then rests; punching re-arms when something changes — a new reflexive address after a bearer handoff, a changed peer offer — rather than on a timer; and a punched path dying triggers a fresh netcheck instead of blind retries. Live links are never disturbed by any of it.
Relay auto-election
Fleets configure several relays for redundancy; without help, N configured relays cost N live sessions of keepalives. With auto_relay = true, both ends of each peer relationship STUN-measure every relay, exchange their RTT vectors, and independently compute the same deterministic ranking — converging on the same shared relay set with no negotiation protocol. Exactly relay_paths (default 1) relay sessions stay active per peer; the rest are gated but configured, and failure promotes the next-best relay on both sides at once, make-before-break. A punched direct path is always additional.
Self-hosted or Nexus Atlas-hosted
- Self-hosted: run the Atlas relay server — a single static binary with zero disk state — on a host reachable by both peers, with two public addresses for full NAT classification. Configure clients statically as above; no control plane is required.
- Hosted: Nexus Atlas operates relays in Europe and North America. With
discover = true(default), a relay link that omitsrelay_server/relay_pubkeyresolves one at runtime from a cryptographically signed relay map — so one config ships to a whole fleet with no hard-coded relay addresses, andregionoptionally pins selection ("eu-central"); unset picks the lowest RTT. Static per-link configuration always wins when present.
Honest edges
- A pair of endpoint-dependent ("hard") NATs stays on the relay — Atlas does not attempt port-prediction punching. Correct, but some theoretically punchable pairs remain relayed.
- A punched path currently reports as
directin the stats — there is nopunchedmarker distinguishing it. - Traversal is verified end-to-end against simulated carrier-NAT topologies and on production relay hardware for election; phrase your own acceptance tests accordingly and run them on your bearers.