os_interface/
network.rs

1use crate::Error;
2use libc::size_t;
3use libc::{
4    freeifaddrs, getifaddrs, if_nametoindex, ifaddrs, sockaddr_in, sockaddr_in6, AF_INET, AF_INET6,
5    IFF_BROADCAST, IFF_LOOPBACK, IFF_MULTICAST, IFF_RUNNING, IFF_UP,
6};
7use std::collections::BTreeMap;
8use std::ffi::{CStr, OsString};
9use std::net::{Ipv4Addr, Ipv6Addr};
10use std::os::unix::ffi::OsStringExt;
11use std::ptr;
12
13/// System network interface
14#[derive(Clone, PartialEq, Eq, Hash, Debug)]
15pub struct NetworkInterface {
16    /// Index
17    pub index: u32,
18    /// Name
19    pub name: String, // e.g. eth0
20    /// Address
21    pub addr: Vec<Addr>,
22    /// MAC address
23    pub mac_addr: Option<String>,
24    /// Interface flags
25    pub flags: Flags,
26}
27
28/// Interface flags
29#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
30pub struct Flags {
31    /// Interface is administratively up
32    pub up: bool,
33    /// Interface is a loopback device
34    pub loopback: bool,
35    /// Interface has resources allocated (operational)
36    pub running: bool,
37    /// Interface supports multicasting
38    pub multicast: bool,
39    /// Interface supports broadcast
40    pub broadcast: bool,
41}
42
43/// Network interface address
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
45pub enum Addr {
46    /// IPv4, AFINET address Family Internet Protocol version 4
47    IPv4(IfAddrV4),
48    /// IPv6, AFINET6 address Family Internet Protocol version 6
49    IPv6(IfAddrV6),
50}
51
52/// IPv4 Interface from the AFINET network interface family
53#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
54pub struct IfAddrV4 {
55    /// The IP address for this network interface
56    pub ip: Ipv4Addr,
57    /// The netmask for this interface
58    pub netmask: Option<Ipv4Addr>,
59    /// The broadcast address for this interface
60    pub broadcast: Option<Ipv4Addr>,
61}
62
63/// IPv6 Interface from the AFINET6 network interface family
64#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
65pub struct IfAddrV6 {
66    /// The IP address for this network interface
67    pub ip: Ipv6Addr,
68    /// The netmask for this interface
69    pub netmask: Option<Ipv6Addr>,
70}
71
72impl IfAddrV6 {
73    fn is_unicast_link_local(&self) -> bool {
74        // A unicast link-local address starts with the fe80::/10 prefix.
75        // This means the first 10 bits must be 1111 1110 10xx xxxx (0xfe80 to 0xfebf).
76        // In newer versions of Rust (1.84.1 and newer), simply use is_unicast_link_local() on Ipv6Addr.
77        (self.ip.segments()[0] & 0xffc0) == 0xfe80
78    }
79}
80
81fn if_addr_v4(ifa: &ifaddrs, flags: &Flags) -> IfAddrV4 {
82    // Get Netmask
83    let mut netmask: Option<Ipv4Addr> = None;
84    if !ifa.ifa_netmask.is_null() {
85        let mask = unsafe { *(ifa.ifa_netmask as *const sockaddr_in) };
86        // Netmask in network byte order (Big Endian)
87        netmask = Some(Ipv4Addr::from(mask.sin_addr.s_addr.to_be()));
88    }
89
90    // Access the broadcast address from the union field
91    // Note: In some libc versions, this is called ifa_ifu; in others, ifa_dstaddr
92    // Use cfg to handle the field name difference
93    #[cfg(any(target_os = "linux", target_os = "android"))]
94    let broad_ptr = ifa.ifa_ifu;
95
96    #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
97    let broad_ptr = ifa.ifa_dstaddr;
98
99    let mut broadcast: Option<Ipv4Addr> = None;
100    // Check if the interface supports broadcasting
101    if !broad_ptr.is_null() && flags.broadcast {
102        let sa = unsafe { *(broad_ptr as *const sockaddr_in) };
103        broadcast = Some(Ipv4Addr::from(u32::from_be(sa.sin_addr.s_addr)));
104    }
105
106    // Get address
107    let socket_addr = unsafe { *(ifa.ifa_addr as *const sockaddr_in) };
108    let ip = Ipv4Addr::from(socket_addr.sin_addr.s_addr.to_be());
109
110    IfAddrV4 {
111        ip,
112        netmask,
113        broadcast,
114    }
115}
116
117fn if_addr_v6(ifa: &ifaddrs) -> IfAddrV6 {
118    let mut netmask: Option<Ipv6Addr> = None;
119    if !ifa.ifa_netmask.is_null() {
120        let mask = unsafe { *(ifa.ifa_netmask as *const sockaddr_in6) };
121        // Access 16-byte array for IPv6 netmask
122        netmask = Some(Ipv6Addr::from(mask.sin6_addr.s6_addr));
123    }
124
125    let socket_addr = unsafe { *(ifa.ifa_addr as *const sockaddr_in6) };
126    let ip = Ipv6Addr::from(socket_addr.sin6_addr.s6_addr);
127
128    IfAddrV6 { ip, netmask }
129}
130
131fn mac_to_string(mac: &[u8]) -> String {
132    let mac_addr = format!(
133        "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
134        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
135    );
136    mac_addr
137}
138
139fn mac_addr(ifa: &ifaddrs, family: i32) -> Option<String> {
140    #[cfg(any(target_os = "linux", target_os = "android"))]
141    if family == libc::AF_PACKET {
142        let sll = unsafe { *(ifa.ifa_addr as *const libc::sockaddr_ll) };
143        let mac = sll.sll_addr;
144        let len = sll.sll_halen as usize;
145
146        // MAC addresses are usually 6 bytes (Ethernet)
147        if len == 6 {
148            return Some(mac_to_string(&mac));
149        }
150    }
151
152    #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
153    if family == libc::AF_LINK {
154        let sdl = unsafe { *(ifa.ifa_addr as *const libc::sockaddr_dl) };
155
156        // Use LLADDR macro logic: it skips the name (sdl_nlen)
157        // to find the start of the hardware address
158        let mac_ptr = unsafe { (&sdl as *const _ as *const u8).add(8 + sdl.sdl_nlen as usize) };
159
160        if sdl.sdl_alen == 6 {
161            let mac = unsafe { std::slice::from_raw_parts(mac_ptr, 6) };
162            return Some(mac_to_string(mac));
163        }
164    }
165
166    None
167}
168
169/// Inserts a new NetworkInterface into the BTreeMap
170/// or updates with another address in the addr list.
171fn update_interfaces(
172    index: u32,
173    name: String,
174    addr: Addr,
175    flags: Flags,
176    interfaces: &mut BTreeMap<u32, NetworkInterface>,
177) {
178    interfaces
179        .entry(index)
180        .and_modify(|i| {
181            i.addr.push(addr);
182        })
183        .or_insert(NetworkInterface {
184            index,
185            name,
186            addr: vec![addr],
187            mac_addr: None,
188            flags,
189        });
190}
191
192/// Inserts a new NetworkInterface into the BTreeMap
193/// or updates the mac address for the given NetworkInterface.
194fn update_interfaces_with_mac(
195    index: u32,
196    name: String,
197    mac_addr: Option<String>,
198    flags: Flags,
199    interfaces: &mut BTreeMap<u32, NetworkInterface>,
200) {
201    interfaces
202        .entry(index)
203        .and_modify(|i| {
204            i.mac_addr = mac_addr.clone();
205        })
206        .or_insert(NetworkInterface {
207            index,
208            name,
209            addr: Vec::new(),
210            mac_addr,
211            flags,
212        });
213}
214
215/// This function exist for backward compatibility.
216/// Use network_interfaces() instead.
217pub fn get_network_interfaces() -> Result<Vec<NetworkInterface>, Error> {
218    network_interfaces()
219}
220
221/// Get all the network interfaces.
222pub fn network_interfaces() -> Result<Vec<NetworkInterface>, Error> {
223    let mut ifaddr_ptr: *mut ifaddrs = ptr::null_mut();
224
225    unsafe {
226        // Retrieve the linked list of interfaces
227        let res = getifaddrs(&mut ifaddr_ptr);
228        if res != 0 {
229            return Err(Error::FailedToGetResource(format!(
230                "getifaddrs returned {res}"
231            )));
232        }
233    }
234
235    let mut interfaces: BTreeMap<u32, NetworkInterface> = BTreeMap::new();
236
237    let mut current_ptr = ifaddr_ptr;
238    while let Some(ifa) = unsafe { current_ptr.as_ref() } {
239        // Extract the interface name
240        let name = unsafe { CStr::from_ptr(ifa.ifa_name).to_string_lossy() };
241
242        // Extract interface index
243        let index = unsafe { if_nametoindex(ifa.ifa_name) };
244        if index == 0 {
245            // Returns 0 on failure (e.g., interface no longer exists)
246            eprint!("Interface no longer exists: {name}");
247        }
248
249        // Extract interface flags
250        let flags = ifa.ifa_flags;
251        let flags = Flags {
252            up: (flags as i32 & IFF_UP) != 0,
253            loopback: (flags as i32 & IFF_LOOPBACK) != 0,
254            running: (flags as i32 & IFF_RUNNING) != 0,
255            multicast: (flags as i32 & IFF_MULTICAST) != 0,
256            broadcast: (flags as i32 & IFF_BROADCAST) != 0,
257        };
258
259        // Process the address if it exists
260        let ifa_addr = match unsafe { ifa.ifa_addr.as_ref() } {
261            Some(ifa_addr) => ifa_addr,
262            None => {
263                current_ptr = ifa.ifa_next;
264                continue;
265            }
266        };
267        let family = ifa_addr.sa_family as i32;
268
269        match family {
270            AF_INET => {
271                let if_addr_v4 = if_addr_v4(ifa, &flags);
272                let addr = Addr::IPv4(if_addr_v4);
273                update_interfaces(index, name.into_owned(), addr, flags, &mut interfaces);
274            }
275            AF_INET6 => {
276                let if_addr_v6 = if_addr_v6(ifa);
277                let addr = Addr::IPv6(if_addr_v6);
278                update_interfaces(index, name.into_owned(), addr, flags, &mut interfaces);
279            }
280            family => {
281                let mac_addr = mac_addr(ifa, family);
282                update_interfaces_with_mac(
283                    index,
284                    name.into_owned(),
285                    mac_addr,
286                    flags,
287                    &mut interfaces,
288                );
289            }
290        }
291
292        current_ptr = ifa.ifa_next;
293    }
294
295    unsafe {
296        freeifaddrs(ifaddr_ptr);
297    }
298
299    Ok(interfaces.into_values().collect())
300}
301
302/// Gets all local IPv4 addresses that are not loopback.
303pub fn local_ipv4_addresses() -> Result<Vec<Ipv4Addr>, Error> {
304    Ok(network_interfaces()?
305        .into_iter()
306        .filter_map(|ni| {
307            if !ni.flags.loopback {
308                ni.addr.into_iter().find_map(|addr| match addr {
309                    Addr::IPv4(addr) => Some(addr.ip),
310                    _ => None,
311                })
312            } else {
313                None
314            }
315        })
316        .collect())
317}
318
319/// Gets all local IPv6 addresses that are not loopback or unicast link local.
320pub fn local_ipv6_addresses() -> Result<Vec<Ipv6Addr>, Error> {
321    Ok(network_interfaces()?
322        .into_iter()
323        .filter_map(|ni| {
324            if !ni.flags.loopback {
325                ni.addr.into_iter().find_map(|addr| match addr {
326                    Addr::IPv6(addr) if !addr.is_unicast_link_local() => Some(addr.ip),
327                    _ => None,
328                })
329            } else {
330                None
331            }
332        })
333        .collect())
334}
335
336/// This function exist for backward compatibility.
337/// Use hostname() instead.
338pub fn get_hostname() -> Result<OsString, Error> {
339    hostname()
340}
341
342/// Get the hostname.
343pub fn hostname() -> Result<OsString, Error> {
344    let mut buf: Vec<u8> = Vec::with_capacity(256);
345    let ptr = buf.as_mut_ptr().cast();
346    let len = buf.capacity() as size_t;
347
348    let res = unsafe { libc::gethostname(ptr, len) };
349    if res != 0 {
350        return Err(Error::FailedToGetResource(format!(
351            "gethostname returned {res}"
352        )));
353    }
354    unsafe {
355        buf.as_mut_ptr().wrapping_add(len - 1).write(0);
356        // In Rust 1.80.1 and newer replace count_bytes() with to_bytes().len()
357        let len = CStr::from_ptr(buf.as_ptr().cast()).to_bytes().len();
358        buf.set_len(len);
359    }
360    Ok(OsString::from_vec(buf))
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_network_interfaces() {
369        let interfaces = network_interfaces().expect("Failed to get network interfaces");
370        println!("Interfaces: {interfaces:#?}");
371        assert!(interfaces.len() > 0);
372        assert!(interfaces[0].name.starts_with("lo"));
373        assert!(interfaces[0].addr.len() > 0);
374    }
375
376    #[test]
377    fn test_local_ipv4_addresses() {
378        let addresses = local_ipv4_addresses().expect("Failed to get IPv4 addresses");
379        assert!(addresses.len() >= 1);
380    }
381
382    #[test]
383    fn test_local_ipv6_addresses() {
384        let addresses = local_ipv6_addresses();
385        assert!(addresses.is_ok());
386    }
387
388    #[test]
389    fn test_hostname() {
390        let hostname = hostname().expect("Failed to get hostname");
391        println!("hostname: {hostname:#?}");
392        assert!(hostname.len() > 0);
393    }
394}