diff --git a/easytier/src/dns/config/policy.rs b/easytier/src/dns/config/policy.rs index 5ffba3eb..28c91f75 100644 --- a/easytier/src/dns/config/policy.rs +++ b/easytier/src/dns/config/policy.rs @@ -1,14 +1,14 @@ use derive_more::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] #[serde(default)] pub struct AclPolicy { pub whitelist: Option>, pub blacklist: Option>, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default, Deref, DerefMut)] +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)] #[serde(default)] pub struct FunctionalityPolicy { #[serde(flatten)] @@ -18,7 +18,7 @@ pub struct FunctionalityPolicy { pub disabled: bool, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default, Deref, DerefMut)] +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize, Deref, DerefMut)] #[serde(default)] pub struct DnsPolicy

{ #[serde(flatten)] @@ -32,16 +32,15 @@ pub type ZoneExportPolicy = FunctionalityPolicy; pub type DnsExportPolicy = DnsPolicy; pub type DnsImportPolicy = DnsPolicy; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] #[serde(default)] pub struct DnsPolicyConfig { pub import: DnsImportPolicy, pub export: Option, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)] #[serde(default)] pub struct ZonePolicyConfig { - #[serde(default)] pub export: Option, } diff --git a/easytier/src/dns/config/zone.rs b/easytier/src/dns/config/zone.rs index 680cfe3c..60690db0 100644 --- a/easytier/src/dns/config/zone.rs +++ b/easytier/src/dns/config/zone.rs @@ -69,20 +69,21 @@ impl ZoneConfig { } } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] +#[derive(Derivative, Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derivative(Default)] +#[serde(default)] pub struct ZoneConfigInner { - #[serde(default = "Uuid::new_v4")] + #[derivative(Default(value = "Uuid::new_v4()"))] #[serde(skip_serializing)] id: Uuid, pub origin: LowerName, - #[serde(default)] pub ttl: u32, - #[serde(default)] pub records: Vec, - #[serde(default)] pub forwarders: NameServerAddrGroup, #[serde(flatten)] pub policy: ZonePolicyConfig, + #[derivative(Default(value = "true"))] + pub fallthrough: bool, } impl From for ZoneData { @@ -93,6 +94,7 @@ impl From for ZoneData { ttl: value.ttl, records: value.records, forwarders: value.forwarders.into(), + fallthrough: value.fallthrough, } } } diff --git a/easytier/src/dns/peer_mgr.rs b/easytier/src/dns/peer_mgr.rs index f33a94f3..19146dc5 100644 --- a/easytier/src/dns/peer_mgr.rs +++ b/easytier/src/dns/peer_mgr.rs @@ -250,6 +250,7 @@ mod tests { ttl: 60, records: vec!["@ IN A 10.0.0.11".to_string()], forwarders: vec![], + fallthrough: false, }], fqdn: "invalid.peer.test".to_string(), }; diff --git a/easytier/src/dns/tests.rs b/easytier/src/dns/tests.rs index c100844e..efe655e2 100644 --- a/easytier/src/dns/tests.rs +++ b/easytier/src/dns/tests.rs @@ -114,6 +114,7 @@ pub fn zone_data_a_with_forwarders(origin: &str, record: &str, forwarders: Vec<& .into_iter() .map(|f| Url::from_str(f).expect("invalid forwarder")) .collect(), + fallthrough: false, } } @@ -344,8 +345,10 @@ async fn wait_peer_zone_visibility( loop { dns.refresh(target_peer_id).await; - let visible = dns - .snapshot() + + let snapshot = dns.snapshot(); + + let visible = snapshot .zones .iter() .any(|z| z.origin.contains(zone_origin_substr)); @@ -354,12 +357,19 @@ async fn wait_peer_zone_visibility( return; } + let origins = snapshot + .zones + .iter() + .map(|z| z.origin.clone()) + .collect::>(); + assert!( Instant::now() < deadline, - "zone visibility mismatch for '{}': expected {}, got {}", + "zone visibility mismatch for '{}': expected {}, got {}, current origins: {:?}", zone_origin_substr, expected_visible, - visible + visible, + origins ); tokio::time::sleep(Duration::from_millis(200)).await; } diff --git a/easytier/src/dns/zone.rs b/easytier/src/dns/zone.rs index be3287c6..3cdd6eb6 100644 --- a/easytier/src/dns/zone.rs +++ b/easytier/src/dns/zone.rs @@ -1,6 +1,6 @@ use crate::common::dns::get_default_resolver_config; use crate::dns::utils::addr::{NameServerAddr, NameServerAddrGroup}; -use crate::dns::utils::zone_handler::ArcZoneHandler; +use crate::dns::utils::zone_handler::{ArcZoneHandler, ChainedZoneHandler}; use crate::proto; use crate::proto::utils::RepeatedMessageModel; use hickory_net::runtime::TokioRuntimeProvider; @@ -23,6 +23,7 @@ pub struct Zone { origin: LowerName, records: BTreeMap, pub forward: Option, + fallthrough: bool, } impl Zone { @@ -35,6 +36,7 @@ impl Zone { }; let mut zone = Self::new(".".parse().unwrap()); zone.forward = Some(forward); + zone.fallthrough = false; zone } } @@ -46,6 +48,7 @@ impl Zone { origin: name, records: BTreeMap::new(), forward: None, + fallthrough: true, } } @@ -64,7 +67,11 @@ impl Zone { .map(|(k, v)| (k, Arc::new(v))), ); - Arc::new(memory) as ArcZoneHandler + if self.fallthrough { + Arc::new(ChainedZoneHandler::from(memory)) as _ + } else { + Arc::new(memory) as _ + } }) } @@ -77,7 +84,13 @@ impl Zone { .build() .inspect_err(|e| tracing::error!("failed to create forward zone_handler: {:?}", e)) .ok() - .map(|f| Arc::new(f) as ArcZoneHandler) + .map(|f| { + if self.fallthrough { + Arc::new(ChainedZoneHandler::from(f)) as _ + } else { + Arc::new(f) as _ + } + }) }) } } @@ -111,6 +124,7 @@ impl TryFrom<&proto::dns::ZoneData> for Zone { origin: origin.into(), records, forward, + fallthrough: value.fallthrough, }) } } @@ -139,6 +153,7 @@ impl From for proto::dns::ZoneData { ttl: 0, records, forwarders, + fallthrough: value.fallthrough, } } } @@ -195,7 +210,12 @@ mod tests { } } - fn zone_data(origin: &str, records: Vec<&str>, forwarders: Vec<&str>) -> ZoneData { + fn zone_data_with_fallthrough( + origin: &str, + records: Vec<&str>, + forwarders: Vec<&str>, + fallthrough: bool, + ) -> ZoneData { ZoneData { id: Some(Uuid::new_v4().into()), origin: origin.to_string(), @@ -205,9 +225,14 @@ mod tests { .into_iter() .map(|f| Url::from_str(f).expect("invalid forwarder")) .collect(), + fallthrough, } } + fn zone_data(origin: &str, records: Vec<&str>, forwarders: Vec<&str>) -> ZoneData { + zone_data_with_fallthrough(origin, records, forwarders, true) + } + fn build_catalog(zones: ZoneGroup) -> Catalog { zones .into_groups() @@ -273,6 +298,7 @@ mod tests { ttl: 60, records: vec!["@ IN A 10.0.0.1".to_string()], forwarders: vec![], + fallthrough: false, }; let err = Zone::try_from(&data).expect_err("missing id should fail"); @@ -416,6 +442,67 @@ mod tests { Ok(()) } + #[tokio::test] + async fn catalog_lookup_falls_back_to_later_zone_handler_with_same_origin() -> anyhow::Result<()> + { + let zones: ZoneGroup = vec![ + // First matching zone exists but does not contain the queried name. + Zone::try_from(&zone_data( + "fallback.test", + vec!["first IN A 10.20.30.1"], + vec![], + ))?, + // Second matching zone should be queried as fallback and answer. + Zone::try_from(&zone_data( + "fallback.test", + vec!["target IN A 10.20.30.2"], + vec![], + ))?, + ] + .into(); + let catalog = build_catalog(zones); + + let (rcode, message) = + lookup_message(&catalog, "target.fallback.test.", RecordType::A).await?; + assert_eq!(rcode, ResponseCode::NoError); + assert!(has_a_answer( + &message.expect("response should exist"), + Ipv4Addr::new(10, 20, 30, 2) + )); + + Ok(()) + } + + #[tokio::test] + async fn catalog_lookup_does_not_fall_back_when_fallthrough_disabled() -> anyhow::Result<()> { + let zones: ZoneGroup = vec![ + Zone::try_from(&zone_data_with_fallthrough( + "fallback-disabled.test", + vec!["first IN A 10.20.31.1"], + vec![], + false, + ))?, + Zone::try_from(&zone_data_with_fallthrough( + "fallback-disabled.test", + vec!["target IN A 10.20.31.2"], + vec![], + false, + ))?, + ] + .into(); + let catalog = build_catalog(zones); + + let (rcode, message) = + lookup_message(&catalog, "target.fallback-disabled.test.", RecordType::A).await?; + + assert_ne!(rcode, ResponseCode::NoError); + if let Some(message) = message.as_ref() { + assert!(!has_a_answer(message, Ipv4Addr::new(10, 20, 31, 2))); + } + + Ok(()) + } + #[tokio::test] async fn catalog_forward_only_zone_queries_upstream() -> anyhow::Result<()> { let (upstream_addr, upstream_handle) = start_upstream_server().await?; diff --git a/easytier/src/proto/dns.proto b/easytier/src/proto/dns.proto index fd0f8528..a2dbcff7 100644 --- a/easytier/src/proto/dns.proto +++ b/easytier/src/proto/dns.proto @@ -10,6 +10,7 @@ message ZoneData { uint32 ttl = 3; repeated string records = 4; repeated common.Url forwarders = 5; + bool fallthrough = 6; } message GetExportConfigRequest {}