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:
@@ -86,7 +86,8 @@ fn provision_client_with_explicit_id() {
|
||||
|
||||
// The client.toml round-trips through the parser cleanly.
|
||||
let cfg = ClientConfigFile::load(&report.client_config).expect("parse client.toml");
|
||||
assert_eq!(cfg.client.server_addr, "203.0.113.10:443");
|
||||
// v3.4: default udp_port is 8443 (was 443 in v3.3).
|
||||
assert_eq!(cfg.client.server_addr, "203.0.113.10:8443");
|
||||
assert_eq!(cfg.client.sni, "vpn.example.com");
|
||||
assert_eq!(cfg.tunnel.local_ip, "10.7.0.2");
|
||||
assert!(cfg.client.bridges.is_empty(), "no bridges by default");
|
||||
@@ -280,6 +281,57 @@ fn provision_client_circuit_hops_too_few_errors() {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// v3.4: `vpn_cidrs` / `direct_cidrs` end up as `[[tunnel.split.vpn]]` / `[[tunnel.split.direct]]`
|
||||
/// blocks in the rendered client.toml, and the server's parser actually loads them into the
|
||||
/// `[tunnel.split]` rule table (proves we are not on the silently-ignored `vpn_cidrs = [...]`
|
||||
/// flat-array footgun any more).
|
||||
#[test]
|
||||
fn provision_client_emits_split_cidr_blocks() {
|
||||
let root = temp_dir("split-cidrs");
|
||||
let ca_dir = root.join("ca");
|
||||
bootstrap_ca(&ca_dir);
|
||||
let bundle = root.join("bundle");
|
||||
|
||||
let mut opts = ProvisionClientOpts::new(
|
||||
&ca_dir,
|
||||
"203.0.113.10",
|
||||
"vpn.example.com",
|
||||
"10.7.0.7",
|
||||
&bundle,
|
||||
);
|
||||
opts.vpn_cidrs = vec!["10.7.0.0/24".to_string(), "1.1.1.1/32".to_string()];
|
||||
opts.direct_cidrs = vec!["192.168.0.0/16".to_string()];
|
||||
let report = init::provision_client(&opts).expect("provision");
|
||||
|
||||
let toml_text = std::fs::read_to_string(&report.client_config).expect("read client.toml");
|
||||
// The rendered TOML uses the array-of-tables syntax the server parser actually understands.
|
||||
assert!(
|
||||
toml_text.contains("[[tunnel.split.vpn]]\ncidr = \"10.7.0.0/24\""),
|
||||
"rendered toml missing 10.7.0.0/24 vpn block:\n{toml_text}"
|
||||
);
|
||||
assert!(
|
||||
toml_text.contains("[[tunnel.split.vpn]]\ncidr = \"1.1.1.1/32\""),
|
||||
"rendered toml missing 1.1.1.1/32 vpn block:\n{toml_text}"
|
||||
);
|
||||
assert!(
|
||||
toml_text.contains("[[tunnel.split.direct]]\ncidr = \"192.168.0.0/16\""),
|
||||
"rendered toml missing 192.168.0.0/16 direct block:\n{toml_text}"
|
||||
);
|
||||
|
||||
// And the parser loads the rules — this is the bit v3.3 silently failed at.
|
||||
let cfg = ClientConfigFile::load(&report.client_config).expect("parse");
|
||||
assert_eq!(cfg.tunnel.split.vpn.len(), 2);
|
||||
assert_eq!(cfg.tunnel.split.direct.len(), 1);
|
||||
assert_eq!(cfg.tunnel.split.vpn[0].cidr.as_deref(), Some("10.7.0.0/24"));
|
||||
assert_eq!(cfg.tunnel.split.vpn[1].cidr.as_deref(), Some("1.1.1.1/32"));
|
||||
assert_eq!(
|
||||
cfg.tunnel.split.direct[0].cidr.as_deref(),
|
||||
Some("192.168.0.0/16")
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
/// A non-empty bundle directory triggers an error without `--force`.
|
||||
#[test]
|
||||
fn provision_client_refuses_non_empty_bundle() {
|
||||
|
||||
Reference in New Issue
Block a user