From da6610c6254539a8d71311e496506091952ca839 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Thu, 21 Aug 2025 17:28:17 -0400 Subject: [PATCH] wip: PXE setup for ipxe and okd files in progress --- Cargo.lock | 1 + examples/nanodc/src/main.rs | 7 +- examples/okd_pxe/src/main.rs | 22 +- examples/opnsense/src/main.rs | 9 +- harmony/src/domain/data/file.rs | 22 + harmony/src/domain/data/mod.rs | 2 + harmony/src/domain/topology/ha_cluster.rs | 49 +- harmony/src/domain/topology/http.rs | 3 +- harmony/src/domain/topology/network.rs | 13 +- harmony/src/infra/opnsense/dhcp.rs | 78 +- harmony/src/infra/opnsense/http.rs | 31 +- harmony/src/infra/opnsense/tftp.rs | 2 +- harmony/src/modules/dhcp.rs | 69 +- harmony/src/modules/http.rs | 19 +- opnsense-config-xml/src/data/caddy.rs | 16 +- opnsense-config-xml/src/data/dnsmasq.rs | 8 +- opnsense-config-xml/src/data/opnsense.rs | 2 +- opnsense-config/Cargo.toml | 1 + opnsense-config/src/config/config.rs | 4 + opnsense-config/src/config/shell/mod.rs | 9 + opnsense-config/src/config/shell/ssh.rs | 9 +- opnsense-config/src/modules/dhcp.rs | 2 + opnsense-config/src/modules/dnsmasq.rs | 177 +++- .../data/config-full-25.7-dnsmasq-options.xml | 896 ++++++++++++++++++ 24 files changed, 1242 insertions(+), 209 deletions(-) create mode 100644 harmony/src/domain/data/file.rs create mode 100644 opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml diff --git a/Cargo.lock b/Cargo.lock index 27a97f8..8f3e86d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3596,6 +3596,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", + "uuid", ] [[package]] diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 10754b4..d1cc726 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -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( diff --git a/examples/okd_pxe/src/main.rs b/examples/okd_pxe/src/main.rs index 576d09d..9778e18 100644 --- a/examples/okd_pxe/src/main.rs +++ b/examples/okd_pxe/src/main.rs @@ -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>> = 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(); } diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 61f8f18..e868829 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -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, diff --git a/harmony/src/domain/data/file.rs b/harmony/src/domain/data/file.rs new file mode 100644 index 0000000..3ae7f3a --- /dev/null +++ b/harmony/src/domain/data/file.rs @@ -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}")), + } + } +} diff --git a/harmony/src/domain/data/mod.rs b/harmony/src/domain/data/mod.rs index e122b20..1529809 100644 --- a/harmony/src/domain/data/mod.rs +++ b/harmony/src/domain/data/mod.rs @@ -1,4 +1,6 @@ mod id; mod version; +mod file; pub use id::*; pub use version::*; +pub use file::*; diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 598ef5b..31d1a7f 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -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) } diff --git a/harmony/src/domain/topology/http.rs b/harmony/src/domain/topology/http.rs index 42a38af..1d35621 100644 --- a/harmony/src/domain/topology/http.rs +++ b/harmony/src/domain/topology/http.rs @@ -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>; diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index 42ff8c3..f68d29c 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -46,16 +46,19 @@ pub trait K8sclient: Send + Sync { async fn k8s_client(&self) -> Result, String>; } +pub struct PxeOptions { + pub ipxe_filename: String, + pub bios_filename: String, + pub efi_filename: String, + pub tftp_ip: Option, +} + #[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>; diff --git a/harmony/src/infra/opnsense/dhcp.rs b/harmony/src/infra/opnsense/dhcp.rs index bea44fe..1d7841c 100644 --- a/harmony/src/infra/opnsense/dhcp.rs +++ b/harmony/src/infra/opnsense/dhcp.rs @@ -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}")) + }) } } diff --git a/harmony/src/infra/opnsense/http.rs b/harmony/src/infra/opnsense/http.rs index a51bf34..1d34d4c 100644 --- a/harmony/src/infra/opnsense/http.rs +++ b/harmony/src/infra/opnsense/http.rs @@ -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> { diff --git a/harmony/src/infra/opnsense/tftp.rs b/harmony/src/infra/opnsense/tftp.rs index c7b7f2b..1bf7b6c 100644 --- a/harmony/src/infra/opnsense/tftp.rs +++ b/harmony/src/infra/opnsense/tftp.rs @@ -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> { diff --git a/harmony/src/modules/dhcp.rs b/harmony/src/modules/dhcp.rs index 04ef093..81643db 100644 --- a/harmony/src/modules/dhcp.rs +++ b/harmony/src/modules/dhcp.rs @@ -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 { - 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, diff --git a/harmony/src/modules/http.rs b/harmony/src/modules/http.rs index 36af092..700d5a3 100644 --- a/harmony/src/modules/http.rs +++ b/harmony/src/modules/http.rs @@ -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, + pub files: Vec, } impl Score for StaticFilesHttpScore { @@ -50,12 +51,20 @@ impl Interpret for StaticFilesHttpInterpret { ) -> Result { 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::>().join(",") ))) } diff --git a/opnsense-config-xml/src/data/caddy.rs b/opnsense-config-xml/src/data/caddy.rs index b4ca0fc..62b311c 100644 --- a/opnsense-config-xml/src/data/caddy.rs +++ b/opnsense-config-xml/src/data/caddy.rs @@ -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, #[yaserde(rename = "TlsDnsOptionalField1")] - pub tls_dns_optional_field1: MaybeString, + pub tls_dns_optional_field1: Option, #[yaserde(rename = "TlsDnsOptionalField2")] - pub tls_dns_optional_field2: MaybeString, + pub tls_dns_optional_field2: Option, #[yaserde(rename = "TlsDnsOptionalField3")] - pub tls_dns_optional_field3: MaybeString, + pub tls_dns_optional_field3: Option, #[yaserde(rename = "TlsDnsOptionalField4")] - pub tls_dns_optional_field4: MaybeString, + pub tls_dns_optional_field4: Option, #[yaserde(rename = "TlsDnsPropagationTimeout")] pub tls_dns_propagation_timeout: Option, #[yaserde(rename = "TlsDnsPropagationTimeoutPeriod")] @@ -47,6 +47,8 @@ pub struct CaddyGeneral { pub tls_dns_propagation_delay: Option, #[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, @@ -56,6 +58,10 @@ pub struct CaddyGeneral { pub http_version: Option, #[yaserde(rename = "HttpVersions")] pub http_versions: Option, + pub timeout_read_body: Option, + pub timeout_read_header: Option, + pub timeout_write: Option, + pub timeout_idle: Option, #[yaserde(rename = "LogCredentials")] pub log_credentials: MaybeString, #[yaserde(rename = "LogAccessPlain")] diff --git a/opnsense-config-xml/src/data/dnsmasq.rs b/opnsense-config-xml/src/data/dnsmasq.rs index a246e74..dd672ac 100644 --- a/opnsense-config-xml/src/data/dnsmasq.rs +++ b/opnsense-config-xml/src/data/dnsmasq.rs @@ -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 element. @@ -35,6 +35,7 @@ pub struct DnsMasq { pub dhcp_ranges: Vec, pub dhcp_options: Vec, pub dhcp_boot: Vec, + pub dhcp_tags: Vec, } // Represents the 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, 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, pub servername: String, pub address: String, - pub description: String, + pub description: Option, } // Represents a single element. diff --git a/opnsense-config-xml/src/data/opnsense.rs b/opnsense-config-xml/src/data/opnsense.rs index 32a548b..c39f1c5 100644 --- a/opnsense-config-xml/src/data/opnsense.rs +++ b/opnsense-config-xml/src/data/opnsense.rs @@ -1509,7 +1509,7 @@ pub struct Vlans { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Bridges { - pub bridged: MaybeString, + pub bridged: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] diff --git a/opnsense-config/Cargo.toml b/opnsense-config/Cargo.toml index 938414e..e70bc12 100644 --- a/opnsense-config/Cargo.toml +++ b/opnsense-config/Cargo.toml @@ -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 diff --git a/opnsense-config/src/config/config.rs b/opnsense-config/src/config/config.rs index 95c5430..96f9a40 100644 --- a/opnsense-config/src/config/config.rs +++ b/opnsense-config/src/config/config.rs @@ -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 { + self.shell.write_content_to_file(content, path).await + } + /// Checks in config file if system.firmware.plugins csv field contains the specified package /// name. /// diff --git a/opnsense-config/src/config/shell/mod.rs b/opnsense-config/src/config/shell/mod.rs index 9159606..aa03837 100644 --- a/opnsense-config/src/config/shell/mod.rs +++ b/opnsense-config/src/config/shell/mod.rs @@ -9,6 +9,7 @@ use crate::Error; pub trait OPNsenseShell: std::fmt::Debug + Send + Sync { async fn exec(&self, command: &str) -> Result; async fn write_content_to_temp_file(&self, content: &str) -> Result; + async fn write_content_to_file(&self, content: &str, filename: &str) -> Result; async fn upload_folder(&self, source: &str, destination: &str) -> Result; } @@ -25,6 +26,14 @@ impl OPNsenseShell for DummyOPNSenseShell { async fn write_content_to_temp_file(&self, _content: &str) -> Result { unimplemented!("This is a dummy implementation"); } + + async fn write_content_to_file( + &self, + _content: &str, + _filename: &str, + ) -> Result { + unimplemented!("This is a dummy implementation"); + } async fn upload_folder(&self, _source: &str, _destination: &str) -> Result { unimplemented!("This is a dummy implementation"); } diff --git a/opnsense-config/src/config/shell/ssh.rs b/opnsense-config/src/config/shell/ssh.rs index 6b29658..93586bc 100644 --- a/opnsense-config/src/config/shell/ssh.rs +++ b/opnsense-config/src/config/shell/ssh.rs @@ -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 { + // 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 { diff --git a/opnsense-config/src/modules/dhcp.rs b/opnsense-config/src/modules/dhcp.rs index c0560a4..8ec3519 100644 --- a/opnsense-config/src/modules/dhcp.rs +++ b/opnsense-config/src/modules/dhcp.rs @@ -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), } } } diff --git a/opnsense-config/src/modules/dnsmasq.rs b/opnsense-config/src/modules/dnsmasq.rs index 2bb03ea..7efb493 100644 --- a/opnsense-config/src/modules/dnsmasq.rs +++ b/opnsense-config/src/modules/dnsmasq.rs @@ -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, } +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) -> Self { Self { @@ -23,47 +27,172 @@ impl<'a> DhcpConfigDnsMasq<'a> { } } + /// Removes a static mapping by its MAC address. + /// Static mappings are stored in the 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 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 = &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, 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, + 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(()) } } diff --git a/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml b/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml new file mode 100644 index 0000000..879d8d6 --- /dev/null +++ b/opnsense-config/src/tests/data/config-full-25.7-dnsmasq-options.xml @@ -0,0 +1,896 @@ + + + opnsense + + + + + 115200 + serial + normal + OPNsense + testpxe.harmony.mcd + + admins + System Administrators + system + 1999 + 0 + page-all + + + + root + System Administrator + system + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + + 0 + 0 + + + + + + + + + + + + + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + 68a72b6f7f776 + + + + + + 1 + yes + 1 + 1 + 1 + 1 + 1 + 1 + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + 1 + + + + + + enabled + 1 + + 1 + + + -1 + -1 + + + + os-tftp + + + 0 + + en_US + + 1 + + + + + vtnet0 + + 1 + + + dhcp + + 0 + 1 + + dhcp6 + 0 + + + + + + vtnet1 + 1 + 192.168.1.1 + 24 + track6 + 64 + + + wan + 0 + + + 1 + lo0 + Loopback + 1 + 127.0.0.1 + none + 1 + 8 + ::1 + 128 + + + + + + + public + + + + + automatic + + + + + pass + lan + inet + Default allow LAN to any rule + + lan + + + + + + + pass + lan + inet6 + Default allow LAN IPv6 to any rule + + lan + + + + + + + + + + + 0.opnsense.pool.ntp.org + + + root@192.168.1.5 + /api/dnsmasq/settings/set made changes + + + + + + + + + + + + + + + v9 + + + + 0 + + 1800 + 15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + wan + 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12 + + + W0D23 + 4 + + + + + + + 0 + 0 + 0 + + + + 0 + 0 + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 16 + 32 + 4 + 1000 + 1 + 0 + 0 + 0 + + + + + + + + 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + + + + + + + + + + 0 + + + + + + + 0 + 0 + + + ipsec + 0 + 1 + + + + + + + + + + + + + 0 + 0 + + 4000 + 1 + raw + + + 0 + + 2 + + + + + + + + 0 + 127.0.0.1 + 8000 + + + + + 0 + 0 + + 4000 + 1 + + + 0 + + 2 + + + + + + + + + + 0 + 120 + 120 + 127.0.0.1 + 25 + + + 0 + auto + 1 + + + + + 0 + root + + 2812 + + + 5 + 1 + + + 0 + root@localhost.local + 0 + + + + + + + 1 + $HOST + + system + + + + 300 + 30 +
+ + + + cfed35dc-f74b-417d-9ed9-682c5de96495,f961277a-07f1-49a4-90ee-bb15738d9ebb,30b2cce2-f650-4e44-a3e2-ee53886cda3f,3c86136f-35a4-4126-865b-82732c6542d9 + + + + + 1 + RootFs + + filesystem + + + / + 300 + 30 +
+ + + + fbb8dfe2-b9ad-4730-a0f3-41d7ecda6289 + + + + + 0 + carp_status_change + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/carp_status + 300 + 30 +
+ + + + 11ceca8a-dff8-45e0-9dc5-ed80dc4b3947 + + + + + 0 + gateway_alert + + custom + + + /usr/local/opnsense/scripts/OPNsense/Monit/gateway_alert + 300 + 30 +
+ + + + fad1f465-4a92-4b93-be66-59d7059b8779 + + + + + Ping + NetworkPing + failed ping + alert + + + + NetworkLink + NetworkInterface + failed link + alert + + + + NetworkSaturation + NetworkInterface + saturation is greater than 75% + alert + + + + MemoryUsage + SystemResource + memory usage is greater than 75% + alert + + + + CPUUsage + SystemResource + cpu usage is greater than 75% + alert + + + + LoadAvg1 + SystemResource + loadavg (1min) is greater than 4 + alert + + + + LoadAvg5 + SystemResource + loadavg (5min) is greater than 3 + alert + + + + LoadAvg15 + SystemResource + loadavg (15min) is greater than 2 + alert + + + + SpaceUsage + SpaceUsage + space usage is greater than 75% + alert + + + + ChangedStatus + ProgramStatus + changed status + alert + + + + NonZeroStatus + ProgramStatus + status != 0 + alert + + + + + + + + + 1 + 1 + 31 + + + + + + + + + + + + 1 + 53 + 0 + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + 0 + 0 + transparent + + 0 + + + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + + 0 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + + 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 + + + + + + + + + + + + + + 0 + + + + + allow + + + 0 + 0 + + + + + +
+ 0 + + + 0 + + + + + + + + + 0 + 0 + 0 + 1 + 0 + + + + + + + + + + + 1 + 192.168.1.1 + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 68a72b6f7f776 + Web GUI TLS certificate + LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQlBxZ0F3SUJBZ0lVUkZqWUQ0Z1U0bzRNZGdiN2pIc29KNU9GVGFnd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhHakFZQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVgpNQk1HQTFVRUNBd01XblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyCkJnTlZCQW9NSkU5UVRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWlRBZUZ3MHkKTlRBNE1qRXhOREl4TXpaYUZ3MHlOakE1TWpJeE5ESXhNelphTUlHR01Sb3dHQVlEVlFRRERCRlBVRTV6Wlc1egpaUzVwYm5SbGNtNWhiREVMTUFrR0ExVUVCaE1DVGt3eEZUQVRCZ05WQkFnTURGcDFhV1F0U0c5c2JHRnVaREVWCk1CTUdBMVVFQnd3TVRXbGtaR1ZzYUdGeWJtbHpNUzB3S3dZRFZRUUtEQ1JQVUU1elpXNXpaU0J6Wld4bUxYTnAKWjI1bFpDQjNaV0lnWTJWeWRHbG1hV05oZEdVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFDbENkeFJ3ZWJQQkxvYlVORnYvL2t3TEdKWExweDl6OFFHV2lyWTNpamVDeUxDQ0FwczBLaE1adTNRClhkczMranppbDRnSE96L0hvUEo5Z0xxMy9FYnR4cE9ENWkvQzZwbXc3SGM1M2tTQ3JCK2tlWUFnVWZ1aDU3MzAKZyt3cGc5RDQzaHFBNzF1L3F0ZC95eitnTVJnTWdZMndEK3ZWQWRrdGxVSWlmN2piTmR1RDRGMmdkL0gwbzljWApEUm5zMzNNQVptTkZwajN4QWFwQi9RWnhKV1JMZ1J5K1A5MWcyZEZFNzhNaWY4ZTRNSCtrU29ndzIwVG1JbmpzCitKdEVTc0xQZmx2eUZUa0lkTVdFbURWOG1HUk5hNXVoYXlEbVNEUU9xV0NUTlZHV3ZVWjZTRnJRZ1Q1MDBEdXgKWnRtYlhGdEVqRzlIaGd5SW5QT0dKbWYzTWVzS3dYclVNMW1BenVCRVBFR0lwOTc3UTY2SitpTDYzWTUvTTB3aAphMGVVNGppNTVRQnJOQjlaWjJsa080bGU2TXdmZm50c29JakMrVDh5RW5tbW5nQTlTdWNPRW9CcFFhd3cvRGhOCmtSNGk4TUptR1JNdmpLazlHVzZ3Z2VNVThJVDhKZDRjTmJOVzdFSGpzV08xN1luTVhIMEUxOVZqa2d1R3dIODAKZ3ZROGtzTmV4WVA3WWo0b0VycnRKbWVhWU8wbFVkV0tGektNdS8va0UvNG5HK0h4emlRUnA5QmdyTURNYks4ZgpkM29mY2tqZFZTTW9Vc1FJaWlmdTFMK1I4V1Y3K3hsTzdTWS80dGk3Y28zcjNXRTYyVlE4Vk9QMVphcStWRFpvClNIMVRCa0lTSU5paVJFRzhZSDQvRHJwNWZ2dHBPcERBRGN1TGdDNDJHcExmS1pwVEtRSURBUUFCbzRJQmREQ0MKQVhBd0NRWURWUjBUQkFJd0FEQVJCZ2xnaGtnQmh2aENBUUVFQkFNQ0JrQXdOQVlKWUlaSUFZYjRRZ0VOQkNjVwpKVTlRVG5ObGJuTmxJRWRsYm1WeVlYUmxaQ0JUWlhKMlpYSWdRMlZ5ZEdsbWFXTmhkR1V3SFFZRFZSME9CQllFCkZIdUVQK05yYlorZWdMdWZVSUFKaUo2M1c4SDFNSUd3QmdOVkhTTUVnYWd3Z2FXaGdZeWtnWWt3Z1lZeEdqQVkKQmdOVkJBTU1FVTlRVG5ObGJuTmxMbWx1ZEdWeWJtRnNNUXN3Q1FZRFZRUUdFd0pPVERFVk1CTUdBMVVFQ0F3TQpXblZwWkMxSWIyeHNZVzVrTVJVd0V3WURWUVFIREF4TmFXUmtaV3hvWVhKdWFYTXhMVEFyQmdOVkJBb01KRTlRClRuTmxibk5sSUhObGJHWXRjMmxuYm1Wa0lIZGxZaUJqWlhKMGFXWnBZMkYwWllJVVJGallENGdVNG80TWRnYjcKakhzb0o1T0ZUYWd3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdFR0NDc0dBUVVGQ0FJQ01Bc0dBMVVkRHdRRQpBd0lGb0RBY0JnTlZIUkVFRlRBVGdoRlBVRTV6Wlc1elpTNXBiblJsY201aGJEQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBZ0VBV2JzM2MwSXYwcEd3Y0wvUmRlbnBiZVJHQ3FsODY0V1ZITEtMZzJIR3BkKytJdmRFcHJEZkZ3SCsKdHdOd2VrZTlXUEtYa20vUkZDWE5DQmVLNjkxeURVWCtCNUJOMjMvSks5N1lzRVdtMURIV3FvSDE1WmdqelZ0QQp2d2JmbnRQdlhCWU1wV2ZQY0Zua0hjN3pxUjI3RzBEZHFUeGg2TjhFenV1S3JRWXFtaWhJUXFkNU9HRVhteW9ZCmdPVjdoZ0lWSUR6a1Z0QkRiS3dFV3VFN2pKYzViMXR4Mk1FUFRsVklEZGo0Zm5vdURWemdkczA2RER4aFM4eXAKbXJOSXhxb045ekEzYXVtTnRNZ2haSHVZRHdjbm5GSnBNZHlJSEdHZ1dlNnZZNHFtdEFSVDd3a0x6MTZnUG9LMAo5bFhVU0RmV3YwUDJGUXFHZTJjaXQ3VVE2ZGtsUWsrVGVtUEFwNnhEV09HR3oxRkdmUUoxN040b3AvOGtlOUo2Cm96RVp3QTh1aDVYTUl2N3loM2dobjV1d1R6RDUyZ1BBZFdaekEyaHVWV3p5cVM0WVc0N3ZkaGV6TTFTUndabVEKUmYzNDk0UVFydWd0bzdycWdMUlRTSXN4WEtkU21MaHZjT0hsSlhISW1XNTRzeFlXNm9NUStpRExFT29ZVVdpcgp1aUJvT1RsNEJaOG5Xcm9pV0JtWlFLaVRPYlFRczVWTkIwYnVybmRISTJVdmtIRTE3QTM0bFYySjY5Q0dNNzJ2CjQ5aE9TN3B2Tzg4cEVKZm90d01YYlRhdkR2WTBHazZxbERFMVBud1U2Wm8ySEprcFdUTUxOSzh1alZ1RkhlMGkKR2JvZi9va08vZW4rUi9PUXNyd1JYbzFwVTRiWnlyWGVQeUdqSSsrdFYzemhjd0IwWjNJPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRQ2xDZHhSd2ViUEJMb2IKVU5Gdi8va3dMR0pYTHB4OXo4UUdXaXJZM2lqZUN5TENDQXBzMEtoTVp1M1FYZHMzK2p6aWw0Z0hPei9Ib1BKOQpnTHEzL0VidHhwT0Q1aS9DNnBtdzdIYzUza1NDckIra2VZQWdVZnVoNTczMGcrd3BnOUQ0M2hxQTcxdS9xdGQvCnl6K2dNUmdNZ1kyd0QrdlZBZGt0bFVJaWY3amJOZHVENEYyZ2QvSDBvOWNYRFJuczMzTUFabU5GcGozeEFhcEIKL1FaeEpXUkxnUnkrUDkxZzJkRkU3OE1pZjhlNE1IK2tTb2d3MjBUbUluanMrSnRFU3NMUGZsdnlGVGtJZE1XRQptRFY4bUdSTmE1dWhheURtU0RRT3FXQ1ROVkdXdlVaNlNGclFnVDUwMER1eFp0bWJYRnRFakc5SGhneUluUE9HCkptZjNNZXNLd1hyVU0xbUF6dUJFUEVHSXA5NzdRNjZKK2lMNjNZNS9NMHdoYTBlVTRqaTU1UUJyTkI5WloybGsKTzRsZTZNd2ZmbnRzb0lqQytUOHlFbm1tbmdBOVN1Y09Fb0JwUWF3dy9EaE5rUjRpOE1KbUdSTXZqS2s5R1c2dwpnZU1VOElUOEpkNGNOYk5XN0VIanNXTzE3WW5NWEgwRTE5VmprZ3VHd0g4MGd2UThrc05leFlQN1lqNG9FcnJ0CkptZWFZTzBsVWRXS0Z6S011Ly9rRS80bkcrSHh6aVFScDlCZ3JNRE1iSzhmZDNvZmNramRWU01vVXNRSWlpZnUKMUwrUjhXVjcreGxPN1NZLzR0aTdjbzNyM1dFNjJWUThWT1AxWmFxK1ZEWm9TSDFUQmtJU0lOaWlSRUc4WUg0LwpEcnA1ZnZ0cE9wREFEY3VMZ0M0MkdwTGZLWnBUS1FJREFRQUJBb0lDQUFTSHc4Tit4aDR5ckFVcDc4WGFTZlhYCmtnK0FtUTBmRWV0MnVDeGgxTTlia09Xd29OQ2gzYXpUT24zNHhaYkF5TUVUbGNsVkNBZ3IwOXc4RjJRTGljcm4KSTQrQVZ4bExwVkprKzFUY1ZCY2VNSFFzWGFjRmVSblZxYkkzbU5qKzVGS2dqaXV4NWx2WmpiYlZWbmJJUWplOQpxcTBGa3R5ekEwb3NDYmUydDlWVW9pVDVtTGhaOG90Ym9BRGkvQzR6YUEyL3djUGNyMkNaUWhvem51U21PUjJWCmVydUNOMHA4VURGTFA1a0gxdXlvY0NpTFh6ZXdIVEVRQ3krK0YwMEZuRmxqeDVSYW5za3JvMnhqWFR5QlZtZUYKcDYwRHF0Q0hkTjVlS2VlQWxDL0dIRlFvL2swdzd3ejMxbHVsVGgza3FDQzJsaXRwYzVpZ2JsTGxaUDgxSUpXTQp0bkhlczNsTXk1RGNDWUx3L3huZFdmVDZFMTB4WlhFNWI0QTdxYjF4Yjhsd1FoNHFJckhDZ2p1NDVPYXNCMERJClBYZ3E2eWkwL2FKWXV6SU5kcjRTeFRibExGUkp6MXlQaGZTZDVGbjdWQVBYU1JNTDlDbzJrL0M1SDlwdG1HMjYKZHBLQVNib1ZMcStrbXg3anVKYXc0a1JNNHZmYndHZGNMZEhqMXByZ08xNkd1ckpQOVRRQ0x5YzhaR0xOekcvaApIMzBpU2FlclJOUmtDRlhmeTEzWWJJZTZHTE12KzVmODlYSENGNmZrZ1JkZjVrbTA3cEc3SCtMZytmZFdtd2lZCm0waExNSFVZeHJ3WkFma2tvZjhlSllscEVQVmQ3ZytCVjd2eTZhYW0yQituUTdHYk84WUprSnlJME04amlSaDEKeGdjRmFZaGZlT21RZGtVbi9BcUJBb0lCQVFEU1JZbDl0SnJyQk5UOXlZN0twWTJiOGVURFJqdDByU1NQRUJvNgppeWoyVWR5S1ZSbHFFdzRma2IrejV3WWt2bnBpMW1uS3NjNFlLZmoyaDVSdXVDbzVzTUNLbmpDUXBpbll4bWRFCk45Z3l6SWRYMmlzRUh6dXNSZkZiajBVSWU1dHc0TE9pL3cyVzFYWGJUc3liOFdhTmhkbVQ4TGxDNjQ5WkNNUWQKeDZkeTdOWS9uYnVWVVQ0KzM3WmV0VlR1eDg1ekl5OTdnMWp4dFZhaXZrd2hQVWtLcWpXWDYzaUZidjFuY1FVdgpiQURrWkFoOXRWYWV2UGZ2NURDeDZITldiVFlObjVRZWd3OTRyVndoSjhYb1V5ZDRqWFB0VmdXU2VkN0tWd2U5CmNkNW9CZWFBOVhDdnJxdkNIRjI4QXg2OUI2YWQrQlk1S0dVcGU2LythQnlKdlQwUkFvSUJBUURJN2c3c0dMc3AKVWJ4dGhJQm9yRzF5MDRKWWlnaE5VMlF4YjdzSkxocnRTc2NtRkxSZU5DYy8zOTBCV3ZrbVFIazFnZkpxV3hDLwp2R0VMT0Iwd3U5VFBBRWFsS25IZ2RhNkFTMURuM3NTWTltcFBRRjYvbEY2cm00cDlNUU1TTFo1V3ZsL0ZNRklHCjUvaXVSVjJaOVdkeTV4QVFWNG5uZmdMOWJDNzhPa2k3VnFPTDJDZk0vcEJEMHdzRUZmOGZiejFSZXo4dEFRZ2QKVXY4cEpFTWdDTCtQeEdkdG5DYUcxYm5obXhEUUxiWmQ4TTFOQkpKOWZLZFgzVWtLcTlDdmFJVXBIZlduSFBWVAprVWNwMUVTYnEzOFVhTzFSS1NBNUtQd1ZiNkVPVGJBSGtlaEN4ZVhpN2F3YkZwYXlTelpIaWl4Y05QQjk1YUtSCkpJQ0J5ekFwQTVTWkFvSUJBRlZKYXlrWGxqWjVNVUwyKy9ucUNIUVdPeW1SVlJCUUlpSDg4QWFLNTBSeGs3aHcKSit6RWFkZ1lMOTl5ZHlWME5RUGQzKzhkQzNEMXBVdXBWbVZLUWFaQXNQZ0lqYjQrQjM4cmlqczdRMi9uVVlZcQpzWVBzZnpHeTlPQ2tUZVhRN1ExdHRxOElNS1RiVkFCdUI4UEF1RTN5Mm51TkNqZkFmOVluSGhUT0pIY1M1UnZNCmlJZForcHRaOWdpWUdDajUxaDBSU25NWXBYejBobjFnSGxUbEhMazhySnhBSUJSUEhtMVVoRHZsM0w3R2JFTkEKeUM5K2lqbzlIaHNySTQwTW92NEhtZlorUmtvMlZzWUQ4ZHYzem15eFF6SWkwQVBIZHJ3dmJLNUVmMmRGN1dhbApKdDI3UldOb1NnUzJaME5ZMVJZQnlGSEt0cTJLdzZtMjVNeGhlMkVDZ2dFQVhSNFdSRXhoMEpCVXB0eVZOZTFTCis3Z1IzRDU4QW5uM0lRSUt5QUpaOEVhTGJKYUQwSFNUREFNUFJTV0grYlkvZGhDMjY1c3djK3MxZmlHUFJacUcKMFRmcmhYZmFOby9UUXhta2NSRElRNnRQTVZNL2xjR0k3amF6UTdtSEZ0R1ZZOVh1UkZCVWMyYmwxTDNJMXlUbgp3RlJkR1hXNEwxUXl4b2R3YnV3RDhPNEI5VGxEbUxrUTJwM2ZxUkVZbnRUS3NneFFCdWRIZjIrTFdPRzVTZ3RECjI3akZ4Z0pyeUdrY0wvWFJJT2xPYnRLK0VrZGdMRStzcmdlYlpocWlKK2hrYmQyNGpxM1k4OVdNQ1ZLYVNScDkKVmxRYVIxYXIzRkdtSWJrT0JyYnlNVS9wTjZqSEZSZllmdVZGQ1hQWnYrWEZFU1pubmJEaVdpbDBkTEpacTJoQgpZUUtDQVFBOVlTcE1wS3dhVGwrTmhTZlovMXU0NjZiMkpZcmJPYlRJM2VCZUowMnFwNXdQTjZYUHJ5aVZaZ1FXClh5cG04a3M5MEJIblBPNUczNFJnKzhLRFlONU1Ed1hBclJubDdSeTEySG5oV3lSaHNKYmdZOEh1c2d4SEROMU8KMEcwSFBpVWtIbTYydlRNYll6bkhPeE5sS1hFdFhBcTlZM3dQYkFjb01rRXZ0MzEwdEdLSUNtdTdEWkpXRlVvTAp1Y3RVS3Boc0V5VWdHbHIwRjJKekVoQWdMRXplczB0S1JpRWdlaFdnbXdlMEhlTEhCdW5oRFBTMmFJY2lCME1pCjY2SGc3cVZyMDlneXpFeGxrY3RLRzhsSm9WbU8vdlhucWQrWDB5M21YTUVZbkFIWHpIeG1Pd2JCNnF3Y3VWTlEKZytqRXliUWF3d3A2OC9id0JncFREQUhORGxrRQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + 0 + + 1400 + + + + + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + lan + 0 + + + + + 0 + 0 + + + 1 + + + 0 + 1 + + 0 + 0 + + 1 + + ipxe + + + pxeEfi + + + pxeBios + + + lan + + 192.168.1.41 + 192.168.1.245 + + + + + + range + + 0 + + + + + + + + + match + + + + + 8d190cf3-8d2d-47db-ab9b-fa21016b533e + iPXE + + + + + match + + + + + 993e079f-09b9-4a0f-a70f-8898872b9983 + 0 + + + + + match + + + + + 0b2982da-198c-4ca4-9a3e-95813667047c + 7 + + + + + + 0b2982da-198c-4ca4-9a3e-95813667047c + ipxe.efi + 192.168.1.1 +
192.168.1.1
+ +
+ + + 8d190cf3-8d2d-47db-ab9b-fa21016b533e + http://192.168.1.1:8080/boot.ipxe + 192.168.1.1 +
192.168.1.1
+ +
+ + + 993e079f-09b9-4a0f-a70f-8898872b9983 + undionly.kpxe + 192.168.1.1 +
192.168.1.1
+ +
+
+