Files
AuraVPN/crates/aura-cli/tests/cli_provision_client.rs
T
xah30 8f0cf1f017 feat(cli): automation bundle + identity-minimization features
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>
2026-05-27 12:14:57 +03:00

202 lines
7.0 KiB
Rust

//! Integration tests for [`aura_cli::init::provision_client`].
//!
//! These tests first generate a CA + server cert via `pki::init` / `pki::issue_server`, then
//! drive `provision_client` against that CA and verify:
//!
//! * the bundle directory ends up with `ca.crt`, `client.crt`, `client.key`, `client.toml`;
//! * the rendered `client.toml` parses;
//! * the issued client cert verifies against the original CA via [`AuraCertVerifier`];
//! * `--id` defaults to a UUID v4 and is reflected as the cert CN.
use std::path::PathBuf;
use aura_cli::config::ClientConfigFile;
use aura_cli::init::{self, ProvisionClientOpts};
use aura_cli::pki;
use aura_pki::AuraCertVerifier;
use rustls_pki_types::CertificateDer;
/// Per-test temp dir.
fn temp_dir(tag: &str) -> PathBuf {
let mut dir = std::env::temp_dir();
dir.push(format!(
"aura-cli-provision-{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
}
/// Generate a CA at `ca_dir` for the rest of the test to use.
fn bootstrap_ca(ca_dir: &std::path::Path) {
pki::init("Aura Provision Test CA", ca_dir).expect("ca init");
}
/// Decode a single-cert PEM into a DER chain for the verifier.
fn pem_chain(pem_path: &std::path::Path) -> Vec<CertificateDer<'static>> {
let pem = std::fs::read(pem_path).expect("read cert");
let (_, parsed) = x509_parser::pem::parse_x509_pem(&pem).expect("parse PEM");
vec![CertificateDer::from(parsed.contents)]
}
/// Extract the certificate's CN via `x509-parser` so we can check that the assigned id ended up
/// in the cert.
fn cert_common_name(pem_path: &std::path::Path) -> String {
let pem = std::fs::read(pem_path).expect("read cert");
let (_, parsed) = x509_parser::pem::parse_x509_pem(&pem).expect("parse PEM");
let (_, cert) = x509_parser::parse_x509_certificate(&parsed.contents).expect("parse cert");
let subject = cert.subject();
for cn in subject.iter_common_name() {
if let Ok(s) = cn.as_str() {
return s.to_string();
}
}
panic!("no CN in subject {subject:?}");
}
/// Happy path: explicit id, bundle materialises and parses, cert verifies against CA.
#[test]
fn provision_client_with_explicit_id() {
let root = temp_dir("happy");
let ca_dir = root.join("ca");
bootstrap_ca(&ca_dir);
let bundle = root.join("client-bundle");
let mut opts = ProvisionClientOpts::new(
&ca_dir,
"203.0.113.10",
"vpn.example.com",
"10.7.0.2",
&bundle,
);
opts.id = Some("phone-1".to_string());
let report = init::provision_client(&opts).expect("provision");
assert_eq!(report.id, "phone-1", "explicit id preserved");
assert!(report.ca_cert.exists());
assert!(report.client_cert.exists());
assert!(report.client_key.exists());
assert!(report.client_config.exists());
// The bundled cert's CN matches the id we passed.
assert_eq!(cert_common_name(&report.client_cert), "phone-1");
// 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");
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");
// The verifier accepts the bundled chain against the same CA we issued from.
let ca_pem = std::fs::read_to_string(ca_dir.join(pki::CA_CERT)).expect("read ca");
let verifier = AuraCertVerifier::new(&ca_pem).expect("verifier");
let chain = pem_chain(&report.client_cert);
let cn = verifier
.verify_client_cert(&chain)
.expect("issued client cert chains to the CA");
assert_eq!(cn, "phone-1");
let _ = std::fs::remove_dir_all(&root);
}
/// Default `--id` path: a fresh UUID v4 is assigned and ends up as the CN.
#[test]
fn provision_client_default_id_is_uuid_v4() {
let root = temp_dir("uuid");
let ca_dir = root.join("ca");
bootstrap_ca(&ca_dir);
let bundle = root.join("bundle");
let opts = ProvisionClientOpts::new(
&ca_dir,
"203.0.113.10",
"vpn.example.com",
"10.7.0.5",
&bundle,
);
let report = init::provision_client(&opts).expect("provision");
// The id is a valid UUID v4 and equals the cert CN.
let parsed = uuid::Uuid::parse_str(&report.id).expect("id is uuid");
assert_eq!(parsed.get_version_num(), 4, "uuid v4");
assert_eq!(cert_common_name(&report.client_cert), report.id);
let _ = std::fs::remove_dir_all(&root);
}
/// `bridges = [...]` ends up in the rendered client.toml and parses back through the config.
#[test]
fn provision_client_writes_bridges() {
let root = temp_dir("bridges");
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.3",
&bundle,
);
opts.bridges = vec!["203.0.113.11".to_string(), "203.0.113.12".to_string()];
let report = init::provision_client(&opts).expect("provision");
let cfg = ClientConfigFile::load(&report.client_config).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".to_string()));
let _ = std::fs::remove_dir_all(&root);
}
/// `enable_knock` / `enable_cover_traffic` flip the rendered TOML's `[transport.knock]` /
/// `[transport.cover]` sections.
#[test]
fn provision_client_anti_surveillance_toggles() {
let root = temp_dir("knock");
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.4",
&bundle,
);
opts.enable_knock = true;
opts.enable_cover_traffic = true;
let report = init::provision_client(&opts).expect("provision");
let cfg = ClientConfigFile::load(&report.client_config).expect("parse");
assert!(cfg.transport.knock.enabled);
assert!(cfg.transport.cover.enabled);
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() {
let root = temp_dir("nonempty");
let ca_dir = root.join("ca");
bootstrap_ca(&ca_dir);
let bundle = root.join("bundle");
std::fs::create_dir_all(&bundle).unwrap();
std::fs::write(bundle.join("junk.txt"), b"hi").unwrap();
let opts = ProvisionClientOpts::new(
&ca_dir,
"203.0.113.10",
"vpn.example.com",
"10.7.0.6",
&bundle,
);
let err = init::provision_client(&opts).unwrap_err().to_string();
assert!(err.contains("not empty"), "got: {err}");
let _ = std::fs::remove_dir_all(&root);
}