use std::cmp::Ordering; use std::net::IpAddr; use std::net::Ipv4Addr; use super::opnsense::{OPNsense, StaticMap}; use crate::infra::maybe_string::MaybeString; use crate::modules::opnsense::NumberOption; use crate::modules::opnsense::Range; use yaserde_derive::{YaDeserialize, YaSerialize}; pub struct DhcpConfig<'a> { opnsense: &'a mut OPNsense, } #[derive(Debug)] pub enum DhcpError { InvalidMacAddress(String), InvalidIpAddress(String), IpAddressAlreadyMapped(String), MacAddressAlreadyMapped(String), IpAddressOutOfRange(String), } impl std::fmt::Display for DhcpError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DhcpError::InvalidMacAddress(mac) => write!(f, "Invalid MAC address format: {}", mac), DhcpError::InvalidIpAddress(ip) => write!(f, "Invalid IP address format: {}", ip), DhcpError::IpAddressAlreadyMapped(ip) => { write!(f, "IP address {} is already mapped", ip) } DhcpError::MacAddressAlreadyMapped(mac) => { write!(f, "MAC address {} is already mapped", mac) } DhcpError::IpAddressOutOfRange(ip) => { write!(f, "IP address {} is out of interface range", ip) } } } } impl std::error::Error for DhcpError {} impl<'a> DhcpConfig<'a> { pub fn new(opnsense: &'a mut OPNsense) -> Self { Self { opnsense } } pub fn add_static_mapping( &mut self, mac: String, ipaddr: Ipv4Addr, hostname: String, ) -> Result<(), DhcpError> { let range: Range = todo!(); if !Self::is_valid_mac(&mac) { return Err(DhcpError::InvalidMacAddress(mac)); } // if !Self::is_ip_in_range(&ipaddr, range) { // return Err(DhcpError::IpAddressOutOfRange(ipaddr)); // } let existing_mappings = &self.opnsense.dhcpd.lan.staticmaps; // TODO // if existing_mappings.iter().any(|m| m.ipaddr == ipaddr) { // return Err(DhcpError::IpAddressAlreadyMapped(ipaddr)); // } // if existing_mappings.iter().any(|m| m.mac == mac) { // return Err(DhcpError::MacAddressAlreadyMapped(mac)); // } // let static_map = StaticMap { // mac, // ipaddr, // hostname, // descr: Default::default(), // winsserver: Default::default(), // dnsserver: Default::default(), // ntpserver: Default::default(), // }; // self.opnsense.dhcpd.lan.staticmaps.push(static_map); Ok(()) } pub fn get_static_mappings(&self) -> &[StaticMap] { &self.opnsense.dhcpd.lan.staticmaps } fn is_valid_mac(mac: &str) -> bool { let parts: Vec<&str> = mac.split(':').collect(); if parts.len() != 6 { return false; } parts .iter() .all(|part| part.len() == 2 && part.chars().all(|c| c.is_ascii_hexdigit())) } fn is_ip_in_range(ip: &Ipv4Addr, range: Range) -> bool { let range_start = range .from .parse::() .expect("Invalid DHCP range start"); let range_end = range.to.parse::().expect("Invalid DHCP range to"); let start_compare = range_start.cmp(ip); let end_compare = range_end.cmp(ip); if (Ordering::Less == start_compare || Ordering::Equal == start_compare) && (Ordering::Greater == end_compare || Ordering::Equal == end_compare) { return true; } else { return false; } } } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde(rename = "dhcpd")] pub struct Dhcpd { #[yaserde(rename = "lan")] pub lan: DhcpInterface, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct DhcpInterface { pub enable: i32, pub gateway: String, pub domain: String, #[yaserde(rename = "ddnsdomainalgorithm")] pub ddns_domain_algorithm: String, #[yaserde(rename = "numberoptions")] pub number_options: Vec, #[yaserde(rename = "range")] pub range: Range, pub winsserver: MaybeString, pub dnsserver: MaybeString, pub ntpserver: MaybeString, #[yaserde(rename = "staticmap")] pub staticmaps: Vec, pub pool: MaybeString, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct DhcpRange { #[yaserde(rename = "from")] pub from: String, #[yaserde(rename = "to")] pub to: String, } #[cfg(test)] mod test { use std::net::Ipv4Addr; use crate::infra::yaserde::to_xml_str; use super::*; use pretty_assertions::assert_eq; #[test] fn dhcpd_should_deserialize_serialize_identical() { let dhcpd: Dhcpd = yaserde::de::from_str(SERIALIZED_DHCPD).expect("Deserialize Dhcpd failed"); assert_eq!( to_xml_str(&dhcpd).expect("Serialize Dhcpd failed"), SERIALIZED_DHCPD ); } const SERIALIZED_DHCPD: &str = " 1 192.168.20.1 somedomain.yourlocal.mcd hmac-md5 192.168.20.50 192.168.20.200 192.168.20.1 55:55:55:55:55:1c 192.168.20.160 somehost983 someservire8 55:55:55:55:55:1c 192.168.20.155 somehost893 55:55:55:55:55:1c 192.168.20.165 somehost893 55:55:55:55:55:1c 192.168.20.50 hostswitch2 switch-2 (bottom) \n"; #[test] fn test_ip_in_range() { let range = Range { from: "192.168.1.100".to_string(), to: "192.168.1.200".to_string(), }; // Test IP within range let ip = "192.168.1.150".parse::().unwrap(); assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true); // Test IP at start of range let ip = "192.168.1.100".parse::().unwrap(); assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true); // Test IP at end of range let ip = "192.168.1.200".parse::().unwrap(); assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true); // Test IP before range let ip = "192.168.1.99".parse::().unwrap(); assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false); // Test IP after range let ip = "192.168.1.201".parse::().unwrap(); assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false); } }