Menu
Public repository / 4422064525bceb67fb9abbf493bded5ef7d76619
AkurAI-VPN / docs/networking.md
Latest change 6cc8ca734598da7417e9c1bd19fbaed08b8095c9 - docs: architecture, protocol, networking for the data plane by Claude Code
# AkurAI VPN — Networking (overlay, packet flow, tests)
## Overlay addressing
The overlay is a layer-3 network reached through a TUN interface. The parameters are
fixed in `akurai-common/src/overlay.rs`:
| Parameter | Value | Constant |
|-----------|-------|----------|
| TUN interface | `akurai0` | `TUN_INTERFACE` |
| IPv4 overlay | `100.88.0.0/16` | `OVERLAY_IPV4_NET` + `OVERLAY_IPV4_PREFIX_LEN` |
| IPv6 overlay | `fd88::/48` (mnemonic `fd88:akurai::/48`) | `OVERLAY_IPV6_NET` + `OVERLAY_IPV6_PREFIX_LEN` |
| MTU | `1280` | `OVERLAY_MTU` |
`100.88.0.0/16` sits in the `100.64.0.0/10` CGNAT range — deliberately outside normal
RFC 1918 LAN ranges, so the overlay does not collide with home/office subnets. The MTU
is a conservative 1280 (the IPv6 minimum) to leave headroom for the relay frame +
crypto envelope without path-MTU discovery in the MVP. The IPv6 prefix is a real
RFC 4193 ULA (`fd88::/48`); `akurai` is a non-hex mnemonic kept in prose only.
`OverlayIpv4::from_index(n)` maps host index `n` to `100.88.0.0 + n` (index 0 is the
network address; `MAX_INDEX = 65535`). `OverlayIpv4::contains(addr)` tests
`octets[0] == 100 && octets[1] == 88`.
### IP allocation (control plane)
`akurai-control/src/ipam.rs` hands each enrolled node a stable, globally-unique
`100.88.0.N/32`:
- Reserved indices: `0` = network address, `1` = control plane
(`CONTROL_PLANE_INDEX`). Node indices start at `FIRST_NODE_INDEX = 2`.
- `allocate()` returns the **lowest free** index ≥ 2; indices freed by node deletion are
reused.
- `choose_for_enrollment(supplied, used)` honors a caller-supplied index only if it is a
valid, free node index; otherwise it allocates the lowest free one.
- `assign_missing()` is the startup **backfill** — assigns IPs to nodes enrolled before
allocation existed; idempotent. The address is stored as a `100.88.0.N/32` entry in
the node's `allowed_ips`, so no schema change was needed.
The node's overlay IP is served to it via `/api/peermap` (peers) and read back from
`/api/endpoints` by the installer, which passes it to `akurai-node up --overlay-ip`.
### Interface bring-up (node) — overlay-only route, NEVER default
`akurai-node`'s `tunnel::run` opens `akurai0` via the one `ioctl` in `akurai-sys`, then
brings the interface up with safe `ip` commands (`std::process::Command`):
```text
ip addr add <overlay_ip>/32 dev akurai0
ip link set akurai0 mtu 1280
ip link set akurai0 up
ip route add 100.88.0.0/16 dev akurai0 # overlay-scoped route ONLY
```
It adds **no default route** — `0.0.0.0/0` is never installed. The host's existing
internet path (e.g. `wlan0`) is inviolable. This is the host-only product default:
only enrolled overlay peers are reachable; there is no subnet routing, exit routing, or
gateway advertisement (those are MVP2, **NOT YET BUILT**).
## Packet flow
```text
local app
│ writes IPv4 packet to dest 100.88.0.3
▼
akurai0 TUN (akurai-sys: /dev/net/tun, IFF_TUN | IFF_NO_PI, raw IPv4, no PI header)
│
▼ tun_pump (akurai-node/src/tunnel.rs)
├─ require IPv4 (len ≥ 20, version nibble == 4); else drop
├─ dest = packet bytes [16..20]
├─ peer = peers.get(dest) ── unknown dest → DROP (fail-closed)
├─ if session established: ct = session.encrypt(packet)
│ send Frame{Data, src=self, dest, ct} to relay
└─ else (first contact): noise::initiate → send Frame{HandshakeInit,…}
drop this packet (TCP/ICMP retransmits later)
│
▼ UDP socket (connected to the relay; src endpoint kept fresh by a
│ Keepalive frame every 20 s)
▼
akurai-relay (forward.rs): decode envelope → learn src endpoint →
forward datagram to dest overlay IP's learned UDP endpoint
(unknown dest → DROP). Never decrypts; holds no keys.
│
▼ UDP at peer
▼ udp_pump (peer's akurai-node/src/tunnel.rs)
├─ HandshakeInit → noise::respond; accept only if recovered initiator static key
│ matches the peer-table entry for src; insert session; reply
│ Frame{HandshakeResp,…}
├─ HandshakeResp → noise::finalize the pending handshake → session established
├─ Data → session.decrypt(payload) → write inner IPv4 packet to akurai0
└─ Keepalive → ignored (only the relay learns from these)
│
▼
akurai0 TUN → local app on the peer
```
The two pumps (`tun_pump` on the main thread, `udp_pump` on a second thread) share a
per-peer `SessionTable` (`established` + `pending` handshakes) under a `Mutex`. A third
thread sends the 20-second keepalive. The node binds `0.0.0.0:0` and `connect()`s the
relay, so all node↔relay traffic is one UDP flow. End to end, the relay only ever
forwards ciphertext.
### Node sources of the peer table (`akurai-node/src/peers.rs`)
- Static file `config/peers`: one `<overlay_ip> <pubkey_b64> [name]` per line; blank
lines and `#` comments ignored; unparseable lines skipped (used for tests and offline
bring-up).
- Control plane `GET /api/peermap` via `curl` with the saved cookie jar
(`--control <url>`); pure-`std` has no TLS client, and the node already shells `ip`,
so `curl` is consistent. JSON is parsed by a hand-rolled, dependency-free reader.
- Fail-closed: an entry that does not parse is skipped; a packet to a destination not in
the table is dropped.
## netns test topology
`tests/netns/e2e.sh` is the end-to-end proof. It stands up a relay and two nodes in
isolated Linux network namespaces — **the host stack is never touched** (`wlan0` is
structurally safe) — and pings node B from node A over the overlay, asserting the
underlay carries only ciphertext.
Underlay topology:
```text
ns-relay 10.0.1.1 <--veth(vrA/vaR)--> 10.0.1.2 ns-a (overlay 100.88.0.2)
ns-relay 10.0.2.1 <--veth(vrB/vbR)--> 10.0.2.2 ns-b (overlay 100.88.0.3)
```
The relay namespace (`akv-relay`) bridges the two node namespaces (`akv-a`, `akv-b`) at
the underlay; the relay listens on `RELAY_PORT=51820`. Each node reaches the relay by
its underlay IP (`10.0.1.1` / `10.0.2.1`).
What the script does:
1. Generate two node identities (`akurai-node install --home …`), read their
`identity.pub`.
2. Write each node a static peer map naming the other (`100.88.0.3 <B_pub> nodeb`, etc.).
3. Build the namespaces + veth underlay.
4. Start the relay (`akurai-relay serve`) in `akv-relay`.
5. `tcpdump` the relay→B link to capture forwarded underlay traffic.
6. Start both nodes (`akurai-node tunnel --overlay-ip … --relay 10.0.x.1:51820 --peers …`).
7. **`OVERLAY_PING`** — `ping -c 4 100.88.0.3` from `akv-a`; must succeed.
8. **`CIPHERTEXT_CHECK`** — the inner ICMP payload (`abcdefghijklmnop`) must NOT appear
in cleartext in the capture; the relay only ever sees AkurAI frames.
9. **`FAILCLOSED_CHECK`** — a ping to an un-mapped overlay IP (`100.88.0.9`) must fail
(no peer/route → dropped).
### Running the tests
```bash
# Unit tests for every crate (frame, noise, session, ipam, peermap, relay, …):
cargo test
# Build the binaries the e2e script needs:
cargo build -p akurai-node -p akurai-relay
# End-to-end overlay proof (needs root for netns + TUN; touches no host interface):
sudo tests/netns/e2e.sh
```
A clean run prints `OVERLAY_PING: PASS`, `CIPHERTEXT_CHECK: PASS`, and
`FAILCLOSED_CHECK: PASS`, and exits 0.
## Not yet built
- **Direct peer mesh / NAT traversal** (MVP3) — every packet transits the relay today.
- **Subnet / exit / gateway routes** (MVP2) — the node installs the overlay-only route
and never a default route.
- **MagicDNS / internal DNS names** (`akurai-dns`) — placeholder.
- **ACL enforcement in the data path** — peers are authenticated by static key and
unknown destinations are dropped, but there is no tag/policy ACL on the packet path.