1use crate::Message;
2
3#[derive(Debug)]
5pub struct Interface {
6 pub name: String,
7 pub is_up: bool,
8 pub index: u32,
9 pub master: Option<String>,
10}
11
12impl Interface {
13 pub fn from_enum(message: &Message) -> Option<Self> {
19 if let Message::InterfaceList(content) = message {
20 let mut it = content.split_ascii_whitespace();
21
22 let name = if let Some(s) = it.next() {
24 s
25 } else {
26 log::error!("ifc: unable to determine name in {content}");
27 return None;
28 };
29
30 let is_up = if let Some(s) = it.next() {
32 match s {
33 "up" => true,
34 "down" => false,
35 _ => {
36 log::error!("ifc: unknown state {s}");
37 return None;
38 }
39 }
40 } else {
41 log::error!("ifc: unable to determine state in {content}");
42 return None;
43 };
44
45 let mut index = -1_i32;
46 let mut master: Option<String> = None;
47 for s in it {
49 let s = s.trim_matches(|c: char| c == '(' || c == ')' || c == ' ');
50 if let Some(_idx) = s.strip_prefix("index=") {
51 index = _idx.parse().unwrap_or(-1);
52 } else if let Some(ms) = s.strip_prefix("master=") {
53 master = Some(ms.to_owned());
54 }
55 }
56 if index < 0 {
57 log::error!("ifc: did not find an appropriate index in {content}");
58 return None;
59 }
60
61 Some(Self {
62 name: name.into(),
63 is_up,
64 index: index as u32,
65 master,
66 })
67 } else {
68 log::error!("ifc: invoked Interface::from_enum on wrong message");
69 None
70 }
71 }
72}
73
74#[derive(Debug)]
76pub struct InterfaceProperties {
77 pub iftype: InterfaceType,
78 flags: u32,
79 pub mtu: u32,
80}
81
82impl InterfaceProperties {
83 pub fn from_enum(message: &Message) -> Option<Self> {
89 if let Message::InterfaceFlags(content) = message {
90 let mut it = content.split_ascii_whitespace();
91 let mut flags = 0_u32;
92 let mut mtu = 0;
93
94 let iftype = if let Some(s) = it.next() {
95 match s {
96 "PtP" => InterfaceType::PointToPoint,
97 "MultiAccess" => InterfaceType::MultiAccess,
98 _ => InterfaceType::Unknown(s.to_owned()),
99 }
100 } else {
101 log::error!("ifc: did not find any iftype in {content}");
102 return None;
103 };
104 for token in content.split_ascii_whitespace() {
105 if let Some(_mtu) = token.strip_prefix("MTU=") {
106 if let Ok(m) = _mtu.parse::<u32>() {
107 mtu = m;
108 } else {
109 log::error!("ifc: found invalid mtu in line {content}");
110 return None;
111 }
112 } else {
113 match token {
114 "Broadcast" => flags |= IF_FLAG_BROADCAST,
115 "Multicast" => flags |= IF_FLAG_MULTICAST,
116 "AdminUp" => flags |= IF_FLAG_ADMIN_UP,
117 "AdminDown" => flags &= !IF_FLAG_ADMIN_UP,
118 "LinkUp" => flags |= IF_FLAG_LINK_UP,
119 "LinkDown" => flags &= !IF_FLAG_LINK_UP,
120 "Loopback" => flags |= IF_FLAG_LOOPBACK,
121 "Ignored" => flags |= IF_FLAG_IGNORED,
122 _ => {}
123 }
124 }
125 }
126
127 if mtu == 0 {
128 log::error!("ifc: did not find any iftype in {content}");
129 }
130
131 Some(InterfaceProperties { iftype, flags, mtu })
132 } else {
133 log::error!("ifc: invoked InterfaceProperties::from_enum on wrong message");
134 None
135 }
136 }
137
138 #[inline]
140 pub fn is_broadcast_set(&self) -> bool {
141 (self.flags & IF_FLAG_BROADCAST) != 0
142 }
143
144 #[inline]
146 pub fn is_multicast_set(&self) -> bool {
147 (self.flags & IF_FLAG_MULTICAST) != 0
148 }
149
150 #[inline]
152 pub fn is_admin_up(&self) -> bool {
153 (self.flags & IF_FLAG_ADMIN_UP) != 0
154 }
155
156 #[inline]
158 pub fn is_link_up(&self) -> bool {
159 (self.flags & IF_FLAG_LINK_UP) != 0
160 }
161
162 #[inline]
164 pub fn is_loopback(&self) -> bool {
165 (self.flags & IF_FLAG_LOOPBACK) != 0
166 }
167
168 #[inline]
170 pub fn is_ignored_for_routing(&self) -> bool {
171 (self.flags & IF_FLAG_IGNORED) != 0
172 }
173}
174
175#[derive(Debug, PartialEq, Eq)]
177pub enum InterfaceType {
178 PointToPoint,
179 MultiAccess,
180 Unknown(String),
181}
182
183#[derive(Debug)]
185pub struct InterfaceAddress {
186 pub ip: String,
188 pub scope: String,
189 pub extra_info: Option<String>,
191}
192
193impl InterfaceAddress {
194 pub fn from_enum(message: &Message) -> Option<Vec<Self>> {
200 let mut addresses = vec![];
201 if let Message::InterfaceAddress(content) = message {
202 for line in content.lines() {
203 let mut it = line.split_ascii_whitespace();
204
205 let mut scope = "undef";
206 let mut extras = String::with_capacity(32);
207 let ip = if let Some(s) = it.next() {
209 s
210 } else {
211 log::error!("ifc: failed to find ip address in {line}");
212 return None;
213 };
214
215 let bc = |c| c == '(' || c == ')' || c == ' ';
217 while let Some(mut s) = it.next() {
218 s = s.trim_matches(bc);
219 if s == "scope" {
220 if let Some(sc) = it.next() {
221 scope = sc.trim_matches(bc).trim_matches(',');
222 } else {
223 log::error!("ifc: encountered scope but not value in {line}");
224 return None;
225 }
226 } else {
227 if !extras.is_empty() {
228 extras.push(' ');
229 }
230 extras.push_str(s);
231 }
232 }
233
234 if !extras.is_empty() {
235 extras = extras.trim_matches(',').into();
236 }
237
238 addresses.push(InterfaceAddress {
239 ip: ip.into(),
240 scope: scope.into(),
241 extra_info: if extras.is_empty() {
242 None
243 } else {
244 Some(extras)
245 },
246 })
247 }
248
249 Some(addresses)
250 } else {
251 log::error!("ifc: invoked InterfaceAddress::from_enum on wrong message");
252 None
253 }
254 }
255}
256
257pub struct InterfaceSummary {
258 pub name: String,
259 pub state: String,
260 pub ipv4_address: Option<String>,
261 pub ipv6_address: Option<String>,
262}
263
264impl InterfaceSummary {
265 pub fn from_enum(message: &Message) -> Option<Vec<Self>> {
271 if let Message::InterfaceSummary(content) = message {
272 let mut entries: Vec<Self> = vec![];
273 for line in content.lines() {
274 let mut it = line.split_ascii_whitespace();
275 let name: String = it.next()?.into();
276 let state: String = it.next()?.into();
277 let mut ipv4_address = None;
278 let mut ipv6_address = None;
279
280 for addr in it {
281 if addr.contains(':') {
282 ipv6_address = Some(addr.to_owned());
283 } else {
284 ipv4_address = Some(addr.to_owned());
285 }
286 }
287
288 entries.push(InterfaceSummary {
289 name,
290 state,
291 ipv4_address,
292 ipv6_address,
293 })
294 }
295 Some(entries)
296 } else {
297 log::error!("ifc: invoked InterfaceSummary::from_enum on wrong message");
298 None
299 }
300 }
301}
302
303const IF_FLAG_BROADCAST: u32 = 1 << 2;
305const IF_FLAG_MULTICAST: u32 = 1 << 3;
307const IF_FLAG_LOOPBACK: u32 = 1 << 5;
309const IF_FLAG_IGNORED: u32 = 1 << 6;
311const IF_FLAG_ADMIN_UP: u32 = 1 << 7;
313const IF_FLAG_LINK_UP: u32 = 1 << 8;
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::Message;
320
321 #[test]
322 #[ignore]
323 fn test_invalid() {
324 let _ = env_logger::try_init();
325 assert!(
326 Interface::from_enum(&Message::Ok).is_none(),
327 "expected None from parsing invalid message type",
328 );
329 }
330
331 #[test]
332 fn test_interface_parsing_without_master() {
333 let _ = env_logger::try_init();
334 let message = Message::InterfaceList("eth0 up (index=2)".into());
335 let ifc = Interface::from_enum(&message).expect("failed to parse");
336 assert_eq!(ifc.name, "eth0");
337 assert!(ifc.is_up);
338 assert_eq!(ifc.index, 2);
339 assert!(ifc.master.is_none(), "was not expecting master");
340 }
341
342 #[test]
343 fn test_interface_parsing_with_master() {
344 let _ = env_logger::try_init();
345 let message = Message::InterfaceList("eth1 down (index=3 master=#2)".into());
346 let ifc = Interface::from_enum(&message).expect("failed to parse");
347 assert_eq!(ifc.name, "eth1");
348 assert!(!ifc.is_up);
349 assert_eq!(ifc.index, 3);
350 assert_eq!(ifc.master.expect("was expecting master"), "#2");
351 }
352
353 #[test]
354 fn test_interface_properties() {
355 let _ = env_logger::try_init();
356 let message = Message::InterfaceFlags(
357 "MultiAccess Broadcast Multicast AdminDown LinkUp MTU=9000".into(),
358 );
359 let props = InterfaceProperties::from_enum(&message).expect("failed to parse");
360 assert_eq!(props.iftype, InterfaceType::MultiAccess);
361 assert_eq!(props.mtu, 9000);
362 assert!(props.is_broadcast_set());
363 assert!(props.is_multicast_set());
364 assert!(!props.is_admin_up());
365 assert!(props.is_link_up());
366 }
367
368 #[test]
369 fn test_interface_address() {
370 let _ = env_logger::try_init();
371 let content = "\t172.30.0.12/16 (Preferred, scope site)\n\t172.29.1.15/32 (scope univ)\n\t172.29.1.16/32 (scope univ)\n\t172.29.1.17/32 (scope univ)\n\tfe80::4495:80ff:fe71:a791/64 (Preferred, scope link)\n\tfe80::4490::72/64 (scope univ)";
372 let message = Message::InterfaceAddress(content.into());
373 let addresses = InterfaceAddress::from_enum(&message).expect("failed to parse");
374 validate_address(&addresses[0], "172.30.0.12/16", "site", "Preferred");
375 validate_address(&addresses[1], "172.29.1.15/32", "univ", "");
376 validate_address(&addresses[2], "172.29.1.16/32", "univ", "");
377 validate_address(&addresses[3], "172.29.1.17/32", "univ", "");
378 validate_address(
379 &addresses[4],
380 "fe80::4495:80ff:fe71:a791/64",
381 "link",
382 "Preferred",
383 );
384 validate_address(&addresses[5], "fe80::4490::72/64", "univ", "");
385 }
386
387 #[test]
388 fn test_interface_summary() {
389 let _ = env_logger::try_init();
390 let content = "lo up 127.0.0.1/8 ::1/128\neth0 up 172.30.0.12/16 fe80::4495:80ff:fe71:a791/64\neth1 up 169.254.199.2/30";
391 let message = Message::InterfaceSummary(content.into());
392 let summaries = InterfaceSummary::from_enum(&message).expect("failed to parse");
393
394 assert_eq!(summaries[0].name, "lo");
395 assert_eq!(summaries[0].state, "up");
396 assert_eq!(summaries[0].ipv4_address.as_ref().unwrap(), "127.0.0.1/8");
397 assert_eq!(summaries[0].ipv6_address.as_ref().unwrap(), "::1/128");
398
399 assert_eq!(summaries[1].name, "eth0");
400 assert_eq!(summaries[1].state, "up");
401 assert_eq!(
402 summaries[1].ipv4_address.as_ref().unwrap(),
403 "172.30.0.12/16",
404 );
405 assert_eq!(
406 summaries[1].ipv6_address.as_ref().unwrap(),
407 "fe80::4495:80ff:fe71:a791/64",
408 );
409
410 assert_eq!(summaries[2].name, "eth1");
411 assert_eq!(summaries[2].state, "up");
412 assert_eq!(
413 summaries[2].ipv4_address.as_ref().unwrap(),
414 "169.254.199.2/30",
415 );
416 assert!(summaries[2].ipv6_address.is_none());
417 }
418
419 fn validate_address(address: &InterfaceAddress, ip: &str, scope: &str, extras: &str) {
420 assert_eq!(address.ip, ip);
421 assert_eq!(address.scope, scope);
422 if let Some(ref ei) = address.extra_info {
423 assert_eq!(ei, extras)
424 } else {
425 assert_eq!(extras, "", "expected empty extra_info");
426 }
427 }
428}