pub use crate::message::*; pub use crate::monitor::*; use serde::Deserialize; extern crate regex; use regex::Regex; extern crate chrono; use chrono::Local; #[derive(Debug)] pub struct DHCPLeases { config: DHCPLeasesConfig, rgx_lease: Regex, rgx_mac: Regex, rgx_date_start: Regex, rgx_date_ends: Regex, } #[derive(Debug, PartialEq, Deserialize)] struct DHCPLeasesConfig { #[serde(default)] path: String, #[serde(default)] mac_rules: HashMap>>, #[serde(default)] period: Option, } impl Monitor for DHCPLeases { fn new(config: serde_yaml::Value) -> Result> { let config = serde_yaml::from_value(config)?; // Regex compilation let rgx_lease = Regex::new(r"(?s)lease\s+(\d+(?:\.\d+){3})\s*\{\n?(.*?)\}").unwrap(); let rgx_mac = Regex::new(r"(?m)^\s*hardware\s+ethernet\s([a-f0-9]{2}(?::[a-f0-9]{2}){5})\s*;") .unwrap(); let rgx_date_start = Regex::new(r"(?m)^\s*starts\s+\d+\s+(.*?)\s*;").unwrap(); let rgx_date_ends = Regex::new(r"(?m)^\s*ends\s+\d+\s+(.*?)\s*;").unwrap(); Ok(DHCPLeases { config: config, rgx_lease: rgx_lease, rgx_mac: rgx_mac, rgx_date_start: rgx_date_start, rgx_date_ends: rgx_date_ends, }) } fn run(&mut self, sender: &mpsc::Sender) { use std::fs::File; use std::io::prelude::*; let config_file_path = match self.config.path.len() { 0 => "/var/lib/dhcp/dhcpd.leases", _ => &self.config.path, }; let mut config_file = File::open(config_file_path).expect("Could not open DHCP leases file"); let mut config_content = String::new(); config_file .read_to_string(&mut config_content) .expect("Could not read DHCP leases file"); let mut unauthorized_macs: Vec = vec![]; for cap in self.rgx_lease.captures_iter(&config_content) { let ip = cap.get(1).unwrap().as_str(); let content = cap.get(2).unwrap().as_str(); if let Some(mac_cap) = self.rgx_mac.captures(content) { let mac = mac_cap.get(1).unwrap().as_str(); let starts_str = self .rgx_date_start .captures(content) .expect("No 'starts' field found in lease") .get(1) .unwrap() .as_str(); let starts = chrono::naive::NaiveDateTime::parse_from_str(starts_str, "%Y/%m/%d %H:%M:%S") .expect(&format!("Bad date format: '{}'", starts_str)); let ends_str = self .rgx_date_ends .captures(content) .expect("No 'ends' field found in lease") .get(1) .unwrap() .as_str(); let ends = chrono::naive::NaiveDateTime::parse_from_str(ends_str, "%Y/%m/%d %H:%M:%S") .expect(&format!("Bad date format: '{}'", ends_str)); let now = Local::now().naive_utc(); if starts <= now && now < ends { log::debug!("Found an active DHCP lease for {}", ip); // Lease is active if let Some(rules) = self.config.mac_rules.get(mac) { // Found rules if let Some(rules_list) = rules { // Rules contains one or more entries for rule in rules_list { if content.find(rule).is_none() { unauthorized_macs.push(mac.to_owned()); sender .send(Message { emitter: "dhcp_leases".to_owned(), level: Level::Issue, msg_type: "dhcp_leases.unauthorized_mac.rule" .to_owned(), text: format!( "Mismatching rule '{}' for device {} at IP {}", rule, mac, ip ), }) .unwrap(); break; } } } } else { unauthorized_macs.push(mac.to_owned()); sender .send(Message { emitter: "dhcp_leases".to_owned(), level: Level::Issue, msg_type: "dhcp_leases.unauthorized_mac.unknown".to_owned(), text: format!("Unknown device {} using IP {}", mac, ip), }) .unwrap(); } } } else { log::warn!("No 'hardware ethernet' field found for IP {}", ip); } } if unauthorized_macs.len() > 0 { sender .send(Message { emitter: "dhcp_leases".to_owned(), level: Level::Issue, msg_type: "dhcp_leases.unknown_mac.sumup".to_owned(), text: format!( "The following macs are not allowed: {:?}", unauthorized_macs ), }) .unwrap(); } std::thread::sleep(std::time::Duration::from_millis( (self.config.period.unwrap_or(60.0) * 1000.0) as u64, )); } } /* lease 192.168.0.26 { starts 4 2018/08/16 22:31:22; ends 4 2018/08/16 23:01:22; tstp 4 2018/08/16 23:01:22; cltt 4 2018/08/16 22:31:22; binding state free; hardware ethernet 84:4b:f5:16:f4:94; } */