mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Auto-detect network interface when not configured
If no interface is set in the config, the ARP scanner now picks the first non-loopback, up, IPv4-enabled interface automatically. The setting is optional in the TOML config, sample config, and Nix module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
49adadf059
commit
08f2dc8340
@@ -4,7 +4,7 @@
|
||||
|
||||
## Backend
|
||||
|
||||
- [ ] Use first available network interface when not configured
|
||||
- [x] Use first available network interface when not configured
|
||||
- [x] Add Swagger and API docs (OpenAPI)
|
||||
- [x] Record an activity log in the database
|
||||
- [x] One event for each device appearance
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Find online devices via ARP
|
||||
let devices =
|
||||
device_finders::arp::find(get_settings().networking.interface.to_string()).await?;
|
||||
device_finders::arp::find(get_settings().networking.interface.clone()).await?;
|
||||
|
||||
info!("Done with ARP probes");
|
||||
info!("Found {} online devices", devices.len());
|
||||
|
||||
@@ -14,18 +14,36 @@ use pnet::{
|
||||
};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::Error>> {
|
||||
debug!("Looking up devices via ARP using interface {}", interface);
|
||||
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: {:?}", interface);
|
||||
|
||||
// Get the network device to use
|
||||
let network_interface: NetworkInterface = match datalink::interfaces()
|
||||
.iter()
|
||||
.filter(|el| el.is_up())
|
||||
.find(|el| el.name == interface)
|
||||
{
|
||||
Some(value) => value.clone(),
|
||||
let all_interfaces = datalink::interfaces();
|
||||
let network_interface: NetworkInterface = match select_interface(&all_interfaces, &interface) {
|
||||
Some(value) => {
|
||||
info!("Using network interface: {}", value.name);
|
||||
value.clone()
|
||||
}
|
||||
None => {
|
||||
error!("Interface ({interface}) not found or not active.");
|
||||
match &interface {
|
||||
Some(name) => error!("Interface ({name}) not found or not active."),
|
||||
None => error!("No suitable non-loopback interface with an IPv4 address found."),
|
||||
}
|
||||
return Err(InvalidDeviceError.into());
|
||||
}
|
||||
};
|
||||
@@ -42,7 +60,7 @@ pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::
|
||||
{
|
||||
Some(value) => value,
|
||||
None => {
|
||||
error!("No IP address found for selected interface ({interface}).");
|
||||
error!("No IP address found for selected interface ({}).", network_interface.name);
|
||||
return Err(NoIPAddressError.into());
|
||||
}
|
||||
};
|
||||
@@ -51,7 +69,7 @@ pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::
|
||||
let mac = match network_interface.mac {
|
||||
Some(mac) => mac,
|
||||
None => {
|
||||
error!("Could not get MAC address for selected interface ({interface}).");
|
||||
error!("Could not get MAC address for selected interface ({}).", network_interface.name);
|
||||
return Err(NoMACAddressError.into());
|
||||
}
|
||||
};
|
||||
@@ -120,3 +138,91 @@ pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub struct Database {
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Networking {
|
||||
pub interface: String,
|
||||
pub interface: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
path = "./oott.db" # Database path. Make sure it's writeable for the user running this process
|
||||
|
||||
[networking]
|
||||
interface = "eno1" # Network interface to use for scans
|
||||
# interface = "eno1" # Optional: network interface to use for scans. If not set, the first non-loopback connected interface is used.
|
||||
|
||||
[log]
|
||||
level = "info" # off, error, warn, info, debug, trace
|
||||
|
||||
@@ -22,9 +22,9 @@ in {
|
||||
default = "/var/lib/oott.db";
|
||||
};
|
||||
networking.interface = mkOption {
|
||||
type = types.str;
|
||||
description = "Network interface to use for scans";
|
||||
default = "eno1";
|
||||
type = types.nullOr types.str;
|
||||
description = "Network interface to use for scans. If not set, the first non-loopback connected interface is used automatically.";
|
||||
default = null;
|
||||
};
|
||||
log.level = mkOption {
|
||||
type = types.str;
|
||||
|
||||
Reference in New Issue
Block a user