feat(opnsense-config): add caddy module with configuration management

Introduce a new Caddy module within opnsense-config to manage Caddy server configurations. This includes enabling/disabling Caddy, setting ports, and reloading/restarting the service via OPNsense shell commands. Additionally, provide a sample Caddy configuration file for PXE booting and a test file in the pxe-http-files directory.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-01-07 17:12:39 -05:00
parent 925e84e4d2
commit 81d40ec163
17 changed files with 342 additions and 17 deletions

View File

@ -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();
}

View File

@ -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"),
}
}
}

View File

@ -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()
))
}
}

View File

@ -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<dyn Firewall>,
pub dhcp_server: Arc<dyn DhcpServer>,
pub tftp_server: Arc<dyn TftpServer>,
pub http_server: Arc<dyn HttpServer>,
pub dns_server: Arc<dyn DnsServer>,
pub control_plane: Vec<LogicalHost>,
pub workers: Vec<LogicalHost>,

View File

@ -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(())
}
}

View File

@ -4,6 +4,7 @@ mod firewall;
mod load_balancer;
mod management;
mod tftp;
mod http;
use std::sync::Arc;
pub use management::*;

View File

@ -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> {

View File

@ -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<Outcome, InterpretError> {
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<Id> {
todo!()
}
}

View File

@ -3,3 +3,4 @@ pub mod dns;
pub mod okd;
pub mod load_balancer;
pub mod tftp;
pub mod http;

View File

@ -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<u8>,
#[yaserde(rename = "HttpPort")]
pub http_port: Option<u16>,
#[yaserde(rename = "HttpsPort")]
pub https_port: Option<u16>,
#[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<i32>,
#[yaserde(rename = "GracePeriod")]
pub grace_period: Option<u16>,
#[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<u16>,
#[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<i32>,
#[yaserde(rename = "AuthToUri")]
pub auth_to_uri: MaybeString,
}

View File

@ -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::*;

View File

@ -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<RawXml>,
pub pischem: Option<Pischem>,
pub ifgroups: Ifgroups,
}
@ -1370,7 +1370,6 @@ pub struct ConfigOpenVPN {
pub StaticKeys: MaybeString,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct StaticRoutes {
#[yaserde(attribute)]

View File

@ -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

View File

@ -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<dyn OPNsenseShell>,
}
impl<'a> CaddyConfig<'a> {
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
Self {
opnsense,
opnsense_shell,
}
}
pub fn get_full_config(&self) -> &Option<Pischem> {
&self.opnsense.pischem
}
fn with_caddy<F, R>(&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(())
}
}

View File

@ -2,3 +2,4 @@ pub mod dhcp;
pub mod dns;
pub mod load_balancer;
pub mod tftp;
pub mod caddy;

View File

@ -0,0 +1,4 @@
:8080 {
root * /usr/local/http
file_server
}

View File

@ -0,0 +1 @@
hey i am paul