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
@@ -1,4 +1,3 @@
|
||||
pub mod arp;
|
||||
pub mod error;
|
||||
pub mod mac_resolver;
|
||||
pub mod mdns;
|
||||
|
||||
@@ -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<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>> {
|
||||
debug!(
|
||||
"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()))
|
||||
}
|
||||
|
||||
#[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 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;
|
||||
|
||||
@@ -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<String>,
|
||||
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");
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod date_serializer;
|
||||
pub mod network;
|
||||
|
||||
@@ -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<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.
|
||||
///
|
||||
/// 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<String>,
|
||||
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<String> {
|
||||
fn parse_proc_net_arp(contents: &str, ip: Ipv4Addr) -> Option<String> {
|
||||
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<String>, 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\
|
||||
Reference in New Issue
Block a user