use std::net::{IpAddr, Ipv4Addr};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct InterfaceFilter {
pub whitelist: Option<Vec<String>>,
pub blacklist: Option<Vec<String>>,
}
impl InterfaceFilter {
pub fn new() -> Self {
Self::default()
}
pub fn with_whitelist(mut self, whitelist: Vec<String>) -> Self {
self.whitelist = Some(whitelist);
self
}
pub fn with_blacklist(mut self, blacklist: Vec<String>) -> Self {
self.blacklist = Some(blacklist);
self
}
pub fn is_ignored(&self, addresses: &[IpAddr]) -> bool {
if matches!(&self.whitelist, Some(wl) if !any_address_matches(addresses, wl)) {
return true;
}
if matches!(&self.blacklist, Some(bl) if any_address_matches(addresses, bl)) {
return true;
}
false
}
}
fn any_address_matches(addresses: &[IpAddr], patterns: &[String]) -> bool {
addresses.iter().any(|address| {
let address = address.to_string();
patterns
.iter()
.any(|pattern| matches_ip_filter(pattern, &address))
})
}
pub fn matches_ip_filter(pattern: &str, address: &str) -> bool {
match pattern.split_once('*') {
None => pattern == address,
Some((prefix, rest)) => {
let Some(remainder) = address.strip_prefix(prefix) else {
return false;
};
let max_len = remainder
.find('.')
.unwrap_or(remainder.len())
.min(remainder.len());
(1..=max_len).any(|len| matches_ip_filter(rest, &remainder[len..]))
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LocalInterfaceV4 {
pub name: String,
pub address: Ipv4Addr,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LocalInterfaceV6 {
pub name: String,
pub index: u32,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LocalInterfaces {
pub v4: Vec<LocalInterfaceV4>,
pub v6: Vec<LocalInterfaceV6>,
}
pub fn local_interfaces(filter: &InterfaceFilter) -> std::io::Result<LocalInterfaces> {
struct Entry {
name: String,
index: Option<u32>,
addresses: Vec<IpAddr>,
}
let mut by_name: Vec<Entry> = Vec::new();
for interface in if_addrs::get_if_addrs()? {
if interface.is_loopback() {
continue;
}
match by_name
.iter_mut()
.find(|entry| entry.name == interface.name)
{
Some(entry) => {
entry.index = entry.index.or(interface.index);
entry.addresses.push(interface.ip());
}
None => by_name.push(Entry {
name: interface.name.clone(),
index: interface.index,
addresses: vec![interface.ip()],
}),
}
}
let mut result = LocalInterfaces {
v4: Vec::new(),
v6: Vec::new(),
};
for entry in by_name {
if filter.is_ignored(&entry.addresses) {
tracing::debug!(
"Ignoring network interface {} ({:?})",
entry.name,
entry.addresses
);
continue;
}
let mut has_v6 = false;
for address in &entry.addresses {
match address {
IpAddr::V4(address) => result.v4.push(LocalInterfaceV4 {
name: entry.name.clone(),
address: *address,
}),
IpAddr::V6(_) => has_v6 = true,
}
}
if has_v6 {
match entry.index {
Some(index) => result.v6.push(LocalInterfaceV6 {
name: entry.name,
index,
}),
None => tracing::debug!(
"Interface {} has no index, not using it for IPv6 multicast",
entry.name
),
}
}
}
Ok(result)
}
pub fn is_sweepable_interface(name: &str, ip: &Ipv4Addr) -> bool {
if ip.is_loopback() {
return false;
}
let octets = ip.octets();
if octets[0] == 100 && (octets[1] & 0xC0) == 64 {
return false;
}
let lower = name.to_ascii_lowercase();
if lower.starts_with("tailscale")
|| lower.starts_with("docker")
|| lower.starts_with("vboxnet")
|| lower.starts_with("veth")
|| lower.starts_with("br-")
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_matches_ip_filter() {
assert!(matches_ip_filter("192.168.1.*", "192.168.1.100"));
assert!(matches_ip_filter("192.168.1.*", "192.168.1.1"));
assert!(!matches_ip_filter("192.168.1.*", "192.168.2.1"));
assert!(!matches_ip_filter("192.168.1.*", "192.168.10.1"));
assert!(matches_ip_filter("10.0.*.*", "10.0.1.2"));
assert!(matches_ip_filter("127.0.0.1", "127.0.0.1"));
assert!(!matches_ip_filter("127.0.0.1", "127.0.0.2"));
}
#[test]
fn test_interface_filter_whitelist_blacklist() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50));
let filter = InterfaceFilter::new().with_whitelist(vec!["192.168.1.*".to_string()]);
assert!(!filter.is_ignored(&[ip]));
let filter = InterfaceFilter::new().with_whitelist(vec!["10.0.0.*".to_string()]);
assert!(filter.is_ignored(&[ip]));
let filter = InterfaceFilter::new().with_blacklist(vec!["192.168.1.*".to_string()]);
assert!(filter.is_ignored(&[ip]));
}
}