//! Private fixture operations compiled only for the library test harness. //! //! This module is deliberately absent from the production library and binary. use std::io::{BufRead, Write}; use std::net::Ipv4Addr; use std::path::{Component, Path, PathBuf}; use std::process::{Command, ExitStatus, Stdio}; use crate::app::{ Application, FirstIssueAttempt, FirstIssueDeliveryOutcome, RestartChoice, RestartStage, }; use crate::config::SettingsFile; use crate::persistence::SavedStore; use crate::service::{RestartExecution, RestartFailure, RestartOutcome}; const FIXTURE_ROOT: &str = "/tmp/wireguard-admin-fixture"; #[derive(Clone, Debug, Eq, PartialEq)] enum FixtureCommand { CreateWireGuard { interface: String, }, LinkUp { interface: String, }, AddIpv4Address { interface: String, address: String, }, AddIpv4Route { interface: String, route: String, }, SetWireGuard { interface: String, config_path: String, }, } impl FixtureCommand { fn program_and_args(&self) -> Result<(&'static str, Vec), FixtureCommandError> { self.program_and_args_with_root(Path::new(FIXTURE_ROOT)) } fn program_and_args_with_root( &self, fixture_root: &Path, ) -> Result<(&'static str, Vec), FixtureCommandError> { match self { Self::CreateWireGuard { interface } => { validate_interface(interface)?; Ok(( "ip", vec![ "link".into(), "add".into(), "dev".into(), interface.clone(), "type".into(), "wireguard".into(), ], )) } Self::LinkUp { interface } => { validate_interface(interface)?; Ok(( "ip", vec![ "link".into(), "set".into(), "dev".into(), interface.clone(), "up".into(), ], )) } Self::AddIpv4Address { interface, address } => { validate_interface(interface)?; let address = address .parse::() .map_err(|_| FixtureCommandError)?; Ok(( "ip", vec![ "address".into(), "add".into(), address.to_string(), "dev".into(), interface.clone(), ], )) } Self::AddIpv4Route { interface, route } => { validate_interface(interface)?; let route = route .parse::() .map_err(|_| FixtureCommandError)?; Ok(( "ip", vec![ "route".into(), "add".into(), route.to_string(), "dev".into(), interface.clone(), ], )) } Self::SetWireGuard { interface, config_path, } => { validate_interface(interface)?; let config_path = resolve_fixture_path(config_path, fixture_root)?; Ok(( "wg", vec![ "setconf".into(), interface.clone(), config_path.to_string_lossy().into_owned(), ], )) } } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct FixtureCommandError; impl std::fmt::Display for FixtureCommandError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("fixture command failed") } } impl std::error::Error for FixtureCommandError {} trait CommandRunner { fn run(&mut self, program: &str, args: &[String]) -> Result; } #[allow(dead_code)] // Executed by M8.5 fixture scenarios, never readiness. struct BusyBoxRunner; impl CommandRunner for BusyBoxRunner { fn run(&mut self, program: &str, args: &[String]) -> Result { Command::new("/bin/busybox") .args(["timeout", "-s", "KILL", "15", program]) .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|_| FixtureCommandError) } } fn execute_with( command: &FixtureCommand, runner: &mut impl CommandRunner, ) -> Result<(), FixtureCommandError> { execute_with_root(command, Path::new(FIXTURE_ROOT), runner) } fn execute_with_root( command: &FixtureCommand, fixture_root: &Path, runner: &mut impl CommandRunner, ) -> Result<(), FixtureCommandError> { let (program, args) = command.program_and_args_with_root(fixture_root)?; let status = runner.run(program, &args)?; if status.success() { Ok(()) } else { Err(FixtureCommandError) } } #[allow(dead_code)] // Executed by M8.5 fixture scenarios, never readiness. fn execute_fixture_command(command: &FixtureCommand) -> Result<(), FixtureCommandError> { execute_with(command, &mut BusyBoxRunner) } fn execute_fixture_command_in( command: &FixtureCommand, fixture_root: &Path, ) -> Result<(), FixtureCommandError> { execute_with_root(command, fixture_root, &mut BusyBoxRunner) } fn validate_interface(interface: &str) -> Result<(), FixtureCommandError> { if !interface.is_empty() && interface.len() <= 15 && interface .bytes() .all(|byte| byte.is_ascii_alphanumeric() || b"_.-".contains(&byte)) { Ok(()) } else { Err(FixtureCommandError) } } fn resolve_fixture_path(path: &str, root: &Path) -> Result { let path = Path::new(path); if !path.is_absolute() || path == root || !path.starts_with(root) || path .components() .any(|component| !matches!(component, Component::RootDir | Component::Normal(_))) { return Err(FixtureCommandError); } // Resolve every existing component without following links. Fixture roots // are private to this test process; this also rejects an in-root link whose // target could be replaced after a lexical check. let relative = path.strip_prefix(root).map_err(|_| FixtureCommandError)?; let mut current = root.to_path_buf(); let root_meta = std::fs::symlink_metadata(¤t).map_err(|_| FixtureCommandError)?; if root_meta.file_type().is_symlink() || !root_meta.is_dir() { return Err(FixtureCommandError); } for component in relative.components() { let Component::Normal(name) = component else { return Err(FixtureCommandError); }; current.push(name); let metadata = std::fs::symlink_metadata(¤t).map_err(|_| FixtureCommandError)?; if metadata.file_type().is_symlink() { return Err(FixtureCommandError); } } let canonical_root = std::fs::canonicalize(root).map_err(|_| FixtureCommandError)?; let canonical_path = std::fs::canonicalize(path).map_err(|_| FixtureCommandError)?; let metadata = std::fs::metadata(&canonical_path).map_err(|_| FixtureCommandError)?; if !canonical_path.starts_with(canonical_root) || !metadata.is_file() { return Err(FixtureCommandError); } Ok(canonical_path) } const MAX_PHASE_REQUESTS: usize = 8; const MAX_PHASE_LINE_BYTES: usize = 512; static NEXT_FIXTURE_SETCONF_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); #[derive(Clone, Debug, Eq, PartialEq)] enum PhaseRequest { Baseline, Create { alias: String, }, Commit, Deliver, Delete { identity: crate::domain::PublicKey, address: String, }, Restore { file_name: String, address: String, }, Apply { config_path: String, released_address: Option, }, Observe, End, ImportReissue, FailurePreventsApplication, } impl PhaseRequest { fn parse(line: &str) -> Result { if line.len() > MAX_PHASE_LINE_BYTES || line.contains('\0') { return Err(PhaseProtocolError); } if line == "COMMIT" { return Ok(Self::Commit); } if line == "DELIVER" { return Ok(Self::Deliver); } if line == "END" { return Ok(Self::End); } if line == "IMPORT_REISSUE" { return Ok(Self::ImportReissue); } if line == "FAILURE_PREVENTS_APPLICATION" { return Ok(Self::FailurePreventsApplication); } if line == "OBSERVE" { return Ok(Self::Observe); } if line == "BASELINE" { return Ok(Self::Baseline); } if let Some(alias) = line.strip_prefix("CREATE\t") { if alias.is_empty() || alias.chars().any(char::is_control) { return Err(PhaseProtocolError); } return Ok(Self::Create { alias: alias.to_owned(), }); } if let Some(config_path) = line.strip_prefix("APPLY\t") { let (config_path, released_address) = config_path .split_once('\t') .map_or((config_path, None), |(path, address)| (path, Some(address))); if config_path.is_empty() || config_path.chars().any(char::is_control) || released_address .is_some_and(|value| value.is_empty() || value.chars().any(char::is_control)) { return Err(PhaseProtocolError); } if let Some(address) = released_address { host_route_address(address)?; } return Ok(Self::Apply { config_path: config_path.to_owned(), released_address: released_address.map(str::to_owned), }); } if let Some(deletion) = line.strip_prefix("DELETE\t") { let (identity, address) = deletion.split_once('\t').ok_or(PhaseProtocolError)?; if address.is_empty() || address.chars().any(char::is_control) { return Err(PhaseProtocolError); } let identity = identity .parse::() .map_err(|_| PhaseProtocolError)?; let address = address .parse::() .map_err(|_| PhaseProtocolError)?; let is_host_route = match address { ipnet::IpNet::V4(network) => network.prefix_len() == 32, ipnet::IpNet::V6(network) => network.prefix_len() == 128, }; if !is_host_route { return Err(PhaseProtocolError); } return Ok(Self::Delete { identity, address: address.addr().to_string(), }); } if let Some(restore) = line.strip_prefix("RESTORE\t") { let (file_name, address) = restore.split_once('\t').ok_or(PhaseProtocolError)?; if !safe_fixture_archive_name(file_name) { return Err(PhaseProtocolError); } let address = address .parse::() .map_err(|_| PhaseProtocolError)?; let is_host_route = match address { ipnet::IpNet::V4(network) => network.prefix_len() == 32, ipnet::IpNet::V6(network) => network.prefix_len() == 128, }; if !is_host_route { return Err(PhaseProtocolError); } return Ok(Self::Restore { file_name: file_name.to_owned(), address: address.addr().to_string(), }); } Err(PhaseProtocolError) } } fn safe_fixture_archive_name(file_name: &str) -> bool { !file_name.is_empty() && file_name.len() <= 128 && file_name .bytes() .all(|byte| byte.is_ascii_alphanumeric() || b"-_.".contains(&byte)) && file_name.ends_with(".tar") } #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct PhaseProtocolError; fn host_route_address(value: &str) -> Result { let network = value .parse::() .map_err(|_| PhaseProtocolError)?; let is_host_route = match network { ipnet::IpNet::V4(network) => network.prefix_len() == 32, ipnet::IpNet::V6(network) => network.prefix_len() == 128, }; if is_host_route { Ok(network.addr()) } else { Err(PhaseProtocolError) } } fn released_address_is_unreserved( released_address: Option<&str>, reserved_addresses: &std::collections::BTreeSet, ) -> Result { released_address .map(|address| { host_route_address(address).map(|address| !reserved_addresses.contains(&address)) }) .transpose() .map(|released| released.unwrap_or(true)) } impl std::fmt::Display for PhaseProtocolError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("fixture phase request rejected") } } impl std::error::Error for PhaseProtocolError {} #[derive(Default)] struct PhaseState { created: Option, committed: bool, delivered: bool, applied: bool, } /// In-process control protocol for M8.5. The session owns one `Application` /// until the bounded request stream ends, preserving first-issue authority. fn run_phase_protocol( app: &mut Application, fixture_root: &Path, mut input: impl BufRead, mut output: impl Write, ) -> Result<(), PhaseProtocolError> { let mut state = PhaseState::default(); for count in 0..MAX_PHASE_REQUESTS { let Some(line) = read_phase_line(&mut input)? else { return Err(PhaseProtocolError); }; if count >= MAX_PHASE_REQUESTS { return Err(PhaseProtocolError); } let request = PhaseRequest::parse(&line)?; let phase = match &request { PhaseRequest::Baseline => "baseline_apply", PhaseRequest::Create { .. } => "create", PhaseRequest::Commit => "commit", PhaseRequest::Deliver => "delivery", PhaseRequest::Delete { .. } => "delete", PhaseRequest::Restore { .. } => "restore", PhaseRequest::Apply { .. } => "fixture_apply", PhaseRequest::Observe => "observe", PhaseRequest::End => "end", PhaseRequest::ImportReissue => "import_reissue", PhaseRequest::FailurePreventsApplication => "failure_prevents_application", }; writeln!(output, "phase={phase} status=started").map_err(|_| PhaseProtocolError)?; match request { PhaseRequest::Baseline => { let mut config_copy = fixture_setconf_copy(&fixture_root.join("server.conf"), fixture_root)?; let command = FixtureCommand::SetWireGuard { interface: "wg-m83-probe".to_owned(), config_path: config_copy.path().to_string_lossy().into_owned(), }; execute_fixture_command_in(&command, fixture_root) .map_err(|_| PhaseProtocolError)?; config_copy.remove()?; let client_text = std::fs::read_to_string(fixture_root.join("baseline-client.conf")) .map_err(|_| PhaseProtocolError)?; let private_key = client_text .lines() .filter_map(|line| line.trim().split_once('=')) .find(|(name, _)| name.trim() == "PrivateKey") .map(|(_, value)| value.trim()) .ok_or(PhaseProtocolError)?; let identity = crate::vault::WireGuardPrivateKey::parse_base64(private_key) .map_err(|_| PhaseProtocolError)? .public_key(); writeln!( output, "phase=baseline_apply status=verified public_key={identity}" ) .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Create { alias } if state.created.is_none() => { let identity = app .prepare_new_client(&alias, None, None) .map_err(|_| PhaseProtocolError)?; state.created = Some(identity); writeln!(output, "phase=create status=ok public_key={identity}") .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Commit if state.created.is_some() && !state.committed => { let backup_path = app.commit().map_err(|_| PhaseProtocolError)?; let backup_name = backup_path .file_name() .and_then(|name| name.to_str()) .filter(|name| safe_fixture_archive_name(name)) .ok_or(PhaseProtocolError)?; state.committed = true; writeln!(output, "phase=commit status=ok backup_name={backup_name}") .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Deliver if state.committed && !state.delivered => { let identity = state.created.ok_or(PhaseProtocolError)?; let destination = fixture_root.join("first-delivery.conf"); let prepared = app .prepare_first_issue_destination(&destination) .map_err(|_| PhaseProtocolError)?; let outcome = app .deliver_first_issue_file(identity, FirstIssueAttempt::Initial, prepared) .map_err(|_| PhaseProtocolError)?; let activation_warning = match outcome { FirstIssueDeliveryOutcome::Delivered(result) => result.activation_warning, _ => return Err(PhaseProtocolError), }; state.delivered = true; writeln!( output, "phase=delivery status=ok activation_warning={activation_warning}" ) .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Delete { identity, address } => { if !app .delete_client(identity) .map_err(|_| PhaseProtocolError)? { return Err(PhaseProtocolError); } let backup_path = app.commit().map_err(|_| PhaseProtocolError)?; let backup_name = backup_path .file_name() .and_then(|name| name.to_str()) .filter(|name| safe_fixture_archive_name(name)) .ok_or(PhaseProtocolError)?; let saved_vault = app .saved_bytes() .vault .as_deref() .and_then(|bytes| std::str::from_utf8(bytes).ok()) .and_then(|text| crate::vault::VaultBundle::from_toml(text).ok()) .ok_or(PhaseProtocolError)?; if saved_vault.contains_record(identity) { return Err(PhaseProtocolError); } let archive = crate::backup::preview_archive(&backup_path).map_err(|_| PhaseProtocolError)?; let archived_vault = archive .saved_bytes .vault .as_deref() .and_then(|bytes| std::str::from_utf8(bytes).ok()) .and_then(|text| crate::vault::VaultBundle::from_toml(text).ok()) .ok_or(PhaseProtocolError)?; if !archived_vault.contains_record(identity) { return Err(PhaseProtocolError); } let endpoint = fixture_endpoint_from_saved_settings(fixture_root)?; *app = open_fixture_application(fixture_root, &endpoint)?; if app.states().saved().get(&identity).is_some() || !app .states() .reserved_addresses() .contains(&address.parse().map_err(|_| PhaseProtocolError)?) { return Err(PhaseProtocolError); } writeln!( output, "phase=delete status=saved public_key={identity} current_record=removed backup_record=retained address_reserved=true backup_name={backup_name}" ) .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Restore { file_name, address } => { let target_address = address .parse::() .map_err(|_| PhaseProtocolError)?; let runtime_before = observe_fixture_peer_set()?; restore_step(&mut output, "runtime_observed")?; let selected_archive = fixture_root.join("Backup/archives").join(&file_name); let archived = crate::backup::preview_archive(&selected_archive) .map_err(|_| PhaseProtocolError)?; if archived.reason != crate::backup::BackupReason::PreCommit { return Err(PhaseProtocolError); } restore_step(&mut output, "archive_validated")?; let archived_bytes = archived.saved_bytes; let archived_document = crate::wireguard::ConfigDocument::parse(&archived_bytes.server_config) .map_err(|_| PhaseProtocolError)?; let archived_peer = archived_document .peers() .iter() .find(|peer| { peer.allowed_ips.iter().any(|network| { network.addr() == target_address && match network { ipnet::IpNet::V4(route) => route.prefix_len() == 32, ipnet::IpNet::V6(route) => route.prefix_len() == 128, } }) }) .and_then(|peer| peer.identity); let target_identity = archived_peer .or_else(|| { app.states() .saved() .clients() .find(|client| client.addresses.iter().any(|ip| ip == target_address)) .map(|client| client.identity) }) .ok_or(PhaseProtocolError)?; let archive_has_target = archived_peer.is_some(); restore_step(&mut output, "target_identified")?; let source_client_before = std::fs::read(fixture_root.join("baseline-client.conf")) .map_err(|_| PhaseProtocolError)?; let before = app.saved_bytes(); let before_settings = before.settings.clone(); let before_server = before.server_config.clone(); let before_state = before.internal_state.clone(); let before_vault = before.vault.clone(); let prior_reservations = app.states().reserved_addresses().clone(); let removed_client_addresses = if archive_has_target { std::collections::BTreeSet::new() } else { app.states() .saved() .get(&target_identity) .map(|client| client.addresses.iter().collect()) .ok_or(PhaseProtocolError)? }; let preview = app .preview_restore(&file_name) .map_err(|_| PhaseProtocolError)?; restore_step(&mut output, "application_previewed")?; let changes = preview.change_summary(before); let expected_change = if archive_has_target { changes.added_clients.len() == 1 && changes.added_clients.contains(&target_identity) && changes.removed_clients.is_empty() } else { changes.removed_clients.len() == 1 && changes.removed_clients.contains(&target_identity) && changes.added_clients.is_empty() }; if !expected_change { return Err(PhaseProtocolError); } restore_step(&mut output, "change_summary_verified")?; let precautionary_path = app .confirm_restore(preview) .map_err(|_| PhaseProtocolError)?; restore_step(&mut output, "application_confirmed")?; verify_fixture_private_archive(&precautionary_path)?; let precautionary = crate::backup::preview_archive(&precautionary_path) .map_err(|_| PhaseProtocolError)?; if precautionary.reason != crate::backup::BackupReason::PreRestore { return Err(PhaseProtocolError); } restore_step(&mut output, "precautionary_backup_validated")?; if precautionary.saved_bytes.settings != before_settings || precautionary.saved_bytes.server_config != before_server || precautionary.saved_bytes.internal_state != before_state || precautionary.saved_bytes.vault != before_vault { return Err(PhaseProtocolError); } restore_step(&mut output, "precautionary_contents_verified")?; let restored = app.saved_bytes(); let restored_addresses = app .states() .saved() .clients() .flat_map(|client| client.addresses.iter()) .collect(); let expected_reservations = expected_restore_reservations( prior_reservations, removed_client_addresses, restored_addresses, ); let settings_match = restored.settings == archived_bytes.settings; let server_match = restored.server_config == archived_bytes.server_config; let vault_match = restored.vault == archived_bytes.vault; let target_presence_match = app.states().saved().get(&target_identity).is_some() == archive_has_target; let reservations_match = app.states().reserved_addresses() == &expected_reservations; let actual_reservations = app.states().reserved_addresses(); writeln!( output, "phase=restore step=saved_state_comparison settings_match={settings_match} server_match={server_match} vault_match={vault_match} target_presence_match={target_presence_match} reservations_match={reservations_match} actual_reservation_count={} expected_reservation_count={} actual_target_reserved={} expected_target_reserved={}", actual_reservations.len(), expected_reservations.len(), actual_reservations.contains(&target_address), expected_reservations.contains(&target_address) ) .map_err(|_| PhaseProtocolError)?; if !settings_match || !server_match || !vault_match || !target_presence_match || !reservations_match { return Err(PhaseProtocolError); } restore_step(&mut output, "saved_state_and_reservations_verified")?; let restored_state = restored .internal_state .as_deref() .map(crate::persistence::PersistedState::from_toml) .transpose() .map_err(|_| PhaseProtocolError)?; let expected_reserved_strings = expected_reservations .iter() .map(ToString::to_string) .collect::>(); if restored_state.as_ref().is_none_or(|state| { state.last_verified_at_utc.is_some() || state .reserved_addresses .iter() .cloned() .collect::>() != expected_reserved_strings }) { return Err(PhaseProtocolError); } restore_step(&mut output, "persisted_reservations_verified")?; if observe_fixture_peer_set()? != runtime_before { return Err(PhaseProtocolError); } restore_step(&mut output, "runtime_unchanged_verified")?; if std::fs::read(fixture_root.join("baseline-client.conf")) .map_err(|_| PhaseProtocolError)? != source_client_before { return Err(PhaseProtocolError); } restore_step(&mut output, "source_client_preserved")?; verify_fixture_private_saved_files(fixture_root)?; restore_step(&mut output, "saved_file_permissions_verified")?; writeln!( output, "phase=restore status=confirmed archive={file_name} reason=pre-commit precautionary_backup=pre-restore settings=true server=true vault=true target_client_present={archive_has_target} pending_address_reserved={} runtime_unchanged=true", app.states().reserved_addresses().contains(&target_address) ) .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Apply { config_path, released_address, } if !state.applied => { let config_path = resolve_fixture_path(&config_path, fixture_root) .map_err(|_| PhaseProtocolError)?; writeln!(output, "phase=fixture_apply step=path_validated") .map_err(|_| PhaseProtocolError)?; let mut config_copy = fixture_setconf_copy(&config_path, fixture_root)?; writeln!(output, "phase=fixture_apply step=config_prepared") .map_err(|_| PhaseProtocolError)?; let apply_config_path = config_copy.path().to_path_buf(); let preview = match app.preview_restart() { Ok(preview) => preview, Err(_) => { writeln!(output, "phase=fixture_apply step=preview_failed") .map_err(|_| PhaseProtocolError)?; return Err(PhaseProtocolError); } }; writeln!(output, "phase=fixture_apply step=preview_ready") .map_err(|_| PhaseProtocolError)?; let interface = preview.interface().to_owned(); let report = app.restart_with_fixture_apply( preview, RestartChoice::RestartSavedState, move |_, _| { let command = FixtureCommand::SetWireGuard { interface, config_path: apply_config_path.to_string_lossy().into_owned(), }; run_fixture_apply_with_cleanup( || execute_fixture_command_in(&command, fixture_root), || config_copy.remove(), ) }, ); writeln!( output, "phase=fixture_apply step=application_returned stage={:?} failure={:?} submitted={}", report.stage, report.failure, report.restart_submitted ) .map_err(|_| PhaseProtocolError)?; if report.stage != RestartStage::Complete || report.failure.is_some() { writeln!( output, "phase=fixture_apply status=failed stage={:?} failure={:?}", report.stage, report.failure ) .map_err(|_| PhaseProtocolError)?; return Err(PhaseProtocolError); } state.applied = true; let address_released = released_address_is_unreserved( released_address.as_deref(), app.states().reserved_addresses(), )?; if !address_released { return Err(PhaseProtocolError); } writeln!( output, "phase=fixture_apply status=verified address_released={address_released}" ) .map_err(|_| PhaseProtocolError)?; } PhaseRequest::Observe => { let observation = crate::runtime::transport::observe_interface( "wg-m83-probe", std::time::Duration::from_secs(2), crate::runtime::netlink::DecodeLimits::default(), ) .map_err(|_| PhaseProtocolError)?; let crate::runtime::InterfaceObservation::Present { name, peers } = observation.interface else { return Err(PhaseProtocolError); }; if peers.completeness != crate::runtime::PeerSetCompleteness::Complete || !peers.duplicate_public_keys.is_empty() { return Err(PhaseProtocolError); } writeln!( output, "phase=observe status=complete interface={name} peers={}", peers.peers.len() ) .map_err(|_| PhaseProtocolError)?; for (identity, peer) in peers.peers { let routes = match peer.allowed_ips { crate::runtime::Observed::Available(routes) => routes .iter() .map(ToString::to_string) .collect::>() .join(","), crate::runtime::Observed::Unavailable => "unavailable".to_owned(), }; let handshake = match peer.last_handshake { crate::runtime::Observed::Available(Some(_)) => "present", crate::runtime::Observed::Available(None) => "none", crate::runtime::Observed::Unavailable => "unavailable", }; let received = match peer.received_bytes { crate::runtime::Observed::Available(value) => value.to_string(), crate::runtime::Observed::Unavailable => "unavailable".to_owned(), }; let transmitted = match peer.transmitted_bytes { crate::runtime::Observed::Available(value) => value.to_string(), crate::runtime::Observed::Unavailable => "unavailable".to_owned(), }; writeln!(output, "peer={identity} routes={routes} handshake={handshake} received={received} transmitted={transmitted}") .map_err(|_| PhaseProtocolError)?; } } PhaseRequest::End => { writeln!(output, "phase=end status=ok").map_err(|_| PhaseProtocolError)?; return Ok(()); } PhaseRequest::ImportReissue => { run_import_reissue_scenario(fixture_root, &mut output)?; } PhaseRequest::FailurePreventsApplication => { run_failure_prevents_application_scenario(app, fixture_root, &mut output)?; } _ => return Err(PhaseProtocolError), } } Err(PhaseProtocolError) } fn restore_step(output: &mut impl Write, step: &str) -> Result<(), PhaseProtocolError> { writeln!(output, "phase=restore step={step}").map_err(|_| PhaseProtocolError) } /// Replace the fixture archive directory with a regular file. This produces a /// deterministic ENOTDIR failure even in the root Alpine container, unlike a /// permission-only fault injection. Drop restores the exact original directory. struct ArchivePathBlocker { archive_dir: PathBuf, held_dir: PathBuf, } impl ArchivePathBlocker { fn install(root: &Path) -> Result { use std::os::unix::fs::OpenOptionsExt; let archive_dir = root.join("Backup/archives"); let held_dir = root.join("Backup/archives-failure-fixture-held"); let archive_metadata = std::fs::symlink_metadata(&archive_dir).map_err(|_| PhaseProtocolError)?; if held_dir.exists() || !archive_metadata.file_type().is_dir() || archive_metadata.file_type().is_symlink() { return Err(PhaseProtocolError); } std::fs::rename(&archive_dir, &held_dir).map_err(|_| PhaseProtocolError)?; let blocker = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&archive_dir); if blocker.is_err() { let _ = std::fs::rename(&held_dir, &archive_dir); return Err(PhaseProtocolError); } Ok(Self { archive_dir, held_dir, }) } } impl Drop for ArchivePathBlocker { fn drop(&mut self) { let _ = std::fs::remove_file(&self.archive_dir); let _ = std::fs::rename(&self.held_dir, &self.archive_dir); } } fn archive_path_restored(root: &Path) -> bool { let archive_dir = root.join("Backup/archives"); let held_dir = root.join("Backup/archives-failure-fixture-held"); std::fs::symlink_metadata(&archive_dir) .is_ok_and(|metadata| metadata.file_type().is_dir() && !metadata.file_type().is_symlink()) && !held_dir.exists() } fn is_commit_backup_failure(result: &Result) -> bool { matches!(result, Err(crate::app::AppError::Backup(_))) } fn run_failure_prevents_application_scenario( app: &mut Application, fixture_root: &Path, output: &mut impl Write, ) -> Result<(), PhaseProtocolError> { use crate::service::{RestartExecution, RestartFailure, RestartOutcome}; let limits = crate::runtime::netlink::DecodeLimits::default(); let missing_interface = crate::runtime::transport::observe_interface( "wga-m85-absent", std::time::Duration::from_secs(2), limits, ); if !matches!( missing_interface, Err(crate::runtime::transport::TransportError::NoDevice) ) { return Err(PhaseProtocolError); } let deadline = crate::runtime::transport::observe_interface( "wg-m83-probe", std::time::Duration::from_nanos(1), limits, ); if !matches!( deadline, Err(crate::runtime::transport::TransportError::Timeout) ) { return Err(PhaseProtocolError); } writeln!( output, "phase=reader_failures status=verified missing_interface=rejected deadline=timeout empty_or_partial=false" ) .map_err(|_| PhaseProtocolError)?; let runtime_before_commit = observe_fixture_peer_set()?; let saved_before_commit = saved_bytes_snapshot(app.saved_bytes()); let files_before_commit = protected_saved_file_snapshot(fixture_root)?; app.prepare_new_client("M8 backup failure", None, None) .map_err(|_| PhaseProtocolError)?; let commit_result = { let _blocker = ArchivePathBlocker::install(fixture_root)?; app.commit() }; let commit_failed_at_backup = is_commit_backup_failure(&commit_result); let commit_archive_restored = archive_path_restored(fixture_root); let saved_unchanged = saved_bytes_snapshot(app.saved_bytes()) == saved_before_commit && protected_saved_file_snapshot(fixture_root)? == files_before_commit; let runtime_unchanged = observe_fixture_peer_set()? == runtime_before_commit; if !commit_failed_at_backup || !commit_archive_restored || !saved_unchanged || !runtime_unchanged || !app.has_draft_changes() { return Err(PhaseProtocolError); } writeln!(output, "phase=commit_backup_failure status=verified saved_files_unchanged=true draft_preserved=true runtime_unchanged=true archive_path_restored=true") .map_err(|_| PhaseProtocolError)?; app.discard_draft().map_err(|_| PhaseProtocolError)?; failure_step(output, "draft_discarded")?; let baseline_client = std::fs::read(fixture_root.join("baseline-client.conf")).map_err(|_| PhaseProtocolError)?; let baseline_identity = crate::vault::import::parse_client_config(&baseline_client) .map_err(|_| PhaseProtocolError)? .identity; failure_step(output, "baseline_identity_parsed")?; let vault_record_removed = app .delete_client(baseline_identity) .map_err(|_| PhaseProtocolError)?; if app.states().draft().get(&baseline_identity).is_some() { return Err(PhaseProtocolError); } writeln!( output, "phase=failure_prevents_application step=baseline_client_deleted vault_record_removed={vault_record_removed}" ) .map_err(|_| PhaseProtocolError)?; app.commit().map_err(|_| PhaseProtocolError)?; failure_step(output, "baseline_deletion_committed")?; let released_addresses = ["10.20.0.10" .parse::() .map_err(|_| PhaseProtocolError)?]; let address_was_reserved = released_addresses .iter() .all(|address| app.states().reserved_addresses().contains(address)); if !address_was_reserved { return Err(PhaseProtocolError); } failure_step(output, "deleted_address_reserved")?; let restart_preview = app.preview_restart().map_err(|_| PhaseProtocolError)?; failure_step(output, "restart_preview_created")?; let saved_before_restart = saved_bytes_snapshot(app.saved_bytes()); let files_before_restart = protected_saved_file_snapshot(fixture_root)?; let reservations_before_restart = app.states().reserved_addresses().clone(); let history_before_restart = saved_last_verified_timestamp(app.saved_bytes())?; let applied_before_restart = app.states().applied().clone(); let applied_snapshot_before_restart = app.states().applied_snapshot().cloned(); let runtime_before_restart = observe_fixture_peer_set()?; let apply_called = std::cell::Cell::new(false); let report = { let _blocker = ArchivePathBlocker::install(fixture_root)?; failure_step(output, "archive_blocker_installed")?; app.restart_with_fixture_apply( restart_preview, crate::app::RestartChoice::RestartSavedState, |_, _| { apply_called.set(true); RestartExecution { outcome: RestartOutcome::Failed(RestartFailure::RequestFailed), submitted: true, phase: crate::service::RestartPhase::WaitForJob, } }, ) }; failure_step(output, "restart_returned")?; let restart_archive_restored = archive_path_restored(fixture_root); let report_is_backup_failure = report.stage == crate::app::RestartStage::Backup && report.failure == Some(crate::app::RestartFailureKind::BackupFailed); let no_restart_submitted = !report.restart_submitted && !apply_called.get(); let saved_unchanged = saved_bytes_snapshot(app.saved_bytes()) == saved_before_restart && protected_saved_file_snapshot(fixture_root)? == files_before_restart; let reservations_unchanged = app.states().reserved_addresses() == &reservations_before_restart && released_addresses .iter() .all(|address| app.states().reserved_addresses().contains(address)); let history_unchanged = saved_last_verified_timestamp(app.saved_bytes())? == history_before_restart && history_before_restart.is_none(); let applied_unchanged = app.states().applied() == &applied_before_restart && app.states().applied_snapshot() == applied_snapshot_before_restart.as_ref(); let runtime_after_restart = observe_fixture_peer_set()?; let runtime_unchanged = runtime_after_restart == runtime_before_restart; if !report_is_backup_failure || !restart_archive_restored || !no_restart_submitted || !saved_unchanged || !reservations_unchanged || !history_unchanged || !applied_unchanged || !runtime_unchanged { writeln!(output, "phase=restart_backup_failure status=diagnostic report_backup_failure={report_is_backup_failure} archive_restored={restart_archive_restored} no_restart_submitted={no_restart_submitted} saved_unchanged={saved_unchanged} reservations_unchanged={reservations_unchanged} history_unchanged={history_unchanged} applied_unchanged={applied_unchanged} runtime_unchanged={runtime_unchanged}") .map_err(|_| PhaseProtocolError)?; return Err(PhaseProtocolError); } writeln!( output, "phase=restart_backup_failure status=verified apply_called=false restart_submitted=false address_reserved=true historical_state=unchanged runtime_unchanged=true archive_path_restored=true" ) .map_err(|_| PhaseProtocolError)?; writeln!(output, "phase=failure_prevents_application status=complete") .map_err(|_| PhaseProtocolError)?; Ok(()) } fn failure_step(output: &mut impl Write, step: &str) -> Result<(), PhaseProtocolError> { writeln!(output, "phase=failure_prevents_application step={step}") .map_err(|_| PhaseProtocolError) } type SavedBytesSnapshot = (Vec, Vec, Option>, Option>); type ProtectedFileSnapshot = Vec<(PathBuf, Option>)>; fn saved_last_verified_timestamp( saved: &crate::backup::SavedBytes, ) -> Result, PhaseProtocolError> { saved .internal_state .as_deref() .map(crate::persistence::PersistedState::from_toml) .transpose() .map(|state| state.and_then(|state| state.last_verified_at_utc)) .map_err(|_| PhaseProtocolError) } fn saved_bytes_snapshot(saved: &crate::backup::SavedBytes) -> SavedBytesSnapshot { ( saved.settings.clone(), saved.server_config.clone(), saved.internal_state.clone(), saved.vault.clone(), ) } fn protected_saved_file_snapshot(root: &Path) -> Result { [ root.join(crate::config::SETTINGS_FILE_NAME), root.join("server.conf"), root.join("Backup/data/internal-state.toml"), root.join("Backup/data/vault.toml"), ] .into_iter() .map(|path| { let bytes = match std::fs::read(&path) { Ok(bytes) => Some(bytes), Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, Err(_) => return Err(PhaseProtocolError), }; Ok((path, bytes)) }) .collect() } fn run_import_reissue_scenario( fixture_root: &Path, output: &mut impl Write, ) -> Result<(), PhaseProtocolError> { const VAULT_PASSWORD: &str = "m8 synthetic fixture password"; let source_valid = fixture_root.join("baseline-client.conf"); let valid_bytes = std::fs::read(&source_valid).map_err(|_| PhaseProtocolError)?; let main_server = std::fs::read(fixture_root.join("server.conf")).map_err(|_| PhaseProtocolError)?; let import_root = fixture_root.join("import-reissue"); initialize_fixture_files(&import_root, "192.0.2.10:51820")?; std::fs::write(import_root.join("server.conf"), &main_server) .map_err(|_| PhaseProtocolError)?; let server_text = std::str::from_utf8(&main_server).map_err(|_| PhaseProtocolError)?; let server_public = server_text .lines() .filter_map(|line| line.trim().split_once('=')) .find(|(name, _)| name.trim() == "PrivateKey") .and_then(|(_, value)| crate::vault::WireGuardPrivateKey::parse_base64(value.trim()).ok()) .map(|key| key.public_key()) .ok_or(PhaseProtocolError)?; let conflict_private = crate::vault::WireGuardPrivateKey::generate().map_err(|_| PhaseProtocolError)?; let conflict_identity = conflict_private.public_key(); let mut server_file = std::fs::OpenOptions::new() .append(true) .open(import_root.join("server.conf")) .map_err(|_| PhaseProtocolError)?; use std::io::Write as _; writeln!( server_file, "\n[Peer]\nPublicKey = {conflict_identity}\nAllowedIPs = 10.20.0.12/32" ) .map_err(|_| PhaseProtocolError)?; drop(server_file); let source_dir = import_root.join("clients"); write_private_fixture_file(&source_dir.join("matched.conf"), &valid_bytes)?; let conflict_text = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.13/32\n\n[Peer]\nPublicKey = {server_public}\nAllowedIPs = 0.0.0.0/0\nEndpoint = 192.0.2.10:51820\n", conflict_private.to_base64() ); write_private_fixture_file(&source_dir.join("conflict.conf"), conflict_text.as_bytes())?; write_private_fixture_file( &source_dir.join("invalid.conf"), b"not a WireGuard config\n", )?; let original_valid = std::fs::read(source_dir.join("matched.conf")).map_err(|_| PhaseProtocolError)?; let original_conflict = std::fs::read(source_dir.join("conflict.conf")).map_err(|_| PhaseProtocolError)?; let original_invalid = std::fs::read(source_dir.join("invalid.conf")).map_err(|_| PhaseProtocolError)?; verify_private_fixture_input(&source_dir)?; let mut app = open_fixture_application(&import_root, "192.0.2.10:51820")?; let report = app .prepare_initial_setup_from_sources(VAULT_PASSWORD) .map_err(|_| PhaseProtocolError)?; let summaries = report.reconciliation.candidate_summaries(); let matched = summaries.iter().any(|(name, status)| { *name == "matched.conf" && matches!( status, crate::vault::import::CandidateStatus::Matched { address_discrepancy: false, server_identity: crate::vault::import::ServerIdentityComparison::Matches, .. } ) }); let conflict = summaries.iter().any(|(name, status)| { *name == "conflict.conf" && matches!( status, crate::vault::import::CandidateStatus::Matched { address_discrepancy: true, server_identity: crate::vault::import::ServerIdentityComparison::Matches, .. } ) }); let invalid = summaries.iter().any(|(name, status)| { *name == "invalid.conf" && matches!(status, crate::vault::import::CandidateStatus::Invalid(_)) }); if !report.scan_complete || !matched || !conflict || !invalid { return Err(PhaseProtocolError); } let backup = app.commit().map_err(|_| PhaseProtocolError)?; verify_fixture_private_archive(&backup)?; // A fresh application proves reissue is authorized only from the encrypted // saved vault. The retained source file is never used as a fallback. app = open_fixture_application(&import_root, "192.0.2.10:51820")?; let wrong_destination = app .prepare_first_issue_destination(import_root.join("wrong-password.conf")) .map_err(|_| PhaseProtocolError)?; let wrong_password_rejected = matches!( app.reissue_client_file( crate::vault::import::parse_client_config(&valid_bytes) .map_err(|_| PhaseProtocolError)? .identity, "wrong synthetic password", wrong_destination, false, ), Err(crate::app::ReissueError { stage: crate::app::ReissueStage::Authorization, kind: crate::app::ReissueFailureKind::PasswordOrRecordRejected, }) ); let wrong_file_absent = !import_root.join("wrong-password.conf").exists(); let valid_identity = crate::vault::import::parse_client_config(&valid_bytes) .map_err(|_| PhaseProtocolError)? .identity; let conflict_identity = crate::vault::import::parse_client_config(&original_conflict) .map_err(|_| PhaseProtocolError)? .identity; let conflict_destination = app .prepare_first_issue_destination(import_root.join("conflict-reissue.conf")) .map_err(|_| PhaseProtocolError)?; let conflict_rejected = matches!( app.reissue_client_file( conflict_identity, VAULT_PASSWORD, conflict_destination, false, ), Err(crate::app::ReissueError { stage: crate::app::ReissueStage::Authorization, kind: crate::app::ReissueFailureKind::PasswordOrRecordRejected, }) ); let conflict_file_absent = !import_root.join("conflict-reissue.conf").exists(); let destination_path = import_root.join("reissued-client.conf"); let destination = app .prepare_first_issue_destination(&destination_path) .map_err(|_| PhaseProtocolError)?; app.reissue_client_file(valid_identity, VAULT_PASSWORD, destination, false) .map_err(|_| PhaseProtocolError)?; use std::os::unix::fs::MetadataExt; let output_metadata = std::fs::symlink_metadata(&destination_path).map_err(|_| PhaseProtocolError)?; if output_metadata.file_type().is_symlink() || !output_metadata.is_file() || output_metadata.mode() & 0o777 != 0o600 || !wrong_password_rejected || !wrong_file_absent || !conflict_rejected || !conflict_file_absent || !source_files_unchanged( &source_dir, &[ ("matched.conf", &original_valid), ("conflict.conf", &original_conflict), ("invalid.conf", &original_invalid), ], ) { return Err(PhaseProtocolError); } verify_fixture_private_saved_files(&import_root)?; writeln!( output, "phase=import_reissue status=verified matched=true conflict_visible=true invalid_visible=true originals_unchanged=true wrong_password_blocked=true conflict_reissue_blocked=true reissue_mode=0600" ) .map_err(|_| PhaseProtocolError)?; Ok(()) } fn source_files_unchanged(source_dir: &Path, originals: &[(&str, &[u8])]) -> bool { originals.iter().all(|(name, original)| { std::fs::read(source_dir.join(name)).is_ok_and(|current| current == *original) }) } fn write_private_fixture_file(path: &Path, bytes: &[u8]) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; let mut file = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(path) .map_err(|_| PhaseProtocolError)?; file.write_all(bytes).map_err(|_| PhaseProtocolError)?; file.sync_all().map_err(|_| PhaseProtocolError)?; std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) .map_err(|_| PhaseProtocolError) } fn verify_private_fixture_input(source_dir: &Path) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::MetadataExt; let directory = std::fs::symlink_metadata(source_dir).map_err(|_| PhaseProtocolError)?; if directory.file_type().is_symlink() || !directory.is_dir() || directory.mode() & 0o777 != 0o700 { return Err(PhaseProtocolError); } for name in ["matched.conf", "conflict.conf", "invalid.conf"] { let metadata = std::fs::symlink_metadata(source_dir.join(name)).map_err(|_| PhaseProtocolError)?; if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.mode() & 0o777 != 0o600 { return Err(PhaseProtocolError); } } Ok(()) } fn expected_restore_reservations( prior: std::collections::BTreeSet, removed_client_addresses: std::collections::BTreeSet, restored_client_addresses: std::collections::BTreeSet, ) -> std::collections::BTreeSet { let mut expected = prior; expected.extend(removed_client_addresses); expected.retain(|address| !restored_client_addresses.contains(address)); expected } fn read_phase_line(input: &mut impl BufRead) -> Result, PhaseProtocolError> { let mut bytes = Vec::with_capacity(MAX_PHASE_LINE_BYTES); loop { let available = input.fill_buf().map_err(|_| PhaseProtocolError)?; if available.is_empty() { return if bytes.is_empty() { Ok(None) } else { Err(PhaseProtocolError) }; } let newline = available.iter().position(|byte| *byte == b'\n'); let consumed = newline.map_or(available.len(), |index| index + 1); if bytes.len().saturating_add(consumed) > MAX_PHASE_LINE_BYTES + 1 { return Err(PhaseProtocolError); } bytes.extend_from_slice(&available[..consumed]); input.consume(consumed); if newline.is_some() { bytes.pop(); return String::from_utf8(bytes) .map(Some) .map_err(|_| PhaseProtocolError); } } } fn open_fixture_application( root: &Path, endpoint: &str, ) -> Result { initialize_fixture_files(root, endpoint)?; let root = std::fs::canonicalize(root).map_err(|_| PhaseProtocolError)?; let settings_path = root.join(crate::config::SETTINGS_FILE_NAME); let settings_text = std::fs::read_to_string(settings_path).map_err(|_| PhaseProtocolError)?; let resolved = SettingsFile::parse_toml(&settings_text) .and_then(SettingsFile::validate) .map(|settings| settings.resolve_paths(&root)) .map_err(|_| PhaseProtocolError)?; let store = SavedStore::new(&root, resolved).map_err(|_| PhaseProtocolError)?; Application::open_from_saved_store(store).map_err(|_| PhaseProtocolError) } fn verify_fixture_private_saved_files(root: &Path) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::MetadataExt; for path in [ root.join(crate::config::SETTINGS_FILE_NAME), root.join("server.conf"), root.join("Backup/data/internal-state.toml"), root.join("Backup/data/vault.toml"), ] { let metadata = std::fs::symlink_metadata(path).map_err(|_| PhaseProtocolError)?; if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.mode() & 0o077 != 0 { return Err(PhaseProtocolError); } } Ok(()) } fn verify_fixture_private_archive(path: &Path) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::MetadataExt; let metadata = std::fs::symlink_metadata(path).map_err(|_| PhaseProtocolError)?; if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.mode() & 0o077 != 0 { return Err(PhaseProtocolError); } Ok(()) } fn observe_fixture_peer_set() -> Result< std::collections::BTreeMap>, PhaseProtocolError, > { let observation = crate::runtime::transport::observe_interface( "wg-m83-probe", std::time::Duration::from_secs(2), crate::runtime::netlink::DecodeLimits::default(), ) .map_err(|_| PhaseProtocolError)?; let crate::runtime::InterfaceObservation::Present { peers, .. } = observation.interface else { return Err(PhaseProtocolError); }; if peers.completeness != crate::runtime::PeerSetCompleteness::Complete || !peers.duplicate_public_keys.is_empty() { return Err(PhaseProtocolError); } peers .peers .into_iter() .map(|(identity, peer)| match peer.allowed_ips { crate::runtime::Observed::Available(routes) => Ok((identity, routes)), crate::runtime::Observed::Unavailable => Err(PhaseProtocolError), }) .collect() } fn fixture_endpoint_from_saved_settings(root: &Path) -> Result { let settings = std::fs::read_to_string(root.join(crate::config::SETTINGS_FILE_NAME)) .map_err(|_| PhaseProtocolError)?; let endpoint = SettingsFile::parse_toml(&settings) .and_then(SettingsFile::validate) .map_err(|_| PhaseProtocolError)? .server_endpoint; let host = match endpoint.host { crate::config::EndpointHost::Hostname(host) => host, crate::config::EndpointHost::Ip(std::net::IpAddr::V4(address)) => address.to_string(), crate::config::EndpointHost::Ip(std::net::IpAddr::V6(address)) => format!("[{address}]"), }; Ok(format!("{host}:{}", endpoint.port)) } fn initialize_fixture_files(root: &Path, endpoint: &str) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; std::fs::create_dir_all(root).map_err(|_| PhaseProtocolError)?; std::fs::set_permissions(root, std::fs::Permissions::from_mode(0o700)) .map_err(|_| PhaseProtocolError)?; let source = root.join("clients"); let backup = root.join("Backup"); let data = backup.join("data"); let archives = backup.join("archives"); for directory in [&source, &backup, &data, &archives] { std::fs::create_dir_all(directory).map_err(|_| PhaseProtocolError)?; std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700)) .map_err(|_| PhaseProtocolError)?; } let settings = format!( r#"interface_name = "wg-m83-probe" server_config_path = "server.conf" client_source_directory = "clients" server_endpoint = "{endpoint}" ipv4_allocation_range = "10.20.0.0/24" ipv6_allocation_range = "fd20::/64" dns_addresses = ["10.20.0.1"] systemd_unit = "wg-quick@wg0.service" restart_timeout_seconds = 30 "# ); let settings_path = root.join(crate::config::SETTINGS_FILE_NAME); if !settings_path.exists() { let mut file = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&settings_path) .map_err(|_| PhaseProtocolError)?; use std::io::Write; file.write_all(settings.as_bytes()) .map_err(|_| PhaseProtocolError)?; } let server_path = root.join("server.conf"); if !server_path.exists() { let server_key = crate::vault::WireGuardPrivateKey::generate().map_err(|_| PhaseProtocolError)?; let server = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.1/24, fd20::1/64\nListenPort = 51820\n", server_key.to_base64() ); let mut file = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&server_path) .map_err(|_| PhaseProtocolError)?; use std::io::Write; file.write_all(server.as_bytes()) .map_err(|_| PhaseProtocolError)?; } Ok(()) } fn initialize_baseline_fixture(root: &Path, endpoint: &str) -> Result<(), PhaseProtocolError> { use std::os::unix::fs::OpenOptionsExt; let baseline_path = root.join("baseline-client.conf"); if baseline_path.exists() { return Ok(()); } let server_path = root.join("server.conf"); let server_text = std::fs::read_to_string(&server_path).map_err(|_| PhaseProtocolError)?; let server_private = server_text .lines() .filter_map(|line| line.trim().split_once('=')) .find(|(name, _)| name.trim() == "PrivateKey") .map(|(_, value)| value.trim()) .ok_or(PhaseProtocolError)?; let server_private = crate::vault::WireGuardPrivateKey::parse_base64(server_private) .map_err(|_| PhaseProtocolError)?; let client_private = crate::vault::WireGuardPrivateKey::generate().map_err(|_| PhaseProtocolError)?; let client_public = client_private.public_key(); let client_address = "10.20.0.10/32"; let mut server_file = std::fs::OpenOptions::new() .append(true) .open(&server_path) .map_err(|_| PhaseProtocolError)?; use std::io::Write; writeln!( server_file, "\n[Peer]\nPublicKey = {client_public}\nAllowedIPs = {client_address}" ) .map_err(|_| PhaseProtocolError)?; drop(server_file); let document = crate::wireguard::ConfigDocument::parse( &std::fs::read(&server_path).map_err(|_| PhaseProtocolError)?, ) .map_err(|_| PhaseProtocolError)?; let document = document .insert_legacy_metadata(&client_public, "M8 baseline", None) .map_err(|_| PhaseProtocolError)?; std::fs::write(&server_path, document.render()).map_err(|_| PhaseProtocolError)?; let client_config = format!( "[Interface]\nPrivateKey = {}\nAddress = {client_address}\n\n[Peer]\nPublicKey = {}\nAllowedIPs = 0.0.0.0/0\nEndpoint = {endpoint}\n", client_private.to_base64(), server_private.public_key() ); let mut client_file = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&baseline_path) .map_err(|_| PhaseProtocolError)?; client_file .write_all(client_config.as_bytes()) .map_err(|_| PhaseProtocolError)?; Ok(()) } fn remove_fixture_file(path: &Path) -> Result<(), PhaseProtocolError> { match std::fs::remove_file(path) { Ok(()) => Ok(()), Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(_) => Err(PhaseProtocolError), } } struct FixtureSetconfCopy { path: PathBuf, armed: bool, } impl FixtureSetconfCopy { fn path(&self) -> &Path { &self.path } fn remove(&mut self) -> Result<(), PhaseProtocolError> { remove_fixture_file(&self.path)?; self.armed = false; Ok(()) } } impl Drop for FixtureSetconfCopy { fn drop(&mut self) { if self.armed { let _ = remove_fixture_file(&self.path); } } } fn run_fixture_apply_with_cleanup( apply: impl FnOnce() -> Result<(), FixtureCommandError>, cleanup: impl FnOnce() -> Result<(), PhaseProtocolError>, ) -> RestartExecution { let applied = apply().is_ok(); let removed = cleanup().is_ok(); RestartExecution { outcome: if applied && removed { RestartOutcome::Succeeded } else { RestartOutcome::Failed(RestartFailure::RequestFailed) }, submitted: applied, phase: crate::service::RestartPhase::WaitForJob, } } fn fixture_setconf_copy( source: &Path, fixture_root: &Path, ) -> Result { let bytes = std::fs::read(source).map_err(|_| PhaseProtocolError)?; if bytes.len() > 4 * 1024 * 1024 { return Err(PhaseProtocolError); } let text = std::str::from_utf8(&bytes).map_err(|_| PhaseProtocolError)?; let mut wireguard_config = String::with_capacity(text.len()); for line in text.split_inclusive('\n') { let trimmed = line.trim_start_matches(|ch: char| ch.is_ascii_whitespace()); let is_address_directive = trimmed .split_once('=') .is_some_and(|(name, _)| name.trim().eq_ignore_ascii_case("Address")); if !is_address_directive { wireguard_config.push_str(line); } } let id = NEXT_FIXTURE_SETCONF_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let path = fixture_root.join(format!( "server.wg-setconf-{}-{id}.conf", std::process::id() )); write_fixture_setconf_copy(&path, wireguard_config.as_bytes(), |file, contents| { use std::io::Write; file.write_all(contents) }) } fn write_fixture_setconf_copy( path: &Path, contents: &[u8], write_contents: impl FnOnce(&mut std::fs::File, &[u8]) -> std::io::Result<()>, ) -> Result { use std::os::unix::fs::OpenOptionsExt; let mut file = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(path) .map_err(|_| PhaseProtocolError)?; let copy = FixtureSetconfCopy { path: path.to_path_buf(), armed: true, }; write_contents(&mut file, contents).map_err(|_| PhaseProtocolError)?; Ok(copy) } fn fixture_underlay_endpoint() -> Result { let output = Command::new("/bin/busybox") .args([ "timeout", "-s", "KILL", "15", "ip", "-4", "-o", "addr", "show", "dev", "eth0", ]) .stdin(Stdio::null()) .stderr(Stdio::null()) .output() .map_err(|_| PhaseProtocolError)?; if !output.status.success() || output.stdout.len() > 128 { return Err(PhaseProtocolError); } let line = std::str::from_utf8(&output.stdout).map_err(|_| PhaseProtocolError)?; let address = line .split_whitespace() .collect::>() .windows(2) .find(|pair| pair[0] == "inet") .and_then(|pair| pair[1].split('/').next()) .and_then(|address| address.parse::().ok()) .filter(|address| !address.is_unspecified() && !address.is_loopback()) .ok_or(PhaseProtocolError)?; Ok(format!("{address}:51820")) } /// Explicitly selected in-container Application scenario driver. All control /// messages and results are bounded and secret-free; fixture root is supplied /// only as a private test-directory path, never as a secret or VPN config. #[test] #[ignore = "selected explicitly by the M8.5 in-container scenario harness"] fn alpine_scenario_driver() { let result = (|| { let root = std::env::var_os("WIREGUARD_ADMIN_FIXTURE_ROOT").ok_or(PhaseProtocolError)?; let root = PathBuf::from(root); let endpoint = fixture_underlay_endpoint()?; initialize_fixture_files(&root, &endpoint)?; initialize_baseline_fixture(&root, &endpoint)?; let mut app = open_fixture_application(&root, &endpoint)?; if !app.vault_initialized() { let vault = crate::vault::VaultBundle::create("m8 synthetic fixture password") .map_err(|_| PhaseProtocolError)?; app.prepare_initial_vault(vault) .map_err(|_| PhaseProtocolError)?; app.commit().map_err(|_| PhaseProtocolError)?; } writeln!( std::io::stdout().lock(), "phase=fixture_bootstrap status=ok" ) .map_err(|_| PhaseProtocolError)?; run_phase_protocol( &mut app, &root, std::io::stdin().lock(), std::io::stdout().lock(), ) })(); if result.is_err() { eprintln!("fixture phase protocol failed (redacted)"); panic!("fixture phase protocol failed"); } } /// Harmless startup probe selected by `runtime-check.sh --driver PATH`. /// Running this proves that the test harness can load and execute in Alpine; /// it does not inspect or mutate a WireGuard interface. #[test] #[ignore = "selected explicitly by the Alpine runtime readiness wrapper"] fn alpine_readiness() { assert_eq!(std::env::consts::OS, "linux"); assert_eq!(std::env::consts::ARCH, "x86_64"); } #[cfg(test)] mod tests { use super::*; #[test] fn scenario_bootstrap_creates_private_synthetic_saved_fixture() { use std::os::unix::fs::PermissionsExt; let root = tempfile::tempdir().expect("fixture root"); initialize_fixture_files(root.path(), "192.0.2.10:51820").expect("fixture bootstrap"); let app = open_fixture_application(root.path(), "192.0.2.10:51820") .expect("fixture application opens"); assert!(!app.vault_initialized()); for path in [ root.path().to_path_buf(), root.path().join("clients"), root.path().join("Backup"), root.path().join("Backup/data"), root.path().join("Backup/archives"), ] { assert_eq!( std::fs::metadata(path) .expect("private fixture directory") .permissions() .mode() & 0o777, 0o700 ); } for name in [crate::config::SETTINGS_FILE_NAME, "server.conf"] { assert_eq!( std::fs::metadata(root.path().join(name)) .expect("private fixture file") .permissions() .mode() & 0o777, 0o600 ); } } #[test] fn fixture_setconf_copy_removes_only_wg_quick_address_directive_privately() { use std::os::unix::fs::PermissionsExt; let root = tempfile::tempdir().expect("fixture root"); let source = root.path().join("server.conf"); std::fs::write( &source, "[Interface]\nPrivateKey = synthetic\nAddress = 10.20.0.1/24\nListenPort = 51820\n\n[Peer]\nPublicKey = synthetic-peer\nAllowedIPs = 10.20.0.2/32\n", ) .expect("synthetic server config"); let copy = fixture_setconf_copy(&source, root.path()).expect("wg setconf copy"); let rendered = std::fs::read_to_string(copy.path()).expect("fixture setconf file"); assert!(rendered.contains("PrivateKey = synthetic")); assert!(rendered.contains("[Peer]")); assert!(!rendered.contains("Address =")); assert_eq!( std::fs::metadata(copy.path()) .expect("private fixture setconf file") .permissions() .mode() & 0o777, 0o600 ); } #[test] fn setconf_copy_is_removed_when_restart_preview_fails() { let root = tempfile::tempdir().expect("fixture root"); let source = root.path().join("server.conf"); let original = b"[Interface]\nPrivateKey = synthetic\nAddress = 10.20.0.1/24\n"; std::fs::write(&source, original).expect("synthetic source config"); let copy_path = { let copy = fixture_setconf_copy(&source, root.path()).expect("setconf copy"); let copy_path = copy.path().to_path_buf(); // Returning from this scope stands in for preview or preflight failure. copy_path }; assert!(!copy_path.exists()); assert_eq!(std::fs::read(source).expect("original config"), original); } #[test] fn setconf_copy_is_removed_after_partial_write_failure() { let root = tempfile::tempdir().expect("fixture root"); let path = root.path().join("partial-secret.conf"); let result = write_fixture_setconf_copy(&path, b"synthetic secret", |file, contents| { use std::io::Write; file.write_all(&contents[..8])?; Err(std::io::Error::from(std::io::ErrorKind::WriteZero)) }); assert!(matches!(result, Err(PhaseProtocolError))); assert!(!path.exists()); } #[test] fn cleanup_failure_preserves_successful_setconf_submission() { let execution = run_fixture_apply_with_cleanup(|| Ok(()), || Err(PhaseProtocolError)); assert_eq!( execution, RestartExecution { outcome: RestartOutcome::Failed(RestartFailure::RequestFailed), submitted: true, phase: crate::service::RestartPhase::WaitForJob, } ); } #[derive(Default)] struct RecordingRunner { invocation: Option<(String, Vec)>, succeeds: bool, } impl CommandRunner for RecordingRunner { fn run( &mut self, program: &str, args: &[String], ) -> Result { self.invocation = Some((program.to_owned(), args.to_vec())); Ok(test_status(self.succeeds)) } } fn test_status(success: bool) -> ExitStatus { // `true` and `false` are used only to obtain portable std::process // ExitStatus values; they are never executed by the fixture driver. let code = if success { 0 } else { 1 }; use std::os::unix::process::ExitStatusExt; ExitStatus::from_raw(code << 8) } #[test] fn fixture_apply_emits_only_explicit_typed_ipv4_commands() { let command = FixtureCommand::AddIpv4Route { interface: "wg-fixture".into(), route: "10.88.0.0/24".into(), }; let mut runner = RecordingRunner { succeeds: true, ..RecordingRunner::default() }; execute_with(&command, &mut runner).expect("fixture route applies"); assert_eq!( runner.invocation, Some(( "ip".into(), vec!["route", "add", "10.88.0.0/24", "dev", "wg-fixture"] .into_iter() .map(str::to_owned) .collect() )) ); } #[test] fn fixture_setup_creates_a_wireguard_link_without_a_shell() { let command = FixtureCommand::CreateWireGuard { interface: "wg-fixture".into(), }; let mut runner = RecordingRunner { succeeds: true, ..RecordingRunner::default() }; execute_with(&command, &mut runner).expect("fixture link created"); assert_eq!( runner.invocation, Some(( "ip".into(), vec!["link", "add", "dev", "wg-fixture", "type", "wireguard"] .into_iter() .map(str::to_owned) .collect() )) ); } #[test] fn fixture_setup_adds_only_a_validated_ipv4_address() { let command = FixtureCommand::AddIpv4Address { interface: "wg-fixture".into(), address: "10.88.0.1/24".into(), }; let mut runner = RecordingRunner { succeeds: true, ..RecordingRunner::default() }; execute_with(&command, &mut runner).expect("fixture address added"); assert_eq!( runner.invocation, Some(( "ip".into(), vec!["address", "add", "10.88.0.1/24", "dev", "wg-fixture"] .into_iter() .map(str::to_owned) .collect() )) ); } #[test] fn fixture_apply_rejects_shell_text_and_paths_outside_private_root() { assert_eq!( FixtureCommand::LinkUp { interface: "wg0; touch /tmp/unexpected".into(), } .program_and_args(), Err(FixtureCommandError) ); assert_eq!( FixtureCommand::SetWireGuard { interface: "wg0".into(), config_path: "/etc/wireguard/wg0.conf".into(), } .program_and_args(), Err(FixtureCommandError) ); assert_eq!( FixtureCommand::SetWireGuard { interface: "wg0".into(), config_path: format!("{FIXTURE_ROOT}/session/../outside.conf"), } .program_and_args(), Err(FixtureCommandError) ); } #[test] fn fixture_apply_failure_is_a_fixed_redacted_category() { let command = FixtureCommand::LinkUp { interface: "wg0".into(), }; let mut runner = RecordingRunner { succeeds: false, ..RecordingRunner::default() }; assert_eq!( execute_with(&command, &mut runner), Err(FixtureCommandError) ); assert_eq!(FixtureCommandError.to_string(), "fixture command failed"); } #[test] fn server_config_path_is_a_fixture_file_path_without_command_interpretation() { let directory = tempfile::tempdir().expect("fixture directory"); let root = directory.path().join("fixture"); std::fs::create_dir(&root).expect("private fixture root"); let session = root.join("session"); std::fs::create_dir(&session).expect("fixture session"); let config = session.join("server.wg"); std::fs::write(&config, b"[Interface]\nPrivateKey = synthetic\n") .expect("synthetic config"); let command = FixtureCommand::SetWireGuard { interface: "wg0".into(), config_path: config.to_string_lossy().into_owned(), }; assert_eq!( command .program_and_args_with_root(&root) .expect("valid fixture path"), ( "wg", vec![ "setconf".into(), "wg0".into(), config.to_string_lossy().into_owned() ] ) ); } #[test] fn setconf_rejects_a_symlink_that_escapes_the_private_fixture_root() { let directory = tempfile::tempdir().expect("fixture directory"); let root = directory.path().join("fixture"); let session = root.join("session"); std::fs::create_dir_all(&session).expect("fixture session"); let outside = directory.path().join("outside.wg"); std::fs::write(&outside, b"synthetic secret config").expect("outside config"); let link = session.join("server.wg"); std::os::unix::fs::symlink(&outside, &link).expect("escaping symlink"); let command = FixtureCommand::SetWireGuard { interface: "wg0".into(), config_path: link.to_string_lossy().into_owned(), }; assert_eq!( command.program_and_args_with_root(&root), Err(FixtureCommandError) ); assert_eq!(FixtureCommandError.to_string(), "fixture command failed"); } #[test] fn baseline_fixture_peer_has_managed_metadata_and_an_ipv4_host_route() { use std::os::unix::fs::PermissionsExt; let root = tempfile::tempdir().expect("fixture root"); initialize_fixture_files(root.path(), "192.0.2.9:51820").expect("fixture files"); initialize_baseline_fixture(root.path(), "192.0.2.9:51820").expect("baseline fixture"); let server = std::fs::read(root.path().join("server.conf")).expect("server config"); let document = crate::wireguard::ConfigDocument::parse(&server).expect("valid document"); let client = std::fs::read_to_string(root.path().join("baseline-client.conf")) .expect("baseline client file"); let private_key = client .lines() .filter_map(|line| line.trim().split_once('=')) .find(|(name, _)| name.trim() == "PrivateKey") .map(|(_, value)| value.trim()) .expect("client private key"); let identity = crate::vault::WireGuardPrivateKey::parse_base64(private_key) .expect("valid synthetic key") .public_key(); let peer = document .peers() .iter() .find(|peer| peer.identity == Some(identity)) .expect("baseline server peer"); assert_eq!( peer.allowed_ips, ["10.20.0.10/32".parse().expect("IPv4 route")] ); assert!(matches!( peer.metadata, crate::wireguard::MetadataAttachment::Valid { ref metadata, .. } if metadata.alias == "M8 baseline" )); assert_eq!( std::fs::metadata(root.path().join("baseline-client.conf")) .expect("client metadata") .permissions() .mode() & 0o777, 0o600 ); } #[test] fn phase_protocol_rejects_unbounded_or_untyped_requests() { let identity = crate::domain::PublicKey::from_bytes([7; 32]); assert_eq!(PhaseRequest::parse("COMMIT"), Ok(PhaseRequest::Commit)); assert_eq!( PhaseRequest::parse("IMPORT_REISSUE"), Ok(PhaseRequest::ImportReissue) ); assert_eq!( PhaseRequest::parse("FAILURE_PREVENTS_APPLICATION"), Ok(PhaseRequest::FailurePreventsApplication) ); assert_eq!( PhaseRequest::parse("CREATE\tSynthetic client"), Ok(PhaseRequest::Create { alias: "Synthetic client".into() }) ); assert_eq!(PhaseRequest::parse("APPLY\t"), Err(PhaseProtocolError)); assert_eq!( PhaseRequest::parse("APPLY\t/tmp/server.conf\t10.20.0.2/32"), Ok(PhaseRequest::Apply { config_path: "/tmp/server.conf".into(), released_address: Some("10.20.0.2/32".into()), }) ); assert_eq!( PhaseRequest::parse("APPLY\t/tmp/server.conf\t10.20.0.2/24"), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse(&format!("DELETE\t{identity}\t10.20.0.2/32")), Ok(PhaseRequest::Delete { identity, address: "10.20.0.2".into(), }) ); assert_eq!( PhaseRequest::parse(&format!("DELETE\t{identity}\t10.20.0.2/24")), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse(&format!("DELETE\t{identity}\tnot-an-address")), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse("RESTORE\twga-pre-commit.tar\t10.20.0.2/32"), Ok(PhaseRequest::Restore { file_name: "wga-pre-commit.tar".into(), address: "10.20.0.2".into(), }) ); assert_eq!( PhaseRequest::parse("RESTORE\t../outside.tar\t10.20.0.2/32"), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse("RESTORE\twga-pre-commit.tar\t10.20.0.2/24"), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse("COMMIT\t/etc/passwd"), Err(PhaseProtocolError) ); assert_eq!( PhaseRequest::parse(&"x".repeat(MAX_PHASE_LINE_BYTES + 1)), Err(PhaseProtocolError) ); assert_eq!( PhaseProtocolError.to_string(), "fixture phase request rejected" ); } #[test] fn archive_path_blocker_forces_not_a_directory_and_restores_archives() { use std::io::ErrorKind; let root = tempfile::tempdir().expect("private fixture root"); let archive_dir = root.path().join("Backup/archives"); std::fs::create_dir_all(&archive_dir).expect("archive directory"); std::fs::write(archive_dir.join("retained.tar"), b"synthetic archive") .expect("synthetic archive"); { let _blocker = ArchivePathBlocker::install(root.path()).expect("path blocker"); let error = std::fs::create_dir(archive_dir.join("new-archive")) .expect_err("a regular file cannot be traversed as a directory"); assert_eq!(error.kind(), ErrorKind::NotADirectory); } assert_eq!( std::fs::read(archive_dir.join("retained.tar")).expect("restored archive"), b"synthetic archive" ); assert!( !root .path() .join("Backup/archives-failure-fixture-held") .exists() ); } #[test] fn commit_backup_failure_requires_the_typed_backup_error() { let validation_error = Err(crate::app::AppError::InvalidSavedState); assert!(!is_commit_backup_failure(&validation_error)); let backup_error = Err(crate::app::AppError::Backup( crate::backup::BackupError::TooLarge, )); assert!(is_commit_backup_failure(&backup_error)); } #[test] fn legacy_peer_deletion_succeeds_without_a_vault_record() { let root = tempfile::tempdir().expect("fixture root"); initialize_fixture_files(root.path(), "192.0.2.10:51820").expect("fixture files"); initialize_baseline_fixture(root.path(), "192.0.2.10:51820") .expect("baseline client fixture"); let baseline = std::fs::read(root.path().join("baseline-client.conf")).expect("baseline client file"); let identity = crate::vault::import::parse_client_config(&baseline) .expect("baseline client parses") .identity; let mut app = open_fixture_application(root.path(), "192.0.2.10:51820") .expect("fixture application opens"); assert!(app.states().draft().get(&identity).is_some()); assert!( !app.delete_client(identity) .expect("legacy peer deletion succeeds") ); assert!(app.states().draft().get(&identity).is_none()); app.commit().expect("legacy peer deletion commits"); assert!(app.states().saved().get(&identity).is_none()); assert!( app.states() .reserved_addresses() .contains(&"10.20.0.10".parse().expect("baseline IPv4 address")) ); } #[test] fn released_ipv4_host_cidr_is_checked_against_reserved_ip_address() { let reserved = ["10.20.0.2".parse().expect("IPv4 address")] .into_iter() .collect(); assert_eq!( released_address_is_unreserved(Some("10.20.0.2/32"), &reserved), Ok(false) ); assert_eq!( released_address_is_unreserved(Some("10.20.0.3/32"), &reserved), Ok(true) ); assert_eq!( released_address_is_unreserved(Some("10.20.0.2/24"), &reserved), Err(PhaseProtocolError) ); } #[test] fn phase_protocol_keeps_creation_commit_and_first_delivery_in_one_app_session() { use std::os::unix::fs::PermissionsExt; const SETTINGS: &str = r#" interface_name = "wg-fixture" server_config_path = "server.conf" client_source_directory = "clients" server_endpoint = "10.88.0.1:51820" ipv4_allocation_range = "10.88.0.0/24" ipv6_allocation_range = "fd88::/64" dns_addresses = ["10.88.0.1"] systemd_unit = "wg-quick@wg-fixture.service" restart_timeout_seconds = 30 "#; let directory = tempfile::tempdir().expect("fixture directory"); let root = directory.path(); let resolved = SettingsFile::parse_toml(SETTINGS) .expect("settings parse") .validate() .expect("settings validate") .resolve_paths(root); std::fs::write(root.join(crate::config::SETTINGS_FILE_NAME), SETTINGS) .expect("fixture settings"); let server_key = crate::vault::WireGuardPrivateKey::generate() .expect("synthetic server key") .to_base64(); std::fs::write( &resolved.server_config_path, format!( "[Interface]\nPrivateKey = {server_key}\nAddress = 10.88.0.1/24, fd88::1/64\nListenPort = 51820\n" ), ) .expect("fixture server config"); std::fs::set_permissions( &resolved.server_config_path, std::fs::Permissions::from_mode(0o600), ) .expect("private server config"); std::fs::create_dir_all(&resolved.data_directory).expect("fixture data directory"); let vault = crate::vault::VaultBundle::create("synthetic fixture password") .expect("synthetic vault") .to_toml() .expect("vault serialization"); let vault_path = resolved.data_directory.join("vault.toml"); std::fs::write(&vault_path, vault).expect("fixture vault"); std::fs::set_permissions(&vault_path, std::fs::Permissions::from_mode(0o600)) .expect("private vault"); let store = SavedStore::new(root, resolved).expect("fixture store"); let mut app = Application::open_from_saved_store(store).expect("fixture application"); let mut result = Vec::new(); run_phase_protocol( &mut app, root, std::io::Cursor::new(b"CREATE\tSynthetic fixture client\nCOMMIT\nDELIVER\nEND\n"), &mut result, ) .expect("bounded lifecycle completes"); let report = String::from_utf8(result).expect("phase output is UTF-8"); assert!(report.contains("phase=create status=ok public_key=")); assert!(report.contains("phase=commit status=ok")); assert!(report.contains("phase=delivery status=ok activation_warning=true")); assert!(report.contains("phase=end status=ok")); assert!(root.join("first-delivery.conf").is_file()); assert_eq!(app.display_projection().clients.len(), 1); let public_key = report .split_once("phase=create status=ok public_key=") .and_then(|(_, tail)| tail.split_whitespace().next()) .expect("created public identity") .to_owned(); let delivery = std::fs::read_to_string(root.join("first-delivery.conf")) .expect("synthetic delivered file"); let address = delivery .lines() .find_map(|line| line.trim().strip_prefix("Address = ")) .and_then(|value| value.split(',').next()) .and_then(|value| value.trim().parse::().ok()) .expect("client host address"); let delete_request = format!("DELETE\t{public_key}\t{address}\nEND\n"); let mut deletion_output = Vec::new(); run_phase_protocol( &mut app, root, std::io::Cursor::new(delete_request.into_bytes()), &mut deletion_output, ) .expect("saved deletion and reopen complete"); let deletion_report = String::from_utf8(deletion_output).expect("phase output is UTF-8"); assert!( deletion_report .contains("current_record=removed backup_record=retained address_reserved=true") ); assert!(app.display_projection().clients.is_empty()); assert!(app.states().saved().clients().next().is_none()); assert!(app.states().reserved_addresses().contains(&address.addr())); } #[test] fn restore_expectation_keeps_every_removed_client_address_until_apply() { use std::net::IpAddr; let prior = ["10.88.0.9".parse().expect("prior reservation")] .into_iter() .collect(); let removed = [ "10.88.0.2".parse().expect("first removed address"), "10.88.0.3".parse().expect("second removed address"), ] .into_iter() .collect(); let restored = ["10.88.0.1".parse().expect("restored client address")] .into_iter() .collect(); let expected = expected_restore_reservations(prior, removed, restored); assert_eq!( expected, [ "10.88.0.2".parse::().expect("removed address"), "10.88.0.3".parse::().expect("removed address"), "10.88.0.9".parse::().expect("prior reservation"), ] .into_iter() .collect() ); } #[test] fn ip_address_input_is_parsed_as_ipv4_cidr_before_use() { let command = FixtureCommand::AddIpv4Address { interface: "wg0".into(), address: format!("{}/24; reboot", Ipv4Addr::new(10, 88, 0, 1)), }; assert_eq!(command.program_and_args(), Err(FixtureCommandError)); } #[test] fn import_reissue_preserves_sources_and_enforces_password_and_conflict_boundaries() { let root = tempfile::tempdir().expect("fixture root"); initialize_fixture_files(root.path(), "192.0.2.10:51820").expect("fixture files"); initialize_baseline_fixture(root.path(), "192.0.2.10:51820") .expect("generated valid baseline client"); let original = std::fs::read(root.path().join("baseline-client.conf")) .expect("original generated client"); let mut report = Vec::new(); run_import_reissue_scenario(root.path(), &mut report) .expect("import and password-authorized reissue scenario"); let report = String::from_utf8(report).expect("secret-free report"); assert!(report.contains("matched=true conflict_visible=true invalid_visible=true")); assert!(report.contains("originals_unchanged=true wrong_password_blocked=true")); assert!(report.contains("conflict_reissue_blocked=true reissue_mode=0600")); assert_eq!( std::fs::read(root.path().join("baseline-client.conf")) .expect("original client remains readable"), original ); assert!( root.path() .join("import-reissue/reissued-client.conf") .is_file() ); assert!( !root .path() .join("import-reissue/wrong-password.conf") .exists() ); assert!( !root .path() .join("import-reissue/conflict-reissue.conf") .exists() ); assert!(!report.contains("PrivateKey")); } #[test] fn source_preservation_check_detects_changed_invalid_candidate() { let source_dir = tempfile::tempdir().expect("source directory"); let originals = [ ("matched.conf", b"matched source".as_slice()), ("conflict.conf", b"conflicting source".as_slice()), ("invalid.conf", b"invalid source".as_slice()), ]; for (name, contents) in originals { std::fs::write(source_dir.path().join(name), contents).expect("write source"); } let snapshots = [ ("matched.conf", b"matched source".as_slice()), ("conflict.conf", b"conflicting source".as_slice()), ("invalid.conf", b"invalid source".as_slice()), ]; assert!(source_files_unchanged(source_dir.path(), &snapshots)); std::fs::write( source_dir.path().join("invalid.conf"), b"changed invalid source", ) .expect("modify invalid source"); assert!(!source_files_unchanged(source_dir.path(), &snapshots)); } }