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:
@@ -27,7 +27,7 @@ use std::sync::Arc;
|
||||
|
||||
use aura_proto::PacketConnection;
|
||||
use aura_tunnel::router::dst_ip;
|
||||
use aura_tunnel::PacketIo;
|
||||
use aura_tunnel::{PacketCounters, PacketIo};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
use crate::pool::IpPool;
|
||||
@@ -119,22 +119,44 @@ pub struct ServerRouter<P: PacketIo> {
|
||||
/// drains the receiver.
|
||||
inbound_tx: mpsc::Sender<Vec<u8>>,
|
||||
inbound_rx: mpsc::Receiver<Vec<u8>>,
|
||||
/// Optional packet counters bumped on every server-side TUN tx/rx. Tx counts packets the
|
||||
/// server read from its own TUN and dispatched to a client; rx counts packets a client sent
|
||||
/// that were successfully written back to the TUN. Wired to the admin `Stats` so `aura status`
|
||||
/// reports live numbers. `None` skips the atomic ops entirely.
|
||||
counters: Option<PacketCounters>,
|
||||
}
|
||||
|
||||
impl<P: PacketIo + 'static> ServerRouter<P> {
|
||||
/// Build a fresh router with empty routes and the given pool.
|
||||
///
|
||||
/// No stats are recorded. Use [`Self::with_stats`] if `aura status` should see live counters.
|
||||
pub fn new(tun: P, pool: Arc<IpPool>) -> Self {
|
||||
Self::from_routes(tun, ServerRoutes::new(pool))
|
||||
}
|
||||
|
||||
/// Like [`Self::new`] but also wires in [`PacketCounters`] for the admin socket.
|
||||
pub fn with_stats(tun: P, pool: Arc<IpPool>, counters: Option<PacketCounters>) -> Self {
|
||||
Self::from_routes_with_stats(tun, ServerRoutes::new(pool), counters)
|
||||
}
|
||||
|
||||
/// Build a router from an existing [`ServerRoutes`] (mainly for tests that pre-seed routes).
|
||||
pub fn from_routes(tun: P, routes: ServerRoutes) -> Self {
|
||||
Self::from_routes_with_stats(tun, routes, None)
|
||||
}
|
||||
|
||||
/// Like [`Self::from_routes`] but also takes the shared admin counters.
|
||||
pub fn from_routes_with_stats(
|
||||
tun: P,
|
||||
routes: ServerRoutes,
|
||||
counters: Option<PacketCounters>,
|
||||
) -> Self {
|
||||
let (inbound_tx, inbound_rx) = mpsc::channel::<Vec<u8>>(INBOUND_CAPACITY);
|
||||
Self {
|
||||
tun,
|
||||
routes,
|
||||
inbound_tx,
|
||||
inbound_rx,
|
||||
counters,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +237,10 @@ impl<P: PacketIo + 'static> ServerRouter<P> {
|
||||
if let Err(e) = self.tun.write_packet(&pkt).await {
|
||||
return Err(anyhow::Error::new(e).context("server TUN write failed"));
|
||||
}
|
||||
// Only count packets actually delivered to the server-side TUN.
|
||||
if let Some(c) = &self.counters {
|
||||
c.inc_rx();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// All inbound senders dropped (the accept-loop and all per-conn
|
||||
@@ -234,7 +260,13 @@ impl<P: PacketIo + 'static> ServerRouter<P> {
|
||||
return Ok(());
|
||||
};
|
||||
match self.routes.dispatch(dst, pkt).await? {
|
||||
true => Ok(()),
|
||||
true => {
|
||||
// Count packets that actually made it to a registered client connection.
|
||||
if let Some(c) = &self.counters {
|
||||
c.inc_tx();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
false => {
|
||||
tracing::trace!(%dst, len = pkt.len(), "no client registered for destination; dropping");
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user