mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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:
co-authored by
Claude Opus 4.8
parent
4e975598f9
commit
08ee4fc0ea
@@ -110,9 +110,9 @@ Finally, in your `configuration.nix` (or in an import file) enable and configure
|
||||
database.path = "/var/lib/oott.db";
|
||||
# networking.interface = "eth0"; # Optional: auto-detected if not set
|
||||
log.level = "info";
|
||||
timings.wait_between_scans = "15m";
|
||||
timings.arp_sender_timeout = "20m";
|
||||
timings.arp_scan_duration = "30m";
|
||||
arp_scanner.wait_between_scans = "15m";
|
||||
arp_scanner.sender_timeout = "20m";
|
||||
arp_scanner.scan_duration = "30m";
|
||||
notifications.method = "pushover";
|
||||
notifications.notify_when_not_seen_for = "1w";
|
||||
notifications.pushover.token = "YOUR API TOKEN GOES HERE";
|
||||
@@ -135,9 +135,15 @@ If you are using Docker I recommend writing the config using TOML, [here](https:
|
||||
|`database.path`|`/var/lib/oott.db`|Location of the system database|
|
||||
|`networking.interface`|`eno1`|Network interface to use for scans. Optional — if not set, the first non-loopback connected interface is used automatically.|
|
||||
|`log.level`|`info`|Log level to use (trace, debug, info, warn, error)|
|
||||
|`timings.wait_between_scans`|`15m`|Time to wait between each network scan (you can express it in seconds, minutes, hours, etc. as a suffix - for example: 30s, 10m, 1h)|
|
||||
|`timings.arp_sender_timeout`|`1m`|If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)|
|
||||
|`timings.arp_scan_duration`|`10m`|How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)|
|
||||
|`arp_scanner.enabled`|`true`|Whether to run the ARP scanner. Defaults to enabled; set to `false` to turn it off.|
|
||||
|`arp_scanner.wait_between_scans`|`15m`|Time to wait between each network scan (you can express it in seconds, minutes, hours, etc. as a suffix - for example: 30s, 10m, 1h)|
|
||||
|`arp_scanner.sender_timeout`|`1m`|If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)|
|
||||
|`arp_scanner.scan_duration`|`10m`|How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)|
|
||||
|`mdns_scanner.enabled`|`true`|Whether to run the mDNS/Bonjour scanner. Defaults to enabled; set to `false` to turn it off.|
|
||||
|`mdns_scanner.probe_timeout`|`2s`|When an mDNS-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address|
|
||||
|`ssdp_scanner.enabled`|`true`|Whether to run the SSDP/UPnP scanner. Defaults to enabled; set to `false` to turn it off.|
|
||||
|`ssdp_scanner.probe_timeout`|`2s`|When an SSDP/UPnP-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address|
|
||||
|`dhcp_scanner.enabled`|`true`|Whether to run the DHCP scanner. Defaults to enabled; set to `false` to turn it off.|
|
||||
|`notifications.method`|`pushover`|For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)|
|
||||
|`notifications.notify_when_not_seen_for`|`1w`|Send a notification if a device comes back online after not being seen for this timeframe (you can use hours, weeks, etc.)|
|
||||
|`notifications.pushover.token`||Your pushover token goes here, just copy&paste from their website after creating the app|
|
||||
@@ -168,7 +174,7 @@ OOTT stores a timestamped event in the database for every device detected on eve
|
||||
|
||||
## Estimates by network size
|
||||
|
||||
The figures below assume a 1-minute scan duration (`arp_scan_duration = 1m`) with a 1-minute wait between scans (`wait_between_scans = 1m`), giving 720 scans per day, and a 365-day retention window. Assume all devices are continuously online (worst case).
|
||||
The figures below assume a 1-minute scan duration (`arp_scanner.scan_duration = 1m`) with a 1-minute wait between scans (`arp_scanner.wait_between_scans = 1m`), giving 720 scans per day, and a 365-day retention window. Assume all devices are continuously online (worst case).
|
||||
|
||||
| Network size | Active devices | Storage / year |
|
||||
|---|---|---|
|
||||
@@ -189,7 +195,7 @@ where `145 bytes` covers the database row and its index entry, and `scans_per_da
|
||||
|
||||
Storage is directly proportional to scan frequency and retention window. The two main levers are:
|
||||
|
||||
**Scan interval** — `timings.wait_between_scans` is the most effective knob. Increasing it reduces scans per day linearly:
|
||||
**Scan interval** — `arp_scanner.wait_between_scans` is the most effective knob. Increasing it reduces scans per day linearly:
|
||||
|
||||
| `wait_between_scans` | Scans/day (with 1m scan) | Storage vs. 1m+1m |
|
||||
|---|---|---|
|
||||
|
||||
@@ -4,40 +4,19 @@
|
||||
|
||||
## Backend
|
||||
|
||||
- [ ] Add configuration options to enable/disable each scanner
|
||||
- [x] Call out in README.md that the system needs to be run as root (to bind the network card and listen on lower ports)
|
||||
- [x] Add the scanner that triggered the event to the device_events table
|
||||
- [x] Improve notifications layout/text
|
||||
- [x] Add configuration options to enable/disable each scanner
|
||||
- [ ] Implement the pushover API call directly to support HTML content and review notification text to use it
|
||||
- [x] Add SSDP/uPNP scanner
|
||||
- [x] Make sure the new config options are in the TOML files and Nix module
|
||||
- [x] Document in README.md the port needs/bindings
|
||||
- [x] Add DHCP scanner
|
||||
|
||||
## Frontend
|
||||
|
||||
- [x] Add the SSDP/UPnP status to the status screen and home screen
|
||||
- [x] Add the DHCP scanner status to the status screen and home screen
|
||||
- [x] In the notifications list, modify the behavior so that you can fully see the text of the notification before navigating to the device details
|
||||
- [ ] Can we add front-end tests?
|
||||
- [x] In the devices list, add a color dot to reflect the last seen status (less than 10 minutes green, otherwise grey)
|
||||
- [x] Change the URI for the homepage from /notifications to /home
|
||||
- [x] In the devices list, the order by name is not consistent (iPad... before Lutron when ordering by name descending, maybe it should be case insensitive)
|
||||
- [x] In the devices list, when the width is "medium", the list looks bad. Maybe make status pills not wrap to new lines
|
||||
- [x] The ARP scanner status change the yellow to gray (yellow conveys problems, the scanner is just waiting)
|
||||
|
||||
## Improve engine
|
||||
|
||||
Several complementary approaches work well alongside ARP:
|
||||
|
||||
Passive (low noise, no probing):
|
||||
- SSDP/UPnP — similar but for smart devices/IoT; multicast on 239.255.255.250:1900
|
||||
- DHCP snooping — monitor DHCP DISCOVER/REQUEST packets; new devices must ask for an IP before doing anything else, so this catches them very early
|
||||
- Passive packet capture — observe any broadcast/multicast traffic; a device that never responds to ARP still generates traffic
|
||||
|
||||
Active (you probe the network):
|
||||
- ICMP ping sweep — ping every host in the subnet range; more universal than ARP but generates traffic
|
||||
- TCP/UDP SYN scan — probe common ports (22, 80, 443, etc.); finds devices that silently drop ICMP
|
||||
- NDP (Neighbor Discovery Protocol) — IPv6 equivalent of ARP; important if the network uses IPv6
|
||||
|
||||
Via infrastructure:
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,22 @@ path = "./oott.db" # Database path. Make sure it's writeable for the user runnin
|
||||
level = "info" # off, error, warn, info, debug, trace
|
||||
|
||||
[arp_scanner]
|
||||
enabled=true # Set to false to disable the ARP scanner
|
||||
wait_between_scans="30m" # Wait time between scans. This does not include the scan time
|
||||
sender_timeout="1m" # If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)
|
||||
scan_duration="10m" # How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)
|
||||
|
||||
[mdns_scanner]
|
||||
enabled=true # Set to false to disable the mDNS/Bonjour scanner
|
||||
probe_timeout="2s" # When an mDNS-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address
|
||||
|
||||
[ssdp_scanner]
|
||||
enabled=true # Set to false to disable the SSDP/UPnP scanner
|
||||
probe_timeout="2s" # When an SSDP/UPnP-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address
|
||||
|
||||
[dhcp_scanner]
|
||||
enabled=true # Set to false to disable the DHCP scanner
|
||||
|
||||
[notifications]
|
||||
method="pushover" # For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)
|
||||
notify_when_not_seen_for="1w" # Send a notification if a device comes back online after not being seen for this timeframe
|
||||
|
||||
@@ -31,6 +31,11 @@ in {
|
||||
description = "Log level for the oott service";
|
||||
default = "info";
|
||||
};
|
||||
arp_scanner.enabled = mkOption {
|
||||
type = types.bool;
|
||||
description = "Whether to run the ARP scanner.";
|
||||
default = true;
|
||||
};
|
||||
arp_scanner.wait_between_scans = mkOption {
|
||||
type = types.str;
|
||||
description = "Wait time between scans. This does not include the scan time.";
|
||||
@@ -46,16 +51,31 @@ in {
|
||||
description = "How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network).";
|
||||
default = "10m";
|
||||
};
|
||||
mdns_scanner.enabled = mkOption {
|
||||
type = types.bool;
|
||||
description = "Whether to run the mDNS/Bonjour scanner.";
|
||||
default = true;
|
||||
};
|
||||
mdns_scanner.probe_timeout = mkOption {
|
||||
type = types.str;
|
||||
description = "When an mDNS-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address.";
|
||||
default = "2s";
|
||||
};
|
||||
ssdp_scanner.enabled = mkOption {
|
||||
type = types.bool;
|
||||
description = "Whether to run the SSDP/UPnP scanner.";
|
||||
default = true;
|
||||
};
|
||||
ssdp_scanner.probe_timeout = mkOption {
|
||||
type = types.str;
|
||||
description = "When an SSDP/UPnP-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address.";
|
||||
default = "2s";
|
||||
};
|
||||
dhcp_scanner.enabled = mkOption {
|
||||
type = types.bool;
|
||||
description = "Whether to run the DHCP scanner.";
|
||||
default = true;
|
||||
};
|
||||
notifications.method = mkOption {
|
||||
type = types.str;
|
||||
description = "For now just pushover, you can set this to none to avoid sending notifications (it will just log).";
|
||||
|
||||
Reference in New Issue
Block a user