8f0cf1f017
Reduces manual setup steps and trims user-identifying data exposed by the server/client, in the spirit of the deployment story: an operator on the wire sees less, and the admin types fewer commands. New CLI subcommands: - `aura server-init`: one shot — pki init + issue-server + writes a ready server.toml with auto-detected egress iface; flags --enable-knock, --enable-cover-traffic, --no-nat, --run-as toggle the new transport defenses and privilege drop. - `aura provision-client`: issues a client cert and assembles the full bundle (ca.crt + client.crt + client.key + client.toml in one directory) ready to hand over to the client device. --id is optional (defaults to a fresh UUIDv4, so client identities don't have to encode anything real). Identity / log minimization: - `aura pki issue-client --id` is now optional — UUIDv4 by default. - `[server]/[client] no_logs = true` filters peer_id, client_ip, source_addr, client_id, local_ip, user, id, assigned_ip, peer field values through a custom tracing FormatFields layer (events still fire but the identifying fields are redacted before being written). - `[client] bridges = [...]`: secondary server addresses; build_dial_targets shuffles them after the primary, so blocking one IP doesn't kill the client. - Auto-detect egress iface in [server.nat] (via detect_default_egress_iface); egress_iface in config becomes optional with graceful fallback. Config examples updated; backward-compatible (all new sections optional with serde defaults). Workspace: 207 tests passed (+22), clippy -D warnings clean, fmt clean. No new workspace deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
135 lines
5.3 KiB
Rust
135 lines
5.3 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");
|
|
assert_eq!(cfg.server.listen, "0.0.0.0:443");
|
|
assert_eq!(cfg.tunnel.pool_cidr, "10.7.0.0/24");
|
|
assert_eq!(cfg.transport.udp_port, 443);
|
|
assert_eq!(cfg.transport.quic_port, 444);
|
|
// 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);
|
|
}
|