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
|
## 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] Add Swagger and API docs (OpenAPI)
|
||||||
- [x] Record an activity log in the database
|
- [x] Record an activity log in the database
|
||||||
- [x] One event for each device appearance
|
- [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
|
// Find online devices via ARP
|
||||||
let devices =
|
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!("Done with ARP probes");
|
||||||
info!("Found {} online devices", devices.len());
|
info!("Found {} online devices", devices.len());
|
||||||
|
|||||||
@@ -14,18 +14,36 @@ use pnet::{
|
|||||||
};
|
};
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::Error>> {
|
fn select_interface<'a>(
|
||||||
debug!("Looking up devices via ARP using interface {}", interface);
|
interfaces: &'a [NetworkInterface],
|
||||||
|
configured: &Option<String>,
|
||||||
// Get the network device to use
|
) -> Option<&'a NetworkInterface> {
|
||||||
let network_interface: NetworkInterface = match datalink::interfaces()
|
match configured {
|
||||||
|
Some(name) => interfaces
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|el| el.is_up())
|
.filter(|el| el.is_up())
|
||||||
.find(|el| el.name == interface)
|
.find(|el| &el.name == name),
|
||||||
{
|
None => interfaces
|
||||||
Some(value) => value.clone(),
|
.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 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 => {
|
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());
|
return Err(InvalidDeviceError.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -42,7 +60,7 @@ pub async fn find(interface: String) -> Result<Vec<Device>, Box<dyn std::error::
|
|||||||
{
|
{
|
||||||
Some(value) => value,
|
Some(value) => value,
|
||||||
None => {
|
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());
|
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 {
|
let mac = match network_interface.mac {
|
||||||
Some(mac) => mac,
|
Some(mac) => mac,
|
||||||
None => {
|
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());
|
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()))
|
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)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
pub struct Networking {
|
pub struct Networking {
|
||||||
pub interface: String,
|
pub interface: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
path = "./oott.db" # Database path. Make sure it's writeable for the user running this process
|
path = "./oott.db" # Database path. Make sure it's writeable for the user running this process
|
||||||
|
|
||||||
[networking]
|
[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]
|
[log]
|
||||||
level = "info" # off, error, warn, info, debug, trace
|
level = "info" # off, error, warn, info, debug, trace
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ in {
|
|||||||
default = "/var/lib/oott.db";
|
default = "/var/lib/oott.db";
|
||||||
};
|
};
|
||||||
networking.interface = mkOption {
|
networking.interface = mkOption {
|
||||||
type = types.str;
|
type = types.nullOr types.str;
|
||||||
description = "Network interface to use for scans";
|
description = "Network interface to use for scans. If not set, the first non-loopback connected interface is used automatically.";
|
||||||
default = "eno1";
|
default = null;
|
||||||
};
|
};
|
||||||
log.level = mkOption {
|
log.level = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
|
|||||||
Reference in New Issue
Block a user