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>
137 lines
5.5 KiB
Rust
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);
|
|
}
|