use std::{
collections::HashSet,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
time::Duration,
};
use tokio::{sync::Notify, time::interval};
use tracing::{debug, error, info};
pub struct InterfaceScanner {
scan_period: Duration,
cancel_notify: Arc<Notify>,
callback: Arc<dyn Fn(Vec<IpAddr>) + Send + Sync>,
}
impl InterfaceScanner {
pub fn new<F>(scan_period: Duration, callback: F) -> Self
where
F: Fn(Vec<IpAddr>) + Send + Sync + 'static,
{
Self {
scan_period,
cancel_notify: Arc::new(Notify::new()),
callback: Arc::new(callback),
}
}
pub async fn enable(&self, enable: bool) {
if enable {
self.start_scanning().await;
} else {
self.cancel_notify.notify_one();
}
}
pub async fn scan_once(&self) {
let interfaces = scan_network_interfaces().await;
(self.callback)(interfaces);
}
async fn start_scanning(&self) {
let mut interval = interval(self.scan_period);
let cancel_notify = self.cancel_notify.clone();
let callback = self.callback.clone();
tokio::spawn(async move {
let mut last_interfaces: Option<HashSet<IpAddr>> = None;
loop {
tokio::select! {
_ = interval.tick() => {
debug!("Scanning network interfaces");
let interfaces = scan_network_interfaces().await;
let interface_set: HashSet<IpAddr> = interfaces.iter().copied().collect();
if last_interfaces.as_ref() != Some(&interface_set) {
info!("Network interfaces changed: {:?}", interfaces);
callback(interfaces);
last_interfaces = Some(interface_set);
}
}
_ = cancel_notify.notified() => {
debug!("Interface scanner cancelled");
break;
}
}
}
});
}
}
pub async fn scan_network_interfaces() -> Vec<IpAddr> {
let mut interfaces = Vec::new();
match get_network_interfaces().await {
Ok(addrs) => {
for addr in addrs {
if is_usable_interface(&addr) {
interfaces.push(addr);
}
}
}
Err(e) => {
error!("Failed to scan network interfaces: {}", e);
}
}
interfaces.sort();
interfaces.dedup();
debug!("Found {} usable network interfaces", interfaces.len());
interfaces
}
async fn get_network_interfaces() -> Result<Vec<IpAddr>, Box<dyn std::error::Error + Send + Sync>> {
tokio::task::spawn_blocking(|| {
let mut addresses = Vec::new();
match if_addrs::get_if_addrs() {
Ok(interfaces) => {
for interface in interfaces {
let addr = interface.ip();
addresses.push(addr);
debug!("Found interface: {} ({})", interface.name, addr);
}
}
Err(e) => {
return Err(format!("Failed to get network interfaces: {}", e).into());
}
}
Ok(addresses)
})
.await?
}
fn is_usable_interface(addr: &IpAddr) -> bool {
match addr {
IpAddr::V4(ipv4) => is_usable_ipv4(ipv4),
IpAddr::V6(ipv6) => is_usable_ipv6(ipv6),
}
}
fn is_usable_ipv4(addr: &Ipv4Addr) -> bool {
!addr.is_loopback()
&& !addr.is_broadcast()
&& !addr.is_multicast()
&& !addr.is_unspecified()
&& !addr.is_link_local()
&& *addr != Ipv4Addr::new(0, 0, 0, 0)
}
fn is_usable_ipv6(addr: &Ipv6Addr) -> bool {
!addr.is_loopback()
&& !addr.is_multicast()
&& !addr.is_unspecified()
&& *addr != Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1) }
pub fn get_ipv6_scope_id(addr: &Ipv6Addr) -> Option<u32> {
if addr.is_unicast_link_local() {
Some(0) } else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[tokio::test]
async fn test_interface_scanning() {
let _ = tracing_subscriber::fmt::try_init();
let interfaces = Arc::new(Mutex::new(Vec::new()));
let interfaces_clone = interfaces.clone();
let scanner = InterfaceScanner::new(Duration::from_millis(100), move |addrs| {
*interfaces_clone.lock().unwrap() = addrs;
});
scanner.scan_once().await;
let scanned_interfaces = interfaces.lock().unwrap().clone();
assert!(
!scanned_interfaces.is_empty(),
"Should find at least one interface"
);
for addr in &scanned_interfaces {
assert!(
is_usable_interface(addr),
"All returned interfaces should be usable"
);
}
info!(
"Found {} interfaces: {:?}",
scanned_interfaces.len(),
scanned_interfaces
);
}
#[test]
fn test_usable_interface_detection() {
assert!(!is_usable_ipv4(&Ipv4Addr::LOCALHOST)); assert!(!is_usable_ipv4(&Ipv4Addr::UNSPECIFIED)); assert!(!is_usable_ipv4(&Ipv4Addr::BROADCAST)); assert!(is_usable_ipv4(&Ipv4Addr::new(192, 168, 1, 100)));
assert!(!is_usable_ipv6(&Ipv6Addr::LOCALHOST)); assert!(!is_usable_ipv6(&Ipv6Addr::UNSPECIFIED)); assert!(is_usable_ipv6(&Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1))); assert!(is_usable_ipv6(&Ipv6Addr::new(
0x2001, 0xdb8, 0, 0, 0, 0, 0, 1
))); }
}