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:
@@ -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