forked from NationTech/harmony
Improved configuration handling for Harmony Opnsense setup. Implemented changes to opnsense-config module to support various settings, including load balancer configuration and DHCP server settings. This update enhances the overall stability and functionality of the Harmony Opnsense setup process.
248 lines
7.8 KiB
Rust
248 lines
7.8 KiB
Rust
use log::info;
|
|
use opnsense_config_xml::MaybeString;
|
|
use opnsense_config_xml::Range;
|
|
use opnsense_config_xml::StaticMap;
|
|
use std::cmp::Ordering;
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::Arc;
|
|
|
|
use opnsense_config_xml::OPNsense;
|
|
|
|
use crate::config::OPNsenseShell;
|
|
use crate::Error;
|
|
|
|
pub struct DhcpConfig<'a> {
|
|
opnsense: &'a mut OPNsense,
|
|
opnsense_shell: Arc<dyn OPNsenseShell>,
|
|
}
|
|
|
|
#[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, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
|
Self {
|
|
opnsense,
|
|
opnsense_shell,
|
|
}
|
|
}
|
|
|
|
pub fn remove_static_mapping(&mut self, mac: &str) {
|
|
let lan_dhcpd = self.get_lan_dhcpd();
|
|
lan_dhcpd
|
|
.staticmaps
|
|
.retain(|static_entry| static_entry.mac != mac);
|
|
}
|
|
|
|
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
|
|
&mut self
|
|
.opnsense
|
|
.dhcpd
|
|
.elements
|
|
.iter_mut()
|
|
.find(|(name, _config)| return name == "lan")
|
|
.expect("Interface lan should have dhcpd activated")
|
|
.1
|
|
}
|
|
|
|
pub fn add_static_mapping(
|
|
&mut self,
|
|
mac: &str,
|
|
ipaddr: Ipv4Addr,
|
|
hostname: &str,
|
|
) -> Result<(), DhcpError> {
|
|
let mac = mac.to_string();
|
|
let hostname = hostname.to_string();
|
|
let lan_dhcpd = self.get_lan_dhcpd();
|
|
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
|
|
|
|
if !Self::is_valid_mac(&mac) {
|
|
return Err(DhcpError::InvalidMacAddress(mac));
|
|
}
|
|
|
|
// TODO verify if address is in subnet range
|
|
// This check here does not do what we want to do, as we want to assign static leases
|
|
// outside of the dynamic DHCP pool
|
|
// let range = &lan_dhcpd.range;
|
|
// if !Self::is_ip_in_range(&ipaddr, range) {
|
|
// return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
|
|
// }
|
|
|
|
if existing_mappings.iter().any(|m| {
|
|
m.ipaddr
|
|
.parse::<Ipv4Addr>()
|
|
.expect("Mapping contains invalid ipv4")
|
|
== ipaddr
|
|
&& m.mac == mac
|
|
}) {
|
|
info!(
|
|
"Mapping already exists for {} [{}], skipping",
|
|
ipaddr.to_string(),
|
|
mac
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
if existing_mappings.iter().any(|m| {
|
|
m.ipaddr
|
|
.parse::<Ipv4Addr>()
|
|
.expect("Mapping contains invalid ipv4")
|
|
== ipaddr
|
|
}) {
|
|
return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string()));
|
|
}
|
|
|
|
if existing_mappings.iter().any(|m| m.mac == mac) {
|
|
return Err(DhcpError::MacAddressAlreadyMapped(mac));
|
|
}
|
|
|
|
let static_map = StaticMap {
|
|
mac,
|
|
ipaddr: ipaddr.to_string(),
|
|
hostname,
|
|
descr: Default::default(),
|
|
winsserver: Default::default(),
|
|
dnsserver: Default::default(),
|
|
ntpserver: Default::default(),
|
|
};
|
|
|
|
existing_mappings.push(static_map);
|
|
Ok(())
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> {
|
|
let list_static_output = self
|
|
.opnsense_shell
|
|
.exec("configctl dhcpd list static")
|
|
.await?;
|
|
|
|
let value: serde_json::Value = serde_json::from_str(&list_static_output).expect(&format!(
|
|
"Got invalid json from configctl {list_static_output}"
|
|
));
|
|
let static_maps = value["dhcpd"]
|
|
.as_array()
|
|
.ok_or(Error::Command(format!(
|
|
"Invalid DHCP data from configctl command, got {list_static_output}"
|
|
)))?
|
|
.iter()
|
|
.map(|entry| StaticMap {
|
|
mac: entry["mac"].as_str().unwrap_or_default().to_string(),
|
|
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
|
|
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
|
|
descr: entry["descr"].as_str().map(MaybeString::from),
|
|
winsserver: MaybeString::default(),
|
|
dnsserver: MaybeString::default(),
|
|
ntpserver: MaybeString::default(),
|
|
})
|
|
.collect();
|
|
|
|
Ok(static_maps)
|
|
}
|
|
pub fn enable_netboot(&mut self) {
|
|
self.get_lan_dhcpd().netboot = Some(1);
|
|
}
|
|
|
|
pub fn set_next_server(&mut self, ip: Ipv4Addr) {
|
|
self.enable_netboot();
|
|
self.get_lan_dhcpd().nextserver = Some(ip.to_string());
|
|
self.get_lan_dhcpd().tftp = Some(ip.to_string());
|
|
}
|
|
|
|
pub fn set_boot_filename(&mut self, boot_filename: &str) {
|
|
self.enable_netboot();
|
|
self.get_lan_dhcpd().filename64 = Some(boot_filename.to_string());
|
|
self.get_lan_dhcpd().bootfilename = Some(boot_filename.to_string());
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::net::Ipv4Addr;
|
|
|
|
#[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), 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), 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), true);
|
|
|
|
// Test IP before range
|
|
let ip = "192.168.1.99".parse::<Ipv4Addr>().unwrap();
|
|
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), false);
|
|
|
|
// Test IP after range
|
|
let ip = "192.168.1.201".parse::<Ipv4Addr>().unwrap();
|
|
assert_eq!(DhcpConfig::is_ip_in_range(&ip, &range), false);
|
|
}
|
|
}
|