Relay directory and federation opt-in · default off
Two nodes in different regions each attach to the relay nearest them — one in eu-central, one in na-east — and still reach each other, with no session across the ocean from either end. Two mechanisms make it work. The relay directory tells a node which relay a peer is on, through a presence record sealed so that only that peer can read it. One-hop federation lets a node's own relay forward to the peer's relay, so neither end has to attach to the other's. Neither gives the control plane a list of which key is on which relay. Read NAT traversal & relays first if relay links are new.
[traversal] enabled = true with at least one relay link, and the peers you want to reach listed as static [[peer]] entries — records are published and looked up for static peers only. Federation additionally needs [crypto] e2e = true on the nodes and [federation] enabled = true on both relays of a path.1 — How a node finds a peer's relay
- Publish. While a node holds at least one established relay session, it posts one presence record per static peer to the control plane: the relays it is on, sealed to that peer and signed by the poster. It re-posts every 300 s and 2 s after its set of established relays changes. Each record asks for a 590 s lifetime, so a node that loses every relay session simply stops being findable within ten minutes. A record names at most 8 relays, and only relays the node holds an established session with.
- Look up. When a static peer has been absent for 30 s — every established relay that can address it answers
UNKNOWN_DEST— the node fetches that peer's record. A peer never seen since start-up (never reached, and no relay has ever shown it present) is looked up after 5 s instead. A node posts its own records 2 s after its first relay session, so when both ends start together the record is already there and a cold bring-up through the directory takes seconds. From then on: one lookup per 60 s while the peer is absent, none while it is present, and none after 10 misses in a row until the peer's next absence. - Attach. For each relay the record names that this node does not hold, the node arms a directory slot with it and the normal handshake follows. The relay's key is pinned from the record; when the node has a signed relay map cached, the relay must be listed in it, and its address and region are taken from the map. A relay the map does not list is refused and journalled as
peer.directory_relay_refused.
The record format, the server's validation and what it stores are on the Presence API reference.
2 — Turn on the directory
| Key | Default | What it does |
|---|---|---|
directory | the value of auto_relay | Publishes sealed presence records for every static peer while a relay session is held, and looks absent peers up. Needs enabled. |
directory_url | unset | Control-plane base URL; records go to {base}/v1/presence. Unset uses relay_map_url, else https://nexusatlas.net/api. |
directory_slots | 2 | Relay links reserved at start for relays learned from the directory. 0 turns lookups off while records are still published — unless federation is on, which looks peers up for its via hint. |
With relay auto-election on, the directory comes with it. A minimal node:
/etc/atlas/atlas.toml (additions)[traversal]
enabled = true
auto_relay = true # directory follows auto_relay unless set
directory = true # explicit — the same as the default here
directory_slots = 2
[[link]]
name = "relay-home"
type = "relay"
bind_device = "wwan0"
bind_address = "0.0.0.0"
relay_server = "203.0.113.10:3478"
relay_pubkey = "RELAY_PUBLIC_KEY" # the relay near this node
[[peer]]
public_key = "PEER_PUBLIC_KEY" # static peer: records are kept for it
allowed_ips = ["10.0.100.2/32"] # no endpoint — found through the relays
3 — Directory slots
A node's links are fixed when it starts, so the relay links a lookup may need are reserved then. Each slot is a copy of the first enabled relay link — the same device and bind address — named directory-relay-1, directory-relay-2 and so on, on a port of its own above the highest listen port in the config. At most 8 are reserved, and none when the config has no [[peer]] or no relay link. A slot stays parked until a record names a relay this node does not hold; the election engine never gates an armed slot, it ranks it.
- Release. A slot whose relay has shown no present peer for 10 minutes is released — its session closed, the link parked again — and journalled as
relay.directory_slot_released. - Re-arm. When every slot is armed and a lookup names a relay this node does not hold, an idle slot (no peer present on it now) is re-armed with that relay. Lookups therefore continue after every slot is armed, at the same cadence.
4 — Turn on federation
With federation, a node does not need to attach to the peer's relay at all. Frames for a peer that is not on the node's relay go to that relay as a ForwardVia naming the peer's relay; the node's relay forwards them exactly one hop, over an inter-relay session, and the peer's relay delivers them as an ordinary Deliver. The peer does the mirror image with this node's record.
| Key | Default | What it does |
|---|---|---|
federation | false | Sends frames for a peer homed on another relay through this node's relay with a via hint, and lets the election rank federated paths. Needs enabled and the directory. |
federation_allow_plain | false | Uses federated paths even when [crypto] e2e is off. |
[crypto]
e2e = true # required for federated paths
[traversal]
enabled = true
auto_relay = true
federation = true # directory is on through auto_relay
# federation_allow_plain = false
[crypto] e2e seals its egress, or with federation_allow_plain = true set knowingly — the Atlas datagrams stay Noise-encrypted between the peers, but metadata and timing are then visible to two relay operators. The rule applies to each node's own sends, so set e2e on both peers. With federation on and neither condition met, the snapshot reads traversal.federation.plain_blocked: true.How the far relay is chosen, for each established relay link and static peer:
- When the election ranks the peer jointly, it ranks relays both ends hold at the cost of the two legs, and federated pairs at our leg plus the relay-to-relay RTT from the map's matrix plus the peer's leg, on one scale.
relay_pathscounts paths of either kind; make-before-break is unchanged. A node follows the elected federated path whose near relay is the link's relay. - Otherwise the peer's record decides: no hint when the record names this link's relay, else the named relay this node does not hold with the lowest relay-to-relay RTT in the map's matrix.
- A relay this node holds is never a via target — the common relay is used instead. The hint applies only while the peer is not present on the link's relay; a peer that is present always gets a plain
Forward. - With federation on and permitted, the first directory hit of an absence episode arms no slot: the node's relay forwards via the peer's. If the peer is still absent a minute later — the node's relay does not federate, say — the second hit arms a slot as in section 3.
- When the node's relay answers
VIA_UNKNOWNorFEDERATION_OFF, the node sends plainForwards on that relay for 60 s, then retries the hint, and journalsrelay.federation_refusedonce per refusal episode.
With federation on, the fragment payload is capped at 1317 bytes, so every leg of a federated path stays one 1500-byte IPv4 datagram. Overheads per leg are on the relay protocol reference.
5 — The relay side
A relay forwards for other relays' clients only when its operator turns it on. Both relays of a federated path need it.
/etc/atlas/relay.toml (additions)[federation]
enabled = true
max_pairs = 16 # inter-relay sessions held at once, each direction
pair_mbps = 20 # per relay pair and direction; 0 = unlimited
# map_url = "https://nexusatlas.net/api/v1/relays" # default: {registry.url}/v1/relays
# map_file = "/etc/atlas/relay-map.json" # the signed map on disk instead
# map_poll_secs = 30 # default: the registry heartbeat period
| Key | Default | What it does |
|---|---|---|
enabled | false | Forwards a ForwardVia one hop to the named relay. Off, a ForwardVia for a key this relay does not hold is answered FEDERATION_OFF. |
max_pairs | 16 | Inter-relay sessions opened at once (outbound) and accepted at once (inbound). The least recently used idle one makes room. Must be at least 1. |
pair_mbps | 20 | Rate cap per relay pair and direction, Mbit/s of forwarded payload; 0 = unlimited. |
allow_plain | false | Reported in the heartbeat; the e2e rule itself is enforced by atlasd. |
map_url | {registry.url}/v1/relays | Where the signed relay map is fetched. |
map_file | unset | Reads the signed map from this file instead, re-read every poll. |
map_poll_secs | the registry heartbeat period | Map refresh period. |
A relay with enabled = true refuses to start without a signed relay map: set [registry], map_url or map_file. Peer relays are known only from that map, verified with the same pinned key clients use; a frame from a relay key the map does not list is dropped. Inter-relay sessions are Noise IK between the relays' static keys, opened on the first via naming a relay, keepalived every 20 s, declared dead after 45 s of silence and closed after 5 minutes unused. The sender's per-session forward quota is charged as for any Forward, and each relay pair has its own pair_mbps bucket on the sending relay and on the receiving one.
A federating relay also measures its round-trip time to every other relay in the map — a STUN binding request every 60 s — and reports it in its heartbeat. The control plane publishes the pairs both relays report as the map's relay-to-relay matrix, which is what the election's federated cost reads. A relay without federation support (relay contract 1) drops a ForwardVia silently — one with federation turned off answers FEDERATION_OFF instead — and the node then reaches the peer through a directory slot once a lookup a minute later still finds it absent.
6 — Verify
curl -s http://node-a:9800/api/stats | jq '.traversal.directory, .traversal.federation'
{
"enabled": true,
"published": 42,
"publish_failures": 0,
"lookups": 3,
"hits": 1
}
{
"enabled": true,
"plain_blocked": false,
"paths": 1,
"frames_via": 18234,
"errors": 0
}
On the relay link that carries a federated path, links[].relay.via names the far relay (server, pubkey_short, region, city), and the peer's relay_presence[] entry for that link carries via_relay. The dashboard shows the path as two relays in series on the topology. In the journal:
curl -s "http://node-a:9800/api/journal?code=relay.federated_path_up"
curl -s "http://node-a:9800/api/journal?code=peer.directory_hit"
Every new snapshot field and journal code is listed on the relay protocol reference.
Boundaries
A federated path is exactly one relay-to-relay hop: a frame that arrived from a relay is delivered or refused, never sent on. Records are kept for static peers only, the number of directory slots is fixed when the daemon starts, and federated paths need [crypto] e2e or federation_allow_plain. The full list lives on Known limitations.