Skip to main content

rama_net/socket/interfaces/
mod.rs

1//! Enumeration of the host's local network interfaces.
2//!
3//! See [`interfaces`] and [`local_addresses`].
4
5use std::fmt;
6use std::io;
7use std::net::IpAddr;
8use std::str::FromStr;
9
10use rama_core::error::{BoxError, BoxErrorExt as _, ErrorContext as _, ErrorExt as _};
11use rama_utils::macros::serde_str::impl_serde_str;
12use rama_utils::str::{eq_ignore_ascii_kebab_case, smol_str::SmolStr};
13
14use crate::address::ip::{IpScopes, ip_scope, ipnet::IpNet};
15
16#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
17mod unix;
18
19#[cfg(target_os = "windows")]
20mod windows;
21
22bitflags::bitflags! {
23    /// Status and capability flags of a local network [`Interface`].
24    ///
25    /// Mapped from `IFF_*` on unix platforms and from the adapter's
26    /// operational status and type on windows; per-flag notes below call out
27    /// where the platforms differ.
28    ///
29    /// # String format
30    ///
31    /// [`InterfaceFlags`] round-trips (`Display`/`FromStr`/serde) through a
32    /// comma-separated list of kebab-case flag names, e.g. `"up,running"`.
33    /// Parsing is allocation-free and lenient: ASCII case-insensitive, `_`
34    /// equals `-`, and `|` is also accepted as separator. An empty string
35    /// parses as the empty set.
36    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37    pub struct InterfaceFlags: u8 {
38        /// Interface is administratively up.
39        ///
40        /// On windows this is set together with [`InterfaceFlags::RUNNING`]
41        /// when the adapter's operational status is up; the two states are
42        /// not reported separately there.
43        const UP = 1 << 0;
44        /// Interface is operationally running (e.g. carrier present).
45        const RUNNING = 1 << 1;
46        /// Loopback interface.
47        const LOOPBACK = 1 << 2;
48        /// Point-to-point link (tunnels, PPP).
49        const POINT_TO_POINT = 1 << 3;
50        /// Broadcast-capable link. Never set on windows.
51        const BROADCAST = 1 << 4;
52        /// Multicast-capable link.
53        const MULTICAST = 1 << 5;
54    }
55}
56
57/// canonical kebab-case name of every flag, in bit order
58const FLAG_NAMES: &[(&str, InterfaceFlags)] = &[
59    ("up", InterfaceFlags::UP),
60    ("running", InterfaceFlags::RUNNING),
61    ("loopback", InterfaceFlags::LOOPBACK),
62    ("point-to-point", InterfaceFlags::POINT_TO_POINT),
63    ("broadcast", InterfaceFlags::BROADCAST),
64    ("multicast", InterfaceFlags::MULTICAST),
65];
66
67impl fmt::Display for InterfaceFlags {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        let mut first = true;
70        for (name, flag) in FLAG_NAMES {
71            if self.contains(*flag) {
72                if !first {
73                    f.write_str(",")?;
74                }
75                first = false;
76                f.write_str(name)?;
77            }
78        }
79        Ok(())
80    }
81}
82
83impl FromStr for InterfaceFlags {
84    type Err = BoxError;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        let mut flags = Self::empty();
88        for token in s.split([',', '|']) {
89            let token = token.trim();
90            if token.is_empty() {
91                continue;
92            }
93            flags |= FLAG_NAMES
94                .iter()
95                .find_map(|(name, flag)| {
96                    eq_ignore_ascii_kebab_case(token.as_bytes(), name.as_bytes()).then_some(*flag)
97                })
98                .ok_or_else(|| {
99                    BoxError::from_static_str("unknown interface flag")
100                        .context_str_field("flag", token)
101                })?;
102        }
103        Ok(flags)
104    }
105}
106
107impl_serde_str!(display InterfaceFlags);
108
109/// Link-layer (MAC) address of an [`Interface`].
110///
111/// # String format
112///
113/// Round-trips (`Display`/`FromStr`/serde) through lowercase colon-separated
114/// hex groups, e.g. `"aa:bb:cc:dd:ee:ff"`; parsing also accepts `-` as
115/// separator and uppercase hex, allocation-free.
116#[derive(Clone, Copy, PartialEq, Eq, Hash)]
117pub struct HardwareAddress {
118    bytes: [u8; Self::MAX_LEN],
119    len: u8,
120}
121
122impl HardwareAddress {
123    const MAX_LEN: usize = 8;
124
125    /// `None` for empty, oversized (> 8 bytes) or all-zero input:
126    /// none of those identify actual hardware.
127    fn try_new(bytes: &[u8]) -> Option<Self> {
128        if bytes.is_empty() || bytes.len() > Self::MAX_LEN || bytes.iter().all(|b| *b == 0) {
129            return None;
130        }
131        let len = u8::try_from(bytes.len()).ok()?;
132        let mut buf = [0u8; Self::MAX_LEN];
133        buf.get_mut(..bytes.len())?.copy_from_slice(bytes);
134        Some(Self { bytes: buf, len })
135    }
136
137    /// The raw address bytes (6 for an ethernet-style MAC).
138    #[must_use]
139    pub fn as_bytes(&self) -> &[u8] {
140        self.bytes.get(..usize::from(self.len)).unwrap_or_default()
141    }
142}
143
144impl TryFrom<&[u8]> for HardwareAddress {
145    type Error = BoxError;
146
147    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
148        Self::try_new(bytes).ok_or_else(|| {
149            BoxError::from_static_str("hardware address must be 1..=8 bytes and not all-zero")
150        })
151    }
152}
153
154impl FromStr for HardwareAddress {
155    type Err = BoxError;
156
157    fn from_str(s: &str) -> Result<Self, Self::Err> {
158        let mut bytes = [0u8; Self::MAX_LEN];
159        let mut len = 0usize;
160        for group in s.split([':', '-']) {
161            if group.len() != 2 || !group.bytes().all(|b| b.is_ascii_hexdigit()) {
162                return Err(BoxError::from_static_str(
163                    "hardware address groups must be exactly two hex digits",
164                ));
165            }
166            let Some(slot) = bytes.get_mut(len) else {
167                return Err(BoxError::from_static_str(
168                    "hardware address is too long (max 8 bytes)",
169                ));
170            };
171            *slot = u8::from_str_radix(group, 16).context("parse hardware address hex group")?;
172            len += 1;
173        }
174        bytes.get(..len).and_then(Self::try_new).ok_or_else(|| {
175            BoxError::from_static_str("hardware address must be 1..=8 bytes and not all-zero")
176        })
177    }
178}
179
180impl TryFrom<&str> for HardwareAddress {
181    type Error = BoxError;
182
183    fn try_from(s: &str) -> Result<Self, Self::Error> {
184        s.parse()
185    }
186}
187
188impl_serde_str!(display HardwareAddress);
189
190impl fmt::Display for HardwareAddress {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        for (i, b) in self.as_bytes().iter().enumerate() {
193            if i > 0 {
194                write!(f, ":")?;
195            }
196            write!(f, "{b:02x}")?;
197        }
198        Ok(())
199    }
200}
201
202impl fmt::Debug for HardwareAddress {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "HardwareAddress({self})")
205    }
206}
207
208/// One IP address assigned to a local network [`Interface`].
209///
210/// # String format
211///
212/// Round-trips (`Display`/`FromStr`/serde) as `"address[%zone][/prefix]"`,
213/// e.g. `"192.168.1.7/24"` or `"fe80::1%3/64"`; the (numeric) zone is only
214/// valid for IPv6 addresses.
215#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
216pub struct InterfaceAddress {
217    address: IpAddr,
218    prefix_len: Option<u8>,
219    scope_id: Option<u32>,
220}
221
222impl InterfaceAddress {
223    #[cfg(any(
224        target_os = "linux",
225        target_os = "android",
226        target_vendor = "apple",
227        target_os = "windows",
228        test
229    ))]
230    fn new(address: IpAddr, prefix_len: Option<u8>, scope_id: Option<u32>) -> Self {
231        let max = if address.is_ipv4() { 32 } else { 128 };
232        Self {
233            address,
234            prefix_len: prefix_len.filter(|prefix| *prefix <= max),
235            scope_id: scope_id.filter(|id| *id != 0 && address.is_ipv6()),
236        }
237    }
238
239    /// The address itself.
240    #[must_use]
241    pub fn address(&self) -> IpAddr {
242        self.address
243    }
244
245    /// Prefix length of the network the address sits in, when the platform
246    /// reported a (contiguous, non-zero) netmask or on-link prefix.
247    #[must_use]
248    pub fn prefix_len(&self) -> Option<u8> {
249        self.prefix_len
250    }
251
252    /// Zone (scope id) of a scoped IPv6 address (e.g. link-local),
253    /// when the platform reported one.
254    ///
255    /// Not to be confused with the special-use classification of
256    /// [`IpScopes`]; this is the RFC 4007 zone index, as also found in
257    /// [`std::net::SocketAddrV6::scope_id`].
258    #[must_use]
259    pub fn scope_id(&self) -> Option<u32> {
260        self.scope_id
261    }
262
263    /// Address and prefix as an (un-truncated) [`IpNet`].
264    ///
265    /// Use [`IpNet::trunc`] on the result to get the network address itself.
266    #[must_use]
267    pub fn ip_net(&self) -> Option<IpNet> {
268        IpNet::new(self.address, self.prefix_len?).ok()
269    }
270}
271
272impl fmt::Display for InterfaceAddress {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        self.address.fmt(f)?;
275        if let Some(zone) = self.scope_id {
276            write!(f, "%{zone}")?;
277        }
278        if let Some(prefix) = self.prefix_len {
279            write!(f, "/{prefix}")?;
280        }
281        Ok(())
282    }
283}
284
285impl FromStr for InterfaceAddress {
286    type Err = BoxError;
287
288    fn from_str(s: &str) -> Result<Self, Self::Err> {
289        let (s, prefix_len) = match s.split_once('/') {
290            Some((s, prefix)) => (
291                s,
292                Some(
293                    prefix
294                        .parse::<u8>()
295                        .context("parse interface address prefix length")?,
296                ),
297            ),
298            None => (s, None),
299        };
300        let (s, scope_id) = match s.split_once('%') {
301            Some((s, zone)) => (
302                s,
303                Some(
304                    zone.parse::<u32>()
305                        .context("parse interface address zone (scope id)")?,
306                ),
307            ),
308            None => (s, None),
309        };
310        let address: IpAddr = s.parse().context("parse interface ip address")?;
311
312        if scope_id.is_some() && address.is_ipv4() {
313            return Err(BoxError::from_static_str(
314                "a zone (scope id) is only valid for an ipv6 interface address",
315            )
316            .context_field("address", address));
317        }
318        let max = if address.is_ipv4() { 32 } else { 128 };
319        if let Some(prefix) = prefix_len
320            && prefix > max
321        {
322            return Err(
323                BoxError::from_static_str("interface address prefix length out of range")
324                    .context_field("prefix", prefix)
325                    .context_field("address", address),
326            );
327        }
328
329        Ok(Self {
330            address,
331            prefix_len,
332            scope_id: scope_id.filter(|id| *id != 0),
333        })
334    }
335}
336
337impl_serde_str!(display InterfaceAddress);
338
339/// A network interface of the local host, as enumerated by [`interfaces`].
340#[derive(Clone, Debug)]
341pub struct Interface {
342    name: SmolStr,
343    index: Option<u32>,
344    flags: InterfaceFlags,
345    hw_address: Option<HardwareAddress>,
346    description: Option<SmolStr>,
347    addresses: Vec<InterfaceAddress>,
348}
349
350impl Interface {
351    /// OS name of the interface: the kernel name on unix platforms (`eth0`,
352    /// `en0`), the adapter's friendly name on windows (`Ethernet 1`).
353    #[must_use]
354    pub fn name(&self) -> &str {
355        self.name.as_str()
356    }
357
358    /// OS interface index, when known.
359    ///
360    /// On windows this is the IPv4 interface index, falling back to the IPv6
361    /// one — the two are distinct numbering spaces there. For the zone of a
362    /// scoped IPv6 address use [`InterfaceAddress::scope_id`] instead.
363    #[must_use]
364    pub fn index(&self) -> Option<u32> {
365        self.index
366    }
367
368    /// Status and capability flags of this interface.
369    #[must_use]
370    pub fn flags(&self) -> InterfaceFlags {
371        self.flags
372    }
373
374    /// Whether the interface is up, administratively and operationally.
375    #[must_use]
376    pub fn is_up(&self) -> bool {
377        self.flags
378            .contains(InterfaceFlags::UP | InterfaceFlags::RUNNING)
379    }
380
381    /// Link-layer (MAC) address, when one is reported.
382    #[must_use]
383    pub fn hardware_address(&self) -> Option<&HardwareAddress> {
384        self.hw_address.as_ref()
385    }
386
387    /// Human-readable adapter description. Only reported on windows.
388    #[must_use]
389    pub fn description(&self) -> Option<&str> {
390        self.description.as_deref()
391    }
392
393    /// The IP addresses assigned to this interface, in OS order.
394    #[must_use]
395    pub fn addresses(&self) -> &[InterfaceAddress] {
396        &self.addresses
397    }
398
399    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
400    #[cfg_attr(
401        docsrs,
402        doc(cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))
403    )]
404    /// This interface's name as a [`DeviceName`], usable with
405    /// [`SocketOptions::device`] to bind a socket to it.
406    ///
407    /// `None` when the kernel name does not pass [`DeviceName`] validation.
408    ///
409    /// [`DeviceName`]: super::DeviceName
410    /// [`SocketOptions::device`]: super::SocketOptions::device
411    #[must_use]
412    pub fn device_name(&self) -> Option<super::DeviceName> {
413        super::DeviceName::try_from(self.name.as_str()).ok()
414    }
415}
416
417/// Enumerate the host's network interfaces and their assigned addresses.
418///
419/// Interfaces and addresses are returned in the order the operating system
420/// reports them; no ordering is guaranteed beyond that. Scoped IPv6 addresses
421/// (e.g. link-local) carry their zone via [`InterfaceAddress::scope_id`].
422///
423/// Supported on linux, android, apple platforms and windows; any other
424/// platform errors with [`io::ErrorKind::Unsupported`]. The call performs a
425/// cheap but blocking system call; hot async paths may want to wrap it in a
426/// blocking task.
427#[inline(always)]
428pub fn interfaces() -> io::Result<Vec<Interface>> {
429    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
430    {
431        unix::interfaces()
432    }
433
434    #[cfg(target_os = "windows")]
435    {
436        windows::interfaces()
437    }
438
439    #[cfg(not(any(
440        target_os = "linux",
441        target_os = "android",
442        target_vendor = "apple",
443        target_os = "windows"
444    )))]
445    {
446        Err(io::Error::new(
447            io::ErrorKind::Unsupported,
448            "network interface enumeration is not supported on this platform",
449        ))
450    }
451}
452
453/// Every address assigned to an interface that is up (administratively and
454/// operationally), filtered to the given [`IpScopes`], deduplicated, in the
455/// order the platform reported them.
456///
457/// See [`interfaces`] for platform support; scope classification is
458/// [`ip_scope`]'s.
459pub fn local_addresses(scopes: IpScopes) -> io::Result<Vec<IpAddr>> {
460    Ok(collect_local_addresses(&interfaces()?, scopes))
461}
462
463/// The address the OS would send from to reach `destination`, without
464/// sending anything: connecting a UDP socket only sets the peer.
465///
466/// This answers what routing would choose, where [`local_addresses`] answers
467/// what exists. `None` when no route is available, or when the socket
468/// reports an unspecified address.
469pub fn route_source_address(destination: std::net::SocketAddr) -> Option<IpAddr> {
470    let bind: std::net::SocketAddr = if destination.is_ipv6() {
471        (std::net::Ipv6Addr::UNSPECIFIED, 0).into()
472    } else {
473        (std::net::Ipv4Addr::UNSPECIFIED, 0).into()
474    };
475
476    let socket = std::net::UdpSocket::bind(bind).ok()?;
477    socket.connect(destination).ok()?;
478    let address = socket.local_addr().ok()?.ip();
479    (!address.is_unspecified()).then_some(address)
480}
481
482fn collect_local_addresses(interfaces: &[Interface], scopes: IpScopes) -> Vec<IpAddr> {
483    let mut out = Vec::new();
484    for interface in interfaces {
485        if !interface.is_up() {
486            continue;
487        }
488        for address in interface.addresses() {
489            let ip = address.address();
490            if scopes.intersects(ip_scope(ip)) && !out.contains(&ip) {
491                out.push(ip);
492            }
493        }
494    }
495    out
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use std::net::{Ipv4Addr, Ipv6Addr};
502
503    fn interface(name: &str, flags: InterfaceFlags, addresses: &[IpAddr]) -> Interface {
504        Interface {
505            name: SmolStr::new(name),
506            index: None,
507            flags,
508            hw_address: None,
509            description: None,
510            addresses: addresses
511                .iter()
512                .map(|addr| InterfaceAddress::new(*addr, None, None))
513                .collect(),
514        }
515    }
516
517    const UP: InterfaceFlags = InterfaceFlags::UP.union(InterfaceFlags::RUNNING);
518
519    #[test]
520    fn collect_skips_interfaces_that_are_not_up() {
521        let ip: IpAddr = Ipv4Addr::new(192, 168, 1, 7).into();
522        let ifaces = [
523            interface("down0", InterfaceFlags::UP, &[ip]),
524            interface("off0", InterfaceFlags::empty(), &[ip]),
525        ];
526        assert!(collect_local_addresses(&ifaces, IpScopes::all()).is_empty());
527    }
528
529    #[test]
530    fn collect_filters_by_scope() {
531        let loopback: IpAddr = Ipv4Addr::LOCALHOST.into();
532        let private: IpAddr = Ipv4Addr::new(10, 0, 0, 1).into();
533        let global: IpAddr = Ipv4Addr::new(1, 1, 1, 1).into();
534        let ifaces = [interface("eth0", UP, &[loopback, private, global])];
535
536        assert_eq!(
537            collect_local_addresses(&ifaces, IpScopes::GLOBAL),
538            vec![global]
539        );
540        assert_eq!(
541            collect_local_addresses(&ifaces, IpScopes::LOOPBACK | IpScopes::PRIVATE),
542            vec![loopback, private]
543        );
544    }
545
546    #[test]
547    fn collect_dedupes_preserving_first_seen_order() {
548        let a: IpAddr = Ipv4Addr::new(10, 0, 0, 1).into();
549        let b: IpAddr = Ipv6Addr::LOCALHOST.into();
550        let ifaces = [
551            interface("eth0", UP, &[a, b]),
552            interface("eth1", UP, &[b, a]),
553        ];
554        assert_eq!(
555            collect_local_addresses(&ifaces, IpScopes::all()),
556            vec![a, b]
557        );
558    }
559
560    #[test]
561    fn hardware_address_validation_and_display() {
562        let _ = HardwareAddress::try_from(&[][..]).unwrap_err();
563        let _ = HardwareAddress::try_from(&[0u8; 6][..]).unwrap_err();
564        let _ = HardwareAddress::try_from(&[1u8; 9][..]).unwrap_err();
565
566        let mac = HardwareAddress::try_from(&[0xaa, 0xbb, 0xcc, 0x0d, 0xee, 0xff][..]).unwrap();
567        assert_eq!(mac.to_string(), "aa:bb:cc:0d:ee:ff");
568        assert_eq!(mac.as_bytes(), &[0xaa, 0xbb, 0xcc, 0x0d, 0xee, 0xff]);
569    }
570
571    #[test]
572    fn interface_address_prefix_and_display() {
573        let addr = InterfaceAddress::new(Ipv4Addr::new(192, 168, 1, 7).into(), Some(24), None);
574        assert_eq!(addr.prefix_len(), Some(24));
575        assert_eq!(addr.to_string(), "192.168.1.7/24");
576        let net = addr.ip_net().unwrap();
577        assert!(net.contains(&addr.address()));
578        assert_eq!(net.trunc().to_string(), "192.168.1.0/24");
579
580        // out-of-range prefix is dropped
581        let addr = InterfaceAddress::new(Ipv4Addr::new(192, 168, 1, 7).into(), Some(33), None);
582        assert_eq!(addr.prefix_len(), None);
583        assert!(addr.ip_net().is_none());
584        assert_eq!(addr.to_string(), "192.168.1.7");
585    }
586
587    #[test]
588    fn interface_address_scope_id() {
589        let link_local: IpAddr = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1).into();
590        let addr = InterfaceAddress::new(link_local, Some(64), Some(3));
591        assert_eq!(addr.scope_id(), Some(3));
592        assert_eq!(addr.to_string(), "fe80::1%3/64");
593        assert_eq!(addr.to_string().parse::<InterfaceAddress>().unwrap(), addr);
594
595        // a zero zone means "no zone", and ipv4 addresses never carry one
596        assert_eq!(
597            InterfaceAddress::new(link_local, None, Some(0)).scope_id(),
598            None
599        );
600        assert_eq!(
601            InterfaceAddress::new(Ipv4Addr::new(10, 0, 0, 1).into(), None, Some(3)).scope_id(),
602            None
603        );
604    }
605
606    #[test]
607    fn interface_getters() {
608        let mac: HardwareAddress = "aa:bb:cc:0d:ee:ff".parse().unwrap();
609        let addr: InterfaceAddress = "192.168.1.7/24".parse().unwrap();
610        let iface = Interface {
611            name: SmolStr::new("eth0"),
612            index: Some(3),
613            flags: UP | InterfaceFlags::MULTICAST,
614            hw_address: Some(mac),
615            description: Some(SmolStr::new("some adapter")),
616            addresses: vec![addr],
617        };
618
619        assert_eq!(iface.name(), "eth0");
620        assert_eq!(iface.index(), Some(3));
621        assert_eq!(iface.flags(), UP | InterfaceFlags::MULTICAST);
622        assert!(iface.is_up());
623        assert_eq!(iface.hardware_address(), Some(&mac));
624        assert_eq!(iface.description(), Some("some adapter"));
625        assert_eq!(iface.addresses(), &[addr]);
626
627        let down = Interface {
628            flags: InterfaceFlags::UP,
629            ..iface
630        };
631        assert!(!down.is_up());
632    }
633
634    #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
635    #[test]
636    fn interface_device_name() {
637        let mut iface = Interface {
638            name: SmolStr::new("eth0"),
639            index: None,
640            flags: InterfaceFlags::empty(),
641            hw_address: None,
642            description: None,
643            addresses: Vec::new(),
644        };
645        assert_eq!(
646            iface.device_name().map(|name| name.as_str().to_owned()),
647            Some("eth0".to_owned())
648        );
649
650        // kernel names that fail DeviceName validation yield None
651        iface.name = SmolStr::new("6in4-wan");
652        assert!(iface.device_name().is_none());
653    }
654
655    #[test]
656    fn hardware_address_boundaries_and_debug() {
657        // 8 bytes is the maximum and is valid
658        let mac = HardwareAddress::try_from(&[1u8; 8][..]).unwrap();
659        assert_eq!(mac.as_bytes(), &[1u8; 8]);
660        assert_eq!(mac.to_string().parse::<HardwareAddress>().unwrap(), mac);
661
662        let mac: HardwareAddress = "aa:bb:cc:0d:ee:ff".parse().unwrap();
663        assert_eq!(format!("{mac:?}"), "HardwareAddress(aa:bb:cc:0d:ee:ff)");
664    }
665
666    #[test]
667    fn interface_address_parse_boundary_prefixes() {
668        assert_eq!(
669            "1.2.3.4/32"
670                .parse::<InterfaceAddress>()
671                .unwrap()
672                .prefix_len(),
673            Some(32)
674        );
675        assert_eq!(
676            "::1/128".parse::<InterfaceAddress>().unwrap().prefix_len(),
677            Some(128)
678        );
679    }
680
681    #[test]
682    fn interface_flags_display_from_str_roundtrip() {
683        for (name, flag) in FLAG_NAMES {
684            assert_eq!(flag.to_string(), *name);
685            assert_eq!(name.parse::<InterfaceFlags>().unwrap(), *flag);
686        }
687
688        let flags = InterfaceFlags::UP | InterfaceFlags::RUNNING | InterfaceFlags::POINT_TO_POINT;
689        assert_eq!(flags.to_string(), "up,running,point-to-point");
690        assert_eq!(flags.to_string().parse::<InterfaceFlags>().unwrap(), flags);
691
692        assert_eq!(
693            "UP|POINT_TO_POINT".parse::<InterfaceFlags>().unwrap(),
694            InterfaceFlags::UP | InterfaceFlags::POINT_TO_POINT
695        );
696        assert_eq!(
697            "".parse::<InterfaceFlags>().unwrap(),
698            InterfaceFlags::empty()
699        );
700        let err = "bogus".parse::<InterfaceFlags>().unwrap_err();
701        assert!(err.to_string().contains("bogus"), "err: {err}");
702    }
703
704    #[test]
705    fn hardware_address_from_str() {
706        let mac: HardwareAddress = "aa:bb:cc:0d:ee:ff".parse().unwrap();
707        assert_eq!(mac.as_bytes(), &[0xaa, 0xbb, 0xcc, 0x0d, 0xee, 0xff]);
708        assert_eq!(mac.to_string().parse::<HardwareAddress>().unwrap(), mac);
709
710        // windows-style separator and uppercase hex
711        assert_eq!("AA-BB-CC-0D-EE-FF".parse::<HardwareAddress>().unwrap(), mac);
712
713        let _ = "".parse::<HardwareAddress>().unwrap_err();
714        let _ = "aa:b:cc".parse::<HardwareAddress>().unwrap_err();
715        let _ = "aa:+f:cc".parse::<HardwareAddress>().unwrap_err();
716        let _ = "00:00:00:00:00:00".parse::<HardwareAddress>().unwrap_err();
717        let _ = "aa:bb:cc:dd:ee:ff:00:11:22"
718            .parse::<HardwareAddress>()
719            .unwrap_err();
720    }
721
722    #[test]
723    fn interface_address_from_str() {
724        let addr: InterfaceAddress = "192.168.1.7/24".parse().unwrap();
725        assert_eq!(addr.address(), IpAddr::from(Ipv4Addr::new(192, 168, 1, 7)));
726        assert_eq!(addr.prefix_len(), Some(24));
727        assert_eq!(addr.to_string().parse::<InterfaceAddress>().unwrap(), addr);
728
729        let addr: InterfaceAddress = "fe80::1".parse().unwrap();
730        assert_eq!(addr.prefix_len(), None);
731
732        let addr: InterfaceAddress = "fe80::1%3/64".parse().unwrap();
733        assert_eq!(addr.scope_id(), Some(3));
734        assert_eq!(addr.prefix_len(), Some(64));
735        let addr: InterfaceAddress = "fe80::1%0".parse().unwrap();
736        assert_eq!(addr.scope_id(), None);
737
738        let _ = "192.168.1.7/33".parse::<InterfaceAddress>().unwrap_err();
739        let _ = "192.168.1.7/x".parse::<InterfaceAddress>().unwrap_err();
740        let _ = "192.168.1.7%3".parse::<InterfaceAddress>().unwrap_err();
741        let _ = "fe80::1%x".parse::<InterfaceAddress>().unwrap_err();
742        let _ = "not-an-ip".parse::<InterfaceAddress>().unwrap_err();
743    }
744
745    #[test]
746    fn serde_string_roundtrips() {
747        let flags = InterfaceFlags::UP | InterfaceFlags::MULTICAST;
748        let json = serde_json::to_string(&flags).unwrap();
749        assert_eq!(json, "\"up,multicast\"");
750        assert_eq!(
751            serde_json::from_str::<InterfaceFlags>(&json).unwrap(),
752            flags
753        );
754
755        let mac: HardwareAddress = "aa:bb:cc:0d:ee:ff".parse().unwrap();
756        let json = serde_json::to_string(&mac).unwrap();
757        assert_eq!(json, "\"aa:bb:cc:0d:ee:ff\"");
758        assert_eq!(serde_json::from_str::<HardwareAddress>(&json).unwrap(), mac);
759
760        let addr: InterfaceAddress = "10.0.0.1/8".parse().unwrap();
761        let json = serde_json::to_string(&addr).unwrap();
762        assert_eq!(json, "\"10.0.0.1/8\"");
763        assert_eq!(
764            serde_json::from_str::<InterfaceAddress>(&json).unwrap(),
765            addr
766        );
767
768        let addr: InterfaceAddress = "fe80::1%3/64".parse().unwrap();
769        let json = serde_json::to_string(&addr).unwrap();
770        assert_eq!(json, "\"fe80::1%3/64\"");
771        assert_eq!(
772            serde_json::from_str::<InterfaceAddress>(&json).unwrap(),
773            addr
774        );
775    }
776
777    // live enumeration hits real OS calls: not available under miri
778    #[cfg(all(
779        any(
780            target_os = "linux",
781            target_os = "android",
782            target_vendor = "apple",
783            target_os = "windows"
784        ),
785        not(miri)
786    ))]
787    mod live {
788        use super::*;
789
790        #[test]
791        fn enumerates_loopback() {
792            let ifaces = interfaces().unwrap();
793            assert!(!ifaces.is_empty());
794            assert!(
795                ifaces
796                    .iter()
797                    .any(|i| i.flags().contains(InterfaceFlags::LOOPBACK))
798            );
799            // a loopback address of either family; single-family hosts
800            // (e.g. ipv6-disabled) are valid configurations
801            assert!(
802                ifaces
803                    .iter()
804                    .flat_map(Interface::addresses)
805                    .any(|a| a.address().is_loopback())
806            );
807            assert!(ifaces.iter().any(Interface::is_up));
808        }
809
810        #[test]
811        fn local_addresses_scope_invariants() {
812            let all = local_addresses(IpScopes::all()).unwrap();
813            let global = local_addresses(IpScopes::GLOBAL).unwrap();
814
815            assert!(all.iter().any(|ip| ip.is_loopback()));
816            for ip in &global {
817                assert!(all.contains(ip));
818                assert!(
819                    !ip_scope(*ip).intersects(IpScopes::LOOPBACK | IpScopes::LINK_LOCAL),
820                    "global result contains non-global address: {ip}"
821                );
822            }
823            // deduplicated
824            for (i, ip) in all.iter().enumerate() {
825                assert!(!all[..i].contains(ip), "duplicate address: {ip}");
826            }
827        }
828    }
829
830    #[test]
831    fn route_source_address_answers_for_loopback() {
832        // loopback always has a route, so this cannot depend on the network
833        let source = route_source_address((std::net::Ipv4Addr::LOCALHOST, 53).into());
834        assert_eq!(source, Some(std::net::Ipv4Addr::LOCALHOST.into()));
835
836        // and it never reports the address it bound to
837        if let Some(source) = route_source_address((std::net::Ipv6Addr::LOCALHOST, 53).into()) {
838            assert!(!source.is_unspecified(), "{source}");
839            assert!(source.is_ipv6(), "{source}");
840        }
841    }
842}