feat(transport,cli,tunnel): v3.4 port auto-detect + bug fixes from live test
Live macOS test against the production server uncovered six bugs (one of which turned out to be a port collision with sing-box, not a real bug); this commit addresses all of them and adds v3.4 port discovery so the same collision is handled transparently next time. ## v3.4 server port-discovery - Defaults moved off 443/444 to 8443/8443/8444 (TransportSection::default, ServerInitOpts, ProvisionClientOpts, CLI flags). 443 is heavily contested in practice (sing-box, Hysteria2, reverse proxies) and the previous default silently lost the bind when a co-tenant was already there. - MultiServer::bind_with_outer_or_scan: scans forward up to DEFAULT_PORT_SCAN_MAX (20) candidates per transport when the requested port is occupied; QUIC keeps walking if it lands on the custom-UDP port. - MultiServer::bound_addrs(): the actual addresses each transport bound to. - Server logs the bound addresses and writes a runtime snapshot (server.toml.runtime.json) when they differ from the requested ones, so `aura sign-bridges` can re-sign the bridges manifest later. - BridgeManifest gains an optional `endpoints: Vec<BridgeEndpoint>` field with per-transport ports. Backward-compatible: old v3.3 clients ignore the field and continue to use the v1 `bridges` line. - `aura sign-bridges --endpoints HOST:tcp=N:quic=N:udp=N` to mint v3.4 manifests; bridges line is auto-synthesised for v3.3 clients. ## Bug fixes from the live test - macOS TUN naming (#41): the tun crate rejects names that don't match ^utun[0-9]+$. On macOS we now substitute `""` (kernel auto-assigns utunN), capture the assigned name via inner.tun_name(), and propagate it through to os_routes::OsRouteGuard::install — so `route add -interface utunN` uses the real interface, not "aura0". - Packet counters (#42): Stats { tx_packets, rx_packets } are now actually bumped by the data path. `aura status` shows live numbers instead of permanent zeros. - render_client_toml schema (#44): provisioner emits proper `[[tunnel.split.vpn]] cidr = "..."` / `[[tunnel.split.direct]]` blocks from new --vpn-cidrs / --direct-cidrs flags. The v3.3 `vpn_cidrs = [...]` flat array was silently ignored by serde, leaving users with `rules: 0` even when their CIDRs looked right. - #43 / #46 (TCP/443 dial early-eof / no payload back): diagnosed as the sing-box port collision, not an Aura bug. The v3.4 port-scan path makes it go away — the server picks a free port and clients learn it from the manifest. ## Test coverage Three new unit tests for the port-scanner (UDP busy, TCP busy, zero budget); two new tests for v3.4 BridgeManifest round-trip with endpoints; one integration test for the new `[[tunnel.split.vpn]]` rendering; tests for the runtime-state file write/read round-trip; agent-added router-counter tests in aura-tunnel/tests/routes.rs. cargo test --workspace, cargo clippy --workspace -- -D warnings, and cargo fmt --check all pass. #45 (silent client exit when underlying QUIC transport breaks) is still outstanding — needs deeper investigation; deferred to a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ pub mod routes;
|
||||
pub mod tun;
|
||||
|
||||
pub use dns::AuraDns;
|
||||
pub use router::{dst_ip, AuraRouter};
|
||||
pub use router::{dst_ip, AuraRouter, PacketCounters};
|
||||
pub use routes::{RouteAction, RouteTable};
|
||||
pub use tun::{AuraTun, PacketIo};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
//! the device in one place while still running both directions concurrently.
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use aura_proto::PacketConnection;
|
||||
@@ -28,6 +29,52 @@ use tokio::sync::{mpsc, RwLock};
|
||||
use crate::routes::{RouteAction, RouteTable};
|
||||
use crate::tun::PacketIo;
|
||||
|
||||
/// Cloneable handle to the data-plane packet counters surfaced over the admin socket.
|
||||
///
|
||||
/// The router owns one of these and bumps `tx` on every packet leaving the TUN (whether the
|
||||
/// classifier sends it through the encrypted connection or to the v1 `send_direct` stub) and `rx`
|
||||
/// on every packet successfully written back to the TUN. The admin layer (`aura status`) reads the
|
||||
/// same atomics through its own clone of this handle, so the counters are always live.
|
||||
///
|
||||
/// Both halves are independently cloneable `Arc<AtomicU64>`s so router and admin can hold their
|
||||
/// own clones without one knowing about the other's type.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PacketCounters {
|
||||
/// Outbound (TUN → peer) packet count.
|
||||
pub tx: Arc<AtomicU64>,
|
||||
/// Inbound (peer → TUN) packet count.
|
||||
pub rx: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl PacketCounters {
|
||||
/// Create a fresh pair of zeroed counters.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Increment the outbound counter.
|
||||
#[inline]
|
||||
pub fn inc_tx(&self) {
|
||||
self.tx.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Increment the inbound counter.
|
||||
#[inline]
|
||||
pub fn inc_rx(&self) {
|
||||
self.rx.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Snapshot the current outbound count.
|
||||
pub fn tx_count(&self) -> u64 {
|
||||
self.tx.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Snapshot the current inbound count.
|
||||
pub fn rx_count(&self) -> u64 {
|
||||
self.rx.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the destination IP address out of a raw IPv4 or IPv6 packet.
|
||||
///
|
||||
/// Returns `None` for packets too short to contain a destination, or whose version nibble is
|
||||
@@ -49,6 +96,10 @@ pub struct AuraRouter<P: PacketIo> {
|
||||
tun: P,
|
||||
routes: Arc<RwLock<RouteTable>>,
|
||||
conn: Arc<dyn PacketConnection>,
|
||||
/// Optional counters bumped on every packet that crosses the TUN in either direction. When
|
||||
/// `None`, the data path skips the atomic operation entirely. The CLI plugs in the same
|
||||
/// counters the admin socket reads from, which is what makes `aura status` show live numbers.
|
||||
counters: Option<PacketCounters>,
|
||||
}
|
||||
|
||||
impl<P: PacketIo + 'static> AuraRouter<P> {
|
||||
@@ -56,8 +107,28 @@ impl<P: PacketIo + 'static> AuraRouter<P> {
|
||||
///
|
||||
/// `tun` is any [`PacketIo`]; the CLI passes an [`AuraTun`](crate::AuraTun) (which implements
|
||||
/// it). `conn` is shared (`Arc`) so both the outbound and inbound flows can use it.
|
||||
///
|
||||
/// No stats are recorded — equivalent to [`Self::with_stats`] with `None`. Use that constructor
|
||||
/// instead if you want `aura status` to see live tx/rx counts.
|
||||
pub fn new(tun: P, routes: Arc<RwLock<RouteTable>>, conn: Arc<dyn PacketConnection>) -> Self {
|
||||
Self { tun, routes, conn }
|
||||
Self::with_stats(tun, routes, conn, None)
|
||||
}
|
||||
|
||||
/// Like [`Self::new`] but also wires in [`PacketCounters`] the router will bump on every
|
||||
/// packet (tx for TUN→peer, rx for peer→TUN). The CLI clones the same counters into its
|
||||
/// `admin::Stats` so the admin socket sees live numbers.
|
||||
pub fn with_stats(
|
||||
tun: P,
|
||||
routes: Arc<RwLock<RouteTable>>,
|
||||
conn: Arc<dyn PacketConnection>,
|
||||
counters: Option<PacketCounters>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tun,
|
||||
routes,
|
||||
conn,
|
||||
counters,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the router until the connection or TUN errors out.
|
||||
@@ -101,6 +172,10 @@ impl<P: PacketIo + 'static> AuraRouter<P> {
|
||||
if let Err(e) = self.tun.write_packet(&pkt).await {
|
||||
break Err(anyhow::Error::new(e).context("TUN write failed"));
|
||||
}
|
||||
// Only count packets actually delivered to the TUN.
|
||||
if let Some(c) = &self.counters {
|
||||
c.inc_rx();
|
||||
}
|
||||
}
|
||||
// Inbound task ended (connection closed/errored).
|
||||
None => break Ok(()),
|
||||
@@ -130,6 +205,11 @@ impl<P: PacketIo + 'static> AuraRouter<P> {
|
||||
self.send_direct(dst, pkt).await?;
|
||||
}
|
||||
}
|
||||
// Every parseable packet that left the TUN counts as a tx, regardless of whether the
|
||||
// classifier put it on the encrypted connection (VPN) or handed it to the direct stub.
|
||||
if let Some(c) = &self.counters {
|
||||
c.inc_tx();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+164
-19
@@ -3,12 +3,16 @@
|
||||
//! [`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.
|
||||
//! yields an `AsyncDevice` whose `recv`/`send` move whole IP packets. On macOS the kernel
|
||||
//! `utun` driver requires interface names to match `^utun[0-9]+$`; any other requested name is
|
||||
//! rewritten to an empty string before creation, which makes the kernel auto-assign the next
|
||||
//! free `utunN`. The actual assigned name is captured via [`tun::AbstractDevice::tun_name`] and
|
||||
//! exposed via [`AuraTun::name`] so callers (e.g. the OS-routes installer) can program the real
|
||||
//! interface instead of the requested-but-ignored config string.
|
||||
//! * **Windows** via the [`wintun`] crate (0.5): `Adapter::create(..)` + `start_session(..)`. The
|
||||
//! adapter accepts arbitrary names (it's a display name, not a kernel interface name), so the
|
||||
//! requested `name` is used verbatim. `cfg(windows)`-gated and validated by inspection on the
|
||||
//! macOS development host.
|
||||
//!
|
||||
//! 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
|
||||
@@ -49,14 +53,30 @@ pub struct AuraTun {
|
||||
_adapter: std::sync::Arc<wintun::Adapter>,
|
||||
#[cfg(windows)]
|
||||
mtu: u16,
|
||||
|
||||
/// The actual kernel-assigned interface name. On Linux and Windows this matches the
|
||||
/// `name` argument passed to [`AuraTun::create`]; on macOS the kernel `utun` driver may
|
||||
/// assign a different `utunN` (see the module docs for why), in which case this field
|
||||
/// holds the assigned name and the requested config string is discarded.
|
||||
name: String,
|
||||
}
|
||||
|
||||
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.
|
||||
/// On macOS `name` is advisory: the kernel `utun` driver only accepts names matching
|
||||
/// `^utun[0-9]+$`, so a non-conforming requested name (e.g. `"aura0"`, the default the v1
|
||||
/// config carries from Linux/Windows) would otherwise fail creation with `invalid device tun
|
||||
/// name`. We rewrite a non-conforming name to the empty string before calling into the
|
||||
/// `tun` crate, which makes the kernel auto-assign the next free `utunN`; the assigned name
|
||||
/// is captured into [`AuraTun::name`] so callers (e.g. the OS-routes installer) can program
|
||||
/// the *actual* interface, not the requested-but-ignored config string.
|
||||
///
|
||||
/// On Linux the requested name is honoured verbatim and recorded as-is.
|
||||
///
|
||||
/// Requires privileges, so this is never called from unit tests except for the macOS
|
||||
/// auto-rename verification gated on `target_os = "macos"`.
|
||||
#[cfg(not(windows))]
|
||||
pub async fn create(
|
||||
name: &str,
|
||||
@@ -74,9 +94,18 @@ impl AuraTun {
|
||||
.with_context(|| format!("invalid TUN address {ip}/{prefix_len}"))?
|
||||
.mask();
|
||||
|
||||
// macOS: the kernel utun driver enforces `^utun[0-9]+$` and rejects anything else with
|
||||
// `invalid device tun name`. Pass the requested name through `sanitize_macos_tun_name`
|
||||
// which returns `""` for non-conforming names; the tun crate treats `""` as
|
||||
// "let the kernel pick the next free utunN".
|
||||
#[cfg(target_os = "macos")]
|
||||
let requested_name = sanitize_macos_tun_name(name);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let requested_name: &str = name;
|
||||
|
||||
let mut config = tun::Configuration::default();
|
||||
config
|
||||
.tun_name(name)
|
||||
.tun_name(requested_name)
|
||||
.address(ip)
|
||||
.netmask(netmask)
|
||||
.mtu(mtu)
|
||||
@@ -86,18 +115,44 @@ impl AuraTun {
|
||||
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)"
|
||||
);
|
||||
}
|
||||
// Capture the kernel-assigned name. On macOS this is the auto-picked `utunN`; on Linux
|
||||
// it matches `name`. If the accessor fails (shouldn't in practice), fall back to the
|
||||
// requested name so the rest of the system still has *something* to log/route against.
|
||||
let actual = inner.tun_name().unwrap_or_else(|_| name.to_string());
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if requested_name.is_empty() {
|
||||
tracing::info!(
|
||||
requested = name,
|
||||
actual = %actual,
|
||||
"macOS kernel utun driver rejects names not matching ^utun[0-9]+$; \
|
||||
auto-assigned an interface — downstream OS-routes / logs use the actual name"
|
||||
);
|
||||
} else if actual != name {
|
||||
// The user passed a `utunN` name explicitly but the kernel handed back a different
|
||||
// one (typically because the requested utunN was already in use).
|
||||
tracing::info!(
|
||||
requested = name,
|
||||
actual = %actual,
|
||||
"macOS kernel assigned a different utunN than requested (requested busy?)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self { inner, mtu })
|
||||
Ok(Self {
|
||||
inner,
|
||||
mtu,
|
||||
name: actual,
|
||||
})
|
||||
}
|
||||
|
||||
/// The actual kernel-assigned interface name. On Linux/Windows this matches the `name`
|
||||
/// passed to [`AuraTun::create`]. On macOS the kernel `utun` driver may auto-assign a
|
||||
/// `utunN` different from the requested name (and *must* do so when the requested name
|
||||
/// doesn't match `^utun[0-9]+$`); callers must use this method, not the original config
|
||||
/// string, when programming OS routes or logging the live device.
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Read one IP packet from the TUN device.
|
||||
@@ -178,6 +233,7 @@ impl AuraTun {
|
||||
inner: std::sync::Arc::new(session),
|
||||
_adapter: adapter,
|
||||
mtu,
|
||||
name: name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -250,3 +306,92 @@ impl PacketIo for AuraTun {
|
||||
.map_err(|e| std::io::Error::other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrite a requested TUN name into a form acceptable to the macOS kernel `utun` driver.
|
||||
///
|
||||
/// The driver only accepts names matching `^utun[0-9]+$`. Anything else (including the Linux
|
||||
/// default `"aura0"`) is mapped to the empty string, which `tun::create_as_async` interprets as
|
||||
/// "let the kernel pick the next free `utunN`". A name that already matches is passed through
|
||||
/// verbatim so the operator can still pin a specific `utunN` from config when they want to.
|
||||
///
|
||||
/// Made `pub(crate)` (and unit-tested below) so the macOS create path is the only public surface
|
||||
/// that sees the rewrite; the function is platform-independent so we always compile it (avoids a
|
||||
/// `cfg`-gated helper that's only exercised on macOS CI).
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
pub(crate) fn sanitize_macos_tun_name(name: &str) -> &str {
|
||||
if is_valid_macos_utun_name(name) {
|
||||
name
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Does `name` match `^utun[0-9]+$` — the only form the macOS kernel `utun` driver accepts?
|
||||
fn is_valid_macos_utun_name(name: &str) -> bool {
|
||||
let Some(digits) = name.strip_prefix("utun") else {
|
||||
return false;
|
||||
};
|
||||
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `sanitize_macos_tun_name` accepts `utunN` verbatim for any non-empty all-digit suffix.
|
||||
#[test]
|
||||
fn sanitize_accepts_valid_utun_names() {
|
||||
assert_eq!(sanitize_macos_tun_name("utun0"), "utun0");
|
||||
assert_eq!(sanitize_macos_tun_name("utun8"), "utun8");
|
||||
assert_eq!(sanitize_macos_tun_name("utun42"), "utun42");
|
||||
assert_eq!(sanitize_macos_tun_name("utun999"), "utun999");
|
||||
}
|
||||
|
||||
/// `sanitize_macos_tun_name` rewrites any non-conforming name (including the Linux default
|
||||
/// `"aura0"` and edge cases like `"utun"` with no digits or `"utunx"` with non-digits) to
|
||||
/// `""` so the kernel auto-assigns the next free `utunN`.
|
||||
#[test]
|
||||
fn sanitize_rewrites_invalid_names_to_empty() {
|
||||
assert_eq!(sanitize_macos_tun_name("aura0"), "");
|
||||
assert_eq!(sanitize_macos_tun_name("aura-srv0"), "");
|
||||
assert_eq!(sanitize_macos_tun_name(""), "");
|
||||
// No digits after `utun` → invalid.
|
||||
assert_eq!(sanitize_macos_tun_name("utun"), "");
|
||||
// Non-digit suffix → invalid.
|
||||
assert_eq!(sanitize_macos_tun_name("utunx"), "");
|
||||
assert_eq!(sanitize_macos_tun_name("utun1a"), "");
|
||||
// Wrong prefix.
|
||||
assert_eq!(sanitize_macos_tun_name("tun0"), "");
|
||||
}
|
||||
|
||||
/// On macOS, requesting a non-`utunN` name (like the Linux/Windows default `"aura0"`) must
|
||||
/// succeed and yield a kernel-assigned `utunN`. Requires root, so the test is gated on
|
||||
/// `AURA_TUN_TEST=1` to keep `cargo test` runnable as a regular user. When the env var is not
|
||||
/// set, the test logs a skip and returns. When it is set but creation fails for any reason
|
||||
/// (e.g. running unprivileged anyway), the test still fails so we don't silently lose
|
||||
/// coverage.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn macos_create_with_non_utun_name_auto_assigns() {
|
||||
if std::env::var_os("AURA_TUN_TEST").is_none() {
|
||||
eprintln!(
|
||||
"skipping macos_create_with_non_utun_name_auto_assigns: \
|
||||
set AURA_TUN_TEST=1 and run as root to exercise this test"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let tun = AuraTun::create("aura0", "10.7.0.2".parse().unwrap(), 24, 1420)
|
||||
.await
|
||||
.expect("creation must succeed even with a non-utunN requested name");
|
||||
let assigned = tun.name();
|
||||
assert!(
|
||||
is_valid_macos_utun_name(assigned),
|
||||
"kernel-assigned name {:?} must match ^utun[0-9]+$",
|
||||
assigned
|
||||
);
|
||||
assert_ne!(
|
||||
assigned, "aura0",
|
||||
"macOS must NOT honour the requested non-utunN name"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use async_trait::async_trait;
|
||||
use aura_proto::PacketConnection;
|
||||
use aura_tunnel::router::dst_ip;
|
||||
use aura_tunnel::tun::PacketIo;
|
||||
use aura_tunnel::{AuraDns, AuraRouter, RouteAction, RouteTable};
|
||||
use aura_tunnel::{AuraDns, AuraRouter, PacketCounters, RouteAction, RouteTable};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
// ---- §8.4 RouteTable classification --------------------------------------------------------------
|
||||
@@ -286,3 +286,110 @@ async fn test_router_direct_not_sent_to_vpn() {
|
||||
drop(tun_in_tx);
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
|
||||
}
|
||||
|
||||
// ---- PacketCounters wiring through AuraRouter ----------------------------------------------------
|
||||
|
||||
/// A VPN-routed outbound packet bumps `tx`; a DIRECT-routed outbound packet *also* bumps `tx`
|
||||
/// (the v1 stub still counts as "tx from the TUN"); a packet pumped through the connection and
|
||||
/// successfully written to the TUN bumps `rx`.
|
||||
#[tokio::test]
|
||||
async fn test_router_packet_counters_increment_for_tx_and_rx() {
|
||||
let (tun_in_tx, tun_in_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (tun_out_tx, mut tun_out_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (conn_sent_tx, mut conn_sent_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (conn_recv_tx, conn_recv_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
|
||||
let tun = MockTun {
|
||||
inbound: tun_in_rx,
|
||||
written: tun_out_tx,
|
||||
};
|
||||
let conn: Arc<dyn PacketConnection> = Arc::new(MockConn {
|
||||
sent: conn_sent_tx,
|
||||
to_recv: tokio::sync::Mutex::new(conn_recv_rx),
|
||||
});
|
||||
|
||||
// Default Vpn with a /16 -> Direct override so we can exercise both classifier branches.
|
||||
let mut table = RouteTable::new(RouteAction::Vpn);
|
||||
table.add_cidr("192.168.0.0/16".parse().unwrap(), RouteAction::Direct);
|
||||
let routes = Arc::new(RwLock::new(table));
|
||||
let counters = PacketCounters::new();
|
||||
let router = AuraRouter::with_stats(tun, routes, conn, Some(counters.clone()));
|
||||
let handle = tokio::spawn(router.run());
|
||||
|
||||
// (a) VPN packet -> reaches connection and bumps tx to 1.
|
||||
let vpn_pkt = ipv4_packet_to(Ipv4Addr::new(8, 8, 8, 8));
|
||||
tun_in_tx.send(vpn_pkt.clone()).await.unwrap();
|
||||
let got = tokio::time::timeout(std::time::Duration::from_secs(2), conn_sent_rx.recv())
|
||||
.await
|
||||
.expect("router did not forward to connection")
|
||||
.expect("conn sent closed");
|
||||
assert_eq!(got, vpn_pkt);
|
||||
|
||||
// (b) DIRECT packet -> goes to send_direct stub but tx still counts.
|
||||
let direct_pkt = ipv4_packet_to(Ipv4Addr::new(192, 168, 1, 1));
|
||||
tun_in_tx.send(direct_pkt).await.unwrap();
|
||||
|
||||
// (c) Inbound packet -> written to TUN, bumps rx to 1.
|
||||
let in_pkt = ipv4_packet_to(Ipv4Addr::new(10, 0, 0, 9));
|
||||
conn_recv_tx.send(in_pkt.clone()).await.unwrap();
|
||||
let written = tokio::time::timeout(std::time::Duration::from_secs(2), tun_out_rx.recv())
|
||||
.await
|
||||
.expect("router did not write inbound packet to TUN")
|
||||
.expect("TUN write closed");
|
||||
assert_eq!(written, in_pkt);
|
||||
|
||||
// Wait until both tx events have been observed (the DIRECT path doesn't surface anywhere
|
||||
// externally — poll the counter).
|
||||
let mut waited_ms = 0u64;
|
||||
while counters.tx_count() < 2 && waited_ms < 2000 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
waited_ms += 10;
|
||||
}
|
||||
assert_eq!(
|
||||
counters.tx_count(),
|
||||
2,
|
||||
"both VPN- and DIRECT-routed packets must bump tx"
|
||||
);
|
||||
assert_eq!(
|
||||
counters.rx_count(),
|
||||
1,
|
||||
"one packet was written to the TUN, so rx must be 1"
|
||||
);
|
||||
|
||||
drop(tun_in_tx);
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
|
||||
}
|
||||
|
||||
/// `AuraRouter::new` (no counters) must not panic and must not blow up on packets — verifies the
|
||||
/// `None` branch of `with_stats` short-circuits safely.
|
||||
#[tokio::test]
|
||||
async fn test_router_no_counters_still_routes() {
|
||||
let (tun_in_tx, tun_in_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (tun_out_tx, _tun_out_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (conn_sent_tx, mut conn_sent_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
let (_conn_recv_tx, conn_recv_rx) = mpsc::channel::<Vec<u8>>(8);
|
||||
|
||||
let tun = MockTun {
|
||||
inbound: tun_in_rx,
|
||||
written: tun_out_tx,
|
||||
};
|
||||
let conn: Arc<dyn PacketConnection> = Arc::new(MockConn {
|
||||
sent: conn_sent_tx,
|
||||
to_recv: tokio::sync::Mutex::new(conn_recv_rx),
|
||||
});
|
||||
let routes = Arc::new(RwLock::new(RouteTable::new(RouteAction::Vpn)));
|
||||
|
||||
let router = AuraRouter::new(tun, routes, conn);
|
||||
let handle = tokio::spawn(router.run());
|
||||
|
||||
let pkt = ipv4_packet_to(Ipv4Addr::new(1, 1, 1, 1));
|
||||
tun_in_tx.send(pkt.clone()).await.unwrap();
|
||||
let got = tokio::time::timeout(std::time::Duration::from_secs(2), conn_sent_rx.recv())
|
||||
.await
|
||||
.expect("router did not forward without counters")
|
||||
.expect("conn sent closed");
|
||||
assert_eq!(got, pkt);
|
||||
|
||||
drop(tun_in_tx);
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), handle).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user