Feat: Vertrauensprüfung für Netzwerke vor nmap-Scan (Schritt 3)
Ein ARP-/Ping-Scan ganzer Subnetze ist auf Netzwerken, die man nicht selbst administriert (Firmen-/Gast-WLAN etc.), potenziell heikel (IDS-Alarm, Verstoß gegen Nutzungsordnungen). nmap wird deshalb künftig nur noch in explizit vertrauten Netzwerken ausgeführt. Das aktuelle Netzwerk wird über die MAC-Adresse seines Default-Gateways identifiziert (stabil über IP-/Subnetzwechsel hinweg, unabhängig von SSID). Neues Modul src/trust.rs prüft vor Schritt 3: - statisch konfigurierte Gateway-MACs (AppConfig::trusted_networks, via --trusted-networks/MAC2IP_TRUSTED_NETWORKS oder Config-Datei), - zuvor per Rückfrage bestätigte Netzwerke (persistiert in einer neuen trusted_networks-Tabelle im bestehenden globalen Cache), - automatische Bestätigung via --auto-trust-networks (merkt sich das Netzwerk danach ebenfalls im Cache). Ist nichts davon der Fall, wird im interaktiven Modus auf stderr nachgefragt; im --json-Modus ist keine Rückfrage möglich, daher wird der Scan sicherheitshalber abgelehnt (neuer Fehler Mac2IpError::UntrustedNetwork). src/network.rs: neue Exec-/Parse-Funktionen zur Gateway-MAC-Ermittlung (run_ip_route_show_default, parse_default_gateway_ip, parse_ip_neigh_mac_for_ip, detect_default_gateway_mac). Ergänzt Unit-/Integrationstests (tests/gateway_trust.rs, tests/config_overrides.rs). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnWfGGqHJGh2AhZ6uhotaD
This commit is contained in:
+61
-1
@@ -38,9 +38,63 @@ impl Cache {
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
self.conn
|
||||
.execute(
|
||||
"CREATE TABLE IF NOT EXISTS trusted_networks (\
|
||||
gateway_mac TEXT PRIMARY KEY, \
|
||||
trusted_at INTEGER NOT NULL\
|
||||
)",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Best-effort: gibt bei jedem Fehler `false` zurück (nur eine Warnung wird geloggt) -
|
||||
/// ein Cache-Lesefehler darf ein zuvor bereits bestätigtes Netzwerk nicht plötzlich
|
||||
/// als unbekannt erscheinen lassen, aber sicherheitshalber auch nicht automatisch
|
||||
/// als vertrauenswürdig gelten lassen; der Aufrufer fragt in diesem Fall erneut nach.
|
||||
pub async fn is_trusted_network(&self, gateway_mac: &MacAddress) -> bool {
|
||||
let key = gateway_mac.to_lower_colon();
|
||||
match self
|
||||
.conn
|
||||
.query(
|
||||
"SELECT 1 FROM trusted_networks WHERE gateway_mac = ?1",
|
||||
(key,),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(mut rows) => matches!(rows.next().await, Ok(Some(_))),
|
||||
Err(e) => {
|
||||
crate::log::warn(
|
||||
"cache",
|
||||
&format!("Cache-Lesefehler (trusted_networks): {e}"),
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort Upsert: merkt sich ein Netzwerk (identifiziert über die Gateway-MAC)
|
||||
/// dauerhaft als vertrauenswürdig für zukünftige nmap-Scans.
|
||||
pub async fn trust_network(&self, gateway_mac: &MacAddress, now: i64) {
|
||||
let key = gateway_mac.to_lower_colon();
|
||||
if let Err(e) = self
|
||||
.conn
|
||||
.execute(
|
||||
"INSERT INTO trusted_networks (gateway_mac, trusted_at) VALUES (?1, ?2) \
|
||||
ON CONFLICT(gateway_mac) DO UPDATE SET trusted_at = excluded.trusted_at",
|
||||
(key, now),
|
||||
)
|
||||
.await
|
||||
{
|
||||
crate::log::warn(
|
||||
"cache",
|
||||
&format!("Cache-Schreibfehler (trusted_networks, ignoriert): {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort: gibt bei jedem Fehler `None` zurück (nur eine Warnung wird geloggt).
|
||||
pub async fn get(&self, mac: &MacAddress) -> Option<CacheEntry> {
|
||||
let key = mac.to_lower_colon();
|
||||
@@ -189,7 +243,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_ensure_cache_dir_creates_new_directory() {
|
||||
let temp_dir = std::env::temp_dir().join(format!("mac2ip_test_{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
|
||||
let temp_dir = std::env::temp_dir().join(format!(
|
||||
"mac2ip_test_{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
assert!(!temp_dir.exists());
|
||||
assert!(ensure_cache_dir(&temp_dir).is_ok());
|
||||
assert!(temp_dir.exists());
|
||||
|
||||
+11
@@ -45,6 +45,17 @@ pub struct Cli {
|
||||
/// Kommagetrennte Liste von CIDR-Subnetzen für den nmap-Scan (überschreibt Auto-Erkennung)
|
||||
#[arg(long, env = "MAC2IP_NETWORKS", value_delimiter = ',')]
|
||||
pub networks: Option<Vec<String>>,
|
||||
|
||||
/// Kommagetrennte Liste von Gateway-MAC-Adressen, deren Netzwerke ohne Rückfrage für
|
||||
/// nmap-Scans (Schritt 3) vertraut werden (überschreibt die Konfigurationsdatei vollständig)
|
||||
#[arg(long, env = "MAC2IP_TRUSTED_NETWORKS", value_delimiter = ',')]
|
||||
pub trusted_networks: Option<Vec<MacAddress>>,
|
||||
|
||||
/// Beantwortet die "nmap-Scan in diesem Netzwerk erlauben?"-Rückfrage vor Schritt 3
|
||||
/// automatisch mit Ja (und merkt sich das Netzwerk dauerhaft im Cache), statt
|
||||
/// interaktiv nachzufragen bzw. im --json-Modus den Scan abzulehnen
|
||||
#[arg(long)]
|
||||
pub auto_trust_networks: bool,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::mac::MacAddress;
|
||||
|
||||
pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 1800; // 30 Min: Ping-Check ist die primäre
|
||||
// Absicherung gegen veraltete Einträge, die TTL ist nur eine zusätzliche Absicherung
|
||||
// gegen den Fall, dass eine alte IP inzwischen an ein anderes, ebenfalls
|
||||
@@ -40,6 +42,11 @@ pub struct AppConfig {
|
||||
pub nmap_timeout_seconds: u64,
|
||||
#[serde(default)]
|
||||
pub networks: Vec<String>,
|
||||
/// Gateway-MAC-Adressen von Netzwerken, in denen ein nmap-Scan (Schritt 3) ohne
|
||||
/// interaktive Rückfrage erlaubt ist. Wird um per Rückfrage/`--auto-trust-networks`
|
||||
/// bestätigte Netzwerke ergänzt, die im Cache gespeichert werden (siehe `src/trust.rs`).
|
||||
#[serde(default)]
|
||||
pub trusted_networks: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
@@ -50,10 +57,23 @@ impl Default for AppConfig {
|
||||
log_level: DEFAULT_LOG_LEVEL.to_string(),
|
||||
nmap_timeout_seconds: DEFAULT_NMAP_TIMEOUT_SECONDS,
|
||||
networks: Vec::new(),
|
||||
trusted_networks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Reine, testbare Funktion: validiert die konfigurierten vertrauten Netzwerke.
|
||||
/// Nicht als MAC-Adresse parsbare Einträge werden übersprungen (kein harter Fehler,
|
||||
/// da eine fehlerhafte Config-Zeile den Lookup nicht insgesamt scheitern lassen soll).
|
||||
pub fn trusted_network_macs(&self) -> Vec<MacAddress> {
|
||||
self.trusted_networks
|
||||
.iter()
|
||||
.filter_map(|s| MacAddress::parse(s).ok())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialisiert den Konfigurationspfad bei `config-ctdra` (Standardname "config",
|
||||
/// optionaler expliziter Pfad via `--config`).
|
||||
pub fn init_config_path(custom_path: Option<&Path>) {
|
||||
@@ -90,4 +110,7 @@ pub fn apply_cli_overrides(config: &mut AppConfig, cli: &crate::cli::Cli) {
|
||||
if let Some(networks) = &cli.networks {
|
||||
config.networks = networks.clone();
|
||||
}
|
||||
if let Some(trusted) = &cli.trusted_networks {
|
||||
config.trusted_networks = trusted.iter().map(MacAddress::to_lower_colon).collect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ use crate::mac::MacAddressError;
|
||||
pub enum Mac2IpError {
|
||||
#[error("keine IP-Adresse für MAC '{mac}' gefunden")]
|
||||
NotFound { mac: String },
|
||||
#[error(
|
||||
"nmap-Scan abgelehnt: Netzwerk nicht vertrauenswürdig{}",
|
||||
.gateway_mac.as_ref().map(|m| format!(" (Gateway-MAC {m})")).unwrap_or_default()
|
||||
)]
|
||||
UntrustedNetwork { gateway_mac: Option<String> },
|
||||
#[error("MAC-Adresse ungültig: {0}")]
|
||||
InvalidMac(#[from] MacAddressError),
|
||||
#[error("E/A-Fehler: {0}")]
|
||||
|
||||
@@ -10,3 +10,4 @@ pub mod mac;
|
||||
pub mod network;
|
||||
pub mod output;
|
||||
pub mod resolver;
|
||||
pub mod trust;
|
||||
|
||||
+9
-1
@@ -23,7 +23,15 @@ async fn main() -> ExitCode {
|
||||
|
||||
let cache = cache::try_open_cache(&app_config.cache_db_path).await;
|
||||
|
||||
match resolver::resolve(&cli.mac, &app_config, cache.as_ref(), cli.json).await {
|
||||
match resolver::resolve(
|
||||
&cli.mac,
|
||||
&app_config,
|
||||
cache.as_ref(),
|
||||
cli.json,
|
||||
cli.auto_trust_networks,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
output::print_success(&result, cli.json);
|
||||
ExitCode::SUCCESS
|
||||
|
||||
+55
-1
@@ -30,6 +30,31 @@ pub async fn run_ip_route_show_scope_link() -> std::io::Result<String> {
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
pub async fn run_ip_route_show_default() -> std::io::Result<String> {
|
||||
let out = tokio::process::Command::new("ip")
|
||||
.args(["-4", "route", "show", "default"])
|
||||
.output()
|
||||
.await?;
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
/// Ermittelt die MAC-Adresse des aktuellen Default-Gateways - dient als stabiler
|
||||
/// Identifikator für "dieses Netzwerk" (bleibt bei DHCP-Subnetzwechsel gleich,
|
||||
/// solange derselbe Router im Einsatz ist), unabhängig von SSID/CIDR.
|
||||
/// Gibt `None` zurück, wenn kein Default-Gateway existiert oder dessen MAC nicht
|
||||
/// ermittelt werden kann (z. B. kein Nachbar-Eintrag trotz Ping).
|
||||
pub async fn detect_default_gateway_mac() -> Option<MacAddress> {
|
||||
let route_output = run_ip_route_show_default().await.ok()?;
|
||||
let gateway_ip = parse_default_gateway_ip(&route_output)?;
|
||||
|
||||
// Erzwingt best-effort einen frischen Nachbar-Tabelleneintrag; Ergebnis wird
|
||||
// ignoriert, da `ip neigh` auch von einem veralteten/STALE-Eintrag profitiert.
|
||||
let _ = ping_check(gateway_ip).await;
|
||||
|
||||
let neigh_output = run_ip_neigh_show().await.ok()?;
|
||||
parse_ip_neigh_mac_for_ip(&neigh_output, gateway_ip)
|
||||
}
|
||||
|
||||
pub async fn ping_check(ip: IpAddr) -> bool {
|
||||
tokio::process::Command::new("ping")
|
||||
.args([
|
||||
@@ -106,7 +131,8 @@ pub async fn run_nmap_scan(
|
||||
let child = cmd.spawn()?;
|
||||
let pgid = child.id();
|
||||
|
||||
let out = match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
|
||||
let out =
|
||||
match tokio::time::timeout(Duration::from_secs(timeout_secs), child.wait_with_output())
|
||||
.await
|
||||
{
|
||||
Ok(out) => out?,
|
||||
@@ -159,6 +185,34 @@ pub fn parse_ip_neigh_output(output: &str, mac: &MacAddress) -> Option<IpAddr> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Parst `ip -4 route show default`, z. B. "default via 192.168.1.1 dev eth0 proto dhcp
|
||||
/// metric 100". Gibt die Gateway-IP zurück (Token direkt nach "via").
|
||||
pub fn parse_default_gateway_ip(output: &str) -> Option<IpAddr> {
|
||||
output.lines().find_map(|line| {
|
||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||
let pos = tokens.iter().position(|&t| t == "via")?;
|
||||
tokens.get(pos + 1)?.parse().ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Parst `ip neigh show`-Ausgabe und gibt die `lladdr`-MAC des Eintrags zurück, dessen
|
||||
/// erste Spalte (IP) mit `target_ip` übereinstimmt. Kehrbild zu [`parse_ip_neigh_output`].
|
||||
pub fn parse_ip_neigh_mac_for_ip(output: &str, target_ip: IpAddr) -> Option<MacAddress> {
|
||||
for line in output.lines() {
|
||||
let tokens: Vec<&str> = line.split_whitespace().collect();
|
||||
if tokens.first().and_then(|t| t.parse::<IpAddr>().ok()) != Some(target_ip) {
|
||||
continue;
|
||||
}
|
||||
if let Some(pos) = tokens.iter().position(|&t| t == "lladdr")
|
||||
&& let Some(&mac_str) = tokens.get(pos + 1)
|
||||
&& let Ok(mac) = mac_str.parse()
|
||||
{
|
||||
return Some(mac);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extrahiert lokale CIDR-Subnetze aus `ip -4 route show scope link`.
|
||||
/// Die CIDR ist bereits das erste whitespace-getrennte Token pro passender Zeile.
|
||||
/// Überspringt `default`-Zeilen und das Loopback-Interface.
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::config::AppConfig;
|
||||
use crate::error::Mac2IpError;
|
||||
use crate::mac::MacAddress;
|
||||
use crate::network::{self, NmapRunError, SudoMode};
|
||||
use crate::trust::{self, TrustDecision};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MatchSource {
|
||||
@@ -44,6 +45,7 @@ pub async fn resolve(
|
||||
config: &AppConfig,
|
||||
cache: Option<&Cache>,
|
||||
json_mode: bool,
|
||||
auto_trust_networks: bool,
|
||||
) -> Result<ResolveResult, Mac2IpError> {
|
||||
let now = now_unix();
|
||||
|
||||
@@ -86,6 +88,20 @@ pub async fn resolve(
|
||||
|
||||
// Schritt 3: nmap
|
||||
crate::log::debug("resolver", "Kein Treffer via ip neigh, starte nmap-Scan");
|
||||
|
||||
match trust::ensure_network_trusted(config, cache, auto_trust_networks, json_mode, now).await {
|
||||
TrustDecision::Allowed => {}
|
||||
TrustDecision::Denied { gateway_mac } => {
|
||||
crate::log::warn(
|
||||
"resolver",
|
||||
"nmap-Scan übersprungen: Netzwerk nicht als vertrauenswürdig bestätigt",
|
||||
);
|
||||
return Err(Mac2IpError::UntrustedNetwork {
|
||||
gateway_mac: gateway_mac.map(|m| m.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let subnets = if !config.networks.is_empty() {
|
||||
config.networks.clone()
|
||||
} else {
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//! Netzwerk-Vertrauensprüfung vor dem nmap-Scan (Schritt 3).
|
||||
//!
|
||||
//! Ein ARP-/Ping-Scan ganzer Subnetze ist auf Netzwerken, die man nicht selbst
|
||||
//! administriert (Firmen-/Gast-WLAN etc.), potenziell heikel (IDS-Alarm, Verstoß gegen
|
||||
//! Nutzungsordnungen). Das aktuelle Netzwerk wird daher über die MAC-Adresse seines
|
||||
//! Default-Gateways identifiziert (stabil über IP-/Subnetzwechsel hinweg) und nmap darf
|
||||
//! nur laufen, wenn dieses Netzwerk explizit vertraut ist:
|
||||
//! - statisch über `AppConfig::trusted_networks` (Config-Datei/CLI/ENV), oder
|
||||
//! - dynamisch über eine frühere Bestätigung, die im Cache gespeichert wurde, oder
|
||||
//! - automatisch, wenn `--auto-trust-networks` gesetzt ist (merkt sich das Netzwerk
|
||||
//! danach ebenfalls im Cache).
|
||||
//!
|
||||
//! Ist keins davon der Fall, wird im interaktiven Modus nachgefragt; im --json-Modus
|
||||
//! ist keine Rückfrage möglich, daher wird sicherheitshalber abgelehnt.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::config::AppConfig;
|
||||
use crate::mac::MacAddress;
|
||||
use crate::network;
|
||||
|
||||
pub enum TrustDecision {
|
||||
Allowed,
|
||||
Denied { gateway_mac: Option<MacAddress> },
|
||||
}
|
||||
|
||||
pub async fn ensure_network_trusted(
|
||||
config: &AppConfig,
|
||||
cache: Option<&Cache>,
|
||||
auto_trust: bool,
|
||||
json_mode: bool,
|
||||
now: i64,
|
||||
) -> TrustDecision {
|
||||
let Some(gateway_mac) = network::detect_default_gateway_mac().await else {
|
||||
crate::log::warn(
|
||||
"trust",
|
||||
"Gateway-MAC konnte nicht ermittelt werden, Netzwerk kann nicht identifiziert werden",
|
||||
);
|
||||
if auto_trust {
|
||||
crate::log::warn(
|
||||
"trust",
|
||||
"--auto-trust-networks gesetzt: nmap-Scan wird trotz unbekannter Gateway-MAC ausgeführt",
|
||||
);
|
||||
return TrustDecision::Allowed;
|
||||
}
|
||||
return TrustDecision::Denied { gateway_mac: None };
|
||||
};
|
||||
|
||||
if config.trusted_network_macs().contains(&gateway_mac) {
|
||||
return TrustDecision::Allowed;
|
||||
}
|
||||
|
||||
if let Some(cache) = cache
|
||||
&& cache.is_trusted_network(&gateway_mac).await
|
||||
{
|
||||
return TrustDecision::Allowed;
|
||||
}
|
||||
|
||||
if auto_trust {
|
||||
crate::log::info(
|
||||
"trust",
|
||||
&format!(
|
||||
"Netzwerk (Gateway-MAC {gateway_mac}) automatisch als vertrauenswürdig markiert"
|
||||
),
|
||||
);
|
||||
if let Some(cache) = cache {
|
||||
cache.trust_network(&gateway_mac, now).await;
|
||||
}
|
||||
return TrustDecision::Allowed;
|
||||
}
|
||||
|
||||
if json_mode {
|
||||
// Keine Rückfrage im --json-Modus möglich (stdin ggf. nicht an ein Terminal
|
||||
// gebunden, Ausgabe darf nicht durch einen Prompt verunreinigt werden).
|
||||
return TrustDecision::Denied {
|
||||
gateway_mac: Some(gateway_mac),
|
||||
};
|
||||
}
|
||||
|
||||
if prompt_trust_confirmation(gateway_mac).await {
|
||||
if let Some(cache) = cache {
|
||||
cache.trust_network(&gateway_mac, now).await;
|
||||
}
|
||||
TrustDecision::Allowed
|
||||
} else {
|
||||
TrustDecision::Denied {
|
||||
gateway_mac: Some(gateway_mac),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blockierende stdin-Abfrage, daher in `spawn_blocking` ausgelagert, damit der
|
||||
/// Tokio-Runtime-Thread währenddessen nicht blockiert.
|
||||
async fn prompt_trust_confirmation(gateway_mac: MacAddress) -> bool {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
eprint!(
|
||||
"Unbekanntes Netzwerk (Gateway-MAC {gateway_mac}). nmap-Scan in diesem Netzwerk erlauben und dauerhaft merken? [y/N]: "
|
||||
);
|
||||
let _ = std::io::stderr().flush();
|
||||
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_err() {
|
||||
return false;
|
||||
}
|
||||
matches!(input.trim().to_lowercase().as_str(), "y" | "yes" | "j" | "ja")
|
||||
})
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -14,6 +14,8 @@ fn base_cli() -> Cli {
|
||||
cache_db_path: None,
|
||||
nmap_timeout_seconds: None,
|
||||
networks: None,
|
||||
trusted_networks: None,
|
||||
auto_trust_networks: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +63,18 @@ fn overrides_networks() {
|
||||
assert_eq!(config.networks, vec!["10.0.0.0/24".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overrides_trusted_networks() {
|
||||
let mut config = AppConfig::default();
|
||||
let mut cli = base_cli();
|
||||
cli.trusted_networks = Some(vec![MacAddress::parse("aa:bb:cc:dd:ee:ff").unwrap()]);
|
||||
apply_cli_overrides(&mut config, &cli);
|
||||
assert_eq!(
|
||||
config.trusted_networks,
|
||||
vec!["aa:bb:cc:dd:ee:ff".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_overrides_only_touch_provided_fields() {
|
||||
let default_config = AppConfig::default();
|
||||
@@ -77,4 +91,5 @@ fn partial_overrides_only_touch_provided_fields() {
|
||||
default_config.nmap_timeout_seconds
|
||||
);
|
||||
assert_eq!(config.networks, default_config.networks);
|
||||
assert_eq!(config.trusted_networks, default_config.trusted_networks);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use mac2ip::config::AppConfig;
|
||||
use mac2ip::mac::MacAddress;
|
||||
use mac2ip::network::{parse_default_gateway_ip, parse_ip_neigh_mac_for_ip};
|
||||
|
||||
fn mac(s: &str) -> MacAddress {
|
||||
MacAddress::parse(s).unwrap()
|
||||
}
|
||||
|
||||
fn ip(s: &str) -> IpAddr {
|
||||
s.parse().unwrap()
|
||||
}
|
||||
|
||||
// --- parse_default_gateway_ip ---
|
||||
|
||||
#[test]
|
||||
fn parses_gateway_ip_from_default_route() {
|
||||
let output = "default via 192.168.1.1 dev eth0 proto dhcp metric 100\n";
|
||||
assert_eq!(parse_default_gateway_ip(output), Some(ip("192.168.1.1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_gateway_ip_ignoring_extra_trailing_fields() {
|
||||
let output = "default via 10.0.0.254 dev wlan0 proto static metric 600 onlink\n";
|
||||
assert_eq!(parse_default_gateway_ip(output), Some(ip("10.0.0.254")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_default_route_returns_none() {
|
||||
assert!(parse_default_gateway_ip("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_via_token_returns_none() {
|
||||
let output = "default dev eth0 proto dhcp metric 100\n";
|
||||
assert!(parse_default_gateway_ip(output).is_none());
|
||||
}
|
||||
|
||||
// --- parse_ip_neigh_mac_for_ip ---
|
||||
|
||||
#[test]
|
||||
fn finds_mac_for_matching_ip() {
|
||||
let output = "192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE\n";
|
||||
let result = parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1"));
|
||||
assert_eq!(result, Some(mac("aa:bb:cc:dd:ee:ff")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_entries_for_other_ips() {
|
||||
let output = "\
|
||||
192.168.1.2 dev eth0 lladdr 11:22:33:44:55:66 STALE
|
||||
192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
|
||||
";
|
||||
let result = parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1"));
|
||||
assert_eq!(result, Some(mac("aa:bb:cc:dd:ee:ff")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_ip_not_present() {
|
||||
let output = "192.168.1.2 dev eth0 lladdr 11:22:33:44:55:66 STALE\n";
|
||||
assert!(parse_ip_neigh_mac_for_ip(output, ip("192.168.1.1")).is_none());
|
||||
}
|
||||
|
||||
// --- AppConfig::trusted_network_macs ---
|
||||
|
||||
#[test]
|
||||
fn trusted_network_macs_parses_valid_entries() {
|
||||
let config = AppConfig {
|
||||
trusted_networks: vec!["aa:bb:cc:dd:ee:ff".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
config.trusted_network_macs(),
|
||||
vec![mac("aa:bb:cc:dd:ee:ff")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_network_macs_skips_invalid_entries() {
|
||||
let config = AppConfig {
|
||||
trusted_networks: vec!["not-a-mac".to_string(), "aa:bb:cc:dd:ee:ff".to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
config.trusted_network_macs(),
|
||||
vec![mac("aa:bb:cc:dd:ee:ff")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_network_macs_empty_by_default() {
|
||||
assert!(AppConfig::default().trusted_network_macs().is_empty());
|
||||
}
|
||||
Reference in New Issue
Block a user