Skip to main content

monitord/
varlink_networkd.rs

1//! # varlink_networkd module
2//!
3//! Collection of systemd-networkd statistics via the `io.systemd.Network` varlink API.
4//! Available from systemd v257+.
5//!
6//! When enabled via `enable_varlink = true` in the `[networkd]` config section, monitord
7//! will attempt to collect interface states via varlink and fall back to parsing
8//! `/run/systemd/netif/links` state files on failure.
9
10use std::str::FromStr;
11
12use tracing::debug;
13
14use crate::networkd::{
15    AddressState, AdminState, BoolState, CarrierState, InterfaceState, NetworkdState, OperState,
16};
17use crate::varlink::network::{Interface, Network};
18
19pub use crate::varlink::network::NETWORK_SOCKET_PATH;
20
21/// Map a varlink [`Interface`] to our [`InterfaceState`] struct.
22fn map_interface(iface: &Interface) -> InterfaceState {
23    let address_state = AddressState::from_str(&iface.address_state).unwrap_or_else(|_| {
24        debug!("Unknown address_state value: {:?}", iface.address_state);
25        AddressState::unknown
26    });
27    let admin_state = AdminState::from_str(&iface.administrative_state).unwrap_or_else(|_| {
28        debug!(
29            "Unknown administrative_state value: {:?}",
30            iface.administrative_state
31        );
32        AdminState::unknown
33    });
34    let carrier_state = CarrierState::from_str(&iface.carrier_state).unwrap_or_else(|_| {
35        debug!("Unknown carrier_state value: {:?}", iface.carrier_state);
36        CarrierState::unknown
37    });
38    let ipv4_address_state =
39        AddressState::from_str(&iface.ipv4_address_state).unwrap_or_else(|_| {
40            debug!(
41                "Unknown ipv4_address_state value: {:?}",
42                iface.ipv4_address_state
43            );
44            AddressState::unknown
45        });
46    let ipv6_address_state =
47        AddressState::from_str(&iface.ipv6_address_state).unwrap_or_else(|_| {
48            debug!(
49                "Unknown ipv6_address_state value: {:?}",
50                iface.ipv6_address_state
51            );
52            AddressState::unknown
53        });
54    let oper_state = OperState::from_str(&iface.operational_state).unwrap_or_else(|_| {
55        debug!(
56            "Unknown operational_state value: {:?}",
57            iface.operational_state
58        );
59        OperState::unknown
60    });
61    let required_for_online = match iface.required_for_online {
62        Some(true) => BoolState::True,
63        Some(false) => BoolState::False,
64        None => BoolState::unknown,
65    };
66    let network_file = iface.network_file.clone().unwrap_or_default();
67
68    InterfaceState {
69        address_state,
70        admin_state,
71        carrier_state,
72        ipv4_address_state,
73        ipv6_address_state,
74        name: iface.name.clone(),
75        network_file,
76        oper_state,
77        required_for_online,
78    }
79}
80
81/// Collect networkd interface stats via the `io.systemd.Network` varlink `Describe` method.
82///
83/// No `spawn_blocking` here, unlike the streaming `io.systemd.Metrics.List`
84/// call in `varlink_units`: a single-shot zlink call holds no `!Send` stream
85/// across an await, so it runs on the main runtime like any other future.
86///
87/// NOTE: measured ~15ms slower than the file-based fallback on a 35-interface
88/// host (systemd 259): `Describe` returns a ~112KB payload — every interface
89/// plus the full route/nexthop/policy tables, which monitord discards — while
90/// the fallback just reads the small state files in `/run/systemd/netif/links`.
91/// The cost is daemon-side serialization, so nothing client-side avoids it;
92/// varlink still wins on authority (one RPC vs. scraping daemon-private files).
93pub async fn get_networkd_state(socket_path: &str) -> anyhow::Result<NetworkdState> {
94    let mut conn = zlink::unix::connect(socket_path).await?;
95    let result = conn.describe().await?;
96    match result {
97        Ok(output) => {
98            let mut interfaces_state = Vec::new();
99            let mut managed_interfaces: u64 = 0;
100            for iface in output.interfaces.unwrap_or_default() {
101                // Only count interfaces that have a network configuration file –
102                // the same criterion used by the file-based parser.
103                if iface.network_file.is_none() {
104                    continue;
105                }
106                managed_interfaces += 1;
107                interfaces_state.push(map_interface(&iface));
108            }
109            Ok(NetworkdState {
110                interfaces_state,
111                managed_interfaces,
112            })
113        }
114        Err(e) => Err(anyhow::anyhow!("io.systemd.Network.Describe error: {}", e)),
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_map_interface_full() {
124        let iface = Interface {
125            index: 2,
126            name: "eth0".to_string(),
127            administrative_state: "configured".to_string(),
128            operational_state: "routable".to_string(),
129            carrier_state: "carrier".to_string(),
130            address_state: "routable".to_string(),
131            ipv4_address_state: "degraded".to_string(),
132            ipv6_address_state: "routable".to_string(),
133            online_state: Some("online".to_string()),
134            network_file: Some("/etc/systemd/network/69-eno4.network".to_string()),
135            required_for_online: Some(true),
136        };
137        let state = map_interface(&iface);
138        assert_eq!(state.name, "eth0");
139        assert_eq!(state.admin_state, AdminState::configured);
140        assert_eq!(state.oper_state, OperState::routable);
141        assert_eq!(state.carrier_state, CarrierState::carrier);
142        assert_eq!(state.address_state, AddressState::routable);
143        assert_eq!(state.ipv4_address_state, AddressState::degraded);
144        assert_eq!(state.ipv6_address_state, AddressState::routable);
145        assert_eq!(state.required_for_online, BoolState::True);
146        assert_eq!(
147            state.network_file,
148            "/etc/systemd/network/69-eno4.network".to_string()
149        );
150    }
151
152    #[test]
153    fn test_map_interface_unknown_states() {
154        let iface = Interface {
155            index: 3,
156            name: "wg0".to_string(),
157            administrative_state: "initialized".to_string(), // new state in systemd 257
158            operational_state: "unknown-oper".to_string(),
159            carrier_state: "no-carrier".to_string(),
160            address_state: "off".to_string(),
161            ipv4_address_state: "off".to_string(),
162            ipv6_address_state: "off".to_string(),
163            online_state: None,
164            network_file: Some("/etc/systemd/network/wg0.network".to_string()),
165            required_for_online: Some(false),
166        };
167        let state = map_interface(&iface);
168        assert_eq!(state.admin_state, AdminState::unknown);
169        assert_eq!(state.oper_state, OperState::unknown);
170        assert_eq!(state.carrier_state, CarrierState::no_carrier);
171        assert_eq!(state.address_state, AddressState::off);
172        assert_eq!(state.required_for_online, BoolState::False);
173    }
174
175    #[test]
176    fn test_map_interface_no_required_for_online() {
177        let iface = Interface {
178            index: 1,
179            name: "lo".to_string(),
180            administrative_state: "unmanaged".to_string(),
181            operational_state: "carrier".to_string(),
182            carrier_state: "carrier".to_string(),
183            address_state: "degraded".to_string(),
184            ipv4_address_state: "degraded".to_string(),
185            ipv6_address_state: "degraded".to_string(),
186            online_state: None,
187            network_file: None,
188            required_for_online: None,
189        };
190        let state = map_interface(&iface);
191        assert_eq!(state.required_for_online, BoolState::unknown);
192        assert_eq!(state.network_file, "");
193    }
194}