Files
AuraVPN/docs/split-tunnel.md
T
xah30 46513354c0 docs: add protocol, PKI, and split-tunnel documentation
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>
2026-05-25 18:40:19 +03:00

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 — Direct is a stub. send_direct currently logs and drops the packet; real raw-socket / OS-stack re-injection is out of scope for v1. The method is already async and 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 remove only removes CIDR rules — it takes --cidr and has no domain form. The library RouteTable has 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 list enumerates a rule mirror. The live RouteTable is the source of truth for classification but does not expose iteration, so the admin layer keeps a parallel mirror in lockstep with every mutation; list echoes that mirror while classify still uses the real table.
  • status reports 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

  • Direct egress is a stubDirect packets 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 remove is 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 dns config field is informational (the system resolver is used).