diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 33327cf9..daaf27db 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -71,6 +71,7 @@ tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] } async-stream = "0.3.5" async-trait = "0.1.74" +maplit = "1.0.2" dashmap = "6.0" timedmap = "=1.0.1" diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index c99607b8..21661dca 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -735,7 +735,7 @@ impl DnsGlobalCtxExt for GlobalCtx { .clone() .unwrap_or_else(|| dns::parse(self.get_hostname())) .into(); - let fqdn = name.append_domain(&*dns.domain).unwrap_or_default().into(); + let fqdn = name.append_domain(&dns.domain).unwrap_or_default().into(); let ipv4 = self.get_ipv4().map(|ip| ip.address()); let ipv6 = self.get_ipv6().map(|ip| ip.address()); let ipv6 = ipv6.map(|a| vec![a]).unwrap_or_default(); diff --git a/easytier/src/dns/config/zone.rs b/easytier/src/dns/config/zone.rs index e2c8fba9..bab57324 100644 --- a/easytier/src/dns/config/zone.rs +++ b/easytier/src/dns/config/zone.rs @@ -3,12 +3,40 @@ use crate::dns::config::policy::{DnsExportPolicy, ZonePolicyConfig}; use crate::dns::utils::addr::NameServerAddrGroup; use crate::dns::zone::Zone; use crate::proto::dns::ZoneData; +use derive_more::From; +use hickory_proto::op::ResponseCode; use hickory_proto::rr::LowerName; +use maplit::hashset; use optional_struct::{Applicable, optional_struct}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; use std::convert::TryFrom; use std::net::{Ipv4Addr, Ipv6Addr}; -use url::Url; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, From, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Fallthrough { + Any, + ResponseCode(ResponseCode), +} + +impl From for i32 { + fn from(value: Fallthrough) -> Self { + match value { + Fallthrough::ResponseCode(code) => u16::from(code).into(), + Fallthrough::Any => -1, + } + } +} + +impl From for Fallthrough { + fn from(value: i32) -> Self { + match u16::try_from(value) { + Ok(value) => Self::ResponseCode(value.into()), + Err(_) => Self::Any, + } + } +} #[optional_struct(ZoneConfigRaw)] #[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)] @@ -21,7 +49,7 @@ pub struct ZoneConfigParsed { #[optional_skip_wrap] #[serde(flatten)] pub policy: ZonePolicyConfig, - pub fallthrough: bool, + pub fallthrough: HashSet, } impl From<&ZoneConfigParsed> for ZoneData { @@ -30,8 +58,8 @@ impl From<&ZoneConfigParsed> for ZoneData { &value.origin, value.ttl, &value.records, - value.forwarders.iter().map(Url::from), - value.fallthrough, + value.forwarders.iter().map(Into::into), + value.fallthrough.iter().copied(), ) } } @@ -43,7 +71,7 @@ impl TryFrom for ZoneConfig { fn try_from(raw: ZoneConfigRaw) -> Result { let default = ZoneConfigParsed { - fallthrough: true, + fallthrough: hashset! {Fallthrough::Any}, ..Default::default() }; diff --git a/easytier/src/dns/peer_mgr.rs b/easytier/src/dns/peer_mgr.rs index f676dbf9..baac45c1 100644 --- a/easytier/src/dns/peer_mgr.rs +++ b/easytier/src/dns/peer_mgr.rs @@ -254,10 +254,10 @@ mod tests { use crate::peers::peer_manager::RouteAlgoType; use crate::peers::tests::{connect_peer_manager, wait_route_appear}; use crate::proto::dns::GetExportConfigRequest; + use std::collections::HashSet; use std::net::Ipv4Addr; use tokio::time::{Duration, sleep}; - use url::Url; async fn create_peer_manager_with_zone( host: &str, @@ -296,13 +296,7 @@ mod tests { #[test] fn dns_peer_info_try_from_invalid_zone_rejected() { let cfg = DnsExportConfig { - zones: vec![ZoneData::new( - &".".parse().unwrap(), - 60, - ["?"], - Vec::::new(), - false, - )], + zones: vec![ZoneData::new(&".".parse().unwrap(), 60, ["?"], [], [])], }; assert!(DnsPeerInfo::try_from(cfg).is_err()); diff --git a/easytier/src/dns/tests.rs b/easytier/src/dns/tests.rs index 591f5300..b38825d4 100644 --- a/easytier/src/dns/tests.rs +++ b/easytier/src/dns/tests.rs @@ -28,6 +28,7 @@ use hickory_proto::rr; use hickory_proto::rr::{DNSClass, Name, RData, RecordType}; use hickory_proto::serialize::binary::{BinEncodable, BinEncoder}; use hickory_server::server::Request; +use maplit::hashset; use tokio::sync::Notify; use uuid::Uuid; @@ -108,10 +109,8 @@ pub fn zone_data_a_with_forwarders(origin: &str, record: &str, forwarders: Vec<& &origin.parse().unwrap(), 60, [format!("@ IN A {record}")], - forwarders - .into_iter() - .map(|f| Url::from_str(f).expect("invalid forwarder")), - false, + forwarders.into_iter().map(|f| Url::from_str(f).unwrap()), + hashset! {}, ) } diff --git a/easytier/src/dns/utils/addr.rs b/easytier/src/dns/utils/addr.rs index 9b2a6783..70aeb565 100644 --- a/easytier/src/dns/utils/addr.rs +++ b/easytier/src/dns/utils/addr.rs @@ -39,31 +39,19 @@ impl From<(IpAddr, &ConnectionConfig)> for NameServerAddr { } } -impl From<&NameServerAddr> for Url { - fn from(value: &NameServerAddr) -> Self { - Url::parse(&format!("{}://{}", value.protocol, value.addr)).unwrap() - } -} - -impl From for Url { - fn from(value: NameServerAddr) -> Self { - (&value).into() - } -} - impl TryFrom<&Url> for NameServerAddr { type Error = Error; - fn try_from(value: &Url) -> Result { - let protocol = match Protocol::deserialize(value.scheme().into_deserializer()).map_err( - |e: de::value::Error| anyhow!("invalid protocol '{}': {}", value.scheme(), e), - )? { + fn try_from(url: &Url) -> Result { + let protocol = match Protocol::deserialize(url.scheme().into_deserializer()) + .map_err(|e: de::value::Error| anyhow!("invalid protocol '{}': {}", url.scheme(), e))? + { Protocol::Udp => ProtocolConfig::Udp, Protocol::Tcp => ProtocolConfig::Tcp, p => return Err(anyhow!("unsupported protocol: {}", p)), }; - let host = value.host_str().ok_or(anyhow!("host not found"))?; - let port = value.port().unwrap_or(protocol.default_port()); + let host = url.host_str().ok_or(anyhow!("host not found"))?; + let port = url.port().unwrap_or(protocol.default_port()); let addr = if let Ok(addr) = IpAddr::from_str(host) { SocketAddr::new(addr, port) } else { @@ -76,17 +64,35 @@ impl TryFrom<&Url> for NameServerAddr { } } -impl From for proto::common::Url { - fn from(value: NameServerAddr) -> Self { - Url::from(value).into() - } -} - impl TryFrom<&proto::common::Url> for NameServerAddr { type Error = Error; fn try_from(value: &proto::common::Url) -> Result { - Self::try_from(&Url::try_from(value)?) + (&Url::try_from(value)?).try_into() + } +} + +impl From<&NameServerAddr> for Url { + fn from(value: &NameServerAddr) -> Self { + Url::parse(&format!("{}://{}", value.protocol, value.addr)).unwrap() + } +} + +impl From<&NameServerAddr> for proto::common::Url { + fn from(value: &NameServerAddr) -> Self { + Url::from(value).into() + } +} + +impl From for Url { + fn from(value: NameServerAddr) -> Self { + (&value).into() + } +} + +impl From for proto::common::Url { + fn from(value: NameServerAddr) -> Self { + (&value).into() } } diff --git a/easytier/src/dns/utils/zone_handler.rs b/easytier/src/dns/utils/zone_handler.rs index 15b1d7de..73bbe9e3 100644 --- a/easytier/src/dns/utils/zone_handler.rs +++ b/easytier/src/dns/utils/zone_handler.rs @@ -1,35 +1,27 @@ +use crate::dns::config::zone::Fallthrough; use delegate::delegate; -use derive_more::{Deref, DerefMut, From}; +use derive_more::{Constructor, Deref, DerefMut}; use hickory_proto::op::ResponseCode; use hickory_proto::rr::{LowerName, RecordType, TSigResponseContext}; use hickory_server::server::{Request, RequestInfo}; use hickory_server::zone_handler::{ AuthLookup, AxfrPolicy, LookupControlFlow, LookupError, LookupOptions, ZoneHandler, ZoneType, }; +use std::collections::HashSet; use std::sync::Arc; pub type ArcZoneHandler = Arc; -pub trait LookupControlFlowExt { - fn skip_negative(self) -> Self; -} - -impl LookupControlFlowExt for LookupControlFlow { - fn skip_negative(self) -> Self { - match self { - Self::Continue(e) | Self::Break(e) if matches!(e, Err(LookupError::NameExists)) => { - Self::Continue(Ok(Default::default())) - } - Self::Continue(Err(_)) | Self::Break(Err(_)) => Self::Skip, - other => other, - } - } -} - -#[derive(From, Deref, DerefMut)] -pub struct ChainedZoneHandler(H) +#[derive(Constructor, Deref, DerefMut)] +pub struct ChainedZoneHandler where - H: ZoneHandler; + H: ZoneHandler, +{ + #[deref] + #[deref_mut] + handler: H, + fallthrough: HashSet, +} #[async_trait::async_trait] impl ZoneHandler for ChainedZoneHandler @@ -37,7 +29,7 @@ where H: ZoneHandler, { delegate! { - to self.0 { + to self.handler { fn zone_type(&self) -> ZoneType; fn axfr_policy(&self) -> AxfrPolicy; fn origin(&self) -> &LowerName; @@ -50,7 +42,7 @@ where update: &Request, now: u64, ) -> (Result, Option) { - self.0.update(update, now).await + self.handler.update(update, now).await } #[inline] async fn lookup( @@ -60,29 +52,9 @@ where request_info: Option<&RequestInfo<'_>>, lookup_options: LookupOptions, ) -> LookupControlFlow { - self.0 + self.handler .lookup(name, rtype, request_info, lookup_options) .await - .skip_negative() - } - #[inline] - async fn consult( - &self, - name: &LowerName, - rtype: RecordType, - request_info: Option<&RequestInfo<'_>>, - lookup_options: LookupOptions, - last_result: LookupControlFlow, - ) -> (LookupControlFlow, Option) { - let result = if let Some(Ok(l)) = last_result.map_result() { - LookupControlFlow::Break(Ok(l)) - } else { - self.0 - .lookup(name, rtype, request_info, lookup_options) - .await - .skip_negative() - }; - (result, None) } #[inline] async fn search( @@ -90,8 +62,17 @@ where request: &Request, lookup_options: LookupOptions, ) -> (LookupControlFlow, Option) { - let (result, tsig) = self.0.search(request, lookup_options).await; - (result.skip_negative(), tsig) + let (result, tsig) = self.handler.search(request, lookup_options).await; + + match &result { + LookupControlFlow::Continue(Err(e)) | LookupControlFlow::Break(Err(e)) + if self.fallthrough.contains(&Fallthrough::Any) + || matches!(e, LookupError::ResponseCode(c) if self.fallthrough.contains(&(*c).into())) => + { + (LookupControlFlow::Skip, None) + } + _ => (result, tsig), + } } #[inline] async fn nsec_records( @@ -99,6 +80,6 @@ where name: &LowerName, lookup_options: LookupOptions, ) -> LookupControlFlow { - self.0.nsec_records(name, lookup_options).await + self.handler.nsec_records(name, lookup_options).await } } diff --git a/easytier/src/dns/zone.rs b/easytier/src/dns/zone.rs index bda697c0..0d1da7e8 100644 --- a/easytier/src/dns/zone.rs +++ b/easytier/src/dns/zone.rs @@ -1,3 +1,4 @@ +use crate::dns::config::zone::Fallthrough; use crate::dns::utils::addr::{NameServerAddr, NameServerAddrGroup}; use crate::dns::utils::zone_handler::{ArcZoneHandler, ChainedZoneHandler}; use crate::proto::dns::ZoneData; @@ -11,16 +12,16 @@ use hickory_server::store::in_memory::InMemoryZoneHandler; use hickory_server::zone_handler::{AxfrPolicy, ZoneType}; use indexmap::IndexMap; use itertools::chain; -use std::collections::BTreeMap; +use maplit::hashset; +use std::collections::{BTreeMap, HashSet}; use std::sync::Arc; -use url::Url; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Zone { origin: LowerName, records: BTreeMap, pub forward: Option, - fallthrough: bool, + fallthrough: HashSet, } impl Zone { @@ -30,23 +31,16 @@ impl Zone { name_servers: config.name_servers().to_vec(), options: Some(opts), }; - let mut zone = Self::new(".".parse().unwrap()); - zone.forward = Some(forward); - zone.fallthrough = false; - zone + Self { + origin: ".".parse().unwrap(), + forward: Some(forward), + fallthrough: hashset! {}, + ..Default::default() + } } } impl Zone { - pub fn new(name: LowerName) -> Self { - Self { - origin: name, - records: BTreeMap::new(), - forward: None, - fallthrough: true, - } - } - pub fn create_memory_zone_handler(&self) -> Option { (!self.records.is_empty()).then(|| { let mut memory = InMemoryZoneHandler::::empty( @@ -62,11 +56,7 @@ impl Zone { .map(|(k, v)| (k, Arc::new(v))), ); - if self.fallthrough { - Arc::new(ChainedZoneHandler::from(memory)) as _ - } else { - Arc::new(memory) as _ - } + Arc::new(ChainedZoneHandler::new(memory, self.fallthrough.clone())) as _ }) } @@ -77,14 +67,10 @@ impl Zone { TokioRuntimeProvider::default(), ) .build() - .inspect_err(|e| tracing::error!("failed to create forward zone_handler: {:?}", e)) + .inspect_err(|error| tracing::error!(?error, "failed to create forward zone_handler")) .ok() - .map(|f| { - if self.fallthrough { - Arc::new(ChainedZoneHandler::from(f)) as _ - } else { - Arc::new(f) as _ - } + .map(|handler| { + Arc::new(ChainedZoneHandler::new(handler, self.fallthrough.clone())) as _ }) }) } @@ -101,7 +87,7 @@ impl TryFrom<&ZoneData> for Zone { let name_servers = value .forwarders .iter() - .map(TryInto::::try_into) + .map(NameServerAddr::try_from) .map(|a| a.map(Into::into)) .collect::, _>>()?; let forward = (!name_servers.is_empty()).then_some(ForwardConfig { @@ -109,11 +95,13 @@ impl TryFrom<&ZoneData> for Zone { options: None, }); + let fallthrough = value.fallthrough.iter().copied().map(Into::into).collect(); + Ok(Self { origin: origin.into(), records, forward, - fallthrough: value.fallthrough, + fallthrough, }) } } @@ -132,7 +120,7 @@ impl From for ZoneData { .flat_map(|f| f.name_servers.into_iter()) .map(|ns| (&ns).into()) .flat_map(NameServerAddrGroup::into_iter) - .map(Url::from); + .map(Into::into); Self::new(&value.origin, 0, records, forwarders, value.fallthrough) } @@ -169,7 +157,9 @@ mod tests { use hickory_proto::rr::{RData, Record, RecordType, RrsetRecords}; use hickory_server::Server; use hickory_server::zone_handler::Catalog; - use std::net::{Ipv4Addr, SocketAddr}; + use maplit::hashset; + use std::collections::HashSet; + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; use std::str::FromStr; use tokio::net::UdpSocket; use tokio::task::JoinHandle; @@ -193,21 +183,21 @@ mod tests { origin: &str, records: Vec<&str>, forwarders: Vec<&str>, - fallthrough: bool, + fallthrough: HashSet, ) -> ZoneData { ZoneData::new( &origin.parse().unwrap(), 60, records, - forwarders.into_iter().map(|url| Url { - url: url.to_string(), - }), + forwarders + .into_iter() + .map(|url| Url::from_str(url).unwrap()), fallthrough, ) } fn zone_data(origin: &str, records: Vec<&str>, forwarders: Vec<&str>) -> ZoneData { - zone_data_with_fallthrough(origin, records, forwarders, true) + zone_data_with_fallthrough(origin, records, forwarders, hashset! {Fallthrough::Any}) } fn build_catalog(zones: ZoneGroup) -> Catalog { @@ -242,6 +232,13 @@ mod tests { .any(|record| matches!(record.data, RData::A(addr) if *addr == expected)) } + fn has_aaaa_answer(message: &Message, expected: Ipv6Addr) -> bool { + message + .answers + .iter() + .any(|record| matches!(record.data, RData::AAAA(addr) if *addr == expected)) + } + async fn start_upstream_server() -> anyhow::Result<(SocketAddr, JoinHandle<()>)> { let upstream = Zone::try_from(&zone_data( "upstream.test", @@ -400,7 +397,7 @@ mod tests { } #[tokio::test] - async fn catalog_lookup_returns_nodata_on_nameexists() -> anyhow::Result<()> { + async fn catalog_lookup_forwards_on_nameexists() -> anyhow::Result<()> { let upstream = Zone::try_from(&zone_data( "forward-aaaa.test", vec!["host 60 IN AAAA 2001:db8::1"], @@ -435,10 +432,7 @@ mod tests { assert_eq!(rcode, ResponseCode::NoError); let message = message.expect("response should exist"); - assert!( - message.answers.is_empty(), - "NameExists should return NODATA" - ); + assert!(has_aaaa_answer(&message, "2001:db8::1".parse()?)); upstream_handle.abort(); let _ = upstream_handle.await; @@ -528,13 +522,13 @@ mod tests { "fallback-disabled.test", vec!["first IN A 10.20.31.1"], vec![], - false, + hashset! {}, ))?, Zone::try_from(&zone_data_with_fallthrough( "fallback-disabled.test", vec!["target IN A 10.20.31.2"], vec![], - false, + hashset! {}, ))?, ] .into(); diff --git a/easytier/src/proto/dns.proto b/easytier/src/proto/dns.proto index 1bf0dd7f..e992d4d5 100644 --- a/easytier/src/proto/dns.proto +++ b/easytier/src/proto/dns.proto @@ -7,7 +7,7 @@ package dns; message ZoneData { string content = 1; repeated common.Url forwarders = 2; - bool fallthrough = 3; + repeated int32 fallthrough = 3; } message GetExportConfigRequest {} diff --git a/easytier/src/proto/dns.rs b/easytier/src/proto/dns.rs index cbf6e17a..77553d91 100644 --- a/easytier/src/proto/dns.rs +++ b/easytier/src/proto/dns.rs @@ -1,3 +1,4 @@ +use crate::dns::config::zone::Fallthrough; use crate::proto::common::Url; use crate::proto::utils::TransientDigest; use hickory_proto::rr::LowerName; @@ -13,19 +14,13 @@ impl HeartbeatRequest { } impl ZoneData { - pub fn new( + pub fn new>( origin: &LowerName, ttl: u32, - records: Records, - forwarders: Urls, - fallthrough: bool, - ) -> Self - where - Records: IntoIterator, - R: AsRef, - Urls: IntoIterator, - U: Into, - { + records: impl IntoIterator, + forwarders: impl IntoIterator, + fallthrough: impl IntoIterator, + ) -> Self { let mut content = String::new(); content.push_str("; EasyTier Magic DNS zone data\n"); @@ -44,7 +39,8 @@ impl ZoneData { content.push('\n'); } - let forwarders = forwarders.into_iter().map(Into::into).collect(); + let forwarders = forwarders.into_iter().collect(); + let fallthrough = fallthrough.into_iter().map(Into::into).collect(); Self { content,