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(
94    endpoint: &crate::varlink::endpoint::VarlinkEndpoint,
95) -> anyhow::Result<NetworkdState> {
96    let mut conn = endpoint.connect().await?;
97    let result = conn.describe().await?;
98    match result {
99        Ok(output) => {
100            let mut interfaces_state = Vec::new();
101            let mut managed_interfaces: u64 = 0;
102            for iface in output.interfaces.unwrap_or_default() {
103                // Only count interfaces that have a network configuration file –
104                // the same criterion used by the file-based parser.
105                if iface.network_file.is_none() {
106                    continue;
107                }
108                managed_interfaces += 1;
109                interfaces_state.push(map_interface(&iface));
110            }
111            Ok(NetworkdState {
112                interfaces_state,
113                managed_interfaces,
114            })
115        }
116        Err(e) => Err(anyhow::anyhow!("io.systemd.Network.Describe error: {}", e)),
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_map_interface_full() {
126        let iface = Interface {
127            index: 2,
128            name: "eth0".to_string(),
129            administrative_state: "configured".to_string(),
130            operational_state: "routable".to_string(),
131            carrier_state: "carrier".to_string(),
132            address_state: "routable".to_string(),
133            ipv4_address_state: "degraded".to_string(),
134            ipv6_address_state: "routable".to_string(),
135            online_state: Some("online".to_string()),
136            network_file: Some("/etc/systemd/network/69-eno4.network".to_string()),
137            required_for_online: Some(true),
138        };
139        let state = map_interface(&iface);
140        assert_eq!(state.name, "eth0");
141        assert_eq!(state.admin_state, AdminState::configured);
142        assert_eq!(state.oper_state, OperState::routable);
143        assert_eq!(state.carrier_state, CarrierState::carrier);
144        assert_eq!(state.address_state, AddressState::routable);
145        assert_eq!(state.ipv4_address_state, AddressState::degraded);
146        assert_eq!(state.ipv6_address_state, AddressState::routable);
147        assert_eq!(state.required_for_online, BoolState::True);
148        assert_eq!(
149            state.network_file,
150            "/etc/systemd/network/69-eno4.network".to_string()
151        );
152    }
153
154    #[test]
155    fn test_map_interface_unknown_states() {
156        let iface = Interface {
157            index: 3,
158            name: "wg0".to_string(),
159            administrative_state: "initialized".to_string(), // new state in systemd 257
160            operational_state: "unknown-oper".to_string(),
161            carrier_state: "no-carrier".to_string(),
162            address_state: "off".to_string(),
163            ipv4_address_state: "off".to_string(),
164            ipv6_address_state: "off".to_string(),
165            online_state: None,
166            network_file: Some("/etc/systemd/network/wg0.network".to_string()),
167            required_for_online: Some(false),
168        };
169        let state = map_interface(&iface);
170        assert_eq!(state.admin_state, AdminState::unknown);
171        assert_eq!(state.oper_state, OperState::unknown);
172        assert_eq!(state.carrier_state, CarrierState::no_carrier);
173        assert_eq!(state.address_state, AddressState::off);
174        assert_eq!(state.required_for_online, BoolState::False);
175    }
176
177    #[test]
178    fn test_map_interface_no_required_for_online() {
179        let iface = Interface {
180            index: 1,
181            name: "lo".to_string(),
182            administrative_state: "unmanaged".to_string(),
183            operational_state: "carrier".to_string(),
184            carrier_state: "carrier".to_string(),
185            address_state: "degraded".to_string(),
186            ipv4_address_state: "degraded".to_string(),
187            ipv6_address_state: "degraded".to_string(),
188            online_state: None,
189            network_file: None,
190            required_for_online: None,
191        };
192        let state = map_interface(&iface);
193        assert_eq!(state.required_for_online, BoolState::unknown);
194        assert_eq!(state.network_file, "");
195    }
196}