1use std::collections::HashMap;
7use std::path::Path;
8use std::path::PathBuf;
9use std::str::FromStr;
10use std::sync::Arc;
11
12use int_enum::IntEnum;
13use serde_repr::*;
14use strum_macros::EnumIter;
15use strum_macros::EnumString;
16use thiserror::Error;
17use tokio::sync::RwLock;
18use tracing::debug;
19use tracing::error;
20
21use crate::MachineStats;
22
23#[derive(Error, Debug)]
24pub enum MonitordNetworkdError {
25 #[error("Networkd D-Bus error: {0}")]
26 ZbusError(#[from] zbus::Error),
27 #[error("IO error: {0}")]
28 IoError(#[from] std::io::Error),
29}
30
31#[allow(non_camel_case_types)]
34#[derive(
35 Serialize_repr,
36 Deserialize_repr,
37 Clone,
38 Copy,
39 Debug,
40 Default,
41 Eq,
42 PartialEq,
43 EnumIter,
44 EnumString,
45 IntEnum,
46 strum_macros::Display,
47)]
48#[repr(u8)]
49pub enum AddressState {
50 #[default]
52 unknown = 0,
53 off = 1,
55 degraded = 2,
57 routable = 3,
59}
60
61#[allow(non_camel_case_types)]
64#[derive(
65 Serialize_repr,
66 Deserialize_repr,
67 Clone,
68 Copy,
69 Debug,
70 Default,
71 Eq,
72 PartialEq,
73 EnumIter,
74 EnumString,
75 IntEnum,
76 strum_macros::Display,
77)]
78#[repr(u8)]
79pub enum AdminState {
80 #[default]
82 unknown = 0,
83 pending = 1,
85 failed = 2,
87 configuring = 3,
89 configured = 4,
91 unmanaged = 5,
93 linger = 6,
95}
96
97#[allow(non_camel_case_types)]
99#[derive(
100 Serialize_repr,
101 Deserialize_repr,
102 Clone,
103 Copy,
104 Debug,
105 Default,
106 Eq,
107 PartialEq,
108 EnumIter,
109 EnumString,
110 IntEnum,
111 strum_macros::Display,
112)]
113#[repr(u8)]
114pub enum BoolState {
115 #[default]
116 unknown = u8::MAX,
117 #[strum(
118 serialize = "false",
119 serialize = "False",
120 serialize = "no",
121 serialize = "No"
122 )]
123 False = 0,
124 #[strum(
125 serialize = "true",
126 serialize = "True",
127 serialize = "yes",
128 serialize = "Yes"
129 )]
130 True = 1,
131}
132
133#[allow(non_camel_case_types)]
136#[derive(
137 Serialize_repr,
138 Deserialize_repr,
139 Clone,
140 Copy,
141 Debug,
142 Default,
143 Eq,
144 PartialEq,
145 EnumIter,
146 EnumString,
147 IntEnum,
148 strum_macros::Display,
149)]
150#[repr(u8)]
151pub enum CarrierState {
152 #[default]
154 unknown = 0,
155 off = 1,
157 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
159 no_carrier = 2,
160 dormant = 3,
162 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
164 degraded_carrier = 4,
165 carrier = 5,
167 enslaved = 6,
169}
170
171#[allow(non_camel_case_types)]
174#[derive(
175 Serialize_repr,
176 Deserialize_repr,
177 Clone,
178 Copy,
179 Debug,
180 Default,
181 Eq,
182 PartialEq,
183 EnumIter,
184 EnumString,
185 IntEnum,
186 strum_macros::Display,
187)]
188#[repr(u8)]
189pub enum OnlineState {
190 #[default]
192 unknown = 0,
193 offline = 1,
195 partial = 2,
197 online = 3,
199}
200
201#[allow(non_camel_case_types)]
204#[derive(
205 Serialize_repr,
206 Deserialize_repr,
207 Clone,
208 Copy,
209 Debug,
210 Default,
211 Eq,
212 PartialEq,
213 EnumIter,
214 EnumString,
215 IntEnum,
216 strum_macros::Display,
217)]
218#[repr(u8)]
219pub enum OperState {
220 #[default]
222 unknown = 0,
223 missing = 1,
225 off = 2,
227 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
229 no_carrier = 3,
230 dormant = 4,
232 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
234 degraded_carrier = 5,
235 carrier = 6,
237 degraded = 7,
239 enslaved = 8,
241 routable = 9,
243}
244
245#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
247pub struct InterfaceState {
248 pub address_state: AddressState,
250 pub admin_state: AdminState,
252 pub carrier_state: CarrierState,
254 pub ipv4_address_state: AddressState,
256 pub ipv6_address_state: AddressState,
258 pub name: String,
260 pub network_file: String,
262 pub oper_state: OperState,
264 pub required_for_online: BoolState,
266}
267
268async fn get_interface_links(
274 connection: &zbus::Connection,
275) -> Result<HashMap<i32, String>, MonitordNetworkdError> {
276 let p = crate::dbus::zbus_networkd::ManagerProxy::builder(connection)
277 .cache_properties(zbus::proxy::CacheProperties::No)
278 .build()
279 .await?;
280 let links = p.list_links().await?;
281 let mut link_int_to_name: HashMap<i32, String> = HashMap::new();
282 for network_link in links {
283 link_int_to_name.insert(network_link.0, network_link.1);
284 }
285 Ok(link_int_to_name)
286}
287
288pub(crate) async fn read_ifindex_map(sysfs_root: &Path) -> HashMap<i32, String> {
300 let mut map = HashMap::new();
301 let mut dir = match tokio::fs::read_dir(sysfs_root.join("class/net")).await {
302 Ok(dir) => dir,
303 Err(err) => {
304 debug!(
305 "Unable to read {}/class/net: {:?}",
306 sysfs_root.display(),
307 err
308 );
309 return map;
310 }
311 };
312 while let Ok(Some(entry)) = dir.next_entry().await {
313 let kind = entry.file_type().await;
322 if !kind.is_ok_and(|kind| kind.is_symlink() || kind.is_dir()) {
323 continue;
324 }
325 let name = entry.file_name().to_string_lossy().into_owned();
326 match tokio::fs::read_to_string(entry.path().join("ifindex")).await {
327 Ok(contents) => match contents.trim().parse::<i32>() {
328 Ok(ifindex) => {
329 map.insert(ifindex, name);
330 }
331 Err(err) => debug!("Unable to parse ifindex for interface {}: {:?}", name, err),
335 },
336 Err(err) => debug!("Unable to read ifindex for interface {}: {:?}", name, err),
337 }
338 }
339 map
340}
341
342#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
344pub struct NetworkdState {
345 pub interfaces_state: Vec<InterfaceState>,
347 pub managed_interfaces: u64,
349}
350
351pub const NETWORKD_STATE_FILES: &str = "/run/systemd/netif/links";
352
353pub fn parse_interface_stats(
355 interface_state_str: &str,
356 interface_id: i32,
357 interface_id_to_name: &HashMap<i32, String>,
358) -> Result<InterfaceState, MonitordNetworkdError> {
359 let mut interface_state = InterfaceState::default();
360
361 if interface_id > 0 {
363 if let Some(name) = interface_id_to_name.get(&interface_id) {
364 interface_state.name = name.clone();
365 }
366 }
367
368 for line in interface_state_str.lines() {
369 if !line.contains('=') {
371 continue;
372 }
373
374 let (key, value) = line
375 .split_once('=')
376 .expect("Unable to split a network state line");
377 match key {
378 "ADDRESS_STATE" => {
379 interface_state.address_state =
380 AddressState::from_str(value).unwrap_or(AddressState::unknown)
381 }
382 "ADMIN_STATE" => {
383 interface_state.admin_state =
384 AdminState::from_str(value).unwrap_or(AdminState::unknown)
385 }
386 "CARRIER_STATE" => {
387 interface_state.carrier_state =
388 CarrierState::from_str(value).unwrap_or(CarrierState::unknown)
389 }
390 "IPV4_ADDRESS_STATE" => {
391 interface_state.ipv4_address_state =
392 AddressState::from_str(value).unwrap_or(AddressState::unknown)
393 }
394 "IPV6_ADDRESS_STATE" => {
395 interface_state.ipv6_address_state =
396 AddressState::from_str(value).unwrap_or(AddressState::unknown)
397 }
398 "NETWORK_FILE" => interface_state.network_file = value.to_string(),
399 "OPER_STATE" => {
400 interface_state.oper_state =
401 OperState::from_str(value).unwrap_or(OperState::unknown)
402 }
403 "REQUIRED_FOR_ONLINE" => {
404 interface_state.required_for_online =
405 BoolState::from_str(value).unwrap_or(BoolState::unknown)
406 }
407 _ => continue,
408 };
409 }
410
411 Ok(interface_state)
412}
413
414pub async fn parse_interface_state_files(
422 states_path: &PathBuf,
423 maybe_network_int_to_name: Option<HashMap<i32, String>>,
424 sysfs_root: &Path,
425 maybe_dbus: Option<(&crate::DbusCell, u64)>,
426) -> Result<NetworkdState, MonitordNetworkdError> {
427 let mut managed_interface_count: u64 = 0;
428 let mut interfaces_state = vec![];
429
430 let network_int_to_name = match maybe_network_int_to_name {
431 Some(valid_hashmap) => valid_hashmap,
432 None => {
433 let sysfs_map = read_ifindex_map(sysfs_root).await;
434 if !sysfs_map.is_empty() {
435 sysfs_map
436 } else if let Some((dbus, dbus_timeout)) = maybe_dbus {
437 let connection = crate::dbus_connection(dbus, dbus_timeout)
442 .await
443 .map_err(|e| MonitordNetworkdError::ZbusError(e.into_zbus()))?;
444 match get_interface_links(&connection).await {
445 Ok(hashmap) => hashmap,
446 Err(err) => {
447 error!(
448 "Unable to get interface links via DBUS - is networkd running?: {:#?}",
449 err
450 );
451 return Ok(NetworkdState::default());
452 }
453 }
454 } else {
455 error!(
456 "Unable to map interface ids to names: sysfs gave no entries and no D-Bus cell supplied"
457 );
458 return Ok(NetworkdState::default());
459 }
460 }
461 };
462
463 let mut state_file_dir_entries = tokio::fs::read_dir(states_path).await?;
464 while let Some(state_file) = state_file_dir_entries.next_entry().await? {
465 if !state_file.path().is_file() {
466 continue;
467 }
468 let interface_stats_file_str = tokio::fs::read_to_string(state_file.path()).await?;
469 if !interface_stats_file_str.contains("NETWORK_FILE") {
470 continue;
471 }
472 managed_interface_count += 1;
473 let fname = state_file.file_name();
474 let interface_id: i32 = i32::from_str(fname.to_str().unwrap_or("0")).unwrap_or(0);
475 match parse_interface_stats(
476 &interface_stats_file_str,
477 interface_id,
478 &network_int_to_name,
479 ) {
480 Ok(interface_state) => interfaces_state.push(interface_state),
481 Err(err) => error!(
482 "Unable to parse interface statistics for {:?}: {}",
483 state_file.path().into_os_string(),
484 err
485 ),
486 }
487 }
488 Ok(NetworkdState {
489 interfaces_state,
490 managed_interfaces: managed_interface_count,
491 })
492}
493
494pub async fn update_networkd_stats(
503 states_path: PathBuf,
504 maybe_network_int_to_name: Option<HashMap<i32, String>>,
505 sysfs_root: PathBuf,
506 maybe_dbus: Option<(crate::DbusCell, u64)>,
507 locked_machine_stats: Arc<RwLock<MachineStats>>,
508) -> anyhow::Result<()> {
509 let maybe_dbus_ref = maybe_dbus.as_ref().map(|(cell, timeout)| (cell, *timeout));
513 match parse_interface_state_files(
514 &states_path,
515 maybe_network_int_to_name,
516 &sysfs_root,
517 maybe_dbus_ref,
518 )
519 .await
520 {
521 Ok(networkd_stats) => {
522 let mut machine_stats = locked_machine_stats.write().await;
523 machine_stats.networkd = networkd_stats;
524 }
525 Err(err) => error!("networkd stats failed: {:?}", err),
526 }
527 Ok(())
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::fs::File;
534 use std::io::Write;
535 use tempfile::tempdir;
536
537 const MOCK_INTERFACE_STATE: &str = r###"# This is private data. Do not parse.
538ADMIN_STATE=configured
539OPER_STATE=routable
540CARRIER_STATE=carrier
541ADDRESS_STATE=routable
542IPV4_ADDRESS_STATE=degraded
543IPV6_ADDRESS_STATE=routable
544ONLINE_STATE=online
545REQUIRED_FOR_ONLINE=yes
546REQUIRED_OPER_STATE_FOR_ONLINE=degraded:routable
547REQUIRED_FAMILY_FOR_ONLINE=any
548ACTIVATION_POLICY=up
549NETWORK_FILE=/etc/systemd/network/69-eno4.network
550NETWORK_FILE_DROPINS=""
551DNS=8.8.8.8 8.8.4.4
552NTP=
553SIP=
554DOMAINS=
555ROUTE_DOMAINS=
556LLMNR=yes
557MDNS=no
558"###;
559
560 fn return_expected_interface_state() -> InterfaceState {
561 InterfaceState {
562 address_state: AddressState::routable,
563 admin_state: AdminState::configured,
564 carrier_state: CarrierState::carrier,
565 ipv4_address_state: AddressState::degraded,
566 ipv6_address_state: AddressState::routable,
567 name: "eth0".to_string(),
568 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
569 oper_state: OperState::routable,
570 required_for_online: BoolState::True,
571 }
572 }
573
574 fn return_mock_int_name_hashmap() -> Option<HashMap<i32, String>> {
575 let mut h: HashMap<i32, String> = HashMap::new();
576 h.insert(2, String::from("eth0"));
577 h.insert(69, String::from("eth69"));
578 Some(h)
579 }
580
581 #[test]
582 fn test_parse_interface_stats() {
583 assert_eq!(
584 return_expected_interface_state(),
585 parse_interface_stats(
586 MOCK_INTERFACE_STATE,
587 2,
588 &return_mock_int_name_hashmap().expect("Failed to get a mock int name hashmap"),
589 )
590 .expect("Failed to parse interface stats"),
591 );
592 }
593
594 #[test]
595 fn test_parse_interface_stats_json() {
596 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}"###;
598 let stats = parse_interface_stats(MOCK_INTERFACE_STATE, 0, &HashMap::new()).unwrap();
599 let stats_json = serde_json::to_string(&stats).unwrap();
600 assert_eq!(expected_interface_state_json.to_string(), stats_json);
601 }
602
603 #[tokio::test]
604 async fn test_parse_interface_state_files() -> Result<(), MonitordNetworkdError> {
605 let expected_files = NetworkdState {
606 interfaces_state: vec![return_expected_interface_state()],
607 managed_interfaces: 1,
608 };
609
610 let temp_dir = tempdir()?;
611 let file_path = temp_dir.path().join("2");
613 let mut state_file = File::create(file_path)?;
614 writeln!(state_file, "{}", MOCK_INTERFACE_STATE)?;
615
616 let path = PathBuf::from(temp_dir.path());
617 assert_eq!(
618 expected_files,
619 parse_interface_state_files(
620 &path,
621 return_mock_int_name_hashmap(),
622 Path::new("/nonexistent-sysfs"),
623 None, )
625 .await
626 .expect("Problem with parsing interface stte files")
627 );
628 Ok(())
629 }
630
631 #[tokio::test]
632 async fn test_read_ifindex_map_from_fake_sysfs() {
633 let temp_dir = tempdir().expect("temp dir");
637 let class_net = temp_dir.path().join("class/net");
638 std::fs::create_dir_all(&class_net).expect("create class dir");
639 let devices = temp_dir.path().join("devices");
640 for (name, ifindex) in [("eth0", "2\n"), ("wlan0", "3\n")] {
641 let target = devices.join(name);
642 std::fs::create_dir_all(&target).expect("create fake device dir");
643 std::fs::write(target.join("ifindex"), ifindex).expect("write fake ifindex");
644 std::os::unix::fs::symlink(&target, class_net.join(name)).expect("link iface");
645 }
646 std::fs::write(class_net.join("bonding_masters"), "").expect("write regular file");
648
649 let map = read_ifindex_map(temp_dir.path()).await;
650 assert_eq!(map.get(&2), Some(&"eth0".to_string()));
651 assert_eq!(map.get(&3), Some(&"wlan0".to_string()));
652 assert_eq!(map.len(), 2);
653 }
654
655 #[tokio::test]
656 async fn test_parse_interface_state_files_uses_sysfs_map() {
657 let temp_dir = tempdir().expect("temp dir");
660 let links_dir = temp_dir.path().join("links");
661 std::fs::create_dir_all(&links_dir).expect("create links dir");
662 let mut state_file = File::create(links_dir.join("2")).expect("create state file");
663 writeln!(state_file, "{}", MOCK_INTERFACE_STATE).expect("write state file");
664 let target = temp_dir.path().join("devices/eth0");
666 std::fs::create_dir_all(&target).expect("create device dir");
667 std::fs::write(target.join("ifindex"), "2\n").expect("write ifindex");
668 std::fs::create_dir_all(temp_dir.path().join("class/net")).expect("create class dir");
669 std::os::unix::fs::symlink(&target, temp_dir.path().join("class/net/eth0"))
670 .expect("link iface");
671
672 let state = parse_interface_state_files(
673 &PathBuf::from(&links_dir),
674 None,
675 temp_dir.path(),
676 None, )
678 .await
679 .expect("parse state files");
680 assert_eq!(state.managed_interfaces, 1);
681 assert_eq!(state.interfaces_state[0].name, "eth0");
682 }
683
684 #[tokio::test]
685 async fn test_parse_interface_state_files_empty_sysfs_no_conn() {
686 let temp_dir = tempdir().expect("temp dir");
689 let links_dir = temp_dir.path().join("links");
690 std::fs::create_dir_all(&links_dir).expect("create links dir");
691 let state =
692 parse_interface_state_files(&PathBuf::from(&links_dir), None, temp_dir.path(), None)
693 .await
694 .expect("parse state files");
695 assert_eq!(state, NetworkdState::default());
696 }
697
698 #[test]
699 fn test_enums_to_ints() -> Result<(), MonitordNetworkdError> {
700 assert_eq!(3, AddressState::routable as u64);
701 let carrier_state_int: u8 = u8::from(CarrierState::degraded_carrier);
702 assert_eq!(4, carrier_state_int);
703 assert_eq!(1, BoolState::True as i64);
704 let bool_state_false_int: u8 = u8::from(BoolState::False);
705 assert_eq!(0, bool_state_false_int);
706
707 Ok(())
708 }
709}