diff --git a/harmony-rs/demo/vbox-opnsense/src/main.rs b/harmony-rs/demo/vbox-opnsense/src/main.rs index 167eb0b..e8956f7 100644 --- a/harmony-rs/demo/vbox-opnsense/src/main.rs +++ b/harmony-rs/demo/vbox-opnsense/src/main.rs @@ -9,7 +9,9 @@ use harmony::{ infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, maestro::Maestro, - modules::{okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore}, + modules::{ + http::HttpScore, okd::{dhcp::OKDBootstrapDhcpScore, dns::OKDBootstrapDnsScore}, tftp::TftpScore + }, topology::{LogicalHost, UnmanagedRouter, Url}, }; use harmony_macros::ip; @@ -24,14 +26,8 @@ async fn main() { }; let opnsense = Arc::new( - harmony::infra::opnsense::OPNSenseFirewall::new( - firewall, - None, - "lan", - "root", - "opnsense", - ) - .await, + harmony::infra::opnsense::OPNSenseFirewall::new(firewall, None, "lan", "root", "opnsense") + .await, ); let lan_subnet = Ipv4Addr::new(10, 100, 8, 0); let gateway_ipv4 = Ipv4Addr::new(10, 100, 8, 1); @@ -45,6 +41,7 @@ async fn main() { load_balancer: opnsense.clone(), firewall: opnsense.clone(), tftp_server: opnsense.clone(), + http_server: opnsense.clone(), dhcp_server: opnsense.clone(), dns_server: opnsense.clone(), control_plane: vec![LogicalHost { @@ -82,9 +79,13 @@ async fn main() { // harmony::modules::okd::load_balancer::OKDLoadBalancerScore::new(&topology); let tftp_score = TftpScore::new(Url::LocalFolder("../../../watchguard/tftpboot".to_string())); + let http_score = HttpScore::new(Url::LocalFolder( + "../../../watchguard/pxe-http-files".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(tftp_score).await.unwrap(); + // maestro.interpret(tftp_score).await.unwrap(); + maestro.interpret(http_score).await.unwrap(); } diff --git a/harmony-rs/harmony/src/domain/interpret/mod.rs b/harmony-rs/harmony/src/domain/interpret/mod.rs index 2cc503d..8260382 100644 --- a/harmony-rs/harmony/src/domain/interpret/mod.rs +++ b/harmony-rs/harmony/src/domain/interpret/mod.rs @@ -14,7 +14,8 @@ pub enum InterpretName { OPNSenseDHCP, OPNSenseDns, LoadBalancer, - Tftp + Tftp, + Http, } impl std::fmt::Display for InterpretName { @@ -24,6 +25,7 @@ impl std::fmt::Display for InterpretName { InterpretName::OPNSenseDns => f.write_str("OPNSenseDns"), InterpretName::LoadBalancer => f.write_str("LoadBalancer"), InterpretName::Tftp => f.write_str("Tftp"), + InterpretName::Http => f.write_str("Http"), } } } diff --git a/harmony-rs/harmony/src/domain/topology/http.rs b/harmony-rs/harmony/src/domain/topology/http.rs new file mode 100644 index 0000000..42a38af --- /dev/null +++ b/harmony-rs/harmony/src/domain/topology/http.rs @@ -0,0 +1,24 @@ +use crate::executors::ExecutorError; +use async_trait::async_trait; + +use super::{IpAddress, Url}; + +#[async_trait] +pub trait HttpServer: 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 HttpServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "HttpServer serving files at {}", + self.get_ip() + )) + } +} diff --git a/harmony-rs/harmony/src/domain/topology/mod.rs b/harmony-rs/harmony/src/domain/topology/mod.rs index b344357..9d2fc8a 100644 --- a/harmony-rs/harmony/src/domain/topology/mod.rs +++ b/harmony-rs/harmony/src/domain/topology/mod.rs @@ -2,12 +2,14 @@ mod host_binding; mod load_balancer; mod router; mod tftp; +mod http; pub use load_balancer::*; pub use router::*; mod network; pub use host_binding::*; pub use network::*; pub use tftp::*; +pub use http::*; use std::{net::IpAddr, sync::Arc}; @@ -19,6 +21,7 @@ pub struct HAClusterTopology { pub firewall: Arc, pub dhcp_server: Arc, pub tftp_server: Arc, + pub http_server: Arc, pub dns_server: Arc, pub control_plane: Vec, pub workers: Vec, diff --git a/harmony-rs/harmony/src/infra/opnsense/http.rs b/harmony-rs/harmony/src/infra/opnsense/http.rs new file mode 100644 index 0000000..e824392 --- /dev/null +++ b/harmony-rs/harmony/src/infra/opnsense/http.rs @@ -0,0 +1,75 @@ +use async_trait::async_trait; +use log::{debug, info}; + +use crate::{ + executors::ExecutorError, + topology::{HttpServer, IpAddress, Url}, +}; + +use super::OPNSenseFirewall; + +#[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}"); + match url { + Url::LocalFolder(path) => { + config + .upload_files(path, http_root_path) + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + } + Url::Remote(url) => todo!(), + } + Ok(()) + } + + fn get_ip(&self) -> IpAddress { + todo!(); + } + + async fn commit_config(&self) -> Result<(), ExecutorError> { + OPNSenseFirewall::commit_config(self).await + } + + async fn reload_restart(&self) -> Result<(), ExecutorError> { + self.opnsense_config + .write() + .await + .caddy() + .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 caddy = config.caddy(); + if let None = caddy.get_full_config() { + info!("Http config not available in opnsense config, installing package"); + config.install_package("os-caddy").await.map_err(|e| { + ExecutorError::UnexpectedError(format!( + "Executor failed when trying to install os-caddy package with error {e:?}" + )) + })?; + } else { + info!("Http config available in opnsense config, assuming it is already installed"); + } + info!("Adding custom caddy config files"); + config + .upload_files( + "../../../watchguard/caddy_config", + "/usr/local/etc/caddy/caddy.d/", + ) + .await + .map_err(|e| ExecutorError::UnexpectedError(e.to_string()))?; + + info!("Enabling http server"); + config.caddy().enable(true); + + Ok(()) + } +} diff --git a/harmony-rs/harmony/src/infra/opnsense/mod.rs b/harmony-rs/harmony/src/infra/opnsense/mod.rs index 93a39b0..502690c 100644 --- a/harmony-rs/harmony/src/infra/opnsense/mod.rs +++ b/harmony-rs/harmony/src/infra/opnsense/mod.rs @@ -4,6 +4,7 @@ mod firewall; mod load_balancer; mod management; mod tftp; +mod http; use std::sync::Arc; pub use management::*; diff --git a/harmony-rs/harmony/src/infra/opnsense/tftp.rs b/harmony-rs/harmony/src/infra/opnsense/tftp.rs index 23e5ad9..0c8b293 100644 --- a/harmony-rs/harmony/src/infra/opnsense/tftp.rs +++ b/harmony-rs/harmony/src/infra/opnsense/tftp.rs @@ -13,7 +13,7 @@ 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; + let config = self.opnsense_config.read().await; info!("Uploading files from url {url} to {tftp_root_path}"); match url { Url::LocalFolder(path) => { @@ -28,7 +28,7 @@ impl TftpServer for OPNSenseFirewall { } fn get_ip(&self) -> IpAddress { - OPNSenseFirewall::get_ip(self) + todo!() } async fn set_ip(&self, ip: IpAddress) -> Result<(), ExecutorError> { diff --git a/harmony-rs/harmony/src/modules/http.rs b/harmony-rs/harmony/src/modules/http.rs new file mode 100644 index 0000000..d6dd374 --- /dev/null +++ b/harmony-rs/harmony/src/modules/http.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 HttpScore { + files_to_serve: Url, +} + +impl Score for HttpScore { + type InterpretType = HttpInterpret; + + fn create_interpret(self) -> Self::InterpretType { + HttpInterpret::new(self) + } +} + +#[derive(Debug, new, Clone)] +pub struct HttpInterpret { + score: HttpScore, +} + +#[async_trait] +impl Interpret for HttpInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &HAClusterTopology, + ) -> Result { + let http_server = &topology.http_server; + http_server.ensure_initialized().await?; + // http_server.set_ip(topology.router.get_gateway()).await?; + http_server.serve_files(&self.score.files_to_serve).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 + ))) + } + + fn get_name(&self) -> InterpretName { + InterpretName::Http + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } +} diff --git a/harmony-rs/harmony/src/modules/mod.rs b/harmony-rs/harmony/src/modules/mod.rs index b20c025..a4f93b3 100644 --- a/harmony-rs/harmony/src/modules/mod.rs +++ b/harmony-rs/harmony/src/modules/mod.rs @@ -3,3 +3,4 @@ pub mod dns; pub mod okd; pub mod load_balancer; pub mod tftp; +pub mod http; diff --git a/harmony-rs/opnsense-config-xml/src/data/caddy.rs b/harmony-rs/opnsense-config-xml/src/data/caddy.rs new file mode 100644 index 0000000..47dc8a5 --- /dev/null +++ b/harmony-rs/opnsense-config-xml/src/data/caddy.rs @@ -0,0 +1,83 @@ +use yaserde::MaybeString; +use yaserde_derive::{YaDeserialize, YaSerialize}; + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Pischem { + pub caddy: Caddy, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct Caddy { + pub general: CaddyGeneral, + pub reverseproxy: MaybeString, +} + +#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] +pub struct CaddyGeneral { + pub enabled: u8, + #[yaserde(rename = "EnableLayer4")] + pub enable_layer4: Option, + #[yaserde(rename = "HttpPort")] + pub http_port: Option, + #[yaserde(rename = "HttpsPort")] + pub https_port: Option, + #[yaserde(rename = "TlsEmail")] + pub tls_email: MaybeString, + #[yaserde(rename = "TlsAutoHttps")] + pub tls_auto_https: MaybeString, + #[yaserde(rename = "TlsDnsProvider")] + pub tls_dns_provider: MaybeString, + #[yaserde(rename = "TlsDnsApiKey")] + pub tls_dns_api_key: MaybeString, + #[yaserde(rename = "TlsDnsSecretApiKey")] + pub tls_dns_secret_api_key: MaybeString, + #[yaserde(rename = "TlsDnsOptionalField1")] + pub tls_dns_optional_field1: MaybeString, + #[yaserde(rename = "TlsDnsOptionalField2")] + pub tls_dns_optional_field2: MaybeString, + #[yaserde(rename = "TlsDnsOptionalField3")] + pub tls_dns_optional_field3: MaybeString, + #[yaserde(rename = "TlsDnsOptionalField4")] + pub tls_dns_optional_field4: MaybeString, + #[yaserde(rename = "TlsDnsPropagationTimeout")] + pub tls_dns_propagation_timeout: MaybeString, + #[yaserde(rename = "TlsDnsPropagationResolvers")] + pub tls_dns_propagation_resolvers: MaybeString, + pub accesslist: MaybeString, + #[yaserde(rename = "DisableSuperuser")] + pub disable_superuser: Option, + #[yaserde(rename = "GracePeriod")] + pub grace_period: Option, + #[yaserde(rename = "HttpVersion")] + pub http_version: MaybeString, + #[yaserde(rename = "LogCredentials")] + pub log_credentials: MaybeString, + #[yaserde(rename = "LogAccessPlain")] + pub log_access_plain: MaybeString, + #[yaserde(rename = "LogAccessPlainKeep")] + pub log_access_plain_keep: Option, + #[yaserde(rename = "LogLevel")] + pub log_level: MaybeString, + #[yaserde(rename = "DynDnsSimpleHttp")] + pub dyn_dns_simple_http: MaybeString, + #[yaserde(rename = "DynDnsInterface")] + pub dyn_dns_interface: MaybeString, + #[yaserde(rename = "DynDnsInterval")] + pub dyn_dns_interval: MaybeString, + #[yaserde(rename = "DynDnsIpVersions")] + pub dyn_dns_ip_versions: MaybeString, + #[yaserde(rename = "DynDnsTtl")] + pub dyn_dns_ttl: MaybeString, + #[yaserde(rename = "DynDnsUpdateOnly")] + pub dyn_dns_update_only: MaybeString, + #[yaserde(rename = "AuthProvider")] + pub auth_provider: MaybeString, + #[yaserde(rename = "AuthToDomain")] + pub auth_to_domain: MaybeString, + #[yaserde(rename = "AuthToPort")] + pub auth_to_port: MaybeString, + #[yaserde(rename = "AuthToTls")] + pub auth_to_tls: Option, + #[yaserde(rename = "AuthToUri")] + pub auth_to_uri: MaybeString, +} diff --git a/harmony-rs/opnsense-config-xml/src/data/mod.rs b/harmony-rs/opnsense-config-xml/src/data/mod.rs index af9c448..ccc1ba1 100644 --- a/harmony-rs/opnsense-config-xml/src/data/mod.rs +++ b/harmony-rs/opnsense-config-xml/src/data/mod.rs @@ -2,6 +2,8 @@ mod opnsense; mod interfaces; mod dhcpd; mod haproxy; +mod caddy; +pub use caddy::*; pub use haproxy::*; pub use opnsense::*; pub use interfaces::*; diff --git a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs index a6f1d7f..e13bf13 100644 --- a/harmony-rs/opnsense-config-xml/src/data/opnsense.rs +++ b/harmony-rs/opnsense-config-xml/src/data/opnsense.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use yaserde::{MaybeString, NamedList, RawXml}; use yaserde_derive::{YaDeserialize, YaSerialize}; -use super::Interface; +use super::{Interface, Pischem}; #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[yaserde(rename = "opnsense")] @@ -44,7 +44,7 @@ pub struct OPNsense { pub wireless: Wireless, pub hasync: Hasync, #[yaserde(rename = "Pischem")] - pub pischem: Option, + pub pischem: Option, pub ifgroups: Ifgroups, } @@ -1370,7 +1370,6 @@ pub struct ConfigOpenVPN { pub StaticKeys: MaybeString, } - #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] pub struct StaticRoutes { #[yaserde(attribute)] diff --git a/harmony-rs/opnsense-config/src/config/config.rs b/harmony-rs/opnsense-config/src/config/config.rs index 00a84c1..cea652b 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, tftp::TftpConfig}, + modules::{caddy::CaddyConfig, dhcp::DhcpConfig, dns::DnsConfig, load_balancer::LoadBalancerConfig, tftp::TftpConfig}, }; use log::{info, trace}; use opnsense_config_xml::OPNsense; @@ -42,6 +42,10 @@ impl Config { TftpConfig::new(&mut self.opnsense, self.shell.clone()) } + pub fn caddy(&mut self) -> CaddyConfig { + CaddyConfig::new(&mut self.opnsense, self.shell.clone()) + } + pub fn load_balancer(&mut self) -> LoadBalancerConfig { LoadBalancerConfig::new(&mut self.opnsense, self.shell.clone()) } @@ -50,6 +54,17 @@ impl Config { self.shell.upload_folder(source, destination).await } + // Here maybe we should take ownership of `mut self` instead of `&mut self` + // I don't think there can be faulty pointers to previous versions of the config but I have a + // hard time wrapping my head around it right now : + // - the caller has a mutable reference to us + // - caller gets a reference to a piece of configuration (.haproxy.general.servers[0]) + // - caller calls install_package wich reloads the config from remote + // - haproxy.general.servers[0] does not exist anymore + // - broken? + // + // Although I did not try explicitely the above workflow so maybe rust prevents taking a + // read-only reference across the &mut call 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/modules/caddy.rs b/harmony-rs/opnsense-config/src/modules/caddy.rs new file mode 100644 index 0000000..ac06bf4 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/caddy.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use opnsense_config_xml::{Caddy, OPNsense, Pischem}; + +use crate::{config::OPNsenseShell, Error}; + +pub struct CaddyConfig<'a> { + opnsense: &'a mut OPNsense, + opnsense_shell: Arc, +} + +impl<'a> CaddyConfig<'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.pischem + } + + fn with_caddy(&mut self, f: F) -> R + where + F: FnOnce(&mut Caddy) -> R, + { + match &mut self.opnsense.pischem.as_mut() { + Some(pischem) => f(&mut pischem.caddy), + None => unimplemented!("Accessing caddy config is not supported when not available yet"), + } + } + + pub fn enable(&mut self, enabled: bool) { + self.with_caddy(|caddy| {caddy.general.enabled = enabled as u8; + caddy.general.http_port = Some(8080); + caddy.general.https_port = Some(8443); + }); + } + + pub async fn reload_restart(&self) -> Result<(), Error> { + self.opnsense_shell.exec("configctl caddy stop").await?; + self.opnsense_shell.exec("configctl template reload OPNsense/Caddy").await?; + self.opnsense_shell.exec("configctl template reload OPNsense/Caddy/rc.conf.d").await?; + self.opnsense_shell.exec("configctl caddy validate").await?; + self.opnsense_shell.exec("configctl caddy start").await?; + Ok(()) + } +} diff --git a/harmony-rs/opnsense-config/src/modules/mod.rs b/harmony-rs/opnsense-config/src/modules/mod.rs index a14b91f..4fd006a 100644 --- a/harmony-rs/opnsense-config/src/modules/mod.rs +++ b/harmony-rs/opnsense-config/src/modules/mod.rs @@ -2,3 +2,4 @@ pub mod dhcp; pub mod dns; pub mod load_balancer; pub mod tftp; +pub mod caddy; diff --git a/watchguard/caddy_config/caddy_pxe.conf b/watchguard/caddy_config/caddy_pxe.conf new file mode 100644 index 0000000..a1c478c --- /dev/null +++ b/watchguard/caddy_config/caddy_pxe.conf @@ -0,0 +1,4 @@ +:8080 { + root * /usr/local/http + file_server +} diff --git a/watchguard/pxe-http-files/paul b/watchguard/pxe-http-files/paul new file mode 100644 index 0000000..00cd64a --- /dev/null +++ b/watchguard/pxe-http-files/paul @@ -0,0 +1 @@ +hey i am paul