feat(transport,tunnel): implement Wave 3 — QUIC transport + split-tunnel router

aura-transport: quinn 0.11 endpoint with HTTP/3 mimicry (ALPN h3/h3-29,
Chrome-like transport params), outer-TLS accept-any (real auth is the inner
Aura handshake), packet padding to HTTPS sizes; AuraServer/AuraClient drive the
proto handshake over a QUIC bidi stream; AuraConnection impls
aura_proto::PacketConnection (full-duplex via Session::split + per-half mutex).
14 tests incl. a real-QUIC loopback end-to-end (crypto+pki+proto+transport).

aura-tunnel: RouteTable (longest-prefix split-tunnel classify), AuraDns
(hickory) host-route registration, AuraRouter over a PacketIo TUN seam +
Arc<dyn PacketConnection>, AuraTun (tun 0.8 unix; wintun cfg-gated Windows).
10 tests (route classify/priority, dst-IP parse, mock router). send_direct is a
v1 stub. Whole workspace: tests green, clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xah30
2026-05-25 18:26:39 +03:00
parent 0a045c248d
commit c19a6c5586
14 changed files with 1887 additions and 4 deletions
+206
View File
@@ -0,0 +1,206 @@
//! Cross-platform TUN device (project §8.1 / §8.2).
//!
//! [`AuraTun`] is a thin async wrapper over a layer-3 TUN interface:
//!
//! * **Unix (Linux + macOS)** via the [`tun`] crate (0.8): `tun::create_as_async(&Configuration)`
//! yields an `AsyncDevice` whose `recv`/`send` move whole IP packets. On macOS the interface name
//! is system-assigned (`utunN`) and the requested name may be ignored — we do not treat a name
//! mismatch as an error.
//! * **Windows** via the [`wintun`] crate (0.5): `Adapter::create(..)` + `start_session(..)`. This
//! path is `cfg(windows)`-gated and is *not compiled* on the macOS development host; it is
//! validated by inspection only.
//!
//! Creating a real TUN needs elevated privileges and cannot run in unit tests. The router talks to
//! the device through the small [`PacketIo`] trait (defined here) so tests can substitute an
//! in-memory fake; [`AuraTun`] is the production implementor.
use async_trait::async_trait;
/// The minimal read/write seam the router needs from a packet device.
///
/// Implemented by the real [`AuraTun`] and, in tests, by an in-memory fake. This is the testability
/// seam that lets [`crate::router::AuraRouter`] be driven without root or a real TUN. It is a tiny,
/// crate-defined trait (deliberately not a general I/O abstraction); it is `pub` only so that the
/// crate's integration tests (which live in an external test crate) can supply a fake implementor.
#[async_trait]
pub trait PacketIo: Send {
/// Read one IP packet from the device.
async fn read_packet(&mut self) -> std::io::Result<Vec<u8>>;
/// Write one IP packet to the device.
async fn write_packet(&mut self, packet: &[u8]) -> std::io::Result<()>;
}
/// A cross-platform layer-3 TUN device.
pub struct AuraTun {
#[cfg(not(windows))]
inner: tun::AsyncDevice,
#[cfg(not(windows))]
mtu: u16,
#[cfg(windows)]
inner: std::sync::Arc<wintun::Session>,
#[cfg(windows)]
mtu: u16,
}
impl AuraTun {
/// Create and bring up a TUN interface named `name` with address `ip`/`prefix_len` and the
/// given `mtu`.
///
/// On macOS `name` is advisory (the kernel assigns `utunN`); a different resulting name is not
/// an error. Requires privileges, so this is never called from unit tests.
#[cfg(not(windows))]
pub async fn create(
name: &str,
ip: std::net::IpAddr,
prefix_len: u8,
mtu: u16,
) -> anyhow::Result<Self> {
use anyhow::Context;
// `tun_name()` (and the other accessors) live on the AbstractDevice trait.
use tun::AbstractDevice;
// Derive the dotted/colon netmask for the requested prefix length from ipnetwork, which
// keeps the v4/v6 mask maths in one well-tested place.
let netmask = ipnetwork::IpNetwork::new(ip, prefix_len)
.with_context(|| format!("invalid TUN address {ip}/{prefix_len}"))?
.mask();
let mut config = tun::Configuration::default();
config
.tun_name(name)
.address(ip)
.netmask(netmask)
.mtu(mtu)
.layer(tun::Layer::L3)
.up();
let inner = tun::create_as_async(&config)
.with_context(|| format!("failed to create TUN device '{name}'"))?;
// macOS hands back a system-assigned utunN; log the real name but don't fail on mismatch.
if let Ok(actual) = inner.tun_name() {
if actual != name {
tracing::info!(
requested = name,
actual = %actual,
"TUN interface name differs from requested (expected on macOS)"
);
}
}
Ok(Self { inner, mtu })
}
/// Read one IP packet from the TUN device.
#[cfg(not(windows))]
pub async fn read_packet(&mut self) -> anyhow::Result<Vec<u8>> {
// Size the buffer to the MTU plus headroom so a full-size packet is never truncated.
let mut buf = vec![0u8; self.mtu as usize + 4];
let n = self.inner.recv(&mut buf).await?;
buf.truncate(n);
Ok(buf)
}
/// Write one IP packet to the TUN device.
#[cfg(not(windows))]
pub async fn write_packet(&mut self, packet: &[u8]) -> anyhow::Result<()> {
self.inner.send(packet).await?;
Ok(())
}
// ---- Windows (wintun 0.5) -------------------------------------------------------------------
// cfg(windows)-gated: not compiled on the macOS host, validated by inspection only.
/// Create and bring up a wintun adapter named `name` with address `ip`/`prefix_len`.
///
/// wintun ignores per-interface MTU (its ring is fixed at 65535), so `mtu` is retained only for
/// read-buffer sizing. Only IPv4 addressing is wired here, matching wintun 0.5's
/// `set_address`/`set_netmask` (which take `Ipv4Addr`); an IPv6 address yields an error.
#[cfg(windows)]
pub async fn create(
name: &str,
ip: std::net::IpAddr,
prefix_len: u8,
mtu: u16,
) -> anyhow::Result<Self> {
use anyhow::Context;
use std::net::IpAddr;
let ipv4 = match ip {
IpAddr::V4(v4) => v4,
IpAddr::V6(_) => {
anyhow::bail!("wintun backend currently supports only IPv4 TUN addresses")
}
};
let netmask = match ipnetwork::IpNetwork::new(ip, prefix_len)
.with_context(|| format!("invalid TUN address {ip}/{prefix_len}"))?
.mask()
{
IpAddr::V4(m) => m,
IpAddr::V6(_) => unreachable!("v4 address yields a v4 mask"),
};
// SAFETY: loads the bundled wintun.dll via its documented entry point.
let wintun = unsafe { wintun::load() }.context("failed to load wintun.dll")?;
let adapter = wintun::Adapter::create(&wintun, name, "Aura", None)
.with_context(|| format!("failed to create wintun adapter '{name}'"))?;
adapter
.set_address(ipv4)
.context("failed to set wintun adapter address")?;
adapter
.set_netmask(netmask)
.context("failed to set wintun adapter netmask")?;
let session = adapter
.start_session(wintun::MAX_RING_CAPACITY)
.context("failed to start wintun session")?;
Ok(Self {
inner: std::sync::Arc::new(session),
mtu,
})
}
/// Read one IP packet from the wintun session.
///
/// `receive_blocking` is a blocking call, so it runs on a blocking thread to avoid stalling the
/// async runtime.
#[cfg(windows)]
pub async fn read_packet(&mut self) -> anyhow::Result<Vec<u8>> {
let session = self.inner.clone();
let packet = tokio::task::spawn_blocking(move || session.receive_blocking()).await??;
Ok(packet.bytes().to_vec())
}
/// Write one IP packet to the wintun session.
#[cfg(windows)]
pub async fn write_packet(&mut self, packet: &[u8]) -> anyhow::Result<()> {
let len: u16 = packet
.len()
.try_into()
.map_err(|_| anyhow::anyhow!("packet too large for wintun ({} bytes)", packet.len()))?;
let mut send = self
.inner
.allocate_send_packet(len)
.map_err(|e| anyhow::anyhow!("wintun allocate_send_packet failed: {e}"))?;
send.bytes_mut().copy_from_slice(packet);
self.inner.send_packet(send);
Ok(())
}
}
#[async_trait]
impl PacketIo for AuraTun {
async fn read_packet(&mut self) -> std::io::Result<Vec<u8>> {
AuraTun::read_packet(self)
.await
.map_err(|e| std::io::Error::other(e.to_string()))
}
async fn write_packet(&mut self, packet: &[u8]) -> std::io::Result<()> {
AuraTun::write_packet(self, packet)
.await
.map_err(|e| std::io::Error::other(e.to_string()))
}
}