Skip to main content

ados_protocol/
plugin_loopback_guard.rs

1//! The plugin loopback-guard verdict sidecar.
2//!
3//! The plugin-host daemon loads an nftables rule at startup that keeps a plugin
4//! granted `network.outbound` off the agent's own loopback listeners, and
5//! records whether it loaded here. The plugin lifecycle reads it before it
6//! grants network access or renders a unit with it; the status surface reports
7//! it. An absent or unreadable sidecar is `unavailable`: until the daemon has
8//! loaded the rule, nothing may assume it is there.
9
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13
14/// The sidecar file name under the run dir.
15pub const SIDECAR_NAME: &str = "plugin-loopback-guard.json";
16
17/// The guard verdict.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct GuardState {
20    /// `true` when the rule is loaded.
21    pub active: bool,
22    /// Why the guard is not active; empty when it is.
23    #[serde(default)]
24    pub reason: String,
25}
26
27impl GuardState {
28    /// An inactive verdict carrying `reason`.
29    pub fn unavailable(reason: impl Into<String>) -> Self {
30        Self {
31            active: false,
32            reason: reason.into(),
33        }
34    }
35
36    /// The status-surface word for this verdict.
37    pub fn label(&self) -> &'static str {
38        if self.active {
39            "active"
40        } else {
41            "unavailable"
42        }
43    }
44}
45
46/// The canonical sidecar path (`ADOS_RUN_DIR`, default `/run/ados`).
47pub fn sidecar_path() -> PathBuf {
48    let run = std::env::var("ADOS_RUN_DIR").unwrap_or_else(|_| "/run/ados".to_string());
49    Path::new(&run).join(SIDECAR_NAME)
50}
51
52/// Read the verdict at `path`; absent or unparseable reads `unavailable`.
53pub fn read_state_at(path: &Path) -> GuardState {
54    std::fs::read_to_string(path)
55        .ok()
56        .and_then(|t| serde_json::from_str(&t).ok())
57        .unwrap_or_else(|| GuardState::unavailable("the plugin host has not loaded the guard"))
58}
59
60/// [`read_state_at`] against the canonical sidecar.
61pub fn read_state() -> GuardState {
62    read_state_at(&sidecar_path())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn an_absent_or_garbled_sidecar_reads_unavailable() {
71        let dir = tempfile::tempdir().unwrap();
72        let path = dir.path().join(SIDECAR_NAME);
73        assert_eq!(read_state_at(&path).label(), "unavailable");
74        std::fs::write(&path, "not json").unwrap();
75        assert_eq!(read_state_at(&path).label(), "unavailable");
76        std::fs::write(&path, r#"{"active":true}"#).unwrap();
77        assert_eq!(read_state_at(&path).label(), "active");
78    }
79}