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::builder(connection)
271 .cache_properties(zbus::proxy::CacheProperties::No)
272 .build()
273 .await?;
274 let links = p.list_links().await?;
275 let mut link_int_to_name: HashMap<i32, String> = HashMap::new();
276 for network_link in links {
277 link_int_to_name.insert(network_link.0, network_link.1);
278 }
279 Ok(link_int_to_name)
280}
281
282#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
284pub struct NetworkdState {
285 pub interfaces_state: Vec<InterfaceState>,
287 pub managed_interfaces: u64,
289}
290
291pub const NETWORKD_STATE_FILES: &str = "/run/systemd/netif/links";
292
293pub fn parse_interface_stats(
295 interface_state_str: &str,
296 interface_id: i32,
297 interface_id_to_name: &HashMap<i32, String>,
298) -> Result<InterfaceState, MonitordNetworkdError> {
299 let mut interface_state = InterfaceState::default();
300
301 if interface_id > 0 {
303 if let Some(name) = interface_id_to_name.get(&interface_id) {
304 interface_state.name = name.clone();
305 }
306 }
307
308 for line in interface_state_str.lines() {
309 if !line.contains('=') {
311 continue;
312 }
313
314 let (key, value) = line
315 .split_once('=')
316 .expect("Unable to split a network state line");
317 match key {
318 "ADDRESS_STATE" => {
319 interface_state.address_state =
320 AddressState::from_str(value).unwrap_or(AddressState::unknown)
321 }
322 "ADMIN_STATE" => {
323 interface_state.admin_state =
324 AdminState::from_str(value).unwrap_or(AdminState::unknown)
325 }
326 "CARRIER_STATE" => {
327 interface_state.carrier_state =
328 CarrierState::from_str(value).unwrap_or(CarrierState::unknown)
329 }
330 "IPV4_ADDRESS_STATE" => {
331 interface_state.ipv4_address_state =
332 AddressState::from_str(value).unwrap_or(AddressState::unknown)
333 }
334 "IPV6_ADDRESS_STATE" => {
335 interface_state.ipv6_address_state =
336 AddressState::from_str(value).unwrap_or(AddressState::unknown)
337 }
338 "NETWORK_FILE" => interface_state.network_file = value.to_string(),
339 "OPER_STATE" => {
340 interface_state.oper_state =
341 OperState::from_str(value).unwrap_or(OperState::unknown)
342 }
343 "REQUIRED_FOR_ONLINE" => {
344 interface_state.required_for_online =
345 BoolState::from_str(value).unwrap_or(BoolState::unknown)
346 }
347 _ => continue,
348 };
349 }
350
351 Ok(interface_state)
352}
353
354pub async fn parse_interface_state_files(
356 states_path: &PathBuf,
357 maybe_network_int_to_name: Option<HashMap<i32, String>>,
358 maybe_connection: Option<&zbus::Connection>,
359) -> Result<NetworkdState, MonitordNetworkdError> {
360 let mut managed_interface_count: u64 = 0;
361 let mut interfaces_state = vec![];
362
363 let network_int_to_name = match maybe_network_int_to_name {
364 None => {
365 if let Some(connection) = maybe_connection {
366 match get_interface_links(connection).await {
367 Ok(hashmap) => hashmap,
368 Err(err) => {
369 error!(
370 "Unable to get interface links via DBUS - is networkd running?: {:#?}",
371 err
372 );
373 return Ok(NetworkdState::default());
374 }
375 }
376 } else {
377 error!(
378 "Unable to get interface links via DBUS and no network_int_to_name supplied"
379 );
380 return Ok(NetworkdState::default());
381 }
382 }
383 Some(valid_hashmap) => valid_hashmap,
384 };
385
386 let mut state_file_dir_entries = tokio::fs::read_dir(states_path).await?;
387 while let Some(state_file) = state_file_dir_entries.next_entry().await? {
388 if !state_file.path().is_file() {
389 continue;
390 }
391 let interface_stats_file_str = tokio::fs::read_to_string(state_file.path()).await?;
392 if !interface_stats_file_str.contains("NETWORK_FILE") {
393 continue;
394 }
395 managed_interface_count += 1;
396 let fname = state_file.file_name();
397 let interface_id: i32 = i32::from_str(fname.to_str().unwrap_or("0")).unwrap_or(0);
398 match parse_interface_stats(
399 &interface_stats_file_str,
400 interface_id,
401 &network_int_to_name,
402 ) {
403 Ok(interface_state) => interfaces_state.push(interface_state),
404 Err(err) => error!(
405 "Unable to parse interface statistics for {:?}: {}",
406 state_file.path().into_os_string(),
407 err
408 ),
409 }
410 }
411 Ok(NetworkdState {
412 interfaces_state,
413 managed_interfaces: managed_interface_count,
414 })
415}
416
417pub async fn update_networkd_stats(
419 states_path: PathBuf,
420 maybe_network_int_to_name: Option<HashMap<i32, String>>,
421 connection: zbus::Connection,
422 locked_machine_stats: Arc<RwLock<MachineStats>>,
423) -> anyhow::Result<()> {
424 match parse_interface_state_files(&states_path, maybe_network_int_to_name, Some(&connection))
425 .await
426 {
427 Ok(networkd_stats) => {
428 let mut machine_stats = locked_machine_stats.write().await;
429 machine_stats.networkd = networkd_stats;
430 }
431 Err(err) => error!("networkd stats failed: {:?}", err),
432 }
433 Ok(())
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use std::fs::File;
440 use std::io::Write;
441 use tempfile::tempdir;
442
443 const MOCK_INTERFACE_STATE: &str = r###"# This is private data. Do not parse.
444ADMIN_STATE=configured
445OPER_STATE=routable
446CARRIER_STATE=carrier
447ADDRESS_STATE=routable
448IPV4_ADDRESS_STATE=degraded
449IPV6_ADDRESS_STATE=routable
450ONLINE_STATE=online
451REQUIRED_FOR_ONLINE=yes
452REQUIRED_OPER_STATE_FOR_ONLINE=degraded:routable
453REQUIRED_FAMILY_FOR_ONLINE=any
454ACTIVATION_POLICY=up
455NETWORK_FILE=/etc/systemd/network/69-eno4.network
456NETWORK_FILE_DROPINS=""
457DNS=8.8.8.8 8.8.4.4
458NTP=
459SIP=
460DOMAINS=
461ROUTE_DOMAINS=
462LLMNR=yes
463MDNS=no
464"###;
465
466 fn return_expected_interface_state() -> InterfaceState {
467 InterfaceState {
468 address_state: AddressState::routable,
469 admin_state: AdminState::configured,
470 carrier_state: CarrierState::carrier,
471 ipv4_address_state: AddressState::degraded,
472 ipv6_address_state: AddressState::routable,
473 name: "eth0".to_string(),
474 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
475 oper_state: OperState::routable,
476 required_for_online: BoolState::True,
477 }
478 }
479
480 fn return_mock_int_name_hashmap() -> Option<HashMap<i32, String>> {
481 let mut h: HashMap<i32, String> = HashMap::new();
482 h.insert(2, String::from("eth0"));
483 h.insert(69, String::from("eth69"));
484 Some(h)
485 }
486
487 #[test]
488 fn test_parse_interface_stats() {
489 assert_eq!(
490 return_expected_interface_state(),
491 parse_interface_stats(
492 MOCK_INTERFACE_STATE,
493 2,
494 &return_mock_int_name_hashmap().expect("Failed to get a mock int name hashmap"),
495 )
496 .expect("Failed to parse interface stats"),
497 );
498 }
499
500 #[test]
501 fn test_parse_interface_stats_json() {
502 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}"###;
504 let stats = parse_interface_stats(MOCK_INTERFACE_STATE, 0, &HashMap::new()).unwrap();
505 let stats_json = serde_json::to_string(&stats).unwrap();
506 assert_eq!(expected_interface_state_json.to_string(), stats_json);
507 }
508
509 #[tokio::test]
510 async fn test_parse_interface_state_files() -> Result<(), MonitordNetworkdError> {
511 let expected_files = NetworkdState {
512 interfaces_state: vec![return_expected_interface_state()],
513 managed_interfaces: 1,
514 };
515
516 let temp_dir = tempdir()?;
517 let file_path = temp_dir.path().join("2");
519 let mut state_file = File::create(file_path)?;
520 writeln!(state_file, "{}", MOCK_INTERFACE_STATE)?;
521
522 let path = PathBuf::from(temp_dir.path());
523 assert_eq!(
524 expected_files,
525 parse_interface_state_files(
526 &path,
527 return_mock_int_name_hashmap(),
528 None, )
530 .await
531 .expect("Problem with parsing interface stte files")
532 );
533 Ok(())
534 }
535
536 #[test]
537 fn test_enums_to_ints() -> Result<(), MonitordNetworkdError> {
538 assert_eq!(3, AddressState::routable as u64);
539 let carrier_state_int: u8 = u8::from(CarrierState::degraded_carrier);
540 assert_eq!(4, carrier_state_int);
541 assert_eq!(1, BoolState::True as i64);
542 let bool_state_false_int: u8 = u8::from(BoolState::False);
543 assert_eq!(0, bool_state_false_int);
544
545 Ok(())
546 }
547}