From 478fd9e941f7bf46a3cb256affc4c6af91019238 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 18 Dec 2024 12:38:04 -0500 Subject: [PATCH 1/3] feat: Add score and opnsense implementation to register dhcp leases in dns server --- harmony-rs/Cargo.lock | 11 ++ .../harmony/src/domain/interpret/mod.rs | 2 + .../harmony/src/domain/topology/network.rs | 3 + harmony-rs/harmony/src/infra/opnsense/mod.rs | 23 ++++ harmony-rs/harmony/src/modules/dns.rs | 101 ++++++++++++++++++ harmony-rs/harmony/src/modules/mod.rs | 1 + .../opnsense-config-xml/src/data/opnsense.rs | 36 ++++--- .../opnsense-config/src/config/config.rs | 21 +++- .../src/config/manager/local_file.rs | 6 +- .../opnsense-config/src/config/manager/mod.rs | 1 + .../opnsense-config/src/config/manager/ssh.rs | 7 +- harmony-rs/opnsense-config/src/modules/dns.rs | 29 +++++ harmony-rs/opnsense-config/src/modules/mod.rs | 1 + 13 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 harmony-rs/harmony/src/modules/dns.rs create mode 100644 harmony-rs/opnsense-config/src/modules/dns.rs diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index 5300a66..b40ba49 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -711,6 +711,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fqm" +version = "0.1.0" +dependencies = [ + "cidr", + "env_logger", + "harmony", + "log", + "tokio", +] + [[package]] name = "funty" version = "2.0.0" diff --git a/harmony-rs/harmony/src/domain/interpret/mod.rs b/harmony-rs/harmony/src/domain/interpret/mod.rs index 7ec1362..0b7609b 100644 --- a/harmony-rs/harmony/src/domain/interpret/mod.rs +++ b/harmony-rs/harmony/src/domain/interpret/mod.rs @@ -12,12 +12,14 @@ use super::{ pub enum InterpretName { OPNSenseDHCP, + OPNSenseDns } impl std::fmt::Display for InterpretName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InterpretName::OPNSenseDHCP => f.write_str("OPNSenseDHCP"), + InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"), } } } diff --git a/harmony-rs/harmony/src/domain/topology/network.rs b/harmony-rs/harmony/src/domain/topology/network.rs index ff19dc0..4f04822 100644 --- a/harmony-rs/harmony/src/domain/topology/network.rs +++ b/harmony-rs/harmony/src/domain/topology/network.rs @@ -57,7 +57,9 @@ impl std::fmt::Debug for dyn DhcpServer { } } +#[async_trait] pub trait DnsServer: Send + Sync { + async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; fn add_record( &mut self, name: &str, @@ -72,6 +74,7 @@ pub trait DnsServer: Send + Sync { fn list_records(&self) -> Vec; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; + async fn commit_config(&self) -> Result<(), ExecutorError>; } impl std::fmt::Debug for dyn DnsServer { diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index 7aa4407..f235725 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -156,6 +156,7 @@ impl DhcpServer for OPNSenseFirewall { } } +#[async_trait] impl DnsServer for OPNSenseFirewall { fn add_record( &mut self, @@ -185,4 +186,26 @@ impl DnsServer for OPNSenseFirewall { fn get_host(&self) -> LogicalHost { self.host.clone() } + + async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError> { + let mut writable_opnsense = self.opnsense_config.write().await; + let mut dns = writable_opnsense.dns(); + dns.register_dhcp_leases(register); + + Ok(()) + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + let opnsense = self.opnsense_config.read().await; + + opnsense + .save() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + + opnsense + .restart_dns() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + } } diff --git a/harmony-rs/harmony/src/modules/dns.rs b/harmony-rs/harmony/src/modules/dns.rs new file mode 100644 index 0000000..ff325a6 --- /dev/null +++ b/harmony-rs/harmony/src/modules/dns.rs @@ -0,0 +1,101 @@ +use async_trait::async_trait; +use derive_new::new; +use log::info; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::HAClusterTopology, +}; + +#[derive(Debug, new, Clone)] +pub struct DnsScore { + register_dhcp_leases: Option, +} + +impl Score for DnsScore { + type InterpretType = DnsInterpret; + + fn create_interpret(self) -> Self::InterpretType { + DnsInterpret::new(self) + } +} + +// https://docs.opnsense.org/manual/dhcp.html#advanced-settings +#[derive(Debug, Clone)] +pub struct DnsInterpret { + score: DnsScore, + version: Version, + id: Id, + name: String, + status: InterpretStatus, +} + +impl DnsInterpret { + pub fn new(score: DnsScore) -> Self { + let version = Version::from("1.0.0").expect("Version should be valid"); + let name = "DnsInterpret".to_string(); + let id = Id::from_string(format!("{name}_{version}")); + + Self { + version, + id, + name, + score, + status: InterpretStatus::QUEUED, + } + } + async fn serve_dhcp_entries( + &self, + _inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let dns = topology.dns_server.clone(); + if let Some(register) = self.score.register_dhcp_leases { + dns.register_dhcp_leases(register).await?; + } + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + "DNS Interpret execution successfull".to_string(), + )) + } +} + +#[async_trait] +impl Interpret for DnsInterpret { + fn get_name(&self) -> InterpretName { + InterpretName::OPNSenseDns + } + + fn get_version(&self) -> crate::domain::data::Version { + self.version.clone() + } + + fn get_status(&self) -> InterpretStatus { + self.status.clone() + } + + fn get_children(&self) -> Vec { + todo!() + } + + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + info!("Executing {} on inventory {inventory:?}", self.get_name()); + + self.serve_dhcp_entries(inventory, topology).await?; + + topology.dns_server.commit_config().await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!("Dns Interpret execution successful"), + )) + } +} diff --git a/harmony-rs/harmony/src/modules/mod.rs b/harmony-rs/harmony/src/modules/mod.rs index 84aeb83..e7a05a4 100644 --- a/harmony-rs/harmony/src/modules/mod.rs +++ b/harmony-rs/harmony/src/modules/mod.rs @@ -1,2 +1,3 @@ pub mod dhcp; +pub mod dns; pub mod okd; diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index 04c4977..bc52206 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -426,7 +426,7 @@ pub struct OPNsenseXmlSection { pub syslog: Option, #[yaserde(rename = "TrafficShaper")] pub traffic_shaper: Option, - pub unboundplus: Option, + pub unboundplus: Option, #[yaserde(rename = "DHCRelay")] pub dhcrelay: Option, pub trust: Option, @@ -858,17 +858,17 @@ pub struct Proxy { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct ConfigGeneral { - pub enabled: i32, + pub enabled: i8, #[yaserde(rename = "error_pages")] pub error_pages: String, pub icpPort: MaybeString, pub logging: Logging, pub alternateDNSservers: MaybeString, - pub dnsV4First: i32, + pub dnsV4First: i8, pub forwardedForHandling: String, pub uriWhitespaceHandling: String, - pub enablePinger: i32, - pub useViaHeader: i32, + pub enablePinger: i8, + pub useViaHeader: i8, pub suppressVersion: i32, pub connecttimeout: MaybeString, #[yaserde(rename = "VisibleEmail")] @@ -889,8 +889,8 @@ pub struct Logging { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Enable { - pub accessLog: i32, - pub storeLog: i32, + pub accessLog: i8, + pub storeLog: i8, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -900,7 +900,7 @@ pub struct Cache { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct LocalCache { - pub enabled: i32, + pub enabled: i8, pub directory: String, pub cache_mem: i32, pub maximum_object_size: MaybeString, @@ -1069,7 +1069,7 @@ pub struct UnboundPlus { pub dots: MaybeString, pub hosts: Hosts, pub aliases: MaybeString, - pub domains: MaybeString, + pub domains: Option, } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1082,9 +1082,9 @@ pub struct UnboundGeneral { pub dns64: MaybeString, pub dns64prefix: MaybeString, pub noarecords: MaybeString, - pub regdhcp: i32, + pub regdhcp: i8, pub regdhcpdomain: MaybeString, - pub regdhcpstatic: i32, + pub regdhcpstatic: i8, pub noreglladdr6: MaybeString, pub noregrecords: MaybeString, pub txtsupport: MaybeString, @@ -1096,12 +1096,13 @@ pub struct UnboundGeneral { #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Advanced { - pub hideidentity: i32, - pub hideversion: i32, - pub prefetch: i32, - pub prefetchkey: i32, - pub dnssecstripped: i32, - pub serveexpired: i32, + pub hideidentity: i8, + pub hideversion: i8, + pub prefetch: i8, + pub prefetchkey: i8, + pub dnssecstripped: i8, + pub aggressivensec: i8, + pub serveexpired: i8, pub serveexpiredreplyttl: MaybeString, pub serveexpiredttl: MaybeString, pub serveexpiredttlreset: i32, @@ -1125,6 +1126,7 @@ pub struct Advanced { pub numqueriesperthread: MaybeString, pub outgoingrange: MaybeString, pub jostletimeout: MaybeString, + pub discardtimeout: MaybeString, pub cachemaxttl: MaybeString, pub cachemaxnegativettl: MaybeString, pub cacheminttl: MaybeString, diff --git a/harmony-rs/opnsense-config/src/config/config.rs b/harmony-rs/opnsense-config/src/config/config.rs index b38fa29..0336223 100644 --- a/harmony-rs/opnsense-config/src/config/config.rs +++ b/harmony-rs/opnsense-config/src/config/config.rs @@ -1,6 +1,6 @@ use std::{net::Ipv4Addr, sync::Arc, time::Duration}; -use crate::{config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, error::Error, modules::dhcp::DhcpConfig}; +use crate::{config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, error::Error, modules::{dhcp::DhcpConfig, dns::DnsConfig}}; use log::trace; use opnsense_config_xml::OPNsense; use russh::client; @@ -35,6 +35,25 @@ impl Config { DhcpConfig::new(&mut self.opnsense, self.shell.clone()) } + pub fn dns(&mut self) -> DnsConfig { + DnsConfig::new(&mut self.opnsense, self.shell.clone()) + } + + pub async fn restart_dns(&self) -> Result<(), Error> { + self.shell.exec("configctl unbound restart").await?; + Ok(()) + } + + /// Save the config to the repository. This method is meant NOT to reload services, only save + /// the config to the live file/database and perhaps take a backup when relevant. + pub async fn save(&self) -> Result<(), Error> { + self.repository + .save_config(&self.opnsense.to_xml()) + .await + } + + /// Save the configuration and reload all services. Be careful with this one as it will cause + /// downtime in many cases, such as a PPPoE renegociation pub async fn apply(&self) -> Result<(), Error> { self.repository .apply_new_config(&self.opnsense.to_xml()) diff --git a/harmony-rs/opnsense-config/src/config/manager/local_file.rs b/harmony-rs/opnsense-config/src/config/manager/local_file.rs index 7e773c8..804b74c 100644 --- a/harmony-rs/opnsense-config/src/config/manager/local_file.rs +++ b/harmony-rs/opnsense-config/src/config/manager/local_file.rs @@ -20,7 +20,11 @@ impl ConfigManager for LocalFileConfigManager { Ok(fs::read_to_string(&self.file_path)?) } - async fn apply_new_config(&self, content: &str) -> Result<(), Error> { + async fn save_config(&self, content: &str) -> Result<(), Error> { Ok(fs::write(&self.file_path, content)?) } + + async fn apply_new_config(&self, content: &str) -> Result<(), Error> { + self.save_config(content).await + } } diff --git a/harmony-rs/opnsense-config/src/config/manager/mod.rs b/harmony-rs/opnsense-config/src/config/manager/mod.rs index 42f95ca..c71af1d 100644 --- a/harmony-rs/opnsense-config/src/config/manager/mod.rs +++ b/harmony-rs/opnsense-config/src/config/manager/mod.rs @@ -9,5 +9,6 @@ use crate::Error; #[async_trait] pub trait ConfigManager: std::fmt::Debug + Send + Sync { async fn load_as_str(&self) -> Result; + async fn save_config(&self, content: &str) -> Result<(), Error>; async fn apply_new_config(&self, content: &str) -> Result<(), Error>; } diff --git a/harmony-rs/opnsense-config/src/config/manager/ssh.rs b/harmony-rs/opnsense-config/src/config/manager/ssh.rs index fdd8d03..775c2bb 100644 --- a/harmony-rs/opnsense-config/src/config/manager/ssh.rs +++ b/harmony-rs/opnsense-config/src/config/manager/ssh.rs @@ -50,13 +50,18 @@ impl ConfigManager for SshConfigManager { self.opnsense_shell.exec("cat /conf/config.xml").await } - async fn apply_new_config(&self, content: &str) -> Result<(), Error> { + async fn save_config(&self, content: &str) -> Result<(), Error> { let temp_filename = self .opnsense_shell .write_content_to_temp_file(content) .await?; self.backup_config_remote().await?; self.move_to_live_config(&temp_filename).await?; + Ok(()) + } + + async fn apply_new_config(&self, content: &str) -> Result<(), Error> { + self.save_config(content).await?; self.reload_all_services().await?; Ok(()) } diff --git a/harmony-rs/opnsense-config/src/modules/dns.rs b/harmony-rs/opnsense-config/src/modules/dns.rs new file mode 100644 index 0000000..016d5e0 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/dns.rs @@ -0,0 +1,29 @@ +use std::sync::Arc; + +use opnsense_config_xml::OPNsense; + +use crate::config::OPNsenseShell; + +pub struct DnsConfig<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> DnsConfig<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + pub fn register_dhcp_leases(&mut self, register: bool) { + let unbound = match &mut self.opnsense.opnsense.unboundplus { + Some(unbound) => unbound, + None => todo!("Handle case where unboundplus is not used"), + }; + + unbound.general.regdhcp = register as i8; + unbound.general.regdhcpstatic = register as i8; + } +} diff --git a/harmony-rs/opnsense-config/src/modules/mod.rs b/harmony-rs/opnsense-config/src/modules/mod.rs index 5833493..87bd11c 100644 --- a/harmony-rs/opnsense-config/src/modules/mod.rs +++ b/harmony-rs/opnsense-config/src/modules/mod.rs @@ -1 +1,2 @@ pub mod dhcp; +pub mod dns; From 0247252474ca7bd4eee4761bbdc07a1418c8cb8c Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Wed, 18 Dec 2024 10:46:36 -0500 Subject: [PATCH 2/3] feat: don't crash if ip:mac already exist, just skip it --- harmony-rs/opnsense-config/src/modules/dhcp.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/harmony-rs/opnsense-config/src/modules/dhcp.rs b/harmony-rs/opnsense-config/src/modules/dhcp.rs index ae9ccbc..deacda3 100644 --- a/harmony-rs/opnsense-config/src/modules/dhcp.rs +++ b/harmony-rs/opnsense-config/src/modules/dhcp.rs @@ -1,3 +1,4 @@ +use log::info; use opnsense_config_xml::MaybeString; use opnsense_config_xml::Range; use opnsense_config_xml::StaticMap; @@ -93,6 +94,21 @@ impl<'a> DhcpConfig<'a> { // return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); // } + if existing_mappings.iter().any(|m| { + m.ipaddr + .parse::() + .expect("Mapping contains invalid ipv4") + == ipaddr + && m.mac == mac + }) { + info!( + "Mapping already exists for {} [{}], skipping", + ipaddr.to_string(), + mac + ); + return Ok(()); + } + if existing_mappings.iter().any(|m| { m.ipaddr .parse::() From 367e96b36a0d3be7ed7a20030650d54e2e7dbaa2 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Wed, 18 Dec 2024 15:58:49 -0500 Subject: [PATCH 3/3] feat: Add OKD DNS score with DNS entries and registering dhcp leases --- harmony-rs/Cargo.lock | 24 +++ harmony-rs/harmony/Cargo.toml | 1 + harmony-rs/harmony/src/domain/topology/mod.rs | 1 + .../harmony/src/domain/topology/network.rs | 200 ++++++++++++++++-- harmony-rs/harmony/src/infra/opnsense/mod.rs | 50 ++++- harmony-rs/harmony/src/modules/dns.rs | 23 +- harmony-rs/harmony/src/modules/okd/dns.rs | 48 +++++ harmony-rs/harmony/src/modules/okd/mod.rs | 1 + harmony-rs/opnsense-config-xml/Cargo.toml | 8 + .../opnsense-config-xml/src/data/opnsense.rs | 24 ++- harmony-rs/opnsense-config/src/modules/dns.rs | 18 +- 11 files changed, 367 insertions(+), 31 deletions(-) create mode 100644 harmony-rs/harmony/src/modules/okd/dns.rs diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index b40ba49..4f7478f 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -896,6 +896,7 @@ dependencies = [ "libredfish", "log", "opnsense-config", + "opnsense-config-xml", "reqwest", "russh", "rust-ipmi", @@ -1406,6 +1407,7 @@ dependencies = [ "serde", "thiserror", "tokio", + "uuid", "xml-rs", "yaserde", "yaserde_derive", @@ -2502,6 +2504,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", + "rand", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/harmony-rs/harmony/Cargo.toml b/harmony-rs/harmony/Cargo.toml index 8029cf1..753fc3b 100644 --- a/harmony-rs/harmony/Cargo.toml +++ b/harmony-rs/harmony/Cargo.toml @@ -18,3 +18,4 @@ env_logger = { workspace = true } async-trait = { workspace = true } cidr = { workspace = true } opnsense-config = { path = "../opnsense-config" } +opnsense-config-xml = { path = "../opnsense-config-xml" } diff --git a/harmony-rs/harmony/src/domain/topology/mod.rs b/harmony-rs/harmony/src/domain/topology/mod.rs index ccad53b..80ccd0e 100644 --- a/harmony-rs/harmony/src/domain/topology/mod.rs +++ b/harmony-rs/harmony/src/domain/topology/mod.rs @@ -11,6 +11,7 @@ use std::{net::IpAddr, sync::Arc}; #[derive(Debug, Clone)] pub struct HAClusterTopology { + pub domain_name: String, pub router: Arc, pub load_balancer: Arc, pub firewall: Arc, diff --git a/harmony-rs/harmony/src/domain/topology/network.rs b/harmony-rs/harmony/src/domain/topology/network.rs index 4f04822..affdcd6 100644 --- a/harmony-rs/harmony/src/domain/topology/network.rs +++ b/harmony-rs/harmony/src/domain/topology/network.rs @@ -1,4 +1,4 @@ -use std::net::Ipv4Addr; +use std::{error::Error, net::Ipv4Addr, str::FromStr}; use async_trait::async_trait; @@ -60,21 +60,32 @@ impl std::fmt::Debug for dyn DhcpServer { #[async_trait] pub trait DnsServer: Send + Sync { async fn register_dhcp_leases(&self, register: bool) -> Result<(), ExecutorError>; - fn add_record( - &mut self, - name: &str, - record_type: DnsRecordType, - value: &str, - ) -> Result<(), ExecutorError>; + async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError>; fn remove_record( &mut self, name: &str, record_type: DnsRecordType, ) -> Result<(), ExecutorError>; - fn list_records(&self) -> Vec; + async fn list_records(&self) -> Vec; fn get_ip(&self) -> IpAddress; fn get_host(&self) -> LogicalHost; async fn commit_config(&self) -> Result<(), ExecutorError>; + async fn ensure_hosts_registered(&self, hosts: Vec) -> Result<(), ExecutorError> { + let current_hosts = self.list_records().await; + let mut hosts_to_register = vec![]; + + for host in hosts { + if !current_hosts.iter().any(|h| h == &host) { + hosts_to_register.push(host); + } + } + + if !hosts_to_register.is_empty() { + self.register_hosts(hosts_to_register).await?; + } + + Ok(()) + } } impl std::fmt::Debug for dyn DnsServer { @@ -136,7 +147,7 @@ impl std::fmt::Display for MacAddress { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum DnsRecordType { A, AAAA, @@ -145,9 +156,170 @@ pub enum DnsRecordType { TXT, } -#[derive(Clone, Debug)] -pub struct DnsRecord { - pub name: String, - pub record_type: DnsRecordType, - pub value: String, +impl std::fmt::Display for DnsRecordType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DnsRecordType::A => write!(f, "A"), + DnsRecordType::AAAA => write!(f, "AAAA"), + DnsRecordType::CNAME => write!(f, "CNAME"), + DnsRecordType::MX => write!(f, "MX"), + DnsRecordType::TXT => write!(f, "TXT"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DnsRecord { + pub host: String, + pub domain: String, + pub record_type: DnsRecordType, + pub value: IpAddress, +} + +impl FromStr for DnsRecordType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "A" => Ok(DnsRecordType::A), + "AAAA" => Ok(DnsRecordType::AAAA), + "CNAME" => Ok(DnsRecordType::CNAME), + "MX" => Ok(DnsRecordType::MX), + "TXT" => Ok(DnsRecordType::TXT), + _ => Err(format!("Unknown DNSRecordType {s}")), + } + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use tokio::sync::RwLock; + + use super::*; + + #[tokio::test] + async fn test_ensure_hosts_registered_no_new_hosts() { + let server = DummyDnsServer::default(); + let existing_host = DnsRecord { + host: "existing".to_string(), + domain: "example.com".to_string(), + record_type: DnsRecordType::A, + value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 2)), + }; + + server + .register_hosts(vec![existing_host.clone()]) + .await + .unwrap(); + + let new_hosts = vec![ + existing_host, // already exists + ]; + + server.ensure_hosts_registered(new_hosts).await.unwrap(); + assert_eq!(server.list_records().await.len(), 1); + } + + #[tokio::test] + async fn test_ensure_hosts_registered_with_new_hosts() { + let server = DummyDnsServer::default(); + + let existing_host = DnsRecord { + host: "existing".to_string(), + domain: "example.com".to_string(), + record_type: DnsRecordType::A, + value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 2)), + }; + + server + .register_hosts(vec![existing_host.clone()]) + .await + .unwrap(); + + let new_hosts = vec![ + existing_host.clone(), // already exists + DnsRecord { + host: "new".to_string(), + domain: "example.com".to_string(), + record_type: DnsRecordType::A, + value: IpAddress::V4(Ipv4Addr::new(192, 168, 1, 3)), + }, + ]; + + server.ensure_hosts_registered(new_hosts).await.unwrap(); + assert_eq!(server.list_records().await.len(), 2); + } + + #[tokio::test] + async fn test_ensure_hosts_registered_no_hosts() { + let server = DummyDnsServer::default(); + + let new_hosts = vec![]; + + server.ensure_hosts_registered(new_hosts).await.unwrap(); + assert_eq!(server.list_records().await.len(), 0); + } + + #[tokio::test] + async fn test_ensure_existing_host_kept_no_new_host() { + let server = DummyDnsServer::default(); + + let new_hosts = vec![]; + + server.ensure_hosts_registered(new_hosts).await.unwrap(); + assert_eq!(server.list_records().await.len(), 0); + } + + #[async_trait::async_trait] + impl DnsServer for DummyDnsServer { + async fn register_dhcp_leases(&self, _register: bool) -> Result<(), ExecutorError> { + Ok(()) + } + + async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError> { + self.hosts.write().await.extend(hosts); + Ok(()) + } + + fn remove_record( + &mut self, + _name: &str, + _record_type: DnsRecordType, + ) -> Result<(), ExecutorError> { + Ok(()) + } + + async fn list_records(&self) -> Vec { + self.hosts.read().await.clone() + } + + fn get_ip(&self) -> IpAddress { + IpAddress::V4(Ipv4Addr::new(192, 168, 0, 1)) + } + + fn get_host(&self) -> LogicalHost { + LogicalHost { + ip: self.get_ip(), + name: "dummy-host".to_string(), + } + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + Ok(()) + } + } + + struct DummyDnsServer { + hosts: Arc>>, + } + + impl Default for DummyDnsServer { + fn default() -> Self { + DummyDnsServer { + hosts: Arc::new(RwLock::new(vec![])), + } + } + } } diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index f235725..e69801c 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -4,13 +4,14 @@ use std::sync::Arc; use async_trait::async_trait; use log::debug; pub use management::*; +use opnsense_config_xml::Host; use tokio::sync::RwLock; use crate::{ executors::ExecutorError, topology::{ - Backend, DHCPStaticEntry, DhcpServer, DnsServer, Firewall, FirewallRule, Frontend, - IpAddress, LoadBalancer, LogicalHost, + Backend, DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule, + Frontend, IpAddress, LoadBalancer, LogicalHost, }, }; @@ -158,13 +159,22 @@ impl DhcpServer for OPNSenseFirewall { #[async_trait] impl DnsServer for OPNSenseFirewall { - fn add_record( - &mut self, - _name: &str, - _record_type: crate::topology::DnsRecordType, - _value: &str, - ) -> Result<(), ExecutorError> { - todo!() + async fn register_hosts(&self, hosts: Vec) -> Result<(), ExecutorError> { + let mut writable_opnsense = self.opnsense_config.write().await; + let mut dns = writable_opnsense.dns(); + let hosts = hosts + .iter() + .map(|h| { + Host::new( + h.host.clone(), + h.domain.clone(), + h.record_type.to_string(), + h.value.to_string(), + ) + }) + .collect(); + dns.register_hosts(hosts); + Ok(()) } fn remove_record( @@ -175,8 +185,26 @@ impl DnsServer for OPNSenseFirewall { todo!() } - fn list_records(&self) -> Vec { - todo!() + async fn list_records(&self) -> Vec { + self.opnsense_config + .write() + .await + .dns() + .get_hosts() + .iter() + .map(|h| DnsRecord { + host: h.hostname.clone(), + domain: h.domain.clone(), + record_type: h + .rr + .parse() + .expect("received invalid record type {h.rr} from opnsense"), + value: h + .server + .parse() + .expect("received invalid ipv4 record from opnsense {h.server}"), + }) + .collect() } fn get_ip(&self) -> IpAddress { diff --git a/harmony-rs/harmony/src/modules/dns.rs b/harmony-rs/harmony/src/modules/dns.rs index ff325a6..76c5be1 100644 --- a/harmony-rs/harmony/src/modules/dns.rs +++ b/harmony-rs/harmony/src/modules/dns.rs @@ -7,11 +7,12 @@ use crate::{ interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, - topology::HAClusterTopology, + topology::{DnsRecord, HAClusterTopology}, }; #[derive(Debug, new, Clone)] pub struct DnsScore { + dns_entries: Vec, register_dhcp_leases: Option, } @@ -62,6 +63,25 @@ impl DnsInterpret { "DNS Interpret execution successfull".to_string(), )) } + + async fn ensure_hosts_registered( + &self, + topology: &HAClusterTopology, + ) -> Result { + let entries = &self.score.dns_entries; + topology + .dns_server + .ensure_hosts_registered(entries.clone()) + .await?; + + Ok(Outcome::new( + InterpretStatus::SUCCESS, + format!( + "DnsInterpret registered {} hosts successfully", + entries.len() + ), + )) + } } #[async_trait] @@ -90,6 +110,7 @@ impl Interpret for DnsInterpret { info!("Executing {} on inventory {inventory:?}", self.get_name()); self.serve_dhcp_entries(inventory, topology).await?; + self.ensure_hosts_registered(&topology).await?; topology.dns_server.commit_config().await?; diff --git a/harmony-rs/harmony/src/modules/okd/dns.rs b/harmony-rs/harmony/src/modules/okd/dns.rs new file mode 100644 index 0000000..38d6c1e --- /dev/null +++ b/harmony-rs/harmony/src/modules/okd/dns.rs @@ -0,0 +1,48 @@ +use crate::{ + modules::dns::DnsScore, + score::Score, + topology::{DnsRecord, DnsRecordType, HAClusterTopology}, +}; + +#[derive(Debug)] +pub struct OKDBootstrapDnsScore { + dns_score: DnsScore, +} + +impl OKDBootstrapDnsScore { + pub fn new(topology: &HAClusterTopology) -> Self { + let cluster_domain_name = &topology.domain_name; + let dns_entries = vec![ + DnsRecord { + host: "api".to_string(), + domain: cluster_domain_name.clone(), + record_type: DnsRecordType::A, + value: topology.dns_server.get_ip(), + }, + DnsRecord { + host: "api-int".to_string(), + domain: cluster_domain_name.clone(), + record_type: DnsRecordType::A, + value: topology.dns_server.get_ip(), + }, + DnsRecord { + host: "*".to_string(), + domain: format!("apps.{}", cluster_domain_name), + record_type: DnsRecordType::A, + value: topology.dns_server.get_ip(), + }, + ]; + + Self { + dns_score: DnsScore::new(dns_entries, Some(true)), + } + } +} + +impl Score for OKDBootstrapDnsScore { + type InterpretType = ::InterpretType; + + fn create_interpret(self) -> Self::InterpretType { + self.dns_score.create_interpret() + } +} diff --git a/harmony-rs/harmony/src/modules/okd/mod.rs b/harmony-rs/harmony/src/modules/okd/mod.rs index 8c32480..62802ea 100644 --- a/harmony-rs/harmony/src/modules/okd/mod.rs +++ b/harmony-rs/harmony/src/modules/okd/mod.rs @@ -1,2 +1,3 @@ pub mod dhcp; +pub mod dns; diff --git a/harmony-rs/opnsense-config-xml/Cargo.toml b/harmony-rs/opnsense-config-xml/Cargo.toml index 3b28dcb..eef282c 100644 --- a/harmony-rs/opnsense-config-xml/Cargo.toml +++ b/harmony-rs/opnsense-config-xml/Cargo.toml @@ -18,6 +18,14 @@ thiserror = "1.0" async-trait = { workspace = true } tokio = { workspace = true } +[dependencies.uuid] +version = "1.11.0" +features = [ + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs +] + [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index bc52206..73b9171 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -1,5 +1,6 @@ use crate::{data::dhcpd::DhcpInterface, xml_utils::to_xml_str}; use log::error; +use uuid::Uuid; use yaserde::{MaybeString, NamedList, RawXml}; use yaserde_derive::{YaDeserialize, YaSerialize}; @@ -1167,18 +1168,34 @@ pub struct Hosts { pub hosts: Vec, } -#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +#[derive(Default, Clone, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct Host { #[yaserde(attribute)] pub uuid: String, - pub enabled: i32, + pub enabled: i8, pub hostname: String, pub domain: String, pub rr: String, pub mxprio: MaybeString, pub mx: MaybeString, pub server: String, - pub description: String, + pub description: Option, +} + +impl Host { + pub fn new(hostname: String, domain: String, rr: String, server: String) -> Self { + Host { + uuid: Uuid::new_v4().to_string(), + enabled: true as i8, + hostname, + domain, + rr, + server, + mxprio: MaybeString::default(), + mx: MaybeString::default(), + description: None + } + } } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] @@ -1468,7 +1485,6 @@ pub struct Tuning { pub h2_max_concurrent_streams_outgoing: Option, #[yaserde(rename = "h2_maxConcurrentStreamsIncoming")] pub h2_max_concurrent_streams_incoming: Option, - } #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] diff --git a/harmony-rs/opnsense-config/src/modules/dns.rs b/harmony-rs/opnsense-config/src/modules/dns.rs index 016d5e0..05fd748 100644 --- a/harmony-rs/opnsense-config/src/modules/dns.rs +++ b/harmony-rs/opnsense-config/src/modules/dns.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use opnsense_config_xml::OPNsense; +use opnsense_config_xml::{Host, OPNsense}; use crate::config::OPNsenseShell; @@ -17,6 +17,22 @@ impl<'a> DnsConfig<'a> { } } + pub fn register_hosts(&mut self, mut hosts: Vec) { + let unbound = match &mut self.opnsense.opnsense.unboundplus { + Some(unbound) => unbound, + None => todo!("Handle case where unboundplus is not used"), + }; + unbound.hosts.hosts.append(&mut hosts); + } + + pub fn get_hosts(&self) -> Vec { + let unbound = match &self.opnsense.opnsense.unboundplus { + Some(unbound) => unbound, + None => todo!("Handle case where unboundplus is not used"), + }; + unbound.hosts.hosts.clone() + } + pub fn register_dhcp_leases(&mut self, register: bool) { let unbound = match &mut self.opnsense.opnsense.unboundplus { Some(unbound) => unbound,