Skip to main content

timestamped_socket/
interface.rs

1use std::{
2    collections::HashMap,
3    net::{IpAddr, SocketAddr},
4    str::FromStr,
5};
6
7use super::cerr;
8
9#[cfg(target_os = "linux")]
10mod linux;
11#[cfg(target_os = "linux")]
12pub use linux::{lookup_phc, ChangeDetector};
13
14// NOTE: this detection logic is not sharable with macos!
15#[cfg(target_os = "freebsd")]
16mod freebsd;
17#[cfg(target_os = "freebsd")]
18pub use freebsd::{lookup_phc, ChangeDetector};
19
20#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
21mod fallback;
22#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
23pub use fallback::{lookup_phc, ChangeDetector};
24
25/// Get a directory of interfaces available on the current machine.
26///
27/// # Errors
28/// May fail if the os is unable to provide a list of network interfaces.
29///
30/// # Panics
31/// Panics if the os provides invalid interfaces.
32pub fn interfaces() -> std::io::Result<HashMap<InterfaceName, InterfaceData>> {
33    let mut elements = HashMap::default();
34
35    for data in InterfaceIterator::new()? {
36        let current: &mut InterfaceData = elements.entry(data.name).or_default();
37
38        current.socket_addrs.extend(data.socket_addr);
39        assert!(!(current.mac.is_some() && data.mac.is_some()));
40        current.mac = current.mac.or(data.mac);
41    }
42
43    Ok(elements)
44}
45
46/// Information on a network interface.
47#[derive(Default, Debug)]
48pub struct InterfaceData {
49    socket_addrs: Vec<SocketAddr>,
50    mac: Option<[u8; 6]>,
51}
52
53impl InterfaceData {
54    /// The given interface has an ip address associated with it.
55    #[must_use]
56    pub fn has_ip_addr(&self, address: IpAddr) -> bool {
57        self.socket_addrs
58            .iter()
59            .any(|socket_addr| socket_addr.ip() == address)
60    }
61
62    /// Iterator over all of the ip addresses associated with the interface.
63    pub fn ips(&self) -> impl Iterator<Item = IpAddr> + '_ {
64        self.socket_addrs.iter().map(std::net::SocketAddr::ip)
65    }
66
67    /// The mac address of the interface, if present.
68    #[must_use]
69    pub fn mac(&self) -> Option<[u8; 6]> {
70        self.mac
71    }
72}
73
74// Invariants:
75// self.base always contains a pointer received from libc::getifaddrs that is not NULL. The region pointed to is never modified in rust code.
76// self.next always contains either a pointer pointing to a valid ifaddr received from libc::getifaddrs or null.
77//
78// These invariants are setup by InterfaceIterator::new and guaranteed by drop and next, which are the only places these pointers are used.
79struct InterfaceIterator {
80    base: *mut libc::ifaddrs,
81    next: *const libc::ifaddrs,
82}
83
84impl InterfaceIterator {
85    pub fn new() -> std::io::Result<Self> {
86        let mut addrs: *mut libc::ifaddrs = std::ptr::null_mut();
87
88        // Safety:
89        // addrs lives for the duration of the call to getifaddrs.
90        //
91        // Invariant preservation:
92        // we validate that the received address is not null, and
93        // by the guarantees from getifaddrs points to a valid
94        // ifaddr returned from getifaddrs
95        unsafe {
96            cerr(libc::getifaddrs(&raw mut addrs))?;
97
98            assert!(!addrs.is_null());
99
100            Ok(Self {
101                base: addrs,
102                next: addrs,
103            })
104        }
105    }
106}
107
108impl Drop for InterfaceIterator {
109    fn drop(&mut self) {
110        // Safety:
111        // By the invariants, self.base is guaranteed to point to a memory region allocated by getifaddrs
112        unsafe { libc::freeifaddrs(self.base) };
113    }
114}
115
116struct InterfaceDataInternal {
117    name: InterfaceName,
118    mac: Option<[u8; 6]>,
119    socket_addr: Option<SocketAddr>,
120}
121
122impl Iterator for InterfaceIterator {
123    type Item = InterfaceDataInternal;
124
125    #[expect(
126        clippy::cast_possible_truncation,
127        reason = "libc constants are of inconsistent sizes."
128    )]
129    fn next(&mut self) -> Option<<Self as Iterator>::Item> {
130        // Safety:
131        // By the invariants, self.next is guaranteed to be a valid pointer to an ifaddrs struct or null.
132        let ifaddr = unsafe { self.next.as_ref() }?;
133
134        // Invariant preservation
135        // By the guarantees given by getifaddrs, ifaddr.ifa_next is either null or points to a valid
136        // ifaddr.
137        self.next = ifaddr.ifa_next;
138
139        // Safety:
140        // getifaddrs guarantees that ifa_name is not null and points to a valid C string.
141        let ifname = unsafe { std::ffi::CStr::from_ptr(ifaddr.ifa_name) };
142        let name = match std::str::from_utf8(ifname.to_bytes()) {
143            Err(_) => unreachable!("interface names must be ascii"),
144            Ok(name) => InterfaceName::from_str(name).expect("name from os"),
145        };
146
147        // Safety:
148        // getifaddrs guarantees that ifa_addr either points to a valid address or is NULL.
149        let family = unsafe { ifaddr.ifa_addr.as_ref() }.map(|a| a.sa_family);
150
151        #[allow(unused)]
152        let mac: Option<[u8; 6]> = None;
153
154        #[cfg(target_os = "linux")]
155        // Safety: getifaddrs ensures that, if an address is present, it is valid. A valid address
156        // of type AF_PACKET is always reinterpret castable to sockaddr_ll, and we know an address
157        // is present since family is not None
158        let mac = if family == Some(libc::AF_PACKET as _) {
159            let sockaddr_ll: libc::sockaddr_ll =
160                unsafe { std::ptr::read_unaligned(ifaddr.ifa_addr as *const _) };
161
162            Some([
163                sockaddr_ll.sll_addr[0],
164                sockaddr_ll.sll_addr[1],
165                sockaddr_ll.sll_addr[2],
166                sockaddr_ll.sll_addr[3],
167                sockaddr_ll.sll_addr[4],
168                sockaddr_ll.sll_addr[5],
169            ])
170        } else {
171            None
172        };
173
174        #[cfg(any(target_os = "freebsd", target_os = "macos"))]
175        #[expect(
176            clippy::cast_sign_loss,
177            reason = "length is communicated as signed number, even though it is always positive."
178        )]
179        let mac = if family == Some(libc::AF_LINK as _) {
180            // From sys/net/if_types.h in freebsd:
181            const IFT_ETHER: u8 = 0x6;
182
183            // Safety: getifaddrs ensures that, if an address is present, it is valid. A valid address
184            // of type AF_LINK is always reinterpret castable to sockaddr_ll, and we know an address
185            // is present since family is not None
186            let sockaddr_dl: libc::sockaddr_dl =
187                unsafe { std::ptr::read_unaligned(ifaddr.ifa_addr as *const _) };
188
189            if sockaddr_dl.sdl_type == IFT_ETHER
190                && sockaddr_dl.sdl_nlen.saturating_add(6) as usize <= sockaddr_dl.sdl_data.len()
191            {
192                Some([
193                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize] as u8,
194                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize + 1] as u8,
195                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize + 2] as u8,
196                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize + 3] as u8,
197                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize + 4] as u8,
198                    sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen as usize + 5] as u8,
199                ])
200            } else {
201                None
202            }
203        } else {
204            None
205        };
206
207        // Safety: ifaddr.ifa_addr is always either NULL, or by the guarantees of getifaddrs, points to a valid address.
208        let socket_addr = unsafe { sockaddr_to_socket_addr(ifaddr.ifa_addr) };
209
210        let data = InterfaceDataInternal {
211            name,
212            mac,
213            socket_addr,
214        };
215
216        Some(data)
217    }
218}
219
220/// The name of a network interface.
221#[derive(Clone, Copy, PartialEq, Eq, Hash)]
222pub struct InterfaceName {
223    bytes: [u8; libc::IFNAMSIZ],
224}
225
226impl InterfaceName {
227    /// The loopback interface's name
228    #[cfg(all(test, target_os = "linux"))]
229    pub const LOOPBACK: Self = Self {
230        bytes: *b"lo\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
231    };
232
233    /// The loopback interface's name
234    #[cfg(all(test, any(target_os = "freebsd", target_os = "macos")))]
235    pub const LOOPBACK: Self = Self {
236        bytes: *b"lo0\0\0\0\0\0\0\0\0\0\0\0\0\0",
237    };
238
239    /// An invalid interface name for testing.
240    #[cfg(test)]
241    pub const INVALID: Self = Self {
242        bytes: *b"123412341234123\0",
243    };
244
245    /// The name of the interface interpreted as string.
246    #[must_use]
247    pub fn as_str(&self) -> &str {
248        std::str::from_utf8(self.bytes.as_slice())
249            .unwrap_or_default()
250            .trim_end_matches('\0')
251    }
252
253    /// The name of the interface as a c string.
254    ///
255    /// # Panics
256    /// May panic if the interface name is invalid and not terminated with a null byte.
257    #[must_use]
258    pub fn as_cstr(&self) -> &std::ffi::CStr {
259        // TODO: in rust 1.69.0, use
260        // std::ffi::CStr::from_bytes_until_nul(&self.bytes[..]).unwrap()
261
262        // it is an invariant of InterfaceName that the bytes are null-terminated
263        let first_null = self.bytes.iter().position(|b| *b == 0).unwrap();
264        std::ffi::CStr::from_bytes_with_nul(&self.bytes[..=first_null]).unwrap()
265    }
266
267    /// Conversion to a type for the `ifr_name` field.
268    #[allow(
269        clippy::cast_possible_wrap,
270        reason = "libc::c_char can be either signed or unsigned"
271    )]
272    #[must_use]
273    pub fn to_ifr_name(self) -> [libc::c_char; libc::IFNAMSIZ] {
274        let mut it = self.bytes.iter().copied();
275        [0; libc::IFNAMSIZ].map(|_| it.next().unwrap_or(0) as libc::c_char)
276    }
277
278    /// Find the interface name for a given local address of a socket.
279    ///
280    /// # Errors
281    /// May fail if the address is not associated with any local interface.
282    pub fn from_socket_addr(local_addr: SocketAddr) -> std::io::Result<Option<Self>> {
283        let matches_inferface = |interface: &InterfaceDataInternal| match interface.socket_addr {
284            None => false,
285            Some(address) => address.ip() == local_addr.ip(),
286        };
287
288        match InterfaceIterator::new()?.find(matches_inferface) {
289            Some(interface) => Ok(Some(interface.name)),
290            None => Ok(None),
291        }
292    }
293
294    /// The index of the socket used by the operating system.
295    #[must_use]
296    pub fn get_index(&self) -> Option<libc::c_uint> {
297        // # SAFETY
298        //
299        // self lives for the duration of the call, and is null terminated.
300        match unsafe { libc::if_nametoindex(self.as_cstr().as_ptr()) } {
301            0 => None,
302            n => Some(n),
303        }
304    }
305
306    /// Do a lookup for the Physical Hardware Clock index for this interface.
307    #[must_use]
308    pub fn lookup_phc(&self) -> Option<u32> {
309        lookup_phc(*self)
310    }
311}
312
313impl std::fmt::Debug for InterfaceName {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        f.debug_tuple("InterfaceName")
316            .field(&self.as_str())
317            .finish()
318    }
319}
320
321impl std::fmt::Display for InterfaceName {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        self.as_str().fmt(f)
324    }
325}
326
327impl std::str::FromStr for InterfaceName {
328    type Err = ();
329
330    fn from_str(s: &str) -> Result<Self, Self::Err> {
331        let mut bytes = [0; libc::IFNAMSIZ];
332
333        // >= so that we always retain a NUL byte at the end
334        if s.len() >= bytes.len() {
335            return Err(());
336        }
337
338        if s.is_empty() {
339            // this causes problems down the line when giving the interface name to tokio
340            return Err(());
341        }
342
343        let mut it = s.bytes();
344        bytes = bytes.map(|_| it.next().unwrap_or_default());
345
346        Ok(Self { bytes })
347    }
348}
349
350#[cfg(feature = "serde")]
351impl<'de> serde::Deserialize<'de> for InterfaceName {
352    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
353    where
354        D: serde::Deserializer<'de>,
355    {
356        let s = String::deserialize(deserializer)?;
357        FromStr::from_str(&s).map_err(|()| serde::de::Error::custom("invalid interface name"))
358    }
359}
360
361/// Convert a `libc::sockaddr` to a rust `std::net::SocketAddr`
362///
363/// # Safety
364///
365/// This function assumes that sockaddr is either NULL or points to a valid address.
366unsafe fn sockaddr_to_socket_addr(sockaddr: *const libc::sockaddr) -> Option<SocketAddr> {
367    // Most (but not all) of the fields in a socket addr are in network byte
368    // ordering. As such, when doing conversions here, we should start from the
369    // NATIVE byte representation, as this will actualy be the big-endian
370    // representation of the underlying value regardless of platform.
371
372    // Check for null pointers
373    if sockaddr.is_null() {
374        return None;
375    }
376
377    // Safety: by the previous check, sockaddr is not NULL and hence points to a valid address
378    match unsafe { libc::c_int::from((*sockaddr).sa_family) } {
379        libc::AF_INET => {
380            // SAFETY: we cast from a libc::sockaddr (alignment 2) to a libc::sockaddr_in (alignment 4)
381            // that means that the pointer is now potentially unaligned. We must used read_unaligned!
382            // However, the rest of the cast is safe as a valid AF_INET address is always reinterpret castable
383            // as a sockaddr_in
384            let inaddr: libc::sockaddr_in =
385                unsafe { std::ptr::read_unaligned(sockaddr.cast::<libc::sockaddr_in>()) };
386
387            let socketaddr = std::net::SocketAddrV4::new(
388                std::net::Ipv4Addr::from(inaddr.sin_addr.s_addr.to_ne_bytes()),
389                u16::from_be_bytes(inaddr.sin_port.to_ne_bytes()),
390            );
391
392            Some(std::net::SocketAddr::V4(socketaddr))
393        }
394        libc::AF_INET6 => {
395            // SAFETY: we cast from a libc::sockaddr (alignment 2) to a libc::sockaddr_in6 (alignment 4)
396            // that means that the pointer is now potentially unaligned. We must used read_unaligned!
397            // However, the cast is safe as a valid AF_INET6 address is always reinterpret catable as a sockaddr_in6
398            let inaddr: libc::sockaddr_in6 =
399                unsafe { std::ptr::read_unaligned(sockaddr.cast::<libc::sockaddr_in6>()) };
400
401            // Safety:
402            // sin_addr lives for the duration fo the call and matches type
403            let sin_addr = inaddr.sin6_addr.s6_addr;
404            let segment_bytes: [u8; 16] =
405                unsafe { std::ptr::read_unaligned((&raw const sin_addr).cast()) };
406
407            let socketaddr = std::net::SocketAddrV6::new(
408                std::net::Ipv6Addr::from(segment_bytes),
409                u16::from_be_bytes(inaddr.sin6_port.to_ne_bytes()),
410                inaddr.sin6_flowinfo, /* NOTE: Despite network byte order, no conversion is needed (see https://github.com/rust-lang/rust/issues/101605) */
411                inaddr.sin6_scope_id,
412            );
413
414            Some(std::net::SocketAddr::V6(socketaddr))
415        }
416        _ => None,
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use std::net::Ipv4Addr;
423
424    use super::*;
425
426    #[test]
427    fn interface_name_from_string() {
428        assert!(InterfaceName::from_str("").is_err());
429        assert!(InterfaceName::from_str("a string that is too long").is_err());
430
431        let input = "enp0s31f6";
432        assert_eq!(InterfaceName::from_str(input).unwrap().as_str(), input);
433
434        #[allow(
435            clippy::cast_possible_wrap,
436            reason = "libc::c_char may be either signed or unsigned, and this is not specified"
437        )]
438        let ifr_name = (*b"enp0s31f6\0\0\0\0\0\0\0").map(|b| b as libc::c_char);
439        assert_eq!(
440            InterfaceName::from_str(input).unwrap().to_ifr_name(),
441            ifr_name
442        );
443    }
444
445    #[test]
446    fn test_mac_address_iterator() {
447        let v: Vec<_> = InterfaceIterator::new()
448            .unwrap()
449            .filter_map(|d| d.mac)
450            .collect();
451
452        assert!(!v.is_empty());
453    }
454
455    #[test]
456    fn test_interface_name_iterator() {
457        let v: Vec<_> = InterfaceIterator::new().unwrap().map(|d| d.name).collect();
458
459        assert!(v.contains(&InterfaceName::LOOPBACK));
460    }
461
462    #[test]
463    fn test_socket_addr_iterator() {
464        let v: Vec<_> = InterfaceIterator::new()
465            .unwrap()
466            .filter_map(|d| d.socket_addr)
467            .collect();
468
469        let localhost_0 = SocketAddr::from((Ipv4Addr::LOCALHOST, 0));
470
471        assert!(v.contains(&localhost_0));
472    }
473
474    #[test]
475    fn interface_index_ipv4() {
476        assert!(InterfaceName::LOOPBACK.get_index().is_some());
477    }
478
479    #[test]
480    fn interface_index_ipv6() {
481        assert!(InterfaceName::LOOPBACK.get_index().is_some());
482    }
483
484    #[test]
485    fn interface_index_invalid() {
486        assert!(InterfaceName::INVALID.get_index().is_none());
487    }
488}