diff --git a/src/main.rs b/src/main.rs index c27ae25..919844b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fs; use std::fs::File; use std::io::{self, BufRead}; +use std::path::Path; use std::process::Command; use std::thread::sleep; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -27,10 +28,22 @@ struct Args { #[arg(short, long, default_value = "/etc/dhcpleasemon")] scripts_dir: String, + /// Name prefix for trigger scripts (IPv4) + #[arg(long, default_value = "lease_trigger_")] + trigger_script_prefix: String, + + /// Name prefix for trigger scripts (IPv6) + #[arg(long, default_value = "lease_trigger_")] + trigger_script_prefix_ipv6: String, + /// Directory monitored for lease changes #[arg(short, long, default_value = "/var/db/dhcpleased")] dhcp_lease_dir: String, + /// Directory monitored for IPv6 lease changes + #[arg(short, long, default_value = "/var/db/dhcp6leased")] + dhcp6_lease_dir: String, + /// Scan interval #[arg(short = 't', long, default_value_t = 1)] interval: u8, @@ -39,6 +52,10 @@ struct Args { #[arg(short, long)] interfaces: Vec, + /// Monitor IPv6 leases as well + #[arg(short = '6', long)] + ipv6: bool, + /// Verbosity #[arg(short, long)] verbosity: bool, @@ -51,10 +68,19 @@ struct LeaseParams { route_addr: String, } +#[derive(PartialEq, Debug)] +struct Lease6Params { + iface_name: String, + ip6_prefix: String, + ip6_prefix_len: String, + route6_addr: String, +} + struct Monitor { args: Args, timestamps: HashMap, lease_params: HashMap, + lease6_params: HashMap, } impl Monitor { @@ -63,6 +89,7 @@ impl Monitor { args, timestamps: HashMap::new(), lease_params: HashMap::new(), + lease6_params: HashMap::new(), } } @@ -97,18 +124,32 @@ impl Monitor { format!("{dhcp_lease_dir}/{iface_name}") } + /// Generates the lease (IPv6) file path for a given interface + fn get_lease6_file_path(&self, iface_name: &str) -> String { + let dhcp6_lease_dir = &self.args.dhcp6_lease_dir; + format!("{dhcp6_lease_dir}/{iface_name}") + } + /// Generates the trigger script path for a given interface fn get_trigger_script_path(&self, iface_name: &str) -> String { let trigger_scripts_path = &self.args.scripts_dir; - format!("{trigger_scripts_path}/lease_trigger_{iface_name}") + let trigger_scripts_prefix = &self.args.trigger_script_prefix; + format!("{trigger_scripts_path}/{trigger_scripts_prefix}{iface_name}") + } + + /// Generates the (IPv6) trigger script path for a given interface + fn get_trigger_script_path_ipv6(&self, iface_name: &str) -> String { + let trigger_scripts_path = &self.args.scripts_dir; + let trigger_scripts_prefix = &self.args.trigger_script_prefix_ipv6; + format!("{trigger_scripts_path}/{trigger_scripts_prefix}{iface_name}") } /// Gets the default route for iface from netstat - fn get_default_route(&self, iface_name: &str) -> Option { + fn get_default_route(&self, iface_name: &str, proto: &str) -> Option { let output = Command::new("netstat") .arg("-rn") .arg("-f") - .arg("inet") + .arg(proto) .output() .expect("Failed to execute netstat"); @@ -138,8 +179,8 @@ impl Monitor { return None; } - /// Extracts the IP address from the lease file - fn get_lease_ip_addr(&self, lease_file_path: &str) -> Option { + /// Extracts the IPv4 address from the lease file + fn get_lease_ip4_addr(&self, lease_file_path: &str) -> Option { if let Ok(f) = File::open(lease_file_path) { let lines = io::BufReader::new(f).lines(); for line in lines.flatten() { @@ -153,18 +194,42 @@ impl Monitor { None } + /// Extract the IPv6 address from the lease file + fn get_lease_ip6_extract(&self, lease_file_path: &str) -> Option<(String, String)> { + if let Ok(f) = File::open(lease_file_path) { + let lines = io::BufReader::new(f).lines(); + for line in lines.flatten() { + let cols: Vec<&str> = line.split_whitespace().collect(); + if cols.len() > 0 { + let root_directive = cols[0].trim().to_string(); + if root_directive == "ia_pd" { + let ip_prefix = cols[2].trim().to_string(); + let ip_prefix_len = cols[3].trim().to_string(); + return Some((ip_prefix, ip_prefix_len)); + } + } + } + } + None + } + /// Execute the trigger script fn run_trigger_script(&mut self, lease_params: &LeaseParams) -> () { let iface_name = lease_params.iface_name.to_owned(); + let trigger_script_path = self.get_trigger_script_path(&iface_name); + + if !Path::new(&trigger_script_path).exists() { + return; + } + let default_route = lease_params.route_addr.to_owned(); let lease_ip_addr = lease_params.ip_addr.to_owned(); - let trigger_script_path = self.get_trigger_script_path(&iface_name); if self.verbosity() { println!("Triggered: {:?}", lease_params); } - let output = Command::new(trigger_script_path) + let output = Command::new(&trigger_script_path) .env("DHCP_IFACE", iface_name) .env("DHCP_IP_ADDR", lease_ip_addr) .env("DHCP_IP_ROUTE", default_route) @@ -173,8 +238,42 @@ impl Monitor { if !output.status.success() { println!( - "Trigger script execution was unsuccessful: {}", - output.status.to_string() + "Trigger script execution was unsuccessful: {} (path: {})", + output.status.to_string(), + &trigger_script_path, + ); + } + } + + fn run_trigger_script_ipv6(&mut self, lease_params: &Lease6Params) -> () { + let iface_name = lease_params.iface_name.to_owned(); + let trigger_script_path = self.get_trigger_script_path_ipv6(&iface_name); + + if !Path::new(&trigger_script_path).exists() { + return; + } + + let default_route = lease_params.route6_addr.to_owned(); + let lease_ip_prefix = lease_params.ip6_prefix.to_owned(); + let lease_ip_prefix_len = lease_params.ip6_prefix_len.to_owned(); + + if self.verbosity() { + println!("Triggered: {:?}", lease_params); + } + + let output = Command::new(&trigger_script_path) + .env("DHCP6_IFACE", iface_name) + .env("DHCP6_IP_PREFIX", lease_ip_prefix) + .env("DHCP6_IP_PREFIX_LEN", lease_ip_prefix_len) + .env("DHCP6_IP_ROUTE", default_route) + .output() + .expect("Failed to execute trigger script"); + + if !output.status.success() { + println!( + "Trigger script execution was unsuccessful: {} (path: {})", + output.status.to_string(), + &trigger_script_path, ); } } @@ -185,51 +284,115 @@ impl Monitor { LeaseParams { iface_name: iface_name.to_string(), ip_addr: self - .get_lease_ip_addr(&lease_file_path) + .get_lease_ip4_addr(&lease_file_path) .unwrap_or(String::from("")), route_addr: self - .get_default_route(&iface_name) + .get_default_route(&iface_name, "inet") .unwrap_or(String::from("")), } } + /// Gathers all params related to the lease associated with an interface + fn get_actual_lease6_params(&self, iface_name: &str) -> Lease6Params { + let lease_file_path = self.get_lease6_file_path(&iface_name); + let (ip6_prefix, ip6_prefix_len) = self + .get_lease_ip6_extract(&lease_file_path) + .unwrap_or((String::from(""), String::from(""))); + let route6_addr = self + .get_default_route(&iface_name, "inet6") + .unwrap_or(String::from("")); + + Lease6Params { + iface_name: iface_name.to_string(), + ip6_prefix, + ip6_prefix_len, + route6_addr, + } + } + + fn check_lease(&mut self, iface_name: &str) { + if self.verbosity() { + println!("Checking (IPv4): {}", iface_name); + } + + let lease_file_path = self.get_lease_file_path(&iface_name); + if self.check_file_modified(&lease_file_path) { + let lease_params = self.get_actual_lease_params(&iface_name); + + let trigger = match self.lease_params.get(iface_name) { + Some(current_lease_params) => { + if *current_lease_params != lease_params { + true + } else { + if self.verbosity() { + println!("Lease params unchanged: {:?}", lease_params); + } + false + } + } + None => true, + }; + + if trigger { + if self.verbosity() { + println!("Triggered: {:?}", lease_params); + } + self.run_trigger_script(&lease_params); + self.lease_params + .insert(iface_name.to_owned(), lease_params); + } + } else { + if self.verbosity() { + println!("File not modified for {}", iface_name); + } + } + } + + fn check_lease6(&mut self, iface_name: &str) { + if self.verbosity() { + println!("Checking (IPv6): {}", iface_name); + } + + let lease_file_path = self.get_lease6_file_path(&iface_name); + if self.check_file_modified(&lease_file_path) { + let lease6_params = self.get_actual_lease6_params(&iface_name); + + let trigger = match self.lease6_params.get(iface_name) { + Some(current_lease6_params) => { + if *current_lease6_params != lease6_params { + true + } else { + if self.verbosity() { + println!("Lease params unchanged: {:?}", lease6_params); + } + false + } + } + None => true, + }; + + if trigger { + if self.verbosity() { + println!("Triggered: {:?}", lease6_params); + } + self.run_trigger_script_ipv6(&lease6_params); + self.lease6_params + .insert(iface_name.to_owned(), lease6_params); + } + } else { + if self.verbosity() { + println!("File not modified for {}", iface_name); + } + } + } + /// The main monitoring loop fn run(&mut self) { loop { for iface_name in self.args.interfaces.clone() { - if self.verbosity() { - println!("Checking: {}", iface_name); - } - let lease_file_path = self.get_lease_file_path(&iface_name); - if self.check_file_modified(&lease_file_path) { - let lease_params = self.get_actual_lease_params(&iface_name); - - let trigger = match self.lease_params.get(&iface_name) { - Some(current_lease_params) => { - if *current_lease_params != lease_params { - true - } else { - if self.verbosity() { - println!("Lease params unchanged: {:?}", lease_params); - } - false - } - } - None => true, - }; - - if trigger { - if self.verbosity() { - println!("Triggered: {:?}", lease_params); - } - self.run_trigger_script(&lease_params); - self.lease_params - .insert(iface_name.to_owned(), lease_params); - } - } else { - if self.verbosity() { - println!("File not modified for {}", iface_name); - } + self.check_lease(&iface_name); + if self.args.ipv6 { + self.check_lease6(&iface_name); } } sleep(Duration::new(self.args.interval.into(), 0));