From 8bc70d59692382cab35258bb0641b3b6e9a8c6ad Mon Sep 17 00:00:00 2001 From: metah3m Date: Mon, 10 Aug 2026 18:20:06 +0800 Subject: [PATCH 1/5] fix(dns): share mdns resources across endpoints --- dhttp/src/ddns.rs | 315 ++++++++++++++++++++++++++++++------------ dhttp/src/endpoint.rs | 29 ++-- dhttp/src/network.rs | 23 +++ 3 files changed, 260 insertions(+), 107 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index e4b53b7..e665a04 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -112,9 +112,13 @@ impl DhttpDnsPlan { type DeferredEndpointResolver = resolvers::deferred::DeferredResolver; type EndpointH3Client = Arc>; +/// Routes DHTTP endpoint names and external authorities to separate scopes. #[derive(Debug)] struct DhttpDnsRouter { + /// Contains only DHTTP-aware and explicitly supplied resolvers. dhttp: ArcResolvers, + + /// Contains system, scoped mDNS, and explicitly supplied resolvers. external: ArcResolvers, } @@ -126,12 +130,13 @@ impl fmt::Display for DhttpDnsRouter { impl Resolve for DhttpDnsRouter { fn lookup<'a>(&'a self, name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { - let resolvers = if uses_h3_dns(name) { - &self.dhttp - } else { - &self.external - }; - Resolve::lookup(resolvers.as_ref(), name) + if is_dhttp_authority(name) { + return Resolve::lookup(self.dhttp.as_ref(), name); + } + + let external = self.external.clone(); + let authority = external_authority(name); + Box::pin(async move { Resolve::lookup(external.as_ref(), &authority).await }) } } @@ -213,11 +218,23 @@ where let deferred_stun_resolver = Arc::new(DeferredStunResolver::new()); let stun_resolver: ArcResolver = deferred_stun_resolver.clone(); let network = builder(stun_resolver); - let final_resolver = - network_stun_resolver_from_plan(dns_plan, network.clone(), bind, h3_dns_server).await?; + let mdns_driver = Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)); + let final_resolver = network_stun_resolver_from_plan( + dns_plan, + network.clone(), + bind, + h3_dns_server, + mdns_driver.clone(), + ) + .await?; - DhttpNetwork::from_deferred_stun_resolver(network, deferred_stun_resolver, final_resolver) - .context(build_dhttp_network_with_dns_error::DeferredStunResolverSnafu) + DhttpNetwork::from_deferred_stun_resolver( + network, + deferred_stun_resolver, + final_resolver, + mdns_driver, + ) + .context(build_dhttp_network_with_dns_error::DeferredStunResolverSnafu) } #[bon::builder(finish_fn = build)] @@ -225,6 +242,8 @@ pub async fn quic_endpoint_builder_with_dns( #[builder(start_fn)] builder: F, #[builder(start_fn)] dns_plan: &DhttpDnsPlan, #[builder(default = Arc::::from(resolvers::DHTTP_H3_DNS_SERVER))] h3_dns_server: Arc, + #[builder(default = Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)))] + mdns_driver: Arc, ) -> Result<(QuicEndpoint, publishers::Publishers), BuildQuicEndpointWithDnsError> where F: FnOnce(ArcResolver) -> Fut, @@ -234,7 +253,7 @@ where let endpoint_resolver: ArcResolver = deferred_endpoint_resolver.clone(); let endpoint = builder(endpoint_resolver).await; let (final_resolver, publishers) = - endpoint_dns_from_quic(dns_plan, &endpoint, h3_dns_server).await?; + endpoint_dns_from_quic(dns_plan, &endpoint, h3_dns_server, mdns_driver).await?; deferred_endpoint_resolver .set(final_resolver) @@ -248,33 +267,50 @@ async fn network_stun_resolver_from_plan( network: Arc, bind: Arc>, h3_dns_server: Arc, + mdns_driver: Arc, ) -> Result { let operations = dns_plan.effective_ops(); - let (h3_resolver, external_resolvers) = if uses_h3(&operations) { - let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()).await?; + let shared_mdns = if uses_mdns(&operations) { + Some(Arc::new( + mdns::MdnsResolvers::bind_with_driver( + network.clone(), + bind.clone(), + mdns_driver.clone(), + ) + .await, + )) + } else { + None + }; + let h3_resolver = if uses_h3(&operations) { + let h3_underlay = network_h3_underlay( + &operations, + network.clone(), + bind.clone(), + mdns_driver.clone(), + ) + .await?; let h3_quic = dedicated_network_h3_client_quic(network.clone(), bind.clone(), h3_underlay.clone()) .await; - ( - Some(Arc::new(h3_resolver_for_network( - h3_dns_server.as_ref(), - h3_quic, - )?)), - Some(h3_underlay), - ) + Some(Arc::new(h3_resolver_for_network( + h3_dns_server.as_ref(), + h3_quic, + )?)) } else { - (None, None) + None }; let mut builder = resolvers::Resolvers::builder(); for operation in &operations { match operation { DhttpDnsOp::Dns(resolvers::DnsScheme::Mdns) => { - builder = builder.mdns(network.clone(), bind.clone()).await; - } - DhttpDnsOp::Dns(resolvers::DnsScheme::System) => { - builder = builder.system(); + let mdns = shared_mdns + .clone() + .expect("BUG: shared mDNS resolver exists when mDNS is configured"); + builder = builder.candidate_resolver(mdns); } + DhttpDnsOp::Dns(resolvers::DnsScheme::System) => {} DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { builder = builder .http() @@ -293,9 +329,8 @@ async fn network_stun_resolver_from_plan( } let dhttp_resolvers = network_resolver_chain(builder.build())?; - let Some(external_resolvers) = external_resolvers else { - return Ok(dhttp_resolvers); - }; + let external_resolvers = + network_resolver_chain(external_resolvers_from_shared(&operations, shared_mdns))?; let router: ArcResolver = Arc::new(DhttpDnsRouter { dhttp: dhttp_resolvers, @@ -308,10 +343,23 @@ async fn endpoint_dns_from_quic( dns_plan: &DhttpDnsPlan, endpoint: &QuicEndpoint, h3_dns_server: Arc, + mdns_driver: Arc, ) -> Result<(resolvers::Resolvers, publishers::Publishers), BuildQuicEndpointWithDnsError> { let operations = dns_plan.effective_ops(); let endpoint_h3 = if uses_h3(&operations) { - Some(endpoint_h3_clients_from_quic(&operations, endpoint).await?) + Some(endpoint_h3_clients_from_quic(&operations, endpoint, mdns_driver.clone()).await?) + } else { + None + }; + let shared_mdns = if uses_mdns(&operations) { + Some(Arc::new( + mdns::MdnsResolvers::bind_with_driver( + endpoint.network().clone(), + endpoint.bind_patterns().clone(), + mdns_driver.clone(), + ) + .await, + )) } else { None }; @@ -322,23 +370,16 @@ async fn endpoint_dns_from_quic( for operation in &operations { match operation { DhttpDnsOp::Dns(resolvers::DnsScheme::Mdns) => { - let mdns = Arc::new( - mdns::MdnsResolvers::bind( - endpoint.network().clone(), - endpoint.bind_patterns().clone(), - resolvers::DHTTP_MDNS_SERVICE, - ) - .await, - ); + let mdns = shared_mdns + .clone() + .expect("BUG: shared mDNS resolver exists when mDNS is configured"); resolver_builder = resolver_builder.candidate_resolver(mdns.clone()); publishers.push(publishers::Publisher::mdns( mdns, Arc::new(endpoint.clone()), )); } - DhttpDnsOp::Dns(resolvers::DnsScheme::System) => { - resolver_builder = resolver_builder.system(); - } + DhttpDnsOp::Dns(resolvers::DnsScheme::System) => {} DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { let http = Arc::new( resolvers::HttpResolver::new(crate::endpoint::BOOTSTRAP_URL) @@ -374,30 +415,22 @@ async fn endpoint_dns_from_quic( } } - let resolvers = endpoint_resolver_chain(resolver_builder.build())?; - let resolvers = if uses_h3(&operations) { - let external = non_h3_resolvers( - &operations, - endpoint.network().clone(), - endpoint.bind_patterns().clone(), - ) - .await; - let router: ArcResolver = Arc::new(DhttpDnsRouter { - dhttp: Arc::new(resolvers), - external: Arc::new(external), - }); - endpoint_resolver_chain(resolvers::Resolvers::new().with(router))? - } else { - resolvers - }; + let dhttp = endpoint_resolver_chain(resolver_builder.build())?; + let external = external_resolvers_from_shared(&operations, shared_mdns); + let router: ArcResolver = Arc::new(DhttpDnsRouter { + dhttp: Arc::new(dhttp), + external: Arc::new(external), + }); + let resolvers = endpoint_resolver_chain(resolvers::Resolvers::new().with(router))?; Ok((resolvers, publishers)) } async fn endpoint_h3_clients_from_quic( operations: &[DhttpDnsOp], endpoint: &QuicEndpoint, + mdns_driver: Arc, ) -> Result { - let h3_underlay = endpoint_h3_underlay(operations, endpoint).await?; + let h3_underlay = endpoint_h3_underlay(operations, endpoint, mdns_driver).await?; let resolver_quic = dedicated_h3_client_quic(endpoint, h3_underlay.clone()).await; let publisher_quic = dedicated_h3_client_quic(endpoint, h3_underlay).await; @@ -445,11 +478,13 @@ async fn dedicated_network_h3_client_quic( async fn endpoint_h3_underlay( operations: &[DhttpDnsOp], endpoint: &QuicEndpoint, + mdns_driver: Arc, ) -> Result { - let resolvers = non_h3_resolvers( + let resolvers = external_resolvers( operations, endpoint.network().clone(), endpoint.bind_patterns().clone(), + mdns_driver, ) .await; @@ -460,43 +495,57 @@ async fn network_h3_underlay( operations: &[DhttpDnsOp], network: Arc, bind: Arc>, + mdns_driver: Arc, ) -> Result { - let resolvers = non_h3_resolvers(operations, network, bind).await; + let resolvers = external_resolvers(operations, network, bind, mdns_driver).await; network_resolver_chain(resolvers) } -async fn non_h3_resolvers( +/// Build the external resolver scope used by bootstrap and normal authorities. +async fn external_resolvers( operations: &[DhttpDnsOp], network: Arc, bind: Arc>, + mdns_driver: Arc, ) -> resolvers::Resolvers { - let mut builder = resolvers::Resolvers::builder(); + let shared_mdns = if uses_mdns(operations) { + Some(Arc::new( + mdns::MdnsResolvers::bind_with_driver(network, bind, mdns_driver).await, + )) + } else { + None + }; + external_resolvers_from_shared(operations, shared_mdns) +} + +/// Build an external resolver scope around an already-shared mDNS view. +fn external_resolvers_from_shared( + operations: &[DhttpDnsOp], + shared_mdns: Option>, +) -> resolvers::Resolvers { + let mut builder = resolvers::Resolvers::builder().system(); for operation in operations { match operation { DhttpDnsOp::Dns(resolvers::DnsScheme::Mdns) => { - builder = builder.mdns(network.clone(), bind.clone()).await; - } - DhttpDnsOp::Dns(resolvers::DnsScheme::System) => { - builder = builder.system(); - } - DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { - builder = builder - .http() - .expect("BUG: DHTTP HTTP DNS server is a valid URL"); + let mdns = shared_mdns + .clone() + .expect("BUG: shared mDNS resolver exists when mDNS is configured"); + builder = builder.candidate_resolver(mdns); } - DhttpDnsOp::Dns(resolvers::DnsScheme::H3) | DhttpDnsOp::Publisher(_) => {} + DhttpDnsOp::Dns( + resolvers::DnsScheme::System + | resolvers::DnsScheme::Http + | resolvers::DnsScheme::H3, + ) + | DhttpDnsOp::Publisher(_) => {} DhttpDnsOp::Resolver(resolver) => { builder = builder.resolver(resolver.clone()); } } } - if uses_h3(operations) && !has_custom_resolver(operations) && !has_system_dns(operations) { - builder = builder.system(); - } - builder.build() } @@ -553,7 +602,15 @@ fn uses_h3(operations: &[DhttpDnsOp]) -> bool { .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::H3))) } -pub(crate) fn uses_h3_dns(name: &str) -> bool { +/// Return whether the plan requires an mDNS resolver view. +fn uses_mdns(operations: &[DhttpDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::Mdns))) +} + +/// Classify a validated authority by host without interpreting its port as a sequence. +fn is_dhttp_authority(name: &str) -> bool { let host = match name.rsplit_once(':') { Some((host, digits)) if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) => @@ -580,16 +637,22 @@ pub(crate) fn uses_h3_dns(name: &str) -> bool { && host.as_bytes()[suffix_start..].eq_ignore_ascii_case(DHTTP_DNS_SUFFIX.as_bytes()) } -fn has_custom_resolver(operations: &[DhttpDnsOp]) -> bool { - operations - .iter() - .any(|operation| matches!(operation, DhttpDnsOp::Resolver(_))) +/// Add port 443 to an external authority only when it has no explicit port. +fn external_authority(name: &str) -> String { + let Ok(authority) = name.parse::<::http::uri::Authority>() else { + return name.to_owned(); + }; + if name.rsplit_once(':').is_some_and(|(host, digits)| { + !host.is_empty() && !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) + }) { + return name.to_owned(); + } + + format!("{authority}:443") } -fn has_system_dns(operations: &[DhttpDnsOp]) -> bool { - operations - .iter() - .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::System))) +pub(crate) fn uses_h3_dns(name: &str) -> bool { + is_dhttp_authority(name) } #[cfg(test)] @@ -598,7 +661,7 @@ mod tests { use std::{ fmt, sync::{ - Arc, + Arc, Mutex, atomic::{AtomicUsize, Ordering}, }, }; @@ -626,6 +689,27 @@ mod tests { } } + #[derive(Debug, Default)] + struct RecordingResolver { + names: Mutex>, + } + + impl fmt::Display for RecordingResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("recording resolver") + } + } + + impl Resolve for RecordingResolver { + fn lookup<'a>(&'a self, name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + self.names + .lock() + .expect("resolver names lock poisoned") + .push(name.to_owned()); + async move { Ok(stream::empty().boxed()) }.boxed() + } + } + #[derive(Debug)] struct CountingPublisher { calls: Arc, @@ -701,6 +785,44 @@ mod tests { assert_eq!(external_calls.load(Ordering::SeqCst), 1); } + #[tokio::test] + async fn router_preserves_dhttp_authorities_and_defaults_external_ports() { + let dhttp = Arc::new(RecordingResolver::default()); + let external = Arc::new(RecordingResolver::default()); + let router = DhttpDnsRouter { + dhttp: Arc::new(resolvers::Resolvers::new().with(dhttp.clone())), + external: Arc::new(resolvers::Resolvers::new().with(external.clone())), + }; + + for name in ["node.dhttp.net", "node.dhttp.net:2"] { + let _records = router.lookup(name).await.expect("DHTTP lookup succeeds"); + } + for name in [ + "nat.genmeta.net", + "nat.genmeta.net:20004", + "nat.genmeta.net:65536", + "printer.local", + "[::1]", + ] { + let _records = router.lookup(name).await.expect("external lookup succeeds"); + } + + assert_eq!( + *dhttp.names.lock().expect("resolver names lock poisoned"), + ["node.dhttp.net", "node.dhttp.net:2"] + ); + assert_eq!( + *external.names.lock().expect("resolver names lock poisoned"), + [ + "nat.genmeta.net:443", + "nat.genmeta.net:20004", + "nat.genmeta.net:65536", + "printer.local:443", + "[::1]:443", + ] + ); + } + #[test] fn dhttp_dns_plan_defaults_only_when_empty() { let empty = DhttpDnsPlan::new(); @@ -793,7 +915,7 @@ mod tests { assert_eq!( endpoint.resolver().to_string(), - "DeferredResolver(Resolvers(counting resolver))" + "DeferredResolver(Resolvers(DHTTP DNS Router))" ); assert!(publishers.iter().next().is_none()); } @@ -857,9 +979,13 @@ mod tests { let source_client = (*source_quic.client_config_mut()).clone(); let source_server = (*source_quic.server_config_mut()).clone(); - let clients = endpoint_h3_clients_from_quic(&operations, &endpoint) - .await - .expect("h3 dns clients should build"); + let clients = endpoint_h3_clients_from_quic( + &operations, + &endpoint, + Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)), + ) + .await + .expect("h3 dns clients should build"); assert!( !Arc::ptr_eq(&clients.resolver, &clients.publisher), @@ -913,9 +1039,14 @@ mod tests { .expect("wildcard bind pattern should parse"), ]); let operations = vec![DhttpDnsOp::Dns(resolvers::DnsScheme::H3)]; - let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()) - .await - .expect("network h3 underlay should build"); + let h3_underlay = network_h3_underlay( + &operations, + network.clone(), + bind.clone(), + Arc::new(mdns::MdnsBindDriver::new(resolvers::DHTTP_MDNS_SERVICE)), + ) + .await + .expect("network h3 underlay should build"); let mut quic = dedicated_network_h3_client_quic(network, bind, h3_underlay).await; diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 6d179a5..029a94e 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -193,6 +193,7 @@ impl Endpoint { }, &dns_plan, ) + .mdns_driver(network.mdns_driver()) .build() .await .context(build_endpoint_error::EndpointDnsSnafu)?; @@ -1229,7 +1230,7 @@ mod tests { } #[tokio::test] - async fn endpoint_with_custom_resolver_only_uses_custom_resolver_chain() { + async fn endpoint_with_custom_resolver_only_uses_routed_resolver_chain() { let resolver: Arc = Arc::new(MarkerResolver); let endpoint = Endpoint::builder() .resolver(resolver) @@ -1238,7 +1239,7 @@ mod tests { .expect("custom resolver endpoint is valid"); assert!(endpoint.dns_publishers().iter().next().is_none()); - assert_eq!(endpoint_resolver_names(&endpoint), vec!["marker resolver"]); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["DHTTP DNS Router"]); } #[tokio::test] @@ -1262,23 +1263,24 @@ mod tests { } #[tokio::test] - async fn endpoint_with_system_dns_and_custom_publisher_builds_both_sides() { + async fn endpoint_with_system_dns_and_custom_publisher_rejects_empty_dhttp_scope() { let publisher: Arc = Arc::new(CountingPublisher { calls: Arc::new(AtomicUsize::new(0)), }); - let endpoint = Endpoint::builder() + let Err(error) = Endpoint::builder() .dns(DnsScheme::System) .publisher(crate::ddns::publishers::PublishScope::WideArea, publisher) .build() .await - .expect("system plus custom publisher endpoint should build"); + else { + panic!("system-only DNS should not populate the DHTTP resolver scope"); + }; - assert_eq!( - endpoint_resolver_names(&endpoint), - vec!["System DNS Resolver"] - ); - assert_eq!(endpoint.dns_publishers().iter().count(), 1); + assert!(matches!( + error, + BuildEndpointError::EndpointDns { .. } | BuildEndpointError::StunDns { .. } + )); } #[tokio::test] @@ -1292,7 +1294,7 @@ mod tests { .await .unwrap(); - assert_eq!(endpoint_resolver_names(&endpoint), vec!["marker resolver"]); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["DHTTP DNS Router"]); } #[tokio::test] @@ -1345,10 +1347,7 @@ mod tests { &raw_network.quic().stun_resolver(), &external_resolver )); - assert_eq!( - endpoint_resolver_names(&endpoint), - vec!["counting resolver"] - ); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["DHTTP DNS Router"]); } #[tokio::test] diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 1d5eb05..0caa081 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -3,6 +3,7 @@ use std::{ops::Deref, sync::Arc}; use crate::ddns::{ ArcPublisher, ArcResolver, BuildDhttpNetworkWithDnsError, DhttpDnsPlan, dhttp_network_builder_with_dns, + mdns::MdnsBindDriver, publishers::PublishScope, resolvers::{DnsScheme, Resolvers, deferred::DeferredResolver, weak::WeakResolver}, }; @@ -17,10 +18,19 @@ use crate::dquic::{ pub(crate) type ArcResolvers = Arc; pub(crate) type DeferredStunResolver = DeferredResolver>; +/// Owns network resources shared by DHTTP endpoints. #[derive(Clone)] pub struct DhttpNetwork { + /// Underlying network used by QUIC, STUN, and interface reconciliation. network: Arc, + + /// Shared backend that gives every endpoint the same mDNS binding identity. + mdns_driver: Arc, + + /// Placeholder retained while the network references its deferred resolver. _deferred_stun_resolver: Option>, + + /// Keeps the final deferred STUN resolver target alive. _stun_resolver: Option, } @@ -30,15 +40,22 @@ impl DhttpNetwork { &self.network } + /// Return the mDNS driver shared by all views of this DHTTP network. + pub(crate) fn mdns_driver(&self) -> Arc { + self.mdns_driver.clone() + } + pub(crate) fn from_deferred_stun_resolver( network: Arc, deferred_stun_resolver: Arc, stun_resolver: ArcResolvers, + mdns_driver: Arc, ) -> Result { deferred_stun_resolver.set(WeakResolver::new(Arc::downgrade(&stun_resolver)))?; let keepalive: ArcResolver = stun_resolver; Ok(Self { network, + mdns_driver, _deferred_stun_resolver: Some(deferred_stun_resolver), _stun_resolver: Some(keepalive), }) @@ -63,6 +80,9 @@ impl From> for DhttpNetwork { fn from(network: Arc) -> Self { Self { network, + mdns_driver: Arc::new(MdnsBindDriver::new( + crate::ddns::resolvers::DHTTP_MDNS_SERVICE, + )), _deferred_stun_resolver: None, _stun_resolver: None, } @@ -123,6 +143,9 @@ impl DhttpNetwork { .build(); return Ok(Self { network, + mdns_driver: Arc::new(MdnsBindDriver::new( + crate::ddns::resolvers::DHTTP_MDNS_SERVICE, + )), _deferred_stun_resolver: None, _stun_resolver: Some(stun_resolver), }); From 59691df0bc7f07c76e86cb29014115a30be8089d Mon Sep 17 00:00:00 2001 From: metah3m Date: Mon, 10 Aug 2026 19:08:13 +0800 Subject: [PATCH 2/5] fix(network): expose shared mdns driver --- dhttp/src/network.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 0caa081..7b4358f 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -41,7 +41,7 @@ impl DhttpNetwork { } /// Return the mDNS driver shared by all views of this DHTTP network. - pub(crate) fn mdns_driver(&self) -> Arc { + pub fn mdns_driver(&self) -> Arc { self.mdns_driver.clone() } From 0027c3f537f5a4092f18a4246ae0c75559084e1a Mon Sep 17 00:00:00 2001 From: metah3m Date: Mon, 10 Aug 2026 20:17:08 +0800 Subject: [PATCH 3/5] refactor(endpoint): encapsulate shared dns resources --- dhttp/src/endpoint.rs | 30 ++++++++++++++++++++++++++++++ dhttp/src/network.rs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 029a94e..daeef85 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -153,6 +153,8 @@ impl Endpoint { identity: Option>, network: Option, + #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_H3_DNS_SERVER))] + h3_dns_server: Arc, #[builder(default = crate::trust::default_client_quic_config())] client: ClientQuicConfig, #[builder(default = crate::trust::default_server_quic_config())] server: ServerQuicConfig, #[builder(default = Arc::new(Vec::new()))] bind: Arc>, @@ -167,6 +169,7 @@ impl Endpoint { None => DhttpNetwork::builder() .dns_plan(dns_plan.clone()) .bind(bind.clone()) + .h3_dns_server(h3_dns_server.clone()) .build() .await .context(build_endpoint_error::StunDnsSnafu)?, @@ -193,6 +196,7 @@ impl Endpoint { }, &dns_plan, ) + .h3_dns_server(h3_dns_server) .mdns_driver(network.mdns_driver()) .build() .await @@ -1152,6 +1156,32 @@ mod tests { assert!(endpoint.dns_publishers().iter().next().is_some()); } + #[tokio::test] + async fn endpoint_builder_applies_custom_h3_dns_server() { + let network = DhttpNetwork::builder() + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("network should build"); + + let Err(error) = Endpoint::builder() + .network(network) + .dns(DnsScheme::H3) + .h3_dns_server(Arc::::from("://invalid")) + .build() + .await + else { + panic!("custom H3 DNS server must be validated by endpoint DNS"); + }; + + assert!(matches!( + error, + BuildEndpointError::EndpointDns { + source: crate::ddns::BuildQuicEndpointWithDnsError::InvalidH3DnsServer { .. } + } + )); + } + #[tokio::test] async fn endpoint_with_custom_resolver_only_has_no_dns_publishers() { let resolver: Arc = Arc::new(MarkerResolver); diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 7b4358f..0caa081 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -41,7 +41,7 @@ impl DhttpNetwork { } /// Return the mDNS driver shared by all views of this DHTTP network. - pub fn mdns_driver(&self) -> Arc { + pub(crate) fn mdns_driver(&self) -> Arc { self.mdns_driver.clone() } From b5144f655208934337834550bb1159436e11f5e7 Mon Sep 17 00:00:00 2001 From: metah3m Date: Tue, 11 Aug 2026 00:11:58 +0800 Subject: [PATCH 4/5] fix(resolve): forward latest lookup contract --- dhttp/src/ddns.rs | 110 +++++++++++++++++++++++++----------------- dhttp/src/endpoint.rs | 16 ++++-- dhttp/src/network.rs | 9 +++- 3 files changed, 87 insertions(+), 48 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index e665a04..67e7169 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -129,14 +129,17 @@ impl fmt::Display for DhttpDnsRouter { } impl Resolve for DhttpDnsRouter { - fn lookup<'a>(&'a self, name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { - if is_dhttp_authority(name) { - return Resolve::lookup(self.dhttp.as_ref(), name); + fn lookup<'a>( + &'a self, + hostname: &'a str, + servname: &'a str, + family: Option, + ) -> crate::dquic::resolver::ResolveFuture<'a> { + if is_dhttp_authority(hostname) { + return Resolve::lookup(self.dhttp.as_ref(), hostname, servname, family); } - let external = self.external.clone(); - let authority = external_authority(name); - Box::pin(async move { Resolve::lookup(external.as_ref(), &authority).await }) + Resolve::lookup(self.external.as_ref(), hostname, servname, family) } } @@ -638,19 +641,6 @@ fn is_dhttp_authority(name: &str) -> bool { } /// Add port 443 to an external authority only when it has no explicit port. -fn external_authority(name: &str) -> String { - let Ok(authority) = name.parse::<::http::uri::Authority>() else { - return name.to_owned(); - }; - if name.rsplit_once(':').is_some_and(|(host, digits)| { - !host.is_empty() && !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) - }) { - return name.to_owned(); - } - - format!("{authority}:443") -} - pub(crate) fn uses_h3_dns(name: &str) -> bool { is_dhttp_authority(name) } @@ -683,7 +673,12 @@ mod tests { } impl Resolve for CountingResolver { - fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + fn lookup<'a>( + &'a self, + _hostname: &'a str, + _servname: &'a str, + _family: Option, + ) -> crate::dquic::resolver::ResolveFuture<'a> { self.calls.fetch_add(1, Ordering::SeqCst); async move { Ok(stream::empty().boxed()) }.boxed() } @@ -691,7 +686,7 @@ mod tests { #[derive(Debug, Default)] struct RecordingResolver { - names: Mutex>, + lookups: Mutex)>>, } impl fmt::Display for RecordingResolver { @@ -701,11 +696,16 @@ mod tests { } impl Resolve for RecordingResolver { - fn lookup<'a>(&'a self, name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { - self.names + fn lookup<'a>( + &'a self, + hostname: &'a str, + servname: &'a str, + family: Option, + ) -> crate::dquic::resolver::ResolveFuture<'a> { + self.lookups .lock() .expect("resolver names lock poisoned") - .push(name.to_owned()); + .push((hostname.to_owned(), servname.to_owned(), family)); async move { Ok(stream::empty().boxed()) }.boxed() } } @@ -771,14 +771,18 @@ mod tests { let router = DhttpDnsRouter { dhttp, external }; let _dhttp_records = router - .lookup("node.dhttp.net") + .lookup("node.dhttp.net", "", None) .await .expect("dhttp STUN name should use dhttp resolvers"); assert_eq!(dhttp_calls.load(Ordering::SeqCst), 1); assert_eq!(external_calls.load(Ordering::SeqCst), 0); let _external_records = router - .lookup("nat.genmeta.net:20004") + .lookup( + "nat.genmeta.net", + "20004", + Some(crate::dquic::qresolve::Family::V4), + ) .await .expect("external STUN name should use external resolvers"); assert_eq!(dhttp_calls.load(Ordering::SeqCst), 1); @@ -786,7 +790,7 @@ mod tests { } #[tokio::test] - async fn router_preserves_dhttp_authorities_and_defaults_external_ports() { + async fn router_forwards_all_lookup_parameters_without_reinterpreting_service() { let dhttp = Arc::new(RecordingResolver::default()); let external = Arc::new(RecordingResolver::default()); let router = DhttpDnsRouter { @@ -795,30 +799,50 @@ mod tests { }; for name in ["node.dhttp.net", "node.dhttp.net:2"] { - let _records = router.lookup(name).await.expect("DHTTP lookup succeeds"); + let _records = router + .lookup(name, "ignored", Some(crate::dquic::qresolve::Family::V6)) + .await + .expect("DHTTP lookup succeeds"); } - for name in [ - "nat.genmeta.net", - "nat.genmeta.net:20004", - "nat.genmeta.net:65536", - "printer.local", - "[::1]", + for (hostname, servname) in [ + ("nat.genmeta.net", ""), + ("nat.genmeta.net", "20004"), + ("nat.genmeta.net", "65536"), + ("printer.local", ""), + ("[::1]", ""), ] { - let _records = router.lookup(name).await.expect("external lookup succeeds"); + let _records = router + .lookup(hostname, servname, None) + .await + .expect("external lookup succeeds"); } assert_eq!( - *dhttp.names.lock().expect("resolver names lock poisoned"), - ["node.dhttp.net", "node.dhttp.net:2"] + *dhttp.lookups.lock().expect("resolver names lock poisoned"), + [ + ( + "node.dhttp.net".to_owned(), + "ignored".to_owned(), + Some(crate::dquic::qresolve::Family::V6) + ), + ( + "node.dhttp.net:2".to_owned(), + "ignored".to_owned(), + Some(crate::dquic::qresolve::Family::V6) + ), + ] ); assert_eq!( - *external.names.lock().expect("resolver names lock poisoned"), + *external + .lookups + .lock() + .expect("resolver names lock poisoned"), [ - "nat.genmeta.net:443", - "nat.genmeta.net:20004", - "nat.genmeta.net:65536", - "printer.local:443", - "[::1]:443", + ("nat.genmeta.net".to_owned(), "".to_owned(), None), + ("nat.genmeta.net".to_owned(), "20004".to_owned(), None), + ("nat.genmeta.net".to_owned(), "65536".to_owned(), None), + ("printer.local".to_owned(), "".to_owned(), None), + ("[::1]".to_owned(), "".to_owned(), None), ] ); } diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index daeef85..9cf711d 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -1140,7 +1140,12 @@ mod tests { } impl crate::dquic::qresolve::Resolve for MarkerResolver { - fn lookup<'l>(&'l self, _name: &'l str) -> crate::dquic::qresolve::ResolveFuture<'l> { + fn lookup<'l>( + &'l self, + _hostname: &'l str, + _servname: &'l str, + _family: Option, + ) -> crate::dquic::qresolve::ResolveFuture<'l> { use futures::{FutureExt, StreamExt, stream}; async { Ok(stream::empty().boxed()) }.boxed() } @@ -1206,7 +1211,12 @@ mod tests { } impl crate::dquic::qresolve::Resolve for CountingResolver { - fn lookup<'l>(&'l self, _name: &'l str) -> crate::dquic::qresolve::ResolveFuture<'l> { + fn lookup<'l>( + &'l self, + _hostname: &'l str, + _servname: &'l str, + _family: Option, + ) -> crate::dquic::qresolve::ResolveFuture<'l> { use futures::{FutureExt, StreamExt, stream}; self.calls.fetch_add(1, Ordering::SeqCst); @@ -1344,7 +1354,7 @@ mod tests { .network() .quic() .stun_resolver() - .lookup("stun.example.test") + .lookup("stun.example.test", "", None) .await .expect("custom STUN resolver should be called"); diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 0caa081..851dea9 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -223,7 +223,12 @@ mod tests { } impl Resolve for CountingResolver { - fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + fn lookup<'a>( + &'a self, + _hostname: &'a str, + _servname: &'a str, + _family: Option, + ) -> crate::dquic::resolver::ResolveFuture<'a> { use futures::{StreamExt, stream}; self.calls.fetch_add(1, Ordering::SeqCst); @@ -326,7 +331,7 @@ mod tests { .network() .quic() .stun_resolver() - .lookup("stun.example.test") + .lookup("stun.example.test", "", None) .await .expect("custom resolver should resolve STUN server"); From d6f6bf063306a0b65bdf00b29f074f36b7076e6b Mon Sep 17 00:00:00 2001 From: metah3m Date: Tue, 11 Aug 2026 17:05:59 +0800 Subject: [PATCH 5/5] chore(release): prepare dhttp 0.6.1 --- CHANGELOG.md | 20 ++++++++++++++++++++ Cargo.toml | 12 ++++++------ access/Cargo.toml | 2 +- api/package-lock.json | 4 ++-- api/package.json | 2 +- 5 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..92435da --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## [0.6.1] - 2026-08-11 + +### Fixed + +- Share network-owned mDNS resources across endpoint identities instead of + opening duplicate multicast bindings. +- Forward service names and address-family constraints through the latest DNS + lookup contract. + +### Dependencies + +- Release manifests target `dquic` v0.7.1, `dyns` v0.7.1, and `h3x` v0.6.1. + +### Packages + +- `dhttp` and `dhttp-api` Rust crates, plus `dhttpy`: v0.6.1 +- `dhttp-access`: v0.4.1 +- `@genmeta/dhttp`: v0.6.1 diff --git a/Cargo.toml b/Cargo.toml index b75d698..a111516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["dhttp", "identity", "home", "api", "access", "log"] [workspace.package] -version = "0.6.1-beta.3" +version = "0.6.1" edition = "2024" license = "Apache-2.0" repository = "https://github.com/genmeta/dhttp" @@ -43,8 +43,8 @@ tracing-subscriber = { version = "0.3", default-features = false, features = ["f # identity types with h3x/ddns in the formal release graph. dhttp-identity = "0.3.0" dhttp-home = { path = "home", version = "0.5.0" } -dquic = { version = "0.7.0", default-features = false } -ddns = { package = "dyns", version = "0.7.1-beta.2", features = [ +dquic = { version = "0.7.1", default-features = false } +ddns = { package = "dyns", version = "0.7.1", features = [ "resolvers", "publishers", "h3", @@ -52,9 +52,9 @@ ddns = { package = "dyns", version = "0.7.1-beta.2", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.6.1-beta.2", features = [ +h3x = { version = "0.6.1", features = [ "dquic", ] } -dhttp = { path = "dhttp", version = "0.6.1-beta.3" } -dhttp-access = { path = "access", version = "0.4.1-beta.1" } +dhttp = { path = "dhttp", version = "0.6.1" } +dhttp-access = { path = "access", version = "0.4.1" } dhttp-log = { path = "log", version = "0.1.0" } diff --git a/access/Cargo.toml b/access/Cargo.toml index 5a59348..8f687d6 100644 --- a/access/Cargo.toml +++ b/access/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-access" description = "Identity-aware access control primitives for DHttp" -version = "0.4.1-beta.1" +version = "0.4.1" edition.workspace = true license.workspace = true repository.workspace = true diff --git a/api/package-lock.json b/api/package-lock.json index 42d1ab3..fe10c6b 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genmeta/dhttp", - "version": "0.6.1-beta.3", + "version": "0.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genmeta/dhttp", - "version": "0.6.1-beta.3", + "version": "0.6.1", "devDependencies": { "@napi-rs/cli": "^3.3.5" } diff --git a/api/package.json b/api/package.json index 6b46d40..b4dc789 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@genmeta/dhttp", - "version": "0.6.1-beta.3", + "version": "0.6.1", "description": "The True Internet", "license": "Apache-2.0", "homepage": "https://dhttp.net/",