Skip to main content

wireforge_io/
interface.rs

1//! Network interface enumeration (cross-platform).
2//!
3//! Does not require elevated privileges on any platform.
4
5use std::net::IpAddr;
6
7use crate::error::IoError;
8
9/// Information about a network interface.
10#[derive(Debug, Clone)]
11pub struct Interface {
12    /// OS-internal name (e.g. "eth0", "en0", "Ethernet0").
13    pub name: String,
14    /// Human-readable name.
15    pub friendly_name: Option<String>,
16    /// MAC address.
17    pub mac_address: Option<[u8; 6]>,
18    /// Bound IP addresses.
19    pub ips: Vec<IpAddr>,
20    /// Whether the interface is a loopback.
21    pub is_loopback: bool,
22    /// Whether the interface is up.
23    pub is_up: bool,
24}
25
26/// List all available network interfaces on the system.
27pub fn list_interfaces() -> Result<Vec<Interface>, IoError> {
28    platform_list_interfaces()
29}
30
31// ---------------------------------------------------------------------------
32// Linux: SIOCGIFCONF + per-interface ioctls
33// ---------------------------------------------------------------------------
34
35#[cfg(target_os = "linux")]
36fn platform_list_interfaces() -> Result<Vec<Interface>, IoError> {
37    use std::os::fd::AsRawFd;
38    use socket2::{Domain, Socket, Type};
39
40    let sock = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
41    let fd = sock.as_raw_fd();
42
43    // SIOCGIFCONF: get all interface configs
44    let mut ifc: libc::ifconf = unsafe { std::mem::zeroed() };
45    let buf_size = 4096usize;
46    let mut buf = vec![0u8; buf_size];
47    ifc.ifc_len = buf_size as i32;
48    ifc.ifc_ifcu.ifcu_buf = buf.as_mut_ptr() as *mut libc::c_char;
49
50    if unsafe { libc::ioctl(fd, libc::SIOCGIFCONF, &mut ifc) } < 0 {
51        return Err(IoError::Socket(std::io::Error::last_os_error()));
52    }
53
54    let mut interfaces = Vec::new();
55    let mut pos = 0usize;
56    while pos < ifc.ifc_len as usize {
57        let ifr: &libc::ifreq = unsafe { &*(ifc.ifc_ifcu.ifcu_buf.add(pos) as *const libc::ifreq) };
58        pos += std::mem::size_of::<libc::ifreq>();
59        let name = cstr_to_string(&ifr.ifr_name);
60
61        let mut ifr_flags = unsafe { std::mem::zeroed::<libc::ifreq>() };
62        copy_ifname(&mut ifr_flags, &name);
63        let is_up = unsafe { libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut ifr_flags) } >= 0
64            && unsafe { ifr_flags.ifr_ifru.ifru_flags & libc::IFF_UP as i16 } != 0;
65        let is_loopback = unsafe { libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut ifr_flags) } >= 0
66            && unsafe { ifr_flags.ifr_ifru.ifru_flags & libc::IFF_LOOPBACK as i16 } != 0;
67
68        // MAC address
69        let mut ifr_hw = unsafe { std::mem::zeroed::<libc::ifreq>() };
70        copy_ifname(&mut ifr_hw, &name);
71        let mac = if unsafe { libc::ioctl(fd, libc::SIOCGIFHWADDR, &mut ifr_hw) } >= 0 {
72            let sa = unsafe { &ifr_hw.ifr_ifru.ifru_hwaddr };
73            Some([sa.sa_data[0] as u8, sa.sa_data[1] as u8, sa.sa_data[2] as u8,
74                  sa.sa_data[3] as u8, sa.sa_data[4] as u8, sa.sa_data[5] as u8])
75        } else { None };
76
77        // IP address
78        let mut ifr_addr = unsafe { std::mem::zeroed::<libc::ifreq>() };
79        copy_ifname(&mut ifr_addr, &name);
80        let ips = if unsafe { libc::ioctl(fd, libc::SIOCGIFADDR, &mut ifr_addr) } >= 0 {
81            let sa = unsafe { &ifr_addr.ifr_ifru.ifru_addr };
82            if sa.sa_family as i32 == libc::AF_INET {
83                let addr_bytes = unsafe { (*(sa as *const libc::sockaddr as *const libc::sockaddr_in)).sin_addr.s_addr };
84                let ip = IpAddr::V4(std::net::Ipv4Addr::from(u32::from_be(addr_bytes)));
85                vec![ip]
86            } else { vec![] }
87        } else { vec![] };
88
89        interfaces.push(Interface { name, friendly_name: None, mac_address: mac, ips, is_loopback, is_up });
90    }
91    Ok(interfaces)
92}
93
94#[cfg(target_os = "linux")]
95fn cstr_to_string(cstr: &[i8]) -> String {
96    let bytes: Vec<u8> = cstr.iter().take_while(|&&b| b != 0).map(|&b| b as u8).collect();
97    String::from_utf8_lossy(&bytes).into_owned()
98}
99
100#[cfg(target_os = "linux")]
101fn copy_ifname(ifr: &mut libc::ifreq, name: &str) {
102    let bytes = name.as_bytes();
103    let len = bytes.len().min(ifr.ifr_name.len() - 1);
104    ifr.ifr_name[..len].copy_from_slice(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const i8, len) });
105}
106
107// ---------------------------------------------------------------------------
108// macOS/BSD: getifaddrs()
109// ---------------------------------------------------------------------------
110
111#[cfg(target_os = "macos")]
112fn platform_list_interfaces() -> Result<Vec<Interface>, IoError> {
113    use std::collections::HashMap;
114    use std::net::{Ipv4Addr, Ipv6Addr};
115
116    let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
117    if unsafe { libc::getifaddrs(&mut ifap) } != 0 {
118        return Err(IoError::Socket(std::io::Error::last_os_error()));
119    }
120
121    struct IfInfo {
122        name: String,
123        mac: Option<[u8; 6]>,
124        is_loopback: bool,
125        is_up: bool,
126        ips: Vec<IpAddr>,
127    }
128    let mut map: HashMap<String, IfInfo> = HashMap::new();
129
130    let mut cur = ifap;
131    while !cur.is_null() {
132        let ifa = unsafe { &*cur };
133        let name = cstr_to_string_osx(ifa.ifa_name);
134        let entry = map.entry(name.clone()).or_insert_with(|| IfInfo {
135            name,
136            mac: None,
137            is_loopback: (ifa.ifa_flags & libc::IFF_LOOPBACK as u32) != 0,
138            is_up: (ifa.ifa_flags & libc::IFF_UP as u32) != 0,
139            ips: vec![],
140        });
141
142        if !ifa.ifa_addr.is_null() {
143            let sa = unsafe { &*ifa.ifa_addr };
144            match sa.sa_family as i32 {
145                libc::AF_LINK => {
146                    let sdl = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_dl) };
147                    if sdl.sdl_alen == 6 {
148                        let mac_addr = unsafe { *(sdl.sdl_data.as_ptr().offset(sdl.sdl_nlen as isize) as *const [u8; 6]) };
149                        entry.mac = Some(mac_addr);
150                    }
151                }
152                libc::AF_INET => {
153                    let sin = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in) };
154                    let addr = Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr));
155                    entry.ips.push(IpAddr::V4(addr));
156                }
157                libc::AF_INET6 => {
158                    let sin6 = unsafe { &*(ifa.ifa_addr as *const libc::sockaddr_in6) };
159                    let addr = Ipv6Addr::from(sin6.sin6_addr.s6_addr);
160                    entry.ips.push(IpAddr::V6(addr));
161                }
162                _ => {}
163            }
164        }
165        cur = ifa.ifa_next;
166    }
167    unsafe { libc::freeifaddrs(ifap); }
168
169    Ok(map.into_values().map(|info| Interface {
170        name: info.name,
171        friendly_name: None,
172        mac_address: info.mac,
173        ips: info.ips,
174        is_loopback: info.is_loopback,
175        is_up: info.is_up,
176    }).collect())
177}
178
179#[cfg(target_os = "macos")]
180fn cstr_to_string_osx(ptr: *const libc::c_char) -> String {
181    if ptr.is_null() { return String::new(); }
182    let bytes = unsafe { std::ffi::CStr::from_ptr(ptr).to_bytes() };
183    String::from_utf8_lossy(bytes).into_owned()
184}
185
186// ---------------------------------------------------------------------------
187// Windows: IP Helper API (GetAdaptersAddresses)
188// ---------------------------------------------------------------------------
189
190#[cfg(target_os = "windows")]
191fn platform_list_interfaces() -> Result<Vec<Interface>, IoError> {
192    use std::net::{Ipv4Addr, Ipv6Addr};
193
194    const AF_UNSPEC: u32 = 0;
195    const GAA_FLAG_INCLUDE_PREFIX: u32 = 0x0010;
196    const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24;
197    const IF_OPER_STATUS_UP: u32 = 1;
198
199    #[repr(C)]
200    struct IpAdapterUnicastAddress {
201        length: u32,
202        _flags: u32,
203        next: *mut IpAdapterUnicastAddress,
204        sockaddr_ptr: *const u8,   // LPSOCKADDR
205        sockaddr_len: i32,
206    }
207
208    #[repr(C)]
209    struct IpAdapterAddresses {
210        length: u32,
211        if_index: u32,
212        next: *mut IpAdapterAddresses,
213        adapter_name: *const u8,
214        first_unicast_address: *mut IpAdapterUnicastAddress,
215        // skip: first_anycast(8), first_multicast(8), first_dns(8), dns_suffix(8), description(8), friendly_name(8)
216        _pad1: [u8; 48],
217        physical_address: [u8; 8],
218        physical_address_length: u32,
219        flags: u32,
220        mtu: u32,
221        if_type: u32,
222        oper_status: u32,
223    }
224
225    extern "system" {
226        fn GetAdaptersAddresses(
227            family: u32,
228            flags: u32,
229            reserved: *const std::ffi::c_void,
230            addresses: *mut IpAdapterAddresses,
231            size: *mut u32,
232        ) -> u32;
233    }
234
235    // First call: get required size
236    let mut size: u32 = 0;
237    let ret = unsafe { GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, std::ptr::null(), std::ptr::null_mut(), &mut size) };
238    if ret != 111 { return Ok(vec![]); } // ERROR_BUFFER_OVERFLOW
239
240    let mut buf = vec![0u8; size as usize];
241    let ret = unsafe { GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, std::ptr::null(), buf.as_mut_ptr() as *mut IpAdapterAddresses, &mut size) };
242    if ret != 0 { return Ok(vec![]); }
243
244    let mut interfaces = Vec::new();
245    let mut cur = buf.as_ptr() as *const IpAdapterAddresses;
246
247    while !cur.is_null() {
248        let a = unsafe { &*cur };
249
250        let name = wide_to_string(a.adapter_name);
251        let friendly_name = None; // field not directly accessible without offset; skip for now
252
253        let mac = if a.physical_address_length == 6 {
254            Some([a.physical_address[0], a.physical_address[1], a.physical_address[2],
255                  a.physical_address[3], a.physical_address[4], a.physical_address[5]])
256        } else { None };
257
258        let mut ips = Vec::new();
259        let mut ua = a.first_unicast_address;
260        while !ua.is_null() {
261            let u = unsafe { &*ua };
262            if !u.sockaddr_ptr.is_null() {
263                let family = unsafe { *(u.sockaddr_ptr as *const u16) };
264                match family {
265                    2 => { // AF_INET
266                        let b = unsafe { std::slice::from_raw_parts(u.sockaddr_ptr.offset(4), 4) };
267                        ips.push(IpAddr::V4(Ipv4Addr::new(b[0], b[1], b[2], b[3])));
268                    }
269                    23 => { // AF_INET6
270                        let b = unsafe { std::slice::from_raw_parts(u.sockaddr_ptr.offset(8), 16) };
271                        let mut arr = [0u8; 16];
272                        arr.copy_from_slice(b);
273                        ips.push(IpAddr::V6(Ipv6Addr::from(arr)));
274                    }
275                    _ => {}
276                }
277            }
278            ua = u.next;
279        }
280
281        interfaces.push(Interface {
282            name,
283            friendly_name,
284            mac_address: mac,
285            ips,
286            is_loopback: a.if_type == IF_TYPE_SOFTWARE_LOOPBACK,
287            is_up: a.oper_status == IF_OPER_STATUS_UP,
288        });
289
290        cur = a.next;
291    }
292
293    Ok(interfaces)
294}
295
296#[cfg(target_os = "windows")]
297fn wide_to_string(ptr: *const u8) -> String {
298    if ptr.is_null() { return String::new(); }
299    let len = unsafe {
300        let mut end = ptr;
301        loop {
302            let w = u16::from_le_bytes([*end, *(end.offset(1))]);
303            if w == 0 { break; }
304            end = end.offset(2);
305        }
306        (end as usize - ptr as usize) / 2
307    };
308    let wide: Vec<u16> = (0..len).map(|i| unsafe {
309        u16::from_le_bytes([*ptr.add(i*2), *ptr.add(i*2+1)])
310    }).collect();
311    String::from_utf16_lossy(&wide)
312}
313
314// ---------------------------------------------------------------------------
315// Tests
316// ---------------------------------------------------------------------------
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn list_interfaces_returns_something() {
324        let ifaces = list_interfaces().expect("list_interfaces should succeed");
325        assert!(!ifaces.is_empty(), "should return at least one interface");
326    }
327
328    #[test]
329    fn list_interfaces_has_loopback() {
330        let ifaces = list_interfaces().expect("list_interfaces should succeed");
331        assert!(ifaces.iter().any(|i| i.is_loopback), "should contain loopback");
332    }
333}