From 33240aa72806c5a52b5e970c2b200b638b35345f Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Tue, 30 Jul 2024 01:43:57 -0400 Subject: [PATCH 1/2] feat: changed the config file to old the keys directly inside TODO: work on a custom Zone implementation to get rid of some dependancies --- config.yml | 9 ++- domains.yml | 4 -- src/config.rs | 15 +++-- src/dns.rs | 45 ++++++++++---- src/fs.rs | 165 ------------------------------------------------- src/key.rs | 71 +++++++++++++++++++++ src/main.rs | 11 ++-- src/watcher.rs | 140 +++++++++++++++++++++++++++++++++++++++++ src/zone.rs | 48 ++++++++++++++ 9 files changed, 318 insertions(+), 190 deletions(-) delete mode 100644 domains.yml delete mode 100644 src/fs.rs create mode 100644 src/key.rs create mode 100644 src/watcher.rs create mode 100644 src/zone.rs diff --git a/config.yml b/config.yml index 9b07b3f..d296fdb 100644 --- a/config.yml +++ b/config.yml @@ -4,8 +4,15 @@ # The log configuration. log: # The log level. This can be one of the following: trace, debug, info, warn, error, or off. - level: info + level: debug # Enable the udp metrics. enable_udp_metrics: true # Enable the tcp metrics. enable_tcp_metrics: true + +keys: + key1: + - thibault-cne.fr + key2: + - example.com + - example.org diff --git a/domains.yml b/domains.yml deleted file mode 100644 index b6cee95..0000000 --- a/domains.yml +++ /dev/null @@ -1,4 +0,0 @@ -domains: - - thibault-cne.fr - - yewolf.fr - - example.fr diff --git a/src/config.rs b/src/config.rs index 6d317b8..d3ab19e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,19 +3,24 @@ use std::path::Path; use serde::Deserialize; use crate::error::Result; +use crate::key::Keys; pub const TSIG_PATH: &str = "/etc/dnsr/keys"; pub const BASE_CONFIG_FILE: &str = "/etc/dnsr/config.yml"; -pub const DOMAIN_FILE: &str = "/etc/dnsr/domains.yml"; -#[derive(Deserialize, Clone)] +#[derive(Deserialize, Clone, Debug)] pub struct Config { pub log: LogConfig, + pub keys: Option, } impl Config { - pub fn domain_path(&self) -> &Path { - Path::new(DOMAIN_FILE) + pub fn take_keys(&mut self) -> Option { + self.keys.take() + } + + pub fn config_file_path() -> String { + std::env::var("DNSR_CONFIG").unwrap_or(BASE_CONFIG_FILE.into()) } pub fn tsig_path(&self) -> &Path { @@ -31,7 +36,7 @@ impl TryFrom<&Vec> for Config { } } -#[derive(Deserialize, Clone, Copy)] +#[derive(Deserialize, Clone, Copy, Debug)] pub struct LogConfig { #[serde(deserialize_with = "de_level_filter")] pub level: log::LevelFilter, diff --git a/src/dns.rs b/src/dns.rs index 4813805..c06a1fb 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -7,12 +7,12 @@ 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::error::ZoneTreeModificationError; -use domain::zonetree::{Answer, ReadableZone, Rrset}; -use domain::zonetree::{Zone, ZoneTree}; +use domain::zonetree::{Answer, ReadableZone, Rrset, Zone}; use octseq::OctetsBuilder; use crate::config::Config; +use crate::error::Error; +use crate::zone::ZoneTree; type Zones = Arc>; @@ -31,21 +31,39 @@ impl State { 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, class).map(|z| z.read())) + f(zones.find_zone(qname).map(|z| z.read())) } - pub fn insert_zone(&self, zone: Zone) -> Result<(), ZoneTreeModificationError> { + 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<(), ZoneTreeModificationError> + 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(); - zones.remove_zone(name, class) + + 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(()) } } @@ -99,14 +117,19 @@ async fn handle_axfr_request( request: Request>, state: Arc, ) -> TransactionStream>, ServiceError>> { - let zones = state.zones.read().unwrap(); let mut stream = TransactionStream::default(); // Look up the zone for the queried name. let question = request.message().sole_question().unwrap(); - let zone = zones - .find_zone(question.qname(), question.qclass()) - .map(|zone| zone.read()); + + if question.qclass() == Class::IN { + let answer = Answer::new(Rcode::NXDOMAIN); + add_to_stream(answer, request.message(), &mut stream); + return stream; + } + + let zones = state.zones.read().unwrap(); + let zone = zones.find_zone(question.qname()).map(|zone| zone.read()); // If not found, return an NXDOMAIN error response. let Some(zone) = zone else { diff --git a/src/fs.rs b/src/fs.rs deleted file mode 100644 index a8a6040..0000000 --- a/src/fs.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::fs::File; -use std::sync::mpsc::channel; -use std::sync::Arc; - -use convert_case::Casing; -use domain::base::iana::Class; -use domain::zonetree::error::ZoneTreeModificationError; -use domain::zonetree::types::StoredName; -use domain::zonetree::{Zone, ZoneBuilder}; -use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; -use serde::Deserialize; - -use crate::dns::State; -use crate::error::{ErrorKind, Result}; - -pub struct Watcher; - -impl Watcher { - pub fn watch_lock(state: Arc) -> Result<()> { - // Retrieve path - let path = state.config().domain_path(); - - // Initialize the watcher - let (tx, rx) = channel(); - let mut watcher = Box::new(RecommendedWatcher::new(tx, Config::default())?); - watcher.watch(path, RecursiveMode::NonRecursive)?; - - // Initialize the dns zones - let mut domains = initialize_dns_zones(&state)?; - - while rx.recv().is_ok() { - let new_domains = serde_yaml::from_reader::(File::open(path)?)?; - - handle_file_change(&domains.domains, &new_domains.domains, &state)?; - - domains = new_domains; - } - - Ok(()) - } -} - -fn initialize_dns_zones(state: &Arc) -> Result { - { - // Create the key folder if it does not exist - let path = state.config().tsig_path(); - if !path.is_dir() { - std::fs::create_dir(path)?; - } - } - - let domains = - serde_yaml::from_reader::(File::open(state.config().domain_path())?)?; - domains.domains.iter().try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into()?; - state.insert_zone(zone)?; - - // If the TSIG key does not exist, create it - match crate::tsig::generate_new_tsig(&state.config().tsig_path().join(d.file_name())) { - Ok(()) => (), - Err(e) if e.kind == ErrorKind::TSIGFileAlreadyExist => { - log::info!(target: "tsig_file", - "TSIG key already exists for domain {} - skipping", - d.domain_name() - ); - } - Err(e) => return Err(e), - } - - Ok(()) - })?; - Ok(domains) -} - -fn handle_file_change( - old_domains: &[Domain], - new_domains: &[Domain], - state: &Arc, -) -> Result<()> { - let deleted_domains = old_domains.iter().filter(|d| !new_domains.contains(d)); - let added_domains = new_domains.iter().filter(|d| !old_domains.contains(d)); - - for d in deleted_domains { - let zone: Zone = d.try_into()?; - match state.remove_zone(zone.apex_name(), zone.class()) { - Ok(_) => (), - Err(ZoneTreeModificationError::ZoneExists) => (), - Err(e) => return Err(e.into()), - } - // # Try to delete the TSIG key - crate::tsig::delete_tsig(&state.config().tsig_path().join(d.file_name()))?; - } - - for d in added_domains { - let zone: Zone = d.try_into()?; - match state.insert_zone(zone) { - Ok(_) => (), - Err(ZoneTreeModificationError::ZoneExists) => (), - Err(e) => return Err(e.into()), - } - // # Try to create the TSIG key - crate::tsig::generate_new_tsig(&state.config().tsig_path().join(d.file_name()))?; - } - - Ok(()) -} - -#[derive(Deserialize)] -pub struct Domains { - domains: Vec, -} - -#[derive(Deserialize, PartialEq, Eq)] -#[serde(untagged)] -pub enum Domain { - Unamed(String), - Named { - name: String, - tsig_file_name: Option, - }, -} - -impl Domain { - fn domain_name(&self) -> &str { - match self { - Self::Unamed(name) => name, - Self::Named { name, .. } => name, - } - } - - fn file_name(&self) -> String { - match self { - Self::Unamed(name) => name.to_case(convert_case::Case::Snake), - Self::Named { - tsig_file_name: Some(file_name), - .. - } => file_name.into(), - Self::Named { name, .. } => name.to_case(convert_case::Case::Snake), - } - } -} - -impl TryFrom for Zone { - type Error = crate::error::Error; - - fn try_from(value: Domain) -> Result { - let apex_name = match value { - Domain::Named { name, .. } => StoredName::from_chars(name.chars())?, - Domain::Unamed(name) => StoredName::from_chars(name.chars())?, - }; - Ok(ZoneBuilder::new(apex_name, Class::IN).build()) - } -} - -impl TryFrom<&Domain> for Zone { - type Error = crate::error::Error; - - fn try_from(value: &Domain) -> Result { - let apex_name = match value { - Domain::Named { name, .. } => StoredName::from_chars(name.chars())?, - Domain::Unamed(name) => StoredName::from_chars(name.chars())?, - }; - Ok(ZoneBuilder::new(apex_name, Class::IN).build()) - } -} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000..6692be8 --- /dev/null +++ b/src/key.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; +use std::ops::Deref; +use std::path::PathBuf; + +use domain::base::iana::Class; +use domain::zonetree::types::StoredName; +use domain::zonetree::{Zone, ZoneBuilder}; +use serde::Deserialize; + +use crate::error::Result; + +#[derive(Debug, Clone, Deserialize, Default)] +pub struct Keys(HashMap>); + +impl Deref for Keys { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum Domain { + Unamed(String), +} + +pub trait TryIntoZones { + fn try_into_zones(self) -> Result>; +} + +impl TryIntoZones for &[Domain] { + fn try_into_zones(self) -> Result> { + self.iter().map(|d| d.try_into()).collect() + } +} + +impl TryFrom<&Domain> for Zone { + type Error = crate::error::Error; + + fn try_from(value: &Domain) -> Result { + let apex_name = match value { + Domain::Unamed(name) => StoredName::bytes_from_str(name)?, + }; + Ok(ZoneBuilder::new(apex_name, Class::IN).build()) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)] +pub struct KeyFile(String); + +impl KeyFile { + pub fn as_pathbuf(&self) -> PathBuf { + 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 delete_key_file(&self) -> Result<()> { + crate::tsig::delete_tsig(&self.as_pathbuf()) + } +} + +impl std::fmt::Display for KeyFile { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/main.rs b/src/main.rs index 0318fca..1746546 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,15 +25,17 @@ use domain::net::server::stream::StreamServer; use domain::net::server::util::service_fn; use tokio::net::{TcpListener, UdpSocket}; -use crate::fs::Watcher; +use crate::watcher::Watcher; mod config; mod dns; mod error; -mod fs; +mod key; mod logger; mod metric; mod tsig; +mod watcher; +mod zone; #[tokio::main()] async fn main() { @@ -46,13 +48,14 @@ async fn main() { exit(1); } }; - let config = match config::Config::try_from(&bytes) { + let mut config = match config::Config::try_from(&bytes) { Ok(c) => c, Err(e) => { eprintln!("Failed to parse config file at path {}: {}", config_path, e); exit(1); } }; + let keys = config.take_keys().unwrap_or_default(); // Initialize the custom logger logger::Logger::new() @@ -83,7 +86,7 @@ async fn main() { tokio::spawn(async move { tcp_srv.run().await }); - tokio::spawn(async move { Watcher::watch_lock(state).unwrap() }); + tokio::spawn(async move { Watcher::watch_lock(keys, state).unwrap() }); tokio::spawn(async move { metric::log_svc(config, udp_metrics, tcp_metrics).await }); diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 0000000..95368a2 --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,140 @@ +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::key::{Domain, KeyFile, Keys, TryIntoZones}; + +#[derive(Debug, Clone)] +pub struct Watcher; + +impl Watcher { + pub fn watch_lock(mut keys: Keys, state: Arc) -> Result<()> { + // Retrieve path + let file_path = crate::config::Config::config_file_path(); + let path = Path::new(&file_path); + + // Initialize the watcher + let (tx, rx) = channel(); + let mut watcher = Box::new(RecommendedWatcher::new(tx, Config::default())?); + watcher.watch(path, RecursiveMode::NonRecursive)?; + + // Initialize the dns zones + initialize_dns_zones(&keys, &state)?; + + while rx.recv().is_ok() { + keys = handle_file_change(&keys, path, &state)?; + } + + Ok(()) + } +} + +fn initialize_dns_zones(keys: &Keys, state: &Arc) -> Result<()> { + { + // Create the key folder if it does not exist + let path = state.config().tsig_path(); + if !path.is_dir() { + std::fs::create_dir(path)?; + } + } + + for (k, v) in keys.iter() { + v.try_into_zones()? + .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 + ); + } + Err(e) => return Err(e), + } + } + + Ok(()) +} + +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); + let new_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())); + + handle_deleted_keys(state, deleted_keys)?; + handle_added_keys(state, added_keys)?; + handle_modified_keys(state, modified_keys)?; + + Ok(new_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_zones()?.into_iter().for_each(|z| { + let _ = state.remove_zone(z.apex_name(), z.class()); + }); + + // # Try to delete the TSIG key + k.delete_key_file()?; + } + + Ok(()) +} + +fn handle_added_keys<'i, I>(state: &Arc, added_keys: I) -> Result<()> +where + I: IntoIterator)>, +{ + for (k, v) in added_keys { + v.try_into_zones()?.into_iter().for_each(|z| { + let _ = state.insert_zone(z); + }); + + // # Try to create the TSIG key + k.generate_key_file()?; + } + + Ok(()) +} + +fn handle_modified_keys<'i, I>(state: &Arc, modified_keys: I) -> Result<()> +where + I: IntoIterator, &'i Vec)>, +{ + for (nv, ov) in modified_keys { + ov.iter() + .filter(|d| !nv.contains(d)) + .try_for_each(|d| -> Result<()> { + let zone: Zone = d.try_into()?; + let _ = state.remove_zone(zone.apex_name(), zone.class()); + Ok(()) + })?; + nv.iter() + .filter(|d| !ov.contains(d)) + .try_for_each(|d| -> Result<()> { + let zone: Zone = d.try_into()?; + let _ = state.insert_zone(zone); + Ok(()) + })?; + } + + Ok(()) +} diff --git a/src/zone.rs b/src/zone.rs new file mode 100644 index 0000000..32aaaba --- /dev/null +++ b/src/zone.rs @@ -0,0 +1,48 @@ +use std::collections::HashMap; + +use bytes::Bytes; +use domain::base::{name::Name, ToName}; +use domain::zonetree::Zone; + +use crate::error::Result; + +#[derive(Debug, Default)] +pub struct ZoneTree { + zones: HashMap, Zone>, +} + +impl ZoneTree { + pub fn new() -> Self { + Default::default() + } + + pub fn iter_zones(&self) -> impl Iterator { + self.zones.values() + } + + pub fn find_zone(&self, qname: &N) -> Option<&Zone> + where + N: ToName, + { + self.zones.get(&qname.to_name::()) + } + + pub fn insert_zone(&mut self, zone: Zone) -> Result<()> { + match self.zones.insert(zone.apex_name().clone(), zone) { + None => Ok(()), + Some(_) => Err(domain::zonetree::error::ZoneTreeModificationError::ZoneExists.into()), + } + } + + pub fn remove_zone(&mut self, name: &N) -> Result<()> + where + N: ToName, + { + match self.zones.remove(&name.to_name::()) { + None => { + Err(domain::zonetree::error::ZoneTreeModificationError::ZoneDoesNotExist.into()) + } + Some(_) => Ok(()), + } + } +} From cd949fb8d90d03e6fd01a69e5a10806ae020f869 Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Tue, 30 Jul 2024 01:47:27 -0400 Subject: [PATCH 2/2] feat: updated the readme with the new config file --- README.md | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e3cf8d6..7263cb3 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ ## How to use it? -In order to use the `dnsr` you have to create some configuration files and a folder to store the TSIG keys generated. +In order to use the `dnsr` you have to create a configuration file and a folder to store the TSIG keys generated. `dnsr` is deployed as a Docker container. You can use the following command to run it: ```bash -docker run -d -p 8053:8053/udp -v ./config.yml:/etc/dnsr/config.yml -v ./domains.yml:/etc/dnsr/domains.yml -v ./keys:/etc/dnsr/keys ghrc.io/thibault-cne/dnsr:latest +docker run -d -p 8053:8053/udp -v ./config.yml:/etc/dnsr/config.yml -v ./keys:/etc/dnsr/keys ghrc.io/thibault-cne/dnsr:latest ``` ### Configuration files @@ -32,25 +32,22 @@ log: enable_udp_metrics: true # Enable the tcp metrics. enable_tcp_metrics: true -``` - -#### domains.yml - -The `domains.yml` file is used to configure the domains that the `dnsr` server will handle. In the following example, the `dnsr` server will handle the `example1.com`, `example2.com`, and `example3.com` domains: -```yaml ---- -domains: - - example1.com - - name: example2.com - - name: example3.com - # The file name of the TSIG key for the domain. - # The file is located in the `tsig_folder` folder. - # This is optional. If not provided, the key file will be named after the domain name in snake case. - tsig_file_name: example3.key +# The keys and domains configuration +keys: + - key1: + - domain1 + - domain2 + - key2: + - domain3 + - domain4 ``` -**Note:** The `dnsr` server constantly whatches the `domains.yml` file for changes. If the file is modified, the server will reload the domains (e.g. add or remove domains). +In the previous example, the `dnsr` server will handle the domain1, domain2, domain3 and domain4 domains. +The key1 will be used to handle the domain1 and domain2 domains and the key2 will be used to handle the domain3 and domain4 domains. + +**Note**: The dnsr server constantly whatches the `config.yml` file for changes. +If the file is modified, the server will reload the domains (e.g. add or remove domains). ### TSIG keys