Skip to main content

lanflare_core/discovery/
interface.rs

1//! Network interface discovery and filtering.
2
3use std::net::{IpAddr, Ipv4Addr};
4
5/// Restricts the network interfaces that are used.
6///
7/// Filters are matched against every address of an interface. A `*` matches one
8/// or more characters that are not a `.`, so `192.168.1.*` matches
9/// `192.168.1.42` but not `192.168.10.1`.
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub struct InterfaceFilter {
12    /// When set, only interfaces with a matching address are used.
13    pub whitelist: Option<Vec<String>>,
14
15    /// When set, interfaces with a matching address are skipped.
16    pub blacklist: Option<Vec<String>>,
17}
18
19impl InterfaceFilter {
20    /// Creates a new empty filter that accepts all interfaces.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Sets the address whitelist.
26    pub fn with_whitelist(mut self, whitelist: Vec<String>) -> Self {
27        self.whitelist = Some(whitelist);
28        self
29    }
30
31    /// Sets the address blacklist.
32    pub fn with_blacklist(mut self, blacklist: Vec<String>) -> Self {
33        self.blacklist = Some(blacklist);
34        self
35    }
36
37    /// Returns `true` when an interface with the given addresses must not be used.
38    pub fn is_ignored(&self, addresses: &[IpAddr]) -> bool {
39        if matches!(&self.whitelist, Some(wl) if !any_address_matches(addresses, wl)) {
40            return true;
41        }
42
43        if matches!(&self.blacklist, Some(bl) if any_address_matches(addresses, bl)) {
44            return true;
45        }
46
47        false
48    }
49
50}
51
52/// Returns `true` when at least one of `addresses` matches at least one of `patterns`.
53fn any_address_matches(addresses: &[IpAddr], patterns: &[String]) -> bool {
54    addresses.iter().any(|address| {
55        let address = address.to_string();
56        patterns
57            .iter()
58            .any(|pattern| matches_ip_filter(pattern, &address))
59    })
60}
61
62/// Matches the whole address against a filter pattern.
63pub fn matches_ip_filter(pattern: &str, address: &str) -> bool {
64    match pattern.split_once('*') {
65        None => pattern == address,
66        Some((prefix, rest)) => {
67            let Some(remainder) = address.strip_prefix(prefix) else {
68                return false;
69            };
70
71            // `*` must consume at least one character and may not cross a `.`.
72            let max_len = remainder
73                .find('.')
74                .unwrap_or(remainder.len())
75                .min(remainder.len());
76
77            (1..=max_len).any(|len| matches_ip_filter(rest, &remainder[len..]))
78        }
79    }
80}
81
82/// A local IPv4 address that a multicast socket can be bound to.
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct LocalInterfaceV4 {
85    /// The name of the interface, only used for logging.
86    pub name: String,
87
88    /// The IPv4 address of the interface, used to join the multicast group.
89    pub address: Ipv4Addr,
90}
91
92/// A local interface that an IPv6 multicast socket can be bound to.
93///
94/// IPv6 group membership is joined per interface rather than per address, so a
95/// single socket per interface covers all of its IPv6 addresses.
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct LocalInterfaceV6 {
98    /// The name of the interface, only used for logging.
99    pub name: String,
100
101    /// The index of the interface, used to join the multicast group.
102    pub index: u32,
103}
104
105/// The local network interfaces that multicast sockets can be bound to.
106#[derive(Clone, Debug, Default, Eq, PartialEq)]
107pub struct LocalInterfaces {
108    /// One entry per IPv4 address of an interface.
109    pub v4: Vec<LocalInterfaceV4>,
110
111    /// One entry per interface that has at least one IPv6 address.
112    pub v6: Vec<LocalInterfaceV6>,
113}
114
115/// Returns the addresses of all non-loopback interfaces that pass `filter`.
116pub fn local_interfaces(filter: &InterfaceFilter) -> std::io::Result<LocalInterfaces> {
117    struct Entry {
118        name: String,
119        index: Option<u32>,
120        addresses: Vec<IpAddr>,
121    }
122
123    // Filters apply to an interface as a whole, so all of its addresses have to
124    // be known before any of them can be accepted.
125    let mut by_name: Vec<Entry> = Vec::new();
126    for interface in if_addrs::get_if_addrs()? {
127        if interface.is_loopback() {
128            continue;
129        }
130
131        match by_name
132            .iter_mut()
133            .find(|entry| entry.name == interface.name)
134        {
135            Some(entry) => {
136                entry.index = entry.index.or(interface.index);
137                entry.addresses.push(interface.ip());
138            }
139            None => by_name.push(Entry {
140                name: interface.name.clone(),
141                index: interface.index,
142                addresses: vec![interface.ip()],
143            }),
144        }
145    }
146
147    let mut result = LocalInterfaces {
148        v4: Vec::new(),
149        v6: Vec::new(),
150    };
151    for entry in by_name {
152        if filter.is_ignored(&entry.addresses) {
153            tracing::debug!(
154                "Ignoring network interface {} ({:?})",
155                entry.name,
156                entry.addresses
157            );
158            continue;
159        }
160
161        let mut has_v6 = false;
162        for address in &entry.addresses {
163            match address {
164                // IPv4 group membership is joined per address.
165                IpAddr::V4(address) => result.v4.push(LocalInterfaceV4 {
166                    name: entry.name.clone(),
167                    address: *address,
168                }),
169                IpAddr::V6(_) => has_v6 = true,
170            }
171        }
172
173        if has_v6 {
174            match entry.index {
175                Some(index) => result.v6.push(LocalInterfaceV6 {
176                    name: entry.name,
177                    index,
178                }),
179                // Joining an IPv6 group requires the interface index.
180                None => tracing::debug!(
181                    "Interface {} has no index, not using it for IPv6 multicast",
182                    entry.name
183                ),
184            }
185        }
186    }
187
188    Ok(result)
189}
190
191/// Determines if an interface is an active LAN subnet suitable for a /24 sweep.
192///
193/// Filters out loopback, virtual containers (docker, veth, vboxnet), and point-to-point
194/// VPN / CGNAT ranges (Tailscale `100.64.0.0/10`, wireguard, tun/tap) which do not carry /24 LANs.
195pub fn is_sweepable_interface(name: &str, ip: &Ipv4Addr) -> bool {
196    if ip.is_loopback() {
197        return false;
198    }
199
200    // Skip CGNAT / Carrier Grade NAT (100.64.0.0/10) used by Tailscale, VPNs, etc.
201    let octets = ip.octets();
202    if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
203        return false;
204    }
205
206    let lower = name.to_ascii_lowercase();
207    if lower.starts_with("tailscale")
208        || lower.starts_with("docker")
209        || lower.starts_with("vboxnet")
210        || lower.starts_with("veth")
211        || lower.starts_with("br-")
212    {
213        return false;
214    }
215
216    true
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use std::net::Ipv4Addr;
223
224    #[test]
225    fn test_matches_ip_filter() {
226        assert!(matches_ip_filter("192.168.1.*", "192.168.1.100"));
227        assert!(matches_ip_filter("192.168.1.*", "192.168.1.1"));
228        assert!(!matches_ip_filter("192.168.1.*", "192.168.2.1"));
229        assert!(!matches_ip_filter("192.168.1.*", "192.168.10.1"));
230        assert!(matches_ip_filter("10.0.*.*", "10.0.1.2"));
231        assert!(matches_ip_filter("127.0.0.1", "127.0.0.1"));
232        assert!(!matches_ip_filter("127.0.0.1", "127.0.0.2"));
233    }
234
235    #[test]
236    fn test_interface_filter_whitelist_blacklist() {
237        let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50));
238        let filter = InterfaceFilter::new().with_whitelist(vec!["192.168.1.*".to_string()]);
239        assert!(!filter.is_ignored(&[ip]));
240
241        let filter = InterfaceFilter::new().with_whitelist(vec!["10.0.0.*".to_string()]);
242        assert!(filter.is_ignored(&[ip]));
243
244        let filter = InterfaceFilter::new().with_blacklist(vec!["192.168.1.*".to_string()]);
245        assert!(filter.is_ignored(&[ip]));
246    }
247}