//! systemctl + dpkg-query helpers for the FIPS daemon. //! //! Read-only queries (`is-active`, `--version`, `dpkg-query`) run as the //! archipelago service user. Write operations (`unmask`, `start`, `stop`, //! `restart`) go through `sudo`, matching the pattern established in //! `src/vpn.rs` and `src/api/rpc/vpn.rs`. The sudoers rule shipped in the //! ISO whitelists exactly these invocations. use anyhow::{Context, Result}; use tokio::process::Command; /// `systemctl is-active ` → "active" / "inactive" / "failed" / "masked" /// / "unknown". Never errors; returns "unknown" on any failure. pub async fn unit_state(unit: &str) -> String { match Command::new("systemctl") .args(["is-active", unit]) .output() .await { Ok(out) => { let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); if s.is_empty() { "unknown".to_string() } else { s } } Err(_) => "unknown".to_string(), } } /// Whether the `fips` debian package is installed on the host. pub async fn package_installed() -> bool { // dpkg-query -W -f='${Status}' fips → "install ok installed" when present. let out = Command::new("dpkg-query") .args(["-W", "-f=${Status}", "fips"]) .output() .await; match out { Ok(o) if o.status.success() => { String::from_utf8_lossy(&o.stdout).contains("install ok installed") } _ => false, } } /// `fipsctl --version` output stripped of the "fipsctl " prefix if present. pub async fn daemon_version() -> Result { let out = Command::new("fipsctl") .arg("--version") .output() .await .context("fipsctl --version failed to launch")?; if !out.status.success() { anyhow::bail!("fipsctl exited with non-zero status"); } let raw = String::from_utf8_lossy(&out.stdout).trim().to_string(); Ok(raw .strip_prefix("fipsctl ") .map(|s| s.to_string()) .unwrap_or(raw)) } /// `sudo systemctl ` — returns stderr on non-zero exit. async fn sudo_systemctl(verb: &str, unit: &str) -> Result<()> { let out = Command::new("sudo") .args(["systemctl", verb, unit]) .output() .await .with_context(|| format!("sudo systemctl {} {} failed to launch", verb, unit))?; if !out.status.success() { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); anyhow::bail!("systemctl {} {}: {}", verb, unit, stderr); } Ok(()) } /// Unmask + start + enable the FIPS service. Idempotent — safe to call /// on every backend startup once the key is on disk. pub async fn activate(unit: &str) -> Result<()> { // Order matters: unmask before enable/start, otherwise enable fails // on a masked unit. sudo_systemctl("unmask", unit).await?; sudo_systemctl("enable", unit).await?; sudo_systemctl("start", unit).await?; Ok(()) } pub async fn stop(unit: &str) -> Result<()> { sudo_systemctl("stop", unit).await } pub async fn restart(unit: &str) -> Result<()> { sudo_systemctl("restart", unit).await } pub async fn mask(unit: &str) -> Result<()> { let _ = sudo_systemctl("stop", unit).await; let _ = sudo_systemctl("disable", unit).await; sudo_systemctl("mask", unit).await } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_unit_state_returns_string_for_bogus_unit() { // Nonexistent unit: systemctl returns "inactive" or "unknown" — we // just care that the helper doesn't panic and returns *something*. let s = unit_state("archipelago-bogus-test.service").await; assert!(!s.is_empty()); } #[tokio::test] async fn test_package_installed_is_bool() { // Must not panic regardless of host state. let _ = package_installed().await; } }