121 lines
3.9 KiB
Rust
121 lines
3.9 KiB
Rust
|
|
//! 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 <unit>` → "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<String> {
|
||
|
|
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 <verb> <unit>` — 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;
|
||
|
|
}
|
||
|
|
}
|