wip: PXE setup for ipxe and okd files in progress
Some checks failed
Run Check Script / check (pull_request) Failing after 36s

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-08-21 17:28:17 -04:00
parent e956772593
commit da6610c625
24 changed files with 1242 additions and 209 deletions

1
Cargo.lock generated
View File

@ -3596,6 +3596,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-util",
"uuid",
]
[[package]]

View File

@ -125,9 +125,10 @@ async fn main() {
harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = StaticFilesHttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(),
));
let http_score = StaticFilesHttpScore {
folder_to_serve: Some(Url::LocalFolder("./data/watchguard/pxe-http-files".to_string())),
files: vec![],
};
let ipxe_score = IpxeScore::new();
harmony_tui::run(

View File

@ -1,7 +1,8 @@
mod topology;
use harmony::{
modules::{dhcp::DhcpScore, tftp::TftpScore},
data::{FileContent, FilePath},
modules::{dhcp::DhcpScore, http::StaticFilesHttpScore, tftp::TftpScore},
score::Score,
topology::{HAClusterTopology, Url},
};
@ -14,6 +15,7 @@ async fn main() {
let topology = get_topology().await;
let gateway_ip = topology.router.get_gateway();
// TODO this should be a single IPXEScore instead of having the user do this step by step
let scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![
Box::new(DhcpScore {
host_binding: vec![],
@ -26,7 +28,23 @@ async fn main() {
Box::new(TftpScore {
files_to_serve: Url::LocalFolder("./data/pxe/okd/tftpboot/".to_string()),
}),
Box::new(StaticFilesHttpScore {
folder_to_serve: None,
files: vec![FileContent {
path: FilePath::Relative("boot.ipxe".to_string()),
content: format!(
"#!ipxe
set base-url http://{gateway_ip}:8080
set hostfile ${{base-url}}/byMAC/01-${{mac:hexhyp}}.ipxe
chain ${{hostfile}} || chain ${{base-url}}/default.ipxe"
),
}],
}),
];
harmony_cli::run(inventory, topology, scores, None).await.unwrap();
harmony_cli::run(inventory, topology, scores, None)
.await
.unwrap();
}

View File

@ -80,9 +80,12 @@ async fn main() {
let load_balancer_score = OKDLoadBalancerScore::new(&topology);
let tftp_score = TftpScore::new(Url::LocalFolder("./data/watchguard/tftpboot".to_string()));
let http_score = StaticFilesHttpScore::new(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(),
));
let http_score = StaticFilesHttpScore {
folder_to_serve: Some(Url::LocalFolder(
"./data/watchguard/pxe-http-files".to_string(),
)),
files: vec![],
};
harmony_tui::run(
inventory,

View File

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContent {
pub path: FilePath,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilePath {
Relative(String),
Absolute(String),
}
impl std::fmt::Display for FilePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FilePath::Relative(path) => f.write_fmt(format_args!("./{path}")),
FilePath::Absolute(path) => f.write_fmt(format_args!("/{path}")),
}
}
}

View File

@ -1,4 +1,6 @@
mod id;
mod version;
mod file;
pub use id::*;
pub use version::*;
pub use file::*;

View File

@ -4,7 +4,9 @@ use harmony_types::net::MacAddress;
use log::debug;
use log::info;
use crate::data::FileContent;
use crate::executors::ExecutorError;
use crate::topology::PxeOptions;
use super::DHCPStaticEntry;
use super::DhcpServer;
@ -155,12 +157,10 @@ impl DhcpServer for HAClusterTopology {
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
self.dhcp_server.list_static_mappings().await
}
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
self.dhcp_server.set_next_server(ip).await
}
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_boot_filename(boot_filename).await
async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
self.dhcp_server.set_pxe_options(options).await
}
fn get_ip(&self) -> IpAddress {
self.dhcp_server.get_ip()
}
@ -170,16 +170,6 @@ impl DhcpServer for HAClusterTopology {
async fn commit_config(&self) -> Result<(), ExecutorError> {
self.dhcp_server.commit_config().await
}
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filename(filename).await
}
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filename64(filename64).await
}
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
self.dhcp_server.set_filenameipxe(filenameipxe).await
}
}
#[async_trait]
@ -223,17 +213,21 @@ impl HttpServer for HAClusterTopology {
self.http_server.serve_files(url).await
}
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
self.http_server.serve_file_content(file).await
}
fn get_ip(&self) -> IpAddress {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
self.http_server.get_ip()
}
async fn ensure_initialized(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
self.http_server.ensure_initialized().await
}
async fn commit_config(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
self.http_server.commit_config().await
}
async fn reload_restart(&self) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
self.http_server.reload_restart().await
}
}
@ -301,19 +295,7 @@ impl DhcpServer for DummyInfra {
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_next_server(&self, _ip: IpAddress) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_boot_filename(&self, _boot_filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filename(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filename64(&self, _filename: &str) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn set_filenameipxe(&self, _filenameipxe: &str) -> Result<(), ExecutorError> {
async fn set_pxe_options(&self, _options: PxeOptions) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
fn get_ip(&self) -> IpAddress {
@ -383,6 +365,9 @@ impl HttpServer for DummyInfra {
async fn serve_files(&self, _url: &Url) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
async fn serve_file_content(&self, _file: &FileContent) -> Result<(), ExecutorError> {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}
fn get_ip(&self) -> IpAddress {
unimplemented!("{}", UNIMPLEMENTED_DUMMY_INFRA)
}

View File

@ -1,4 +1,4 @@
use crate::executors::ExecutorError;
use crate::{data::FileContent, executors::ExecutorError};
use async_trait::async_trait;
use super::{IpAddress, Url};
@ -6,6 +6,7 @@ use super::{IpAddress, Url};
#[async_trait]
pub trait HttpServer: Send + Sync {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>;
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress;
// async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>;

View File

@ -46,16 +46,19 @@ pub trait K8sclient: Send + Sync {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
}
pub struct PxeOptions {
pub ipxe_filename: String,
pub bios_filename: String,
pub efi_filename: String,
pub tftp_ip: Option<IpAddress>,
}
#[async_trait]
pub trait DhcpServer: Send + Sync + std::fmt::Debug {
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>;
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError>;
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError>;
async fn set_filename64(&self, filename64: &str) -> Result<(), ExecutorError>;
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError>;
async fn set_pxe_options(&self, pxe_options: PxeOptions) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress;
fn get_host(&self) -> LogicalHost;
async fn commit_config(&self) -> Result<(), ExecutorError>;

View File

@ -1,10 +1,10 @@
use async_trait::async_trait;
use harmony_types::net::MacAddress;
use log::debug;
use log::{debug, info};
use crate::{
executors::ExecutorError,
topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost},
topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost, PxeOptions},
};
use super::OPNSenseFirewall;
@ -26,7 +26,7 @@ impl DhcpServer for OPNSenseFirewall {
.unwrap();
}
debug!("Registered {:?}", entry);
info!("Registered {:?}", entry);
Ok(())
}
@ -46,57 +46,25 @@ impl DhcpServer for OPNSenseFirewall {
self.host.clone()
}
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError> {
let ipv4 = match ip {
std::net::IpAddr::V4(ipv4_addr) => ipv4_addr,
std::net::IpAddr::V6(_) => todo!("ipv6 not supported yet"),
};
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_next_server(ipv4);
debug!("OPNsense dhcp server set next server {ipv4}");
}
Ok(())
}
async fn set_boot_filename(&self, boot_filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_boot_filename(boot_filename);
debug!("OPNsense dhcp server set boot filename {boot_filename}");
}
Ok(())
}
async fn set_filename(&self, filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filename(filename);
debug!("OPNsense dhcp server set filename {filename}");
}
Ok(())
}
async fn set_filename64(&self, filename: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filename64(filename);
debug!("OPNsense dhcp server set filename {filename}");
}
Ok(())
}
async fn set_filenameipxe(&self, filenameipxe: &str) -> Result<(), ExecutorError> {
{
let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense.dhcp().set_filenameipxe(filenameipxe);
debug!("OPNsense dhcp server set filenameipxe {filenameipxe}");
}
Ok(())
async fn set_pxe_options(&self, options: PxeOptions) -> Result<(), ExecutorError> {
let mut writable_opnsense = self.opnsense_config.write().await;
let PxeOptions {
ipxe_filename,
bios_filename,
efi_filename,
tftp_ip,
} = options;
writable_opnsense
.dhcp()
.set_pxe_options(
tftp_ip.map(|i| i.to_string()),
bios_filename,
efi_filename,
ipxe_filename,
)
.await
.map_err(|dhcp_error| {
ExecutorError::UnexpectedError(format!("Failed to set_pxe_options : {dhcp_error}"))
})
}
}

View File

@ -2,23 +2,23 @@ use async_trait::async_trait;
use log::info;
use crate::{
data::FileContent,
executors::ExecutorError,
topology::{HttpServer, IpAddress, Url},
};
use super::OPNSenseFirewall;
const OPNSENSE_HTTP_ROOT_PATH: &str = "/usr/local/http";
#[async_trait]
impl HttpServer for OPNSenseFirewall {
async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> {
let http_root_path = "/usr/local/http";
let config = self.opnsense_config.read().await;
info!("Uploading files from url {url} to {http_root_path}");
info!("Uploading files from url {url} to {OPNSENSE_HTTP_ROOT_PATH}");
match url {
Url::LocalFolder(path) => {
config
.upload_files(path, http_root_path)
.upload_files(path, OPNSENSE_HTTP_ROOT_PATH)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
}
@ -27,8 +27,29 @@ impl HttpServer for OPNSenseFirewall {
Ok(())
}
async fn serve_file_content(&self, file: &FileContent) -> Result<(), ExecutorError> {
let path = match &file.path {
crate::data::FilePath::Relative(path) => {
format!("{OPNSENSE_HTTP_ROOT_PATH}/{}", path.to_string())
}
crate::data::FilePath::Absolute(path) => {
return Err(ExecutorError::ConfigurationError(format!(
"Cannot serve file from http server with absolute path : {path}"
)));
}
};
let config = self.opnsense_config.read().await;
info!("Uploading file content to {}", path);
config
.upload_file_content(&path, &file.content)
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?;
Ok(())
}
fn get_ip(&self) -> IpAddress {
todo!();
OPNSenseFirewall::get_ip(self)
}
async fn commit_config(&self) -> Result<(), ExecutorError> {

View File

@ -28,7 +28,7 @@ impl TftpServer for OPNSenseFirewall {
}
fn get_ip(&self) -> IpAddress {
todo!()
OPNSenseFirewall::get_ip(self)
}
async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> {

View File

@ -7,7 +7,7 @@ use crate::{
domain::{data::Version, interpret::InterpretStatus},
interpret::{Interpret, InterpretError, InterpretName, Outcome},
inventory::Inventory,
topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, Topology},
topology::{DHCPStaticEntry, DhcpServer, HostBinding, IpAddress, PxeOptions, Topology},
};
use crate::domain::score::Score;
@ -98,69 +98,14 @@ impl DhcpInterpret {
_inventory: &Inventory,
dhcp_server: &D,
) -> Result<Outcome, InterpretError> {
let next_server_outcome = match self.score.next_server {
Some(next_server) => {
dhcp_server.set_next_server(next_server).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set next boot to {next_server}"),
)
}
None => Outcome::noop(),
let pxe_options = PxeOptions {
ipxe_filename: self.score.filenameipxe.clone().unwrap_or_default(),
bios_filename: self.score.filename.clone().unwrap_or_default(),
efi_filename: self.score.filename64.clone().unwrap_or_default(),
tftp_ip: self.score.next_server,
};
let boot_filename_outcome = match &self.score.boot_filename {
Some(boot_filename) => {
dhcp_server.set_boot_filename(boot_filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set boot filename to {boot_filename}"),
)
}
None => Outcome::noop(),
};
let filename_outcome = match &self.score.filename {
Some(filename) => {
dhcp_server.set_filename(filename).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename to {filename}"),
)
}
None => Outcome::noop(),
};
let filename64_outcome = match &self.score.filename64 {
Some(filename64) => {
dhcp_server.set_filename64(filename64).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filename64 to {filename64}"),
)
}
None => Outcome::noop(),
};
let filenameipxe_outcome = match &self.score.filenameipxe {
Some(filenameipxe) => {
dhcp_server.set_filenameipxe(filenameipxe).await?;
Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set filenameipxe to {filenameipxe}"),
)
}
None => Outcome::noop(),
};
if next_server_outcome.status == InterpretStatus::NOOP
&& boot_filename_outcome.status == InterpretStatus::NOOP
&& filename_outcome.status == InterpretStatus::NOOP
&& filename64_outcome.status == InterpretStatus::NOOP
&& filenameipxe_outcome.status == InterpretStatus::NOOP
{
return Ok(Outcome::noop());
}
dhcp_server.set_pxe_options(pxe_options).await?;
Ok(Outcome::new(
InterpretStatus::SUCCESS,

View File

@ -3,7 +3,7 @@ use derive_new::new;
use serde::Serialize;
use crate::{
data::{Id, Version},
data::{FileContent, Id, Version},
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
inventory::Inventory,
score::Score,
@ -23,7 +23,8 @@ use crate::{
/// ```
#[derive(Debug, new, Clone, Serialize)]
pub struct StaticFilesHttpScore {
files_to_serve: Url,
pub folder_to_serve: Option<Url>,
pub files: Vec<FileContent>,
}
impl<T: Topology + HttpServer> Score<T> for StaticFilesHttpScore {
@ -50,12 +51,20 @@ impl<T: Topology + HttpServer> Interpret<T> for StaticFilesHttpInterpret {
) -> Result<Outcome, InterpretError> {
http_server.ensure_initialized().await?;
// http_server.set_ip(topology.router.get_gateway()).await?;
http_server.serve_files(&self.score.files_to_serve).await?;
if let Some(folder) = self.score.folder_to_serve.as_ref() {
http_server.serve_files(folder).await?;
}
for f in self.score.files.iter() {
http_server.serve_file_content(&f).await?
}
http_server.commit_config().await?;
http_server.reload_restart().await?;
Ok(Outcome::success(format!(
"Http Server running and serving files from {}",
self.score.files_to_serve
"Http Server running and serving files from folder {:?} and content for {}",
self.score.folder_to_serve,
self.score.files.iter().map(|f| f.path.to_string()).collect::<Vec<String>>().join(",")
)))
}

View File

@ -30,15 +30,15 @@ pub struct CaddyGeneral {
#[yaserde(rename = "TlsDnsApiKey")]
pub tls_dns_api_key: MaybeString,
#[yaserde(rename = "TlsDnsSecretApiKey")]
pub tls_dns_secret_api_key: MaybeString,
pub tls_dns_secret_api_key: Option<MaybeString>,
#[yaserde(rename = "TlsDnsOptionalField1")]
pub tls_dns_optional_field1: MaybeString,
pub tls_dns_optional_field1: Option<MaybeString>,
#[yaserde(rename = "TlsDnsOptionalField2")]
pub tls_dns_optional_field2: MaybeString,
pub tls_dns_optional_field2: Option<MaybeString>,
#[yaserde(rename = "TlsDnsOptionalField3")]
pub tls_dns_optional_field3: MaybeString,
pub tls_dns_optional_field3: Option<MaybeString>,
#[yaserde(rename = "TlsDnsOptionalField4")]
pub tls_dns_optional_field4: MaybeString,
pub tls_dns_optional_field4: Option<MaybeString>,
#[yaserde(rename = "TlsDnsPropagationTimeout")]
pub tls_dns_propagation_timeout: Option<MaybeString>,
#[yaserde(rename = "TlsDnsPropagationTimeoutPeriod")]
@ -47,6 +47,8 @@ pub struct CaddyGeneral {
pub tls_dns_propagation_delay: Option<MaybeString>,
#[yaserde(rename = "TlsDnsPropagationResolvers")]
pub tls_dns_propagation_resolvers: MaybeString,
#[yaserde(rename = "TlsDnsEchDomain")]
pub tls_dns_ech_domain: MaybeString,
pub accesslist: MaybeString,
#[yaserde(rename = "DisableSuperuser")]
pub disable_superuser: Option<i32>,
@ -56,6 +58,10 @@ pub struct CaddyGeneral {
pub http_version: Option<MaybeString>,
#[yaserde(rename = "HttpVersions")]
pub http_versions: Option<MaybeString>,
pub timeout_read_body: Option<MaybeString>,
pub timeout_read_header: Option<MaybeString>,
pub timeout_write: Option<MaybeString>,
pub timeout_idle: Option<MaybeString>,
#[yaserde(rename = "LogCredentials")]
pub log_credentials: MaybeString,
#[yaserde(rename = "LogAccessPlain")]

View File

@ -1,4 +1,4 @@
use yaserde::MaybeString;
use yaserde::{MaybeString, RawXml};
use yaserde_derive::{YaDeserialize, YaSerialize};
// This is the top-level struct that represents the entire <dnsmasq> element.
@ -35,6 +35,7 @@ pub struct DnsMasq {
pub dhcp_ranges: Vec<DhcpRange>,
pub dhcp_options: Vec<DhcpOptions>,
pub dhcp_boot: Vec<DhcpBoot>,
pub dhcp_tags: Vec<RawXml>,
}
// Represents the <dhcp> element and its nested fields.
@ -44,6 +45,7 @@ pub struct Dhcp {
pub no_interface: MaybeString,
pub fqdn: u8,
pub domain: MaybeString,
pub local: Option<MaybeString>,
pub lease_max: MaybeString,
pub authoritative: u8,
pub default_fw_rules: u8,
@ -86,10 +88,10 @@ pub struct DhcpBoot {
pub uuid: String,
pub interface: MaybeString,
pub tag: MaybeString,
pub filename: String,
pub filename: Option<String>,
pub servername: String,
pub address: String,
pub description: String,
pub description: Option<String>,
}
// Represents a single <dhcp_options> element.

View File

@ -1509,7 +1509,7 @@ pub struct Vlans {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Bridges {
pub bridged: MaybeString,
pub bridged: Option<MaybeString>,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]

View File

@ -20,6 +20,7 @@ russh-sftp = "2.0.6"
serde_json = "1.0.133"
tokio-util = { version = "0.7.13", features = ["codec"] }
tokio-stream = "0.1.17"
uuid.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@ -70,6 +70,10 @@ impl Config {
self.shell.upload_folder(source, destination).await
}
pub async fn upload_file_content(&self, path: &str, content: &str) -> Result<String, Error> {
self.shell.write_content_to_file(content, path).await
}
/// Checks in config file if system.firmware.plugins csv field contains the specified package
/// name.
///

View File

@ -9,6 +9,7 @@ use crate::Error;
pub trait OPNsenseShell: std::fmt::Debug + Send + Sync {
async fn exec(&self, command: &str) -> Result<String, Error>;
async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error>;
async fn write_content_to_file(&self, content: &str, filename: &str) -> Result<String, Error>;
async fn upload_folder(&self, source: &str, destination: &str) -> Result<String, Error>;
}
@ -25,6 +26,14 @@ impl OPNsenseShell for DummyOPNSenseShell {
async fn write_content_to_temp_file(&self, _content: &str) -> Result<String, Error> {
unimplemented!("This is a dummy implementation");
}
async fn write_content_to_file(
&self,
_content: &str,
_filename: &str,
) -> Result<String, Error> {
unimplemented!("This is a dummy implementation");
}
async fn upload_folder(&self, _source: &str, _destination: &str) -> Result<String, Error> {
unimplemented!("This is a dummy implementation");
}

View File

@ -44,6 +44,11 @@ impl OPNsenseShell for SshOPNSenseShell {
.unwrap()
.as_millis()
);
self.write_content_to_file(content, &temp_filename).await
}
async fn write_content_to_file(&self, content: &str, filename: &str) -> Result<String, Error> {
// TODO fix this to create the directory before uploading the file
let channel = self.get_ssh_channel().await?;
channel
.request_subsystem(true, "sftp")
@ -53,10 +58,10 @@ impl OPNsenseShell for SshOPNSenseShell {
.await
.expect("Should acquire sftp subsystem");
let mut file = sftp.create(&temp_filename).await.unwrap();
let mut file = sftp.create(filename).await.unwrap();
file.write_all(content.as_bytes()).await?;
Ok(temp_filename)
Ok(filename.to_string())
}
async fn upload_folder(&self, source: &str, destination: &str) -> Result<String, Error> {

View File

@ -5,6 +5,7 @@ pub enum DhcpError {
IpAddressAlreadyMapped(String),
MacAddressAlreadyMapped(String),
IpAddressOutOfRange(String),
Configuration(String),
}
impl std::fmt::Display for DhcpError {
@ -21,6 +22,7 @@ impl std::fmt::Display for DhcpError {
DhcpError::IpAddressOutOfRange(ip) => {
write!(f, "IP address {} is out of interface range", ip)
}
DhcpError::Configuration(msg) => f.write_str(&msg),
}
}
}

View File

@ -1,9 +1,11 @@
// dnsmasq.rs
use crate::modules::dhcp::DhcpError;
use log::info;
use opnsense_config_xml::MaybeString;
use opnsense_config_xml::StaticMap;
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;
@ -15,6 +17,8 @@ pub struct DhcpConfigDnsMasq<'a> {
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 {
@ -23,47 +27,172 @@ impl<'a> DhcpConfigDnsMasq<'a> {
}
}
/// 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) {
todo!()
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 {
todo!()
&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> {
todo!()
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> {
todo!()
}
pub fn enable_netboot(&mut self) {
todo!()
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 fn set_next_server(&mut self, ip: Ipv4Addr) {
todo!()
}
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}"));
pub fn set_boot_filename(&mut self, boot_filename: &str) {
todo!()
}
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
pub fn set_filename(&mut self, filename: &str) {
todo!()
}
# Provide ipxe efi file to uefi but NOT ipxe clients
dhcp-boot=tag:efi,tag:!ipxe,{efi_filename}{tftp_str}
pub fn set_filename64(&mut self, filename64: &str) {
todo!()
}
# Provide ipxe boot script to ipxe clients
dhcp-boot=tag:ipxe,{ipxe_filename}{tftp_str}
pub fn set_filenameipxe(&mut self, filenameipxe: &str) {
todo!()
# 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(())
}
}

View File

@ -0,0 +1,896 @@
<?xml version="1.0"?>
<opnsense>
<theme>opnsense</theme>
<sysctl>
<item/>
</sysctl>
<system>
<serialspeed>115200</serialspeed>
<primaryconsole>serial</primaryconsole>
<optimization>normal</optimization>
<hostname>OPNsense</hostname>
<domain>testpxe.harmony.mcd</domain>
<group>
<name>admins</name>
<description>System Administrators</description>
<scope>system</scope>
<gid>1999</gid>
<member>0</member>
<priv>page-all</priv>
<source_networks/>
</group>
<user>
<name>root</name>
<descr>System Administrator</descr>
<scope>system</scope>
<password>$2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS</password>
<pwd_changed_at/>
<uid>0</uid>
<disabled>0</disabled>
<landing_page/>
<comment/>
<email/>
<apikeys/>
<priv/>
<language/>
<expires/>
<authorizedkeys/>
<dashboard/>
<otp_seed/>
<shell/>
</user>
<timezone>Etc/UTC</timezone>
<timeservers>0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org</timeservers>
<webgui>
<protocol>https</protocol>
<ssl-certref>68a72b6f7f776</ssl-certref>
<port/>
<ssl-ciphers/>
<interfaces/>
<compression/>
</webgui>
<usevirtualterminal>1</usevirtualterminal>
<disablenatreflection>yes</disablenatreflection>
<disableconsolemenu>1</disableconsolemenu>
<disablevlanhwfilter>1</disablevlanhwfilter>
<disablechecksumoffloading>1</disablechecksumoffloading>
<disablesegmentationoffloading>1</disablesegmentationoffloading>
<disablelargereceiveoffloading>1</disablelargereceiveoffloading>
<ipv6allow>1</ipv6allow>
<powerd_ac_mode>hadp</powerd_ac_mode>
<powerd_battery_mode>hadp</powerd_battery_mode>
<powerd_normal_mode>hadp</powerd_normal_mode>
<bogons>
<interval>monthly</interval>
</bogons>
<pf_share_forward>1</pf_share_forward>
<lb_use_sticky>1</lb_use_sticky>
<ssh>
<group>admins</group>
<noauto>1</noauto>
<interfaces/>
<kex/>
<ciphers/>
<macs/>
<keys/>
<enabled>enabled</enabled>
<passwordauth>1</passwordauth>
<keysig/>
<permitrootlogin>1</permitrootlogin>
<rekeylimit/>
</ssh>
<rrdbackup>-1</rrdbackup>
<netflowbackup>-1</netflowbackup>
<firmware version="1.0.1">
<mirror/>
<flavour/>
<plugins>os-tftp</plugins>
<type/>
<subscription/>
<reboot>0</reboot>
</firmware>
<language>en_US</language>
<dnsserver/>
<dnsallowoverride>1</dnsallowoverride>
<dnsallowoverride_exclude/>
</system>
<interfaces>
<wan>
<if>vtnet0</if>
<mtu/>
<enable>1</enable>
<spoofmac/>
<mss/>
<ipaddr>dhcp</ipaddr>
<dhcphostname/>
<blockpriv>0</blockpriv>
<blockbogons>1</blockbogons>
<subnet/>
<ipaddrv6>dhcp6</ipaddrv6>
<dhcp6-ia-pd-len>0</dhcp6-ia-pd-len>
<gateway/>
<media/>
<mediaopt/>
</wan>
<lan>
<if>vtnet1</if>
<enable>1</enable>
<ipaddr>192.168.1.1</ipaddr>
<subnet>24</subnet>
<ipaddrv6>track6</ipaddrv6>
<subnetv6>64</subnetv6>
<media/>
<mediaopt/>
<track6-interface>wan</track6-interface>
<track6-prefix-id>0</track6-prefix-id>
</lan>
<lo0>
<internal_dynamic>1</internal_dynamic>
<if>lo0</if>
<descr>Loopback</descr>
<enable>1</enable>
<ipaddr>127.0.0.1</ipaddr>
<type>none</type>
<virtual>1</virtual>
<subnet>8</subnet>
<ipaddrv6>::1</ipaddrv6>
<subnetv6>128</subnetv6>
</lo0>
</interfaces>
<dhcpd/>
<snmpd>
<syslocation/>
<syscontact/>
<rocommunity>public</rocommunity>
</snmpd>
<syslog/>
<nat>
<outbound>
<mode>automatic</mode>
</outbound>
</nat>
<filter>
<rule>
<type>pass</type>
<interface>lan</interface>
<ipprotocol>inet</ipprotocol>
<descr>Default allow LAN to any rule</descr>
<source>
<network>lan</network>
</source>
<destination>
<any/>
</destination>
</rule>
<rule>
<type>pass</type>
<interface>lan</interface>
<ipprotocol>inet6</ipprotocol>
<descr>Default allow LAN IPv6 to any rule</descr>
<source>
<network>lan</network>
</source>
<destination>
<any/>
</destination>
</rule>
</filter>
<rrd>
<enable/>
</rrd>
<ntpd>
<prefer>0.opnsense.pool.ntp.org</prefer>
</ntpd>
<revision>
<username>root@192.168.1.5</username>
<description>/api/dnsmasq/settings/set made changes</description>
<time>1755800176.40</time>
</revision>
<OPNsense>
<captiveportal version="1.0.4">
<zones/>
<templates/>
</captiveportal>
<cron version="1.0.4">
<jobs/>
</cron>
<Netflow version="1.0.1">
<capture>
<interfaces/>
<egress_only/>
<version>v9</version>
<targets/>
</capture>
<collect>
<enable>0</enable>
</collect>
<activeTimeout>1800</activeTimeout>
<inactiveTimeout>15</inactiveTimeout>
</Netflow>
<Firewall>
<Lvtemplate version="0.0.1">
<templates/>
</Lvtemplate>
<Category version="1.0.0">
<categories/>
</Category>
<Filter version="1.0.4">
<rules/>
<snatrules/>
<npt/>
<onetoone/>
</Filter>
<Alias version="1.0.1">
<geoip>
<url/>
</geoip>
<aliases/>
</Alias>
</Firewall>
<IDS version="1.1.0">
<rules/>
<policies/>
<userDefinedRules/>
<files/>
<fileTags/>
<general>
<enabled>0</enabled>
<ips>0</ips>
<promisc>0</promisc>
<interfaces>wan</interfaces>
<homenet>192.168.0.0/16,10.0.0.0/8,172.16.0.0/12</homenet>
<defaultPacketSize/>
<UpdateCron/>
<AlertLogrotate>W0D23</AlertLogrotate>
<AlertSaveLogs>4</AlertSaveLogs>
<MPMAlgo/>
<detect>
<Profile/>
<toclient_groups/>
<toserver_groups/>
</detect>
<syslog>0</syslog>
<syslog_eve>0</syslog_eve>
<LogPayload>0</LogPayload>
<verbosity/>
<eveLog>
<http>
<enable>0</enable>
<extended>0</extended>
<dumpAllHeaders/>
</http>
<tls>
<enable>0</enable>
<extended>0</extended>
<sessionResumption>0</sessionResumption>
<custom/>
</tls>
</eveLog>
</general>
</IDS>
<IPsec version="1.0.4">
<general>
<enabled/>
<preferred_oldsa>0</preferred_oldsa>
<disablevpnrules>0</disablevpnrules>
<passthrough_networks/>
<user_source/>
<local_group/>
</general>
<keyPairs/>
<preSharedKeys/>
<charon>
<max_ikev1_exchanges/>
<threads>16</threads>
<ikesa_table_size>32</ikesa_table_size>
<ikesa_table_segments>4</ikesa_table_segments>
<init_limit_half_open>1000</init_limit_half_open>
<ignore_acquire_ts>1</ignore_acquire_ts>
<install_routes>0</install_routes>
<cisco_unity>0</cisco_unity>
<make_before_break>0</make_before_break>
<retransmit_tries/>
<retransmit_timeout/>
<retransmit_base/>
<retransmit_jitter/>
<retransmit_limit/>
<syslog>
<daemon>
<ike_name>1</ike_name>
<log_level>0</log_level>
<app>1</app>
<asn>1</asn>
<cfg>1</cfg>
<chd>1</chd>
<dmn>1</dmn>
<enc>1</enc>
<esp>1</esp>
<ike>1</ike>
<imc>1</imc>
<imv>1</imv>
<job>1</job>
<knl>1</knl>
<lib>1</lib>
<mgr>1</mgr>
<net>1</net>
<pts>1</pts>
<tls>1</tls>
<tnc>1</tnc>
</daemon>
</syslog>
<plugins>
<attr>
<subnet/>
<split-include/>
<x_28674/>
<x_28675/>
<x_28672/>
<x_28673>0</x_28673>
<x_28679/>
<dns/>
<nbns/>
</attr>
<eap-radius>
<servers/>
<accounting>0</accounting>
<class_group>0</class_group>
</eap-radius>
<xauth-pam>
<pam_service>ipsec</pam_service>
<session>0</session>
<trim_email>1</trim_email>
</xauth-pam>
</plugins>
</charon>
</IPsec>
<Interfaces>
<vxlans version="1.0.2"/>
<loopbacks version="1.0.0"/>
<neighbors version="1.0.0"/>
</Interfaces>
<Kea>
<dhcp4 version="1.0.4" persisted_at="1755786069.95">
<general>
<enabled>0</enabled>
<manual_config>0</manual_config>
<interfaces/>
<valid_lifetime>4000</valid_lifetime>
<fwrules>1</fwrules>
<dhcp_socket_type>raw</dhcp_socket_type>
</general>
<ha>
<enabled>0</enabled>
<this_server_name/>
<max_unacked_clients>2</max_unacked_clients>
</ha>
<subnets/>
<reservations/>
<ha_peers/>
</dhcp4>
<ctrl_agent version="0.0.1" persisted_at="1755786069.95">
<general>
<enabled>0</enabled>
<http_host>127.0.0.1</http_host>
<http_port>8000</http_port>
</general>
</ctrl_agent>
<dhcp6 version="1.0.0" persisted_at="1755786069.95">
<general>
<enabled>0</enabled>
<manual_config>0</manual_config>
<interfaces/>
<valid_lifetime>4000</valid_lifetime>
<fwrules>1</fwrules>
</general>
<ha>
<enabled>0</enabled>
<this_server_name/>
<max_unacked_clients>2</max_unacked_clients>
</ha>
<subnets/>
<reservations/>
<pd_pools/>
<ha_peers/>
</dhcp6>
</Kea>
<monit version="1.0.13">
<general>
<enabled>0</enabled>
<interval>120</interval>
<startdelay>120</startdelay>
<mailserver>127.0.0.1</mailserver>
<port>25</port>
<username/>
<password/>
<ssl>0</ssl>
<sslversion>auto</sslversion>
<sslverify>1</sslverify>
<logfile/>
<statefile/>
<eventqueuePath/>
<eventqueueSlots/>
<httpdEnabled>0</httpdEnabled>
<httpdUsername>root</httpdUsername>
<httpdPassword/>
<httpdPort>2812</httpdPort>
<httpdAllow/>
<mmonitUrl/>
<mmonitTimeout>5</mmonitTimeout>
<mmonitRegisterCredentials>1</mmonitRegisterCredentials>
</general>
<alert uuid="ce8ca7d9-66ab-41d5-acea-598f4803e8ba">
<enabled>0</enabled>
<recipient>root@localhost.local</recipient>
<noton>0</noton>
<events/>
<format/>
<reminder/>
<description/>
</alert>
<service uuid="dc3b9298-4a56-4c45-bd61-be2fdb103383">
<enabled>1</enabled>
<name>$HOST</name>
<description/>
<type>system</type>
<pidfile/>
<match/>
<path/>
<timeout>300</timeout>
<starttimeout>30</starttimeout>
<address/>
<interface/>
<start/>
<stop/>
<tests>cfed35dc-f74b-417d-9ed9-682c5de96495,f961277a-07f1-49a4-90ee-bb15738d9ebb,30b2cce2-f650-4e44-a3e2-ee53886cda3f,3c86136f-35a4-4126-865b-82732c6542d9</tests>
<depends/>
<polltime/>
</service>
<service uuid="b4d5bdb4-206d-4afe-8d86-377ffbbdb2ec">
<enabled>1</enabled>
<name>RootFs</name>
<description/>
<type>filesystem</type>
<pidfile/>
<match/>
<path>/</path>
<timeout>300</timeout>
<starttimeout>30</starttimeout>
<address/>
<interface/>
<start/>
<stop/>
<tests>fbb8dfe2-b9ad-4730-a0f3-41d7ecda6289</tests>
<depends/>
<polltime/>
</service>
<service uuid="f96e3cbb-6c98-4d20-8337-bab717d4ab54">
<enabled>0</enabled>
<name>carp_status_change</name>
<description/>
<type>custom</type>
<pidfile/>
<match/>
<path>/usr/local/opnsense/scripts/OPNsense/Monit/carp_status</path>
<timeout>300</timeout>
<starttimeout>30</starttimeout>
<address/>
<interface/>
<start/>
<stop/>
<tests>11ceca8a-dff8-45e0-9dc5-ed80dc4b3947</tests>
<depends/>
<polltime/>
</service>
<service uuid="69bbd4d5-3a50-42a7-ab64-050450504038">
<enabled>0</enabled>
<name>gateway_alert</name>
<description/>
<type>custom</type>
<pidfile/>
<match/>
<path>/usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert</path>
<timeout>300</timeout>
<starttimeout>30</starttimeout>
<address/>
<interface/>
<start/>
<stop/>
<tests>fad1f465-4a92-4b93-be66-59d7059b8779</tests>
<depends/>
<polltime/>
</service>
<test uuid="2bd5d8c0-6a4a-430b-b953-34214a107ccf">
<name>Ping</name>
<type>NetworkPing</type>
<condition>failed ping</condition>
<action>alert</action>
<path/>
</test>
<test uuid="0f06ffff-9bfa-463d-b75e-f7195cd8dcab">
<name>NetworkLink</name>
<type>NetworkInterface</type>
<condition>failed link</condition>
<action>alert</action>
<path/>
</test>
<test uuid="79b119ce-10e0-4a6a-bd1a-b0be371d0fd7">
<name>NetworkSaturation</name>
<type>NetworkInterface</type>
<condition>saturation is greater than 75%</condition>
<action>alert</action>
<path/>
</test>
<test uuid="cfed35dc-f74b-417d-9ed9-682c5de96495">
<name>MemoryUsage</name>
<type>SystemResource</type>
<condition>memory usage is greater than 75%</condition>
<action>alert</action>
<path/>
</test>
<test uuid="f961277a-07f1-49a4-90ee-bb15738d9ebb">
<name>CPUUsage</name>
<type>SystemResource</type>
<condition>cpu usage is greater than 75%</condition>
<action>alert</action>
<path/>
</test>
<test uuid="30b2cce2-f650-4e44-a3e2-ee53886cda3f">
<name>LoadAvg1</name>
<type>SystemResource</type>
<condition>loadavg (1min) is greater than 4</condition>
<action>alert</action>
<path/>
</test>
<test uuid="3c86136f-35a4-4126-865b-82732c6542d9">
<name>LoadAvg5</name>
<type>SystemResource</type>
<condition>loadavg (5min) is greater than 3</condition>
<action>alert</action>
<path/>
</test>
<test uuid="27e91c6f-3e8e-4570-bb3a-27f46dd301a7">
<name>LoadAvg15</name>
<type>SystemResource</type>
<condition>loadavg (15min) is greater than 2</condition>
<action>alert</action>
<path/>
</test>
<test uuid="fbb8dfe2-b9ad-4730-a0f3-41d7ecda6289">
<name>SpaceUsage</name>
<type>SpaceUsage</type>
<condition>space usage is greater than 75%</condition>
<action>alert</action>
<path/>
</test>
<test uuid="11ceca8a-dff8-45e0-9dc5-ed80dc4b3947">
<name>ChangedStatus</name>
<type>ProgramStatus</type>
<condition>changed status</condition>
<action>alert</action>
<path/>
</test>
<test uuid="fad1f465-4a92-4b93-be66-59d7059b8779">
<name>NonZeroStatus</name>
<type>ProgramStatus</type>
<condition>status != 0</condition>
<action>alert</action>
<path/>
</test>
</monit>
<OpenVPNExport version="0.0.1">
<servers/>
</OpenVPNExport>
<Syslog version="1.0.2">
<general>
<enabled>1</enabled>
<loglocal>1</loglocal>
<maxpreserve>31</maxpreserve>
<maxfilesize/>
</general>
<destinations/>
</Syslog>
<TrafficShaper version="1.0.3" persisted_at="1755786069.77">
<pipes/>
<queues/>
<rules/>
</TrafficShaper>
<unboundplus version="1.0.12">
<general>
<enabled>1</enabled>
<port>53</port>
<stats>0</stats>
<active_interface/>
<dnssec>0</dnssec>
<dns64>0</dns64>
<dns64prefix/>
<noarecords>0</noarecords>
<regdhcp>0</regdhcp>
<regdhcpdomain/>
<regdhcpstatic>0</regdhcpstatic>
<noreglladdr6>0</noreglladdr6>
<noregrecords>0</noregrecords>
<txtsupport>0</txtsupport>
<cacheflush>0</cacheflush>
<local_zone_type>transparent</local_zone_type>
<outgoing_interface/>
<enable_wpad>0</enable_wpad>
</general>
<advanced>
<hideidentity>0</hideidentity>
<hideversion>0</hideversion>
<prefetch>0</prefetch>
<prefetchkey>0</prefetchkey>
<dnssecstripped>0</dnssecstripped>
<aggressivensec>1</aggressivensec>
<serveexpired>0</serveexpired>
<serveexpiredreplyttl/>
<serveexpiredttl/>
<serveexpiredttlreset>0</serveexpiredttlreset>
<serveexpiredclienttimeout/>
<qnameminstrict>0</qnameminstrict>
<extendedstatistics>0</extendedstatistics>
<logqueries>0</logqueries>
<logreplies>0</logreplies>
<logtagqueryreply>0</logtagqueryreply>
<logservfail>0</logservfail>
<loglocalactions>0</loglocalactions>
<logverbosity>1</logverbosity>
<valloglevel>0</valloglevel>
<privatedomain/>
<privateaddress>0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.0.2.0/24,192.168.0.0/16,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,233.252.0.0/24,::1/128,2001:db8::/32,fc00::/8,fd00::/8,fe80::/10</privateaddress>
<insecuredomain/>
<msgcachesize/>
<rrsetcachesize/>
<outgoingnumtcp/>
<incomingnumtcp/>
<numqueriesperthread/>
<outgoingrange/>
<jostletimeout/>
<discardtimeout/>
<cachemaxttl/>
<cachemaxnegativettl/>
<cacheminttl/>
<infrahostttl/>
<infrakeepprobing>0</infrakeepprobing>
<infracachenumhosts/>
<unwantedreplythreshold/>
</advanced>
<acls>
<default_action>allow</default_action>
</acls>
<dnsbl>
<enabled>0</enabled>
<safesearch>0</safesearch>
<type/>
<lists/>
<whitelists/>
<blocklists/>
<wildcards/>
<address/>
<nxdomain>0</nxdomain>
</dnsbl>
<forwarding>
<enabled>0</enabled>
</forwarding>
<dots/>
<hosts/>
<aliases/>
</unboundplus>
<DHCRelay version="1.0.1" persisted_at="1755786069.97"/>
<trust>
<general version="1.0.1" persisted_at="1755786070.08">
<store_intermediate_certs>0</store_intermediate_certs>
<install_crls>0</install_crls>
<fetch_crls>0</fetch_crls>
<enable_legacy_sect>1</enable_legacy_sect>
<enable_config_constraints>0</enable_config_constraints>
<CipherString/>
<Ciphersuites/>
<SignatureAlgorithms/>
<groups/>
<MinProtocol/>
<MinProtocol_DTLS/>
</general>
</trust>
<tftp>
<general version="0.0.1">
<enabled>1</enabled>
<listen>192.168.1.1</listen>
</general>
</tftp>
<wireguard>
<general version="0.0.1">
<enabled>0</enabled>
</general>
<server version="1.0.0">
<servers/>
</server>
<client version="1.0.0">
<clients/>
</client>
</wireguard>
<Swanctl version="1.0.0">
<Connections/>
<locals/>
<remotes/>
<children/>
<Pools/>
<VTIs/>
<SPDs/>
</Swanctl>
<OpenVPN version="1.0.1">
<Overwrites/>
<Instances/>
<StaticKeys/>
</OpenVPN>
<Gateways version="1.0.0" persisted_at="1755786217.76"/>
</OPNsense>
<staticroutes version="1.0.0"/>
<ca/>
<cert>
<refid>68a72b6f7f776</refid>
<descr>Web GUI TLS certificate</descr>
<crt>LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVUkZqWUQ0Z1U0bzRNZGdiN2pIc29KNU9GVGFnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qRXhOREl4TXpaYUZ3MHlOakE1TWpJeE5ESXhNelphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFDbENkeFJ3ZWJQQkxvYlVORnYvL2t3TEdKWExweDl6OFFHV2lyWTNpamVDeUxDQ0FwczBLaE1adTNRClhkczMranppbDRnSE96L0hvUEo5Z0xxMy9FYnR4cE9ENWkvQzZwbXc3SGM1M2tTQ3JCK2tlWUFnVWZ1aDU3MzAKZyt3cGc5RDQzaHFBNzF1L3F0ZC95eitnTVJnTWdZMndEK3ZWQWRrdGxVSWlmN2piTmR1RDRGMmdkL0gwbzljWApEUm5zMzNNQVptTkZwajN4QWFwQi9RWnhKV1JMZ1J5K1A5MWcyZEZFNzhNaWY4ZTRNSCtrU29ndzIwVG1JbmpzCitKdEVTc0xQZmx2eUZUa0lkTVdFbURWOG1HUk5hNXVoYXlEbVNEUU9xV0NUTlZHV3ZVWjZTRnJRZ1Q1MDBEdXgKWnRtYlhGdEVqRzlIaGd5SW5QT0dKbWYzTWVzS3dYclVNMW1BenVCRVBFR0lwOTc3UTY2SitpTDYzWTUvTTB3aAphMGVVNGppNTVRQnJOQjlaWjJsa080bGU2TXdmZm50c29JakMrVDh5RW5tbW5nQTlTdWNPRW9CcFFhd3cvRGhOCmtSNGk4TUptR1JNdmpLazlHVzZ3Z2VNVThJVDhKZDRjTmJOVzdFSGpzV08xN1luTVhIMEUxOVZqa2d1R3dIODAKZ3ZROGtzTmV4WVA3WWo0b0VycnRKbWVhWU8wbFVkV0tGektNdS8va0UvNG5HK0h4emlRUnA5QmdyTURNYks4ZgpkM29mY2tqZFZTTW9Vc1FJaWlmdTFMK1I4V1Y3K3hsTzdTWS80dGk3Y28zcjNXRTYyVlE4Vk9QMVphcStWRFpvClNIMVRCa0lTSU5paVJFRzhZSDQvRHJwNWZ2dHBPcERBRGN1TGdDNDJHcExmS1pwVEtRSURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZIdUVQK05yYlorZWdMdWZVSUFKaUo2M1c4SDFNSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVVJGallENGdVNG80TWRnYjcKakhzb0o1T0ZUYWd3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBV2JzM2MwSXYwcEd3Y0wvUmRlbnBiZVJHQ3FsODY0V1ZITEtMZzJIR3BkKytJdmRFcHJEZkZ3SCsKdHdOd2VrZTlXUEtYa20vUkZDWE5DQmVLNjkxeURVWCtCNUJOMjMvSks5N1lzRVdtMURIV3FvSDE1WmdqelZ0QQp2d2JmbnRQdlhCWU1wV2ZQY0Zua0hjN3pxUjI3RzBEZHFUeGg2TjhFenV1S3JRWXFtaWhJUXFkNU9HRVhteW9ZCmdPVjdoZ0lWSUR6a1Z0QkRiS3dFV3VFN2pKYzViMXR4Mk1FUFRsVklEZGo0Zm5vdURWemdkczA2RER4aFM4eXAKbXJOSXhxb045ekEzYXVtTnRNZ2haSHVZRHdjbm5GSnBNZHlJSEdHZ1dlNnZZNHFtdEFSVDd3a0x6MTZnUG9LMAo5bFhVU0RmV3YwUDJGUXFHZTJjaXQ3VVE2ZGtsUWsrVGVtUEFwNnhEV09HR3oxRkdmUUoxN040b3AvOGtlOUo2Cm96RVp3QTh1aDVYTUl2N3loM2dobjV1d1R6RDUyZ1BBZFdaekEyaHVWV3p5cVM0WVc0N3ZkaGV6TTFTUndabVEKUmYzNDk0UVFydWd0bzdycWdMUlRTSXN4WEtkU21MaHZjT0hsSlhISW1XNTRzeFlXNm9NUStpRExFT29ZVVdpcgp1aUJvT1RsNEJaOG5Xcm9pV0JtWlFLaVRPYlFRczVWTkIwYnVybmRISTJVdmtIRTE3QTM0bFYySjY5Q0dNNzJ2CjQ5aE9TN3B2Tzg4cEVKZm90d01YYlRhdkR2WTBHazZxbERFMVBud1U2Wm8ySEprcFdUTUxOSzh1alZ1RkhlMGkKR2JvZi9va08vZW4rUi9PUXNyd1JYbzFwVTRiWnlyWGVQeUdqSSsrdFYzemhjd0IwWjNJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==</crt>
<prv>LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRQ2xDZHhSd2ViUEJMb2IKVU5Gdi8va3dMR0pYTHB4OXo4UUdXaXJZM2lqZUN5TENDQXBzMEtoTVp1M1FYZHMzK2p6aWw0Z0hPei9Ib1BKOQpnTHEzL0VidHhwT0Q1aS9DNnBtdzdIYzUza1NDckIra2VZQWdVZnVoNTczMGcrd3BnOUQ0M2hxQTcxdS9xdGQvCnl6K2dNUmdNZ1kyd0QrdlZBZGt0bFVJaWY3amJOZHVENEYyZ2QvSDBvOWNYRFJuczMzTUFabU5GcGozeEFhcEIKL1FaeEpXUkxnUnkrUDkxZzJkRkU3OE1pZjhlNE1IK2tTb2d3MjBUbUluanMrSnRFU3NMUGZsdnlGVGtJZE1XRQptRFY4bUdSTmE1dWhheURtU0RRT3FXQ1ROVkdXdlVaNlNGclFnVDUwMER1eFp0bWJYRnRFakc5SGhneUluUE9HCkptZjNNZXNLd1hyVU0xbUF6dUJFUEVHSXA5NzdRNjZKK2lMNjNZNS9NMHdoYTBlVTRqaTU1UUJyTkI5WloybGsKTzRsZTZNd2ZmbnRzb0lqQytUOHlFbm1tbmdBOVN1Y09Fb0JwUWF3dy9EaE5rUjRpOE1KbUdSTXZqS2s5R1c2dwpnZU1VOElUOEpkNGNOYk5XN0VIanNXTzE3WW5NWEgwRTE5VmprZ3VHd0g4MGd2UThrc05leFlQN1lqNG9FcnJ0CkptZWFZTzBsVWRXS0Z6S011Ly9rRS80bkcrSHh6aVFScDlCZ3JNRE1iSzhmZDNvZmNramRWU01vVXNRSWlpZnUKMUwrUjhXVjcreGxPN1NZLzR0aTdjbzNyM1dFNjJWUThWT1AxWmFxK1ZEWm9TSDFUQmtJU0lOaWlSRUc4WUg0LwpEcnA1ZnZ0cE9wREFEY3VMZ0M0MkdwTGZLWnBUS1FJREFRQUJBb0lDQUFTSHc4Tit4aDR5ckFVcDc4WGFTZlhYCmtnK0FtUTBmRWV0MnVDeGgxTTlia09Xd29OQ2gzYXpUT24zNHhaYkF5TUVUbGNsVkNBZ3IwOXc4RjJRTGljcm4KSTQrQVZ4bExwVkprKzFUY1ZCY2VNSFFzWGFjRmVSblZxYkkzbU5qKzVGS2dqaXV4NWx2WmpiYlZWbmJJUWplOQpxcTBGa3R5ekEwb3NDYmUydDlWVW9pVDVtTGhaOG90Ym9BRGkvQzR6YUEyL3djUGNyMkNaUWhvem51U21PUjJWCmVydUNOMHA4VURGTFA1a0gxdXlvY0NpTFh6ZXdIVEVRQ3krK0YwMEZuRmxqeDVSYW5za3JvMnhqWFR5QlZtZUYKcDYwRHF0Q0hkTjVlS2VlQWxDL0dIRlFvL2swdzd3ejMxbHVsVGgza3FDQzJsaXRwYzVpZ2JsTGxaUDgxSUpXTQp0bkhlczNsTXk1RGNDWUx3L3huZFdmVDZFMTB4WlhFNWI0QTdxYjF4Yjhsd1FoNHFJckhDZ2p1NDVPYXNCMERJClBYZ3E2eWkwL2FKWXV6SU5kcjRTeFRibExGUkp6MXlQaGZTZDVGbjdWQVBYU1JNTDlDbzJrL0M1SDlwdG1HMjYKZHBLQVNib1ZMcStrbXg3anVKYXc0a1JNNHZmYndHZGNMZEhqMXByZ08xNkd1ckpQOVRRQ0x5YzhaR0xOekcvaApIMzBpU2FlclJOUmtDRlhmeTEzWWJJZTZHTE12KzVmODlYSENGNmZrZ1JkZjVrbTA3cEc3SCtMZytmZFdtd2lZCm0waExNSFVZeHJ3WkFma2tvZjhlSllscEVQVmQ3ZytCVjd2eTZhYW0yQituUTdHYk84WUprSnlJME04amlSaDEKeGdjRmFZaGZlT21RZGtVbi9BcUJBb0lCQVFEU1JZbDl0SnJyQk5UOXlZN0twWTJiOGVURFJqdDByU1NQRUJvNgppeWoyVWR5S1ZSbHFFdzRma2IrejV3WWt2bnBpMW1uS3NjNFlLZmoyaDVSdXVDbzVzTUNLbmpDUXBpbll4bWRFCk45Z3l6SWRYMmlzRUh6dXNSZkZiajBVSWU1dHc0TE9pL3cyVzFYWGJUc3liOFdhTmhkbVQ4TGxDNjQ5WkNNUWQKeDZkeTdOWS9uYnVWVVQ0KzM3WmV0VlR1eDg1ekl5OTdnMWp4dFZhaXZrd2hQVWtLcWpXWDYzaUZidjFuY1FVdgpiQURrWkFoOXRWYWV2UGZ2NURDeDZITldiVFlObjVRZWd3OTRyVndoSjhYb1V5ZDRqWFB0VmdXU2VkN0tWd2U5CmNkNW9CZWFBOVhDdnJxdkNIRjI4QXg2OUI2YWQrQlk1S0dVcGU2LythQnlKdlQwUkFvSUJBUURJN2c3c0dMc3AKVWJ4dGhJQm9yRzF5MDRKWWlnaE5VMlF4YjdzSkxocnRTc2NtRkxSZU5DYy8zOTBCV3ZrbVFIazFnZkpxV3hDLwp2R0VMT0Iwd3U5VFBBRWFsS25IZ2RhNkFTMURuM3NTWTltcFBRRjYvbEY2cm00cDlNUU1TTFo1V3ZsL0ZNRklHCjUvaXVSVjJaOVdkeTV4QVFWNG5uZmdMOWJDNzhPa2k3VnFPTDJDZk0vcEJEMHdzRUZmOGZiejFSZXo4dEFRZ2QKVXY4cEpFTWdDTCtQeEdkdG5DYUcxYm5obXhEUUxiWmQ4TTFOQkpKOWZLZFgzVWtLcTlDdmFJVXBIZlduSFBWVAprVWNwMUVTYnEzOFVhTzFSS1NBNUtQd1ZiNkVPVGJBSGtlaEN4ZVhpN2F3YkZwYXlTelpIaWl4Y05QQjk1YUtSCkpJQ0J5ekFwQTVTWkFvSUJBRlZKYXlrWGxqWjVNVUwyKy9ucUNIUVdPeW1SVlJCUUlpSDg4QWFLNTBSeGs3aHcKSit6RWFkZ1lMOTl5ZHlWME5RUGQzKzhkQzNEMXBVdXBWbVZLUWFaQXNQZ0lqYjQrQjM4cmlqczdRMi9uVVlZcQpzWVBzZnpHeTlPQ2tUZVhRN1ExdHRxOElNS1RiVkFCdUI4UEF1RTN5Mm51TkNqZkFmOVluSGhUT0pIY1M1UnZNCmlJZForcHRaOWdpWUdDajUxaDBSU25NWXBYejBobjFnSGxUbEhMazhySnhBSUJSUEhtMVVoRHZsM0w3R2JFTkEKeUM5K2lqbzlIaHNySTQwTW92NEhtZlorUmtvMlZzWUQ4ZHYzem15eFF6SWkwQVBIZHJ3dmJLNUVmMmRGN1dhbApKdDI3UldOb1NnUzJaME5ZMVJZQnlGSEt0cTJLdzZtMjVNeGhlMkVDZ2dFQVhSNFdSRXhoMEpCVXB0eVZOZTFTCis3Z1IzRDU4QW5uM0lRSUt5QUpaOEVhTGJKYUQwSFNUREFNUFJTV0grYlkvZGhDMjY1c3djK3MxZmlHUFJacUcKMFRmcmhYZmFOby9UUXhta2NSRElRNnRQTVZNL2xjR0k3amF6UTdtSEZ0R1ZZOVh1UkZCVWMyYmwxTDNJMXlUbgp3RlJkR1hXNEwxUXl4b2R3YnV3RDhPNEI5VGxEbUxrUTJwM2ZxUkVZbnRUS3NneFFCdWRIZjIrTFdPRzVTZ3RECjI3akZ4Z0pyeUdrY0wvWFJJT2xPYnRLK0VrZGdMRStzcmdlYlpocWlKK2hrYmQyNGpxM1k4OVdNQ1ZLYVNScDkKVmxRYVIxYXIzRkdtSWJrT0JyYnlNVS9wTjZqSEZSZllmdVZGQ1hQWnYrWEZFU1pubmJEaVdpbDBkTEpacTJoQgpZUUtDQVFBOVlTcE1wS3dhVGwrTmhTZlovMXU0NjZiMkpZcmJPYlRJM2VCZUowMnFwNXdQTjZYUHJ5aVZaZ1FXClh5cG04a3M5MEJIblBPNUczNFJnKzhLRFlONU1Ed1hBclJubDdSeTEySG5oV3lSaHNKYmdZOEh1c2d4SEROMU8KMEcwSFBpVWtIbTYydlRNYll6bkhPeE5sS1hFdFhBcTlZM3dQYkFjb01rRXZ0MzEwdEdLSUNtdTdEWkpXRlVvTAp1Y3RVS3Boc0V5VWdHbHIwRjJKekVoQWdMRXplczB0S1JpRWdlaFdnbXdlMEhlTEhCdW5oRFBTMmFJY2lCME1pCjY2SGc3cVZyMDlneXpFeGxrY3RLRzhsSm9WbU8vdlhucWQrWDB5M21YTUVZbkFIWHpIeG1Pd2JCNnF3Y3VWTlEKZytqRXliUWF3d3A2OC9id0JncFREQUhORGxrRQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==</prv>
</cert>
<dhcpdv6/>
<virtualip version="1.0.1">
<vip/>
</virtualip>
<openvpn/>
<ppps>
<ppp/>
</ppps>
<vlans version="1.0.0">
<vlan/>
</vlans>
<bridges>
<bridged/>
</bridges>
<gifs version="1.0.0">
<gif/>
</gifs>
<gres version="1.0.0">
<gre/>
</gres>
<laggs version="1.0.0">
<lagg/>
</laggs>
<wireless>
<clone/>
</wireless>
<hasync version="1.0.2">
<pfsyncinterface/>
<synchronizetoip/>
<verifypeer>0</verifypeer>
<username/>
<password/>
<disablepreempt>0</disablepreempt>
<disconnectppps>0</disconnectppps>
<pfsyncpeerip/>
<pfsyncversion>1400</pfsyncversion>
<syncitems/>
</hasync>
<ifgroups version="1.0.0"/>
<dnsmasq version="1.0.7" persisted_at="1755800176.40">
<enable>1</enable>
<regdhcp>0</regdhcp>
<regdhcpstatic>0</regdhcpstatic>
<dhcpfirst>0</dhcpfirst>
<strict_order>0</strict_order>
<domain_needed>0</domain_needed>
<no_private_reverse>0</no_private_reverse>
<no_resolv>0</no_resolv>
<log_queries>0</log_queries>
<no_hosts>0</no_hosts>
<strictbind>0</strictbind>
<dnssec>0</dnssec>
<regdhcpdomain/>
<interface>lan</interface>
<port>0</port>
<dns_forward_max/>
<cache_size/>
<local_ttl/>
<add_mac/>
<add_subnet>0</add_subnet>
<strip_subnet>0</strip_subnet>
<dhcp>
<no_interface/>
<fqdn>1</fqdn>
<domain/>
<lease_max/>
<authoritative>0</authoritative>
<default_fw_rules>1</default_fw_rules>
<reply_delay/>
<enable_ra>0</enable_ra>
<nosync>0</nosync>
</dhcp>
<no_ident>1</no_ident>
<dhcp_tags uuid="8d190cf3-8d2d-47db-ab9b-fa21016b533e">
<tag>ipxe</tag>
</dhcp_tags>
<dhcp_tags uuid="0b2982da-198c-4ca4-9a3e-95813667047c">
<tag>pxeEfi</tag>
</dhcp_tags>
<dhcp_tags uuid="993e079f-09b9-4a0f-a70f-8898872b9983">
<tag>pxeBios</tag>
</dhcp_tags>
<dhcp_ranges uuid="843574fc-4c3f-4f81-9e86-56be45f4ba49">
<interface>lan</interface>
<set_tag/>
<start_addr>192.168.1.41</start_addr>
<end_addr>192.168.1.245</end_addr>
<subnet_mask/>
<constructor/>
<mode/>
<prefix_len/>
<lease_time/>
<domain_type>range</domain_type>
<domain/>
<nosync>0</nosync>
<ra_mode/>
<ra_priority/>
<ra_mtu/>
<ra_interval/>
<ra_router_lifetime/>
<description/>
</dhcp_ranges>
<dhcp_options uuid="1e8d6f0f-7c2c-4873-8960-95a5c9447318">
<type>match</type>
<option>77</option>
<option6/>
<interface/>
<tag/>
<set_tag>8d190cf3-8d2d-47db-ab9b-fa21016b533e</set_tag>
<value>iPXE</value>
<force/>
<description/>
</dhcp_options>
<dhcp_options uuid="9b54181b-aa68-4fd6-9d59-10a77c291fcb">
<type>match</type>
<option>93</option>
<option6/>
<interface/>
<tag/>
<set_tag>993e079f-09b9-4a0f-a70f-8898872b9983</set_tag>
<value>0</value>
<force/>
<description/>
</dhcp_options>
<dhcp_options uuid="26402b93-91bb-48a9-92da-b567a91ed4d8">
<type>match</type>
<option>93</option>
<option6/>
<interface/>
<tag/>
<set_tag>0b2982da-198c-4ca4-9a3e-95813667047c</set_tag>
<value>7</value>
<force/>
<description/>
</dhcp_options>
<dhcp_boot uuid="57436655-6f95-4590-bcdc-dfe542347560">
<interface/>
<tag>0b2982da-198c-4ca4-9a3e-95813667047c</tag>
<filename>ipxe.efi</filename>
<servername>192.168.1.1</servername>
<address>192.168.1.1</address>
<description/>
</dhcp_boot>
<dhcp_boot uuid="dcc00bf2-5148-40ef-9f79-1f17bf572f6c">
<interface/>
<tag>8d190cf3-8d2d-47db-ab9b-fa21016b533e</tag>
<filename>http://192.168.1.1:8080/boot.ipxe</filename>
<servername>192.168.1.1</servername>
<address>192.168.1.1</address>
<description/>
</dhcp_boot>
<dhcp_boot uuid="8b9263a4-d242-4c41-8ff2-6f1af5001c41">
<interface/>
<tag>993e079f-09b9-4a0f-a70f-8898872b9983</tag>
<filename>undionly.kpxe</filename>
<servername>192.168.1.1</servername>
<address>192.168.1.1</address>
<description/>
</dhcp_boot>
</dnsmasq>
</opnsense>