Architecture & core tunnel

Documents the atlasd v0.2 release lineView as Markdown

One daemon owns one TUN interface and a set of UDP sockets, one per underlay link. Everything between the TUN and the sockets — encryption, fragmentation, scheduling, probing — is designed around a single question: what happens when a link misbehaves? This page walks the data path and answers it at each stage.

The data path

An application writes an IPv4 packet to the tunnel interface ([interface] name, e.g. nexus0). The daemon reads it, decides which peer owns the destination (longest-prefix match over each peer's allowed_ips), encrypts it for that peer, fragments it if it exceeds the link budget, hands the fragments to the scheduler, and the scheduler places them on a link. On the far side the same machinery runs in reverse. The inner MTU is [interface] mtu (default 1420 — 1500 minus tunnel overhead); packets above the fragment budget are split into fragments sharing an ID and reassembled at the peer, with a janitor expiring half-arrived buffers so they can never leak memory.

Two properties of the path are worth designing around:

  • All fragments of one packet ride one link. The scheduler places packets, not fragments. Striping one packet's fragments across lossy links multiplies its loss probability (lose any fragment, lose the packet) and forces cross-path reordering — so it deliberately never happens. Cross-link distribution happens per packet, or per FEC shard in the transport-class path, where parity makes it safe.
  • A wedged link cannot block the others. Each link has its own send workers and a bounded queue (512 packets, drop-on-full, non-blocking): a saturated slow link sheds its own load instead of back-pressuring the TUN reader and stalling the whole bond.

Encryption

Each peer relationship runs the Noise IK handshake — X25519 key agreement, ChaCha20-Poly1305 AEAD, BLAKE2s hashing — the same peer-reviewed construction family used by modern commercial VPNs. Node identity is a static Curve25519 keypair from atlasd keygen; decoded private keys live in zeroizing memory and a config file looser than mode 0600 draws a load-time warning.

  • Rekeying is make-before-break: driven by [crypto] rekey_interval_secs (default 120) and/or rekey_data_limit (default 1 GiB), a fresh handshake is established beside the live session and both sides keep the previous keys warm for in-flight packets — measured at zero packet loss across rekeys in the end-to-end suite.
  • Simultaneous initiation is resolved deterministically (no duelling sessions), stalled handshakes retry on a budget, and an initiator that goes unanswered for 10 seconds rolls its session and re-initiates — recovering from a lost handshake response without a restart.
  • Anti-replay is a 1024-bit sliding window per session, checked after authentication. It doubles as the deduplication layer for the redundancy modes that intentionally send copies of a packet on several links.
  • A protocol version byte is checked on every packet decode. There is no version negotiation: mixed protocol versions fail loudly at decode rather than corrupting silently, so plan fleet upgrades as an upgrade, not a drift.

Each [[link]] binds a UDP socket to a device (SO_BINDTODEVICE) and address. Peers are addressed positionally — endpoint[i] reaches the peer over link i — or explicitly via endpoint_per_link when each radio has its own point-to-point addressing. Then reality intervenes, and three mechanisms deal with it:

  • Endpoint roaming ([interface] endpoint_roaming, default on, WireGuard semantics per link): an authenticated packet arriving from a new source address updates that peer's endpoint on that link. A node whose address changes mid-session — a vehicle re-associating to a different access point — keeps its tunnel without anyone restarting anything. Unauthenticated probes and keepalives can never steer an endpoint.
  • The rebind watchdog: after three consecutive send errors a link's socket is torn down and rebuilt. A USB radio that un-enumerates and returns recovers without a daemon restart; if the configured bind address is temporarily gone, the rebind falls back to the device's current address and returns to the configured one the moment it reappears.
  • Persistent keepalive ([[peer]] persistent_keepalive, seconds) keeps NAT pinholes and idle radio paths warm.
One rule worth memorising: two links sharing the same bind_device and bind_address must set distinct listen_port values — the sockets both set SO_REUSEPORT, and the kernel would otherwise load-balance inbound datagrams between them, silently costing each link half its traffic. atlasd config check catches it.

Address families

The underlay speaks both families: a link may bind an IPv6 address and peer endpoints may be bracketed IPv6 literals; probes, handshakes and roaming are family-agnostic, and a family-mismatched link/peer pair is warned about and skipped without disturbing other links. The inner tunnel data path is IPv4-only today — stated plainly on the limitations page.

Who is allowed to connect

Peers listed in the config are trusted by definition. What happens when a handshake arrives from an unknown key is the job of [auth] — default: reject. A hot-reloaded keyfile allowlist and an external decision hook are available for fleets that outgrow hand-edited peer lists, and every failure mode fails closed. The peer authentication guide covers all three providers.

The operational surface around the tunnel

The daemon assumes it will run unattended and be asked awkward questions later. It writes a 1 Hz stats snapshot (/tmp/atlas/<iface>.json) that the CLI, dashboard and your tooling all read; keeps a persistent, size-capped event journal so "what happened at 14:32?" has an answer after the fact; feeds the systemd watchdog only while the data path is genuinely moving; and captures crashes locally — redacted, and never uploaded without an operator's explicit action. The details live in Telemetry & observability.

Embedding note

The engine is a library first: it can adopt an externally created TUN file descriptor and per-link sockets (the mechanism behind the Android VpnService port) and accepts a custom peer authenticator. If you are evaluating Atlas as a component rather than a daemon, raise it in your access request — the integration surface is broader than the config file.

Esc
↑ ↓ navigateEnter — openEsc — close