From 873211fac6d2bb8094218cbe2f1c3f6a228261aa Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Tue, 30 Jul 2024 10:40:42 -0400 Subject: [PATCH 1/2] feat: added the new configuration file to handle correct domain creation for acme --- config.yml | 15 ++++++++-- src/error.rs | 9 ++++++ src/key.rs | 75 ++++++++++++++++++++++++++++++++++++++++---------- src/watcher.rs | 22 +++++++++------ 4 files changed, 95 insertions(+), 26 deletions(-) diff --git a/config.yml b/config.yml index d296fdb..00d8609 100644 --- a/config.yml +++ b/config.yml @@ -12,7 +12,16 @@ log: keys: key1: - - thibault-cne.fr + sub.example.fr: + mname: ns-acme.example.fr. + rname: postmaster.example.fr. + example.fr: + mname: ns-acme.example.fr. + rname: postmaster.example.fr. key2: - - example.com - - example.org + another-example.fr: + mname: ns-acme.another-example.fr. + rname: postmaster.another-example.fr. + fake.another-example.fr: + mname: ns-acme.another-example.fr. + rname: postmaster.another-example.fr. diff --git a/src/error.rs b/src/error.rs index 5af3bf7..14b5b3a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -89,6 +89,15 @@ impl From for Error { } } +impl From for Error { + fn from(_: domain::zonetree::error::OutOfZone) -> Self { + Self { + kind: ErrorKind::DomainZone, + message: Some("out of zone".to_string()), + } + } +} + impl From for Error { fn from(value: std::io::Error) -> Self { Self { diff --git a/src/key.rs b/src/key.rs index 6692be8..1262956 100644 --- a/src/key.rs +++ b/src/key.rs @@ -3,17 +3,19 @@ use std::ops::Deref; use std::path::PathBuf; use domain::base::iana::Class; -use domain::zonetree::types::StoredName; -use domain::zonetree::{Zone, ZoneBuilder}; +use domain::base::{Record, Serial, Ttl}; +use domain::rdata::Soa; +use domain::zonetree::types::{StoredName, StoredRecord}; +use domain::zonetree::{Rrset, SharedRrset, Zone, ZoneBuilder}; use serde::Deserialize; use crate::error::Result; #[derive(Debug, Clone, Deserialize, Default)] -pub struct Keys(HashMap>); +pub struct Keys(HashMap>); impl Deref for Keys { - type Target = HashMap>; + type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 @@ -21,29 +23,72 @@ impl Deref for Keys { } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -#[serde(untagged)] -pub enum Domain { - Unamed(String), +pub struct DomainInfo { + mname: String, + rname: String, +} + +pub trait TryIntoZone { + fn try_into_zone(self) -> Result; } pub trait TryIntoZones { fn try_into_zones(self) -> Result>; } -impl TryIntoZones for &[Domain] { +trait TryIntoStoredName { + fn try_into_stored_name(self) -> Result; +} + +impl TryIntoZones for &HashMap { fn try_into_zones(self) -> Result> { - self.iter().map(|d| d.try_into()).collect() + self.iter().map(|d| d.try_into_zone()).collect() } } -impl TryFrom<&Domain> for Zone { +impl TryFrom<&DomainInfo> for SharedRrset { 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()) + fn try_from(value: &DomainInfo) -> std::result::Result { + let record: StoredRecord = Record::new( + (&value.mname).try_into_stored_name()?, + Class::IN, + Ttl::HOUR, + Soa::new( + (&value.mname).try_into_stored_name()?, + (&value.rname).try_into_stored_name()?, + Serial::now(), + Ttl::HOUR, + Ttl::HOUR, + Ttl::HOUR, + Ttl::HOUR, + ) + .into(), + ); + let rset: Rrset = record.into(); + + Ok(rset.into_shared()) + } +} + +impl TryIntoZone for (S, &DomainInfo) +where + S: AsRef, +{ + fn try_into_zone(self) -> Result { + let (name, info) = self; + let mut builder = ZoneBuilder::new((&name).try_into_stored_name()?, Class::IN); + builder.insert_rrset(&name.try_into_stored_name()?, info.try_into()?)?; + Ok(builder.build()) + } +} + +impl TryIntoStoredName for S +where + S: AsRef, +{ + fn try_into_stored_name(self) -> Result { + Ok(StoredName::bytes_from_str(self.as_ref())?) } } diff --git a/src/watcher.rs b/src/watcher.rs index 95368a2..2ec8b85 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::File; use std::path::Path; use std::sync::mpsc::channel; @@ -8,7 +9,7 @@ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher use crate::dns::State; use crate::error::{ErrorKind, Result}; -use crate::key::{Domain, KeyFile, Keys, TryIntoZones}; +use crate::key::{DomainInfo, KeyFile, Keys, TryIntoZone, TryIntoZones}; #[derive(Debug, Clone)] pub struct Watcher; @@ -85,7 +86,7 @@ fn handle_file_change(keys: &Keys, config_path: &Path, state: &Arc) -> Re fn handle_deleted_keys<'i, I>(state: &Arc, deleted_keys: I) -> Result<()> where - I: IntoIterator)>, + I: IntoIterator)>, { for (k, v) in deleted_keys { v.try_into_zones()?.into_iter().for_each(|z| { @@ -101,7 +102,7 @@ where fn handle_added_keys<'i, I>(state: &Arc, added_keys: I) -> Result<()> where - I: IntoIterator)>, + I: IntoIterator)>, { for (k, v) in added_keys { v.try_into_zones()?.into_iter().for_each(|z| { @@ -117,20 +118,25 @@ where fn handle_modified_keys<'i, I>(state: &Arc, modified_keys: I) -> Result<()> where - I: IntoIterator, &'i Vec)>, + I: IntoIterator< + Item = ( + &'i HashMap, + &'i HashMap, + ), + >, { for (nv, ov) in modified_keys { ov.iter() - .filter(|d| !nv.contains(d)) + .filter(|&(d, _)| nv.get(d).is_none()) .try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into()?; + let zone: Zone = d.try_into_zone()?; let _ = state.remove_zone(zone.apex_name(), zone.class()); Ok(()) })?; nv.iter() - .filter(|d| !ov.contains(d)) + .filter(|&(d, _)| ov.get(d).is_none()) .try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into()?; + let zone: Zone = d.try_into_zone()?; let _ = state.insert_zone(zone); Ok(()) })?; From 40d8de0a78ada550e446678b5fea8e2ec4eb43a4 Mon Sep 17 00:00:00 2001 From: Thibault Cheneviere Date: Tue, 30 Jul 2024 11:41:51 -0400 Subject: [PATCH 2/2] feat: fixed the record creation for each domain registered --- README.md | 32 ++++++++++++++++------ src/error.rs | 20 ++++++++++++++ src/key.rs | 74 +++++++++++++++++++++++++++++--------------------- src/watcher.rs | 20 +++++++------- 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 7263cb3..aef95a7 100644 --- a/README.md +++ b/README.md @@ -35,16 +35,32 @@ log: # The keys and domains configuration keys: - - key1: - - domain1 - - domain2 - - key2: - - domain3 - - domain4 + key1: + sub.example.fr: + mname: ns-acme.example.fr. + rname: postmaster.example.fr. + example.fr: + mname: ns-acme.example.fr. + rname: postmaster.example.fr. + key2: + another-example.fr: + mname: ns-acme.another-example.fr. + rname: postmaster.another-example.fr. + fake.another-example.fr: + mname: ns-acme.another-example.fr. + rname: postmaster.another-example.fr. ``` -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. +In the previous example, the `dnsr` server will handle the `sub.example.fr`, `example.fr`, `another-example.fr` and `fake.another-example.fr` domains. +The key1 will be used to handle the `sub.example.fr` and `example.fr` domains and the key2 will be used to handle the `another-example.fr` and `fake.another-example.fr` domains. + +The record created is the following for the `sub.example.fr` domain: + +```text +_acme-challenge.sub.example.fr. 3600 IN SOA ns-acme.example.fr. postmaster.example.fr. 1722353587 10800 3600 605800 3600 +``` + +**Note**: The prefix `_acme-challenge` is automatically added to the domain name. **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). diff --git a/src/error.rs b/src/error.rs index 14b5b3a..b5b5917 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ pub enum ErrorKind { TSIGFileAlreadyExist, RingUnspecified, Base16, + Utf8, } impl std::fmt::Display for Error { @@ -40,6 +41,7 @@ impl std::fmt::Display for ErrorKind { TSIGFileAlreadyExist => write!(f, "tsig file already exists"), RingUnspecified => write!(f, "ring unspecified error"), Base16 => write!(f, "base16 error"), + Utf8 => write!(f, "utf8 error"), } } } @@ -53,6 +55,15 @@ impl From for Error { } } +impl From for Error { + fn from(value: std::str::Utf8Error) -> Self { + Self { + kind: ErrorKind::Utf8, + message: Some(value.to_string()), + } + } +} + impl From for Error { fn from(value: notify::Error) -> Self { Self { @@ -80,6 +91,15 @@ impl From for Error { } } +impl From for Error { + fn from(value: domain::base::name::NameError) -> Self { + Self { + kind: ErrorKind::DomainStr, + message: Some(value.to_string()), + } + } +} + impl From for Error { fn from(value: domain::zonetree::error::ZoneTreeModificationError) -> Self { Self { diff --git a/src/key.rs b/src/key.rs index 1262956..fe5c018 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,7 +1,9 @@ +use core::str; use std::collections::HashMap; use std::ops::Deref; use std::path::PathBuf; +use bytes::BytesMut; use domain::base::iana::Class; use domain::base::{Record, Serial, Ttl}; use domain::rdata::Soa; @@ -12,10 +14,10 @@ use serde::Deserialize; use crate::error::Result; #[derive(Debug, Clone, Deserialize, Default)] -pub struct Keys(HashMap>); +pub struct Keys(HashMap>); impl Deref for Keys { - type Target = HashMap>; + type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 @@ -28,21 +30,16 @@ pub struct DomainInfo { rname: String, } -pub trait TryIntoZone { - fn try_into_zone(self) -> Result; -} - -pub trait TryIntoZones { - fn try_into_zones(self) -> Result>; -} +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)] +pub struct DomainName(String); -trait TryIntoStoredName { - fn try_into_stored_name(self) -> Result; +pub trait TryInto { + fn try_into_t(self) -> Result; } -impl TryIntoZones for &HashMap { - fn try_into_zones(self) -> Result> { - self.iter().map(|d| d.try_into_zone()).collect() +impl TryInto> for &HashMap { + fn try_into_t(self) -> Result> { + self.iter().map(|d| d.try_into_t()).collect() } } @@ -50,45 +47,60 @@ impl TryFrom<&DomainInfo> for SharedRrset { type Error = crate::error::Error; fn try_from(value: &DomainInfo) -> std::result::Result { + let mut owner = BytesMut::with_capacity(16 + value.mname.len()); + owner.extend_from_slice(b"_acme-challenge."); + owner.extend_from_slice(value.mname.as_bytes()); + let record: StoredRecord = Record::new( - (&value.mname).try_into_stored_name()?, + owner.freeze().try_into_t()?, Class::IN, Ttl::HOUR, Soa::new( - (&value.mname).try_into_stored_name()?, - (&value.rname).try_into_stored_name()?, + (&value.mname).try_into_t()?, + (&value.rname).try_into_t()?, Serial::now(), + Ttl::from_secs(10800), Ttl::HOUR, - Ttl::HOUR, - Ttl::HOUR, + Ttl::from_secs(605800), Ttl::HOUR, ) .into(), ); + log::debug!(target: "record", "new record created: {:?}", record); let rset: Rrset = record.into(); Ok(rset.into_shared()) } } -impl TryIntoZone for (S, &DomainInfo) -where - S: AsRef, -{ - fn try_into_zone(self) -> Result { +impl TryInto for (&DomainName, &DomainInfo) { + fn try_into_t(self) -> Result { let (name, info) = self; - let mut builder = ZoneBuilder::new((&name).try_into_stored_name()?, Class::IN); - builder.insert_rrset(&name.try_into_stored_name()?, info.try_into()?)?; - Ok(builder.build()) + let mut builder = ZoneBuilder::new(name.try_into_t()?, Class::IN); + builder.insert_rrset(&name.try_into_t()?, info.try_into()?)?; + let zone = builder.build(); + log::debug!(target: "zone", "new zone created: {:?}", zone); + Ok(zone) + } +} + +impl TryInto for &DomainName { + fn try_into_t(self) -> Result { + let mut owner = BytesMut::with_capacity(16 + self.0.len()); + owner.extend_from_slice(b"_acme-challenge."); + owner.extend_from_slice(self.0.as_bytes()); + + owner.freeze().try_into_t() } } -impl TryIntoStoredName for S +impl TryInto for B where - S: AsRef, + B: AsRef<[u8]>, { - fn try_into_stored_name(self) -> Result { - Ok(StoredName::bytes_from_str(self.as_ref())?) + fn try_into_t(self) -> Result { + let str = str::from_utf8(self.as_ref())?; + Ok(StoredName::bytes_from_str(str)?) } } diff --git a/src/watcher.rs b/src/watcher.rs index 2ec8b85..81ff773 100644 --- a/src/watcher.rs +++ b/src/watcher.rs @@ -9,7 +9,7 @@ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher use crate::dns::State; use crate::error::{ErrorKind, Result}; -use crate::key::{DomainInfo, KeyFile, Keys, TryIntoZone, TryIntoZones}; +use crate::key::{DomainInfo, DomainName, KeyFile, Keys, TryInto}; #[derive(Debug, Clone)] pub struct Watcher; @@ -46,7 +46,7 @@ fn initialize_dns_zones(keys: &Keys, state: &Arc) -> Result<()> { } for (k, v) in keys.iter() { - v.try_into_zones()? + v.try_into_t()? .into_iter() .try_for_each(|z| state.insert_zone(z))?; match k.generate_key_file() { @@ -86,10 +86,10 @@ fn handle_file_change(keys: &Keys, config_path: &Path, state: &Arc) -> Re fn handle_deleted_keys<'i, I>(state: &Arc, deleted_keys: I) -> Result<()> where - I: IntoIterator)>, + I: IntoIterator)>, { for (k, v) in deleted_keys { - v.try_into_zones()?.into_iter().for_each(|z| { + v.try_into_t()?.into_iter().for_each(|z| { let _ = state.remove_zone(z.apex_name(), z.class()); }); @@ -102,10 +102,10 @@ where fn handle_added_keys<'i, I>(state: &Arc, added_keys: I) -> Result<()> where - I: IntoIterator)>, + I: IntoIterator)>, { for (k, v) in added_keys { - v.try_into_zones()?.into_iter().for_each(|z| { + v.try_into_t()?.into_iter().for_each(|z| { let _ = state.insert_zone(z); }); @@ -120,8 +120,8 @@ fn handle_modified_keys<'i, I>(state: &Arc, modified_keys: I) -> Result<( where I: IntoIterator< Item = ( - &'i HashMap, - &'i HashMap, + &'i HashMap, + &'i HashMap, ), >, { @@ -129,14 +129,14 @@ where ov.iter() .filter(|&(d, _)| nv.get(d).is_none()) .try_for_each(|d| -> Result<()> { - let zone: Zone = d.try_into_zone()?; + 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_zone()?; + let zone: Zone = d.try_into_t()?; let _ = state.insert_zone(zone); Ok(()) })?;