docs/protocol.md, docs/pki.md, docs/split-tunnel.md — written from the actual implementation (pinned handshake order, ML-KEM-768/FIPS 203, seq||AEAD records with replay window, QUIC/H3 mimicry) including honest v1 limitations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
9.5 KiB
Aura Split Tunnel
Split tunneling decides, per destination IP, whether a packet travels through the encrypted VPN or egresses directly (bypassing the tunnel). It lets you keep, say, RFC1918 LAN traffic local while sending the rest through Aura — or the reverse.
It is implemented in the aura-tunnel crate (routes.rs, router.rs, dns.rs), configured
statically via the [tunnel.split] section of client.toml
(crates/aura-cli/src/config.rs), and managed live via the aura route / aura status
admin commands (crates/aura-cli/src/admin.rs).
Concept: VPN vs DIRECT
Every outbound IP packet read from the TUN device is classified into one of two actions
(RouteAction):
Vpn— encrypt and send the packet over the Aura connection to the server.Direct— let the packet egress directly, bypassing the tunnel.
The router (AuraRouter::run, router.rs) parses each packet's destination IP, classifies
it, and dispatches:
TUN read --> parse dst IP --> RouteTable.classify(dst) --> Vpn? -> conn.send_packet()
\ Direct? -> send_direct() (v1 stub)
v1 limitation —
Directis a stub.send_directcurrently logs and drops the packet; real raw-socket / OS-stack re-injection is out of scope for v1. The method is alreadyasyncand fallible so a real egress path can slot in without changing call sites. The VPN path is fully functional end-to-end. Packets whose destination cannot be parsed (not IPv4/IPv6, or too short) are dropped with a trace.
The inbound direction is straightforward: decrypted IP packets received from the peer are written back to the TUN device.
Rules
The routing table (RouteTable, routes.rs) holds three things: a set of CIDR rules, a
set of domain rules, and a default action.
CIDR rules
A CIDR rule is an IpNetwork (e.g. 10.0.0.0/8) plus an action. CIDR rules are keyed by
network, so re-adding the same network overwrites its action.
Domain rules
A domain rule is a domain name plus an action. Domains do not match IPs directly. Instead
AuraDns (dns.rs) resolves the domain via the system resolver (hickory) and inserts each
resulting address as a host route — /32 for IPv4, /128 for IPv6 — so it participates
in the normal longest-prefix match. Resolution results are cached.
Because domain rules become host routes at resolution time, they only take effect once the domain has been resolved (at startup, or on demand). They reflect the addresses seen at resolution time and are not continuously re-resolved in v1.
Default action
If no CIDR rule (including resolved domain host routes) matches a destination, the table's default action applies.
Longest-prefix precedence
classify(dst_ip) performs a longest-prefix match (routes.rs):
Among all CIDR rules whose network contains the destination, the rule with the largest prefix length (most specific) wins. If no rule matches, the default action is returned.
This lets a specific range override a broader one regardless of insertion order. IPv4 rules only match IPv4 destinations and IPv6 rules only match IPv6 destinations.
Example (from the shipped config): with default = VPN, 10.0.0.0/8 = Direct, and
10.7.0.0/24 = Vpn:
| Destination | Matched rule | Action |
|---|---|---|
10.1.2.3 |
10.0.0.0/8 |
Direct |
10.7.0.9 |
10.7.0.0/24 (more specific, wins over /8) |
Vpn |
192.168.1.1 |
192.168.0.0/16 |
Direct |
8.8.8.8 |
(none) → default | Vpn |
Edge case: if two rules share the same prefix length, the last-inserted one wins (it overwrites the earlier entry, since rules are keyed by network).
Static config: [tunnel.split]
The split tunnel is configured in client.toml under [tunnel.split]
(crates/aura-cli/src/config.rs). build_route_table turns it into a RouteTable: CIDR
rules are applied directly; domain rules are recorded and returned for the client to resolve
at startup.
Schema
| Key | Type | Default | Meaning |
|---|---|---|---|
default |
string | "VPN" |
Action when no rule matches: VPN / DIRECT (case-insensitive) |
[[tunnel.split.direct]] |
array of rules | [] |
Rules forcing matching destinations to Direct |
[[tunnel.split.vpn]] |
array of rules | [] |
Rules forcing matching destinations through the VPN |
Each rule in direct / vpn is a table with exactly one of:
| Key | Type | Example |
|---|---|---|
cidr |
string | "192.168.0.0/16" |
domain |
string | "intranet.example.com" |
A rule with both cidr and domain, or neither, is rejected when the route table is built.
Example
# Split-tunnel routing: the default action plus per-destination overrides.
[tunnel.split]
# Default for destinations matching no rule below: "VPN" or "DIRECT".
default = "VPN"
# Send these directly (bypass the tunnel): RFC1918 ranges stay on the LAN...
[[tunnel.split.direct]]
cidr = "192.168.0.0/16"
[[tunnel.split.direct]]
cidr = "10.0.0.0/8"
# ...and a corporate domain egresses directly (resolved to host routes at startup).
[[tunnel.split.direct]]
domain = "intranet.example.com"
# Force a more-specific range back through the VPN (longest-prefix wins over 10.0.0.0/8).
[[tunnel.split.vpn]]
cidr = "10.7.0.0/24"
This is the configuration shipped in config/client.toml.example.
Live management: aura route / aura status
A running aura client (or aura server) hosts an admin socket — a tiny JSON
line-protocol over a Unix domain socket (crates/aura-cli/src/admin.rs). The aura route and aura status subcommands connect to it to inspect and mutate the live routing
table without restarting the tunnel. The default socket path is /tmp/aura-admin.sock
(override with --admin-socket).
Platform note: the admin socket uses Unix domain sockets (Linux/macOS). On Windows it is a
cfg-gated stub that returns an explanatory error (a named-pipe transport is future work), so the rest of the CLI still compiles there.
Commands
aura route add (--cidr <CIDR> | --domain <DOMAIN>) --action <vpn|direct> [--admin-socket <PATH>]
aura route list [--admin-socket <PATH>]
aura route remove --cidr <CIDR> [--admin-socket <PATH>]
aura status [--admin-socket <PATH>]
route add takes exactly one of --cidr / --domain (they are mutually exclusive, and
one is required), plus --action vpn or --action direct.
# Send a CIDR directly, live.
aura route add --cidr 8.8.8.0/24 --action direct
# ok
# Route a domain through the VPN (resolved into host routes).
aura route add --domain example.com --action vpn
# ok
# Inspect the current rules and default.
aura route list
# default: vpn
# cidr 8.8.8.0/24 direct
# domain example.com vpn
# Remove a CIDR rule.
aura route remove --cidr 8.8.8.0/24
# ok (removed) # or: "ok (nothing to remove)" if it wasn't present
# Tunnel status / counters.
aura status
# Aura tunnel status
# peer: client-1
# default: vpn
# rules: 1
# rx packets: 0
# tx packets: 0
Behavior notes
route removeonly removes CIDR rules — it takes--cidrand has no domain form. The libraryRouteTablehas no per-rule remove API, so a removal rebuilds the table from the surviving rules (preserving the default). Domain rules are re-added on rebuild, but their previously resolved host routes are dropped and re-resolved on demand.route listenumerates a rule mirror. The liveRouteTableis the source of truth for classification but does not expose iteration, so the admin layer keeps a parallel mirror in lockstep with every mutation;listechoes that mirror whileclassifystill uses the real table.statusreports the verified peer id, the default action, the total rule count (CIDR + domain), and inbound/outbound packet counters.
Wire protocol (for reference)
One JSON object per line, request then response (crates/aura-cli/src/admin.rs):
-> {"cmd":"route_add","cidr":"8.8.8.0/24","action":"direct"}
<- {"ok":true}
-> {"cmd":"route_list"}
<- {"ok":true,"default":"vpn","cidrs":[{"cidr":"8.8.8.0/24","action":"direct"}],"domains":[]}
-> {"cmd":"route_remove","cidr":"8.8.8.0/24"}
<- {"ok":true,"removed":true}
-> {"cmd":"status"}
<- {"ok":true,"peer_id":"client-1","rx_packets":0,"tx_packets":0,"default":"vpn","rules":1}
On error the response is {"ok":false,"error":"..."}.
v1 limitations summary
Directegress is a stub —Directpackets are logged and dropped, not re-injected to the OS stack. The VPN path is fully functional.- Domain rules are resolved once (at startup / on demand) into host routes; no continuous re-resolution.
route removeis CIDR-only and rebuilds the table (domain host routes are re-resolved on demand afterward).- Admin socket is Unix-only; Windows is a
cfg-gated stub. - The server is a single shared TUN in v1, and the tunnel resolver
dnsconfig field is informational (the system resolver is used).