monitord/
networkd.rs

1//! # networkd module
2//!
3//! All structs, enums and methods specific to systemd-networkd.
4//! Enumerations were copied from <https://github.com/systemd/systemd/blob/main/src/libsystemd/sd-network/network-util.h>
5
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::sync::Arc;
10
11use anyhow::Result;
12use int_enum::IntEnum;
13use serde_repr::*;
14use strum_macros::EnumIter;
15use strum_macros::EnumString;
16use tokio::sync::RwLock;
17use tracing::error;
18
19use crate::MachineStats;
20
21/// Enumeration of networkd address states
22#[allow(non_camel_case_types)]
23#[derive(
24    Serialize_repr,
25    Deserialize_repr,
26    Clone,
27    Copy,
28    Debug,
29    Default,
30    Eq,
31    PartialEq,
32    EnumIter,
33    EnumString,
34    IntEnum,
35    strum_macros::Display,
36)]
37#[repr(u8)]
38pub enum AddressState {
39    #[default]
40    unknown = 0,
41    off = 1,
42    degraded = 2,
43    routable = 3,
44}
45
46/// Enumeration of interface administratve states
47#[allow(non_camel_case_types)]
48#[derive(
49    Serialize_repr,
50    Deserialize_repr,
51    Clone,
52    Copy,
53    Debug,
54    Default,
55    Eq,
56    PartialEq,
57    EnumIter,
58    EnumString,
59    IntEnum,
60    strum_macros::Display,
61)]
62#[repr(u8)]
63pub enum AdminState {
64    #[default]
65    unknown = 0,
66    pending = 1,
67    failed = 2,
68    configuring = 3,
69    configured = 4,
70    unmanaged = 5,
71    linger = 6,
72}
73
74/// Enumeration of a true (yes) / false (no) options - e.g. required for online
75#[allow(non_camel_case_types)]
76#[derive(
77    Serialize_repr,
78    Deserialize_repr,
79    Clone,
80    Copy,
81    Debug,
82    Default,
83    Eq,
84    PartialEq,
85    EnumIter,
86    EnumString,
87    IntEnum,
88    strum_macros::Display,
89)]
90#[repr(u8)]
91pub enum BoolState {
92    #[default]
93    unknown = u8::MAX,
94    #[strum(
95        serialize = "false",
96        serialize = "False",
97        serialize = "no",
98        serialize = "No"
99    )]
100    False = 0,
101    #[strum(
102        serialize = "true",
103        serialize = "True",
104        serialize = "yes",
105        serialize = "Yes"
106    )]
107    True = 1,
108}
109
110/// Enumeration of networkd physical signal / state of interfaces
111#[allow(non_camel_case_types)]
112#[derive(
113    Serialize_repr,
114    Deserialize_repr,
115    Clone,
116    Copy,
117    Debug,
118    Default,
119    Eq,
120    PartialEq,
121    EnumIter,
122    EnumString,
123    IntEnum,
124    strum_macros::Display,
125)]
126#[repr(u8)]
127pub enum CarrierState {
128    #[default]
129    unknown = 0,
130    off = 1,
131    #[strum(serialize = "no-carrier", serialize = "no_carrier")]
132    no_carrier = 2,
133    dormant = 3,
134    #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
135    degraded_carrier = 4,
136    carrier = 5,
137    enslaved = 6,
138}
139
140/// Enumeration of the networkd online state
141#[allow(non_camel_case_types)]
142#[derive(
143    Serialize_repr,
144    Deserialize_repr,
145    Clone,
146    Copy,
147    Debug,
148    Default,
149    Eq,
150    PartialEq,
151    EnumIter,
152    EnumString,
153    IntEnum,
154    strum_macros::Display,
155)]
156#[repr(u8)]
157pub enum OnlineState {
158    #[default]
159    unknown = 0,
160    offline = 1,
161    partial = 2,
162    online = 3,
163}
164
165/// Enumeration of networkd's operational state
166#[allow(non_camel_case_types)]
167#[derive(
168    Serialize_repr,
169    Deserialize_repr,
170    Clone,
171    Copy,
172    Debug,
173    Default,
174    Eq,
175    PartialEq,
176    EnumIter,
177    EnumString,
178    IntEnum,
179    strum_macros::Display,
180)]
181#[repr(u8)]
182pub enum OperState {
183    #[default]
184    unknown = 0,
185    missing = 1,
186    off = 2,
187    #[strum(serialize = "no-carrier", serialize = "no_carrier")]
188    no_carrier = 3,
189    dormant = 4,
190    #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
191    degraded_carrier = 5,
192    carrier = 6,
193    degraded = 7,
194    enslaved = 8,
195    routable = 9,
196}
197
198/// Main per interface networkd state structure
199#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
200pub struct InterfaceState {
201    pub address_state: AddressState,
202    pub admin_state: AdminState,
203    pub carrier_state: CarrierState,
204    pub ipv4_address_state: AddressState,
205    pub ipv6_address_state: AddressState,
206    pub name: String,
207    pub network_file: String,
208    pub oper_state: OperState,
209    pub required_for_online: BoolState,
210}
211
212/// Get interface id + name from dbus list_links API
213async fn get_interface_links(
214    connection: &zbus::Connection,
215) -> Result<HashMap<i32, String>, Box<dyn std::error::Error + Send + Sync>> {
216    let p = crate::dbus::zbus_networkd::ManagerProxy::new(connection).await?;
217    let links = p.list_links().await?;
218    let mut link_int_to_name: HashMap<i32, String> = HashMap::new();
219    for network_link in links {
220        link_int_to_name.insert(network_link.0, network_link.1);
221    }
222    Ok(link_int_to_name)
223}
224
225/// Main networkd structure with per interface state and a count of managed interfaces
226#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
227pub struct NetworkdState {
228    pub interfaces_state: Vec<InterfaceState>,
229    pub managed_interfaces: u64,
230}
231
232pub const NETWORKD_STATE_FILES: &str = "/run/systemd/netif/links";
233
234/// Parse a networkd state file contents + convert int ID to name via DBUS
235pub fn parse_interface_stats(
236    interface_state_str: String,
237    interface_id: i32,
238    interface_id_to_name: &HashMap<i32, String>,
239) -> Result<InterfaceState, String> {
240    let mut interface_state = InterfaceState::default();
241
242    for line in interface_state_str.lines() {
243        // Skip comments + lines without =
244        if !line.contains('=') {
245            continue;
246        }
247
248        // Pull interface name out of list_links generated HashMap
249        if interface_id > 0 {
250            interface_state.name = interface_id_to_name
251                .get(&interface_id)
252                .unwrap_or(&String::from(""))
253                .to_string();
254        }
255
256        let (key, value) = line
257            .split_once('=')
258            .expect("Unable to split a network state line");
259        match key {
260            "ADDRESS_STATE" => {
261                interface_state.address_state =
262                    AddressState::from_str(value).unwrap_or(AddressState::unknown)
263            }
264            "ADMIN_STATE" => {
265                interface_state.admin_state =
266                    AdminState::from_str(value).unwrap_or(AdminState::unknown)
267            }
268            "CARRIER_STATE" => {
269                interface_state.carrier_state =
270                    CarrierState::from_str(value).unwrap_or(CarrierState::unknown)
271            }
272            "IPV4_ADDRESS_STATE" => {
273                interface_state.ipv4_address_state =
274                    AddressState::from_str(value).unwrap_or(AddressState::unknown)
275            }
276            "IPV6_ADDRESS_STATE" => {
277                interface_state.ipv6_address_state =
278                    AddressState::from_str(value).unwrap_or(AddressState::unknown)
279            }
280            "NETWORK_FILE" => interface_state.network_file = value.to_string(),
281            "OPER_STATE" => {
282                interface_state.oper_state =
283                    OperState::from_str(value).unwrap_or(OperState::unknown)
284            }
285            "REQUIRED_FOR_ONLINE" => {
286                interface_state.required_for_online =
287                    BoolState::from_str(value).unwrap_or(BoolState::unknown)
288            }
289            _ => continue,
290        };
291    }
292
293    Ok(interface_state)
294}
295
296/// Parse interface state files in directory supplied
297pub async fn parse_interface_state_files(
298    states_path: &PathBuf,
299    maybe_network_int_to_name: Option<HashMap<i32, String>>,
300    maybe_connection: Option<&zbus::Connection>,
301) -> Result<NetworkdState, std::io::Error> {
302    let mut managed_interface_count: u64 = 0;
303    let mut interfaces_state = vec![];
304
305    let network_int_to_name = match maybe_network_int_to_name {
306        None => {
307            if let Some(connection) = maybe_connection {
308                match get_interface_links(connection).await {
309                    Ok(hashmap) => hashmap,
310                    Err(err) => {
311                        error!(
312                            "Unable to get interface links via DBUS - is networkd running?: {:#?}",
313                            err
314                        );
315                        return Ok(NetworkdState::default());
316                    }
317                }
318            } else {
319                error!(
320                    "Unable to get interface links via DBUS and no network_int_to_name supplied"
321                );
322                return Ok(NetworkdState::default());
323            }
324        }
325        Some(valid_hashmap) => valid_hashmap,
326    };
327
328    let mut state_file_dir_entries = tokio::fs::read_dir(states_path).await?;
329    while let Some(state_file) = state_file_dir_entries.next_entry().await? {
330        if !state_file.path().is_file() {
331            continue;
332        }
333        let interface_stats_file_str = tokio::fs::read_to_string(state_file.path()).await?;
334        if !interface_stats_file_str.contains("NETWORK_FILE") {
335            continue;
336        }
337        managed_interface_count += 1;
338        let fname = state_file.file_name();
339        let interface_id: i32 = i32::from_str(fname.to_str().unwrap_or("0")).unwrap_or(0);
340        match parse_interface_stats(interface_stats_file_str, interface_id, &network_int_to_name) {
341            Ok(interface_state) => interfaces_state.push(interface_state),
342            Err(err) => error!(
343                "Unable to parse interface statistics for {:?}: {}",
344                state_file.path().into_os_string(),
345                err
346            ),
347        }
348    }
349    Ok(NetworkdState {
350        interfaces_state,
351        managed_interfaces: managed_interface_count,
352    })
353}
354
355/// Async wrapper than can update networkd stats when passed a locked struct
356pub async fn update_networkd_stats(
357    states_path: PathBuf,
358    maybe_network_int_to_name: Option<HashMap<i32, String>>,
359    connection: zbus::Connection,
360    locked_machine_stats: Arc<RwLock<MachineStats>>,
361) -> anyhow::Result<()> {
362    match parse_interface_state_files(&states_path, maybe_network_int_to_name, Some(&connection))
363        .await
364    {
365        Ok(networkd_stats) => {
366            let mut machine_stats = locked_machine_stats.write().await;
367            machine_stats.networkd = networkd_stats;
368        }
369        Err(err) => error!("networkd stats failed: {:?}", err),
370    }
371    Ok(())
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377    use std::fs::File;
378    use std::io::Write;
379    use tempfile::tempdir;
380
381    const MOCK_INTERFACE_STATE: &str = r###"# This is private data. Do not parse.
382ADMIN_STATE=configured
383OPER_STATE=routable
384CARRIER_STATE=carrier
385ADDRESS_STATE=routable
386IPV4_ADDRESS_STATE=degraded
387IPV6_ADDRESS_STATE=routable
388ONLINE_STATE=online
389REQUIRED_FOR_ONLINE=yes
390REQUIRED_OPER_STATE_FOR_ONLINE=degraded:routable
391REQUIRED_FAMILY_FOR_ONLINE=any
392ACTIVATION_POLICY=up
393NETWORK_FILE=/etc/systemd/network/69-eno4.network
394NETWORK_FILE_DROPINS=""
395DNS=8.8.8.8 8.8.4.4
396NTP=
397SIP=
398DOMAINS=
399ROUTE_DOMAINS=
400LLMNR=yes
401MDNS=no
402"###;
403
404    fn return_expected_interface_state() -> InterfaceState {
405        InterfaceState {
406            address_state: AddressState::routable,
407            admin_state: AdminState::configured,
408            carrier_state: CarrierState::carrier,
409            ipv4_address_state: AddressState::degraded,
410            ipv6_address_state: AddressState::routable,
411            name: "eth0".to_string(),
412            network_file: "/etc/systemd/network/69-eno4.network".to_string(),
413            oper_state: OperState::routable,
414            required_for_online: BoolState::True,
415        }
416    }
417
418    fn return_mock_int_name_hashmap() -> Option<HashMap<i32, String>> {
419        let mut h: HashMap<i32, String> = HashMap::new();
420        h.insert(2, String::from("eth0"));
421        h.insert(69, String::from("eth69"));
422        Some(h)
423    }
424
425    #[test]
426    fn test_parse_interface_stats() {
427        assert_eq!(
428            return_expected_interface_state(),
429            parse_interface_stats(
430                MOCK_INTERFACE_STATE.to_string(),
431                2,
432                &return_mock_int_name_hashmap().expect("Failed to get a mock int name hashmap"),
433            )
434            .expect("Failed to parse interface stats"),
435        );
436    }
437
438    #[test]
439    fn test_parse_interface_stats_json() {
440        // 'name' stays as an empty string cause we don't pass in networkctl json or an interface id
441        let expected_interface_state_json = r###"{"address_state":3,"admin_state":4,"carrier_state":5,"ipv4_address_state":2,"ipv6_address_state":3,"name":"","network_file":"/etc/systemd/network/69-eno4.network","oper_state":9,"required_for_online":1}"###;
442        let stats =
443            parse_interface_stats(MOCK_INTERFACE_STATE.to_string(), 0, &HashMap::new()).unwrap();
444        let stats_json = serde_json::to_string(&stats).unwrap();
445        assert_eq!(expected_interface_state_json.to_string(), stats_json);
446    }
447
448    #[tokio::test]
449    async fn test_parse_interface_state_files() -> Result<()> {
450        let expected_files = NetworkdState {
451            interfaces_state: vec![return_expected_interface_state()],
452            managed_interfaces: 1,
453        };
454
455        let temp_dir = tempdir()?;
456        // Filename of '2' is important as it needs to correspond to the interface id / index
457        let file_path = temp_dir.path().join("2");
458        let mut state_file = File::create(file_path)?;
459        writeln!(state_file, "{}", MOCK_INTERFACE_STATE)?;
460
461        let path = PathBuf::from(temp_dir.path());
462        assert_eq!(
463            expected_files,
464            parse_interface_state_files(
465                &path,
466                return_mock_int_name_hashmap(),
467                None, // No DBUS in tests
468            )
469            .await
470            .expect("Problem with parsing interface stte files")
471        );
472        Ok(())
473    }
474
475    #[test]
476    fn test_enums_to_ints() -> Result<()> {
477        assert_eq!(3, AddressState::routable as u64);
478        let carrier_state_int: u8 = u8::from(CarrierState::degraded_carrier);
479        assert_eq!(4, carrier_state_int);
480        assert_eq!(1, BoolState::True as i64);
481        let bool_state_false_int: u8 = u8::from(BoolState::False);
482        assert_eq!(0, bool_state_false_int);
483
484        Ok(())
485    }
486}