Skip to main content

planarian_daemon/
hold.rs

1//! §12d *Network security fails closed*, the daemon's half (#35 part 1).
2//!
3//! A network-security unit (interim, B-21: the module's exported `holds`)
4//! that has failed has its traffic held. systemd's `OnFailure=` holds it on a
5//! crash or kill; the daemon holds it for a stop outside Planarian that is not
6//! attributed, and journals either one (`HoldApplied`, before its own call).
7//! The daemon lifts the hold once the unit is Running and every Readiness
8//! probe passes (`HoldLifted`, before the call). If repair fails, the hold
9//! stays and administrators are notified. `planarian hold release` is an
10//! attested Override on the hold unit, through the executor like any
11//! override, so a non-administrator's release asks an administrator.
12//!
13//! The hold is a failure posture response, not a transaction (docs/bypasses.md).
14
15use planarian_schema::*;
16
17/// The alarm when a held unit goes down (§12d, §11), word for word.
18pub fn alarm(unit: &UnitId) -> String {
19    format!("ALARM: {unit} stopped unexpectedly. Network traffic is blocked until it is back.")
20}
21
22/// The systemd unit that holds `unit`'s traffic (lane-nix's
23/// `planarian-hold@<unit>.service`; the instance is the unit's name).
24pub fn hold_unit(unit: &UnitId) -> UnitId {
25    UnitId::service(&format!("planarian-hold@{}", unit.name))
26}
27
28/// The held unit a hold unit stands for, if `u` is one.
29pub fn held_by(u: &UnitId) -> Option<UnitId> {
30    u.name.strip_prefix("planarian-hold@").map(UnitId::service)
31}
32
33/// The hold unit's definition: known, so an override on it derives the held
34/// unit's sensitivity (§11: releasing a hold that protects a Security unit
35/// needs an administrator), but never expected, so it is never watched,
36/// repaired or counted as an incident.
37pub fn hold_def(held: &UnitDef) -> UnitDef {
38    let mut d = held.clone();
39    d.id = hold_unit(&held.id);
40    d.desired = Desired { status: DesiredStatus::Stopped, when: Condition::Any(vec![]) };
41    d.edges.clear();
42    d.probes.clear();
43    d.synonyms.clear();
44    d.systemd_restart = None;
45    d.runtime_repair = None;
46    d.restart_disruption = Disruption::None;
47    d.holds = false;
48    d.hold_allow.clear();
49    d.hold_interfaces.clear();
50    d
51}
52
53/// The units whose failure holds traffic (interim: the exported `holds`).
54pub fn network_security(defs: &UnitDefs) -> impl Iterator<Item = &UnitDef> {
55    defs.values().filter(|d| d.holds)
56}
57
58/// It has failed in §12d's sense: observed failed, or not running while it
59/// is desired running. A stop through an attested override is not a failure
60/// (the override makes it not expected).
61pub fn failed(s: &SystemState, defs: &UnitDefs, u: &UnitId) -> bool {
62    defs.get(u).is_some_and(|d| d.desired.status == DesiredStatus::Running)
63        && s.expected(u, defs)
64        && matches!(s.status(u, defs), UnitStatus::Failed(_) | UnitStatus::Stopped | UnitStatus::Absent)
65}
66
67/// Back: Running, and every Readiness probe has fresh passing evidence.
68/// A unit with no Readiness probe is back when it is Running.
69pub fn back(s: &SystemState, defs: &UnitDefs, u: &UnitId) -> bool {
70    let Some(def) = defs.get(u) else { return false };
71    let readiness = def.probes.iter().filter(|p| p.role == ProbeRole::Readiness).count();
72    s.status(u, defs) == UnitStatus::Running && s.passing_readiness(u, defs).len() == readiness
73}
74
75/// `--until` as the release marker takes it: `None` is the next boot. A
76/// release is never forever (§12d), so an explicit, unbounded one ends at
77/// the next boot too.
78pub fn release_until(u: &Until) -> Option<Timestamp> {
79    match u {
80        Until::At(t) => Some(*t),
81        Until::NextBoot | Until::Explicit => None,
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn the_hold_unit_names_its_unit_and_back() {
91        let wg = UnitId::service("wireguard-gym");
92        assert_eq!(hold_unit(&wg), UnitId::service("planarian-hold@wireguard-gym"));
93        assert_eq!(held_by(&hold_unit(&wg)), Some(wg.clone()));
94        assert_eq!(held_by(&wg), None);
95        assert_eq!(
96            alarm(&wg),
97            "ALARM: service:wireguard-gym stopped unexpectedly. Network traffic is blocked until it is back."
98        );
99    }
100
101    #[test]
102    fn the_hold_def_carries_sensitivity_and_is_never_expected() {
103        let mut wg = planarian_schema::fixtures::service("wireguard-gym");
104        wg.sensitivity = Sensitivities::of(&[Sensitivity::Security, Sensitivity::External]);
105        wg.holds = true;
106        let d = hold_def(&wg);
107        assert_eq!(d.id, UnitId::service("planarian-hold@wireguard-gym"));
108        assert_eq!(d.sensitivity, wg.sensitivity);
109        assert!(!d.holds);
110        let mut defs = UnitDefs::new();
111        defs.insert(d.id.clone(), d.clone());
112        let s = planarian_schema::fixtures::empty_state(0);
113        assert!(!s.expected(&d.id, &defs), "never watched");
114    }
115
116    #[test]
117    fn a_release_ends_at_the_next_boot_unless_timed() {
118        assert_eq!(release_until(&Until::At(Timestamp(5))), Some(Timestamp(5)));
119        assert_eq!(release_until(&Until::NextBoot), None);
120        assert_eq!(release_until(&Until::Explicit), None);
121    }
122}