ndn_faces/
iface.rs

1//! Network interface enumeration and name filtering.
2//!
3//! Provides a cross-platform [`list_interfaces`] function and glob-based
4//! whitelist/blacklist filtering used by the face system auto-configuration.
5
6use std::net::Ipv4Addr;
7
8/// Information about a single network interface.
9#[derive(Debug, Clone)]
10pub struct InterfaceInfo {
11    /// Interface name (e.g. `"eth0"`, `"en0"`, `"enp3s0"`).
12    pub name: String,
13    /// All IPv4 addresses assigned to this interface.
14    pub ipv4_addrs: Vec<Ipv4Addr>,
15    /// Interface is UP (administratively enabled and carrier present).
16    pub is_up: bool,
17    /// Interface supports multicast.
18    pub is_multicast: bool,
19    /// Interface is a loopback (e.g. `lo`, `lo0`).
20    pub is_loopback: bool,
21}
22
23/// Returns `true` if `name` passes the whitelist/blacklist filter.
24///
25/// - Blacklist is checked first: any match → denied.
26/// - Whitelist is checked next: at least one match required (empty = allow all).
27pub fn interface_allowed(name: &str, whitelist: &[String], blacklist: &[String]) -> bool {
28    if blacklist
29        .iter()
30        .any(|p| glob_match(p.as_bytes(), name.as_bytes()))
31    {
32        return false;
33    }
34    whitelist.is_empty()
35        || whitelist
36            .iter()
37            .any(|p| glob_match(p.as_bytes(), name.as_bytes()))
38}
39
40/// Minimal glob matcher supporting `*` (any sequence) and `?` (one char).
41///
42/// Operates on byte slices; interface names are always ASCII so this is safe.
43pub fn glob_match(pattern: &[u8], name: &[u8]) -> bool {
44    match (pattern, name) {
45        // Both exhausted → full match.
46        ([], []) => true,
47        // Pattern exhausted but name has chars left → no match.
48        ([], _) => false,
49        // `*` in pattern: try matching zero chars OR consume one name char.
50        ([b'*', rest @ ..], _) => {
51            glob_match(rest, name) || (!name.is_empty() && glob_match(pattern, &name[1..]))
52        }
53        // `?` matches exactly one name char.
54        ([b'?', p_rest @ ..], [_, n_rest @ ..]) => glob_match(p_rest, n_rest),
55        // `?` with name exhausted → no match.
56        ([b'?', ..], []) => false,
57        // Literal match.
58        ([p, p_rest @ ..], [n, n_rest @ ..]) if p == n => glob_match(p_rest, n_rest),
59        // Mismatch.
60        _ => false,
61    }
62}
63
64// ── Platform implementations ──────────────────────────────────────────────────
65
66/// Enumerate all network interfaces on this host.
67///
68/// Returns an empty `Vec` on unsupported platforms or when the OS call fails.
69pub fn list_interfaces() -> Vec<InterfaceInfo> {
70    #[cfg(unix)]
71    {
72        list_interfaces_unix()
73    }
74    #[cfg(windows)]
75    {
76        list_interfaces_windows()
77    }
78    #[cfg(not(any(unix, windows)))]
79    {
80        vec![]
81    }
82}
83
84#[cfg(unix)]
85fn list_interfaces_unix() -> Vec<InterfaceInfo> {
86    use std::collections::HashMap;
87
88    let mut map: HashMap<String, InterfaceInfo> = HashMap::new();
89
90    unsafe {
91        let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
92        if libc::getifaddrs(&mut ifap) != 0 {
93            tracing::warn!(
94                error = %std::io::Error::last_os_error(),
95                "getifaddrs failed — interface enumeration unavailable"
96            );
97            return vec![];
98        }
99
100        let mut ifa = ifap;
101        while !ifa.is_null() {
102            let name_ptr = (*ifa).ifa_name;
103            if name_ptr.is_null() {
104                ifa = (*ifa).ifa_next;
105                continue;
106            }
107            let name = std::ffi::CStr::from_ptr(name_ptr)
108                .to_string_lossy()
109                .into_owned();
110
111            let flags = (*ifa).ifa_flags;
112            let is_up =
113                flags & (libc::IFF_UP as u32) != 0 && flags & (libc::IFF_RUNNING as u32) != 0;
114            let is_multicast = flags & (libc::IFF_MULTICAST as u32) != 0;
115            let is_loopback = flags & (libc::IFF_LOOPBACK as u32) != 0;
116
117            let entry = map.entry(name.clone()).or_insert_with(|| InterfaceInfo {
118                name: name.clone(),
119                ipv4_addrs: Vec::new(),
120                is_up,
121                is_multicast,
122                is_loopback,
123            });
124            // Flags are repeated for every address entry — update each time.
125            entry.is_up = is_up;
126            entry.is_multicast = is_multicast;
127            entry.is_loopback = is_loopback;
128
129            // Extract IPv4 address if present.
130            if !(*ifa).ifa_addr.is_null() {
131                let sa_family = (*(*ifa).ifa_addr).sa_family as i32;
132                if sa_family == libc::AF_INET {
133                    let sin = (*ifa).ifa_addr as *const libc::sockaddr_in;
134                    // sin_addr.s_addr is in network byte order on all platforms.
135                    let raw = u32::from_be((*sin).sin_addr.s_addr);
136                    entry.ipv4_addrs.push(Ipv4Addr::from(raw));
137                }
138            }
139
140            ifa = (*ifa).ifa_next;
141        }
142
143        libc::freeifaddrs(ifap);
144    }
145
146    map.into_values().collect()
147}
148
149#[cfg(windows)]
150fn list_interfaces_windows() -> Vec<InterfaceInfo> {
151    // Enumerate via GetAdaptersAddresses.
152    use std::collections::HashMap;
153    use windows_sys::Win32::NetworkManagement::IpHelper::{
154        GAA_FLAG_INCLUDE_PREFIX, GetAdaptersAddresses, IP_ADAPTER_ADDRESSES_LH,
155    };
156    use windows_sys::Win32::Networking::WinSock::{AF_INET, SOCKADDR_IN};
157
158    const AF_UNSPEC: u32 = 0;
159    const ERROR_BUFFER_OVERFLOW: u32 = 111;
160    const IF_TYPE_SOFTWARE_LOOPBACK: u32 = 24;
161
162    let mut buf_len: u32 = 16 * 1024;
163    let mut buf: Vec<u8> = vec![0u8; buf_len as usize];
164
165    // First call to get required size.
166    let rc = unsafe {
167        GetAdaptersAddresses(
168            AF_UNSPEC,
169            GAA_FLAG_INCLUDE_PREFIX,
170            std::ptr::null_mut(),
171            buf.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH,
172            &mut buf_len,
173        )
174    };
175    if rc == ERROR_BUFFER_OVERFLOW {
176        buf.resize(buf_len as usize, 0);
177    } else if rc != 0 {
178        return vec![];
179    }
180
181    // Second call with correctly-sized buffer.
182    let rc = unsafe {
183        GetAdaptersAddresses(
184            AF_UNSPEC,
185            GAA_FLAG_INCLUDE_PREFIX,
186            std::ptr::null_mut(),
187            buf.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH,
188            &mut buf_len,
189        )
190    };
191    if rc != 0 {
192        return vec![];
193    }
194
195    let mut map: HashMap<String, InterfaceInfo> = HashMap::new();
196    unsafe {
197        let mut adapter = buf.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH;
198        while !adapter.is_null() {
199            let friendly = if (*adapter).FriendlyName.is_null() {
200                String::new()
201            } else {
202                // FriendlyName is a PWSTR (UTF-16).
203                let mut len = 0usize;
204                let ptr = (*adapter).FriendlyName;
205                while *ptr.add(len) != 0 {
206                    len += 1;
207                }
208                String::from_utf16_lossy(std::slice::from_raw_parts(ptr, len))
209            };
210
211            let is_up = (*adapter).OperStatus == 1; // IfOperStatusUp
212            let is_loopback = (*adapter).IfType == IF_TYPE_SOFTWARE_LOOPBACK;
213            // Windows doesn't expose a simple multicast flag per adapter;
214            // treat all non-loopback UP adapters as multicast-capable.
215            let is_multicast = is_up && !is_loopback;
216
217            let entry = map
218                .entry(friendly.clone())
219                .or_insert_with(|| InterfaceInfo {
220                    name: friendly.clone(),
221                    ipv4_addrs: Vec::new(),
222                    is_up,
223                    is_multicast,
224                    is_loopback,
225                });
226
227            // Walk unicast addresses.
228            let mut ua = (*adapter).FirstUnicastAddress;
229            while !ua.is_null() {
230                let sa = (*ua).Address.lpSockaddr;
231                if !sa.is_null() && (*sa).sa_family == AF_INET as u16 {
232                    let sin = sa as *const SOCKADDR_IN;
233                    let raw = u32::from_be((*sin).sin_addr.S_un.S_addr);
234                    entry.ipv4_addrs.push(Ipv4Addr::from(raw));
235                }
236                ua = (*ua).Next;
237            }
238
239            adapter = (*adapter).Next;
240        }
241    }
242
243    map.into_values().collect()
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn glob_exact() {
252        assert!(glob_match(b"eth0", b"eth0"));
253        assert!(!glob_match(b"eth0", b"eth1"));
254    }
255
256    #[test]
257    fn glob_star_prefix() {
258        assert!(glob_match(b"eth*", b"eth0"));
259        assert!(glob_match(b"eth*", b"eth10"));
260        assert!(!glob_match(b"eth*", b"enp3s0"));
261    }
262
263    #[test]
264    fn glob_star_all() {
265        assert!(glob_match(b"*", b"eth0"));
266        assert!(glob_match(b"*", b"lo"));
267        assert!(glob_match(b"*", b""));
268    }
269
270    #[test]
271    fn glob_question_mark() {
272        assert!(glob_match(b"eth?", b"eth0"));
273        assert!(!glob_match(b"eth?", b"eth10"));
274    }
275
276    #[test]
277    fn glob_docker_blacklist() {
278        assert!(glob_match(b"docker*", b"docker0"));
279        assert!(glob_match(b"docker*", b"docker_gwbridge"));
280        assert!(!glob_match(b"docker*", b"eth0"));
281    }
282
283    #[test]
284    fn interface_allowed_basic() {
285        let wl = vec!["eth*".to_owned(), "en*".to_owned()];
286        let bl = vec!["lo".to_owned(), "docker*".to_owned()];
287        assert!(interface_allowed("eth0", &wl, &bl));
288        assert!(interface_allowed("en0", &wl, &bl));
289        assert!(!interface_allowed("lo", &wl, &bl));
290        assert!(!interface_allowed("docker0", &wl, &bl));
291        assert!(!interface_allowed("virbr0", &wl, &bl)); // not in whitelist
292    }
293
294    #[test]
295    fn interface_allowed_empty_whitelist_allows_all() {
296        let bl = vec!["lo".to_owned()];
297        assert!(interface_allowed("eth0", &[], &bl));
298        assert!(!interface_allowed("lo", &[], &bl));
299    }
300}