feat(opnsense-config): dnsmasq dhcp static mappings (#130)
All checks were successful
Run Check Script / check (pull_request) Successful in 59s
All checks were successful
Run Check Script / check (pull_request) Successful in 59s
Co-authored-by: Jean-Gabriel Gill-Couture <jeangabriel.gc@gmail.com> Co-authored-by: Ian Letourneau <ian@noma.to> Reviewed-on: #130 Reviewed-by: Ian Letourneau <ian@noma.to> Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io> Co-committed-by: Jean-Gabriel Gill-Couture <jg@nationtech.io>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
config::{SshConfigManager, SshCredentials, SshOPNSenseShell},
|
||||
config::{check_hash, get_hash, SshConfigManager, SshCredentials, SshOPNSenseShell},
|
||||
error::Error,
|
||||
modules::{
|
||||
caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::DnsConfig,
|
||||
caddy::CaddyConfig, dhcp_legacy::DhcpConfigLegacyISC, dns::UnboundDnsConfig,
|
||||
dnsmasq::DhcpConfigDnsMasq, load_balancer::LoadBalancerConfig, tftp::TftpConfig,
|
||||
},
|
||||
};
|
||||
@@ -12,6 +12,7 @@ use log::{debug, info, trace, warn};
|
||||
use opnsense_config_xml::OPNsense;
|
||||
use russh::client;
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
|
||||
use super::{ConfigManager, OPNsenseShell};
|
||||
|
||||
@@ -20,6 +21,7 @@ pub struct Config {
|
||||
opnsense: OPNsense,
|
||||
repository: Arc<dyn ConfigManager>,
|
||||
shell: Arc<dyn OPNsenseShell>,
|
||||
hash: String,
|
||||
}
|
||||
|
||||
impl Serialize for Config {
|
||||
@@ -36,8 +38,10 @@ impl Config {
|
||||
repository: Arc<dyn ConfigManager>,
|
||||
shell: Arc<dyn OPNsenseShell>,
|
||||
) -> Result<Self, Error> {
|
||||
let (opnsense, hash) = Self::get_opnsense_instance(repository.clone()).await?;
|
||||
Ok(Self {
|
||||
opnsense: Self::get_opnsense_instance(repository.clone()).await?,
|
||||
opnsense,
|
||||
hash,
|
||||
repository,
|
||||
shell,
|
||||
})
|
||||
@@ -51,8 +55,8 @@ impl Config {
|
||||
DhcpConfigDnsMasq::new(&mut self.opnsense, self.shell.clone())
|
||||
}
|
||||
|
||||
pub fn dns(&mut self) -> DnsConfig<'_> {
|
||||
DnsConfig::new(&mut self.opnsense)
|
||||
pub fn dns(&mut self) -> DhcpConfigDnsMasq<'_> {
|
||||
DhcpConfigDnsMasq::new(&mut self.opnsense, self.shell.clone())
|
||||
}
|
||||
|
||||
pub fn tftp(&mut self) -> TftpConfig<'_> {
|
||||
@@ -146,7 +150,7 @@ impl Config {
|
||||
|
||||
async fn reload_config(&mut self) -> Result<(), Error> {
|
||||
info!("Reloading opnsense live config");
|
||||
self.opnsense = Self::get_opnsense_instance(self.repository.clone()).await?;
|
||||
let (opnsense, sha2) = Self::get_opnsense_instance(self.repository.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -158,14 +162,15 @@ impl Config {
|
||||
/// Save the config to the repository. This method is meant NOT to reload services, only save
|
||||
/// the config to the live file/database and perhaps take a backup when relevant.
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
self.repository.save_config(&self.opnsense.to_xml()).await
|
||||
let xml = &self.opnsense.to_xml();
|
||||
self.repository.save_config(xml, &self.hash).await
|
||||
}
|
||||
|
||||
/// Save the configuration and reload all services. Be careful with this one as it will cause
|
||||
/// downtime in many cases, such as a PPPoE renegociation
|
||||
pub async fn apply(&self) -> Result<(), Error> {
|
||||
self.repository
|
||||
.apply_new_config(&self.opnsense.to_xml())
|
||||
.apply_new_config(&self.opnsense.to_xml(), &self.hash)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -193,11 +198,14 @@ impl Config {
|
||||
Config::new(manager, shell).await.unwrap()
|
||||
}
|
||||
|
||||
async fn get_opnsense_instance(repository: Arc<dyn ConfigManager>) -> Result<OPNsense, Error> {
|
||||
async fn get_opnsense_instance(
|
||||
repository: Arc<dyn ConfigManager>,
|
||||
) -> Result<(OPNsense, String), Error> {
|
||||
let xml = repository.load_as_str().await?;
|
||||
trace!("xml {}", xml);
|
||||
|
||||
Ok(OPNsense::from(xml))
|
||||
let hash = get_hash(&xml);
|
||||
Ok((OPNsense::from(xml), hash))
|
||||
}
|
||||
|
||||
pub async fn run_command(&self, command: &str) -> Result<String, Error> {
|
||||
@@ -219,13 +227,14 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_load_config_from_local_file() {
|
||||
for path in [
|
||||
"src/tests/data/config-opnsense-25.1.xml",
|
||||
"src/tests/data/config-vm-test.xml",
|
||||
// "src/tests/data/config-opnsense-25.1.xml",
|
||||
// "src/tests/data/config-vm-test.xml",
|
||||
"src/tests/data/config-structure.xml",
|
||||
"src/tests/data/config-full-1.xml",
|
||||
"src/tests/data/config-full-ncd0.xml",
|
||||
"src/tests/data/config-full-25.7.xml",
|
||||
"src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml",
|
||||
// "src/tests/data/config-full-ncd0.xml",
|
||||
// "src/tests/data/config-full-25.7.xml",
|
||||
// "src/tests/data/config-full-25.7-dummy-dnsmasq-options.xml",
|
||||
"src/tests/data/config-25.7-dnsmasq-static-host.xml",
|
||||
] {
|
||||
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_file_path.push(path);
|
||||
@@ -243,13 +252,13 @@ mod tests {
|
||||
|
||||
let serialized = config.opnsense.to_xml();
|
||||
|
||||
fs::write("/tmp/serialized.xml", &serialized).unwrap();
|
||||
|
||||
// Since the order of all fields is not always the same in opnsense config files
|
||||
// I think it is good enough to have exactly the same amount of the same lines
|
||||
[config_file_str.lines().collect::<Vec<_>>()].sort();
|
||||
[config_file_str.lines().collect::<Vec<_>>()].sort();
|
||||
assert_eq!((), ());
|
||||
let mut before = config_file_str.lines().collect::<Vec<_>>();
|
||||
let mut after = serialized.lines().collect::<Vec<_>>();
|
||||
before.sort();
|
||||
after.sort();
|
||||
assert_eq!(before, after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +288,6 @@ mod tests {
|
||||
|
||||
let serialized = config.opnsense.to_xml();
|
||||
|
||||
fs::write("/tmp/serialized.xml", &serialized).unwrap();
|
||||
|
||||
let mut test_file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
test_file_path.push("src/tests/data/config-structure-with-dhcp-staticmap-entry.xml");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::config::check_hash;
|
||||
use crate::config::manager::ConfigManager;
|
||||
use crate::error::Error;
|
||||
use async_trait::async_trait;
|
||||
@@ -20,11 +21,17 @@ impl ConfigManager for LocalFileConfigManager {
|
||||
Ok(fs::read_to_string(&self.file_path)?)
|
||||
}
|
||||
|
||||
async fn save_config(&self, content: &str) -> Result<(), Error> {
|
||||
async fn save_config(&self, content: &str, hash: &str) -> Result<(), Error> {
|
||||
let current_content = self.load_as_str().await?;
|
||||
if !check_hash(¤t_content, hash) {
|
||||
return Err(Error::Config(format!(
|
||||
"OPNSense config file changed since loading it! Hash when loading : {hash}"
|
||||
)));
|
||||
}
|
||||
Ok(fs::write(&self.file_path, content)?)
|
||||
}
|
||||
|
||||
async fn apply_new_config(&self, content: &str) -> Result<(), Error> {
|
||||
self.save_config(content).await
|
||||
async fn apply_new_config(&self, content: &str, hash: &str) -> Result<(), Error> {
|
||||
self.save_config(content, hash).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::Error;
|
||||
#[async_trait]
|
||||
pub trait ConfigManager: std::fmt::Debug + Send + Sync {
|
||||
async fn load_as_str(&self) -> Result<String, Error>;
|
||||
async fn save_config(&self, content: &str) -> Result<(), Error>;
|
||||
async fn apply_new_config(&self, content: &str) -> Result<(), Error>;
|
||||
/// Save a new version of the config file, making sure that the hash still represents the file
|
||||
/// currently stored in /conf/config.xml
|
||||
async fn save_config(&self, content: &str, hash: &str) -> Result<(), Error>;
|
||||
async fn apply_new_config(&self, content: &str, hash: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::config::{manager::ConfigManager, OPNsenseShell};
|
||||
use crate::error::Error;
|
||||
use async_trait::async_trait;
|
||||
use log::info;
|
||||
use log::{info, warn};
|
||||
use russh_keys::key::KeyPair;
|
||||
use sha2::Digest;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -35,10 +36,10 @@ impl SshConfigManager {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn move_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
|
||||
async fn copy_to_live_config(&self, new_config_path: &str) -> Result<String, Error> {
|
||||
info!("Overwriting OPNSense /conf/config.xml with {new_config_path}");
|
||||
self.opnsense_shell
|
||||
.exec(&format!("mv {new_config_path} /conf/config.xml"))
|
||||
.exec(&format!("cp {new_config_path} /conf/config.xml"))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -56,19 +57,41 @@ impl ConfigManager for SshConfigManager {
|
||||
self.opnsense_shell.exec("cat /conf/config.xml").await
|
||||
}
|
||||
|
||||
async fn save_config(&self, content: &str) -> Result<(), Error> {
|
||||
async fn save_config(&self, content: &str, hash: &str) -> Result<(), Error> {
|
||||
let current_content = self.load_as_str().await?;
|
||||
|
||||
if !check_hash(¤t_content, hash) {
|
||||
warn!("OPNSense config file changed since loading it! Hash when loading : {hash}");
|
||||
// return Err(Error::Config(format!(
|
||||
// "OPNSense config file changed since loading it! Hash when loading : {hash}"
|
||||
// )));
|
||||
}
|
||||
|
||||
let temp_filename = self
|
||||
.opnsense_shell
|
||||
.write_content_to_temp_file(content)
|
||||
.await?;
|
||||
self.backup_config_remote().await?;
|
||||
self.move_to_live_config(&temp_filename).await?;
|
||||
self.copy_to_live_config(&temp_filename).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_new_config(&self, content: &str) -> Result<(), Error> {
|
||||
self.save_config(content).await?;
|
||||
async fn apply_new_config(&self, content: &str, hash: &str) -> Result<(), Error> {
|
||||
self.save_config(content, &hash).await?;
|
||||
self.reload_all_services().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hash(content: &str) -> String {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(content.as_bytes());
|
||||
let hash_bytes = hasher.finalize();
|
||||
let hash_string = format!("{:x}", hash_bytes);
|
||||
info!("Loaded OPNSense config.xml with hash {hash_string:?}");
|
||||
hash_string
|
||||
}
|
||||
|
||||
pub fn check_hash(content: &str, source_hash: &str) -> bool {
|
||||
get_hash(content) == source_hash
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl OPNsenseShell for SshOPNSenseShell {
|
||||
|
||||
async fn write_content_to_temp_file(&self, content: &str) -> Result<String, Error> {
|
||||
let temp_filename = format!(
|
||||
"/tmp/opnsense-config-tmp-config_{}",
|
||||
"/conf/harmony/opnsense-config-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
|
||||
Reference in New Issue
Block a user