From 41e9ea3f324e88592055ab6695ae13ab938e8d1d Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Wed, 31 Jul 2024 12:08:24 -0400 Subject: [PATCH 1/2] feat: added support for tsig queries --- Cargo.lock | 3 + Cargo.toml | 1 + src/dns/mod.rs | 80 +++++++++++++++ src/{dns.rs => dns/service.rs} | 174 +++++++++++++++++---------------- src/error.rs | 35 +++++++ src/key.rs | 103 ++++++++++++++++++- src/main.rs | 10 +- src/tsig.rs | 36 ++++++- src/watcher.rs | 141 ++++++++++++-------------- 9 files changed, 416 insertions(+), 167 deletions(-) create mode 100644 src/dns/mod.rs rename src/{dns.rs => dns/service.rs} (70%) diff --git a/Cargo.lock b/Cargo.lock index 9165c61..01337d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,7 +184,9 @@ dependencies = [ "octseq", "parking_lot", "rand", + "ring", "serde", + "smallvec", "time", "tokio", "tracing", @@ -535,6 +537,7 @@ checksum = "2ed2eaec452d98ccc1c615dd843fd039d9445f2fb4da114ee7e6af5fcb68be98" dependencies = [ "bytes", "serde", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b0c566c..3d5668f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ domain = { version = "0.10.1", features = [ "net", "unstable-server-transport", "unstable-zonetree", + "tsig", ] } log = { version = "0.4.22", features = ["std"] } notify = { version = "6.1.1" } diff --git a/src/dns/mod.rs b/src/dns/mod.rs new file mode 100644 index 0000000..86c9465 --- /dev/null +++ b/src/dns/mod.rs @@ -0,0 +1,80 @@ +use std::sync::{Arc, RwLock}; + +use domain::base::iana::{Class, Rcode}; +use domain::base::ToName; + +use domain::zonetree::{Answer, ReadableZone, Zone}; + +use crate::config::Config; +use crate::error::Error; +use crate::key::KeyStore; +use crate::zone::ZoneTree; + +pub use service::dns; + +mod service; + +type Zones = Arc>; + +pub struct State { + pub config: Config, + pub zones: Zones, + pub keystore: Arc>, +} + +impl State { + pub fn config(&self) -> &Config { + &self.config + } + + fn find_zone(&self, qname: &N, class: Class, f: F) -> Answer + where + N: ToName, + F: FnOnce(Option>) -> Answer, + { + if class != Class::IN { + return Answer::new(Rcode::NXDOMAIN); + } + + let zones = self.zones.read().unwrap(); + f(zones.find_zone(qname).map(|z| z.read())) + } + + pub fn insert_zone(&self, zone: Zone) -> Result<(), Error> { + log::info!(target: "zone_change", "adding zone {}", zone.apex_name()); + let mut zones = self.zones.write().unwrap(); + zones.insert_zone(zone) + } + + pub fn remove_zone(&self, name: &N, class: Class) -> Result<(), Error> + where + N: ToName, + { + log::info!(target: "zone_change", "removing zone {} {}", name.to_bytes(), class); + + let mut zones = self.zones.write().unwrap(); + + for z in zones.iter_zones() { + log::debug!(target: "zone", "zone {:?}", z); + } + + zones.remove_zone(name)?; + + for z in zones.iter_zones() { + log::debug!(target: "zone", "zone {}", z.apex_name()); + } + + Ok(()) + } +} + +impl From for State { + fn from(config: Config) -> Self { + let zones = Arc::new(RwLock::new(ZoneTree::new())); + State { + config, + zones, + keystore: KeyStore::new_shared(), + } + } +} diff --git a/src/dns.rs b/src/dns/service.rs similarity index 70% rename from src/dns.rs rename to src/dns/service.rs index c06a1fb..1004b68 100644 --- a/src/dns.rs +++ b/src/dns/service.rs @@ -1,5 +1,5 @@ use std::future::{ready, Future}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex}; use domain::base::iana::{Class, Opcode, Rcode}; use domain::base::message_builder::AdditionalBuilder; @@ -7,72 +7,14 @@ use domain::base::{Message, Name, Rtype, ToName}; use domain::net::server::message::Request; use domain::net::server::service::{CallResult, ServiceError, Transaction, TransactionStream}; use domain::net::server::util::mk_builder_for_target; -use domain::zonetree::{Answer, ReadableZone, Rrset, Zone}; +use domain::rdata::tsig::Time48; +use domain::tsig::{Key, ServerSequence, ServerTransaction}; +use domain::zonetree::{Answer, Rrset}; use octseq::OctetsBuilder; -use crate::config::Config; -use crate::error::Error; -use crate::zone::ZoneTree; +use crate::key::KeyStore; -type Zones = Arc>; - -pub struct State { - config: Config, - zones: Zones, -} - -impl State { - pub fn config(&self) -> &Config { - &self.config - } - - fn find_zone(&self, qname: &N, class: Class, f: F) -> Answer - where - N: ToName, - F: FnOnce(Option>) -> Answer, - { - if class != Class::IN { - return Answer::new(Rcode::NXDOMAIN); - } - - let zones = self.zones.read().unwrap(); - f(zones.find_zone(qname).map(|z| z.read())) - } - - pub fn insert_zone(&self, zone: Zone) -> Result<(), Error> { - log::info!(target: "zone_change", "adding zone {}", zone.apex_name()); - let mut zones = self.zones.write().unwrap(); - zones.insert_zone(zone) - } - - pub fn remove_zone(&self, name: &N, class: Class) -> Result<(), Error> - where - N: ToName, - { - log::info!(target: "zone_change", "removing zone {} {}", name.to_bytes(), class); - - let mut zones = self.zones.write().unwrap(); - - for z in zones.iter_zones() { - log::debug!(target: "zone", "zone {:?}", z); - } - - zones.remove_zone(name)?; - - for z in zones.iter_zones() { - log::debug!(target: "zone", "zone {}", z.apex_name()); - } - - Ok(()) - } -} - -impl From for State { - fn from(config: Config) -> Self { - let zones = Arc::new(RwLock::new(ZoneTree::new())); - State { config, zones } - } -} +use super::State; pub fn dns( request: Request>, @@ -98,18 +40,31 @@ async fn handle_non_axfr_request( request: Request>, state: Arc, ) -> Result>, ServiceError> { - let question = request.message().sole_question().unwrap(); - let answer = state.find_zone(question.qname(), question.qclass(), |zone| match zone { - Some(zone) => { - let qname = question.qname().to_bytes(); - let qtype = question.qtype(); - zone.query(qname, qtype).unwrap() - } - None => Answer::new(Rcode::NXDOMAIN), - }); + let answer = { + let question = request.message().sole_question().unwrap(); + state.find_zone(question.qname(), question.qclass(), |zone| match zone { + Some(zone) => { + let qname = question.qname().to_bytes(); + let qtype = question.qtype(); + zone.query(qname, qtype).unwrap() + } + None => Answer::new(Rcode::NXDOMAIN), + }) + }; let builder = mk_builder_for_target(); - let additional = answer.to_message(request.message(), builder); + let mut additional = answer.to_message(request.message(), builder); + + let keystore = state.keystore.read().unwrap(); + let mut message = request.message().clone(); + let message = Arc::make_mut(&mut message); + + match ServerTransaction::>::request::(&keystore, message, Time48::now()) { + Ok(None) => (), + Ok(Some(transaction)) => transaction.answer(&mut additional, Time48::now()).unwrap(), + _ => (), + } + Ok(CallResult::new(additional)) } @@ -118,13 +73,35 @@ async fn handle_axfr_request( state: Arc, ) -> TransactionStream>, ServiceError>> { let mut stream = TransactionStream::default(); + let keystore = state.keystore.read().unwrap(); + let mut message = request.message().clone(); + let message = Arc::make_mut(&mut message); + + let mut server_sequence = + match ServerSequence::>::request::(&keystore, message, Time48::now()) + { + Ok(sequence) => sequence, + _ => return stream, + }; + + let request = Request::new( + request.client_addr(), + request.received_at(), + message.to_owned(), + request.transport_ctx().to_owned(), + ); // Look up the zone for the queried name. let question = request.message().sole_question().unwrap(); if question.qclass() == Class::IN { let answer = Answer::new(Rcode::NXDOMAIN); - add_to_stream(answer, request.message(), &mut stream); + add_to_stream( + server_sequence.as_mut(), + answer, + request.message(), + &mut stream, + ); return stream; } @@ -134,7 +111,12 @@ async fn handle_axfr_request( // If not found, return an NXDOMAIN error response. let Some(zone) = zone else { let answer = Answer::new(Rcode::NXDOMAIN); - add_to_stream(answer, request.message(), &mut stream); + add_to_stream( + server_sequence.as_mut(), + answer, + request.message(), + &mut stream, + ); return stream; }; @@ -158,12 +140,22 @@ async fn handle_axfr_request( let qname = question.qname().to_bytes(); let Ok(soa_answer) = zone.query(qname, Rtype::SOA) else { let answer = Answer::new(Rcode::SERVFAIL); - add_to_stream(answer, request.message(), &mut stream); + add_to_stream( + server_sequence.as_mut(), + answer, + request.message(), + &mut stream, + ); return stream; }; // Push the begin SOA response message into the stream - add_to_stream(soa_answer.clone(), request.message(), &mut stream); + add_to_stream( + server_sequence.as_mut(), + soa_answer.clone(), + request.message(), + &mut stream, + ); // "The AXFR protocol treats the zone contents as an unordered // collection (or to use the mathematical term, a "set") of @@ -195,6 +187,8 @@ async fn handle_axfr_request( let stream = Arc::new(Mutex::new(stream)); let cloned_stream = stream.clone(); let cloned_msg = request.message().clone(); + let server_sequence = Arc::new(Mutex::new(server_sequence)); + let cloned_server_sequence = server_sequence.clone(); let op = Box::new(move |owner: Name<_>, rrset: &Rrset| { if rrset.rtype() != Rtype::SOA { @@ -206,38 +200,52 @@ async fn handle_axfr_request( let additional = answer.additional(); let mut stream = cloned_stream.lock().unwrap(); - add_additional_to_stream(additional, &cloned_msg, &mut stream); + let mut server_sequence = cloned_server_sequence.lock().unwrap(); + add_additional_to_stream( + server_sequence.as_mut(), + additional, + &cloned_msg, + &mut stream, + ); } }); zone.walk(op); let mutex = Arc::try_unwrap(stream).unwrap(); let mut stream = mutex.into_inner().unwrap(); + let mutex = Arc::try_unwrap(server_sequence).unwrap(); + let mut server_sequence = mutex.into_inner().unwrap(); // Push the end SOA response message into the stream - add_to_stream(soa_answer, request.message(), &mut stream); + add_to_stream( + server_sequence.as_mut(), + soa_answer, + request.message(), + &mut stream, + ); stream } -#[allow(clippy::type_complexity)] fn add_to_stream( + sequence: Option<&mut ServerSequence>>, answer: Answer, msg: &Message>, stream: &mut TransactionStream>, ServiceError>>, ) { let builder = mk_builder_for_target(); let additional = answer.to_message(msg, builder); - add_additional_to_stream(additional, msg, stream); + add_additional_to_stream(sequence, additional, msg, stream); } -#[allow(clippy::type_complexity)] fn add_additional_to_stream( + sequence: Option<&mut ServerSequence>>, mut additional: AdditionalBuilder>>, msg: &Message>, stream: &mut TransactionStream>, ServiceError>>, ) { set_axfr_header(msg, &mut additional); + sequence.map(|sequence| sequence.answer(&mut additional, Time48::now())); stream.push(ready(Ok(CallResult::new(additional)))); } diff --git a/src/error.rs b/src/error.rs index b5b5917..3e1d4e1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,9 +14,13 @@ pub enum ErrorKind { DomainZone, Io, TSIGFileAlreadyExist, + TSIGFileNotFound, + TSIGKey, RingUnspecified, Base16, Utf8, + PushError, + OctsetShortBuffer, } impl std::fmt::Display for Error { @@ -39,9 +43,13 @@ impl std::fmt::Display for ErrorKind { DomainZone => write!(f, "domain zone error"), Io => write!(f, "io error"), TSIGFileAlreadyExist => write!(f, "tsig file already exists"), + TSIGFileNotFound => write!(f, "tsig file not found"), + TSIGKey => write!(f, "tsig key error"), RingUnspecified => write!(f, "ring unspecified error"), Base16 => write!(f, "base16 error"), Utf8 => write!(f, "utf8 error"), + PushError => write!(f, "tsig push error"), + OctsetShortBuffer => write!(f, "octset short buffer error"), } } } @@ -145,6 +153,33 @@ impl From for Error { } } +impl From for Error { + fn from(value: domain::base::message_builder::PushError) -> Self { + Self { + kind: ErrorKind::PushError, + message: Some(value.to_string()), + } + } +} + +impl From for Error { + fn from(value: domain::tsig::NewKeyError) -> Self { + Self { + kind: ErrorKind::TSIGKey, + message: Some(value.to_string()), + } + } +} + +impl From for Error { + fn from(value: octseq::ShortBuf) -> Self { + Self { + kind: ErrorKind::OctsetShortBuffer, + message: Some(value.to_string()), + } + } +} + mod macros { #[macro_export] macro_rules! error { diff --git a/src/key.rs b/src/key.rs index fe5c018..5f1fbc7 100644 --- a/src/key.rs +++ b/src/key.rs @@ -2,20 +2,38 @@ use core::str; use std::collections::HashMap; use std::ops::Deref; use std::path::PathBuf; +use std::sync::{Arc, RwLock}; use bytes::BytesMut; use domain::base::iana::Class; -use domain::base::{Record, Serial, Ttl}; +use domain::base::{Record, Serial, ToName, Ttl}; use domain::rdata::Soa; +use domain::tsig::{Algorithm, Key, KeyName}; use domain::zonetree::types::{StoredName, StoredRecord}; use domain::zonetree::{Rrset, SharedRrset, Zone, ZoneBuilder}; use serde::Deserialize; -use crate::error::Result; +use crate::error::{ErrorKind, Result}; #[derive(Debug, Clone, Deserialize, Default)] pub struct Keys(HashMap>); +impl Keys { + pub fn keys(&self) -> Vec<&KeyFile> { + self.0.keys().collect() + } + + pub fn domains(&self) -> Vec<(&DomainName, &DomainInfo)> { + let mut domains = Vec::new(); + self.0.iter().for_each(|(_, v)| { + v.iter().for_each(|(k, v)| { + domains.push((k, v)); + }); + }); + domains + } +} + impl Deref for Keys { type Target = HashMap>; @@ -112,8 +130,12 @@ impl KeyFile { PathBuf::from(crate::config::TSIG_PATH).join(&self.0) } - pub fn generate_key_file(&self) -> Result<()> { - crate::tsig::generate_new_tsig(&self.as_pathbuf()) + pub fn generate_key_file(&self) -> Result { + crate::tsig::generate_new_tsig(&self.as_pathbuf(), self) + } + + pub fn load_key(&self) -> Result { + crate::tsig::load_tsig(&self.as_pathbuf(), self) } pub fn delete_key_file(&self) -> Result<()> { @@ -121,8 +143,81 @@ impl KeyFile { } } +impl TryFrom<&KeyFile> for KeyName { + type Error = crate::error::Error; + + fn try_from(kf: &KeyFile) -> Result { + let mut bytes = octseq::Array::new(); + bytes.resize_raw(kf.0.len())?; + bytes.copy_from_slice(kf.0.as_bytes()); + + // SAFETY: `bytes` is a valid octet sequence + // and `KeyFile` is a valid utf-8 string + Ok(unsafe { Self::from_octets_unchecked(bytes) }) + } +} + +impl TryFrom<&KeyFile> for (KeyName, Algorithm) { + type Error = crate::error::Error; + + fn try_from(kf: &KeyFile) -> Result { + Ok((kf.try_into()?, Algorithm::Sha512)) + } +} + impl std::fmt::Display for KeyFile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } + +pub struct KeyStore { + keys: HashMap<(KeyName, Algorithm), Arc>, +} + +impl KeyStore { + pub fn new_shared() -> Arc> { + Arc::new(RwLock::new(Self { + keys: HashMap::new(), + })) + } + + pub fn remove_key(&mut self, key: &KeyFile) -> Result<()> { + if self.keys.remove(&key.try_into()?).is_some() { + key.delete_key_file()?; + } + Ok(()) + } + + pub fn add_key(&mut self, key: &KeyFile) -> Result<()> { + let k = match key.generate_key_file() { + Ok(key) => key, + Err(e) if e.kind == ErrorKind::TSIGFileAlreadyExist => { + log::info!(target: "tsig_file", "tsig key {} already exists - skipping", key); + key.load_key()? + } + Err(e) => return Err(e), + }; + self.keys.insert(key.try_into()?, Arc::new(k)); + Ok(()) + } +} + +impl domain::tsig::KeyStore for KeyStore { + type Key = Arc; + + fn get_key(&self, name: &N, algorithm: Algorithm) -> Option + where + N: ToName, + { + self.keys.get_key(name, algorithm) + } +} + +impl Deref for KeyStore { + type Target = HashMap<(KeyName, Algorithm), Arc>; + + fn deref(&self) -> &Self::Target { + &self.keys + } +} diff --git a/src/main.rs b/src/main.rs index 1746546..2f2f0cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,7 +86,15 @@ async fn main() { tokio::spawn(async move { tcp_srv.run().await }); - tokio::spawn(async move { Watcher::watch_lock(keys, state).unwrap() }); + tokio::spawn(async move { + match Watcher::watch_lock(keys, state) { + Ok(_) => (), + Err(e) => { + log::error!(target: "watcher", "failed to watch lock: {}", e); + exit(1); + } + } + }); tokio::spawn(async move { metric::log_svc(config, udp_metrics, tcp_metrics).await }); diff --git a/src/tsig.rs b/src/tsig.rs index f4a4c91..45ee5cd 100644 --- a/src/tsig.rs +++ b/src/tsig.rs @@ -2,6 +2,7 @@ use std::ffi::OsStr; use std::io::Write; use bytes::BytesMut; +use domain::tsig::{Key, KeyName}; use ring::hkdf::KeyType; use ring::rand::SecureRandom; @@ -21,9 +22,10 @@ where Ok(()) } -pub fn generate_new_tsig

(fpath: &P) -> Result<()> +pub fn generate_new_tsig(fpath: &P, name: N) -> Result where P: AsRef, + N: TryInto, { let path = std::path::Path::new(fpath); @@ -48,5 +50,35 @@ where let mut file = std::fs::File::create(path)?; file.write_all(&key)?; - Ok(()) + Ok(Key::new( + domain::tsig::Algorithm::Sha512, + &bytes, + name.try_into()?, + None, + None, + )?) +} + +pub fn load_tsig(fpath: &P, name: N) -> Result +where + P: AsRef, + N: TryInto, +{ + let path = std::path::Path::new(fpath); + + if !path.is_file() { + return Err( + error!(TSIGFileNotFound => "TSIG file at path ({}) not found", fpath.as_ref().to_string_lossy()), + ); + } + + let key = std::fs::read(path)?; + + Ok(Key::new( + domain::tsig::Algorithm::Sha512, + &key, + name.try_into()?, + None, + None, + )?) } diff --git a/src/watcher.rs b/src/watcher.rs index 81ff773..59281fd 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -1,14 +1,12 @@ -use std::collections::HashMap; use std::fs::File; use std::path::Path; use std::sync::mpsc::channel; use std::sync::Arc; -use domain::zonetree::Zone; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; use crate::dns::State; -use crate::error::{ErrorKind, Result}; +use crate::error::Result; use crate::key::{DomainInfo, DomainName, KeyFile, Keys, TryInto}; #[derive(Debug, Clone)] @@ -46,19 +44,14 @@ fn initialize_dns_zones(keys: &Keys, state: &Arc) -> Result<()> { } for (k, v) in keys.iter() { - v.try_into_t()? - .into_iter() - .try_for_each(|z| state.insert_zone(z))?; - match k.generate_key_file() { - Ok(()) => (), - Err(e) if e.kind == ErrorKind::TSIGFileAlreadyExist => { - log::info!(target: "tsig_file", - "TSIG key {} already exists - skipping", - k - ); + v.try_into_t()?.into_iter().try_for_each(|z| { + { + let mut keystore = state.keystore.write().unwrap(); + keystore.add_key(k)?; } - Err(e) => return Err(e), - } + + state.insert_zone(z) + })?; } Ok(()) @@ -68,79 +61,73 @@ fn handle_file_change(keys: &Keys, config_path: &Path, state: &Arc) -> Re let mut new_config = serde_yaml::from_reader::(File::open(config_path)?)?; log::debug!(target: "config_file", "new config {:?}", new_config); - let new_keys = new_config.take_keys().unwrap_or_default(); + let loaded_keys = new_config.take_keys().unwrap_or_default(); - let deleted_keys = keys.iter().filter(|(k, _)| !new_keys.contains_key(k)); - let added_keys = new_keys.iter().filter(|(k, _)| !keys.contains_key(k)); - let modified_keys = new_keys - .iter() - .filter(|(k, v)| keys.contains_key(k) && keys.get(k) != Some(v)) - .map(|(k, v)| (v, keys.get(k).unwrap())); + let new_domains = loaded_keys.domains(); + let old_domains = keys.domains(); + let new_keys = loaded_keys.keys(); + let old_keys = keys.keys(); - handle_deleted_keys(state, deleted_keys)?; - handle_added_keys(state, added_keys)?; - handle_modified_keys(state, modified_keys)?; + handle_keys_change(state, &old_keys, &new_keys)?; + handle_domains_change(state, &old_domains, &new_domains)?; - Ok(new_keys) + Ok(loaded_keys) } -fn handle_deleted_keys<'i, I>(state: &Arc, deleted_keys: I) -> Result<()> -where - I: IntoIterator)>, -{ - for (k, v) in deleted_keys { - v.try_into_t()?.into_iter().for_each(|z| { - let _ = state.remove_zone(z.apex_name(), z.class()); - }); - - // # Try to delete the TSIG key - k.delete_key_file()?; - } +fn handle_keys_change( + state: &Arc, + old_keys: &[&KeyFile], + new_keys: &[&KeyFile], +) -> Result<()> { + let mut deleted_keys = old_keys.iter().filter(|k| !new_keys.contains(k)); + let mut added_keys = new_keys.iter().filter(|k| !old_keys.contains(k)); - Ok(()) -} + deleted_keys.try_for_each(|&k| -> Result<()> { + let mut keystore = state.keystore.write().unwrap(); + keystore.remove_key(k)?; -fn handle_added_keys<'i, I>(state: &Arc, added_keys: I) -> Result<()> -where - I: IntoIterator)>, -{ - for (k, v) in added_keys { - v.try_into_t()?.into_iter().for_each(|z| { - let _ = state.insert_zone(z); - }); - - // # Try to create the TSIG key - k.generate_key_file()?; - } + Ok(()) + })?; + + added_keys.try_for_each(|&k| -> Result<()> { + let mut keystore = state.keystore.write().unwrap(); + keystore.add_key(k)?; + + Ok(()) + })?; Ok(()) } -fn handle_modified_keys<'i, I>(state: &Arc, modified_keys: I) -> Result<()> -where - I: IntoIterator< - Item = ( - &'i HashMap, - &'i HashMap, - ), - >, -{ - for (nv, ov) in modified_keys { - ov.iter() - .filter(|&(d, _)| nv.get(d).is_none()) - .try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into_t()?; - let _ = state.remove_zone(zone.apex_name(), zone.class()); - Ok(()) - })?; - nv.iter() - .filter(|&(d, _)| ov.get(d).is_none()) - .try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into_t()?; - let _ = state.insert_zone(zone); - Ok(()) - })?; - } +fn handle_domains_change( + state: &Arc, + old_domains: &[(&DomainName, &DomainInfo)], + new_domains: &[(&DomainName, &DomainInfo)], +) -> Result<()> { + let mut deleted_domains = old_domains.iter().filter(|d| !new_domains.contains(d)); + let mut added_domains = new_domains.iter().filter(|d| !old_domains.contains(d)); + let mut modified_domains = new_domains + .iter() + .filter(|(n, _)| old_domains.iter().any(|(o, _)| n == o)); + + deleted_domains.try_for_each(|d| -> Result<()> { + let z = d.try_into_t()?; + state.remove_zone(z.apex_name(), z.class())?; + Ok(()) + })?; + + added_domains.try_for_each(|d| -> Result<()> { + let z = d.try_into_t()?; + state.insert_zone(z)?; + Ok(()) + })?; + + modified_domains.try_for_each(|d| -> Result<()> { + let z = d.try_into_t()?; + state.remove_zone(z.apex_name(), z.class())?; + state.insert_zone(z)?; + Ok(()) + })?; Ok(()) } From 5b09ee6ecac771ac765565b011c9eaf8b011365e Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Wed, 31 Jul 2024 12:11:03 -0400 Subject: [PATCH 2/2] feat: changed logs output --- src/watcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watcher.rs b/src/watcher.rs index 59281fd..ba72690 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -60,7 +60,7 @@ fn initialize_dns_zones(keys: &Keys, state: &Arc) -> Result<()> { fn handle_file_change(keys: &Keys, config_path: &Path, state: &Arc) -> Result { let mut new_config = serde_yaml::from_reader::(File::open(config_path)?)?; - log::debug!(target: "config_file", "new config {:?}", new_config); + log::debug!(target: "config_file", "new config loaded {:?}", new_config); let loaded_keys = new_config.take_keys().unwrap_or_default(); let new_domains = loaded_keys.domains();