Add per-scanner enable/disable configuration

Each scanner (ARP, mDNS, SSDP/UPnP, DHCP) can now be turned off via an
`enabled` flag in its config section, defaulting to true so existing
deployments are unchanged. A disabled scanner's entry function returns
early and never starts.

Documented in the README options table (also fixing the stale `timings.*`
key names to the actual `arp_scanner.*` keys), and added to the TOML
samples and the NixOS module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-01 19:00:56 -04:00
co-authored by Claude Opus 4.8
parent 4e975598f9
commit 08ee4fc0ea
9 changed files with 160 additions and 30 deletions
+5
View File
@@ -9,6 +9,11 @@ use log::{debug, info};
use tokio::time::{Duration, sleep};
pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
if !get_settings().arp_scanner.enabled {
info!("ARP scanner disabled in configuration; not starting");
return Ok(());
}
loop {
status::set_running();
+6
View File
@@ -9,12 +9,18 @@ use crate::db;
use crate::events;
use crate::model::device_events::DeviceEventScanner;
use crate::model::devices::Device;
use crate::settings::get_settings;
/// Passively snoop DHCP DISCOVER/REQUEST broadcasts and feed discovered devices into the
/// same pipeline used by the ARP, mDNS and SSDP scanners (devices table + events +
/// notifications). Because a device must request an address before doing almost anything
/// else, this catches new devices very early — often before they have an IP.
pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
if !get_settings().dhcp_scanner.enabled {
info!("DHCP scanner disabled in configuration; not starting");
return Ok(());
}
let socket = finder::open_socket()?;
status::set_listening();
info!("DHCP scanner listening for client requests");
+5
View File
@@ -17,6 +17,11 @@ use crate::settings::get_settings;
/// Passively listen for mDNS/Bonjour announcements and feed discovered devices into the same
/// pipeline used by the ARP scanner (devices table + events + notifications).
pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
if !get_settings().mdns_scanner.enabled {
info!("mDNS scanner disabled in configuration; not starting");
return Ok(());
}
let interface = get_settings().networking.interface.clone();
let socket = finder::open_socket(interface.clone())?;
status::set_listening();
+5
View File
@@ -17,6 +17,11 @@ use crate::settings::get_settings;
/// Passively listen for SSDP/UPnP NOTIFY announcements and feed discovered devices into the same
/// pipeline used by the ARP and mDNS scanners (devices table + events + notifications).
pub async fn listen() -> Result<(), Box<dyn std::error::Error>> {
if !get_settings().ssdp_scanner.enabled {
info!("SSDP scanner disabled in configuration; not starting");
return Ok(());
}
let interface = get_settings().networking.interface.clone();
let socket = finder::open_socket(interface.clone())?;
status::set_listening();
+98
View File
@@ -6,6 +6,10 @@ use serde::Deserialize;
// -----------------------------------------------------------
// Configuration structure
fn default_true() -> bool {
true
}
#[derive(Debug, Deserialize, Clone)]
pub struct Database {
pub path: String,
@@ -23,6 +27,8 @@ pub struct Log {
#[derive(Debug, Deserialize, Clone)]
pub struct ArpScanner {
#[serde(default = "default_true")]
pub enabled: bool,
pub wait_between_scans: DurationString,
pub sender_timeout: DurationString,
pub scan_duration: DurationString,
@@ -30,12 +36,15 @@ pub struct ArpScanner {
#[derive(Debug, Deserialize, Clone)]
pub struct MdnsScanner {
#[serde(default = "default_true")]
pub enabled: bool,
pub probe_timeout: DurationString,
}
impl Default for MdnsScanner {
fn default() -> Self {
MdnsScanner {
enabled: true,
probe_timeout: DurationString::try_from("2s".to_string()).unwrap(),
}
}
@@ -43,17 +52,32 @@ impl Default for MdnsScanner {
#[derive(Debug, Deserialize, Clone)]
pub struct SsdpScanner {
#[serde(default = "default_true")]
pub enabled: bool,
pub probe_timeout: DurationString,
}
impl Default for SsdpScanner {
fn default() -> Self {
SsdpScanner {
enabled: true,
probe_timeout: DurationString::try_from("2s".to_string()).unwrap(),
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct DhcpScanner {
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for DhcpScanner {
fn default() -> Self {
DhcpScanner { enabled: true }
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct Pushover {
pub token: String,
@@ -101,6 +125,8 @@ pub struct Settings {
pub mdns_scanner: MdnsScanner,
#[serde(default)]
pub ssdp_scanner: SsdpScanner,
#[serde(default)]
pub dhcp_scanner: DhcpScanner,
}
// End configuration structure
// -----------------------------------------------------------
@@ -138,3 +164,75 @@ pub fn get_settings() -> &'static Settings {
pub fn init(config_path: String) {
let _ = SETTINGS.set(Settings::new(config_path).unwrap());
}
#[cfg(test)]
mod tests {
use super::*;
use config::{Config, FileFormat};
fn parse(toml: &str) -> Settings {
Config::builder()
.add_source(config::File::from_str(toml, FileFormat::Toml))
.build()
.unwrap()
.try_deserialize()
.unwrap()
}
const BASE_CONFIG: &str = r#"
[database]
path = "./oott.db"
[networking]
[log]
level = "info"
[arp_scanner]
wait_between_scans = "1m"
sender_timeout = "1m"
scan_duration = "2m"
[notifications]
method = "none"
notify_when_not_seen_for = "1w"
[notifications.pushover]
token = ""
user_key = ""
[web_server]
ip_address = "0.0.0.0"
port = 3000
api_key = "test"
"#;
#[test]
fn scanners_enabled_by_default_when_flag_omitted() {
let settings = parse(BASE_CONFIG);
assert!(settings.arp_scanner.enabled);
assert!(settings.mdns_scanner.enabled);
assert!(settings.ssdp_scanner.enabled);
assert!(settings.dhcp_scanner.enabled);
}
#[test]
fn scanners_can_be_disabled() {
let toml = format!(
"{BASE_CONFIG}
[mdns_scanner]
enabled = false
probe_timeout = \"2s\"
[ssdp_scanner]
enabled = false
probe_timeout = \"2s\"
[dhcp_scanner]
enabled = false
"
);
// Disable the ARP scanner via its existing section too.
let toml = toml.replace(
"wait_between_scans = \"1m\"",
"enabled = false\n wait_between_scans = \"1m\"",
);
let settings = parse(&toml);
assert!(!settings.arp_scanner.enabled);
assert!(!settings.mdns_scanner.enabled);
assert!(!settings.ssdp_scanner.enabled);
assert!(!settings.dhcp_scanner.enabled);
}
}