harmony/harmony-rs/opnsense-config/src/modules/dhcp.rs

261 lines
7.4 KiB
Rust

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::<Ipv4Addr>()
.expect("Invalid DHCP range start");
let range_end = range.to.parse::<Ipv4Addr>().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<NumberOption>,
#[yaserde(rename = "range")]
pub range: Range,
pub winsserver: MaybeString,
pub dnsserver: MaybeString,
pub ntpserver: MaybeString,
#[yaserde(rename = "staticmap")]
pub staticmaps: Vec<StaticMap>,
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 = "<?xml version=\"1.0\"?>
<dhcpd>
<lan>
<enable>1</enable>
<gateway>192.168.20.1</gateway>
<domain>somedomain.yourlocal.mcd</domain>
<ddnsdomainalgorithm>hmac-md5</ddnsdomainalgorithm>
<numberoptions>
<item/>
</numberoptions>
<range>
<from>192.168.20.50</from>
<to>192.168.20.200</to>
</range>
<winsserver/>
<dnsserver>192.168.20.1</dnsserver>
<ntpserver/>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.160</ipaddr>
<hostname>somehost983</hostname>
<descr>someservire8</descr>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.155</ipaddr>
<hostname>somehost893</hostname>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.165</ipaddr>
<hostname>somehost893</hostname>
<descr/>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<staticmap>
<mac>55:55:55:55:55:1c</mac>
<ipaddr>192.168.20.50</ipaddr>
<hostname>hostswitch2</hostname>
<descr>switch-2 (bottom)</descr>
<winsserver/>
<dnsserver/>
<ntpserver/>
</staticmap>
<pool/>
</lan>
</dhcpd>\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::<Ipv4Addr>().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::<Ipv4Addr>().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::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), true);
// Test IP before range
let ip = "192.168.1.99".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false);
// Test IP after range
let ip = "192.168.1.201".parse::<Ipv4Addr>().unwrap();
assert_eq!(DhcpConfig::is_ip_in_range(&ip, range.clone()), false);
}
}