wip: New crate opnsense-config

This commit is contained in:
Jean-Gabriel Gill-Couture 2024-10-13 08:48:56 -04:00
parent 6a5ebdbac7
commit 32cea6c3ff
10 changed files with 272 additions and 12 deletions

85
harmony-rs/Cargo.lock generated
View File

@ -124,7 +124,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -400,7 +400,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -428,7 +428,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -695,7 +695,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -822,6 +822,12 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -1208,7 +1214,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -1229,6 +1235,20 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "opnsense-config"
version = "0.1.0"
dependencies = [
"async-trait",
"russh",
"russh-keys",
"thiserror",
"tokio",
"xml-rs",
"yaserde",
"yaserde_derive",
]
[[package]]
name = "p256"
version = "0.13.2"
@ -1826,7 +1846,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -1998,6 +2018,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.77"
@ -2072,7 +2103,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -2115,7 +2146,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -2177,7 +2208,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]
@ -2298,7 +2329,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
"wasm-bindgen-shared",
]
@ -2332,7 +2363,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2542,6 +2573,12 @@ dependencies = [
"tap",
]
[[package]]
name = "xml-rs"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
[[package]]
name = "xml_dom"
version = "0.2.8"
@ -2554,6 +2591,30 @@ dependencies = [
"tracing",
]
[[package]]
name = "yaserde"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8198a8ee4113411b7be1086e10b654f83653c01e4bd176fb98fe9d11951af5e"
dependencies = [
"log",
"xml-rs",
]
[[package]]
name = "yaserde_derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82eaa312529cc56b0df120253c804a8c8d593d2b5fe8deb5402714f485f62d79"
dependencies = [
"heck",
"log",
"proc-macro2",
"quote",
"syn 1.0.109",
"xml-rs",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -2572,7 +2633,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.77",
]
[[package]]

View File

@ -3,6 +3,7 @@ resolver = "2"
members = [
"private_repos/*",
"harmony",
"opnsense-config",
]
[workspace.package]
@ -18,3 +19,5 @@ async-trait = "0.1.82"
tokio = { version = "1.40.0", features = ["io-std"] }
cidr = "0.2.3"
xml_dom = "0.2.8"
russh = "0.45.0"
russh-keys = "0.45.0"

View File

@ -0,0 +1,14 @@
[package]
name = "opnsense-config"
version = "0.1.0"
edition = "2021"
[dependencies]
russh = { workspace = true }
russh-keys = { workspace = true }
yaserde = "0.11.1"
yaserde_derive = "0.11.1"
xml-rs = "0.8"
thiserror = "1.0"
async-trait = { workspace = true }
tokio = { workspace = true }

View File

@ -0,0 +1,38 @@
# Architecture Decision Record: Using yaserde for OPNsense Config Parsing
- Status : Proposed
- Author : Jean-Gabriel Gill-Couture
## Context
We need to parse and manipulate the OPNsense config.xml file in our Rust crate. We considered several XML parsing libraries, including quick-xml, xml-dom, minidom and yaserde. Each library has its own strengths and trade-offs in terms of performance, ease of use, and robustness.
## Decision
We have decided to use yaserde for parsing and manipulating the OPNsense config.xml file.
## Rationale
1. Type Safety: yaserde allows us to define a complete Rust representation of the config.xml structure. This provides strong type safety and makes it easier to catch errors at compile-time rather than runtime.
2. Robustness: By mapping the entire config structure to Rust types, we ensure that our code interacts with the config in a well-defined manner. This reduces the risk of runtime errors due to unexpected XML structures.
3. Ease of Use: Working with native Rust types is more intuitive and less error-prone than manipulating XML directly. This can lead to more maintainable and readable code.
4. Memory Usage: While yaserde may use more memory than streaming parsers like quick-xml, the OPNsense config files are typically not large enough for this to be a significant concern. We prioritize robustness and ease of use over minimal memory usage in this context.
5. Serialization/Deserialization: yaserde provides both deserialization (XML to Rust structs) and serialization (Rust structs to XML) out of the box, which simplifies our implementation.
## Consequences
Positive:
- Increased type safety and robustness in handling the config.xml structure.
- More intuitive API for developers working with the config.
- Easier to extend and maintain the code that interacts with different parts of the config.
Negative:
- It will be harder to maintain when there are breaking changes in the config.xml format. Any structural changes in the XML will require corresponding updates to our Rust struct definitions.
- Slightly higher memory usage compared to streaming parsers.
- Initial development time may be longer due to the need to define the entire config structure upfront.
We accept the trade-off of potentially more difficult maintenance in the face of breaking config.xml changes, as we believe the benefits of increased robustness and type safety outweigh this drawback. When OPNsense releases updates that change the config.xml structure, we will need to update our Rust struct definitions accordingly.

View File

@ -0,0 +1,55 @@
use crate::error::Error;
use crate::modules::opnsense::OPNsense;
use russh::client::{Config as SshConfig, Handler};
use std::sync::Arc;
use russh_keys::key;
pub struct Config {
opnsense: OPNsense,
ssh_config: Arc<SshConfig>,
host: String,
username: String,
}
impl Config {
pub async fn new(host: &str, username: &str, key_path: &str) -> Result<Self, Error> {
let key = russh_keys::load_secret_key(key_path, None).expect("Secret key failed loading");
let config = SshConfig::default();
let config = Arc::new(config);
let mut ssh = russh::client::connect(config.clone(), host, Handler).await?;
ssh.authenticate_publickey(username, key).await?;
let (xml, _) = ssh.exec(true, "cat /conf/config.xml").await?;
let xml = String::from_utf8(xml).map_err(|e| Error::Config(e.to_string()))?;
let opnsense = yaserde::de::from_str(&xml).map_err(|e| Error::Xml(e.to_string()))?;
Ok(Self {
opnsense,
ssh_config: config,
host: host.to_string(),
username: username.to_string(),
})
}
pub fn get_opnsense(&self) -> &OPNsense {
&self.opnsense
}
pub fn get_opnsense_mut(&mut self) -> &mut OPNsense {
&mut self.opnsense
}
pub async fn save(&self) -> Result<(), Error> {
let xml = yaserde::ser::to_string(&self.opnsense).map_err(|e| Error::Xml(e.to_string()))?;
let mut ssh = russh::client::connect(self.ssh_config.clone(), &self.host, Handler).await?;
ssh.authenticate_publickey(&self.username, key).await?;
ssh.exec(true, &format!("echo '{}' > /conf/config.xml", xml))
.await?;
Ok(())
}
}

View File

@ -0,0 +1,13 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("XML error: {0}")]
Xml(String),
#[error("SSH error: {0}")]
Ssh(#[from] russh::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Config error: {0}")]
Config(String),
}

View File

@ -0,0 +1,6 @@
pub mod config;
pub mod modules;
pub mod error;
pub use config::Config;
pub use error::Error;

View File

@ -0,0 +1,24 @@
use super::opnsense::{OPNsense, StaticMap};
pub struct DhcpConfig<'a> {
opnsense: &'a mut OPNsense,
}
impl<'a> DhcpConfig<'a> {
pub fn new(opnsense: &'a mut OPNsense) -> Self {
Self { opnsense }
}
pub fn add_static_mapping(&mut self, mac: String, ipaddr: String, hostname: String) {
let static_map = StaticMap {
mac,
ipaddr,
hostname,
};
self.opnsense.dhcpd.lan.staticmaps.push(static_map);
}
pub fn get_static_mappings(&self) -> &[StaticMap] {
&self.opnsense.dhcpd.lan.staticmaps
}
}

View File

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

View File

@ -0,0 +1,44 @@
use yaserde_derive::{YaDeserialize, YaSerialize};
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
#[yaserde(rename = "opnsense")]
pub struct OPNsense {
#[yaserde(rename = "dhcpd")]
pub dhcpd: Dhcpd,
// Add other top-level elements as needed
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct Dhcpd {
#[yaserde(rename = "lan")]
pub lan: DhcpInterface,
// Add other interfaces as needed
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct DhcpInterface {
#[yaserde(rename = "enable")]
pub enable: bool,
#[yaserde(rename = "range")]
pub range: DhcpRange,
#[yaserde(rename = "staticmap")]
pub staticmaps: Vec<StaticMap>,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct DhcpRange {
#[yaserde(rename = "from")]
pub from: String,
#[yaserde(rename = "to")]
pub to: String,
}
#[derive(Default, PartialEq, Debug, YaSerialize, YaDeserialize)]
pub struct StaticMap {
#[yaserde(rename = "mac")]
pub mac: String,
#[yaserde(rename = "ipaddr")]
pub ipaddr: String,
#[yaserde(rename = "hostname")]
pub hostname: String,
}