ba8d6b796f
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>
98 lines
3.2 KiB
Rust
98 lines
3.2 KiB
Rust
//! Integration tests for the `[client] bridges` field + [`aura_cli::dial_targets::build_dial_targets`].
|
|
//!
|
|
//! Parses a synthetic `client.toml` with bridges, walks through `build_dial_targets`, and asserts
|
|
//! the resulting candidate list shape. Real dial attempts are out of scope (no server running);
|
|
//! this test focuses on the parse-build-shape contract that `client::run` relies on.
|
|
|
|
use aura_cli::config::ClientConfigFile;
|
|
use aura_cli::dial_targets::build_dial_targets;
|
|
|
|
const CLIENT_TOML: &str = r#"
|
|
[client]
|
|
name = "laptop"
|
|
server_addr = "203.0.113.10:443"
|
|
sni = "vpn.example.com"
|
|
bridges = ["203.0.113.11", "203.0.113.12:9999"]
|
|
|
|
[pki]
|
|
ca_cert = "ca.crt"
|
|
cert = "client.crt"
|
|
key = "client.key"
|
|
|
|
[tunnel]
|
|
local_ip = "10.7.0.2"
|
|
"#;
|
|
|
|
#[test]
|
|
fn bridges_parse_into_client_config() {
|
|
let cfg = ClientConfigFile::parse(CLIENT_TOML).expect("parse");
|
|
assert_eq!(cfg.client.bridges.len(), 2);
|
|
assert!(cfg.client.bridges.contains(&"203.0.113.11".to_string()));
|
|
assert!(cfg
|
|
.client
|
|
.bridges
|
|
.contains(&"203.0.113.12:9999".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn build_dial_targets_from_parsed_client_config() {
|
|
let cfg = ClientConfigFile::parse(CLIENT_TOML).expect("parse");
|
|
let dial = cfg.dial_config().expect("dial config");
|
|
let targets = build_dial_targets(&dial.endpoints, &cfg.client.bridges);
|
|
assert_eq!(targets.len(), 3, "primary + two bridges");
|
|
|
|
// The primary is always first. v3.4 default udp_port is 8443 (not 443).
|
|
assert_eq!(targets[0].udp.unwrap().to_string(), "203.0.113.10:8443");
|
|
|
|
// Each bridge entry must keep the per-transport ports (the bridge `:9999` in the second
|
|
// string is ignored — transports always use [transport] ports, which default to 8443/8444
|
|
// in v3.4).
|
|
for t in &targets[1..] {
|
|
assert_eq!(t.udp.unwrap().port(), 8443);
|
|
assert_eq!(t.quic.unwrap().port(), 8444);
|
|
}
|
|
|
|
// Both bridge IPs are represented.
|
|
let bridge_ips: std::collections::HashSet<String> = targets[1..]
|
|
.iter()
|
|
.map(|e| e.udp.unwrap().ip().to_string())
|
|
.collect();
|
|
assert!(bridge_ips.contains("203.0.113.11"));
|
|
assert!(bridge_ips.contains("203.0.113.12"));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_bridges_field_yields_only_primary() {
|
|
let toml = r#"
|
|
[client]
|
|
name = "laptop"
|
|
server_addr = "203.0.113.10:443"
|
|
sni = "vpn.example.com"
|
|
|
|
[pki]
|
|
ca_cert = "ca.crt"
|
|
cert = "client.crt"
|
|
key = "client.key"
|
|
|
|
[tunnel]
|
|
local_ip = "10.7.0.2"
|
|
"#;
|
|
let cfg = ClientConfigFile::parse(toml).expect("parse minimal");
|
|
assert!(cfg.client.bridges.is_empty(), "no bridges field");
|
|
let dial = cfg.dial_config().expect("dial config");
|
|
let targets = build_dial_targets(&dial.endpoints, &cfg.client.bridges);
|
|
assert_eq!(targets.len(), 1, "only primary when bridges omitted");
|
|
}
|
|
|
|
/// `detect_default_egress_iface` is best-effort and tolerated to be `None`. When it does return a
|
|
/// value, the iface name must be non-empty.
|
|
#[test]
|
|
fn detect_default_egress_iface_is_tolerant() {
|
|
match aura_cli::os_routes::detect_default_egress_iface() {
|
|
Some(iface) => assert!(!iface.is_empty(), "detected iface name must be non-empty"),
|
|
None => {
|
|
// CI / sandboxed environments often have no default route. Tolerated.
|
|
}
|
|
}
|
|
}
|