Files
AuraVPN/crates/aura-cli/tests/cli_server_init.rs
T
xah30 ba8d6b796f 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>
2026-05-29 17:14:45 +03:00

137 lines
5.5 KiB
Rust

//! Integration tests for [`aura_cli::init::server_init`].
//!
//! Drives the in-process helper directly (no clap parsing, no binary spawn) and asserts that the
//! generated CA + server cert + server.toml exist on disk and parse cleanly. Each switch on the
//! `ServerInitOpts` flips the corresponding section in the rendered TOML.
use std::path::PathBuf;
use aura_cli::config::ServerConfigFile;
use aura_cli::init::{self, ServerInitOpts};
/// Unique temp dir for one test (no `tempfile` dependency in the workspace).
fn temp_dir(tag: &str) -> PathBuf {
let mut dir = std::env::temp_dir();
dir.push(format!(
"aura-cli-server-init-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
/// Build a baseline options struct with the temp directories pre-filled. Per-test mutations layer
/// on top of this.
fn base_opts(tag: &str) -> (ServerInitOpts, PathBuf) {
let root = temp_dir(tag);
let pki = root.join("pki");
let cfg = root.join("server.toml");
let mut opts = ServerInitOpts::new("vpn.example.com", &pki);
opts.out_config = cfg.clone();
// Force the no_nat path by default — the integration test runner may or may not have a
// detectable default route, so the per-test `egress_iface` / `no_nat` overrides are explicit.
opts.no_nat = true;
(opts, root)
}
/// Happy path: CA, server cert and server.toml all written and the TOML parses back.
#[test]
fn server_init_writes_and_parses() {
let (opts, root) = base_opts("happy");
let report = init::server_init(&opts).expect("server-init succeeds");
assert!(report.ca_cert.exists(), "ca.crt exists");
assert!(report.ca_key.exists(), "ca.key exists");
assert!(report.server_cert.exists(), "server.crt exists");
assert!(report.server_key.exists(), "server.key exists");
assert!(report.server_config.exists(), "server.toml exists");
let cfg = ServerConfigFile::load(&report.server_config).expect("server.toml parses");
// v3.4: server-init defaults moved off 443/444 to 8443/8444 to dodge sing-box / Hysteria2
// collisions; the listen-address derives from udp_port.
assert_eq!(cfg.server.listen, "0.0.0.0:8443");
assert_eq!(cfg.tunnel.pool_cidr, "10.7.0.0/24");
assert_eq!(cfg.transport.udp_port, 8443);
assert_eq!(cfg.transport.quic_port, 8444);
// no-nat was set in the baseline.
assert!(cfg.server.nat.is_none(), "no [server.nat] section");
// knock / cover default to disabled.
assert!(!cfg.transport.knock.enabled);
assert!(!cfg.transport.cover.enabled);
// PKI section points at the generated files.
assert_eq!(cfg.pki.ca_cert, report.ca_cert.to_string_lossy());
// Cleanup is best-effort.
let _ = std::fs::remove_dir_all(&root);
}
/// `--enable-knock` and `--enable-cover-traffic` flip the [transport.*] sections on.
#[test]
fn server_init_enables_anti_surveillance() {
let (mut opts, root) = base_opts("knock");
opts.enable_knock = true;
opts.enable_cover_traffic = true;
let report = init::server_init(&opts).expect("server-init succeeds");
let cfg = ServerConfigFile::load(&report.server_config).expect("parse");
assert!(cfg.transport.knock.enabled, "knock enabled");
assert_eq!(cfg.transport.knock.knock_secret_source, "ca_fingerprint");
assert!(cfg.transport.cover.enabled, "cover enabled");
assert_eq!(cfg.transport.cover.mean_interval_ms, 500);
let _ = std::fs::remove_dir_all(&root);
}
/// `egress_iface = "eth0"` + `no_nat = false` writes a `[server.nat]` section.
#[test]
fn server_init_writes_nat_when_egress_explicit() {
let (mut opts, root) = base_opts("nat");
opts.no_nat = false;
opts.egress_iface = Some("eth0".to_string());
let report = init::server_init(&opts).expect("server-init succeeds");
let cfg = ServerConfigFile::load(&report.server_config).expect("parse");
let nat = cfg.server.nat.expect("[server.nat] present");
assert!(nat.auto, "nat.auto = true");
assert_eq!(nat.egress_iface, "eth0");
let _ = std::fs::remove_dir_all(&root);
}
/// `run_as = "nobody"` ends up in `[server] run_as` and `no_logs` toggles parse cleanly.
#[test]
fn server_init_run_as_and_no_logs_present() {
let (mut opts, root) = base_opts("runas");
opts.run_as = Some("nobody".to_string());
let report = init::server_init(&opts).expect("server-init succeeds");
let cfg = ServerConfigFile::load(&report.server_config).expect("parse");
assert_eq!(cfg.server.run_as.as_deref(), Some("nobody"));
// `no_logs` is emitted with the default `false`.
assert!(!cfg.server.no_logs);
let _ = std::fs::remove_dir_all(&root);
}
/// Without `--force`, re-running over an existing CA errors out cleanly.
#[test]
fn server_init_refuses_to_clobber_without_force() {
let (opts, root) = base_opts("clobber");
init::server_init(&opts).expect("first run succeeds");
// Re-run should fail because the CA already exists.
let err = init::server_init(&opts).unwrap_err().to_string();
assert!(
err.contains("CA already exists") || err.contains("already exists"),
"expected overwrite refusal, got: {err}"
);
// With force the second run succeeds.
let mut forced = opts.clone();
forced.force = true;
let report = init::server_init(&forced).expect("--force overwrites");
assert!(report.ca_cert.exists());
let _ = std::fs::remove_dir_all(&root);
}