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:
@@ -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()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user