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