macos_routing_table/
route_entry.rs

1use crate::{Destination, Entity, Protocol, RoutingFlag};
2use cidr::AnyIpCidr;
3use mac_address::MacAddress;
4use std::{
5    collections::HashSet,
6    net::{IpAddr, Ipv4Addr, Ipv6Addr},
7    time::Duration,
8};
9
10/// A single route obtained from the `netstat -rn` output
11#[derive(Debug, Clone)]
12pub struct RouteEntry {
13    /// Protocol
14    pub proto: Protocol,
15
16    /// Destination.  E.g., a host or CIDR
17    pub dest: Destination,
18
19    /// Gateway (i.e., how to reach the destination)
20    pub gateway: Destination,
21
22    /// Routing flags
23    pub flags: HashSet<RoutingFlag>,
24
25    /// Network interface that holds this route
26    pub net_if: String,
27
28    /// RouteEntry expiration.  This is primarily seen for ARP-derived entries
29    pub expires: Option<Duration>,
30}
31
32impl std::fmt::Display for RouteEntry {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        #[allow(unused_variables)]
35        let RouteEntry {
36            proto,
37            dest,
38            gateway,
39            flags,
40            net_if,
41            expires,
42        } = self;
43        write!(f, "{proto:?}({dest} -> {gateway} if={net_if}")
44    }
45}
46
47#[derive(Debug, thiserror::Error)]
48pub enum Error {
49    #[error("parsing destination CIDR {value:?}: {err}")]
50    ParseDestination {
51        value: String,
52        err: cidr::errors::NetworkParseError,
53    },
54
55    #[error("parsing MAC addr {dest:?}: {err}")]
56    ParseMacAddr {
57        dest: String,
58        err: mac_address::MacParseError,
59    },
60
61    #[error("unparseable byte in IPv4 address {addr:?}: {err}")]
62    ParseIPv4AddrBadInt {
63        addr: String,
64        err: std::num::ParseIntError,
65    },
66
67    #[error("invalid number of IPv4 address components ({n_comps}) in {addr:?}")]
68    ParseIPv4AddrNComps { n_comps: usize, addr: String },
69
70    #[error("invalid expiration {expiration:?}: {err}")]
71    ParseExpiration {
72        expiration: String,
73        err: std::num::ParseIntError,
74    },
75
76    #[error("missing destination")]
77    MissingDestination,
78
79    #[error("missing gateway")]
80    MissingGateway,
81
82    #[error("missing network interface")]
83    MissingInterface,
84}
85
86impl RouteEntry {
87    /// Parse a textual route entry from the netstat output, specifying the
88    /// current protocols and active column headers.
89    pub(crate) fn parse(proto: Protocol, line: &str, headers: &[&str]) -> Result<Self, Error> {
90        let fields: Vec<String> = line.split_ascii_whitespace().map(str::to_string).collect();
91        let mut flags = HashSet::new();
92        let mut dest = None;
93        let mut gateway = None;
94        let mut net_if: Option<String> = None;
95        let mut expires = None;
96
97        // Scan through the fields, matching them up with the headers.
98        for (header, field) in headers.iter().zip(fields) {
99            match *header {
100                "Destination" => dest = Some(parse_destination(&field)?),
101                "Gateway" => gateway = Some(parse_destination(&field)?),
102                "Flags" => flags = parse_flags(&field),
103                "Netif" => net_if = Some(field),
104                "Expire" => expires = parse_expire(&field)?,
105                _ => (),
106            }
107        }
108
109        let route = RouteEntry {
110            proto,
111            dest: dest.ok_or(Error::MissingDestination)?,
112            gateway: gateway.ok_or(Error::MissingGateway)?,
113            flags,
114            net_if: net_if.ok_or(Error::MissingInterface)?,
115            expires,
116        };
117        Ok(route)
118    }
119
120    /// Return whether the specified route's destination is appropriate for the given address
121    pub(crate) fn contains(&self, addr: IpAddr) -> bool {
122        match self.dest.entity {
123            Entity::Cidr(cidr) => cidr.contains(&addr),
124            Entity::Default => match self.gateway.entity {
125                Entity::Cidr(_) => match addr {
126                    IpAddr::V4(_) => matches!(self.proto, Protocol::V4),
127                    // FIXME: IPv6 should take zone into account
128                    IpAddr::V6(_) => matches!(self.proto, Protocol::V6),
129                },
130                // Ignore these -- they never "contain" any IpAddr
131                Entity::Link(_) | Entity::Mac(_) | Entity::Default => false,
132            },
133            _ => false,
134        }
135    }
136
137    /// Compare two routes, returning the one that is more-precise based on whether
138    /// it resolves to an identified device or interface, or has a larger network
139    /// length
140    pub(crate) fn most_precise<'a>(&'a self, other: &'a Self) -> &'a Self {
141        match self.dest.entity {
142            // If this is a hardware address, we already know it's on the same
143            // local network, and it's in the ARP table
144            Entity::Mac(_) => self,
145            Entity::Link(_) => match other.dest.entity {
146                // The other specifies a hardware address -- it's better
147                Entity::Mac(_) => other,
148                // Otherwise, just default to the LHS
149                _ => self,
150            },
151            Entity::Cidr(cidr) => match other.dest.entity {
152                Entity::Mac(_) | Entity::Link(_) => other,
153                Entity::Cidr(other_cidr) => {
154                    let Some(cidr_nl) = cidr.network_length() else {
155                        // Can't compare gateway CIDR of 'Any' type
156                        return other;
157                    };
158
159                    let Some(other_nl) = other_cidr.network_length() else {
160                        // Can't compare gateway CIDR of 'Any' type
161                        return self;
162                    };
163
164                    // Choose the one with the longest network length
165                    if cidr_nl >= other_nl {
166                        self
167                    } else {
168                        other
169                    }
170                }
171                Entity::Default => self,
172            },
173            Entity::Default => match other.dest.entity {
174                // Never prefer a default route
175                Entity::Default => self,
176                _ => other,
177            },
178        }
179    }
180}
181
182fn parse_destination(dest: &str) -> Result<Destination, Error> {
183    if dest.starts_with("link") {
184        return Ok(Destination {
185            entity: Entity::Link(dest.to_owned()),
186            zone: None,
187        });
188    }
189    Ok(if let Some((addr, zone_etc)) = dest.split_once('%') {
190        // This route contains a zone ID
191        // See: https://superuser.com/questions/99746/why-is-there-a-percent-sign-in-the-ipv6-address
192        let addr: AnyIpCidr = addr.parse().map_err(|err| Error::ParseDestination {
193            value: addr.into(),
194            err,
195        })?;
196        let mut zone_etc = zone_etc.split('/');
197        let zone = zone_etc.next().map(ToOwned::to_owned);
198
199        if let Some(bits) = zone_etc.next() {
200            // Just reassemble it without the %zone and run it through the regular parser
201            let s = format!("{addr}{bits}");
202            Destination {
203                entity: parse_simple_destination(&s)?,
204                zone,
205            }
206        } else {
207            Destination {
208                entity: Entity::Cidr(addr),
209                zone,
210            }
211        }
212    } else {
213        Destination {
214            entity: parse_simple_destination(dest)?,
215            zone: None,
216        }
217    })
218}
219
220fn parse_simple_destination(dest: &str) -> Result<Entity, Error> {
221    Ok(match dest {
222        "default" => Entity::Default,
223
224        cidr if cidr.contains('/') => {
225            Entity::Cidr(cidr.parse().map_err(|err| Error::ParseDestination {
226                value: cidr.into(),
227                err,
228            })?)
229        }
230        // IPv4 host
231        addr if addr.contains('.') => {
232            if let Ok(ipv4addr) = parse_ipv4dest(addr) {
233                Entity::Cidr(AnyIpCidr::new_host(IpAddr::V4(ipv4addr)))
234            } else {
235                // Bridge broadcast addresses sometimes contain a dot-delimited MAC address
236                Entity::Mac(
237                    addr.replace('.', ":")
238                        .parse::<MacAddress>()
239                        .map_err(|err| Error::ParseMacAddr {
240                            dest: addr.into(),
241                            err,
242                        })?,
243                )
244            }
245        }
246        // IPv6 host
247        addr if addr.contains(':') => {
248            if let Ok(v6addr) = addr.parse::<Ipv6Addr>() {
249                Entity::Cidr(AnyIpCidr::new_host(IpAddr::V6(v6addr)))
250            } else {
251                // Try as a MAC address
252                Entity::Mac(
253                    addr.parse::<MacAddress>()
254                        .map_err(|err| Error::ParseMacAddr {
255                            dest: addr.into(),
256                            err,
257                        })?,
258                )
259            }
260        }
261        // Match bare numbers
262        num => Entity::Cidr(AnyIpCidr::new_host(IpAddr::V4(parse_ipv4dest(num)?))),
263    })
264}
265
266fn parse_flags(flags_s: &str) -> HashSet<RoutingFlag> {
267    flags_s.chars().map(RoutingFlag::from).collect()
268}
269
270fn parse_expire(s: &str) -> Result<Option<Duration>, Error> {
271    match s {
272        "!" => Ok(None),
273        n => Ok(Some(Duration::from_secs(n.parse().map_err(|err| {
274            Error::ParseExpiration {
275                expiration: s.into(),
276                err,
277            }
278        })?))),
279    }
280}
281
282fn parse_ipv4dest(dest: &str) -> Result<Ipv4Addr, Error> {
283    dest.parse::<Ipv4Addr>().or_else(|_| {
284        let parts: Vec<u8> = dest
285            .split('.')
286            .map(str::parse)
287            .collect::<std::result::Result<Vec<u8>, std::num::ParseIntError>>()
288            .map_err(|err| Error::ParseIPv4AddrBadInt {
289                addr: dest.into(),
290                err,
291            })?;
292        // This bizarre byte-ordering comes from inet_addr(3)
293        match parts.len() {
294            3 => Ok(Ipv4Addr::new(parts[0], parts[1], 0, parts[2])),
295            2 => Ok(Ipv4Addr::new(parts[0], 0, 0, parts[1])),
296            1 => Ok(Ipv4Addr::new(0, 0, 0, parts[0])),
297            len => Err(Error::ParseIPv4AddrNComps {
298                n_comps: len,
299                addr: dest.into(),
300            }),
301        }
302    })
303}