forked from NationTech/harmony
186 lines
5.7 KiB
Rust
186 lines
5.7 KiB
Rust
use log::info;
|
|
use opnsense_config_xml::MaybeString;
|
|
use opnsense_config_xml::StaticMap;
|
|
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 validate that address is in subnet range
|
|
|
|
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()))
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|