mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Centralize networking helpers in utils::network module
Move select_interface and MAC-resolution logic (resolve_mac_address, parse_proc_net_arp, probe_mac) out of the device_finders modules into a single utils::network utility so future scanners can reuse them. Rename resolve to resolve_mac_address and make parse_proc_net_arp private. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ca83009944
commit
c6fdf9e181
@@ -11,7 +11,7 @@
|
|||||||
- [x] Add date filter to device event list method (from)
|
- [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 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
|
- [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)
|
- [ ] 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
|
- [ ] Add the scanner that triggered the event to the device_events table
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod arp;
|
pub mod arp;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod mac_resolver;
|
|
||||||
pub mod mdns;
|
pub mod mdns;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::device_finders::error::{
|
|||||||
};
|
};
|
||||||
use crate::model::devices::Device;
|
use crate::model::devices::Device;
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
|
use crate::utils::network::select_interface;
|
||||||
use duration_string::DurationString;
|
use duration_string::DurationString;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use packet_send_receive::{listen_for_packets, send_packet};
|
use packet_send_receive::{listen_for_packets, send_packet};
|
||||||
@@ -14,21 +15,6 @@ use pnet::{
|
|||||||
};
|
};
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
pub(crate) fn select_interface<'a>(
|
|
||||||
interfaces: &'a [NetworkInterface],
|
|
||||||
configured: &Option<String>,
|
|
||||||
) -> 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<String>) -> Result<Vec<Device>, Box<dyn std::error::Error>> {
|
pub async fn find(interface: Option<String>) -> Result<Vec<Device>, Box<dyn std::error::Error>> {
|
||||||
debug!(
|
debug!(
|
||||||
"Looking up devices via ARP, configured interface: {:?}",
|
"Looking up devices via ARP, configured interface: {:?}",
|
||||||
@@ -147,91 +133,3 @@ pub async fn find(interface: Option<String>) -> Result<Vec<Device>, Box<dyn std:
|
|||||||
|
|
||||||
Ok(result_receive.unwrap_or(Vec::new()))
|
Ok(result_receive.unwrap_or(Vec::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pnet::datalink::NetworkInterface;
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use simple_dns::rdata::RData;
|
|||||||
use socket2::{Domain, Protocol, Socket, Type};
|
use socket2::{Domain, Protocol, Socket, Type};
|
||||||
use tokio::net::UdpSocket;
|
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_GROUP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
|
||||||
const MDNS_PORT: u16 = 5353;
|
const MDNS_PORT: u16 = 5353;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use chrono::Local;
|
|||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::device_finders::{mac_resolver, mdns};
|
use crate::device_finders::mdns;
|
||||||
use crate::events;
|
use crate::events;
|
||||||
use crate::mac_vendor_finder;
|
use crate::mac_vendor_finder;
|
||||||
use crate::mdns_scanner_status;
|
use crate::mdns_scanner_status;
|
||||||
@@ -53,7 +53,7 @@ async fn process_announcement(
|
|||||||
interface: Option<String>,
|
interface: Option<String>,
|
||||||
probe_timeout: Duration,
|
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(),
|
Some(mac) => mac.to_string(),
|
||||||
None => {
|
None => {
|
||||||
debug!("Could not resolve MAC for mDNS device {src_ip} ({hostname}); skipping");
|
debug!("Could not resolve MAC for mDNS device {src_ip} ({hostname}); skipping");
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
pub mod date_serializer;
|
pub mod date_serializer;
|
||||||
|
pub mod network;
|
||||||
|
|||||||
@@ -2,23 +2,41 @@ use std::net::Ipv4Addr;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use pnet::datalink::{self, Channel, Config};
|
use pnet::datalink::{self, Channel, Config, NetworkInterface};
|
||||||
use pnet::ipnetwork::IpNetwork;
|
use pnet::ipnetwork::IpNetwork;
|
||||||
use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket};
|
use pnet::packet::arp::{ArpHardwareTypes, ArpOperations, ArpPacket, MutableArpPacket};
|
||||||
use pnet::packet::ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket};
|
use pnet::packet::ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket};
|
||||||
use pnet::packet::{MutablePacket, Packet};
|
use pnet::packet::{MutablePacket, Packet};
|
||||||
use pnet::util::MacAddr;
|
use pnet::util::MacAddr;
|
||||||
|
|
||||||
use crate::device_finders::arp::select_interface;
|
|
||||||
|
|
||||||
const PROC_NET_ARP: &str = "/proc/net/arp";
|
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<String>,
|
||||||
|
) -> 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.
|
/// 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
|
/// 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
|
/// present there, falls back to a single targeted ARP probe on the given interface. Returns
|
||||||
/// `None` if neither method resolves the address.
|
/// `None` if neither method resolves the address.
|
||||||
pub async fn resolve(
|
pub async fn resolve_mac_address(
|
||||||
ip: Ipv4Addr,
|
ip: Ipv4Addr,
|
||||||
interface: Option<String>,
|
interface: Option<String>,
|
||||||
probe_timeout: Duration,
|
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
|
/// 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.
|
/// given IP, or `None` if it is absent or its entry is incomplete.
|
||||||
pub fn parse_proc_net_arp(contents: &str, ip: Ipv4Addr) -> Option<String> {
|
fn parse_proc_net_arp(contents: &str, ip: Ipv4Addr) -> Option<String> {
|
||||||
let ip_str = ip.to_string();
|
let ip_str = ip.to_string();
|
||||||
// First line is a header.
|
// First line is a header.
|
||||||
for line in contents.lines().skip(1) {
|
for line in contents.lines().skip(1) {
|
||||||
@@ -123,6 +141,88 @@ fn probe_mac(target_ip: Ipv4Addr, interface: Option<String>, timeout: Duration)
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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\
|
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\
|
192.168.0.10 0x1 0x2 aa:bb:cc:dd:ee:ff * eth0\n\
|
||||||
Reference in New Issue
Block a user