From 32cea6c3ff460e1ae87e4a78a1756884c35dbb25 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sun, 13 Oct 2024 08:48:56 -0400 Subject: [PATCH] wip: New crate opnsense-config --- harmony-rs/Cargo.lock | 85 ++++++++++++++++--- harmony-rs/Cargo.toml | 3 + harmony-rs/opnsense-config/Cargo.toml | 14 +++ harmony-rs/opnsense-config/adr/001-yaserde.md | 38 +++++++++ harmony-rs/opnsense-config/src/config.rs | 55 ++++++++++++ harmony-rs/opnsense-config/src/error.rs | 13 +++ harmony-rs/opnsense-config/src/lib.rs | 6 ++ .../opnsense-config/src/modules/dhcp.rs | 24 ++++++ harmony-rs/opnsense-config/src/modules/mod.rs | 2 + .../opnsense-config/src/modules/opnsense.rs | 44 ++++++++++ 10 files changed, 272 insertions(+), 12 deletions(-) create mode 100644 harmony-rs/opnsense-config/Cargo.toml create mode 100644 harmony-rs/opnsense-config/adr/001-yaserde.md create mode 100644 harmony-rs/opnsense-config/src/config.rs create mode 100644 harmony-rs/opnsense-config/src/error.rs create mode 100644 harmony-rs/opnsense-config/src/lib.rs create mode 100644 harmony-rs/opnsense-config/src/modules/dhcp.rs create mode 100644 harmony-rs/opnsense-config/src/modules/mod.rs create mode 100644 harmony-rs/opnsense-config/src/modules/opnsense.rs diff --git a/harmony-rs/Cargo.lock b/harmony-rs/Cargo.lock index ff15101..db0ca21 100644 --- a/harmony-rs/Cargo.lock +++ b/harmony-rs/Cargo.lock @@ -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]] diff --git a/harmony-rs/Cargo.toml b/harmony-rs/Cargo.toml index 0263f23..89dbb19 100644 --- a/harmony-rs/Cargo.toml +++ b/harmony-rs/Cargo.toml @@ -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" diff --git a/harmony-rs/opnsense-config/Cargo.toml b/harmony-rs/opnsense-config/Cargo.toml new file mode 100644 index 0000000..8499311 --- /dev/null +++ b/harmony-rs/opnsense-config/Cargo.toml @@ -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 } diff --git a/harmony-rs/opnsense-config/adr/001-yaserde.md b/harmony-rs/opnsense-config/adr/001-yaserde.md new file mode 100644 index 0000000..69e61df --- /dev/null +++ b/harmony-rs/opnsense-config/adr/001-yaserde.md @@ -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. diff --git a/harmony-rs/opnsense-config/src/config.rs b/harmony-rs/opnsense-config/src/config.rs new file mode 100644 index 0000000..99c8a45 --- /dev/null +++ b/harmony-rs/opnsense-config/src/config.rs @@ -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, + host: String, + username: String, +} + +impl Config { + pub async fn new(host: &str, username: &str, key_path: &str) -> Result { + 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(()) + } +} diff --git a/harmony-rs/opnsense-config/src/error.rs b/harmony-rs/opnsense-config/src/error.rs new file mode 100644 index 0000000..733746b --- /dev/null +++ b/harmony-rs/opnsense-config/src/error.rs @@ -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), +} diff --git a/harmony-rs/opnsense-config/src/lib.rs b/harmony-rs/opnsense-config/src/lib.rs new file mode 100644 index 0000000..7673ffc --- /dev/null +++ b/harmony-rs/opnsense-config/src/lib.rs @@ -0,0 +1,6 @@ +pub mod config; +pub mod modules; +pub mod error; + +pub use config::Config; +pub use error::Error; diff --git a/harmony-rs/opnsense-config/src/modules/dhcp.rs b/harmony-rs/opnsense-config/src/modules/dhcp.rs new file mode 100644 index 0000000..2bb7362 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/dhcp.rs @@ -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 + } +} diff --git a/harmony-rs/opnsense-config/src/modules/mod.rs b/harmony-rs/opnsense-config/src/modules/mod.rs new file mode 100644 index 0000000..d847dcb --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/mod.rs @@ -0,0 +1,2 @@ +pub mod opnsense; +pub mod dhcp; diff --git a/harmony-rs/opnsense-config/src/modules/opnsense.rs b/harmony-rs/opnsense-config/src/modules/opnsense.rs new file mode 100644 index 0000000..2ca04c0 --- /dev/null +++ b/harmony-rs/opnsense-config/src/modules/opnsense.rs @@ -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, +} + +#[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, +}