1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::sync::Arc;
10
11use int_enum::IntEnum;
12use serde_repr::*;
13use strum_macros::EnumIter;
14use strum_macros::EnumString;
15use thiserror::Error;
16use tokio::sync::RwLock;
17use tracing::error;
18
19use crate::MachineStats;
20
21#[derive(Error, Debug)]
22pub enum MonitordNetworkdError {
23 #[error("Networkd D-Bus error: {0}")]
24 ZbusError(#[from] zbus::Error),
25 #[error("IO error: {0}")]
26 IoError(#[from] std::io::Error),
27}
28
29#[allow(non_camel_case_types)]
32#[derive(
33 Serialize_repr,
34 Deserialize_repr,
35 Clone,
36 Copy,
37 Debug,
38 Default,
39 Eq,
40 PartialEq,
41 EnumIter,
42 EnumString,
43 IntEnum,
44 strum_macros::Display,
45)]
46#[repr(u8)]
47pub enum AddressState {
48 #[default]
50 unknown = 0,
51 off = 1,
53 degraded = 2,
55 routable = 3,
57}
58
59#[allow(non_camel_case_types)]
62#[derive(
63 Serialize_repr,
64 Deserialize_repr,
65 Clone,
66 Copy,
67 Debug,
68 Default,
69 Eq,
70 PartialEq,
71 EnumIter,
72 EnumString,
73 IntEnum,
74 strum_macros::Display,
75)]
76#[repr(u8)]
77pub enum AdminState {
78 #[default]
80 unknown = 0,
81 pending = 1,
83 failed = 2,
85 configuring = 3,
87 configured = 4,
89 unmanaged = 5,
91 linger = 6,
93}
94
95#[allow(non_camel_case_types)]
97#[derive(
98 Serialize_repr,
99 Deserialize_repr,
100 Clone,
101 Copy,
102 Debug,
103 Default,
104 Eq,
105 PartialEq,
106 EnumIter,
107 EnumString,
108 IntEnum,
109 strum_macros::Display,
110)]
111#[repr(u8)]
112pub enum BoolState {
113 #[default]
114 unknown = u8::MAX,
115 #[strum(
116 serialize = "false",
117 serialize = "False",
118 serialize = "no",
119 serialize = "No"
120 )]
121 False = 0,
122 #[strum(
123 serialize = "true",
124 serialize = "True",
125 serialize = "yes",
126 serialize = "Yes"
127 )]
128 True = 1,
129}
130
131#[allow(non_camel_case_types)]
134#[derive(
135 Serialize_repr,
136 Deserialize_repr,
137 Clone,
138 Copy,
139 Debug,
140 Default,
141 Eq,
142 PartialEq,
143 EnumIter,
144 EnumString,
145 IntEnum,
146 strum_macros::Display,
147)]
148#[repr(u8)]
149pub enum CarrierState {
150 #[default]
152 unknown = 0,
153 off = 1,
155 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
157 no_carrier = 2,
158 dormant = 3,
160 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
162 degraded_carrier = 4,
163 carrier = 5,
165 enslaved = 6,
167}
168
169#[allow(non_camel_case_types)]
172#[derive(
173 Serialize_repr,
174 Deserialize_repr,
175 Clone,
176 Copy,
177 Debug,
178 Default,
179 Eq,
180 PartialEq,
181 EnumIter,
182 EnumString,
183 IntEnum,
184 strum_macros::Display,
185)]
186#[repr(u8)]
187pub enum OnlineState {
188 #[default]
190 unknown = 0,
191 offline = 1,
193 partial = 2,
195 online = 3,
197}
198
199#[allow(non_camel_case_types)]
202#[derive(
203 Serialize_repr,
204 Deserialize_repr,
205 Clone,
206 Copy,
207 Debug,
208 Default,
209 Eq,
210 PartialEq,
211 EnumIter,
212 EnumString,
213 IntEnum,
214 strum_macros::Display,
215)]
216#[repr(u8)]
217pub enum OperState {
218 #[default]
220 unknown = 0,
221 missing = 1,
223 off = 2,
225 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
227 no_carrier = 3,
228 dormant = 4,
230 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
232 degraded_carrier = 5,
233 carrier = 6,
235 degraded = 7,
237 enslaved = 8,
239 routable = 9,
241}
242
243#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
245pub struct InterfaceState {
246 pub address_state: AddressState,
248 pub admin_state: AdminState,
250 pub carrier_state: CarrierState,
252 pub ipv4_address_state: AddressState,
254 pub ipv6_address_state: AddressState,
256 pub name: String,
258 pub network_file: String,
260 pub oper_state: OperState,
262 pub required_for_online: BoolState,
264}
265
266async fn get_interface_links(
268 connection: &zbus::Connection,
269) -> Result<HashMap<i32, String>, MonitordNetworkdError> {
270 let p = crate::dbus::zbus_networkd::ManagerProxy::new(connection).await?;
271 let links = p.list_links().await?;
272 let mut link_int_to_name: HashMap<i32, String> = HashMap::new();
273 for network_link in links {
274 link_int_to_name.insert(network_link.0, network_link.1);
275 }
276 Ok(link_int_to_name)
277}
278
279#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
281pub struct NetworkdState {
282 pub interfaces_state: Vec<InterfaceState>,
284 pub managed_interfaces: u64,
286}
287
288pub const NETWORKD_STATE_FILES: &str = "/run/systemd/netif/links";
289
290pub fn parse_interface_stats(
292 interface_state_str: &str,
293 interface_id: i32,
294 interface_id_to_name: &HashMap<i32, String>,
295) -> Result<InterfaceState, MonitordNetworkdError> {
296 let mut interface_state = InterfaceState::default();
297
298 if interface_id > 0 {
300 if let Some(name) = interface_id_to_name.get(&interface_id) {
301 interface_state.name = name.clone();
302 }
303 }
304
305 for line in interface_state_str.lines() {
306 if !line.contains('=') {
308 continue;
309 }
310
311 let (key, value) = line
312 .split_once('=')
313 .expect("Unable to split a network state line");
314 match key {
315 "ADDRESS_STATE" => {
316 interface_state.address_state =
317 AddressState::from_str(value).unwrap_or(AddressState::unknown)
318 }
319 "ADMIN_STATE" => {
320 interface_state.admin_state =
321 AdminState::from_str(value).unwrap_or(AdminState::unknown)
322 }
323 "CARRIER_STATE" => {
324 interface_state.carrier_state =
325 CarrierState::from_str(value).unwrap_or(CarrierState::unknown)
326 }
327 "IPV4_ADDRESS_STATE" => {
328 interface_state.ipv4_address_state =
329 AddressState::from_str(value).unwrap_or(AddressState::unknown)
330 }
331 "IPV6_ADDRESS_STATE" => {
332 interface_state.ipv6_address_state =
333 AddressState::from_str(value).unwrap_or(AddressState::unknown)
334 }
335 "NETWORK_FILE" => interface_state.network_file = value.to_string(),
336 "OPER_STATE" => {
337 interface_state.oper_state =
338 OperState::from_str(value).unwrap_or(OperState::unknown)
339 }
340 "REQUIRED_FOR_ONLINE" => {
341 interface_state.required_for_online =
342 BoolState::from_str(value).unwrap_or(BoolState::unknown)
343 }
344 _ => continue,
345 };
346 }
347
348 Ok(interface_state)
349}
350
351pub async fn parse_interface_state_files(
353 states_path: &PathBuf,
354 maybe_network_int_to_name: Option<HashMap<i32, String>>,
355 maybe_connection: Option<&zbus::Connection>,
356) -> Result<NetworkdState, MonitordNetworkdError> {
357 let mut managed_interface_count: u64 = 0;
358 let mut interfaces_state = vec![];
359
360 let network_int_to_name = match maybe_network_int_to_name {
361 None => {
362 if let Some(connection) = maybe_connection {
363 match get_interface_links(connection).await {
364 Ok(hashmap) => hashmap,
365 Err(err) => {
366 error!(
367 "Unable to get interface links via DBUS - is networkd running?: {:#?}",
368 err
369 );
370 return Ok(NetworkdState::default());
371 }
372 }
373 } else {
374 error!(
375 "Unable to get interface links via DBUS and no network_int_to_name supplied"
376 );
377 return Ok(NetworkdState::default());
378 }
379 }
380 Some(valid_hashmap) => valid_hashmap,
381 };
382
383 let mut state_file_dir_entries = tokio::fs::read_dir(states_path).await?;
384 while let Some(state_file) = state_file_dir_entries.next_entry().await? {
385 if !state_file.path().is_file() {
386 continue;
387 }
388 let interface_stats_file_str = tokio::fs::read_to_string(state_file.path()).await?;
389 if !interface_stats_file_str.contains("NETWORK_FILE") {
390 continue;
391 }
392 managed_interface_count += 1;
393 let fname = state_file.file_name();
394 let interface_id: i32 = i32::from_str(fname.to_str().unwrap_or("0")).unwrap_or(0);
395 match parse_interface_stats(
396 &interface_stats_file_str,
397 interface_id,
398 &network_int_to_name,
399 ) {
400 Ok(interface_state) => interfaces_state.push(interface_state),
401 Err(err) => error!(
402 "Unable to parse interface statistics for {:?}: {}",
403 state_file.path().into_os_string(),
404 err
405 ),
406 }
407 }
408 Ok(NetworkdState {
409 interfaces_state,
410 managed_interfaces: managed_interface_count,
411 })
412}
413
414pub async fn update_networkd_stats(
416 states_path: PathBuf,
417 maybe_network_int_to_name: Option<HashMap<i32, String>>,
418 connection: zbus::Connection,
419 locked_machine_stats: Arc<RwLock<MachineStats>>,
420) -> anyhow::Result<()> {
421 match parse_interface_state_files(&states_path, maybe_network_int_to_name, Some(&connection))
422 .await
423 {
424 Ok(networkd_stats) => {
425 let mut machine_stats = locked_machine_stats.write().await;
426 machine_stats.networkd = networkd_stats;
427 }
428 Err(err) => error!("networkd stats failed: {:?}", err),
429 }
430 Ok(())
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use std::fs::File;
437 use std::io::Write;
438 use tempfile::tempdir;
439
440 const MOCK_INTERFACE_STATE: &str = r###"# This is private data. Do not parse.
441ADMIN_STATE=configured
442OPER_STATE=routable
443CARRIER_STATE=carrier
444ADDRESS_STATE=routable
445IPV4_ADDRESS_STATE=degraded
446IPV6_ADDRESS_STATE=routable
447ONLINE_STATE=online
448REQUIRED_FOR_ONLINE=yes
449REQUIRED_OPER_STATE_FOR_ONLINE=degraded:routable
450REQUIRED_FAMILY_FOR_ONLINE=any
451ACTIVATION_POLICY=up
452NETWORK_FILE=/etc/systemd/network/69-eno4.network
453NETWORK_FILE_DROPINS=""
454DNS=8.8.8.8 8.8.4.4
455NTP=
456SIP=
457DOMAINS=
458ROUTE_DOMAINS=
459LLMNR=yes
460MDNS=no
461"###;
462
463 fn return_expected_interface_state() -> InterfaceState {
464 InterfaceState {
465 address_state: AddressState::routable,
466 admin_state: AdminState::configured,
467 carrier_state: CarrierState::carrier,
468 ipv4_address_state: AddressState::degraded,
469 ipv6_address_state: AddressState::routable,
470 name: "eth0".to_string(),
471 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
472 oper_state: OperState::routable,
473 required_for_online: BoolState::True,
474 }
475 }
476
477 fn return_mock_int_name_hashmap() -> Option<HashMap<i32, String>> {
478 let mut h: HashMap<i32, String> = HashMap::new();
479 h.insert(2, String::from("eth0"));
480 h.insert(69, String::from("eth69"));
481 Some(h)
482 }
483
484 #[test]
485 fn test_parse_interface_stats() {
486 assert_eq!(
487 return_expected_interface_state(),
488 parse_interface_stats(
489 MOCK_INTERFACE_STATE,
490 2,
491 &return_mock_int_name_hashmap().expect("Failed to get a mock int name hashmap"),
492 )
493 .expect("Failed to parse interface stats"),
494 );
495 }
496
497 #[test]
498 fn test_parse_interface_stats_json() {
499 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}"###;
501 let stats = parse_interface_stats(MOCK_INTERFACE_STATE, 0, &HashMap::new()).unwrap();
502 let stats_json = serde_json::to_string(&stats).unwrap();
503 assert_eq!(expected_interface_state_json.to_string(), stats_json);
504 }
505
506 #[tokio::test]
507 async fn test_parse_interface_state_files() -> Result<(), MonitordNetworkdError> {
508 let expected_files = NetworkdState {
509 interfaces_state: vec![return_expected_interface_state()],
510 managed_interfaces: 1,
511 };
512
513 let temp_dir = tempdir()?;
514 let file_path = temp_dir.path().join("2");
516 let mut state_file = File::create(file_path)?;
517 writeln!(state_file, "{}", MOCK_INTERFACE_STATE)?;
518
519 let path = PathBuf::from(temp_dir.path());
520 assert_eq!(
521 expected_files,
522 parse_interface_state_files(
523 &path,
524 return_mock_int_name_hashmap(),
525 None, )
527 .await
528 .expect("Problem with parsing interface stte files")
529 );
530 Ok(())
531 }
532
533 #[test]
534 fn test_enums_to_ints() -> Result<(), MonitordNetworkdError> {
535 assert_eq!(3, AddressState::routable as u64);
536 let carrier_state_int: u8 = u8::from(CarrierState::degraded_carrier);
537 assert_eq!(4, carrier_state_int);
538 assert_eq!(1, BoolState::True as i64);
539 let bool_state_false_int: u8 = u8::from(BoolState::False);
540 assert_eq!(0, bool_state_false_int);
541
542 Ok(())
543 }
544}