Some checks failed
Run Check Script / check (pull_request) Failing after 37s
197 lines
6.5 KiB
Rust
197 lines
6.5 KiB
Rust
// dnsmasq.rs
|
|
use crate::modules::dhcp::DhcpError;
|
|
use log::{debug, info};
|
|
use opnsense_config_xml::dnsmasq::{DhcpBoot, DhcpOptions, DnsMasq};
|
|
use opnsense_config_xml::{MaybeString, StaticMap};
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use opnsense_config_xml::OPNsense;
|
|
|
|
use crate::config::OPNsenseShell;
|
|
use crate::Error;
|
|
|
|
pub struct DhcpConfigDnsMasq<'a> {
|
|
opnsense: &'a mut OPNsense,
|
|
opnsense_shell: Arc<dyn OPNsenseShell>,
|
|
}
|
|
|
|
const DNS_MASQ_PXE_CONFIG_FILE: &str = "/usr/local/etc/dnsmasq.conf.d/pxe.conf";
|
|
|
|
impl<'a> DhcpConfigDnsMasq<'a> {
|
|
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
|
Self {
|
|
opnsense,
|
|
opnsense_shell,
|
|
}
|
|
}
|
|
|
|
/// Removes a static mapping by its MAC address.
|
|
/// Static mappings are stored in the <dhcpd> section of the config, shared with the ISC module.
|
|
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);
|
|
}
|
|
|
|
/// Retrieves a mutable reference to the LAN interface's DHCP configuration.
|
|
/// This is located in the shared <dhcpd> section of the config.
|
|
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
|
|
&mut self
|
|
.opnsense
|
|
.dhcpd
|
|
.elements
|
|
.iter_mut()
|
|
.find(|(name, _config)| name == "lan")
|
|
.expect("Interface lan should have dhcpd activated")
|
|
.1
|
|
}
|
|
|
|
fn dnsmasq(&mut self) -> &mut DnsMasq {
|
|
self.opnsense
|
|
.dnsmasq
|
|
.as_mut()
|
|
.expect("Dnsmasq config should exist. Maybe it is not installed yet")
|
|
}
|
|
|
|
/// Adds a new static DHCP mapping.
|
|
/// Validates the MAC address and checks for existing mappings to prevent conflicts.
|
|
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 the IP address is within a configured DHCP range.
|
|
|
|
if existing_mappings
|
|
.iter()
|
|
.any(|m| m.ipaddr == ipaddr.to_string() && m.mac == mac)
|
|
{
|
|
info!("Mapping already exists for {} [{}], skipping", ipaddr, mac);
|
|
return Ok(());
|
|
}
|
|
|
|
if existing_mappings
|
|
.iter()
|
|
.any(|m| m.ipaddr == ipaddr.to_string())
|
|
{
|
|
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: hostname,
|
|
..Default::default()
|
|
};
|
|
|
|
existing_mappings.push(static_map);
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to validate a MAC address format.
|
|
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()))
|
|
}
|
|
|
|
/// Retrieves the list of current static mappings by shelling out to `configctl`.
|
|
/// This provides the real-time state from the running system.
|
|
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)
|
|
.unwrap_or_else(|_| panic!("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),
|
|
..Default::default()
|
|
})
|
|
.collect();
|
|
|
|
Ok(static_maps)
|
|
}
|
|
|
|
pub async fn set_pxe_options(
|
|
&self,
|
|
tftp_ip: Option<String>,
|
|
bios_filename: String,
|
|
efi_filename: String,
|
|
ipxe_filename: String,
|
|
) -> Result<(), DhcpError> {
|
|
// As of writing this opnsense does not support negative tags, and the dnsmasq config is a
|
|
// bit complicated anyways. So we are writing directly a dnsmasq config file to
|
|
// /usr/local/etc/dnsmasq.conf.d
|
|
let tftp_str = tftp_ip.map_or(String::new(), |i| format!(",{i},{i}"));
|
|
|
|
let config = format!(
|
|
"
|
|
# Add tag ipxe to dhcp requests with user class (77) = iPXE
|
|
dhcp-match=set:ipxe,77,iPXE
|
|
# Add tag bios to dhcp requests with arch (93) = 0
|
|
dhcp-match=set:bios,93,0
|
|
# Add tag efi to dhcp requests with arch (93) = 7
|
|
dhcp-match=set:efi,93,7
|
|
|
|
# Provide ipxe efi file to uefi but NOT ipxe clients
|
|
dhcp-boot=tag:efi,tag:!ipxe,{efi_filename}{tftp_str}
|
|
|
|
# Provide ipxe boot script to ipxe clients
|
|
dhcp-boot=tag:ipxe,{ipxe_filename}{tftp_str}
|
|
|
|
# Provide undionly to legacy bios clients
|
|
dhcp-boot=tag:bios,{bios_filename}{tftp_str}
|
|
"
|
|
);
|
|
info!("Writing configuration file to {DNS_MASQ_PXE_CONFIG_FILE}");
|
|
debug!("Content:\n{config}");
|
|
self.opnsense_shell
|
|
.write_content_to_file(&config, DNS_MASQ_PXE_CONFIG_FILE)
|
|
.await
|
|
.map_err(|e| {
|
|
DhcpError::Configuration(format!(
|
|
"Could not configure pxe for dhcp because of : {e}"
|
|
))
|
|
})?;
|
|
|
|
info!("Restarting dnsmasq to apply changes");
|
|
self.opnsense_shell
|
|
.exec("configctl dnsmasq restart")
|
|
.await
|
|
.map_err(|e| DhcpError::Configuration(format!("Restarting dnsmasq failed : {e}")))?;
|
|
Ok(())
|
|
}
|
|
}
|