ipfrs_network/
network_monitor.rs

1//! Network interface monitoring and switch detection
2//!
3//! This module provides functionality to detect network interface changes,
4//! which is crucial for mobile devices that switch between WiFi and cellular
5//! networks, or for any device where network connectivity may change.
6
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::net::IpAddr;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use thiserror::Error;
13use tracing::{debug, info, warn};
14
15/// Network interface information
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct NetworkInterface {
18    /// Interface name (e.g., "eth0", "wlan0", "cellular0")
19    pub name: String,
20    /// IP addresses assigned to this interface
21    pub addresses: Vec<IpAddr>,
22    /// Interface type
23    pub interface_type: InterfaceType,
24    /// Whether the interface is currently active
25    pub is_active: bool,
26}
27
28/// Type of network interface
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum InterfaceType {
31    /// Wired Ethernet connection
32    Ethernet,
33    /// WiFi connection
34    WiFi,
35    /// Cellular/mobile data connection
36    Cellular,
37    /// Loopback interface
38    Loopback,
39    /// Unknown or other type
40    Other,
41}
42
43impl InterfaceType {
44    /// Determine interface type from interface name
45    pub fn from_name(name: &str) -> Self {
46        let lower = name.to_lowercase();
47        if lower.contains("eth") || lower.contains("en") && lower.contains("p") {
48            InterfaceType::Ethernet
49        } else if lower.contains("wlan") || lower.contains("wifi") || lower.contains("wl") {
50            InterfaceType::WiFi
51        } else if lower.contains("cellular") || lower.contains("wwan") || lower.contains("ppp") {
52            InterfaceType::Cellular
53        } else if lower.contains("lo") {
54            InterfaceType::Loopback
55        } else {
56            InterfaceType::Other
57        }
58    }
59
60    /// Get priority for this interface type (higher = preferred)
61    pub fn priority(&self) -> u8 {
62        match self {
63            InterfaceType::Ethernet => 3,
64            InterfaceType::WiFi => 2,
65            InterfaceType::Cellular => 1,
66            InterfaceType::Loopback => 0,
67            InterfaceType::Other => 0,
68        }
69    }
70}
71
72/// Network change event
73#[derive(Debug, Clone)]
74pub enum NetworkChange {
75    /// New interface became available
76    InterfaceAdded(NetworkInterface),
77    /// Interface was removed
78    InterfaceRemoved(NetworkInterface),
79    /// Primary interface changed (e.g., switched from WiFi to Cellular)
80    PrimaryInterfaceChanged {
81        old: Option<NetworkInterface>,
82        new: NetworkInterface,
83    },
84    /// IP address changed on an interface
85    AddressChanged {
86        interface: String,
87        old_addresses: Vec<IpAddr>,
88        new_addresses: Vec<IpAddr>,
89    },
90    /// Interface became active
91    InterfaceUp(NetworkInterface),
92    /// Interface became inactive
93    InterfaceDown(NetworkInterface),
94}
95
96/// Network monitor configuration
97#[derive(Debug, Clone)]
98pub struct NetworkMonitorConfig {
99    /// Polling interval for checking network changes
100    pub poll_interval: Duration,
101    /// Minimum time between network change notifications (debouncing)
102    pub debounce_duration: Duration,
103    /// Whether to monitor loopback interfaces
104    pub monitor_loopback: bool,
105}
106
107impl Default for NetworkMonitorConfig {
108    fn default() -> Self {
109        Self {
110            poll_interval: Duration::from_secs(5),
111            debounce_duration: Duration::from_millis(500),
112            monitor_loopback: false,
113        }
114    }
115}
116
117impl NetworkMonitorConfig {
118    /// Mobile device configuration with frequent polling
119    pub fn mobile() -> Self {
120        Self {
121            poll_interval: Duration::from_secs(2), // Poll more frequently
122            debounce_duration: Duration::from_millis(1000), // Longer debounce
123            monitor_loopback: false,
124        }
125    }
126
127    /// Server configuration with less frequent polling
128    pub fn server() -> Self {
129        Self {
130            poll_interval: Duration::from_secs(30), // Poll less frequently
131            debounce_duration: Duration::from_millis(100),
132            monitor_loopback: false,
133        }
134    }
135}
136
137/// Network monitor for detecting interface changes
138pub struct NetworkMonitor {
139    config: NetworkMonitorConfig,
140    /// Current state of network interfaces
141    interfaces: Arc<RwLock<HashMap<String, NetworkInterface>>>,
142    /// Primary (preferred) interface
143    primary_interface: Arc<RwLock<Option<NetworkInterface>>>,
144    /// Last time a change was detected
145    last_change: Arc<RwLock<Instant>>,
146    /// Statistics
147    stats: Arc<RwLock<NetworkMonitorStats>>,
148}
149
150/// Network monitor statistics
151#[derive(Debug, Clone, Default)]
152pub struct NetworkMonitorStats {
153    /// Number of interface additions detected
154    pub interfaces_added: usize,
155    /// Number of interface removals detected
156    pub interfaces_removed: usize,
157    /// Number of primary interface changes
158    pub primary_changes: usize,
159    /// Number of address changes detected
160    pub address_changes: usize,
161    /// Total network change events
162    pub total_changes: usize,
163}
164
165/// Errors that can occur during network monitoring
166#[derive(Debug, Error)]
167pub enum NetworkMonitorError {
168    #[error("Failed to get network interfaces: {0}")]
169    InterfaceQueryFailed(String),
170
171    #[error("No active network interfaces found")]
172    NoActiveInterfaces,
173}
174
175impl NetworkMonitor {
176    /// Create a new network monitor
177    pub fn new(config: NetworkMonitorConfig) -> Self {
178        Self {
179            config,
180            interfaces: Arc::new(RwLock::new(HashMap::new())),
181            primary_interface: Arc::new(RwLock::new(None)),
182            last_change: Arc::new(RwLock::new(Instant::now())),
183            stats: Arc::new(RwLock::new(NetworkMonitorStats::default())),
184        }
185    }
186
187    /// Get current network interfaces
188    pub fn get_interfaces(&self) -> HashMap<String, NetworkInterface> {
189        self.interfaces.read().clone()
190    }
191
192    /// Get the primary (preferred) interface
193    pub fn get_primary_interface(&self) -> Option<NetworkInterface> {
194        self.primary_interface.read().clone()
195    }
196
197    /// Check for network changes and return any detected changes
198    ///
199    /// This should be called periodically (e.g., from a background task)
200    pub fn check_for_changes(&self) -> Result<Vec<NetworkChange>, NetworkMonitorError> {
201        // Get current interfaces from the system
202        let current_interfaces = self.query_system_interfaces()?;
203
204        // Debounce: skip if too soon after last change
205        {
206            let last_change = *self.last_change.read();
207            if last_change.elapsed() < self.config.debounce_duration {
208                return Ok(Vec::new());
209            }
210        }
211
212        let mut changes = Vec::new();
213        let mut interfaces_lock = self.interfaces.write();
214
215        // Detect added and changed interfaces
216        for (name, new_iface) in &current_interfaces {
217            if let Some(old_iface) = interfaces_lock.get(name) {
218                // Check for address changes
219                if old_iface.addresses != new_iface.addresses {
220                    debug!(
221                        "Address changed on interface {}: {:?} -> {:?}",
222                        name, old_iface.addresses, new_iface.addresses
223                    );
224                    changes.push(NetworkChange::AddressChanged {
225                        interface: name.clone(),
226                        old_addresses: old_iface.addresses.clone(),
227                        new_addresses: new_iface.addresses.clone(),
228                    });
229                    self.stats.write().address_changes += 1;
230                }
231
232                // Check for status changes
233                if old_iface.is_active != new_iface.is_active {
234                    if new_iface.is_active {
235                        info!("Interface {} is now active", name);
236                        changes.push(NetworkChange::InterfaceUp(new_iface.clone()));
237                    } else {
238                        info!("Interface {} is now inactive", name);
239                        changes.push(NetworkChange::InterfaceDown(new_iface.clone()));
240                    }
241                }
242            } else {
243                // New interface
244                info!("New interface detected: {}", name);
245                changes.push(NetworkChange::InterfaceAdded(new_iface.clone()));
246                self.stats.write().interfaces_added += 1;
247            }
248        }
249
250        // Detect removed interfaces
251        for (name, old_iface) in interfaces_lock.iter() {
252            if !current_interfaces.contains_key(name) {
253                info!("Interface removed: {}", name);
254                changes.push(NetworkChange::InterfaceRemoved(old_iface.clone()));
255                self.stats.write().interfaces_removed += 1;
256            }
257        }
258
259        // Update stored interfaces
260        *interfaces_lock = current_interfaces.clone();
261        drop(interfaces_lock);
262
263        // Determine primary interface
264        let old_primary = self.primary_interface.read().clone();
265        let new_primary = self.select_primary_interface(&current_interfaces);
266
267        if old_primary != new_primary {
268            if let Some(new) = &new_primary {
269                info!(
270                    "Primary interface changed: {:?} -> {}",
271                    old_primary.as_ref().map(|i| i.name.as_str()),
272                    new.name
273                );
274                changes.push(NetworkChange::PrimaryInterfaceChanged {
275                    old: old_primary,
276                    new: new.clone(),
277                });
278                self.stats.write().primary_changes += 1;
279            }
280            *self.primary_interface.write() = new_primary;
281        }
282
283        if !changes.is_empty() {
284            *self.last_change.write() = Instant::now();
285            self.stats.write().total_changes += changes.len();
286        }
287
288        Ok(changes)
289    }
290
291    /// Select the primary interface based on availability and priority
292    fn select_primary_interface(
293        &self,
294        interfaces: &HashMap<String, NetworkInterface>,
295    ) -> Option<NetworkInterface> {
296        interfaces
297            .values()
298            .filter(|iface| {
299                iface.is_active
300                    && !iface.addresses.is_empty()
301                    && (self.config.monitor_loopback
302                        || iface.interface_type != InterfaceType::Loopback)
303            })
304            .max_by_key(|iface| iface.interface_type.priority())
305            .cloned()
306    }
307
308    /// Query system for current network interfaces
309    ///
310    /// This is a simplified implementation that creates mock data for demonstration.
311    /// In a real implementation, this would use platform-specific APIs to query
312    /// actual network interfaces (e.g., getifaddrs on Unix, GetAdaptersAddresses on Windows)
313    fn query_system_interfaces(
314        &self,
315    ) -> Result<HashMap<String, NetworkInterface>, NetworkMonitorError> {
316        // This is a placeholder implementation
317        // In production, use platform-specific APIs or crates like `if-addrs` or `pnet`
318
319        warn!("Using mock network interface detection - implement platform-specific querying for production use");
320
321        let interfaces = HashMap::new();
322
323        // Mock interface - in real implementation, query actual interfaces
324        // For now, return empty to avoid errors
325
326        Ok(interfaces)
327    }
328
329    /// Get statistics
330    pub fn stats(&self) -> NetworkMonitorStats {
331        self.stats.read().clone()
332    }
333
334    /// Reset statistics
335    pub fn reset_stats(&self) {
336        *self.stats.write() = NetworkMonitorStats::default();
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_interface_type_from_name() {
346        assert_eq!(InterfaceType::from_name("eth0"), InterfaceType::Ethernet);
347        assert_eq!(InterfaceType::from_name("wlan0"), InterfaceType::WiFi);
348        assert_eq!(InterfaceType::from_name("wwan0"), InterfaceType::Cellular);
349        assert_eq!(InterfaceType::from_name("lo"), InterfaceType::Loopback);
350    }
351
352    #[test]
353    fn test_interface_priority() {
354        assert!(InterfaceType::Ethernet.priority() > InterfaceType::WiFi.priority());
355        assert!(InterfaceType::WiFi.priority() > InterfaceType::Cellular.priority());
356        assert!(InterfaceType::Cellular.priority() > InterfaceType::Loopback.priority());
357    }
358
359    #[test]
360    fn test_network_monitor_creation() {
361        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
362        assert!(monitor.get_interfaces().is_empty());
363        assert!(monitor.get_primary_interface().is_none());
364    }
365
366    #[test]
367    fn test_select_primary_interface() {
368        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
369        let mut interfaces = HashMap::new();
370
371        // Add WiFi interface
372        interfaces.insert(
373            "wlan0".to_string(),
374            NetworkInterface {
375                name: "wlan0".to_string(),
376                addresses: vec!["192.168.1.100".parse().unwrap()],
377                interface_type: InterfaceType::WiFi,
378                is_active: true,
379            },
380        );
381
382        // Add cellular interface
383        interfaces.insert(
384            "wwan0".to_string(),
385            NetworkInterface {
386                name: "wwan0".to_string(),
387                addresses: vec!["10.0.0.100".parse().unwrap()],
388                interface_type: InterfaceType::Cellular,
389                is_active: true,
390            },
391        );
392
393        let primary = monitor.select_primary_interface(&interfaces);
394        assert!(primary.is_some());
395        // WiFi should be preferred over cellular
396        assert_eq!(primary.unwrap().interface_type, InterfaceType::WiFi);
397    }
398
399    #[test]
400    fn test_ethernet_preferred_over_wifi() {
401        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
402        let mut interfaces = HashMap::new();
403
404        interfaces.insert(
405            "eth0".to_string(),
406            NetworkInterface {
407                name: "eth0".to_string(),
408                addresses: vec!["192.168.1.50".parse().unwrap()],
409                interface_type: InterfaceType::Ethernet,
410                is_active: true,
411            },
412        );
413
414        interfaces.insert(
415            "wlan0".to_string(),
416            NetworkInterface {
417                name: "wlan0".to_string(),
418                addresses: vec!["192.168.1.100".parse().unwrap()],
419                interface_type: InterfaceType::WiFi,
420                is_active: true,
421            },
422        );
423
424        let primary = monitor.select_primary_interface(&interfaces);
425        assert!(primary.is_some());
426        assert_eq!(primary.unwrap().interface_type, InterfaceType::Ethernet);
427    }
428
429    #[test]
430    fn test_inactive_interface_not_selected() {
431        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
432        let mut interfaces = HashMap::new();
433
434        interfaces.insert(
435            "wlan0".to_string(),
436            NetworkInterface {
437                name: "wlan0".to_string(),
438                addresses: vec!["192.168.1.100".parse().unwrap()],
439                interface_type: InterfaceType::WiFi,
440                is_active: false, // Inactive
441            },
442        );
443
444        let primary = monitor.select_primary_interface(&interfaces);
445        assert!(primary.is_none());
446    }
447
448    #[test]
449    fn test_interface_without_addresses_not_selected() {
450        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
451        let mut interfaces = HashMap::new();
452
453        interfaces.insert(
454            "wlan0".to_string(),
455            NetworkInterface {
456                name: "wlan0".to_string(),
457                addresses: vec![], // No addresses
458                interface_type: InterfaceType::WiFi,
459                is_active: true,
460            },
461        );
462
463        let primary = monitor.select_primary_interface(&interfaces);
464        assert!(primary.is_none());
465    }
466
467    #[test]
468    fn test_loopback_filtering() {
469        let config = NetworkMonitorConfig {
470            monitor_loopback: false,
471            ..Default::default()
472        };
473        let monitor = NetworkMonitor::new(config);
474        let mut interfaces = HashMap::new();
475
476        interfaces.insert(
477            "lo".to_string(),
478            NetworkInterface {
479                name: "lo".to_string(),
480                addresses: vec!["127.0.0.1".parse().unwrap()],
481                interface_type: InterfaceType::Loopback,
482                is_active: true,
483            },
484        );
485
486        let primary = monitor.select_primary_interface(&interfaces);
487        assert!(primary.is_none()); // Loopback should be filtered out
488    }
489
490    #[test]
491    fn test_stats_initialization() {
492        let monitor = NetworkMonitor::new(NetworkMonitorConfig::default());
493        let stats = monitor.stats();
494        assert_eq!(stats.interfaces_added, 0);
495        assert_eq!(stats.interfaces_removed, 0);
496        assert_eq!(stats.primary_changes, 0);
497        assert_eq!(stats.address_changes, 0);
498        assert_eq!(stats.total_changes, 0);
499    }
500
501    #[test]
502    fn test_mobile_config() {
503        let config = NetworkMonitorConfig::mobile();
504        assert_eq!(config.poll_interval, Duration::from_secs(2));
505        assert!(!config.monitor_loopback);
506    }
507
508    #[test]
509    fn test_server_config() {
510        let config = NetworkMonitorConfig::server();
511        assert_eq!(config.poll_interval, Duration::from_secs(30));
512        assert!(!config.monitor_loopback);
513    }
514}