use parking_lot::RwLock;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkInterface {
pub name: String,
pub addresses: Vec<IpAddr>,
pub interface_type: InterfaceType,
pub is_active: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InterfaceType {
Ethernet,
WiFi,
Cellular,
Loopback,
Other,
}
impl InterfaceType {
pub fn from_name(name: &str) -> Self {
let lower = name.to_lowercase();
if lower.contains("eth") || lower.contains("en") && lower.contains("p") {
InterfaceType::Ethernet
} else if lower.contains("wlan") || lower.contains("wifi") || lower.contains("wl") {
InterfaceType::WiFi
} else if lower.contains("cellular") || lower.contains("wwan") || lower.contains("ppp") {
InterfaceType::Cellular
} else if lower.contains("lo") {
InterfaceType::Loopback
} else {
InterfaceType::Other
}
}
pub fn priority(&self) -> u8 {
match self {
InterfaceType::Ethernet => 3,
InterfaceType::WiFi => 2,
InterfaceType::Cellular => 1,
InterfaceType::Loopback => 0,
InterfaceType::Other => 0,
}
}
}
#[derive(Debug, Clone)]
pub enum NetworkChange {
InterfaceAdded(NetworkInterface),
InterfaceRemoved(NetworkInterface),
PrimaryInterfaceChanged {
old: Option<NetworkInterface>,
new: NetworkInterface,
},
AddressChanged {
interface: String,
old_addresses: Vec<IpAddr>,
new_addresses: Vec<IpAddr>,
},
InterfaceUp(NetworkInterface),
InterfaceDown(NetworkInterface),
}
#[derive(Debug, Clone)]
pub struct NetworkMonitorConfig {
pub poll_interval: Duration,
pub debounce_duration: Duration,
pub monitor_loopback: bool,
}
impl Default for NetworkMonitorConfig {
fn default() -> Self {
Self {
poll_interval: Duration::from_secs(5),
debounce_duration: Duration::from_millis(500),
monitor_loopback: false,
}
}
}
impl NetworkMonitorConfig {
pub fn mobile() -> Self {
Self {
poll_interval: Duration::from_secs(2), debounce_duration: Duration::from_millis(1000), monitor_loopback: false,
}
}
pub fn server() -> Self {
Self {
poll_interval: Duration::from_secs(30), debounce_duration: Duration::from_millis(100),
monitor_loopback: false,
}
}
}
pub struct NetworkMonitor {
config: NetworkMonitorConfig,
interfaces: Arc<RwLock<HashMap<String, NetworkInterface>>>,
primary_interface: Arc<RwLock<Option<NetworkInterface>>>,
last_change: Arc<RwLock<Instant>>,
stats: Arc<RwLock<NetworkMonitorStats>>,
}
#[derive(Debug, Clone, Default)]
pub struct NetworkMonitorStats {
pub interfaces_added: usize,
pub interfaces_removed: usize,
pub primary_changes: usize,
pub address_changes: usize,
pub total_changes: usize,
}
#[derive(Debug, Error)]
pub enum NetworkMonitorError {
#[error("Failed to get network interfaces: {0}")]
InterfaceQueryFailed(String),
#[error("No active network interfaces found")]
NoActiveInterfaces,
}
impl NetworkMonitor {
pub fn new(config: NetworkMonitorConfig) -> Self {
Self {
config,
interfaces: Arc::new(RwLock::new(HashMap::new())),
primary_interface: Arc::new(RwLock::new(None)),
last_change: Arc::new(RwLock::new(Instant::now())),
stats: Arc::new(RwLock::new(NetworkMonitorStats::default())),
}
}
pub fn get_interfaces(&self) -> HashMap<String, NetworkInterface> {
self.interfaces.read().clone()
}
pub fn get_primary_interface(&self) -> Option<NetworkInterface> {
self.primary_interface.read().clone()
}
pub fn check_for_changes(&self) -> Result<Vec<NetworkChange>, NetworkMonitorError> {
let current_interfaces = self.query_system_interfaces()?;
{
let last_change = *self.last_change.read();
if last_change.elapsed() < self.config.debounce_duration {
return Ok(Vec::new());
}
}
let mut changes = Vec::new();
let mut interfaces_lock = self.interfaces.write();
for (name, new_iface) in ¤t_interfaces {
if let Some(old_iface) = interfaces_lock.get(name) {
if old_iface.addresses != new_iface.addresses {
debug!(
"Address changed on interface {}: {:?} -> {:?}",
name, old_iface.addresses, new_iface.addresses
);
changes.push(NetworkChange::AddressChanged {
interface: name.clone(),
old_addresses: old_iface.addresses.clone(),
new_addresses: new_iface.addresses.clone(),
});
self.stats.write().address_changes += 1;
}
if old_iface.is_active != new_iface.is_active {
if new_iface.is_active {
info!("Interface {} is now active", name);
changes.push(NetworkChange::InterfaceUp(new_iface.clone()));
} else {
info!("Interface {} is now inactive", name);
changes.push(NetworkChange::InterfaceDown(new_iface.clone()));
}
}
} else {
info!("New interface detected: {}", name);
changes.push(NetworkChange::InterfaceAdded(new_iface.clone()));
self.stats.write().interfaces_added += 1;
}
}
for (name, old_iface) in interfaces_lock.iter() {
if !current_interfaces.contains_key(name) {
info!("Interface removed: {}", name);
changes.push(NetworkChange::InterfaceRemoved(old_iface.clone()));
self.stats.write().interfaces_removed += 1;
}
}
*interfaces_lock = current_interfaces.clone();
drop(interfaces_lock);
let old_primary = self.primary_interface.read().clone();
let new_primary = self.select_primary_interface(¤t_interfaces);
if old_primary != new_primary {
if let Some(new) = &new_primary {
info!(
"Primary interface changed: {:?} -> {}",
old_primary.as_ref().map(|i| i.name.as_str()),
new.name
);
changes.push(NetworkChange::PrimaryInterfaceChanged {
old: old_primary,
new: new.clone(),
});
self.stats.write().primary_changes += 1;
}
*self.primary_interface.write() = new_primary;
}
if !changes.is_empty() {
*self.last_change.write() = Instant::now();
self.stats.write().total_changes += changes.len();
}
Ok(changes)
}
fn select_primary_interface(
&self,
interfaces: &HashMap<String, NetworkInterface>,
) -> Option<NetworkInterface> {
interfaces
.values()
.filter(|iface| {
iface.is_active
&& !iface.addresses.is_empty()
&& (self.config.monitor_loopback
|| iface.interface_type != InterfaceType::Loopback)
})
.max_by_key(|iface| iface.interface_type.priority())
.cloned()
}
fn query_system_interfaces(
&self,
) -> Result<HashMap<String, NetworkInterface>, NetworkMonitorError> {
warn!("Using mock network interface detection - implement platform-specific querying for production use");
let interfaces = HashMap::new();
Ok(interfaces)
}
pub fn stats(&self) -> NetworkMonitorStats {
self.stats.read().clone()
}
pub fn reset_stats(&self) {
*self.stats.write() = NetworkMonitorStats::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interface_type_from_name() {
assert_eq!(InterfaceType::from_name("eth0"), InterfaceType::Ethernet);
assert_eq!(InterfaceType::from_name("wlan0"), InterfaceType::WiFi);
assert_eq!(InterfaceType::from_name("wwan0"), InterfaceType::Cellular);
assert_eq!(InterfaceType::from_name("lo"), InterfaceType::Loopback);
}
#[test]
fn test_interface_priority() {
assert!(InterfaceType::Ethernet.priority() > InterfaceType::WiFi.priority());
assert!(InterfaceType::WiFi.priority() > InterfaceType::Cellular.priority());
assert!(InterfaceType::Cellular.priority() > InterfaceType::Loopback.priority());
}
#[test]
fn test_network_monitor_creation() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
assert!(monitor.get_interfaces().is_empty());
assert!(monitor.get_primary_interface().is_none());
}
#[test]
fn test_select_primary_interface() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
let mut interfaces = HashMap::new();
interfaces.insert(
"wlan0".to_string(),
NetworkInterface {
name: "wlan0".to_string(),
addresses: vec!["192.168.1.100"
.parse()
.expect("test: valid IP address literal should parse")],
interface_type: InterfaceType::WiFi,
is_active: true,
},
);
interfaces.insert(
"wwan0".to_string(),
NetworkInterface {
name: "wwan0".to_string(),
addresses: vec!["10.0.0.100"
.parse()
.expect("test: valid IP address literal should parse")],
interface_type: InterfaceType::Cellular,
is_active: true,
},
);
let primary = monitor.select_primary_interface(&interfaces);
assert!(primary.is_some());
assert_eq!(
primary
.expect("test: primary interface should be Some for active interfaces")
.interface_type,
InterfaceType::WiFi
);
}
#[test]
fn test_ethernet_preferred_over_wifi() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
let mut interfaces = HashMap::new();
interfaces.insert(
"eth0".to_string(),
NetworkInterface {
name: "eth0".to_string(),
addresses: vec!["192.168.1.50"
.parse()
.expect("test: valid IP address literal should parse")],
interface_type: InterfaceType::Ethernet,
is_active: true,
},
);
interfaces.insert(
"wlan0".to_string(),
NetworkInterface {
name: "wlan0".to_string(),
addresses: vec!["192.168.1.100"
.parse()
.expect("test: valid IP address literal should parse")],
interface_type: InterfaceType::WiFi,
is_active: true,
},
);
let primary = monitor.select_primary_interface(&interfaces);
assert!(primary.is_some());
assert_eq!(
primary
.expect("test: primary interface should be Some when ethernet is active")
.interface_type,
InterfaceType::Ethernet
);
}
#[test]
fn test_inactive_interface_not_selected() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
let mut interfaces = HashMap::new();
interfaces.insert(
"wlan0".to_string(),
NetworkInterface {
name: "wlan0".to_string(),
addresses: vec!["192.168.1.100"
.parse()
.expect("test: valid IP address literal should parse")],
interface_type: InterfaceType::WiFi,
is_active: false, },
);
let primary = monitor.select_primary_interface(&interfaces);
assert!(primary.is_none());
}
#[test]
fn test_interface_without_addresses_not_selected() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
let mut interfaces = HashMap::new();
interfaces.insert(
"wlan0".to_string(),
NetworkInterface {
name: "wlan0".to_string(),
addresses: vec![], interface_type: InterfaceType::WiFi,
is_active: true,
},
);
let primary = monitor.select_primary_interface(&interfaces);
assert!(primary.is_none());
}
#[test]
fn test_loopback_filtering() {
let config = NetworkMonitorConfig {
monitor_loopback: false,
..Default::default()
};
let monitor = NetworkMonitor::new(config);
let mut interfaces = HashMap::new();
interfaces.insert(
"lo".to_string(),
NetworkInterface {
name: "lo".to_string(),
addresses: vec!["127.0.0.1"
.parse()
.expect("test: valid loopback IP literal should parse")],
interface_type: InterfaceType::Loopback,
is_active: true,
},
);
let primary = monitor.select_primary_interface(&interfaces);
assert!(primary.is_none()); }
#[test]
fn test_stats_initialization() {
let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
let stats = monitor.stats();
assert_eq!(stats.interfaces_added, 0);
assert_eq!(stats.interfaces_removed, 0);
assert_eq!(stats.primary_changes, 0);
assert_eq!(stats.address_changes, 0);
assert_eq!(stats.total_changes, 0);
}
#[test]
fn test_mobile_config() {
let config = NetworkMonitorConfig::mobile();
assert_eq!(config.poll_interval, Duration::from_secs(2));
assert!(!config.monitor_loopback);
}
#[test]
fn test_server_config() {
let config = NetworkMonitorConfig::server();
assert_eq!(config.poll_interval, Duration::from_secs(30));
assert!(!config.monitor_loopback);
}
}