use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use wireguard_admin::app::{ Application, FirstIssueAttempt, FirstIssueDeliveryOutcome, ReissueFailureKind, ReissueStage, }; use wireguard_admin::config::{ResolvedSettings, SettingsFile}; use wireguard_admin::delivery::files::FileOutputError; use wireguard_admin::domain::{ Client, ClientAddresses, ClientStates, RuntimeObservation, Snapshot, }; use wireguard_admin::persistence::SavedStore; use wireguard_admin::vault::import::{parse_client_config, reconcile}; use wireguard_admin::vault::{VaultBundle, WireGuardPrivateKey}; use wireguard_admin::wireguard::ConfigDocument; const SETTINGS: &str = r#" interface_name = "wg0" server_config_path = "server.conf" client_source_directory = "clients" server_endpoint = "vpn.example.test:51820" 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 "#; struct Fixture { root: tempfile::TempDir, settings: ResolvedSettings, store: SavedStore, states: ClientStates, } fn fixture(server: String, states: ClientStates) -> Fixture { let root = tempfile::tempdir().expect("private temporary fixture"); let settings = SettingsFile::parse_toml(SETTINGS) .expect("fixture settings parse") .validate() .expect("fixture settings validate") .resolve_paths(root.path()); fs::create_dir_all(&settings.client_source_directory).expect("source directory"); fs::write(ResolvedSettings::settings_path(root.path()), SETTINGS).expect("settings file"); fs::write(&settings.server_config_path, server).expect("server configuration"); fs::set_permissions( &settings.server_config_path, fs::Permissions::from_mode(0o600), ) .expect("private server configuration"); let store = SavedStore::new(root.path(), settings.clone()).expect("saved store"); Fixture { root, settings, store, states, } } fn empty_states() -> ClientStates { ClientStates::new(Snapshot::default(), None, RuntimeObservation::Unknown).expect("empty state") } fn open(fixture: &Fixture) -> Application { Application::open(fixture.store.clone(), fixture.states.clone()).expect("application opens") } fn new_client_fixture() -> (Fixture, Application, wireguard_admin::domain::PublicKey) { let server_key = WireGuardPrivateKey::generate().expect("synthetic server key"); let server = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.1/24, fd20::1/64\n", server_key.to_base64() ); let fixture = fixture(server, empty_states()); let mut app = open(&fixture); app.prepare_initial_vault(VaultBundle::create("fixture vault password").expect("vault")) .expect("stage vault"); app.commit().expect("install vault"); let identity = app .prepare_new_client("same alias", None, None) .expect("prepare client"); app.commit().expect("commit client"); (fixture, app, identity) } fn destination( app: &Application, path: impl AsRef, ) -> wireguard_admin::delivery::files::PreparedDestination { app.prepare_first_issue_destination(path) .expect("prepare selected destination") } fn archive_name(path: &Path) -> &str { path.file_name() .and_then(|name| name.to_str()) .expect("synthetic archive filename") } #[test] fn committed_creation_retries_after_overwrite_decline_and_duplicate_aliases_are_independent() { let server_key = WireGuardPrivateKey::generate().expect("synthetic server key"); let server = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.1/24, fd20::1/64\n", server_key.to_base64() ); let fixture = fixture(server, empty_states()); let mut app = open(&fixture); app.prepare_initial_vault(VaultBundle::create("fixture vault password").expect("vault")) .expect("stage vault"); app.commit().expect("install vault"); let first = app .prepare_new_client("duplicate", None, None) .expect("first client"); let second = app .prepare_new_client("duplicate", None, None) .expect("second client"); app.commit().expect("commit clients"); let existing = fixture.root.path().join("occupied.conf"); fs::write(&existing, b"existing output").expect("existing regular file"); let declined = app .deliver_first_issue_file( first, FirstIssueAttempt::Initial, destination(&app, &existing), ) .expect("decline reported as an operation result"); assert!(matches!( declined, FirstIssueDeliveryOutcome::Failed { error: FileOutputError::OverwriteDeclined, .. } )); let retry_path = fixture.root.path().join("new-dir/first.conf"); let retried = app .deliver_first_issue_file( first, FirstIssueAttempt::Retry { confirm_possible_overwrite: false, confirm_manual_cleanup: false, }, destination(&app, &retry_path), ) .expect("retry remains password free in creation session"); assert!(matches!( retried, FirstIssueDeliveryOutcome::Delivered(delivery) if delivery.activation_warning )); assert_eq!( fs::metadata(&retry_path) .expect("delivered file") .permissions() .mode() & 0o777, 0o600 ); let second_path = fixture.root.path().join("second.conf"); let delivered_second = app .deliver_first_issue_file( second, FirstIssueAttempt::Initial, destination(&app, second_path), ) .expect("second key has independent eligibility"); assert!(matches!( delivered_second, FirstIssueDeliveryOutcome::Delivered(delivery) if delivery.activation_warning )); } #[test] fn reissue_unlocks_installed_saved_record_each_time_and_warns_after_reopen() { let (fixture, mut app, identity) = new_client_fixture(); let initial_path = fixture.root.path().join("initial.conf"); let issued = app .deliver_first_issue_file( identity, FirstIssueAttempt::Initial, destination(&app, &initial_path), ) .expect("first file issued"); assert!(matches!( issued, FirstIssueDeliveryOutcome::Delivered(delivery) if delivery.activation_warning )); // A draft rename must not change the saved configuration used by reissue. app.rename_client(identity, "unsaved draft alias") .expect("prepare unrelated draft rename"); let wrong_path = fixture.root.path().join("wrong-password.conf"); let wrong = app.reissue_client_file( identity, "wrong password", destination(&app, &wrong_path), false, ); let error = wrong.expect_err("wrong password rejected"); assert_eq!(error.stage, ReissueStage::Authorization); assert_eq!(error.kind, ReissueFailureKind::PasswordOrRecordRejected); assert!(!wrong_path.exists()); let output = fixture.root.path().join("reissue/client.conf"); let result = app .reissue_client_file( identity, "fixture vault password", destination(&app, &output), true, ) .expect("fresh password authorizes reissue"); assert!(result.activation_warning); assert!(result.qr.is_some_and(|qr| qr.is_ok())); let bytes = fs::read(&output).expect("mandatory reissue file"); let parsed = parse_client_config(&bytes).expect("rendered file parses"); assert_eq!(parsed.identity, identity); assert_eq!( fs::metadata(&output) .expect("private output") .permissions() .mode() & 0o777, 0o600 ); let next_action_path = fixture.root.path().join("next-action.conf"); // A successful action does not preserve an unlocked identity for the next // action in the same session. let next_action = app.reissue_client_file( identity, "wrong password", destination(&app, next_action_path), false, ); assert_eq!( next_action .expect_err("previous action does not cache an unlocked identity") .kind, ReissueFailureKind::PasswordOrRecordRejected ); } #[test] fn imported_ipv4_only_parameters_are_preserved_and_authenticated_conflict_blocks_reissue() { let server_key = WireGuardPrivateKey::generate().expect("synthetic server key"); let client_key = WireGuardPrivateKey::generate().expect("synthetic client key"); let identity = client_key.public_key(); let server = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.1/24\n\n[Peer]\nPublicKey = {identity}\nAllowedIPs = 10.20.0.2/32\n", server_key.to_base64() ); let client = Client::new( identity, "imported", ClientAddresses { ipv4: "10.20.0.2".parse().expect("IPv4"), ipv6: None, }, ) .expect("IPv4-only domain client"); let snapshot = Snapshot::new([client]).expect("snapshot"); let import_fixture = fixture( server.clone(), ClientStates::new(snapshot, None, RuntimeObservation::Unknown).expect("client state"), ); let document = ConfigDocument::parse(server.as_bytes()).expect("server document"); let psk = WireGuardPrivateKey::generate().expect("synthetic preshared key"); let source = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.2/32\nDNS = 1.1.1.1\nMTU = 1380\n\n[Peer]\nPublicKey = {}\nPresharedKey = {}\nEndpoint = imported.example.test:12345\nAllowedIPs = 0.0.0.0/0\nPersistentKeepalive = 25\n", client_key.to_base64(), server_key.public_key(), psk.to_base64() ); let report = reconcile(&document, [("client.conf".to_owned(), source.into_bytes())]); let mut bundle = VaultBundle::create("fixture vault password").expect("vault"); report .encrypt_candidate(&mut bundle, 0) .expect("encrypt matching import"); let mut app = open(&import_fixture); app.prepare_initial_vault(bundle) .expect("stage imported vault"); app.commit().expect("commit imported vault"); let output = import_fixture.root.path().join("imported.conf"); app.reissue_client_file( identity, "fixture vault password", destination(&app, &output), false, ) .expect("imported record reissues"); let rendered = fs::read_to_string(&output).expect("rendered imported file"); assert!(rendered.contains("Address = 10.20.0.2/32\n")); assert!(rendered.contains("DNS = 1.1.1.1\n")); assert!(rendered.contains("MTU = 1380\n")); assert!(rendered.contains("Endpoint = imported.example.test:12345\n")); assert!(rendered.contains("AllowedIPs = 0.0.0.0/0\n")); assert!(rendered.contains("PersistentKeepalive = 25\n")); assert!(rendered.contains("PresharedKey = ")); assert!(!rendered.contains("Address = 10.20.0.2/32, fd20::")); // A saved peer with a mismatching retained address is authenticated as // blocked during import; an edited public status bit cannot authorize it. let conflict_key = WireGuardPrivateKey::generate().expect("second synthetic key"); let blocked_server = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.1/24\n\n[Peer]\nPublicKey = {}\nAllowedIPs = 10.20.0.9/32\n", server_key.to_base64(), conflict_key.public_key() ); let blocked_document = ConfigDocument::parse(blocked_server.as_bytes()).expect("server"); let blocked_source = format!( "[Interface]\nPrivateKey = {}\nAddress = 10.20.0.2/32\n[Peer]\nPublicKey = {}\nEndpoint = vpn.example.test:51820\nAllowedIPs = 0.0.0.0/0\n", conflict_key.to_base64(), server_key.public_key() ); let blocked_report = reconcile( &blocked_document, [("blocked.conf".to_owned(), blocked_source.into_bytes())], ); let mut blocked_bundle = VaultBundle::create("fixture vault password").expect("vault"); blocked_report .encrypt_candidate(&mut blocked_bundle, 0) .expect("encrypt blocked candidate"); let blocked_fixture = fixture(blocked_server, empty_states()); let mut blocked_app = open(&blocked_fixture); blocked_app .prepare_initial_vault(blocked_bundle) .expect("stage blocked vault"); blocked_app.commit().expect("commit blocked record"); let blocked_path = blocked_fixture.root.path().join("blocked.conf"); let blocked = blocked_app.reissue_client_file( conflict_key.public_key(), "fixture vault password", destination(&blocked_app, blocked_path), false, ); assert_eq!( blocked .expect_err("authenticated discrepancy blocks reissue") .kind, ReissueFailureKind::PasswordOrRecordRejected ); } #[test] fn deletion_and_restore_remove_password_free_authority_and_reopen_requires_vault_password() { let (fixture, mut app, identity) = new_client_fixture(); let before_delete_archive = app.create_manual_backup().expect("saved client archive"); assert_eq!( fs::metadata(&fixture.settings.server_config_path) .expect("server remains installed") .permissions() .mode() & 0o777, 0o600 ); assert!(app.delete_client(identity).expect("delete client")); app.commit().expect("commit deletion"); let deleted = app.reissue_client_file( identity, "fixture vault password", destination(&app, fixture.root.path().join("deleted.conf")), false, ); assert!(deleted.is_err()); assert!(!fixture.root.path().join("deleted.conf").exists()); let preview = app .preview_restore(archive_name(&before_delete_archive)) .expect("preview prior saved client"); app.confirm_restore(preview) .expect("restore saved client only"); let reopened = open(&fixture); let restored_path = fixture.root.path().join("restored.conf"); let restored = reopened .reissue_client_file( identity, "fixture vault password", destination(&reopened, &restored_path), false, ) .expect("restored encrypted record requires and accepts password"); assert!(restored.activation_warning); assert!(restored_path.exists()); } #[test] fn overwrite_confirmation_is_bound_to_the_selected_destination() { let (fixture, app, identity) = new_client_fixture(); let path: PathBuf = fixture.root.path().join("output.conf"); fs::write(&path, b"replace only after confirmation").expect("existing file"); let prepared = destination(&app, &path); let declined = app.reissue_client_file(identity, "fixture vault password", prepared, false); assert!(matches!( declined, Err(error) if error.stage == ReissueStage::FilePublication && error.kind == ReissueFailureKind::FileOutput(FileOutputError::OverwriteDeclined) )); let confirmed = destination(&app, &path) .confirm_overwrite() .expect("confirm exact destination"); app.reissue_client_file(identity, "fixture vault password", confirmed, false) .expect("confirmed reissue replaces destination"); let parsed = parse_client_config(&fs::read(&path).expect("replacement output")) .expect("replacement is valid config"); assert_eq!(parsed.identity, identity); }