Skip to main content

planarian_backend_netwatch/
host.rs

1//! What the watch needs from the running machine, as pure functions where it
2//! can be (tested), reading `/etc`, `/sys` and `/proc` only in `read_*`.
3
4use planarian_schema::UnitId;
5use std::collections::{BTreeMap, BTreeSet};
6use std::path::Path;
7
8/// The export's top-level `networkTools` (`/etc/planarian/export.json`):
9/// unit names, a template ending in `@`. `None`: missing or unparsable,
10/// which the caller treats as fail closed (the watch is not attached).
11pub fn network_tools(export_json: &str) -> Option<Vec<String>> {
12    let v: serde_json::Value = serde_json::from_str(export_json).ok()?;
13    v.get("networkTools")?.as_array()?.iter().map(|t| t.as_str().map(str::to_string)).collect()
14}
15
16/// The unit a cgroup path belongs to: the first `<name>.service` under
17/// `system.slice`, past any slices (a template's instances sit in its own
18/// slice; a service's sub-cgroups are its own). `None` for user sessions,
19/// the root, slices: changes from there are unattributed.
20pub fn unit_of_cgroup(rel: &str) -> Option<UnitId> {
21    let mut parts = rel.trim_start_matches('/').split('/');
22    if parts.next()? != "system.slice" {
23        return None;
24    }
25    // A template's instances sit in the template's own slice
26    // (`system.slice/system-openvpn.slice/openvpn@work.service`): skip
27    // slices; the first service below them is the unit.
28    let svc = parts.find(|p| !p.ends_with(".slice"))?.strip_suffix(".service")?;
29    (!svc.is_empty()).then(|| UnitId::service(svc))
30}
31
32/// The network tools now: plain names as they are, a template (`name@`)
33/// as each running instance (`name@inst`) among `running`.
34pub fn expand_tools(tools: &[String], running: &BTreeSet<String>) -> Vec<UnitId> {
35    let mut out = BTreeSet::new();
36    for t in tools {
37        if t.ends_with('@') {
38            out.extend(running.iter().filter(|r| r.starts_with(t.as_str()) && r.len() > t.len()).cloned());
39        } else {
40            out.insert(t.trim_end_matches(".service").to_string());
41        }
42    }
43    out.into_iter().map(|n| UnitId::service(&n)).collect()
44}
45
46/// CLOCK_BOOTTIME whole seconds: the integer part of `/proc/uptime`.
47pub fn boottime_secs() -> Option<u64> {
48    let s = std::fs::read_to_string("/proc/uptime").ok()?;
49    s.split(['.', ' ']).next()?.parse().ok()
50}
51
52/// cgroup id (the cgroup directory's inode) → unit, for every service
53/// cgroup under `root` (`/sys/fs/cgroup`), and the running services' names.
54pub fn read_cgroups(root: &Path) -> (BTreeMap<u64, UnitId>, BTreeSet<String>) {
55    use std::os::unix::fs::MetadataExt;
56    let mut ids = BTreeMap::new();
57    let mut running = BTreeSet::new();
58    let mut stack = vec![root.join("system.slice")];
59    while let Some(dir) = stack.pop() {
60        let Ok(entries) = std::fs::read_dir(&dir) else { continue };
61        if let (Ok(meta), Ok(rel)) = (std::fs::metadata(&dir), dir.strip_prefix(root))
62            && let Some(u) = unit_of_cgroup(&rel.to_string_lossy())
63        {
64            running.insert(u.name.clone());
65            ids.insert(meta.ino(), u);
66        }
67        stack.extend(entries.flatten().filter(|e| e.file_type().is_ok_and(|t| t.is_dir())).map(|e| e.path()));
68    }
69    (ids, running)
70}
71
72/// The virtual links standing: those whose `/sys/class/net/<name>`
73/// resolves under `/sys/devices/virtual/` (tun, tap, wireguard, dummy,
74/// veth, bridges, `lo`). A physical NIC's resolves under its bus device.
75pub fn read_virtual_ifaces(class_net: &Path) -> BTreeSet<String> {
76    std::fs::read_dir(class_net)
77        .into_iter()
78        .flatten()
79        .flatten()
80        .filter(|e| std::fs::read_link(e.path()).is_ok_and(|t| t.to_string_lossy().contains("/devices/virtual/")))
81        .map(|e| e.file_name().to_string_lossy().into_owned())
82        .collect()
83}
84
85/// Interface index → name, from `/sys/class/net`.
86pub fn read_ifnames() -> BTreeMap<u32, String> {
87    let mut out = BTreeMap::new();
88    for e in std::fs::read_dir("/sys/class/net").into_iter().flatten().flatten() {
89        let name = e.file_name().to_string_lossy().into_owned();
90        if let Some(i) = std::fs::read_to_string(e.path().join("ifindex")).ok().and_then(|s| s.trim().parse().ok()) {
91            out.insert(i, name);
92        }
93    }
94    out
95}