diff --git a/TODO.md b/TODO.md index 13c059e..e1369d9 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ - [x] Add date filter to device event list method (from) - [x] Add a data set (json) to store maps from vendors -> device type; modify the device creation so that it uses it automatically - [x] Add an mDNS/Bonjour scanner -- [ ] Extract the select_interface method and interface logic from ARP scanner to a centralized utility file +- [x] Extract the select_interface method and interface logic from ARP scanner to a centralized utility file - [ ] Figure out if we can univocally identify devices that mask their MAC address (like apple) - [ ] Add the scanner that triggered the event to the device_events table diff --git a/backend/src/device_finders.rs b/backend/src/device_finders.rs index 3806204..670b106 100644 --- a/backend/src/device_finders.rs +++ b/backend/src/device_finders.rs @@ -1,4 +1,3 @@ pub mod arp; pub mod error; -pub mod mac_resolver; pub mod mdns; diff --git a/backend/src/device_finders/arp.rs b/backend/src/device_finders/arp.rs index 62942a3..e86ab82 100644 --- a/backend/src/device_finders/arp.rs +++ b/backend/src/device_finders/arp.rs @@ -5,6 +5,7 @@ use crate::device_finders::error::{ }; use crate::model::devices::Device; use crate::settings::get_settings; +use crate::utils::network::select_interface; use duration_string::DurationString; use log::{debug, error, info, warn}; use packet_send_receive::{listen_for_packets, send_packet}; @@ -14,21 +15,6 @@ use pnet::{ }; use tokio::time::{Duration, timeout}; -pub(crate) fn select_interface<'a>( - interfaces: &'a [NetworkInterface], - configured: &Option, -) -> Option<&'a NetworkInterface> { - match configured { - Some(name) => interfaces - .iter() - .filter(|el| el.is_up()) - .find(|el| &el.name == name), - None => interfaces - .iter() - .find(|el| el.is_up() && !el.is_loopback() && el.ips.iter().any(|ip| ip.is_ipv4())), - } -} - pub async fn find(interface: Option) -> Result, Box> { debug!( "Looking up devices via ARP, configured interface: {:?}", @@ -147,91 +133,3 @@ pub async fn find(interface: Option) -> Result, Box NetworkInterface { - let mut flags: u32 = 0; - if up { - flags |= IFF_UP; - } - if loopback { - flags |= IFF_LOOPBACK; - } - let ips = if ipv4 { - vec![IpNetwork::V4("192.168.1.1/24".parse().unwrap())] - } else { - vec![] - }; - NetworkInterface { - name: name.to_string(), - description: String::new(), - index: 0, - mac: None, - ips, - flags, - } - } - - #[test] - fn test_select_configured_interface_found() { - let ifaces = vec![ - make_interface("eth0", true, false, true), - make_interface("wlan0", true, false, true), - ]; - let result = select_interface(&ifaces, &Some("wlan0".to_string())); - assert!(result.is_some()); - assert_eq!(result.unwrap().name, "wlan0"); - } - - #[test] - fn test_select_configured_interface_not_found() { - let ifaces = vec![make_interface("eth0", true, false, true)]; - let result = select_interface(&ifaces, &Some("missing0".to_string())); - assert!(result.is_none()); - } - - #[test] - fn test_select_configured_interface_down_not_found() { - let ifaces = vec![make_interface("eth0", false, false, true)]; - let result = select_interface(&ifaces, &Some("eth0".to_string())); - assert!(result.is_none()); - } - - #[test] - fn test_auto_select_skips_loopback() { - let ifaces = vec![ - make_interface("lo", true, true, true), - make_interface("eth0", true, false, true), - ]; - let result = select_interface(&ifaces, &None); - assert!(result.is_some()); - assert_eq!(result.unwrap().name, "eth0"); - } - - #[test] - fn test_auto_select_only_loopback_returns_none() { - let ifaces = vec![make_interface("lo", true, true, true)]; - let result = select_interface(&ifaces, &None); - assert!(result.is_none()); - } - - #[test] - fn test_auto_select_skips_interface_without_ipv4() { - let ifaces = vec![ - make_interface("eth0", true, false, false), - make_interface("wlan0", true, false, true), - ]; - let result = select_interface(&ifaces, &None); - assert!(result.is_some()); - assert_eq!(result.unwrap().name, "wlan0"); - } -} diff --git a/backend/src/device_finders/mdns.rs b/backend/src/device_finders/mdns.rs index 74f2908..b9dbb50 100644 --- a/backend/src/device_finders/mdns.rs +++ b/backend/src/device_finders/mdns.rs @@ -8,7 +8,7 @@ use simple_dns::rdata::RData; use socket2::{Domain, Protocol, Socket, Type}; use tokio::net::UdpSocket; -use crate::device_finders::arp::select_interface; +use crate::utils::network::select_interface; const MDNS_GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251); const MDNS_PORT: u16 = 5353; diff --git a/backend/src/mdns_scanner.rs b/backend/src/mdns_scanner.rs index e46164f..e47e4b0 100644 --- a/backend/src/mdns_scanner.rs +++ b/backend/src/mdns_scanner.rs @@ -5,7 +5,7 @@ use chrono::Local; use log::{debug, error, info, warn}; use crate::db; -use crate::device_finders::{mac_resolver, mdns}; +use crate::device_finders::mdns; use crate::events; use crate::mac_vendor_finder; use crate::mdns_scanner_status; @@ -53,7 +53,7 @@ async fn process_announcement( interface: Option, probe_timeout: Duration, ) { - let mac = match mac_resolver::resolve(src_ip, interface, probe_timeout).await { + let mac = match crate::utils::network::resolve_mac_address(src_ip, interface, probe_timeout).await { Some(mac) => mac.to_string(), None => { debug!("Could not resolve MAC for mDNS device {src_ip} ({hostname}); skipping"); diff --git a/backend/src/utils.rs b/backend/src/utils.rs index 21d9b0a..cb12eac 100644 --- a/backend/src/utils.rs +++ b/backend/src/utils.rs @@ -1 +1,2 @@ pub mod date_serializer; +pub mod network; diff --git a/backend/src/device_finders/mac_resolver.rs b/backend/src/utils/network.rs similarity index 59% rename from backend/src/device_finders/mac_resolver.rs rename to backend/src/utils/network.rs index ff96992..beb3bcc 100644 --- a/backend/src/device_finders/mac_resolver.rs +++ b/backend/src/utils/network.rs @@ -2,23 +2,41 @@ use std::net::Ipv4Addr; use std::time::{Duration, Instant}; use log::debug; -use pnet::datalink::{self, Channel, Config}; +use pnet::datalink::{self, Channel, Config, NetworkInterface}; use pnet::ipnetwork::IpNetwork; use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket}; use pnet::packet::ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket}; use pnet::packet::{MutablePacket, Packet}; use pnet::util::MacAddr; -use crate::device_finders::arp::select_interface; - const PROC_NET_ARP: &str = "/proc/net/arp"; +/// Select the network interface to use for scanning. +/// +/// If `configured` names an interface, returns that interface as long as it is up. +/// Otherwise, auto-selects the first interface that is up, not loopback, and has an +/// IPv4 address. Returns `None` if no suitable interface is found. +pub fn select_interface<'a>( + interfaces: &'a [NetworkInterface], + configured: &Option, +) -> Option<&'a NetworkInterface> { + match configured { + Some(name) => interfaces + .iter() + .filter(|el| el.is_up()) + .find(|el| &el.name == name), + None => interfaces + .iter() + .find(|el| el.is_up() && !el.is_loopback() && el.ips.iter().any(|ip| ip.is_ipv4())), + } +} + /// Resolve the MAC address for an IPv4 address. /// /// Tries the OS neighbor cache (`/proc/net/arp`) first to stay passive; if the IP is not /// present there, falls back to a single targeted ARP probe on the given interface. Returns /// `None` if neither method resolves the address. -pub async fn resolve( +pub async fn resolve_mac_address( ip: Ipv4Addr, interface: Option, probe_timeout: Duration, @@ -39,7 +57,7 @@ pub async fn resolve( /// Parse the contents of `/proc/net/arp` and return the MAC address (as a string) for the /// given IP, or `None` if it is absent or its entry is incomplete. -pub fn parse_proc_net_arp(contents: &str, ip: Ipv4Addr) -> Option { +fn parse_proc_net_arp(contents: &str, ip: Ipv4Addr) -> Option { let ip_str = ip.to_string(); // First line is a header. for line in contents.lines().skip(1) { @@ -123,6 +141,88 @@ fn probe_mac(target_ip: Ipv4Addr, interface: Option, timeout: Duration) #[cfg(test)] mod tests { use super::*; + use pnet::ipnetwork::IpNetwork; + + // Raw Linux IFF flag values used by pnet's is_up() / is_loopback() + const IFF_UP: u32 = 0x1; + const IFF_LOOPBACK: u32 = 0x8; + + fn make_interface(name: &str, up: bool, loopback: bool, ipv4: bool) -> NetworkInterface { + let mut flags: u32 = 0; + if up { + flags |= IFF_UP; + } + if loopback { + flags |= IFF_LOOPBACK; + } + let ips = if ipv4 { + vec![IpNetwork::V4("192.168.1.1/24".parse().unwrap())] + } else { + vec![] + }; + NetworkInterface { + name: name.to_string(), + description: String::new(), + index: 0, + mac: None, + ips, + flags, + } + } + + #[test] + fn test_select_configured_interface_found() { + let ifaces = vec![ + make_interface("eth0", true, false, true), + make_interface("wlan0", true, false, true), + ]; + let result = select_interface(&ifaces, &Some("wlan0".to_string())); + assert!(result.is_some()); + assert_eq!(result.unwrap().name, "wlan0"); + } + + #[test] + fn test_select_configured_interface_not_found() { + let ifaces = vec![make_interface("eth0", true, false, true)]; + let result = select_interface(&ifaces, &Some("missing0".to_string())); + assert!(result.is_none()); + } + + #[test] + fn test_select_configured_interface_down_not_found() { + let ifaces = vec![make_interface("eth0", false, false, true)]; + let result = select_interface(&ifaces, &Some("eth0".to_string())); + assert!(result.is_none()); + } + + #[test] + fn test_auto_select_skips_loopback() { + let ifaces = vec![ + make_interface("lo", true, true, true), + make_interface("eth0", true, false, true), + ]; + let result = select_interface(&ifaces, &None); + assert!(result.is_some()); + assert_eq!(result.unwrap().name, "eth0"); + } + + #[test] + fn test_auto_select_only_loopback_returns_none() { + let ifaces = vec![make_interface("lo", true, true, true)]; + let result = select_interface(&ifaces, &None); + assert!(result.is_none()); + } + + #[test] + fn test_auto_select_skips_interface_without_ipv4() { + let ifaces = vec![ + make_interface("eth0", true, false, false), + make_interface("wlan0", true, false, true), + ]; + let result = select_interface(&ifaces, &None); + assert!(result.is_some()); + assert_eq!(result.unwrap().name, "wlan0"); + } const SAMPLE: &str = "IP address HW type Flags HW address Mask Device\n\ 192.168.0.10 0x1 0x2 aa:bb:cc:dd:ee:ff * eth0\n\