IPv6 lease monitoring

This commit is contained in:
2025-01-04 22:28:45 +00:00
parent c932e46ade
commit cffb6522e3

View File

@@ -4,6 +4,7 @@ use std::collections::HashMap;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::path::Path;
use std::process::Command; use std::process::Command;
use std::thread::sleep; use std::thread::sleep;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -27,10 +28,22 @@ struct Args {
#[arg(short, long, default_value = "/etc/dhcpleasemon")] #[arg(short, long, default_value = "/etc/dhcpleasemon")]
scripts_dir: String, 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 /// Directory monitored for lease changes
#[arg(short, long, default_value = "/var/db/dhcpleased")] #[arg(short, long, default_value = "/var/db/dhcpleased")]
dhcp_lease_dir: String, dhcp_lease_dir: String,
/// Directory monitored for IPv6 lease changes
#[arg(short, long, default_value = "/var/db/dhcp6leased")]
dhcp6_lease_dir: String,
/// Scan interval /// Scan interval
#[arg(short = 't', long, default_value_t = 1)] #[arg(short = 't', long, default_value_t = 1)]
interval: u8, interval: u8,
@@ -39,6 +52,10 @@ struct Args {
#[arg(short, long)] #[arg(short, long)]
interfaces: Vec<String>, interfaces: Vec<String>,
/// Monitor IPv6 leases as well
#[arg(short = '6', long)]
ipv6: bool,
/// Verbosity /// Verbosity
#[arg(short, long)] #[arg(short, long)]
verbosity: bool, verbosity: bool,
@@ -51,10 +68,19 @@ struct LeaseParams {
route_addr: String, route_addr: String,
} }
#[derive(PartialEq, Debug)]
struct Lease6Params {
iface_name: String,
ip6_prefix: String,
ip6_prefix_len: String,
route6_addr: String,
}
struct Monitor { struct Monitor {
args: Args, args: Args,
timestamps: HashMap<String, SystemTime>, timestamps: HashMap<String, SystemTime>,
lease_params: HashMap<String, LeaseParams>, lease_params: HashMap<String, LeaseParams>,
lease6_params: HashMap<String, Lease6Params>,
} }
impl Monitor { impl Monitor {
@@ -63,6 +89,7 @@ impl Monitor {
args, args,
timestamps: HashMap::new(), timestamps: HashMap::new(),
lease_params: HashMap::new(), lease_params: HashMap::new(),
lease6_params: HashMap::new(),
} }
} }
@@ -97,18 +124,32 @@ impl Monitor {
format!("{dhcp_lease_dir}/{iface_name}") 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 /// Generates the trigger script path for a given interface
fn get_trigger_script_path(&self, iface_name: &str) -> String { fn get_trigger_script_path(&self, iface_name: &str) -> String {
let trigger_scripts_path = &self.args.scripts_dir; 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 /// Gets the default route for iface from netstat
fn get_default_route(&self, iface_name: &str) -> Option<String> { fn get_default_route(&self, iface_name: &str, proto: &str) -> Option<String> {
let output = Command::new("netstat") let output = Command::new("netstat")
.arg("-rn") .arg("-rn")
.arg("-f") .arg("-f")
.arg("inet") .arg(proto)
.output() .output()
.expect("Failed to execute netstat"); .expect("Failed to execute netstat");
@@ -138,8 +179,8 @@ impl Monitor {
return None; return None;
} }
/// Extracts the IP address from the lease file /// Extracts the IPv4 address from the lease file
fn get_lease_ip_addr(&self, lease_file_path: &str) -> Option<String> { fn get_lease_ip4_addr(&self, lease_file_path: &str) -> Option<String> {
if let Ok(f) = File::open(lease_file_path) { if let Ok(f) = File::open(lease_file_path) {
let lines = io::BufReader::new(f).lines(); let lines = io::BufReader::new(f).lines();
for line in lines.flatten() { for line in lines.flatten() {
@@ -153,18 +194,42 @@ impl Monitor {
None 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 /// Execute the trigger script
fn run_trigger_script(&mut self, lease_params: &LeaseParams) -> () { fn run_trigger_script(&mut self, lease_params: &LeaseParams) -> () {
let iface_name = lease_params.iface_name.to_owned(); 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 default_route = lease_params.route_addr.to_owned();
let lease_ip_addr = lease_params.ip_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() { if self.verbosity() {
println!("Triggered: {:?}", lease_params); 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_IFACE", iface_name)
.env("DHCP_IP_ADDR", lease_ip_addr) .env("DHCP_IP_ADDR", lease_ip_addr)
.env("DHCP_IP_ROUTE", default_route) .env("DHCP_IP_ROUTE", default_route)
@@ -173,8 +238,42 @@ impl Monitor {
if !output.status.success() { if !output.status.success() {
println!( println!(
"Trigger script execution was unsuccessful: {}", "Trigger script execution was unsuccessful: {} (path: {})",
output.status.to_string() 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,26 +284,42 @@ impl Monitor {
LeaseParams { LeaseParams {
iface_name: iface_name.to_string(), iface_name: iface_name.to_string(),
ip_addr: self ip_addr: self
.get_lease_ip_addr(&lease_file_path) .get_lease_ip4_addr(&lease_file_path)
.unwrap_or(String::from("")), .unwrap_or(String::from("")),
route_addr: self route_addr: self
.get_default_route(&iface_name) .get_default_route(&iface_name, "inet")
.unwrap_or(String::from("")), .unwrap_or(String::from("")),
} }
} }
/// The main monitoring loop /// Gathers all params related to the lease associated with an interface
fn run(&mut self) { fn get_actual_lease6_params(&self, iface_name: &str) -> Lease6Params {
loop { let lease_file_path = self.get_lease6_file_path(&iface_name);
for iface_name in self.args.interfaces.clone() { let (ip6_prefix, ip6_prefix_len) = self
if self.verbosity() { .get_lease_ip6_extract(&lease_file_path)
println!("Checking: {}", iface_name); .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); let lease_file_path = self.get_lease_file_path(&iface_name);
if self.check_file_modified(&lease_file_path) { if self.check_file_modified(&lease_file_path) {
let lease_params = self.get_actual_lease_params(&iface_name); let lease_params = self.get_actual_lease_params(&iface_name);
let trigger = match self.lease_params.get(&iface_name) { let trigger = match self.lease_params.get(iface_name) {
Some(current_lease_params) => { Some(current_lease_params) => {
if *current_lease_params != lease_params { if *current_lease_params != lease_params {
true true
@@ -232,6 +347,54 @@ impl Monitor {
} }
} }
} }
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() {
self.check_lease(&iface_name);
if self.args.ipv6 {
self.check_lease6(&iface_name);
}
}
sleep(Duration::new(self.args.interval.into(), 0)); sleep(Duration::new(self.args.interval.into(), 0));
} }
} }