diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index 79abd3b..5d570c6 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -529,6 +529,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -893,6 +904,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "url", "uuid", ] @@ -1057,13 +1069,142 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1153,6 +1294,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -1384,6 +1531,8 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tokio-stream", + "tokio-util", ] [[package]] @@ -2230,6 +2379,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "subtle" version = "2.6.1" @@ -2264,6 +2419,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -2334,20 +2500,15 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.40.0" @@ -2388,9 +2549,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -2399,9 +2560,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -2447,27 +2608,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "universal-hash" version = "0.5.1" @@ -2480,15 +2626,27 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2527,6 +2685,7 @@ dependencies = [ "harmony_macros", "log", "tokio", + "url", ] [[package]] @@ -2834,6 +2993,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -2875,6 +3046,30 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2896,8 +3091,51 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] diff --git a/harmony-rs/Cargo.toml b/harmony-rs/Cargo.toml index dbf8814..ff7b3a1 100644 --- a/harmony-rs/Cargo.toml +++ b/harmony-rs/Cargo.toml @@ -17,11 +17,12 @@ log = "0.4.22" env_logger = "0.11.5" derive-new = "0.7.0" async-trait = "0.1.82" -tokio = { version = "1.40.0", features = ["io-std"] } +tokio = { version = "1.40.0", features = ["io-std", "fs"] } cidr = "0.2.3" russh = "0.45.0" russh-keys = "0.45.0" rand = "0.8.5" +url = "2.5.4" [workspace.dependencies.uuid] version = "1.11.0" diff --git a/harmony-rs/demo/vbox-opnsense/Cargo.toml b/harmony-rs/demo/vbox-opnsense/Cargo.toml index 6a460e1..332ad11 100644 --- a/harmony-rs/demo/vbox-opnsense/Cargo.toml +++ b/harmony-rs/demo/vbox-opnsense/Cargo.toml @@ -12,3 +12,4 @@ tokio = { workspace = true } harmony_macros = { version = "1.0.0", path = "../../harmony_macros" } log = { workspace = true } env_logger = { workspace = true } +url = { workspace = true } diff --git a/harmony-rs/demo/vbox-opnsense/src/main.rs b/harmony-rs/demo/vbox-opnsense/src/main.rs index 3a2ee59..167eb0b 100644 --- a/harmony-rs/demo/vbox-opnsense/src/main.rs +++ b/harmony-rs/demo/vbox-opnsense/src/main.rs @@ -9,8 +9,8 @@ use harmony::{ infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, maestro::Maestro, - modules::okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, - topology::{LogicalHost, UnmanagedRouter}, + modules::{okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore}, + topology::{LogicalHost, UnmanagedRouter, Url}, }; use harmony_macros::ip; @@ -23,14 +23,10 @@ async fn main() { name: String::from("opnsense-1"), }; - let firewall = harmony::topology::LogicalHost { - ip: ip!("127.0.0.1"), - name: String::from("opnsense-1"), - }; let opnsense = Arc::new( harmony::infra::opnsense::OPNSenseFirewall::new( firewall, - Some(2222), + None, "lan", "root", "opnsense", @@ -48,6 +44,7 @@ async fn main() { )), load_balancer: opnsense.clone(), firewall: opnsense.clone(), + tftp_server: opnsense.clone(), dhcp_server: opnsense.clone(), dns_server: opnsense.clone(), control_plane: vec![LogicalHost { @@ -81,11 +78,13 @@ async fn main() { // let dhcp_score = OKDBootstrapDhcpScore::new(&topology, &inventory); // let dns_score = OKDBootstrapDnsScore::new(&topology); - let load_balancer_score = - harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); + // let load_balancer_score = + // harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); + let tftp_score = TftpScore::new(Url::LocalFolder("../../../watchguard/tftpboot".to_string())); let maestro = Maestro::new(inventory, topology); // maestro.interpret(dns_score).await.unwrap(); // maestro.interpret(dhcp_score).await.unwrap(); - maestro.interpret(load_balancer_score).await.unwrap(); + // maestro.interpret(load_balancer_score).await.unwrap(); + maestro.interpret(tftp_score).await.unwrap(); } diff --git a/harmony-rs/harmony/Cargo.toml b/harmony-rs/harmony/Cargo.toml index dd3b6c7..4a59c4b 100644 --- a/harmony-rs/harmony/Cargo.toml +++ b/harmony-rs/harmony/Cargo.toml @@ -20,3 +20,4 @@ cidr = { workspace = true } opnsense-config = { path = "../opnsense-config" } opnsense-config-xml = { path = "../opnsense-config-xml" } uuid = { workspace = true } +url = { workspace = true } diff --git a/harmony-rs/harmony/src/domain/interpret/mod.rs b/harmony-rs/harmony/src/domain/interpret/mod.rs index 32f5893..2cc503d 100644 --- a/harmony-rs/harmony/src/domain/interpret/mod.rs +++ b/harmony-rs/harmony/src/domain/interpret/mod.rs @@ -14,6 +14,7 @@ pub enum InterpretName { OPNSenseDHCP, OPNSenseDns, LoadBalancer, + Tftp } impl std::fmt::Display for InterpretName { @@ -22,6 +23,7 @@ impl std::fmt::Display for InterpretName { InterpretName::OPNSenseDHCP => f.write_str("OPNSenseDHCP"), InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"), InterpretName::LoadBalancer => f.write_str("LoadBalancer"), + InterpretName::Tftp => f.write_str("Tftp"), } } } @@ -52,6 +54,13 @@ impl Outcome { message: String::new(), } } + + pub fn success(message: String) -> Self { + Self { + status: InterpretStatus::SUCCESS, + message, + } + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/harmony-rs/harmony/src/domain/topology/mod.rs b/harmony-rs/harmony/src/domain/topology/mod.rs index 80ccd0e..b344357 100644 --- a/harmony-rs/harmony/src/domain/topology/mod.rs +++ b/harmony-rs/harmony/src/domain/topology/mod.rs @@ -1,11 +1,13 @@ mod host_binding; mod load_balancer; mod router; +mod tftp; pub use load_balancer::*; pub use router::*; mod network; pub use host_binding::*; pub use network::*; +pub use tftp::*; use std::{net::IpAddr, sync::Arc}; @@ -16,6 +18,7 @@ pub struct HAClusterTopology { pub load_balancer: Arc, pub firewall: Arc, pub dhcp_server: Arc, + pub tftp_server: Arc, pub dns_server: Arc, pub control_plane: Vec, pub workers: Vec, @@ -24,6 +27,21 @@ pub struct HAClusterTopology { pub type IpAddress = IpAddr; +#[derive(Debug, Clone)] +pub enum Url { + LocalFolder(String), + Remote(url::Url), +} + +impl std::fmt::Display for Url { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Url::LocalFolder(path) => write!(f, "{}", path), + Url::Remote(url) => write!(f, "{}", url), + } + } +} + /// Represents a logical member of a cluster that provides one or more services. /// /// A LogicalHost can represent various roles within the infrastructure, such as: @@ -75,7 +93,11 @@ impl LogicalHost { /// assert_eq!(hosts[2].ip, IpAddress::from_str("192.168.0.22").unwrap()); /// assert_eq!(hosts[2].name, "worker2"); /// ``` - pub fn create_hosts(number_hosts: u32, start_ip: IpAddress, hostname_prefix: &str) -> Vec { + pub fn create_hosts( + number_hosts: u32, + start_ip: IpAddress, + hostname_prefix: &str, + ) -> Vec { let mut hosts = Vec::with_capacity(number_hosts.try_into().unwrap()); for i in 0..number_hosts { let new_ip = increment_ip(start_ip, i).expect("IP address overflow"); diff --git a/harmony-rs/harmony/src/domain/topology/tftp.rs b/harmony-rs/harmony/src/domain/topology/tftp.rs new file mode 100644 index 0000000..ed99bab --- /dev/null +++ b/harmony-rs/harmony/src/domain/topology/tftp.rs @@ -0,0 +1,24 @@ +use crate::executors::ExecutorError; +use async_trait::async_trait; + +use super::{IpAddress, Url}; + +#[async_trait] +pub trait TftpServer: Send + Sync { + async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError>; + fn get_ip(&self) -> IpAddress; + + async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError>; + async fn ensure_initialized(&self) -> Result<(), ExecutorError>; + async fn commit_config(&self) -> Result<(), ExecutorError>; + async fn reload_restart(&self) -> Result<(), ExecutorError>; +} + +impl std::fmt::Debug for dyn TftpServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "TftpServer serving files at {}", + self.get_ip() + )) + } +} diff --git a/harmony-rs/harmony/src/infra/opnsense/dhcp.rs b/harmony-rs/harmony/src/infra/opnsense/dhcp.rs new file mode 100644 index 0000000..d19ce4e --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/dhcp.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; +use log::debug; + +use crate::{executors::ExecutorError, topology::{DHCPStaticEntry, DhcpServer, IpAddress, LogicalHost}}; + +use super::OPNSenseFirewall; + +#[async_trait] +impl DhcpServer for OPNSenseFirewall { + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await + } + + async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { + let mac: String = String::from(&entry.mac); + + { + let mut writable_opnsense = self.opnsense_config.write().await; + writable_opnsense + .dhcp() + .add_static_mapping(&mac, entry.ip, &entry.name) + .unwrap(); + } + + debug!("Registered {:?}", entry); + Ok(()) + } + + async fn remove_static_mapping( + &self, + _mac: &crate::topology::MacAddress, + ) -> Result<(), ExecutorError> { + todo!() + } + + async fn list_static_mappings(&self) -> Vec<(crate::topology::MacAddress, IpAddress)> { + todo!() + } + + fn get_ip(&self) -> IpAddress { + OPNSenseFirewall::get_ip(self) + } + + fn get_host(&self) -> LogicalHost { + 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(()) + } +} diff --git a/harmony-rs/harmony/src/infra/opnsense/dns.rs b/harmony-rs/harmony/src/infra/opnsense/dns.rs new file mode 100644 index 0000000..0026c3f --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/dns.rs @@ -0,0 +1,88 @@ +use crate::infra::opnsense::Host; +use crate::infra::opnsense::IpAddress; +use crate::infra::opnsense::LogicalHost; +use async_trait::async_trait; +use crate::{executors::ExecutorError, topology::{DnsRecord, DnsServer}}; + +use super::OPNSenseFirewall; + +#[async_trait] +impl DnsServer for OPNSenseFirewall { + 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( + &mut self, + _name: &str, + _record_type: crate::topology::DnsRecordType, + ) -> Result<(), ExecutorError> { + 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 { + OPNSenseFirewall::get_ip(&self) + } + + 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/infra/opnsense/firewall.rs b/harmony-rs/harmony/src/infra/opnsense/firewall.rs new file mode 100644 index 0000000..6e3e194 --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/firewall.rs @@ -0,0 +1,24 @@ +use crate::{executors::ExecutorError, topology::{Firewall, FirewallRule, IpAddress, LogicalHost}}; + +use super::OPNSenseFirewall; + +impl Firewall for OPNSenseFirewall { + fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> { + todo!() + } + + fn remove_rule(&mut self, _rule_id: &str) -> Result<(), ExecutorError> { + todo!() + } + + fn list_rules(&self) -> Vec { + todo!() + } + + fn get_ip(&self) -> IpAddress { + OPNSenseFirewall::get_ip(self) + } + fn get_host(&self) -> LogicalHost { + self.host.clone() + } +} diff --git a/harmony-rs/harmony/src/infra/opnsense/haproxy.rs b/harmony-rs/harmony/src/infra/opnsense/load_balancer.rs similarity index 84% rename from harmony-rs/harmony/src/infra/opnsense/haproxy.rs rename to harmony-rs/harmony/src/infra/opnsense/load_balancer.rs index 8562d80..3480657 100644 --- a/harmony-rs/harmony/src/infra/opnsense/haproxy.rs +++ b/harmony-rs/harmony/src/infra/opnsense/load_balancer.rs @@ -1,11 +1,72 @@ +use async_trait::async_trait; use log::{debug, info, warn}; -use opnsense_config::Config; use opnsense_config_xml::{Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer}; use uuid::Uuid; -use crate::topology::{ - BackendServer, HealthCheck, HttpMethod, HttpStatusCode, LoadBalancerService, -}; +use crate::{executors::ExecutorError, topology::{ + BackendServer, HealthCheck, HttpMethod, HttpStatusCode, IpAddress, LoadBalancer, LoadBalancerService, LogicalHost +}}; + +use super::OPNSenseFirewall; + +#[async_trait] +impl LoadBalancer for OPNSenseFirewall { + fn get_ip(&self) -> IpAddress { + OPNSenseFirewall::get_ip(self) + } + fn get_host(&self) -> LogicalHost { + self.host.clone() + } + + async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let (frontend, backend, servers, healthcheck) = + harmony_load_balancer_service_to_haproxy_xml(service); + let mut load_balancer = config.load_balancer(); + load_balancer.add_backend(backend); + load_balancer.add_frontend(frontend); + load_balancer.add_servers(servers); + if let Some(healthcheck) = healthcheck { + load_balancer.add_healthcheck(healthcheck); + } + + Ok(()) + } + + async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { + todo!() + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await?; + todo!("Make sure load balancer is reloaded properly") + } + + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let load_balancer = config.load_balancer(); + if let Some(_) = load_balancer.get_full_config() { + debug!("HAProxy config available in opnsense config, assuming it is already installed"); + return Ok(()); + } + + config.install_package("os-haproxy").await.map_err(|e| { + ExecutorError::UnexpectedError(format!( + "Executor failed when trying to install os-haproxy package with error {e:?}" + )) + })?; + config.load_balancer().enable(true); + Ok(()) + } + + async fn list_services(&self) -> Vec { + let mut config = self.opnsense_config.write().await; + let load_balancer = config.load_balancer(); + let haproxy_xml_config = load_balancer.get_full_config(); + haproxy_xml_config_to_harmony_loadbalancer(haproxy_xml_config) + } +} + pub(crate) fn haproxy_xml_config_to_harmony_loadbalancer( haproxy: &Option, diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index 7c8c514..93a39b0 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -1,22 +1,18 @@ -mod haproxy; +mod dhcp; +mod dns; +mod firewall; +mod load_balancer; mod management; +mod tftp; use std::sync::Arc; -use async_trait::async_trait; -use haproxy::{ - haproxy_xml_config_to_harmony_loadbalancer, harmony_load_balancer_service_to_haproxy_xml, -}; -use log::debug; pub use management::*; use opnsense_config_xml::Host; use tokio::sync::RwLock; use crate::{ executors::ExecutorError, - topology::{ - DHCPStaticEntry, DhcpServer, DnsRecord, DnsServer, Firewall, FirewallRule, IpAddress, - LoadBalancer, LoadBalancerService, LogicalHost, - }, + topology::{IpAddress, LogicalHost}, }; #[derive(Clone)] @@ -56,228 +52,3 @@ impl OPNSenseFirewall { .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) } } - -impl Firewall for OPNSenseFirewall { - fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), ExecutorError> { - todo!() - } - - fn remove_rule(&mut self, _rule_id: &str) -> Result<(), ExecutorError> { - todo!() - } - - fn list_rules(&self) -> Vec { - todo!() - } - - fn get_ip(&self) -> IpAddress { - OPNSenseFirewall::get_ip(self) - } - fn get_host(&self) -> LogicalHost { - self.host.clone() - } -} - -#[async_trait] -impl LoadBalancer for OPNSenseFirewall { - fn get_ip(&self) -> IpAddress { - OPNSenseFirewall::get_ip(self) - } - fn get_host(&self) -> LogicalHost { - self.host.clone() - } - - async fn add_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { - let mut config = self.opnsense_config.write().await; - let (frontend, backend, servers, healthcheck) = - harmony_load_balancer_service_to_haproxy_xml(service); - let mut load_balancer = config.load_balancer(); - load_balancer.add_backend(backend); - load_balancer.add_frontend(frontend); - load_balancer.add_servers(servers); - if let Some(healthcheck) = healthcheck { - load_balancer.add_healthcheck(healthcheck); - } - - Ok(()) - } - - async fn remove_service(&self, service: &LoadBalancerService) -> Result<(), ExecutorError> { - todo!() - } - - async fn commit_config(&self) -> Result<(), ExecutorError> { - OPNSenseFirewall::commit_config(self).await?; - todo!("Make sure load balancer is reloaded properly") - } - - async fn ensure_initialized(&self) -> Result<(), ExecutorError> { - let mut config = self.opnsense_config.write().await; - let load_balancer = config.load_balancer(); - if let Some(_) = load_balancer.get_full_config() { - debug!("HAProxy config available in opnsense config, assuming it is already installed"); - return Ok(()); - } - - config.install_package("os-haproxy").await.map_err(|e| { - ExecutorError::UnexpectedError(format!( - "Executor failed when trying to install os-haproxy package with error {e:?}" - )) - })?; - config.load_balancer().enable(true); - Ok(()) - } - - async fn list_services(&self) -> Vec { - let mut config = self.opnsense_config.write().await; - let load_balancer = config.load_balancer(); - let haproxy_xml_config = load_balancer.get_full_config(); - haproxy_xml_config_to_harmony_loadbalancer(haproxy_xml_config) - } -} - -#[async_trait] -impl DhcpServer for OPNSenseFirewall { - async fn commit_config(&self) -> Result<(), ExecutorError> { - OPNSenseFirewall::commit_config(self).await - } - - async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { - let mac: String = String::from(&entry.mac); - - { - let mut writable_opnsense = self.opnsense_config.write().await; - writable_opnsense - .dhcp() - .add_static_mapping(&mac, entry.ip, &entry.name) - .unwrap(); - } - - debug!("Registered {:?}", entry); - Ok(()) - } - - async fn remove_static_mapping( - &self, - _mac: &crate::topology::MacAddress, - ) -> Result<(), ExecutorError> { - todo!() - } - - async fn list_static_mappings(&self) -> Vec<(crate::topology::MacAddress, IpAddress)> { - todo!() - } - - fn get_ip(&self) -> IpAddress { - OPNSenseFirewall::get_ip(self) - } - - fn get_host(&self) -> LogicalHost { - 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_trait] -impl DnsServer for OPNSenseFirewall { - 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( - &mut self, - _name: &str, - _record_type: crate::topology::DnsRecordType, - ) -> Result<(), ExecutorError> { - 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 { - OPNSenseFirewall::get_ip(&self) - } - - 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/infra/opnsense/tftp.rs b/harmony-rs/harmony/src/infra/opnsense/tftp.rs new file mode 100644 index 0000000..23e5ad9 --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/tftp.rs @@ -0,0 +1,77 @@ +use async_trait::async_trait; +use log::{debug, info}; + +use crate::{ + executors::ExecutorError, + topology::{IpAddress, TftpServer, Url}, +}; + +use super::OPNSenseFirewall; + +#[async_trait] +impl TftpServer for OPNSenseFirewall { + async fn serve_files(&self, url: &Url) -> Result<(), ExecutorError> { + let tftp_root_path = "/usr/local/tftp"; + + let config = self.opnsense_config.write().await; + info!("Uploading files from url {url} to {tftp_root_path}"); + match url { + Url::LocalFolder(path) => { + config + .upload_files(path, tftp_root_path) + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + } + Url::Remote(url) => todo!(), + } + Ok(()) + } + + fn get_ip(&self) -> IpAddress { + OPNSenseFirewall::get_ip(self) + } + + async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> { + info!("Setting listen_ip to {}", &ip); + self.opnsense_config + .write() + .await + .tftp() + .listen_ip(&ip.to_string()); + Ok(()) + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.opnsense_config + .write() + .await + .tftp() + .reload_restart() + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string())) + } + + async fn ensure_initialized(&self) -> Result<(), ExecutorError> { + let mut config = self.opnsense_config.write().await; + let tftp = config.tftp(); + if let None = tftp.get_full_config() { + info!("Tftp config not available in opnsense config, installing package"); + config.install_package("os-tftp").await.map_err(|e| { + ExecutorError::UnexpectedError(format!( + "Executor failed when trying to install os-tftp package with error {e:?}" + )) + })?; + } else { + info!("Tftp config available in opnsense config, assuming it is already installed"); + } + + info!("Enabling tftp server"); + config.tftp().enable(true); + + Ok(()) + } +} diff --git a/harmony-rs/harmony/src/modules/mod.rs b/harmony-rs/harmony/src/modules/mod.rs index ccaa136..b20c025 100644 --- a/harmony-rs/harmony/src/modules/mod.rs +++ b/harmony-rs/harmony/src/modules/mod.rs @@ -2,3 +2,4 @@ pub mod dhcp; pub mod dns; pub mod okd; pub mod load_balancer; +pub mod tftp; diff --git a/harmony-rs/harmony/src/modules/tftp.rs b/harmony-rs/harmony/src/modules/tftp.rs new file mode 100644 index 0000000..f41e5bf --- /dev/null +++ b/harmony-rs/harmony/src/modules/tftp.rs @@ -0,0 +1,64 @@ +use async_trait::async_trait; +use derive_new::new; + +use crate::{ + data::{Id, Version}, + interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, + inventory::Inventory, + score::Score, + topology::{HAClusterTopology, Url}, +}; + +#[derive(Debug, new, Clone)] +pub struct TftpScore { + files_to_serve: Url, +} + +impl Score for TftpScore { + type InterpretType = TftpInterpret; + + fn create_interpret(self) -> Self::InterpretType { + TftpInterpret::new(self) + } +} + +#[derive(Debug, new, Clone)] +pub struct TftpInterpret { + score: TftpScore, +} + +#[async_trait] +impl Interpret for TftpInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let tftp_server = &topology.tftp_server; + tftp_server.ensure_initialized().await?; + tftp_server.set_ip(topology.router.get_gateway()).await?; + tftp_server.serve_files(&self.score.files_to_serve).await?; + tftp_server.commit_config().await?; + tftp_server.reload_restart().await?; + Ok(Outcome::success(format!( + "TFTP Server running and serving files from {}", + self.score.files_to_serve + ))) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Tftp + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index a60390b..a6f1d7f 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -453,57 +453,57 @@ pub struct OPNsenseXmlSection { #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] pub struct Tftp { - general: TftpGeneral, + pub general: TftpGeneral, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] pub struct TftpGeneral { #[yaserde(attribute)] - version: String, - enabled: u8, - listen: String, + pub version: String, + pub enabled: u8, + pub listen: String, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] #[yaserde(rename = "IDS")] pub struct IDS { #[yaserde(attribute)] - version: String, - rules: MaybeString, - policies: MaybeString, + pub version: String, + pub rules: MaybeString, + pub policies: MaybeString, #[yaserde(rename = "userDefinedRules")] - user_defined_rules: MaybeString, - files: MaybeString, + pub user_defined_rules: MaybeString, + pub files: MaybeString, #[yaserde(rename = "fileTags")] - file_tags: MaybeString, - general: IDSGeneral, + pub file_tags: MaybeString, + pub general: IDSGeneral, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] pub struct IDSGeneral { - enabled: Option, - ips: Option, - promisc: Option, - interfaces: String, - homenet: String, + pub enabled: Option, + pub ips: Option, + pub promisc: Option, + pub interfaces: String, + pub homenet: String, #[yaserde(rename = "defaultPacketSize")] - default_packet_size: MaybeString, + pub default_packet_size: MaybeString, #[yaserde(rename = "UpdateCron")] - update_cron: MaybeString, + pub update_cron: MaybeString, #[yaserde(rename = "AlertLogrotate")] - alert_logrotate: String, + pub alert_logrotate: String, #[yaserde(rename = "AlertSaveLogs")] - alert_save_logs: u8, + pub alert_save_logs: u8, #[yaserde(rename = "MPMAlgo")] - mpm_algo: MaybeString, - detect: Detect, - syslog: Option, - syslog_eve: Option, + pub mpm_algo: MaybeString, + pub detect: Detect, + pub syslog: Option, + pub syslog_eve: Option, #[yaserde(rename = "LogPayload")] - log_payload: Option, - verbosity: MaybeString, + pub log_payload: Option, + pub verbosity: MaybeString, #[yaserde(rename = "eveLog")] - eve_log: Option, + pub eve_log: Option, } #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] diff --git a/harmony-rs/opnsense-config/Cargo.toml b/harmony-rs/opnsense-config/Cargo.toml index b75f146..16954eb 100644 --- a/harmony-rs/opnsense-config/Cargo.toml +++ b/harmony-rs/opnsense-config/Cargo.toml @@ -18,6 +18,8 @@ opnsense-config-xml = { path = "../opnsense-config-xml" } chrono = "0.4.38" russh-sftp = "2.0.6" serde_json = "1.0.133" +tokio-util = "0.7.13" +tokio-stream = "0.1.17" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/harmony-rs/opnsense-config/src/config/config.rs b/harmony-rs/opnsense-config/src/config/config.rs index fc998ff..00a84c1 100644 --- a/harmony-rs/opnsense-config/src/config/config.rs +++ b/harmony-rs/opnsense-config/src/config/config.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration}; use crate::{ config::{SshConfigManager, SshCredentials, SshOPNSenseShell}, error::Error, - modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig}, + modules::{dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, tftp::TftpConfig}, }; use log::{info, trace}; use opnsense_config_xml::OPNsense; @@ -38,10 +38,18 @@ impl Config { DnsConfig::new(&mut self.opnsense, self.shell.clone()) } + pub fn tftp(&mut self) -> TftpConfig { + TftpConfig::new(&mut self.opnsense, self.shell.clone()) + } + pub fn load_balancer(&mut self) -> LoadBalancerConfig { LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) } + pub async fn upload_files(&self, source: &str, destination: &str) -> Result { + self.shell.upload_folder(source, destination).await + } + pub async fn install_package(&mut self, package_name: &str) -> Result<(), Error> { info!("Installing opnsense package {package_name}"); let output = self.shell diff --git a/harmony-rs/opnsense-config/src/config/manager/ssh.rs b/harmony-rs/opnsense-config/src/config/manager/ssh.rs index 775c2bb..2b2f3dd 100644 --- a/harmony-rs/opnsense-config/src/config/manager/ssh.rs +++ b/harmony-rs/opnsense-config/src/config/manager/ssh.rs @@ -27,19 +27,25 @@ impl SshConfigManager { let ts = chrono::Utc::now(); let backup_filename = format!("config-{}-harmony.xml", ts.format("%s%.3f")); - self.opnsense_shell.exec(&format!("cp /conf/config.xml /conf/backup/{}", backup_filename)) + self.opnsense_shell + .exec(&format!( + "cp /conf/config.xml /conf/backup/{}", + backup_filename + )) .await } async fn move_to_live_config(&self, new_config_path: &str) -> Result { info!("Overwriting OPNSense /conf/config.xml with {new_config_path}"); - self.opnsense_shell.exec(&format!("mv {new_config_path} /conf/config.xml")) + self.opnsense_shell + .exec(&format!("mv {new_config_path} /conf/config.xml")) .await } async fn reload_all_services(&self) -> Result { info!("Reloading all opnsense services"); - self.opnsense_shell.exec(&format!("configctl service reload all")) + self.opnsense_shell + .exec(&format!("configctl service reload all")) .await } } diff --git a/harmony-rs/opnsense-config/src/config/shell/mod.rs b/harmony-rs/opnsense-config/src/config/shell/mod.rs index 3a644a6..9159606 100644 --- a/harmony-rs/opnsense-config/src/config/shell/mod.rs +++ b/harmony-rs/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 upload_folder(&self, source: &str, destination: &str) -> Result; } #[cfg(test)] @@ -24,4 +25,7 @@ impl OPNsenseShell for DummyOPNSenseShell { async fn write_content_to_temp_file(&self, _content: &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/harmony-rs/opnsense-config/src/config/shell/ssh.rs b/harmony-rs/opnsense-config/src/config/shell/ssh.rs index 4b6eb2c..159ff7c 100644 --- a/harmony-rs/opnsense-config/src/config/shell/ssh.rs +++ b/harmony-rs/opnsense-config/src/config/shell/ssh.rs @@ -3,6 +3,7 @@ use std::{ sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; +use tokio_stream::StreamExt; use async_trait::async_trait; use log::{debug, info}; @@ -11,12 +12,15 @@ use russh::{ Channel, }; use russh_keys::key; -use russh_sftp::client::SftpSession; -use tokio::io::AsyncWriteExt; +use russh_sftp::{client::SftpSession, protocol::OpenFlags}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{config::SshCredentials, Error}; use super::OPNsenseShell; +use tokio::fs::read_dir; +use tokio::fs::File; +use tokio_util::codec::{BytesCodec, FramedRead}; #[derive(Debug)] pub struct SshOPNSenseShell { @@ -54,6 +58,72 @@ impl OPNsenseShell for SshOPNSenseShell { Ok(temp_filename) } + + async fn upload_folder(&self, source: &str, destination: &str) -> Result { + let channel = self.get_ssh_channel().await?; + channel + .request_subsystem(true, "sftp") + .await + .expect("Should request sftp subsystem"); + let sftp = SftpSession::new(channel.into_stream()) + .await + .expect("Should acquire sftp subsystem"); + + if !sftp.try_exists(destination).await? { + info!("Creating remote directory {destination}"); + sftp.create_dir(destination).await?; + } + + info!("Reading local directory {source}"); + let mut entries = read_dir(source).await?; + while let Some(entry) = entries.next_entry().await? { + info!( + "Checking directory entry {}", + entry + .path() + .to_str() + .expect("Directory entry should have a path : {entry:?}") + ); + if entry.file_type().await?.is_file() { + debug!("Got a file"); + let local_path = entry.path(); + debug!("path {local_path:?}"); + let file_name = local_path.file_name().unwrap().to_string_lossy(); + let remote_path = format!("{}/{}", destination, file_name); + info!( + "Uploading local file {} to remote {}", + local_path.to_str().unwrap_or_default(), + remote_path + ); + + debug!("Creating file {remote_path:?}"); + let mut remote_file = sftp.create(remote_path.as_str()).await?; + + debug!("Writing file {remote_path:?}"); + let local_file = File::open(&local_path).await?; + let mut reader = FramedRead::new(local_file, BytesCodec::new()); + + while let Some(result) = reader.next().await { + match result { + Ok(bytes) => { + if !bytes.is_empty() { + remote_file.write(&bytes).await?; + } + } + Err(e) => todo!("Error unhandled {e}"), + }; + } + } else if entry.file_type().await?.is_dir() { + let sub_source = entry.path(); + let sub_destination = + format!("{}/{}", destination, entry.file_name().to_string_lossy()); + self.upload_folder(sub_source.to_str().unwrap(), &sub_destination) + .await?; + } + } + + Ok(destination.to_string()) + } } impl SshOPNSenseShell { @@ -80,6 +150,7 @@ impl SshOPNSenseShell { } pub fn new(host: (IpAddr, u16), credentials: SshCredentials, ssh_config: Arc) -> Self { + info!("Initializing SshOPNSenseShell on host {host:?}"); Self { host, credentials, diff --git a/harmony-rs/opnsense-config/src/error.rs b/harmony-rs/opnsense-config/src/error.rs index 03d3075..dbf237a 100644 --- a/harmony-rs/opnsense-config/src/error.rs +++ b/harmony-rs/opnsense-config/src/error.rs @@ -6,6 +6,8 @@ pub enum Error { Xml(String), #[error("SSH error: {0}")] Ssh(#[from] russh::Error), + #[error("SSH Client error: {0}")] + SftpClient(#[from] russh_sftp::client::error::Error), #[error("Command failed : {0}")] Command(String), #[error("I/O error: {0}")] diff --git a/harmony-rs/opnsense-config/src/modules/mod.rs b/harmony-rs/opnsense-config/src/modules/mod.rs index 8cd675d..a14b91f 100644 --- a/harmony-rs/opnsense-config/src/modules/mod.rs +++ b/harmony-rs/opnsense-config/src/modules/mod.rs @@ -1,3 +1,4 @@ pub mod dhcp; pub mod dns; pub mod load_balancer; +pub mod tftp; diff --git a/harmony-rs/opnsense-config/src/modules/tftp.rs b/harmony-rs/opnsense-config/src/modules/tftp.rs new file mode 100644 index 0000000..67b3bc3 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/tftp.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use opnsense_config_xml::{OPNsense, Tftp}; + +use crate::{config::OPNsenseShell, Error}; + +pub struct TftpConfig<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> TftpConfig<'a> { + pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc) -> Self { + Self { + opnsense, + opnsense_shell, + } + } + + pub fn get_full_config(&self) -> &Option { + &self.opnsense.opnsense.tftp + } + + fn with_tftp(&mut self, f: F) -> R + where + F: FnOnce(&mut Tftp) -> R, + { + match &mut self.opnsense.opnsense.tftp.as_mut() { + Some(tftp) => f(tftp), + None => unimplemented!("Accessing tftp config is not supported when not available yet"), + } + } + + pub fn enable(&mut self, enabled: bool) { + self.with_tftp(|tftp| tftp.general.enabled = enabled as u8); + } + + pub fn listen_ip(&mut self, ip: &str) { + self.with_tftp(|tftp| tftp.general.listen = ip.to_string()); + } + + pub async fn reload_restart(&self) -> Result<(), Error> { + self.opnsense_shell.exec("configctl tftp stop").await?; + self.opnsense_shell.exec("configctl template reload OPNsense/Tftp").await?; + self.opnsense_shell.exec("configctl tftp start").await?; + Ok(()) + } +}