Merge pull request 'feat/opnsenseDNS' (#6) from feat/opnsenseDNS into master

Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/6
This commit is contained in:
johnride 2024-12-18 14:21:54 +00:00
commit 58f81f0e58
20 changed files with 468 additions and 154 deletions

51
harmony-rs/Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -152,7 +152,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -471,7 +471,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -499,7 +499,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -784,7 +784,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -905,6 +905,15 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "harmony_macros"
version = "1.0.0"
dependencies = [
"harmony",
"quote",
"syn 2.0.90",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@ -1346,7 +1355,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -1620,9 +1629,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2062,7 +2071,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -2247,9 +2256,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2319,7 +2328,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -2371,7 +2380,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]
@ -2542,7 +2551,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2576,7 +2585,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2786,6 +2795,16 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wk"
version = "0.1.0"
dependencies = [
"cidr",
"harmony",
"harmony_macros",
"tokio",
]
[[package]] [[package]]
name = "wyz" name = "wyz"
version = "0.5.1" version = "0.5.1"
@ -2845,7 +2864,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.77", "syn 2.0.90",
] ]
[[package]] [[package]]

View File

@ -3,7 +3,7 @@ resolver = "2"
members = [ members = [
"private_repos/*", "private_repos/*",
"harmony", "harmony",
"opnsense-config", "opnsense-config-xml", "opnsense-config", "opnsense-config-xml", "harmony_macros",
] ]
[workspace.package] [workspace.package]

View File

@ -96,10 +96,10 @@ pub enum StorageKind {
} }
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct Storage { pub struct Storage {
connection: StorageConnectionType, pub connection: StorageConnectionType,
kind: StorageKind, pub kind: StorageKind,
size: u64, pub size: u64,
serial: String, pub serial: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -7,7 +7,6 @@ use super::{
data::{Id, Version}, data::{Id, Version},
executors::ExecutorError, executors::ExecutorError,
inventory::Inventory, inventory::Inventory,
score::Score,
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
@ -41,9 +40,13 @@ pub struct Outcome {
status: InterpretStatus, status: InterpretStatus,
message: String, message: String,
} }
impl std::fmt::Display for Outcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { impl Outcome {
f.write_fmt(format_args!("Outcome {}: {}", self.status, self.message)) pub fn noop() -> Self {
Self {
status: InterpretStatus::NOOP,
message: String::new(),
}
} }
} }
@ -54,6 +57,7 @@ pub enum InterpretStatus {
RUNNING, RUNNING,
QUEUED, QUEUED,
BLOCKED, BLOCKED,
NOOP,
} }
impl std::fmt::Display for InterpretStatus { impl std::fmt::Display for InterpretStatus {
@ -64,6 +68,7 @@ impl std::fmt::Display for InterpretStatus {
InterpretStatus::RUNNING => "RUNNING", InterpretStatus::RUNNING => "RUNNING",
InterpretStatus::QUEUED => "QUEUED", InterpretStatus::QUEUED => "QUEUED",
InterpretStatus::BLOCKED => "BLOCKED", InterpretStatus::BLOCKED => "BLOCKED",
InterpretStatus::NOOP => "NO_OP",
}; };
f.write_str(msg) f.write_str(msg)
} }

View File

@ -34,7 +34,79 @@ pub type IpAddress = IpAddr;
/// This abstraction focuses on the logical role and services, independent of the physical hardware. /// This abstraction focuses on the logical role and services, independent of the physical hardware.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LogicalHost { pub struct LogicalHost {
/// The set of services this logical host provides /// The IP address of this logical host.
pub ip: IpAddress, pub ip: IpAddress,
/// The name of this logical host.
pub name: String, pub name: String,
} }
impl LogicalHost {
/// Creates a list of `LogicalHost` instances.
///
/// # Arguments
///
/// * `number_hosts` - The number of logical hosts to create.
/// * `start_ip` - The starting IP address. Each subsequent host's IP will be incremented.
/// * `hostname_prefix` - The prefix for the host names. Host names will be in the form `prefix<index>`.
///
/// # Returns
///
/// A `Vec<LogicalHost>` containing the specified number of logical hosts, each with a unique IP and name.
///
/// # Panics
///
/// This function will panic if adding `number_hosts` to `start_ip` exceeds the valid range of IP addresses.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// use harmony::topology::{IpAddress, LogicalHost};
///
/// let start_ip = IpAddress::from_str("192.168.0.20").unwrap();
/// let hosts = LogicalHost::create_hosts(3, start_ip, "worker");
///
/// assert_eq!(hosts.len(), 3);
/// assert_eq!(hosts[0].ip, IpAddress::from_str("192.168.0.20").unwrap());
/// assert_eq!(hosts[0].name, "worker0");
/// assert_eq!(hosts[1].ip, IpAddress::from_str("192.168.0.21").unwrap());
/// assert_eq!(hosts[1].name, "worker1");
/// 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<LogicalHost> {
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");
let name = format!("{}{}", hostname_prefix, i);
hosts.push(LogicalHost { ip: new_ip, name });
}
hosts
}
}
/// Increments an IP address by a given value.
///
/// # Arguments
///
/// * `ip` - The starting IP address.
/// * `increment` - The amount to add to the IP address.
///
/// # Returns
///
/// A new `IpAddress` that is the result of incrementing the original by `increment`.
///
/// # Panics
///
/// This function panics if the resulting IP address exceeds the valid range.
fn increment_ip(ip: IpAddress, increment: u32) -> Option<IpAddress> {
match ip {
IpAddress::V4(ipv4) => {
let new_ip = u32::from(ipv4) + increment;
Some(IpAddress::V4(new_ip.into()))
}
IpAddress::V6(_) => {
todo!("Ipv6 not supported yet")
}
}
}

View File

@ -45,8 +45,10 @@ pub trait DhcpServer: Send + Sync {
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>; async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError>;
async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>; async fn remove_static_mapping(&self, mac: &MacAddress) -> Result<(), ExecutorError>;
async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>; async fn list_static_mappings(&self) -> Vec<(MacAddress, IpAddress)>;
async fn set_next_server(&self, ip: IpAddress) -> Result<(), ExecutorError>;
fn get_ip(&self) -> IpAddress; fn get_ip(&self) -> IpAddress;
fn get_host(&self) -> LogicalHost; fn get_host(&self) -> LogicalHost;
async fn commit_config(&self) -> Result<(), ExecutorError>;
} }
impl std::fmt::Debug for dyn DhcpServer { impl std::fmt::Debug for dyn DhcpServer {

View File

@ -3,9 +3,7 @@ use derive_new::new;
use crate::{hardware::ManagementInterface, topology::MacAddress}; use crate::{hardware::ManagementInterface, topology::MacAddress};
#[derive(new)] #[derive(new)]
pub struct OPNSenseManagementInterface { pub struct OPNSenseManagementInterface {}
mac: MacAddress,
}
impl ManagementInterface for OPNSenseManagementInterface { impl ManagementInterface for OPNSenseManagementInterface {
fn boot_to_pxe(&self) { fn boot_to_pxe(&self) {
@ -13,7 +11,7 @@ impl ManagementInterface for OPNSenseManagementInterface {
} }
fn get_mac_address(&self) -> MacAddress { fn get_mac_address(&self) -> MacAddress {
self.mac.clone() todo!("OPNSense can have multiple mac addresses using SSH. I'm not sure it even belongs in the ManagementInterface trait")
} }
fn get_supported_protocol_names(&self) -> String { fn get_supported_protocol_names(&self) -> String {

View File

@ -1,9 +1,10 @@
mod management; mod management;
use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard}; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
pub use management::*; pub use management::*;
use tokio::sync::RwLock;
use crate::{ use crate::{
executors::ExecutorError, executors::ExecutorError,
@ -97,14 +98,24 @@ impl LoadBalancer for OPNSenseFirewall {
#[async_trait] #[async_trait]
impl DhcpServer for OPNSenseFirewall { impl DhcpServer for OPNSenseFirewall {
async fn commit_config(&self) -> Result<(), ExecutorError> {
self.opnsense_config
.read()
.await
.apply()
.await
.map_err(|e| ExecutorError::UnexpectedError(e.to_string()))
}
async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> { async fn add_static_mapping(&self, entry: &DHCPStaticEntry) -> Result<(), ExecutorError> {
let mac: String = String::from(&entry.mac); let mac: String = String::from(&entry.mac);
{ {
let mut writable_opnsense = self.opnsense_config.write().unwrap(); let mut writable_opnsense = self.opnsense_config.write().await;
writable_opnsense writable_opnsense
.dhcp() .dhcp()
.add_static_mapping(&mac, entry.ip, &entry.name).unwrap(); .add_static_mapping(&mac, entry.ip, &entry.name)
.unwrap();
} }
debug!("Registered {:?}", entry); debug!("Registered {:?}", entry);
@ -125,9 +136,24 @@ impl DhcpServer for OPNSenseFirewall {
fn get_ip(&self) -> IpAddress { fn get_ip(&self) -> IpAddress {
OPNSenseFirewall::get_ip(self) OPNSenseFirewall::get_ip(self)
} }
fn get_host(&self) -> LogicalHost { fn get_host(&self) -> LogicalHost {
self.host.clone() 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(())
}
} }
impl DnsServer for OPNSenseFirewall { impl DnsServer for OPNSenseFirewall {

View File

@ -2,3 +2,8 @@ mod domain;
pub use domain::*; pub use domain::*;
pub mod infra; pub mod infra;
pub mod modules; pub mod modules;
#[cfg(test)]
mod test {
use crate::infra::opnsense::OPNSenseFirewall;
}

View File

@ -1,4 +1,4 @@
use std::{net::Ipv4Addr, sync::Arc}; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
@ -11,50 +11,15 @@ use crate::{
}, },
interpret::{Interpret, InterpretError, InterpretName, Outcome}, interpret::{Interpret, InterpretError, InterpretName, Outcome},
inventory::Inventory, inventory::Inventory,
topology::{DHCPStaticEntry, HAClusterTopology, HostBinding}, topology::{DHCPStaticEntry, HAClusterTopology, HostBinding, IpAddress},
}; };
use crate::domain::score::Score; use crate::domain::score::Score;
/// OPNSenseDhcpScore will set static DHCP entries using index based hostname
/// and ip addresses.
///
/// For example :
/// ```rust
///
/// let node1 = todo!(); // Node pointing to clustermember controlplane0 with ip 10.10.0.20 and host with mac 01
/// let node2 = todo!(); // Node pointing to clustermember controlplane1 with ip 10.10.0.21 and host with mac 02
/// let node3 = todo!(); // Node pointing to clustermember controlplane2 with ip 10.10.0.22 and host with mac 03
///
/// let score = OPNSenseDhcpScore {
/// nodes: vec![node1, node2, node3],
/// }
/// ```
///
/// Running such a score would create these static entries :
///
/// ```rust
/// let entries = vec![
/// DHCPEntry {
/// mac: 01,
/// ip: 10.10.0.20,
/// hostname: "controlplane0"
/// }
/// DHCPEntry {
/// mac: 02,
/// ip: 10.10.0.21,
/// hostname: "controlplane0"
/// }
/// DHCPEntry {
/// mac: 03,
/// ip: 10.10.0.22,
/// hostname: "controlplane2"
/// }
/// ]
/// ```
#[derive(Debug, new, Clone)] #[derive(Debug, new, Clone)]
pub struct DhcpScore { pub struct DhcpScore {
host_binding: Vec<HostBinding>, host_binding: Vec<HostBinding>,
next_server: Option<IpAddress>,
} }
impl Score for DhcpScore { impl Score for DhcpScore {
@ -78,7 +43,7 @@ pub struct DhcpInterpret {
impl DhcpInterpret { impl DhcpInterpret {
pub fn new(score: DhcpScore) -> Self { pub fn new(score: DhcpScore) -> Self {
let version = Version::from("1.0.0").expect("Version should be valid"); let version = Version::from("1.0.0").expect("Version should be valid");
let name = "OPNSenseDhcpScore".to_string(); let name = "DhcpInterpret".to_string();
let id = Id::from_string(format!("{name}_{version}")); let id = Id::from_string(format!("{name}_{version}"));
Self { Self {
@ -89,6 +54,67 @@ impl DhcpInterpret {
status: InterpretStatus::QUEUED, status: InterpretStatus::QUEUED,
} }
} }
async fn add_static_entries(
&self,
_inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
let dhcp_entries: Vec<DHCPStaticEntry> = self
.score
.host_binding
.iter()
.map(|binding| {
let ip = match binding.logical_host.ip {
std::net::IpAddr::V4(ipv4) => ipv4,
std::net::IpAddr::V6(_) => {
unimplemented!("DHCPStaticEntry only supports ipv4 at the moment")
}
};
DHCPStaticEntry {
name: binding.logical_host.name.clone(),
mac: binding.physical_host.cluster_mac(),
ip,
}
})
.collect();
info!("DHCPStaticEntry : {:?}", dhcp_entries);
let dhcp_server = Arc::new(topology.dhcp_server.clone());
info!("DHCP server : {:?}", dhcp_server);
let number_new_entries = dhcp_entries.len();
for entry in dhcp_entries.into_iter() {
match dhcp_server.add_static_mapping(&entry).await {
Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry),
Err(_) => todo!(),
}
}
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret registered {} entries", number_new_entries),
))
}
async fn set_next_server(
&self,
_inventory: &Inventory,
topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> {
match self.score.next_server {
Some(next_server) => {
let dhcp_server = Arc::new(topology.dhcp_server.clone());
dhcp_server.set_next_server(next_server).await?;
Ok(Outcome::new(
InterpretStatus::SUCCESS,
format!("Dhcp Interpret Set next boot to {next_server}"),
))
}
None => Ok(Outcome::noop()),
}
}
} }
#[async_trait] #[async_trait]
@ -116,63 +142,15 @@ impl Interpret for DhcpInterpret {
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
info!("Executing {} on inventory {inventory:?}", self.get_name()); info!("Executing {} on inventory {inventory:?}", self.get_name());
let dhcp_entries: Vec<DHCPStaticEntry> = self self.set_next_server(inventory, topology).await?;
.score
.host_binding
.iter()
.map(|binding| {
let ip = match binding.logical_host.ip {
std::net::IpAddr::V4(ipv4) => ipv4,
std::net::IpAddr::V6(_) => unimplemented!("DHCPStaticEntry only supports ipv4 at the moment"),
};
DHCPStaticEntry { // self.add_static_entries(inventory, topology).await?;
name: binding.logical_host.name.clone(),
mac: binding.physical_host.cluster_mac(),
ip,
}
})
.collect();
info!("DHCPStaticEntry : {:?}", dhcp_entries);
let dhcp = Arc::new(Box::new(topology.dhcp_server.clone())); topology.dhcp_server.commit_config().await?;
info!("DHCP server : {:?}", dhcp);
for entry in dhcp_entries.into_iter() {
match dhcp.add_static_mapping(&entry).await {
Ok(_) => info!("Successfully registered DHCPStaticEntry {}", entry),
Err(_) => todo!(),
}
}
todo!("Configure DHCPServer");
Ok(Outcome::new( Ok(Outcome::new(
InterpretStatus::SUCCESS, InterpretStatus::SUCCESS,
"Connection test successful".to_string(), format!("Dhcp Interpret execution successful"),
)) ))
} }
} }
#[cfg(test)]
mod test {
#[test]
fn opnsense_dns_score_should_do_nothing_on_empty_inventory() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_bootstrap_node() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_control_plane_members() {
todo!();
}
#[test]
fn opnsense_dns_score_should_set_entry_for_workers() {
todo!();
}
}

View File

@ -1 +1,2 @@
pub mod dhcp; pub mod dhcp;
pub mod okd;

View File

@ -0,0 +1,45 @@
use crate::{
inventory::Inventory,
modules::dhcp::DhcpScore,
score::Score,
topology::{HAClusterTopology, HostBinding},
};
#[derive(Debug)]
pub struct OKDBootstrapDhcpScore {
dhcp_score: DhcpScore,
}
impl OKDBootstrapDhcpScore {
pub fn new(topology: &HAClusterTopology, inventory: &Inventory) -> Self {
let host_binding = topology
.control_plane
.iter()
.enumerate()
.map(|(index, topology_entry)| HostBinding {
logical_host: topology_entry.clone(),
physical_host: inventory
.control_plane_host
.get(index)
.expect("Iventory should contain at least as many physical hosts as topology")
.clone(),
})
.collect();
Self {
dhcp_score: DhcpScore::new(
host_binding,
// TODO : we should add a tftp server to the topology instead of relying on the
// router address, this is leaking implementation details
Some(topology.router.get_gateway()),
),
}
}
}
impl Score for OKDBootstrapDhcpScore {
type InterpretType = <DhcpScore as Score>::InterpretType;
fn create_interpret(self) -> Self::InterpretType {
self.dhcp_score.create_interpret()
}
}

View File

@ -0,0 +1,2 @@
pub mod dhcp;

46
harmony-rs/harmony_macros/Cargo.lock generated Normal file
View File

@ -0,0 +1,46 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "harmony_macros"
version = "1.0.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"

View File

@ -0,0 +1,12 @@
[package]
name = "harmony_macros"
edition = "2024"
version = "1.0.0"
[lib]
proc-macro = true
[dependencies]
harmony = { version = "0.1.0", path = "../harmony" }
quote = "1.0.37"
syn = "2.0.90"

View File

@ -0,0 +1,66 @@
extern crate proc_macro;
use harmony::topology::MacAddress;
use proc_macro::TokenStream;
use quote::quote;
use syn::{LitStr, parse_macro_input};
#[proc_macro]
pub fn ip(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let ip_str = input.value();
if let Ok(_) = ip_str.parse::<std::net::Ipv4Addr>() {
let expanded =
quote! { std::net::IpAddr::V4(#ip_str.parse::<std::net::Ipv4Addr>().unwrap()) };
return TokenStream::from(expanded);
}
if let Ok(_) = ip_str.parse::<std::net::Ipv6Addr>() {
let expanded =
quote! { std::net::IpAddr::V4(#ip_str.parse::<std::net::Ipv6Addr>().unwrap()) };
return TokenStream::from(expanded);
}
panic!("Invalid IP address: {}", ip_str);
}
#[proc_macro]
pub fn mac_address(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let mac_str = input.value();
match parse_mac_address(&mac_str) {
Ok(bytes) => {
let b0 = bytes[0];
let b1 = bytes[1];
let b2 = bytes[2];
let b3 = bytes[3];
let b4 = bytes[4];
let b5 = bytes[5];
quote! {
MacAddress( [#b0, #b1, #b2, #b3, #b4, #b5] )
}
.into()
}
Err(err) => syn::Error::new(input.span(), err).to_compile_error().into(),
}
}
fn parse_mac_address(mac: &str) -> Result<[u8; 6], String> {
let parts: Vec<&str> = mac.split(':').collect();
if parts.len() != 6 {
return Err("MAC address must contain exactly six octets separated by colons".to_string());
}
let mut bytes = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
match u8::from_str_radix(part, 16) {
Ok(byte) => bytes[i] = byte,
Err(_) => return Err(format!("Invalid MAC address octet: {}", part)),
}
}
Ok(bytes)
}

View File

@ -16,6 +16,8 @@ pub struct DhcpInterface {
pub enable: Option<MaybeString>, pub enable: Option<MaybeString>,
pub gateway: Option<MaybeString>, pub gateway: Option<MaybeString>,
pub domain: Option<MaybeString>, pub domain: Option<MaybeString>,
pub netboot: Option<u32>,
pub nextserver: Option<String>,
#[yaserde(rename = "ddnsdomainalgorithm")] #[yaserde(rename = "ddnsdomainalgorithm")]
pub ddns_domain_algorithm: Option<MaybeString>, pub ddns_domain_algorithm: Option<MaybeString>,
#[yaserde(rename = "numberoptions")] #[yaserde(rename = "numberoptions")]

View File

@ -27,7 +27,7 @@ pub struct OPNsense {
pub opnsense: OPNsenseXmlSection, pub opnsense: OPNsenseXmlSection,
pub staticroutes: StaticRoutes, pub staticroutes: StaticRoutes,
pub ca: MaybeString, pub ca: MaybeString,
pub gateways: Option<Gateways>, pub gateways: Option<RawXml>,
pub cert: Vec<Cert>, pub cert: Vec<Cert>,
pub dhcpdv6: DhcpDv6, pub dhcpdv6: DhcpDv6,
pub virtualip: VirtualIp, pub virtualip: VirtualIp,
@ -60,7 +60,6 @@ impl OPNsense {
} }
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct LoadBalancer { pub struct LoadBalancer {
pub monitor_type: Vec<MonitorType>, pub monitor_type: Vec<MonitorType>,
@ -415,6 +414,8 @@ pub struct OPNsenseXmlSection {
pub ipsec: Option<IPsec>, pub ipsec: Option<IPsec>,
#[yaserde(rename = "Interfaces")] #[yaserde(rename = "Interfaces")]
pub interfaces: Option<ConfigInterfaces>, pub interfaces: Option<ConfigInterfaces>,
#[yaserde(rename = "NodeExporter")]
pub node_exporter: Option<RawXml>,
#[yaserde(rename = "Kea")] #[yaserde(rename = "Kea")]
pub kea: Option<RawXml>, pub kea: Option<RawXml>,
pub monit: Option<Monit>, pub monit: Option<Monit>,
@ -428,6 +429,7 @@ pub struct OPNsenseXmlSection {
pub unboundplus: Option<RawXml>, pub unboundplus: Option<RawXml>,
#[yaserde(rename = "DHCRelay")] #[yaserde(rename = "DHCRelay")]
pub dhcrelay: Option<RawXml>, pub dhcrelay: Option<RawXml>,
pub trust: Option<RawXml>,
pub wireguard: Option<Wireguard>, pub wireguard: Option<Wireguard>,
#[yaserde(rename = "Swanctl")] #[yaserde(rename = "Swanctl")]
pub swanctl: Swanctl, pub swanctl: Swanctl,
@ -479,6 +481,8 @@ pub struct IDSGeneral {
#[yaserde(rename = "LogPayload")] #[yaserde(rename = "LogPayload")]
log_payload: Option<u8>, log_payload: Option<u8>,
verbosity: MaybeString, verbosity: MaybeString,
#[yaserde(rename = "eveLog")]
eve_log: Option<RawXml>,
} }
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
@ -498,11 +502,15 @@ pub struct IPsec {
key_pairs: MaybeString, key_pairs: MaybeString,
#[yaserde(rename = "preSharedKeys")] #[yaserde(rename = "preSharedKeys")]
pre_shared_keys: MaybeString, pre_shared_keys: MaybeString,
charon: Option<RawXml>,
} }
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
pub struct GeneralIpsec { pub struct GeneralIpsec {
enabled: MaybeString, enabled: MaybeString,
preferred_oldsa: MaybeString,
disablevpnrules: MaybeString,
passthrough_networks: MaybeString,
} }
#[derive(Debug, YaSerialize, YaDeserialize, PartialEq)] #[derive(Debug, YaSerialize, YaDeserialize, PartialEq)]
@ -1214,6 +1222,8 @@ pub struct WireguardServerItem {
pub gateway: MaybeString, pub gateway: MaybeString,
pub carp_depend_on: MaybeString, pub carp_depend_on: MaybeString,
pub peers: String, pub peers: String,
pub endpoint: MaybeString,
pub peer_dns: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@ -1320,7 +1330,6 @@ pub struct ConfigOpenVPN {
pub StaticKeys: MaybeString, pub StaticKeys: MaybeString,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
#[yaserde(rename = "HAProxy")] #[yaserde(rename = "HAProxy")]
pub struct HAProxy { pub struct HAProxy {
@ -1411,6 +1420,8 @@ pub struct Tuning {
#[yaserde(rename = "maxConnections")] #[yaserde(rename = "maxConnections")]
pub max_connections: MaybeString, pub max_connections: MaybeString,
pub nbthread: i32, pub nbthread: i32,
#[yaserde(rename = "resolversPrefer")]
pub resolvers_prefer: String,
#[yaserde(rename = "sslServerVerify")] #[yaserde(rename = "sslServerVerify")]
pub ssl_server_verify: String, pub ssl_server_verify: String,
#[yaserde(rename = "maxDHSize")] #[yaserde(rename = "maxDHSize")]
@ -1425,6 +1436,12 @@ pub struct Tuning {
pub lua_max_mem: i32, pub lua_max_mem: i32,
#[yaserde(rename = "customOptions")] #[yaserde(rename = "customOptions")]
pub custom_options: MaybeString, pub custom_options: MaybeString,
#[yaserde(rename = "ocspUpdateEnabled")]
pub ocs_update_enabled: MaybeString,
#[yaserde(rename = "ocspUpdateMinDelay")]
pub ocs_update_min_delay: MaybeString,
#[yaserde(rename = "ocspUpdateMaxDelay")]
pub ocs_update_max_delay: MaybeString,
#[yaserde(rename = "ssl_defaultsEnabled")] #[yaserde(rename = "ssl_defaultsEnabled")]
pub ssl_defaults_enabled: i32, pub ssl_defaults_enabled: i32,
#[yaserde(rename = "ssl_bindOptions")] #[yaserde(rename = "ssl_bindOptions")]
@ -1437,6 +1454,19 @@ pub struct Tuning {
pub ssl_cipher_list: String, pub ssl_cipher_list: String,
#[yaserde(rename = "ssl_cipherSuites")] #[yaserde(rename = "ssl_cipherSuites")]
pub ssl_cipher_suites: String, pub ssl_cipher_suites: String,
#[yaserde(rename = "h2_initialWindowSize")]
pub h2_initial_window_size: Option<MaybeString>,
#[yaserde(rename = "h2_initialWindowSizeOutgoing")]
pub h2_initial_window_size_outgoing: Option<MaybeString>,
#[yaserde(rename = "h2_initialWindowSizeIncoming")]
pub h2_initial_window_size_incoming: Option<MaybeString>,
#[yaserde(rename = "h2_maxConcurrentStreams")]
pub h2_max_concurrent_streams: Option<MaybeString>,
#[yaserde(rename = "h2_maxConcurrentStreamsOutgoing")]
pub h2_max_concurrent_streams_outgoing: Option<MaybeString>,
#[yaserde(rename = "h2_maxConcurrentStreamsIncoming")]
pub h2_max_concurrent_streams_incoming: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
@ -1684,25 +1714,22 @@ pub struct Backend {
pub http2_enabled_nontls: u8, pub http2_enabled_nontls: u8,
#[yaserde(rename = "ba_advertised_protocols")] #[yaserde(rename = "ba_advertised_protocols")]
pub ba_advertised_protocols: String, pub ba_advertised_protocols: String,
#[yaserde(rename = "persistence")] #[yaserde(rename = "forwardFor")]
pub persistence: String, pub forward_for: Option<i32>,
#[yaserde(rename = "persistence_cookiemode")] #[yaserde(rename = "forwardedHeader")]
pub forwarded_header: Option<MaybeString>,
#[yaserde(rename = "forwardedHeaderParameters")]
pub forwarded_header_parameters: Option<MaybeString>,
pub persistence: MaybeString,
pub persistence_cookiemode: String, pub persistence_cookiemode: String,
#[yaserde(rename = "persistence_cookiename")]
pub persistence_cookiename: MaybeString, pub persistence_cookiename: MaybeString,
#[yaserde(rename = "persistence_stripquotes")]
pub persistence_stripquotes: u8, pub persistence_stripquotes: u8,
#[yaserde(rename = "stickiness_pattern")] pub stickiness_pattern: MaybeString,
pub stickiness_pattern: String,
#[yaserde(rename = "stickiness_dataTypes")] #[yaserde(rename = "stickiness_dataTypes")]
pub stickiness_data_types: MaybeString, pub stickiness_data_types: MaybeString,
#[yaserde(rename = "stickiness_expire")]
pub stickiness_expire: String, pub stickiness_expire: String,
#[yaserde(rename = "stickiness_size")]
pub stickiness_size: String, pub stickiness_size: String,
#[yaserde(rename = "stickiness_cookiename")]
pub stickiness_cookiename: MaybeString, pub stickiness_cookiename: MaybeString,
#[yaserde(rename = "stickiness_cookielength")]
pub stickiness_cookielength: MaybeString, pub stickiness_cookielength: MaybeString,
#[yaserde(rename = "stickiness_connRatePeriod")] #[yaserde(rename = "stickiness_connRatePeriod")]
pub stickiness_conn_rate_period: String, pub stickiness_conn_rate_period: String,
@ -1863,12 +1890,6 @@ pub struct StaticRoutes {
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Ca {} pub struct Ca {}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Gateways {
#[yaserde(rename = "gateway_item")]
pub gateway_item: RawXml
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Cert { pub struct Cert {
#[yaserde(attribute)] #[yaserde(attribute)]
@ -1975,14 +1996,14 @@ pub struct Bridges {
pub struct Gifs { pub struct Gifs {
#[yaserde(attribute)] #[yaserde(attribute)]
pub version: Option<String>, pub version: Option<String>,
pub gif: MaybeString, pub gif: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Gres { pub struct Gres {
#[yaserde(attribute)] #[yaserde(attribute)]
pub version: Option<String>, pub version: Option<String>,
pub gre: MaybeString, pub gre: Option<MaybeString>,
} }
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)] #[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]

View File

@ -24,9 +24,10 @@ impl SshConfigManager {
impl SshConfigManager { impl SshConfigManager {
async fn backup_config_remote(&self) -> Result<String, Error> { async fn backup_config_remote(&self) -> Result<String, Error> {
let backup_filename = format!("config_{}.xml", chrono::Local::now().format("%Y%m%d%H%M%S")); 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 /tmp/{}", backup_filename)) self.opnsense_shell.exec(&format!("cp /conf/config.xml /conf/backup/{}", backup_filename))
.await .await
} }

View File

@ -54,7 +54,9 @@ impl<'a> DhcpConfig<'a> {
pub fn remove_static_mapping(&mut self, mac: &str) { pub fn remove_static_mapping(&mut self, mac: &str) {
let lan_dhcpd = self.get_lan_dhcpd(); let lan_dhcpd = self.get_lan_dhcpd();
lan_dhcpd.staticmaps.retain(|static_entry| static_entry.mac != mac); lan_dhcpd
.staticmaps
.retain(|static_entry| static_entry.mac != mac);
} }
fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface { fn get_lan_dhcpd(&mut self) -> &mut opnsense_config_xml::DhcpInterface {
@ -77,16 +79,19 @@ impl<'a> DhcpConfig<'a> {
let mac = mac.to_string(); let mac = mac.to_string();
let hostname = hostname.to_string(); let hostname = hostname.to_string();
let lan_dhcpd = self.get_lan_dhcpd(); let lan_dhcpd = self.get_lan_dhcpd();
let range = &lan_dhcpd.range;
let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps; let existing_mappings: &mut Vec<StaticMap> = &mut lan_dhcpd.staticmaps;
if !Self::is_valid_mac(&mac) { if !Self::is_valid_mac(&mac) {
return Err(DhcpError::InvalidMacAddress(mac)); return Err(DhcpError::InvalidMacAddress(mac));
} }
if !Self::is_ip_in_range(&ipaddr, range) { // TODO verify if address is in subnet range
return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string())); // This check here does not do what we want to do, as we want to assign static leases
} // outside of the dynamic DHCP pool
// let range = &lan_dhcpd.range;
// if !Self::is_ip_in_range(&ipaddr, range) {
// return Err(DhcpError::IpAddressOutOfRange(ipaddr.to_string()));
// }
if existing_mappings.iter().any(|m| { if existing_mappings.iter().any(|m| {
m.ipaddr m.ipaddr
@ -123,7 +128,7 @@ impl<'a> DhcpConfig<'a> {
parts parts
.iter() .iter()
.all(|part| part.len() == 2 && part.chars().all(|c| c.is_ascii_hexdigit())) .all(|part| part.len() <= 2 && part.chars().all(|c| c.is_ascii_hexdigit()))
} }
fn is_ip_in_range(ip: &Ipv4Addr, range: &Range) -> bool { fn is_ip_in_range(ip: &Ipv4Addr, range: &Range) -> bool {
@ -173,6 +178,14 @@ impl<'a> DhcpConfig<'a> {
Ok(static_maps) 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());
}
} }
#[cfg(test)] #[cfg(test)]