forked from NationTech/harmony
chore: Reorganize file tree for easier onboarding. Rust project now at the root for simple git clone && cargo run
This commit is contained in:
56
opnsense-config/src/modules/caddy.rs
Normal file
56
opnsense-config/src/modules/caddy.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
185
opnsense-config/src/modules/dhcp.rs
Normal file
185
opnsense-config/src/modules/dhcp.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use log::info;
|
||||
use opnsense_config_xml::MaybeString;
|
||||
use opnsense_config_xml::StaticMap;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use opnsense_config_xml::OPNsense;
|
||||
|
||||
use crate::config::OPNsenseShell;
|
||||
use crate::Error;
|
||||
|
||||
pub struct DhcpConfig<'a> {
|
||||
opnsense: &'a mut OPNsense,
|
||||
opnsense_shell: Arc<dyn OPNsenseShell>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DhcpError {
|
||||
InvalidMacAddress(String),
|
||||
InvalidIpAddress(String),
|
||||
IpAddressAlreadyMapped(String),
|
||||
MacAddressAlreadyMapped(String),
|
||||
IpAddressOutOfRange(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DhcpError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DhcpError::InvalidMacAddress(mac) => write!(f, "Invalid MAC address format: {}", mac),
|
||||
DhcpError::InvalidIpAddress(ip) => write!(f, "Invalid IP address format: {}", ip),
|
||||
DhcpError::IpAddressAlreadyMapped(ip) => {
|
||||
write!(f, "IP address {} is already mapped", ip)
|
||||
}
|
||||
DhcpError::MacAddressAlreadyMapped(mac) => {
|
||||
write!(f, "MAC address {} is already mapped", mac)
|
||||
}
|
||||
DhcpError::IpAddressOutOfRange(ip) => {
|
||||
write!(f, "IP address {} is out of interface range", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DhcpError {}
|
||||
|
||||
impl<'a> DhcpConfig<'a> {
|
||||
pub fn new(opnsense: &'a mut OPNsense, opnsense_shell: Arc<dyn OPNsenseShell>) -> Self {
|
||||
Self {
|
||||
opnsense,
|
||||
opnsense_shell,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_static_mapping(&mut self, mac: &str) {
|
||||
let lan_dhcpd = self.get_lan_dhcpd();
|
||||
lan_dhcpd
|
||||
.staticmaps
|
||||
.retain(|static_entry| static_entry.mac != mac);
|
||||
}
|
||||
|
||||
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
|
||||
&mut self
|
||||
.opnsense
|
||||
.dhcpd
|
||||
.elements
|
||||
.iter_mut()
|
||||
.find(|(name, _config)| return name == "lan")
|
||||
.expect("Interface lan should have dhcpd activated")
|
||||
.1
|
||||
}
|
||||
|
||||
pub fn add_static_mapping(
|
||||
&mut self,
|
||||
mac: &str,
|
||||
ipaddr: Ipv4Addr,
|
||||
hostname: &str,
|
||||
) -> Result<(), DhcpError> {
|
||||
let mac = mac.to_string();
|
||||
let hostname = hostname.to_string();
|
||||
let lan_dhcpd = self.get_lan_dhcpd();
|
||||
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
|
||||
|
||||
if !Self::is_valid_mac(&mac) {
|
||||
return Err(DhcpError::InvalidMacAddress(mac));
|
||||
}
|
||||
|
||||
// TODO validate that address is in subnet range
|
||||
|
||||
if existing_mappings.iter().any(|m| {
|
||||
m.ipaddr
|
||||
.parse::<Ipv4Addr>()
|
||||
.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::<Ipv4Addr>()
|
||||
.expect("Mapping contains invalid ipv4")
|
||||
== ipaddr
|
||||
}) {
|
||||
return Err(DhcpError::IpAddressAlreadyMapped(ipaddr.to_string()));
|
||||
}
|
||||
|
||||
if existing_mappings.iter().any(|m| m.mac == mac) {
|
||||
return Err(DhcpError::MacAddressAlreadyMapped(mac));
|
||||
}
|
||||
|
||||
let static_map = StaticMap {
|
||||
mac,
|
||||
ipaddr: ipaddr.to_string(),
|
||||
hostname,
|
||||
descr: Default::default(),
|
||||
winsserver: Default::default(),
|
||||
dnsserver: Default::default(),
|
||||
ntpserver: Default::default(),
|
||||
};
|
||||
|
||||
existing_mappings.push(static_map);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_valid_mac(mac: &str) -> bool {
|
||||
let parts: Vec<&str> = mac.split(':').collect();
|
||||
if parts.len() != 6 {
|
||||
return false;
|
||||
}
|
||||
|
||||
parts
|
||||
.iter()
|
||||
.all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
pub async fn get_static_mappings(&self) -> Result<Vec<StaticMap>, Error> {
|
||||
let list_static_output = self
|
||||
.opnsense_shell
|
||||
.exec("configctl dhcpd list static")
|
||||
.await?;
|
||||
|
||||
let value: serde_json::Value = serde_json::from_str(&list_static_output).expect(&format!(
|
||||
"Got invalid json from configctl {list_static_output}"
|
||||
));
|
||||
let static_maps = value["dhcpd"]
|
||||
.as_array()
|
||||
.ok_or(Error::Command(format!(
|
||||
"Invalid DHCP data from configctl command, got {list_static_output}"
|
||||
)))?
|
||||
.iter()
|
||||
.map(|entry| StaticMap {
|
||||
mac: entry["mac"].as_str().unwrap_or_default().to_string(),
|
||||
ipaddr: entry["ipaddr"].as_str().unwrap_or_default().to_string(),
|
||||
hostname: entry["hostname"].as_str().unwrap_or_default().to_string(),
|
||||
descr: entry["descr"].as_str().map(MaybeString::from),
|
||||
winsserver: MaybeString::default(),
|
||||
dnsserver: MaybeString::default(),
|
||||
ntpserver: MaybeString::default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(static_maps)
|
||||
}
|
||||
pub fn enable_netboot(&mut self) {
|
||||
self.get_lan_dhcpd().netboot = Some(1);
|
||||
}
|
||||
|
||||
pub fn set_next_server(&mut self, ip: Ipv4Addr) {
|
||||
self.enable_netboot();
|
||||
self.get_lan_dhcpd().nextserver = Some(ip.to_string());
|
||||
self.get_lan_dhcpd().tftp = Some(ip.to_string());
|
||||
}
|
||||
|
||||
pub fn set_boot_filename(&mut self, boot_filename: &str) {
|
||||
self.enable_netboot();
|
||||
self.get_lan_dhcpd().filename64 = Some(boot_filename.to_string());
|
||||
self.get_lan_dhcpd().bootfilename = Some(boot_filename.to_string());
|
||||
}
|
||||
}
|
||||
37
opnsense-config/src/modules/dns.rs
Normal file
37
opnsense-config/src/modules/dns.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use opnsense_config_xml::{Host, OPNsense};
|
||||
|
||||
pub struct DnsConfig<'a> {
|
||||
opnsense: &'a mut OPNsense,
|
||||
}
|
||||
|
||||
impl<'a> DnsConfig<'a> {
|
||||
pub fn new(opnsense: &'a mut OPNsense) -> Self {
|
||||
Self { opnsense }
|
||||
}
|
||||
|
||||
pub fn register_hosts(&mut self, mut hosts: Vec<Host>) {
|
||||
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<Host> {
|
||||
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,
|
||||
None => todo!("Handle case where unboundplus is not used"),
|
||||
};
|
||||
|
||||
unbound.general.regdhcp = Some(register as i8);
|
||||
unbound.general.regdhcpstatic = Some(register as i8);
|
||||
}
|
||||
}
|
||||
84
opnsense-config/src/modules/load_balancer.rs
Normal file
84
opnsense-config/src/modules/load_balancer.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
use opnsense_config_xml::{
|
||||
Frontend, HAProxy, HAProxyBackend, HAProxyHealthCheck, HAProxyServer, OPNsense,
|
||||
};
|
||||
|
||||
use crate::{config::OPNsenseShell, Error};
|
||||
|
||||
pub struct LoadBalancerConfig<'a> {
|
||||
opnsense: &'a mut OPNsense,
|
||||
opnsense_shell: Arc<dyn OPNsenseShell>,
|
||||
}
|
||||
|
||||
impl<'a> LoadBalancerConfig<'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<HAProxy> {
|
||||
&self.opnsense.opnsense.haproxy
|
||||
}
|
||||
|
||||
fn with_haproxy<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut HAProxy) -> R,
|
||||
{
|
||||
match &mut self.opnsense.opnsense.haproxy.as_mut() {
|
||||
Some(haproxy) => f(haproxy),
|
||||
None => unimplemented!(
|
||||
"Adding a backend is not supported when haproxy config does not exist yet"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, enabled: bool) {
|
||||
self.with_haproxy(|haproxy| haproxy.general.enabled = enabled as i32);
|
||||
}
|
||||
|
||||
pub fn add_backend(&mut self, backend: HAProxyBackend) {
|
||||
warn!("TODO make sure this new backend does not refer non-existing entities like servers or health checks");
|
||||
self.with_haproxy(|haproxy| haproxy.backends.backends.push(backend));
|
||||
}
|
||||
|
||||
pub fn add_frontend(&mut self, frontend: Frontend) {
|
||||
self.with_haproxy(|haproxy| haproxy.frontends.frontend.push(frontend));
|
||||
}
|
||||
|
||||
pub fn add_healthcheck(&mut self, healthcheck: HAProxyHealthCheck) {
|
||||
self.with_haproxy(|haproxy| haproxy.healthchecks.healthchecks.push(healthcheck));
|
||||
}
|
||||
|
||||
pub fn add_servers(&mut self, mut servers: Vec<HAProxyServer>) {
|
||||
self.with_haproxy(|haproxy| haproxy.servers.servers.append(&mut servers));
|
||||
}
|
||||
|
||||
pub async fn reload_restart(&self) -> Result<(), Error> {
|
||||
self.opnsense_shell.exec("configctl haproxy stop").await?;
|
||||
self.opnsense_shell
|
||||
.exec("configctl template reload OPNsense/HAProxy")
|
||||
.await?;
|
||||
self.opnsense_shell
|
||||
.exec("configctl template reload OPNsense/Syslog")
|
||||
.await?;
|
||||
self.opnsense_shell
|
||||
.exec("/usr/local/sbin/haproxy -c -f /usr/local/etc/haproxy.conf.staging")
|
||||
.await?;
|
||||
|
||||
// This script copies the staging config to production config. I am not 100% sure it is
|
||||
// required in the context
|
||||
self.opnsense_shell
|
||||
.exec("/usr/local/opnsense/scripts/OPNsense/HAProxy/setup.sh deploy")
|
||||
.await?;
|
||||
|
||||
self.opnsense_shell
|
||||
.exec("configctl haproxy configtest")
|
||||
.await?;
|
||||
self.opnsense_shell.exec("configctl haproxy start").await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
opnsense-config/src/modules/mod.rs
Normal file
5
opnsense-config/src/modules/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod caddy;
|
||||
pub mod dhcp;
|
||||
pub mod dns;
|
||||
pub mod load_balancer;
|
||||
pub mod tftp;
|
||||
50
opnsense-config/src/modules/tftp.rs
Normal file
50
opnsense-config/src/modules/tftp.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
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<dyn OPNsenseShell>,
|
||||
}
|
||||
|
||||
impl<'a> TftpConfig<'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<Tftp> {
|
||||
&self.opnsense.opnsense.tftp
|
||||
}
|
||||
|
||||
fn with_tftp<F, R>(&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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user