birdc/models/
interface.rs

1use crate::Message;
2
3/// A network interface, as seen by Bird
4#[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    /// Parse the response of a 1001 response. Returns None if `message` isn't a
14    /// [Message::InterfaceList], or if we encounter an unrecoverable error during
15    /// parsing.
16    ///
17    /// Details [here](https://gitlab.nic.cz/labs/bird/-/blob/master/nest/iface.c)
18    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            // parse name - we eat up any failures
23            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            // parse state - we eat up any failures
31            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            // parse things inside the brackets
48            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/// Properties of an interface (flags, MTU) as seen by Bird
75#[derive(Debug)]
76pub struct InterfaceProperties {
77    pub iftype: InterfaceType,
78    flags: u32,
79    pub mtu: u32,
80}
81
82impl InterfaceProperties {
83    /// Parse the response of a 1004 response. Returns None if `message` isn't a
84    /// [Message::InterfaceFlags], or if we encounter an unrecoverable error during
85    /// parsing.
86    ///
87    /// Details [here](https://gitlab.nic.cz/labs/bird/-/blob/master/nest/iface.c)
88    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    /// Interface has broadcast address set
139    #[inline]
140    pub fn is_broadcast_set(&self) -> bool {
141        (self.flags & IF_FLAG_BROADCAST) != 0
142    }
143
144    /// Interface supports multicast
145    #[inline]
146    pub fn is_multicast_set(&self) -> bool {
147        (self.flags & IF_FLAG_MULTICAST) != 0
148    }
149
150    /// Interface is up & running
151    #[inline]
152    pub fn is_admin_up(&self) -> bool {
153        (self.flags & IF_FLAG_ADMIN_UP) != 0
154    }
155
156    /// Interface has its lower link up
157    #[inline]
158    pub fn is_link_up(&self) -> bool {
159        (self.flags & IF_FLAG_LINK_UP) != 0
160    }
161
162    /// Interface is a loopback device
163    #[inline]
164    pub fn is_loopback(&self) -> bool {
165        (self.flags & IF_FLAG_LOOPBACK) != 0
166    }
167
168    /// Interface is ignored by routing protocols
169    #[inline]
170    pub fn is_ignored_for_routing(&self) -> bool {
171        (self.flags & IF_FLAG_IGNORED) != 0
172    }
173}
174
175/// Type of interface
176#[derive(Debug, PartialEq, Eq)]
177pub enum InterfaceType {
178    PointToPoint,
179    MultiAccess,
180    Unknown(String),
181}
182
183/// IP addresses assigned to an [Interface]
184#[derive(Debug)]
185pub struct InterfaceAddress {
186    /// IP address, in address/prefix format
187    pub ip: String,
188    pub scope: String,
189    /// Any extra information
190    pub extra_info: Option<String>,
191}
192
193impl InterfaceAddress {
194    /// Parse the response of a 1003 response. Returns None if `message` isn't a
195    /// [Message::InterfaceAddress], or if we encounter an unrecoverable error during
196    /// parsing.
197    ///
198    /// Details [here](https://gitlab.nic.cz/labs/bird/-/blob/master/nest/iface.c)
199    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                // process ip address and prefix length
208                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                // process scope and extra info
216                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    /// Parse the response of a 1005 response. Returns None if `message` isn't a
266    /// [Message::InterfaceAddress], or if we encounter an unrecoverable error during
267    /// parsing.
268    ///
269    /// Details [here](https://gitlab.nic.cz/labs/bird/-/blob/master/nest/iface.c)
270    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
303/// Valid broadcast address set
304const IF_FLAG_BROADCAST: u32 = 1 << 2;
305/// Supports multicast
306const IF_FLAG_MULTICAST: u32 = 1 << 3;
307/// Is a loopback device
308const IF_FLAG_LOOPBACK: u32 = 1 << 5;
309/// Not to be used by routing protocols (loopbacks etc.)
310const IF_FLAG_IGNORED: u32 = 1 << 6;
311/// Interface is running
312const IF_FLAG_ADMIN_UP: u32 = 1 << 7;
313/// L1 layer is up
314const 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}