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/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..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 { @@ -89,6 +109,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..fe5c018 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,19 +1,23 @@ +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::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 +25,82 @@ 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 TryIntoZones { - fn try_into_zones(self) -> Result>; +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)] +pub struct DomainName(String); + +pub trait TryInto { + fn try_into_t(self) -> Result; } -impl TryIntoZones for &[Domain] { - fn try_into_zones(self) -> Result> { - self.iter().map(|d| d.try_into()).collect() +impl TryInto> for &HashMap { + fn try_into_t(self) -> Result> { + self.iter().map(|d| d.try_into_t()).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 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( + owner.freeze().try_into_t()?, + Class::IN, + Ttl::HOUR, + Soa::new( + (&value.mname).try_into_t()?, + (&value.rname).try_into_t()?, + Serial::now(), + Ttl::from_secs(10800), + 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 TryInto for (&DomainName, &DomainInfo) { + fn try_into_t(self) -> Result { + let (name, info) = self; + 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 TryInto for B +where + B: AsRef<[u8]>, +{ + 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 95368a2..81ff773 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, DomainName, KeyFile, Keys, TryInto}; #[derive(Debug, Clone)] pub struct Watcher; @@ -45,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() { @@ -85,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()); }); @@ -101,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); }); @@ -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_t()?; 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_t()?; let _ = state.insert_zone(zone); Ok(()) })?;