Skip to main content

viva_gige/
nic.rs

1//! Network interface utilities for GigE Vision streaming.
2//!
3//! This module provides helpers for querying network interface capabilities and
4//! constructing UDP sockets tuned for high-throughput GVSP traffic. The
5//! functionality is intentionally conservative so it can operate on most Unix
6//! like systems without additional privileges. Platform specific code paths are
7//! gated via conditional compilation and otherwise fall back to sane defaults.
8
9use std::collections::VecDeque;
10#[cfg(any(target_os = "linux", target_os = "android"))]
11use std::fs;
12use std::io;
13use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket as StdUdpSocket};
14use std::sync::Mutex;
15
16use bytes::BytesMut;
17use if_addrs::IfAddr;
18use socket2::{Domain, Protocol, SockRef, Socket, Type};
19use tokio::net::UdpSocket;
20use tracing::{debug, info};
21
22#[cfg(any(target_os = "linux", target_os = "android", windows))]
23use tracing::warn;
24
25/// Default socket receive buffer size used when the caller does not provide a
26/// custom value. The number mirrors what many operating systems allow without
27/// requiring elevated privileges.
28pub const DEFAULT_RCVBUF_BYTES: usize = 4 << 20; // 4 MiB
29
30/// Maximum length for interface names. On Linux this matches `IFNAMSIZ - 1`
31/// (15). On Windows, interface names are GUIDs like
32/// `{F5DA665D-1913-11F1-9F17-806E6F6E6963}` (38 chars), so a larger limit
33/// is needed.
34#[cfg(not(target_os = "windows"))]
35const IFACE_NAME_MAX: usize = 15;
36#[cfg(target_os = "windows")]
37const IFACE_NAME_MAX: usize = 64;
38
39/// Resolve an interface index from its name using platform-specific APIs.
40#[cfg(any(target_os = "linux", target_os = "android"))]
41fn iface_name_to_index(name: &str) -> io::Result<u32> {
42    let index_path = format!("/sys/class/net/{name}/ifindex");
43    fs::read_to_string(&index_path)
44        .map_err(|err| io::Error::new(err.kind(), format!("{index_path}: {err}")))?
45        .trim()
46        .parse::<u32>()
47        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
48}
49
50/// Resolve an interface index from its name using `if_nametoindex(3)`.
51#[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
52fn iface_name_to_index(name: &str) -> io::Result<u32> {
53    use std::ffi::CString;
54
55    let c_name = CString::new(name).map_err(|_| {
56        io::Error::new(
57            io::ErrorKind::InvalidInput,
58            format!("interface name contains null byte: '{name}'"),
59        )
60    })?;
61    // SAFETY: `if_nametoindex` is a POSIX function that takes a valid C string.
62    let index = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
63    if index == 0 {
64        Err(io::Error::new(
65            io::ErrorKind::NotFound,
66            format!("interface '{name}' not found"),
67        ))
68    } else {
69        Ok(index)
70    }
71}
72
73/// Resolve an interface index from its name on Windows.
74#[cfg(target_os = "windows")]
75fn iface_name_to_index(name: &str) -> io::Result<u32> {
76    // Last resort only: `Iface::resolve` prefers `if_addrs`'s `index`, which
77    // on Windows comes from the adapter's real `IfIndex`. This positional
78    // fallback runs only when the OS reported index 0 ("unknown"), and its
79    // result is a guess rather than a kernel interface index.
80    for (idx, iface) in if_addrs::get_if_addrs()?.iter().enumerate() {
81        if iface.name == name {
82            return Ok((idx + 1) as u32);
83        }
84    }
85    Err(io::Error::new(
86        io::ErrorKind::NotFound,
87        format!("interface '{name}' not found"),
88    ))
89}
90
91/// Representation of a host network interface.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Iface {
94    name: String,
95    index: u32,
96    ipv4: Option<Ipv4Addr>,
97    ipv6: Option<Ipv6Addr>,
98}
99
100impl Iface {
101    /// Resolve an interface from the operating system by its name.
102    ///
103    /// When the interface carries several IPv4 addresses the first one is
104    /// used; [`Iface::from_ipv4`] preserves the address that was asked for.
105    pub fn from_system(name: &str) -> io::Result<Self> {
106        Self::resolve(name, None)
107    }
108
109    /// Resolve an interface by one of its IPv4 addresses.
110    ///
111    /// The resulting [`Iface`] reports `addr` itself, not whichever address
112    /// the OS happens to list last for that interface. A multi-homed NIC —
113    /// a stale DHCP lease alongside a link-local address, say — would
114    /// otherwise resolve to a different address than the caller selected
115    /// and bind the socket to the wrong one (#57).
116    pub fn from_ipv4(addr: Ipv4Addr) -> io::Result<Self> {
117        for iface in if_addrs::get_if_addrs()? {
118            if let IfAddr::V4(v4) = iface.addr
119                && v4.ip == addr
120            {
121                return Self::resolve(&iface.name, Some(addr));
122            }
123        }
124        Err(io::Error::new(
125            io::ErrorKind::NotFound,
126            format!("no interface with IPv4 {addr}"),
127        ))
128    }
129
130    /// Resolve the local interface selected by the operating system for a
131    /// remote IPv4 address.
132    pub fn from_remote_ipv4(remote: Ipv4Addr) -> io::Result<Self> {
133        const GVCP_PORT: u16 = 3956;
134
135        let socket = StdUdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).map_err(|err| {
136            io::Error::new(
137                err.kind(),
138                format!("failed to create route probe for remote IPv4 {remote}: {err}"),
139            )
140        })?;
141        socket.connect((remote, GVCP_PORT)).map_err(|err| {
142            io::Error::new(
143                err.kind(),
144                format!("failed to resolve route to remote IPv4 {remote}: {err}"),
145            )
146        })?;
147        let local = match socket.local_addr()?.ip() {
148            IpAddr::V4(local) => local,
149            IpAddr::V6(_) => {
150                return Err(io::Error::new(
151                    io::ErrorKind::AddrNotAvailable,
152                    format!("route to remote IPv4 {remote} selected an IPv6 address"),
153                ));
154            }
155        };
156
157        Self::from_ipv4(local).map_err(|err| {
158            io::Error::new(
159                err.kind(),
160                format!(
161                    "route to remote IPv4 {remote} selected local IPv4 {local}, \
162                     but its interface could not be resolved: {err}"
163                ),
164            )
165        })
166    }
167
168    /// Every interface the operating system reports, one entry per name.
169    ///
170    /// This is the library's own view of the machine, which is the point: #57
171    /// was an interface that existed but that we could not see, and no amount
172    /// of `ipconfig` output would have shown that. Ordered by name so two
173    /// runs can be diffed.
174    pub fn list() -> io::Result<Vec<Self>> {
175        let mut names: Vec<String> = if_addrs::get_if_addrs()?
176            .into_iter()
177            .map(|iface| iface.name)
178            .collect();
179        names.sort();
180        names.dedup();
181        Ok(names
182            .into_iter()
183            .filter_map(|name| Self::from_system(&name).ok())
184            .collect())
185    }
186
187    /// Every IPv4 address assigned to this interface.
188    ///
189    /// [`Iface::ipv4`] reports the one the socket will bind to; a multi-homed
190    /// NIC has more, and which ones they are is exactly the question #57
191    /// turned on.
192    pub fn all_ipv4(&self) -> io::Result<Vec<Ipv4Addr>> {
193        Ok(if_addrs::get_if_addrs()?
194            .into_iter()
195            .filter(|iface| iface.name == self.name)
196            .filter_map(|iface| match iface.addr {
197                IfAddr::V4(v4) => Some(v4.ip),
198                IfAddr::V6(_) => None,
199            })
200            .collect())
201    }
202
203    fn resolve(name: &str, preferred: Option<Ipv4Addr>) -> io::Result<Self> {
204        if name.is_empty() || name.len() > IFACE_NAME_MAX {
205            return Err(io::Error::new(
206                io::ErrorKind::InvalidInput,
207                format!("invalid interface name '{name}'"),
208            ));
209        }
210
211        let mut ipv4: Option<Ipv4Addr> = None;
212        let mut ipv6 = None;
213        // `if_addrs` reports the kernel's own interface index where the
214        // platform exposes it, which is what multicast joins need.
215        let mut index: Option<u32> = None;
216        let mut found = false;
217        for iface in if_addrs::get_if_addrs()? {
218            if iface.name != name {
219                continue;
220            }
221            found = true;
222            index = index.or(iface.index);
223            match iface.addr {
224                IfAddr::V4(v4) => {
225                    // Take the requested address if we see it, otherwise the
226                    // first one, and never let a later address displace it.
227                    if Some(v4.ip) == preferred || ipv4.is_none() {
228                        ipv4 = Some(v4.ip);
229                    }
230                }
231                IfAddr::V6(v6) => {
232                    if ipv6.is_none() {
233                        ipv6 = Some(v6.ip);
234                    }
235                }
236            }
237        }
238
239        // An interface with no address at all is still resolvable by name.
240        let index = match index {
241            Some(index) => index,
242            None => iface_name_to_index(name)?,
243        };
244        if !found && ipv4.is_none() && ipv6.is_none() {
245            // `iface_name_to_index` already errors for an unknown name on
246            // every platform, so reaching here means the name is valid but
247            // unaddressed.
248            debug!(name, "interface has no assigned IP address");
249        }
250
251        Ok(Self {
252            name: name.to_string(),
253            index,
254            ipv4,
255            ipv6,
256        })
257    }
258
259    /// Interface name as provided by the operating system (e.g. `eth0`).
260    pub fn name(&self) -> &str {
261        &self.name
262    }
263
264    /// Interface index as reported by the kernel. The index is used by some
265    /// socket options (e.g. multicast subscriptions).
266    pub fn index(&self) -> u32 {
267        self.index
268    }
269
270    /// Primary IPv4 address associated with the interface, if any.
271    pub fn ipv4(&self) -> Option<Ipv4Addr> {
272        self.ipv4
273    }
274
275    /// Primary IPv6 address associated with the interface, if any.
276    #[allow(dead_code)]
277    pub fn ipv6(&self) -> Option<Ipv6Addr> {
278        self.ipv6
279    }
280}
281
282/// How a user names a host interface on a command line or in an API call.
283///
284/// Parsed as an IPv4 literal first and taken as an operating-system interface
285/// name otherwise. The two spellings cannot collide: an interface name that
286/// parses as an IPv4 address does not exist on any platform we support.
287///
288/// Both spellings are needed. A Windows interface name is a GUID
289/// (`{6394C55F-F630-4BC7-92D2-7AC320C73D1C}`), which a user has no easy way to
290/// obtain, so IPv4 has to work; and an interface that carries no address yet
291/// can only be named, so the name has to work too. Before this existed
292/// `viva-camctl` accepted only the address and `viva-service` only the name,
293/// and a user who had discovered a camera with one tool could not stream it
294/// with the other ([#109](https://github.com/VitalyVorobyev/viva-genicam/issues/109)).
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum IfaceSelector {
297    /// One of the interface's own IPv4 addresses, e.g. `169.254.105.106`.
298    Ipv4(Ipv4Addr),
299    /// The name the operating system gives the interface, e.g. `en0`.
300    Name(String),
301}
302
303impl IfaceSelector {
304    /// Resolve the selector against the interfaces this host reports.
305    ///
306    /// Delegates to [`Iface::from_ipv4`] or [`Iface::from_system`]; the only
307    /// thing added here is the failure message, which lists what we can
308    /// actually see. That listing is the point rather than a nicety: the
309    /// Windows GUID a user needs is not discoverable from the error it
310    /// replaces, and `Iface::list` is already the library's own view of the
311    /// machine (#57).
312    pub fn resolve(&self) -> io::Result<Iface> {
313        let resolved = match self {
314            Self::Ipv4(addr) => Iface::from_ipv4(*addr),
315            Self::Name(name) => Iface::from_system(name),
316        };
317        resolved.map_err(|err| io::Error::new(err.kind(), format!("{err}{}", available())))
318    }
319}
320
321impl std::str::FromStr for IfaceSelector {
322    type Err = std::convert::Infallible;
323
324    fn from_str(s: &str) -> Result<Self, Self::Err> {
325        Ok(match s.parse::<Ipv4Addr>() {
326            Ok(addr) => Self::Ipv4(addr),
327            Err(_) => Self::Name(s.to_string()),
328        })
329    }
330}
331
332impl std::fmt::Display for IfaceSelector {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        match self {
335            Self::Ipv4(addr) => write!(f, "{addr}"),
336            Self::Name(name) => f.write_str(name),
337        }
338    }
339}
340
341/// The interfaces this host reports, formatted for appending to an error.
342///
343/// Returns an empty string when the enumeration itself fails, so a failure to
344/// explain never replaces the failure being explained.
345fn available() -> String {
346    let Ok(ifaces) = Iface::list() else {
347        return String::new();
348    };
349    if ifaces.is_empty() {
350        return String::new();
351    }
352    let mut out = String::from("\n  interfaces this host reports:");
353    for iface in ifaces {
354        let addrs = iface.all_ipv4().unwrap_or_default();
355        out.push_str(&format!("\n    {}", iface.name()));
356        if !addrs.is_empty() {
357            let list: Vec<String> = addrs.iter().map(|a| a.to_string()).collect();
358            out.push_str(&format!("  {}", list.join(", ")));
359        }
360    }
361    out
362}
363
364/// Read the MTU configured for the provided interface.
365///
366/// On Linux the value is obtained from `/sys/class/net/<iface>/mtu` to avoid
367/// platform specific `ioctl` calls. The function falls back to the canonical
368/// Ethernet MTU (1500 bytes) when the information cannot be fetched.
369pub fn mtu(_iface: &Iface) -> io::Result<u32> {
370    #[cfg(target_os = "linux")]
371    {
372        let path = format!("/sys/class/net/{}/mtu", _iface.name());
373        match fs::read_to_string(path) {
374            Ok(contents) => {
375                let mtu = contents
376                    .trim()
377                    .parse::<u32>()
378                    .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
379                tracing::debug!(name = _iface.name(), mtu, "resolved interface MTU");
380                return Ok(mtu);
381            }
382            Err(err) => {
383                warn!(name = _iface.name(), error = %err, "failed to read MTU, using default");
384            }
385        }
386    }
387
388    #[cfg(windows)]
389    {
390        use windows_sys::Win32::NetworkManagement::IpHelper::{GetIfEntry2, MIB_IF_ROW2};
391
392        let mut row = MIB_IF_ROW2 {
393            InterfaceIndex: _iface.index(),
394            ..Default::default()
395        };
396        let status = unsafe { GetIfEntry2(&mut row) };
397        if status == 0 {
398            tracing::debug!(
399                name = _iface.name(),
400                mtu = row.Mtu,
401                "resolved interface MTU"
402            );
403            return Ok(row.Mtu);
404        }
405
406        let err = io::Error::from_raw_os_error(status as i32);
407        warn!(
408            name = _iface.name(),
409            error = %err,
410            "failed to read MTU, using default"
411        );
412    }
413
414    Ok(1500)
415}
416
417/// Largest packet an IPv4 datagram can carry.
418///
419/// The IPv4 total-length field is 16 bits, so no datagram can exceed this no
420/// matter how large the link MTU claims to be. Loopback interfaces routinely
421/// report more: Linux `lo` is 65536 and macOS `lo0` is 16384.
422const MAX_IPV4_PACKET_SIZE: u32 = 65535;
423
424/// Compute an optimal GVSP packet size from the link MTU.
425///
426/// `GevSCPSPacketSize` is the transmitted IP packet size, so it tracks the link
427/// MTU rather than subtracting Ethernet, IPv4, or UDP headers — corroborated by
428/// aravis, which derives its payload block size as
429/// `packet_size - (IP + UDP + GVSP headers)` and so excludes L2 from the
430/// negotiated value (`ARV_GVSP_PACKET_PROTOCOL_OVERHEAD`).
431///
432/// The MTU is clamped to 65535, the largest an IPv4 datagram can be, because an
433/// unclamped value is not merely suboptimal, it is unsendable: on Linux
434/// loopback (MTU 65536) the
435/// sender would build a `65536 - 36` byte payload and a 65508-byte UDP datagram,
436/// one byte past the 65507-byte maximum, and `send_to` fails for every packet.
437pub fn best_packet_size(mtu: u32) -> u32 {
438    mtu.min(MAX_IPV4_PACKET_SIZE)
439}
440
441/// Multicast socket options applied while binding.
442#[derive(Debug, Clone)]
443pub struct McOptions {
444    /// Whether multicast packets sent locally should be looped back.
445    pub loopback: bool,
446    /// IPv4 time-to-live for outbound multicast packets.
447    pub ttl: u32,
448    /// Receive buffer size in bytes.
449    pub rcvbuf_bytes: usize,
450    /// Whether to enable address/port reuse when binding.
451    pub reuse_addr: bool,
452}
453
454impl Default for McOptions {
455    fn default() -> Self {
456        Self {
457            loopback: false,
458            ttl: 1,
459            rcvbuf_bytes: DEFAULT_RCVBUF_BYTES,
460            reuse_addr: true,
461        }
462    }
463}
464
465/// Bind a UDP socket configured for GVSP traffic.
466pub async fn bind_udp(
467    bind: IpAddr,
468    port: u16,
469    iface: Option<Iface>,
470    recv_buffer: Option<usize>,
471) -> io::Result<UdpSocket> {
472    let recv_buffer = recv_buffer.unwrap_or(DEFAULT_RCVBUF_BYTES);
473    if let Some(ipv4) = iface.as_ref().and_then(|iface| iface.ipv4()) {
474        info!(name = iface.as_ref().map(Iface::name), %ipv4, port, "binding GVSP socket");
475    } else {
476        info!(%bind, port, "binding GVSP socket");
477    }
478
479    let domain = match bind {
480        IpAddr::V4(_) => Domain::IPV4,
481        IpAddr::V6(_) => Domain::IPV6,
482    };
483    let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
484
485    socket.set_reuse_address(true)?;
486    #[cfg(all(unix, not(target_os = "solaris")))]
487    socket.set_reuse_port(true)?;
488
489    socket.set_recv_buffer_size(recv_buffer)?;
490    let actual_recv_buffer = socket.recv_buffer_size()?;
491    debug!(
492        requested_recv_buffer = recv_buffer,
493        actual_recv_buffer, "configured GVSP receive buffer"
494    );
495
496    #[cfg(any(target_os = "linux", target_os = "android"))]
497    if let Some(iface) = iface.as_ref()
498        && let Err(err) = socket.bind_device(Some(iface.name().as_bytes()))
499    {
500        warn!(name = iface.name(), error = %err, "SO_BINDTODEVICE failed");
501    }
502
503    let addr = SocketAddr::new(bind, port);
504    socket.bind(&addr.into())?;
505
506    let std_socket: std::net::UdpSocket = socket.into();
507    std_socket.set_nonblocking(true)?;
508    UdpSocket::from_std(std_socket)
509}
510
511fn validate_multicast_inputs(group: Ipv4Addr, ttl: u32) -> io::Result<()> {
512    if ttl > 255 {
513        return Err(io::Error::new(
514            io::ErrorKind::InvalidInput,
515            "multicast TTL must be <= 255",
516        ));
517    }
518    if (group.octets()[0] & 0xF0) != 0xE0 {
519        return Err(io::Error::new(
520            io::ErrorKind::InvalidInput,
521            "multicast group must be within 224.0.0.0/4",
522        ));
523    }
524    Ok(())
525}
526
527/// Bind a UDP socket subscribed to the provided multicast group on the interface.
528pub async fn bind_multicast(
529    iface: &Iface,
530    group: Ipv4Addr,
531    port: u16,
532    opts: &McOptions,
533) -> io::Result<UdpSocket> {
534    validate_multicast_inputs(group, opts.ttl)?;
535    let iface_addr = iface
536        .ipv4()
537        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "interface lacks IPv4"))?;
538
539    info!(name = iface.name(), %group, port, "binding multicast GVSP socket");
540
541    let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
542
543    if opts.reuse_addr {
544        socket.set_reuse_address(true)?;
545        #[cfg(all(unix, not(target_os = "solaris")))]
546        socket.set_reuse_port(true)?;
547    }
548
549    socket.set_recv_buffer_size(opts.rcvbuf_bytes)?;
550    socket.set_multicast_loop_v4(opts.loopback)?;
551    socket.set_multicast_ttl_v4(opts.ttl)?;
552    socket.set_multicast_if_v4(&iface_addr)?;
553
554    #[cfg(any(target_os = "linux", target_os = "android"))]
555    if let Err(err) = socket.bind_device(Some(iface.name().as_bytes())) {
556        warn!(name = iface.name(), error = %err, "SO_BINDTODEVICE failed");
557    }
558
559    let bind_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
560    socket.bind(&bind_addr.into())?;
561    socket.join_multicast_v4(&group, &iface_addr)?;
562
563    let std_socket: std::net::UdpSocket = socket.into();
564    std_socket.set_nonblocking(true)?;
565    UdpSocket::from_std(std_socket)
566}
567
568/// Subscribe the provided socket to a multicast group on the supplied interface.
569pub fn join_multicast(sock: &UdpSocket, group: Ipv4Addr, iface: &Iface) -> io::Result<()> {
570    let socket = SockRef::from(sock);
571    let iface_addr = iface.ipv4().unwrap_or(Ipv4Addr::UNSPECIFIED);
572    socket.join_multicast_v4(&group, &iface_addr)?;
573    Ok(())
574}
575
576/// Simple lock-free pool for reusable buffers backing frame assembly.
577#[derive(Debug)]
578pub struct BufferPool {
579    buffers: Mutex<VecDeque<BytesMut>>,
580    size: usize,
581}
582
583impl BufferPool {
584    /// Create a pool with the given capacity and buffer size.
585    pub fn new(capacity: usize, size: usize) -> Self {
586        let mut buffers = VecDeque::with_capacity(capacity);
587        for _ in 0..capacity {
588            buffers.push_back(BytesMut::with_capacity(size));
589        }
590        Self {
591            buffers: Mutex::new(buffers),
592            size,
593        }
594    }
595
596    /// Acquire a buffer from the pool.
597    pub fn acquire(&self) -> Option<BytesMut> {
598        self.buffers
599            .lock()
600            .ok()
601            .and_then(|mut guard| guard.pop_front())
602    }
603
604    /// Return a buffer to the pool.
605    pub fn release(&self, mut buffer: BytesMut) {
606        buffer.truncate(0);
607        buffer.reserve(self.size);
608        if let Ok(mut guard) = self.buffers.lock() {
609            guard.push_back(buffer);
610        }
611    }
612}
613
614/// Helper returning the default bind address for discovery convenience.
615pub fn default_bind_addr() -> IpAddr {
616    IpAddr::V4(Ipv4Addr::UNSPECIFIED)
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    #[test]
624    fn reject_invalid_ttl() {
625        let err = validate_multicast_inputs(Ipv4Addr::new(239, 0, 0, 1), 512).unwrap_err();
626        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
627    }
628
629    #[test]
630    fn reject_non_multicast_group() {
631        let err = validate_multicast_inputs(Ipv4Addr::new(192, 168, 1, 1), 1).unwrap_err();
632        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
633    }
634
635    #[test]
636    fn accept_valid_group() {
637        assert!(validate_multicast_inputs(Ipv4Addr::new(239, 192, 1, 10), 1).is_ok());
638    }
639
640    #[test]
641    #[cfg(not(target_os = "windows"))]
642    fn from_system_loopback() {
643        let lo_name = if cfg!(target_os = "macos") {
644            "lo0"
645        } else {
646            "lo"
647        };
648        let iface = Iface::from_system(lo_name).expect("loopback interface should exist");
649        assert!(iface.ipv4().unwrap().is_loopback());
650    }
651
652    #[test]
653    fn from_remote_ipv4_uses_route_selected_local_address() {
654        let remote = Ipv4Addr::new(127, 0, 0, 2);
655        let iface =
656            Iface::from_remote_ipv4(remote).expect("loopback route should select an interface");
657
658        assert_eq!(iface.ipv4(), Some(Ipv4Addr::LOCALHOST));
659        assert_ne!(iface.ipv4(), Some(remote));
660    }
661
662    #[test]
663    fn packet_size_matches_link_mtu() {
664        let mtu = 1500;
665        let size = best_packet_size(mtu);
666        assert_eq!(size, mtu);
667    }
668
669    #[test]
670    fn packet_size_is_clamped_to_the_ipv4_maximum() {
671        // Linux loopback reports MTU 65536. Left unclamped the sender emits a
672        // 65508-byte UDP datagram — one past the 65507-byte maximum — and every
673        // `send_to` fails, so the stream delivers no frames at all.
674        assert_eq!(best_packet_size(65536), MAX_IPV4_PACKET_SIZE);
675        assert_eq!(best_packet_size(u32::MAX), MAX_IPV4_PACKET_SIZE);
676
677        // The clamped value must still leave the datagram inside the UDP limit.
678        const UDP_MAX_PAYLOAD: u32 = 65535 - 20 - 8;
679        let gvsp_payload = best_packet_size(65536) - 20 - 8 - 8;
680        assert!(gvsp_payload + 8 <= UDP_MAX_PAYLOAD);
681    }
682
683    #[test]
684    fn selector_parses_an_ipv4_literal_as_an_address_and_anything_else_as_a_name() {
685        use std::str::FromStr;
686
687        assert_eq!(
688            IfaceSelector::from_str("169.254.105.106").unwrap(),
689            IfaceSelector::Ipv4(Ipv4Addr::new(169, 254, 105, 106))
690        );
691        assert_eq!(
692            IfaceSelector::from_str("en0").unwrap(),
693            IfaceSelector::Name("en0".into())
694        );
695        // The spelling #109's reporter had to use on Windows.
696        let guid = "{6394C55F-F630-4BC7-92D2-7AC320C73D1C}";
697        assert_eq!(
698            IfaceSelector::from_str(guid).unwrap(),
699            IfaceSelector::Name(guid.into())
700        );
701        // Not an address, so a name — and rejected by `Iface::resolve`, which
702        // is where the length check lives.
703        assert_eq!(
704            IfaceSelector::from_str("").unwrap(),
705            IfaceSelector::Name(String::new())
706        );
707        // A partial address is a name, not a parse error: `from_str` cannot
708        // fail, so the diagnosis is deferred to `resolve`.
709        assert_eq!(
710            IfaceSelector::from_str("169.254").unwrap(),
711            IfaceSelector::Name("169.254".into())
712        );
713    }
714
715    #[test]
716    #[cfg(not(target_os = "windows"))]
717    fn both_spellings_of_loopback_resolve_to_the_same_interface() {
718        use std::str::FromStr;
719
720        let lo_name = if cfg!(target_os = "macos") {
721            "lo0"
722        } else {
723            "lo"
724        };
725        let by_name = IfaceSelector::from_str(lo_name).unwrap().resolve().unwrap();
726        let by_addr = IfaceSelector::from_str("127.0.0.1")
727            .unwrap()
728            .resolve()
729            .unwrap();
730
731        assert_eq!(by_name.name(), by_addr.name());
732        assert_eq!(by_addr.ipv4(), Some(Ipv4Addr::LOCALHOST));
733    }
734
735    #[test]
736    fn an_unresolvable_selector_names_the_interfaces_we_can_see() {
737        use std::str::FromStr;
738
739        // The whole reason the selector owns the error: #109's reporter could
740        // not have guessed their adapter's GUID from `no interface with IPv4
741        // 169.254.105.106`.
742        let err = IfaceSelector::from_str("no-such-iface")
743            .unwrap()
744            .resolve()
745            .expect_err("a name no host has must not resolve");
746        let msg = err.to_string();
747        assert!(
748            msg.contains("interfaces this host reports:"),
749            "expected the interface listing, got: {msg}"
750        );
751        assert!(
752            Iface::list()
753                .unwrap()
754                .iter()
755                .any(|iface| msg.contains(iface.name())),
756            "expected a real interface name in: {msg}"
757        );
758    }
759
760    #[test]
761    fn buffer_pool_recycles() {
762        let pool = BufferPool::new(2, 1024);
763        let mut buf = pool.acquire().expect("buffer");
764        buf.extend_from_slice(&[1, 2, 3]);
765        pool.release(buf);
766        let buf2 = pool.acquire().expect("buffer");
767        assert!(buf2.is_empty());
768        assert!(buf2.capacity() >= 1024);
769    }
770}