Complete terminal management workflow

This commit is contained in:
2026-10-21 23:25:31 +03:00
parent f718f739b4
commit 2ccd67d64c
19 changed files with 7313 additions and 53 deletions
+138 -4
View File
@@ -1,11 +1,20 @@
use std::ffi::OsString;
use std::fmt;
use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
use crate::app::Application;
use crate::config::{ResolvedSettings, SettingsFile};
use crate::persistence::SavedStore;
const MAX_SETTINGS_BYTES: u64 = 1024 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StartupAction {
Help,
Version,
ManagementUnavailable,
Management,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -25,6 +34,64 @@ impl fmt::Display for StartupError {
impl std::error::Error for StartupError {}
/// Fixed, secret-free reasons management session startup can fail.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SessionLoadError {
RootRequired,
SettingsUnavailable,
SettingsInvalid,
StoreInvalid,
SavedStateInvalid,
}
impl fmt::Display for SessionLoadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::RootRequired => "management operations require root",
Self::SettingsUnavailable => "executable-relative settings are unavailable",
Self::SettingsInvalid => "executable-relative settings are invalid",
Self::StoreInvalid => "saved-state paths are unsafe or inconsistent",
Self::SavedStateInvalid => "saved state could not be loaded safely",
})
}
}
impl std::error::Error for SessionLoadError {}
/// Load a management session only after checking privilege. Settings are read
/// beside the executable, then SavedStore performs journal recovery and
/// validates the complete saved state before Application is exposed.
/// Terminal suitability is checked by the later terminal-startup boundary.
pub fn open_management_session(
executable_directory: &Path,
effective_uid_is_root: impl FnOnce() -> bool,
) -> Result<Application, SessionLoadError> {
if !effective_uid_is_root() {
return Err(SessionLoadError::RootRequired);
}
let settings_path = ResolvedSettings::settings_path(executable_directory);
let metadata =
fs::symlink_metadata(&settings_path).map_err(|_| SessionLoadError::SettingsUnavailable)?;
if !metadata.file_type().is_file() || metadata.len() > MAX_SETTINGS_BYTES {
return Err(SessionLoadError::SettingsUnavailable);
}
let mut bytes = Vec::with_capacity(metadata.len() as usize);
File::open(&settings_path)
.and_then(|file| file.take(MAX_SETTINGS_BYTES + 1).read_to_end(&mut bytes))
.map_err(|_| SessionLoadError::SettingsUnavailable)?;
if bytes.len() as u64 > MAX_SETTINGS_BYTES {
return Err(SessionLoadError::SettingsUnavailable);
}
let text = std::str::from_utf8(&bytes).map_err(|_| SessionLoadError::SettingsInvalid)?;
let resolved = SettingsFile::parse_toml(text)
.and_then(SettingsFile::validate)
.map(|validated| validated.resolve_paths(executable_directory))
.map_err(|_| SessionLoadError::SettingsInvalid)?;
let store = SavedStore::new(executable_directory, resolved)
.map_err(|_| SessionLoadError::StoreInvalid)?;
Application::open_from_saved_store(store).map_err(|_| SessionLoadError::SavedStateInvalid)
}
/// Resolves the supported command line and checks management privilege lazily.
///
/// The injected root check keeps this boundary testable without running tests as
@@ -36,7 +103,7 @@ pub fn start(
match arguments {
[] => {
if effective_uid_is_root() {
Ok(StartupAction::ManagementUnavailable)
Ok(StartupAction::Management)
} else {
Err(StartupError::RootRequired)
}
@@ -72,9 +139,9 @@ mod tests {
}
#[test]
fn root_management_startup_reports_unimplemented_without_side_effects() {
fn root_management_startup_is_authorized_for_terminal_boundary() {
let result = start(&[], || true);
assert_eq!(result, Ok(StartupAction::ManagementUnavailable));
assert_eq!(result, Ok(StartupAction::Management));
}
#[test]
@@ -89,4 +156,71 @@ mod tests {
assert_eq!(result, Err(StartupError::UnknownArguments));
}
}
#[test]
fn management_session_checks_root_before_reading_settings() {
let result = super::open_management_session(
std::path::Path::new("/path/that/does/not/exist"),
|| false,
);
assert_eq!(result.err(), Some(super::SessionLoadError::RootRequired));
}
#[test]
fn management_session_loads_supported_state_and_keeps_unsupported_peers_visible() {
use std::os::unix::fs::PermissionsExt;
let directory = std::env::temp_dir().join(format!(
"wireguard-admin-startup-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&directory).unwrap();
std::fs::write(
directory.join(crate::config::SETTINGS_FILE_NAME),
r#"interface_name = "wg0"
server_config_path = "wg0.conf"
client_source_directory = "clients"
server_endpoint = "vpn.example.org:51820"
ipv4_allocation_range = "10.80.0.0/24"
ipv6_allocation_range = "fd80::/64"
dns_addresses = ["10.80.0.1"]
systemd_unit = "wg-quick@wg0.service"
restart_timeout_seconds = 10
"#,
)
.unwrap();
let server_path = directory.join("wg0.conf");
std::fs::write(
&server_path,
b"[Interface]\nAddress = 10.80.0.1/24\n# wg-admin:begin\n# wg-admin:version=1\n# wg-admin:alias=Y2xlYW4\n# wg-admin:end\n[Peer]\nPublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nAllowedIPs = 10.80.0.2/32, fd80::2/128\n[Peer]\nPublicKey = AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=\nAllowedIPs = 10.80.0.0/24\n",
)
.unwrap();
std::fs::set_permissions(&server_path, std::fs::Permissions::from_mode(0o600)).unwrap();
let app = super::open_management_session(&directory, || true)
.expect("valid synthetic installation should open");
let projection = app.display_projection();
assert_eq!(projection.clients.len(), 2);
assert_eq!(app.states().saved().clients().count(), 1);
assert!(projection.clients[0].mutable_identity.is_some());
assert!(projection.clients[1].mutable_identity.is_none());
assert_eq!(projection.clients[1].allowed_ips, ["10.80.0.0/24"]);
assert!(!format!("{projection:?}").contains("PrivateKey"));
let internal_state = directory.join("Backup/data");
std::fs::create_dir_all(&internal_state).unwrap();
std::fs::write(
internal_state.join("internal-state.toml"),
b"not valid state",
)
.unwrap();
assert_eq!(
super::open_management_session(&directory, || true).err(),
Some(super::SessionLoadError::SavedStateInvalid),
"corrupt persisted state must stop session loading rather than start empty"
);
std::fs::remove_dir_all(directory).unwrap();
}
}