Files
wireguard-admin/src/container_driver.rs
T
2026-10-27 22:21:44 +02:00

2519 lines
98 KiB
Rust

//! 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<String>), 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<String>), 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::<ipnet::Ipv4Net>()
.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::<ipnet::Ipv4Net>()
.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<ExitStatus, FixtureCommandError>;
}
#[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<ExitStatus, FixtureCommandError> {
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<PathBuf, FixtureCommandError> {
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(&current).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(&current).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<String>,
},
Observe,
End,
ImportReissue,
FailurePreventsApplication,
}
impl PhaseRequest {
fn parse(line: &str) -> Result<Self, PhaseProtocolError> {
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::<crate::domain::PublicKey>()
.map_err(|_| PhaseProtocolError)?;
let address = address
.parse::<ipnet::IpNet>()
.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::<ipnet::IpNet>()
.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<std::net::IpAddr, PhaseProtocolError> {
let network = value
.parse::<ipnet::IpNet>()
.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<std::net::IpAddr>,
) -> Result<bool, PhaseProtocolError> {
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<crate::domain::PublicKey>,
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::<std::net::IpAddr>()
.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::<std::collections::BTreeSet<_>>();
if restored_state.as_ref().is_none_or(|state| {
state.last_verified_at_utc.is_some()
|| state
.reserved_addresses
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>()
!= 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::<Vec<_>>()
.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<Self, PhaseProtocolError> {
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<PathBuf, crate::app::AppError>) -> 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::<std::net::IpAddr>()
.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<u8>, Vec<u8>, Option<Vec<u8>>, Option<Vec<u8>>);
type ProtectedFileSnapshot = Vec<(PathBuf, Option<Vec<u8>>)>;
fn saved_last_verified_timestamp(
saved: &crate::backup::SavedBytes,
) -> Result<Option<String>, 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<ProtectedFileSnapshot, PhaseProtocolError> {
[
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<std::net::IpAddr>,
removed_client_addresses: std::collections::BTreeSet<std::net::IpAddr>,
restored_client_addresses: std::collections::BTreeSet<std::net::IpAddr>,
) -> std::collections::BTreeSet<std::net::IpAddr> {
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<Option<String>, 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<Application, PhaseProtocolError> {
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<crate::domain::PublicKey, Vec<ipnet::IpNet>>,
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<String, PhaseProtocolError> {
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<FixtureSetconfCopy, PhaseProtocolError> {
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<FixtureSetconfCopy, PhaseProtocolError> {
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<String, PhaseProtocolError> {
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::<Vec<_>>()
.windows(2)
.find(|pair| pair[0] == "inet")
.and_then(|pair| pair[1].split('/').next())
.and_then(|address| address.parse::<Ipv4Addr>().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<String>)>,
succeeds: bool,
}
impl CommandRunner for RecordingRunner {
fn run(
&mut self,
program: &str,
args: &[String],
) -> Result<ExitStatus, FixtureCommandError> {
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::<ipnet::IpNet>().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::<IpAddr>().expect("removed address"),
"10.88.0.3".parse::<IpAddr>().expect("removed address"),
"10.88.0.9".parse::<IpAddr>().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));
}
}