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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! 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.
|
||||
assert_eq!(targets[0].udp.unwrap().to_string(), "203.0.113.10:443");
|
||||
|
||||
// Each bridge entry must keep the per-transport ports (the bridge `:9999` in the second
|
||||
// string is ignored — transports always use [transport] ports).
|
||||
for t in &targets[1..] {
|
||||
assert_eq!(t.udp.unwrap().port(), 443);
|
||||
assert_eq!(t.quic.unwrap().port(), 444);
|
||||
}
|
||||
|
||||
// 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user