From e86ffcab428c2b184c378cfcd2b5a29f8f86f6e2 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 16 Jun 2026 20:35:57 +0800 Subject: [PATCH 01/12] refactor: build endpoint dns publishers --- Cargo.toml | 9 +++-- dhttp/src/ddns.rs | 2 +- dhttp/src/endpoint.rs | 85 +++++++++++++++++++++++++++++++------------ dhttp/src/network.rs | 56 ++++++++++++++++++++++++++-- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d495a20..bcc2692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,9 +41,12 @@ chrono = { version = "0.4", features = ["serde"] } dhttp-identity = "0.1.0" dhttp-home = { path = "home", version = "0.1.0" } ddns = { package = "dyns", version = "0.3.0", features = [ - "h3x-resolver", - "mdns-resolver", - "http-resolver", + "resolvers", + "publishers", + "h3", + "http", + "mdns", + "dquic-network", ] } h3x = { version = "0.3.1", features = [ "dquic", diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index 9344718..b427980 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -1,3 +1,3 @@ //! Re-export of the ddns crate APIs used by DHTTP. -pub use ddns::{core, mdns, publisher, resolvers}; +pub use ddns::{core, mdns, publishers, resolvers}; diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index a8cb877..573b19b 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -2,10 +2,10 @@ use std::{path::PathBuf, str::FromStr, sync::Arc}; use bon::bon; use http::uri::Authority; -use snafu::ResultExt; +use snafu::{OptionExt, ResultExt}; use crate::ddns::resolvers::{ - DHTTP_H3_DNS_SERVER, DnsScheme, deferred::DeferredResolver, h3::H3Resolver, + DHTTP_H3_DNS_SERVER, DnsScheme, H3Resolver, deferred::DeferredResolver, }; use crate::dquic::{ Identity, QuicEndpoint, binds::BindPattern, client::ClientQuicConfig, @@ -22,7 +22,7 @@ pub mod client; pub mod server; use self::client::Request; -use crate::network::{ArcResolver, DhttpNetwork, ResolverPlan}; +use crate::network::{ArcEndpointH3Resolver, ArcResolver, DhttpNetwork, ResolverPlan}; /// A DHttp endpoint bound to a QUIC connection. /// @@ -36,6 +36,7 @@ use crate::network::{ArcResolver, DhttpNetwork, ResolverPlan}; pub struct Endpoint { inner: Arc, network: DhttpNetwork, + publishers: crate::ddns::publishers::Publishers, } impl TryFrom> for Endpoint { @@ -44,7 +45,11 @@ impl TryFrom> for Endpoint { fn try_from(inner: Arc) -> Result { Self::validate_identity(inner.quic().identity().as_deref())?; let network = DhttpNetwork::from(inner.quic().network().clone()); - Ok(Self { inner, network }) + Ok(Self { + inner, + network, + publishers: crate::ddns::publishers::Publishers::new(), + }) } } @@ -61,6 +66,13 @@ pub enum InvalidEndpointIdentityError { }, } +#[derive(Debug, snafu::Snafu)] +#[snafu(module(create_endpoint_publication_loop_error))] +pub enum CreateEndpointPublicationLoopError { + #[snafu(display("anonymous endpoint cannot publish dns records"))] + AnonymousEndpoint, +} + /// Default STUN server for NAT traversal. /// /// STUN server resolution uses this authority so the well-known port remains @@ -77,9 +89,7 @@ fn normalize_bind(bind: Arc>) -> Arc> { } } -type DeferredH3Resolver = DeferredResolver>; - -fn deferred_h3_resolver() -> Arc { +fn deferred_h3_resolver() -> ArcEndpointH3Resolver { Arc::new(DeferredResolver::new()) } @@ -138,11 +148,12 @@ impl Endpoint { let resolver_without_h3 = resolver_plan.resolver_without_h3(resolvers_without_h3); let endpoint_h3_deferred = resolver_plan.uses_h3().then(deferred_h3_resolver); - let endpoint_h3_resolver = endpoint_h3_deferred - .as_ref() - .map(|resolver| resolver.clone() as ArcResolver); - let endpoint_resolvers = resolver_plan - .build_resolvers(endpoint_h3_resolver, raw_network.clone(), bind.clone()) + let (endpoint_resolvers, publishers) = resolver_plan + .build_resolvers_and_publishers( + endpoint_h3_deferred.clone(), + raw_network.clone(), + bind.clone(), + ) .await; let quic_resolver = resolver_plan.final_resolver(endpoint_resolvers); @@ -187,6 +198,7 @@ impl Endpoint { Ok(Self { inner: Arc::new(h3), network, + publishers, }) } } @@ -303,30 +315,35 @@ impl Endpoint { self.inner.quic().resolver().clone() } + pub fn dns_publishers(&self) -> &crate::ddns::publishers::Publishers { + &self.publishers + } + /// Return the bind patterns owned by this endpoint. pub fn bind_patterns(&self) -> Arc> { self.inner.quic().bind_patterns().clone() } - fn dns_publication_loop( + pub fn dns_publication_loop( &self, ) -> Result< - crate::ddns::publisher::EndpointPublisherLoop, - crate::ddns::publisher::CreatePublisherError, + crate::ddns::publishers::EndpointPublicationLoop< + crate::ddns::publishers::EndpointBindingAddresses, + >, + CreateEndpointPublicationLoopError, > { let identity = self .identity() - .ok_or(crate::ddns::publisher::CreatePublisherError::AnonymousEndpoint)?; + .context(create_endpoint_publication_loop_error::AnonymousEndpointSnafu)?; let name = identity.name().to_owned(); - let identity: Arc = identity; - let signer = crate::ddns::publisher::EndpointRecordSigner::new(identity); - let publisher = crate::ddns::publisher::Publisher::new(signer, self.resolver()); - let source = crate::ddns::publisher::EndpointBindingAddresses::new( + let source = crate::ddns::publishers::EndpointBindingAddresses::new( self.network().network().clone(), self.bind_patterns(), ); - Ok(crate::ddns::publisher::EndpointPublicationLoop::new( - name, publisher, source, + Ok(crate::ddns::publishers::EndpointPublicationLoop::new( + name, + self.publishers.clone(), + source, )) } @@ -492,7 +509,7 @@ impl Endpoint { async move { let publisher_loop = match publisher_loop { Ok(publisher_loop) => publisher_loop, - Err(crate::ddns::publisher::CreatePublisherError::AnonymousEndpoint) => { + Err(CreateEndpointPublicationLoopError::AnonymousEndpoint) => { return Err(crate::h3x::dquic::AcceptError::ServerUnavailable); } }; @@ -727,6 +744,28 @@ mod tests { } } + #[tokio::test] + async fn endpoint_default_builds_dns_publishers() { + let endpoint = Endpoint::builder() + .build() + .await + .expect("anonymous endpoint is valid"); + + assert!(endpoint.dns_publishers().iter().next().is_some()); + } + + #[tokio::test] + async fn endpoint_with_custom_resolver_only_has_no_dns_publishers() { + let resolver: Arc = Arc::new(MarkerResolver); + let endpoint = Endpoint::builder() + .resolver(resolver) + .build() + .await + .expect("anonymous endpoint is valid"); + + assert!(endpoint.dns_publishers().iter().next().is_none()); + } + #[derive(Debug)] struct CountingResolver { calls: Arc, diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 44bac8b..b62e291 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -1,10 +1,15 @@ use std::{ops::Deref, sync::Arc}; -use crate::ddns::resolvers::{ - DnsScheme, Resolvers, deferred::DeferredResolver, weak::WeakResolver, +use crate::ddns::{ + mdns::MdnsResolvers, + publishers::{PublishScope, Publisher, Publishers}, + resolvers::{ + DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsScheme, H3Resolver, HttpResolver, Resolvers, + deferred::DeferredResolver, weak::WeakResolver, + }, }; use crate::dquic::{ - Network, + Network, QuicEndpoint, binds::BindPattern, net::{Devices, InterfaceManager, Locations, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY}, resolver::{Resolve, handy::SystemResolver}, @@ -13,6 +18,8 @@ use crate::dquic::{ pub(crate) type DynResolver = dyn Resolve + Send + Sync; pub(crate) type ArcResolver = Arc; pub(crate) type DeferredStunResolver = DeferredResolver>; +pub(crate) type DeferredEndpointH3Resolver = DeferredResolver>; +pub(crate) type ArcEndpointH3Resolver = Arc; #[derive(Clone)] pub(crate) struct ResolverPlan { @@ -77,6 +84,49 @@ impl ResolverPlan { builder.build() } + pub(crate) async fn build_resolvers_and_publishers( + &self, + h3_resolver: Option, + network: Arc, + bind: Arc>, + ) -> (Resolvers, Publishers) { + let mut resolver_builder = Resolvers::builder(); + let mut publishers = Publishers::new(); + + if self.schemes.contains(&DnsScheme::Mdns) { + let mdns = + Arc::new(MdnsResolvers::bind(network.clone(), bind, DHTTP_MDNS_SERVICE).await); + resolver_builder = resolver_builder.resolver(mdns.clone()); + publishers.push(Publisher::mdns(mdns)); + } + + if self.schemes.contains(&DnsScheme::System) { + resolver_builder = resolver_builder.system(); + } + + if self.schemes.contains(&DnsScheme::Http) { + let http = Arc::new( + HttpResolver::new(DHTTP_HTTP_DNS_SERVER) + .expect("BUG: DHTTP HTTP DNS server is a valid URL"), + ); + resolver_builder = resolver_builder.resolver(http.clone()); + publishers.push(Publisher::http(http)); + } + + if self.uses_h3() + && let Some(h3_resolver) = h3_resolver + { + resolver_builder = resolver_builder.resolver(h3_resolver.clone()); + publishers.push(Publisher::new(PublishScope::WideArea, h3_resolver)); + } + + if let Some(custom) = self.custom.clone() { + resolver_builder = resolver_builder.resolver(custom); + } + + (resolver_builder.build(), publishers) + } + pub(crate) fn select_resolver(&self, resolvers: Resolvers) -> ArcResolver { if self.schemes.is_empty() && let Some(custom) = self.custom.clone() From 4aa48a050bcf92b316760cd072fb1f8ce2a6d8f6 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 01:08:00 +0800 Subject: [PATCH 02/12] release: prepare v0.2.0 --- Cargo.toml | 14 +++++++------- README.md | 4 ++-- api/package-lock.json | 6 +++--- api/package.json | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bcc2692..f18dfd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "3" members = ["dhttp", "identity", "home", "api", "access"] [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "Apache-2.0" repository = "https://github.com/genmeta/dhttp" @@ -38,9 +38,9 @@ chrono = { version = "0.4", features = ["serde"] } # Keep same-repository workspace members as path dependencies. Cross-repo # release dependencies resolve through crates.io in the formal release graph. -dhttp-identity = "0.1.0" -dhttp-home = { path = "home", version = "0.1.0" } -ddns = { package = "dyns", version = "0.3.0", features = [ +dhttp-identity = "0.2.0" +dhttp-home = { path = "home", version = "0.2.0" } +ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "dev/v0.4.0", version = "0.4.0", features = [ "resolvers", "publishers", "h3", @@ -48,8 +48,8 @@ ddns = { package = "dyns", version = "0.3.0", features = [ "mdns", "dquic-network", ] } -h3x = { version = "0.3.1", features = [ +h3x = { git = "https://github.com/genmeta/h3x.git", branch = "dev/v0.4.0", version = "0.4.0", features = [ "dquic", ] } -dhttp = { path = "dhttp", version = "0.1.0" } -dhttp-access = { path = "access", version = "0.1.0" } +dhttp = { path = "dhttp", version = "0.2.0" } +dhttp-access = { path = "access", version = "0.2.0" } diff --git a/README.md b/README.md index 5be1799..ce134a6 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ Lower-level crates can be used independently, but application code should normal ### Add the Rust SDK -Until the crates are published through the registry flow you use, depend on this repository directly: +Add the published crate to your Cargo manifest: ```toml [dependencies] -dhttp = { git = "https://github.com/genmeta/dhttp.git", version = "0.1.0" } +dhttp = "0.2.0" ``` ### Build an endpoint diff --git a/api/package-lock.json b/api/package-lock.json index 634d466..285f365 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genmeta/dhttp", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genmeta/dhttp", - "version": "0.1.0", + "version": "0.2.0", "devDependencies": { "@napi-rs/cli": "^3.3.5" } @@ -1872,4 +1872,4 @@ "license": "ISC" } } -} \ No newline at end of file +} diff --git a/api/package.json b/api/package.json index c5950a7..ca72753 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@genmeta/dhttp", - "version": "0.1.0", + "version": "0.2.0", "description": "The True Internet", "license": "Apache-2.0", "homepage": "https://dhttp.net/", From e8ae14d857e84c43e51b29aeacdb97e744bb9d5e Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:23:18 +0800 Subject: [PATCH 03/12] test: specify endpoint dns plan semantics --- dhttp/src/network.rs | 213 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index b62e291..2a6dcdf 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -294,6 +294,9 @@ impl DhttpNetwork { #[cfg(test)] mod tests { use super::*; + use crate::ddns::publishers::PublishScope; + use crate::dquic::resolver::{Publish, PublishFuture}; + use futures::FutureExt; use std::sync::atomic::{AtomicUsize, Ordering}; #[derive(Debug)] @@ -316,6 +319,216 @@ mod tests { } } + #[derive(Debug)] + struct CountingPublisher { + calls: Arc, + } + + impl std::fmt::Display for CountingPublisher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("counting publisher") + } + } + + impl Publish for CountingPublisher { + fn publish<'a>(&'a self, _name: &'a str, _packet: &'a [u8]) -> PublishFuture<'a> { + self.calls.fetch_add(1, Ordering::SeqCst); + async move { Ok(()) }.boxed() + } + } + + #[derive(Debug)] + struct NamedResolver(&'static str); + + impl std::fmt::Display for NamedResolver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0) + } + } + + impl Resolve for NamedResolver { + fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + use futures::{StreamExt, stream}; + + async move { Ok(stream::empty().boxed()) }.boxed() + } + } + + fn resolver_names(resolver: &ArcResolver) -> Vec { + let any: &dyn std::any::Any = resolver.as_ref(); + let resolvers = any + .downcast_ref::() + .expect("resolver should be a resolver chain"); + resolvers + .iter() + .map(|resolver| resolver.to_string()) + .collect() + } + + fn arc_resolvers_names(resolvers: &ArcResolvers) -> Vec { + resolvers + .iter() + .map(|resolver| resolver.to_string()) + .collect() + } + + fn publisher_names(publishers: &Publishers) -> Vec { + publishers + .iter() + .map(|publisher| publisher.to_string()) + .collect() + } + + #[tokio::test] + async fn endpoint_dns_plan_defaults_when_no_ops() { + let network = Network::builder().build(); + let plan = EndpointDnsPlan::new(); + let built = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect("default plan should build"); + + let names = resolver_names(&built.endpoint_resolver); + assert!( + names + .iter() + .any(|name| name.starts_with("DeferredResolver(")) + ); + assert!(names.iter().any(|name| name == "mDNS resolvers")); + assert!(names.iter().any(|name| name == "System DNS Resolver")); + + let publisher_names = publisher_names(&built.endpoint_publishers); + assert!( + publisher_names + .iter() + .any(|name| name.starts_with("DeferredResolver(")) + ); + assert!(publisher_names.iter().any(|name| name == "mDNS resolvers")); + } + + #[tokio::test] + async fn endpoint_dns_plan_custom_resolver_only_has_no_publishers() { + let network = Network::builder().build(); + let custom: ArcResolver = Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + }); + let plan = EndpointDnsPlan::new().with_resolver(custom); + let built = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect("custom resolver plan should build"); + + assert_eq!( + resolver_names(&built.endpoint_resolver), + vec!["counting resolver"] + ); + assert!(built.endpoint_publishers.iter().next().is_none()); + } + + #[tokio::test] + async fn endpoint_dns_plan_custom_publisher_only_is_empty_resolver_error() { + let network = Network::builder().build(); + let publisher = Arc::new(CountingPublisher { + calls: Arc::new(AtomicUsize::new(0)), + }); + let plan = EndpointDnsPlan::new().with_publisher(PublishScope::WideArea, publisher); + let error = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect_err("publisher-only plan should not build a resolver"); + + assert!(matches!(error, BuildEndpointDnsError::EmptyResolver)); + } + + #[tokio::test] + async fn endpoint_dns_plan_deduplicates_dns_schemes_but_not_custom_resolvers() { + let network = Network::builder().build(); + let calls = Arc::new(AtomicUsize::new(0)); + let first: ArcResolver = Arc::new(CountingResolver { + calls: calls.clone(), + }); + let second = first.clone(); + let plan = EndpointDnsPlan::new() + .with_dns(DnsScheme::System) + .with_resolver(first) + .with_dns(DnsScheme::System) + .with_resolver(second); + let built = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect("mixed plan should build"); + + assert_eq!( + resolver_names(&built.endpoint_resolver), + vec![ + "System DNS Resolver".to_string(), + "counting resolver".to_string(), + "counting resolver".to_string(), + ] + ); + } + + #[tokio::test] + async fn endpoint_dns_plan_h3_underlay_uses_custom_without_system_fallback() { + let network = Network::builder().build(); + let custom: ArcResolver = Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + }); + let plan = EndpointDnsPlan::new() + .with_dns(DnsScheme::H3) + .with_resolver(custom); + let built = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect("h3 plus custom resolver should build"); + + assert_eq!( + resolver_names(&built.endpoint_h3_underlay), + vec!["counting resolver"] + ); + } + + #[tokio::test] + async fn endpoint_dns_plan_h3_underlay_adds_system_without_custom_or_system() { + let network = Network::builder().build(); + let plan = EndpointDnsPlan::new().with_dns(DnsScheme::H3); + let built = plan + .build_endpoint_dns(network, Arc::new(Vec::new())) + .await + .expect("h3-only plan should build"); + + assert_eq!( + resolver_names(&built.endpoint_h3_underlay), + vec!["System DNS Resolver"] + ); + } + + #[tokio::test] + async fn build_stun_dns_keeps_h3_position_and_custom_resolvers() { + let network = Network::builder().build(); + let h3_marker: ArcResolver = Arc::new(NamedResolver("stun h3 marker")); + let custom: ArcResolver = Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + }); + let plan = EndpointDnsPlan::new() + .with_dns(DnsScheme::H3) + .with_resolver(custom) + .with_dns(DnsScheme::System); + let stun_dns = plan + .build_stun_dns(Some(h3_marker), network, Arc::new(Vec::new())) + .await + .expect("stun dns should build"); + + assert_eq!( + arc_resolvers_names(&stun_dns), + vec![ + "stun h3 marker".to_string(), + "counting resolver".to_string(), + "System DNS Resolver".to_string(), + ] + ); + } + #[tokio::test] async fn from_arc_network_preserves_external_network() { let network = Network::builder().build(); From 067d18af5400eb1d07eb346ae8a4e3eb17397f8a Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:25:04 +0800 Subject: [PATCH 04/12] feat: model endpoint dns operations --- dhttp/src/network.rs | 260 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 1 deletion(-) diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 2a6dcdf..4b19535 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -1,5 +1,7 @@ use std::{ops::Deref, sync::Arc}; +use snafu::Snafu; + use crate::ddns::{ mdns::MdnsResolvers, publishers::{PublishScope, Publisher, Publishers}, @@ -12,7 +14,7 @@ use crate::dquic::{ Network, QuicEndpoint, binds::BindPattern, net::{Devices, InterfaceManager, Locations, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY}, - resolver::{Resolve, handy::SystemResolver}, + resolver::{Publish, Resolve, handy::SystemResolver}, }; pub(crate) type DynResolver = dyn Resolve + Send + Sync; @@ -20,6 +22,262 @@ pub(crate) type ArcResolver = Arc; pub(crate) type DeferredStunResolver = DeferredResolver>; pub(crate) type DeferredEndpointH3Resolver = DeferredResolver>; pub(crate) type ArcEndpointH3Resolver = Arc; +pub(crate) type DynPublisher = dyn Publish + Send + Sync; +pub(crate) type ArcPublisher = Arc; +pub(crate) type ArcResolvers = Arc; + +#[derive(Debug, Snafu)] +#[snafu(module(build_endpoint_dns_error))] +pub enum BuildEndpointDnsError { + #[snafu(display("endpoint dns resolver set is empty"))] + EmptyResolver, +} + +#[derive(Debug, Snafu)] +#[snafu(module(build_stun_dns_error))] +pub enum BuildStunDnsError { + #[snafu(display("stun dns resolver set is empty"))] + EmptyResolver, +} + +#[derive(Clone)] +pub(crate) enum EndpointDnsOp { + Dns(DnsScheme), + Resolver(ArcResolver), + Publisher(Publisher), +} + +#[derive(Clone, Default)] +pub(crate) struct EndpointDnsPlan { + ops: Vec, +} + +#[derive(Debug)] +pub(crate) struct BuiltEndpointDns { + pub(crate) endpoint_resolver: ArcResolver, + pub(crate) endpoint_publishers: Publishers, + pub(crate) endpoint_h3_deferred: Option, + pub(crate) endpoint_h3_underlay: ArcResolver, +} + +impl EndpointDnsPlan { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn with_dns(mut self, scheme: DnsScheme) -> Self { + self.push_dns(scheme); + self + } + + pub(crate) fn with_resolver(mut self, resolver: ArcResolver) -> Self { + self.push_resolver(resolver); + self + } + + pub(crate) fn with_publisher(mut self, scope: PublishScope, publisher: ArcPublisher) -> Self { + self.push_publisher(scope, publisher); + self + } + + pub(crate) fn push_dns(&mut self, scheme: DnsScheme) { + self.ops.push(EndpointDnsOp::Dns(scheme)); + } + + pub(crate) fn push_resolver(&mut self, resolver: ArcResolver) { + self.ops.push(EndpointDnsOp::Resolver(resolver)); + } + + pub(crate) fn push_publisher(&mut self, scope: PublishScope, publisher: ArcPublisher) { + self.ops + .push(EndpointDnsOp::Publisher(Publisher::new(scope, publisher))); + } + + fn effective_ops(&self) -> Vec { + let source = if self.ops.is_empty() { + vec![ + EndpointDnsOp::Dns(DnsScheme::H3), + EndpointDnsOp::Dns(DnsScheme::Mdns), + EndpointDnsOp::Dns(DnsScheme::System), + ] + } else { + self.ops.clone() + }; + + let mut seen = std::collections::BTreeSet::new(); + source + .into_iter() + .filter(|operation| match operation { + EndpointDnsOp::Dns(scheme) => seen.insert(*scheme), + EndpointDnsOp::Resolver(_) | EndpointDnsOp::Publisher(_) => true, + }) + .collect() + } + + pub(crate) fn uses_h3(&self) -> bool { + self.effective_ops() + .iter() + .any(|operation| matches!(operation, EndpointDnsOp::Dns(DnsScheme::H3))) + } + + pub(crate) async fn build_endpoint_dns( + &self, + network: Arc, + bind: Arc>, + ) -> Result { + let operations = self.effective_ops(); + let mut resolver_builder = Resolvers::builder(); + let mut publishers = Publishers::new(); + let mut endpoint_h3_deferred = None; + + for operation in &operations { + match operation { + EndpointDnsOp::Dns(DnsScheme::Mdns) => { + let mdns = Arc::new( + MdnsResolvers::bind(network.clone(), bind.clone(), DHTTP_MDNS_SERVICE) + .await, + ); + resolver_builder = resolver_builder.resolver(mdns.clone()); + publishers.push(Publisher::mdns(mdns)); + } + EndpointDnsOp::Dns(DnsScheme::System) => { + resolver_builder = resolver_builder.system(); + } + EndpointDnsOp::Dns(DnsScheme::Http) => { + let http = Arc::new( + HttpResolver::new(DHTTP_HTTP_DNS_SERVER) + .expect("BUG: DHTTP HTTP DNS server is a valid URL"), + ); + resolver_builder = resolver_builder.resolver(http.clone()); + publishers.push(Publisher::http(http)); + } + EndpointDnsOp::Dns(DnsScheme::H3) => { + let h3 = Arc::new(DeferredEndpointH3Resolver::new()); + resolver_builder = resolver_builder.resolver(h3.clone()); + publishers.push(Publisher::new(PublishScope::WideArea, h3.clone())); + endpoint_h3_deferred = Some(h3); + } + EndpointDnsOp::Resolver(resolver) => { + resolver_builder = resolver_builder.resolver(resolver.clone()); + } + EndpointDnsOp::Publisher(publisher) => { + publishers.push(publisher.clone()); + } + } + } + + let endpoint_resolver = endpoint_resolver_chain(resolver_builder.build())?; + let endpoint_h3_underlay = self.build_endpoint_h3_underlay(network, bind).await?; + + Ok(BuiltEndpointDns { + endpoint_resolver, + endpoint_publishers: publishers, + endpoint_h3_deferred, + endpoint_h3_underlay, + }) + } + + async fn build_endpoint_h3_underlay( + &self, + network: Arc, + bind: Arc>, + ) -> Result { + let operations = self.effective_ops(); + let mut builder = Resolvers::builder(); + + for operation in &operations { + match operation { + EndpointDnsOp::Dns(DnsScheme::Mdns) => { + builder = builder.mdns(network.clone(), bind.clone()).await; + } + EndpointDnsOp::Dns(DnsScheme::System) => { + builder = builder.system(); + } + EndpointDnsOp::Dns(DnsScheme::Http) => { + builder = builder + .http() + .expect("BUG: DHTTP HTTP DNS server is a valid URL"); + } + EndpointDnsOp::Dns(DnsScheme::H3) | EndpointDnsOp::Publisher(_) => {} + EndpointDnsOp::Resolver(resolver) => { + builder = builder.resolver(resolver.clone()); + } + } + } + + if self.uses_h3() && !has_custom_resolver(&operations) && !has_system_dns(&operations) { + builder = builder.system(); + } + + endpoint_resolver_chain(builder.build()) + } + + pub(crate) async fn build_stun_dns( + &self, + h3_resolver: Option, + network: Arc, + bind: Arc>, + ) -> Result { + let operations = self.effective_ops(); + let mut builder = Resolvers::builder(); + + for operation in &operations { + match operation { + EndpointDnsOp::Dns(DnsScheme::Mdns) => { + builder = builder.mdns(network.clone(), bind.clone()).await; + } + EndpointDnsOp::Dns(DnsScheme::System) => { + builder = builder.system(); + } + EndpointDnsOp::Dns(DnsScheme::Http) => { + builder = builder + .http() + .expect("BUG: DHTTP HTTP DNS server is a valid URL"); + } + EndpointDnsOp::Dns(DnsScheme::H3) => { + if let Some(h3_resolver) = h3_resolver.clone() { + builder = builder.resolver(h3_resolver); + } + } + EndpointDnsOp::Resolver(resolver) => { + builder = builder.resolver(resolver.clone()); + } + EndpointDnsOp::Publisher(_) => {} + } + } + + stun_resolver_chain(builder.build()) + } +} + +fn endpoint_resolver_chain(resolvers: Resolvers) -> Result { + if resolvers.iter().next().is_none() { + build_endpoint_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(Arc::new(resolvers)) + } +} + +fn stun_resolver_chain(resolvers: Resolvers) -> Result { + if resolvers.iter().next().is_none() { + build_stun_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(Arc::new(resolvers)) + } +} + +fn has_custom_resolver(operations: &[EndpointDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, EndpointDnsOp::Resolver(_))) +} + +fn has_system_dns(operations: &[EndpointDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, EndpointDnsOp::Dns(DnsScheme::System))) +} + #[derive(Clone)] pub(crate) struct ResolverPlan { From 655b6687800a1590086eb371947be967b643028b Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:27:33 +0800 Subject: [PATCH 05/12] feat: wire endpoint dns plan into builder --- dhttp/src/endpoint.rs | 217 +++++++++++++++++++++++----------- dhttp/src/network.rs | 262 ++++-------------------------------------- 2 files changed, 171 insertions(+), 308 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 573b19b..bf49f59 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -4,9 +4,7 @@ use bon::bon; use http::uri::Authority; use snafu::{OptionExt, ResultExt}; -use crate::ddns::resolvers::{ - DHTTP_H3_DNS_SERVER, DnsScheme, H3Resolver, deferred::DeferredResolver, -}; +use crate::ddns::resolvers::{DHTTP_H3_DNS_SERVER, DnsScheme, H3Resolver}; use crate::dquic::{ Identity, QuicEndpoint, binds::BindPattern, client::ClientQuicConfig, connection::Connection as QuicConnection, resolver::Resolve, server::ServerQuicConfig, @@ -22,7 +20,9 @@ pub mod client; pub mod server; use self::client::Request; -use crate::network::{ArcEndpointH3Resolver, ArcResolver, DhttpNetwork, ResolverPlan}; +use crate::network::{ + ArcResolver, BuildEndpointDnsError, BuildStunDnsError, DhttpNetwork, EndpointDnsPlan, +}; /// A DHttp endpoint bound to a QUIC connection. /// @@ -66,6 +66,19 @@ pub enum InvalidEndpointIdentityError { }, } +#[derive(Debug, snafu::Snafu)] +#[snafu(module(build_endpoint_error))] +pub enum BuildEndpointError { + #[snafu(display("invalid endpoint identity"))] + InvalidIdentity { + source: InvalidEndpointIdentityError, + }, + #[snafu(display("failed to build endpoint dns"))] + EndpointDns { source: BuildEndpointDnsError }, + #[snafu(display("failed to build stun dns"))] + StunDns { source: BuildStunDnsError }, +} + #[derive(Debug, snafu::Snafu)] #[snafu(module(create_endpoint_publication_loop_error))] pub enum CreateEndpointPublicationLoopError { @@ -89,10 +102,6 @@ fn normalize_bind(bind: Arc>) -> Arc> { } } -fn deferred_h3_resolver() -> ArcEndpointH3Resolver { - Arc::new(DeferredResolver::new()) -} - fn h3_resolver_from_quic(quic: QuicEndpoint) -> H3Resolver { let h3 = Arc::new(H3Endpoint::new(quic)); H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, h3) @@ -113,7 +122,7 @@ impl Endpoint { /// [`Endpoint::load`]. #[builder] pub async fn new( - #[builder(field)] dns_schemes: Vec, + #[builder(field)] dns_plan: EndpointDnsPlan, identity: Option>, network: Option, @@ -121,65 +130,46 @@ impl Endpoint { #[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>, - resolver: Option>, #[builder(default)] connection_builder: Arc>, - ) -> Result { - Self::validate_identity(identity.as_deref())?; + ) -> Result { + Self::validate_identity(identity.as_deref()) + .context(build_endpoint_error::InvalidIdentitySnafu)?; let bind = normalize_bind(bind); - let resolver_plan = ResolverPlan::new(dns_schemes, resolver); - let (mut network, owns_network) = match network { Some(network) => (network, false), - None => { - let builder = DhttpNetwork::builder().dns_schemes(resolver_plan.schemes().to_vec()); - let network = match resolver_plan.custom() { - Some(resolver) => builder.resolver(resolver).build(), - None => builder.build(), - }; - (network, true) - } + None => (DhttpNetwork::endpoint_owned(), true), }; let raw_network = network.network().clone(); - let resolvers_without_h3 = resolver_plan - .build_resolvers(None, raw_network.clone(), bind.clone()) - .await; - let resolver_without_h3 = resolver_plan.resolver_without_h3(resolvers_without_h3); - - let endpoint_h3_deferred = resolver_plan.uses_h3().then(deferred_h3_resolver); - let (endpoint_resolvers, publishers) = resolver_plan - .build_resolvers_and_publishers( - endpoint_h3_deferred.clone(), - raw_network.clone(), - bind.clone(), - ) - .await; - let quic_resolver = resolver_plan.final_resolver(endpoint_resolvers); + let built_dns = dns_plan + .build_endpoint_dns(raw_network.clone(), bind.clone()) + .await + .context(build_endpoint_error::EndpointDnsSnafu)?; let quic = QuicEndpoint::builder() .network(raw_network.clone()) .maybe_identity(identity) - .resolver(quic_resolver) + .resolver(built_dns.endpoint_resolver.clone()) .client(client.clone()) .server(server) .bind(bind.clone()) .build() .await; - if let Some(endpoint_h3_deferred) = endpoint_h3_deferred { + if let Some(endpoint_h3_deferred) = built_dns.endpoint_h3_deferred { let mut dns_quic = quic.clone(); - dns_quic.set_resolver(resolver_without_h3.clone()); + dns_quic.set_resolver(built_dns.endpoint_h3_underlay.clone()); endpoint_h3_deferred .set(h3_resolver_from_quic(dns_quic)) .expect("BUG: endpoint H3 resolver is set exactly once"); } if owns_network { - let stun_h3_resolver = if resolver_plan.uses_h3() { + let stun_h3_resolver = if dns_plan.uses_h3() { let stun_quic = QuicEndpoint::builder() - .network(raw_network) - .resolver(resolver_without_h3) + .network(raw_network.clone()) + .resolver(built_dns.endpoint_h3_underlay) .client(client) .bind(bind.clone()) .build() @@ -188,7 +178,11 @@ impl Endpoint { } else { None }; - network.finish_stun_resolver(stun_h3_resolver, bind).await; + let stun_dns = dns_plan + .build_stun_dns(stun_h3_resolver, raw_network, bind.clone()) + .await + .context(build_endpoint_error::StunDnsSnafu)?; + network.finish_stun_resolver(stun_dns); } let h3 = H3Endpoint::builder() @@ -198,14 +192,28 @@ impl Endpoint { Ok(Self { inner: Arc::new(h3), network, - publishers, + publishers: built_dns.endpoint_publishers, }) } } impl EndpointBuilder { pub fn dns(mut self, scheme: DnsScheme) -> Self { - self.dns_schemes.push(scheme); + self.dns_plan.push_dns(scheme); + self + } + + pub fn resolver(mut self, resolver: Arc) -> Self { + self.dns_plan.push_resolver(resolver); + self + } + + pub fn publisher( + mut self, + scope: crate::ddns::publishers::PublishScope, + publisher: Arc, + ) -> Self { + self.dns_plan.push_publisher(scope, publisher); self } } @@ -231,9 +239,7 @@ where source: crate::home::identity::ssl::LoadIdentityError, }, #[snafu(display("failed to build endpoint"))] - BuildEndpoint { - source: InvalidEndpointIdentityError, - }, + BuildEndpoint { source: BuildEndpointError }, } #[derive(Debug, snafu::Snafu)] @@ -248,9 +254,7 @@ pub enum LoadEndpointFromPathError { source: crate::home::identity::ssl::LoadIdentityError, }, #[snafu(display("failed to build endpoint"))] - BuildEndpoint { - source: InvalidEndpointIdentityError, - }, + BuildEndpoint { source: BuildEndpointError }, } #[derive(Debug, snafu::Snafu)] @@ -614,7 +618,9 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidName { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidName { .. } + } )); } @@ -635,7 +641,9 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + } )); } @@ -656,7 +664,9 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + } )); } @@ -786,6 +796,89 @@ mod tests { } } + #[derive(Debug)] + struct CountingPublisher { + calls: Arc, + } + + impl fmt::Display for CountingPublisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("counting publisher") + } + } + + impl crate::dquic::resolver::Publish for CountingPublisher { + fn publish<'a>( + &'a self, + _name: &'a str, + _packet: &'a [u8], + ) -> crate::dquic::resolver::PublishFuture<'a> { + use futures::FutureExt; + + self.calls.fetch_add(1, Ordering::SeqCst); + async move { Ok(()) }.boxed() + } + } + + fn endpoint_resolver_names(endpoint: &Endpoint) -> Vec { + let resolver = endpoint.resolver(); + let any: &dyn Any = resolver.as_ref(); + let resolvers = any + .downcast_ref::() + .expect("endpoint resolver should be an aggregate"); + resolvers + .iter() + .map(|resolver| resolver.to_string()) + .collect() + } + + #[tokio::test] + async fn endpoint_with_custom_resolver_only_uses_custom_resolver_chain() { + let resolver: Arc = Arc::new(MarkerResolver); + let endpoint = Endpoint::builder() + .resolver(resolver) + .build() + .await + .expect("custom resolver endpoint is valid"); + + assert!(endpoint.dns_publishers().iter().next().is_none()); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["marker resolver"]); + } + + #[tokio::test] + async fn endpoint_with_publisher_only_fails_empty_resolver() { + let publisher: Arc = + Arc::new(CountingPublisher { + calls: Arc::new(AtomicUsize::new(0)), + }); + let Err(error) = Endpoint::builder() + .publisher(crate::ddns::publishers::PublishScope::WideArea, publisher) + .build() + .await + else { + panic!("publisher-only endpoint should fail without resolver"); + }; + + assert!(matches!(error, BuildEndpointError::EndpointDns { .. })); + } + + #[tokio::test] + async fn endpoint_with_system_dns_and_custom_publisher_builds_both_sides() { + let publisher: Arc = + Arc::new(CountingPublisher { + calls: Arc::new(AtomicUsize::new(0)), + }); + let endpoint = Endpoint::builder() + .dns(DnsScheme::System) + .publisher(crate::ddns::publishers::PublishScope::WideArea, publisher) + .build() + .await + .expect("system plus custom publisher endpoint should build"); + + assert_eq!(endpoint_resolver_names(&endpoint), vec!["System DNS Resolver"]); + assert_eq!(endpoint.dns_publishers().iter().count(), 1); + } + #[tokio::test] async fn builder_accepts_explicit_resolver() { let resolver: Arc = @@ -802,22 +895,6 @@ mod tests { assert!(any.downcast_ref::().is_some()); } - #[test] - fn h3_only_resolver_without_h3_uses_system_for_h3_underlay() { - let resolver_plan = ResolverPlan::new(vec![DnsScheme::H3], None); - let resolver = resolver_plan.resolver_without_h3(Resolvers::new()); - let any: &dyn Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("resolver_without_h3 is a resolver chain"); - let resolver_names = resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert_eq!(resolver_names, vec!["System DNS Resolver"]); - } - #[tokio::test] async fn owned_network_with_custom_resolver_uses_custom_for_stun_resolution() { let calls = Arc::new(AtomicUsize::new(0)); diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 4b19535..e957e35 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -295,96 +295,6 @@ impl ResolverPlan { Self { schemes, custom } } - pub(crate) fn schemes(&self) -> &[DnsScheme] { - &self.schemes - } - - pub(crate) fn custom(&self) -> Option { - self.custom.clone() - } - - pub(crate) fn uses_h3(&self) -> bool { - self.schemes.contains(&DnsScheme::H3) - } - - pub(crate) async fn build_resolvers( - &self, - h3_resolver: Option, - network: Arc, - bind: Arc>, - ) -> Resolvers { - let mut builder = Resolvers::builder(); - - if self.schemes.contains(&DnsScheme::Mdns) { - builder = builder.mdns(network.clone(), bind).await; - } - - if self.schemes.contains(&DnsScheme::System) { - builder = builder.system(); - } - - if self.schemes.contains(&DnsScheme::Http) { - builder = builder - .http() - .expect("BUG: DHTTP HTTP DNS server is a valid URL"); - } - - if self.uses_h3() - && let Some(h3_resolver) = h3_resolver - { - builder = builder.resolver(h3_resolver); - } - - if let Some(custom) = self.custom.clone() { - builder = builder.resolver(custom); - } - - builder.build() - } - - pub(crate) async fn build_resolvers_and_publishers( - &self, - h3_resolver: Option, - network: Arc, - bind: Arc>, - ) -> (Resolvers, Publishers) { - let mut resolver_builder = Resolvers::builder(); - let mut publishers = Publishers::new(); - - if self.schemes.contains(&DnsScheme::Mdns) { - let mdns = - Arc::new(MdnsResolvers::bind(network.clone(), bind, DHTTP_MDNS_SERVICE).await); - resolver_builder = resolver_builder.resolver(mdns.clone()); - publishers.push(Publisher::mdns(mdns)); - } - - if self.schemes.contains(&DnsScheme::System) { - resolver_builder = resolver_builder.system(); - } - - if self.schemes.contains(&DnsScheme::Http) { - let http = Arc::new( - HttpResolver::new(DHTTP_HTTP_DNS_SERVER) - .expect("BUG: DHTTP HTTP DNS server is a valid URL"), - ); - resolver_builder = resolver_builder.resolver(http.clone()); - publishers.push(Publisher::http(http)); - } - - if self.uses_h3() - && let Some(h3_resolver) = h3_resolver - { - resolver_builder = resolver_builder.resolver(h3_resolver.clone()); - publishers.push(Publisher::new(PublishScope::WideArea, h3_resolver)); - } - - if let Some(custom) = self.custom.clone() { - resolver_builder = resolver_builder.resolver(custom); - } - - (resolver_builder.build(), publishers) - } - pub(crate) fn select_resolver(&self, resolvers: Resolvers) -> ArcResolver { if self.schemes.is_empty() && let Some(custom) = self.custom.clone() @@ -398,20 +308,12 @@ impl ResolverPlan { pub(crate) fn final_resolver(&self, resolvers: Resolvers) -> ArcResolver { self.select_resolver(resolvers) } - - pub(crate) fn resolver_without_h3(&self, mut resolvers: Resolvers) -> ArcResolver { - if self.uses_h3() && self.custom.is_none() && !self.schemes.contains(&DnsScheme::System) { - resolvers.push(Arc::new(SystemResolver)); - } - self.select_resolver(resolvers) - } } #[derive(Clone)] pub struct DhttpNetwork { network: Arc, deferred_stun_resolver: Option>, - stun_resolver_plan: Option, _stun_resolver: Option, } @@ -421,26 +323,29 @@ impl DhttpNetwork { &self.network } - pub(crate) async fn finish_stun_resolver( - &mut self, - h3_resolver: Option, - bind: Arc>, - ) { - let (Some(deferred), Some(plan)) = ( - self.deferred_stun_resolver.clone(), - self.stun_resolver_plan.clone(), - ) else { + pub(crate) fn finish_stun_resolver(&mut self, stun_resolver: ArcResolvers) { + let Some(deferred) = self.deferred_stun_resolver.clone() else { return; }; - - let resolvers = plan - .build_resolvers(h3_resolver, self.network.clone(), bind) - .await; - let stun_resolver = Arc::new(resolvers); deferred .set(WeakResolver::new(Arc::downgrade(&stun_resolver))) .expect("BUG: network STUN resolver is set exactly once"); - self._stun_resolver = Some(stun_resolver); + let keepalive: ArcResolver = stun_resolver; + self._stun_resolver = Some(keepalive); + } + + pub(crate) fn endpoint_owned() -> Self { + let deferred = Arc::new(DeferredStunResolver::new()); + let stun_resolver: ArcResolver = deferred.clone(); + let network = Network::builder() + .maybe_stun_server(Some(Arc::::from(crate::endpoint::STUN_SERVER))) + .stun_resolver(stun_resolver) + .build(); + Self { + network, + deferred_stun_resolver: Some(deferred), + _stun_resolver: None, + } } } @@ -463,7 +368,6 @@ impl From> for DhttpNetwork { Self { network, deferred_stun_resolver: None, - stun_resolver_plan: None, _stun_resolver: None, } } @@ -511,23 +415,21 @@ impl DhttpNetwork { let plan = dns_schemes .or_else(|| resolver.as_ref().map(|_| Vec::new())) .map(|schemes| ResolverPlan::new(schemes, resolver)); - let (stun_resolver, deferred_stun_resolver, stun_resolver_plan, keepalive_resolver) = + let (stun_resolver, deferred_stun_resolver, keepalive_resolver) = match (stun_resolver, plan) { - (Some(stun_resolver), _) => { - (stun_resolver.clone(), None, None, Some(stun_resolver)) - } + (Some(stun_resolver), _) => (stun_resolver.clone(), None, Some(stun_resolver)), (None, Some(plan)) if plan.schemes.is_empty() => { let stun_resolver = plan.final_resolver(Resolvers::new()); - (stun_resolver.clone(), None, None, Some(stun_resolver)) + (stun_resolver.clone(), None, Some(stun_resolver)) } - (None, Some(plan)) => { + (None, Some(_plan)) => { let deferred = Arc::new(DeferredStunResolver::new()); let stun_resolver: ArcResolver = deferred.clone(); - (stun_resolver, Some(deferred), Some(plan), None) + (stun_resolver, Some(deferred), None) } (None, None) => { let stun_resolver: ArcResolver = Arc::new(SystemResolver); - (stun_resolver.clone(), None, None, Some(stun_resolver)) + (stun_resolver.clone(), None, Some(stun_resolver)) } }; @@ -543,7 +445,6 @@ impl DhttpNetwork { DhttpNetwork { network, deferred_stun_resolver, - stun_resolver_plan, _stun_resolver: keepalive_resolver, } } @@ -903,119 +804,4 @@ mod tests { assert_eq!(resolver.to_string(), "counting resolver"); } - #[test] - fn resolver_without_h3_keeps_non_h3_schemes_and_custom_resolver() { - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = ResolverPlan::new( - vec![ - DnsScheme::H3, - DnsScheme::Mdns, - DnsScheme::Http, - DnsScheme::System, - ], - Some(custom.clone()), - ); - - let resolver = plan.resolver_without_h3(Resolvers::builder().system().build().with(custom)); - let any: &dyn std::any::Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("resolver_without_h3 returns a resolver chain"); - let names = resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert!(names.iter().any(|name| name == "System DNS Resolver")); - assert!(names.iter().any(|name| name == "counting resolver")); - assert!( - !names - .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) - ); - } - - #[test] - fn resolver_without_h3_adds_system_when_h3_only_without_custom() { - let plan = ResolverPlan::new(vec![DnsScheme::H3], None); - - let resolver = plan.resolver_without_h3(Resolvers::new()); - let any: &dyn std::any::Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("h3-only resolver_without_h3 returns a concrete resolver chain"); - let names = resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert_eq!(names, vec!["System DNS Resolver"]); - } - - #[test] - fn resolver_without_h3_adds_system_to_h3_mdns_without_custom() { - let plan = ResolverPlan::new(vec![DnsScheme::H3, DnsScheme::Mdns], None); - let mdns_marker: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - - let resolver = plan.resolver_without_h3(Resolvers::new().with(mdns_marker)); - let any: &dyn std::any::Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("h3+mdns resolver_without_h3 returns a resolver chain"); - let names = resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert!(names.iter().any(|name| name == "counting resolver")); - assert!(names.iter().any(|name| name == "System DNS Resolver")); - assert!( - !names - .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) - ); - } - - #[test] - fn resolver_without_h3_does_not_auto_add_system_when_custom_is_present() { - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let mdns_marker: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = ResolverPlan::new(vec![DnsScheme::H3, DnsScheme::Mdns], Some(custom.clone())); - - let resolver = plan.resolver_without_h3(Resolvers::new().with(mdns_marker).with(custom)); - let any: &dyn std::any::Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("h3+mdns+custom resolver_without_h3 returns a resolver chain"); - let names = resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect::>(); - - assert_eq!( - names, - vec!["counting resolver", "counting resolver"], - "custom suppresses automatic System insertion; duplicate marker names represent mdns-marker plus custom" - ); - } - - #[test] - fn resolver_without_h3_does_not_override_custom_only_plan() { - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = ResolverPlan::new(Vec::new(), Some(custom)); - - let resolver = plan.resolver_without_h3(Resolvers::new()); - - assert_eq!(resolver.to_string(), "counting resolver"); - } } From 717ddf0bc22671bc8dd617aac085f45fe7d023d1 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:28:21 +0800 Subject: [PATCH 06/12] fix: allow listening without dns publishers --- dhttp/src/endpoint.rs | 52 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index bf49f59..ab525d0 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -331,24 +331,30 @@ impl Endpoint { pub fn dns_publication_loop( &self, ) -> Result< - crate::ddns::publishers::EndpointPublicationLoop< - crate::ddns::publishers::EndpointBindingAddresses, + Option< + crate::ddns::publishers::EndpointPublicationLoop< + crate::ddns::publishers::EndpointBindingAddresses, + >, >, CreateEndpointPublicationLoopError, > { let identity = self .identity() .context(create_endpoint_publication_loop_error::AnonymousEndpointSnafu)?; + if self.publishers.iter().next().is_none() { + return Ok(None); + } + let name = identity.name().to_owned(); let source = crate::ddns::publishers::EndpointBindingAddresses::new( self.network().network().clone(), self.bind_patterns(), ); - Ok(crate::ddns::publishers::EndpointPublicationLoop::new( + Ok(Some(crate::ddns::publishers::EndpointPublicationLoop::new( name, self.publishers.clone(), source, - )) + ))) } /// Load an endpoint from a domain name. @@ -519,13 +525,18 @@ impl Endpoint { }; let listen = h3.listen_owned(service); - let publish = publisher_loop.run(); - futures::pin_mut!(listen); - futures::pin_mut!(publish); - - match futures::future::select(listen, publish).await { - futures::future::Either::Left((result, _publish)) => result, - futures::future::Either::Right((never, _listen)) => match never {}, + match publisher_loop { + Some(publisher_loop) => { + let publish = publisher_loop.run(); + futures::pin_mut!(listen); + futures::pin_mut!(publish); + + match futures::future::select(listen, publish).await { + futures::future::Either::Left((result, _publish)) => result, + futures::future::Either::Right((never, _listen)) => match never {}, + } + } + None => listen.await, } } } @@ -738,6 +749,25 @@ mod tests { )); } + #[tokio::test] + async fn named_endpoint_with_empty_publishers_has_no_publication_loop() { + let identity = valid_dhttp_identity("empty-publisher.example.dhttp.net"); + let endpoint = Endpoint::builder() + .identity(Arc::new(identity)) + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("custom resolver endpoint should build"); + + assert!(endpoint.dns_publishers().iter().next().is_none()); + assert!( + endpoint + .dns_publication_loop() + .expect("named endpoint can evaluate publication loop") + .is_none() + ); + } + #[derive(Debug)] struct MarkerResolver; From 9d0f182b8d29c6351e3104e3190483238345e85d Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:29:45 +0800 Subject: [PATCH 07/12] test: update endpoint dns expectations --- dhttp/src/endpoint.rs | 9 +++++---- dhttp/src/network.rs | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index ab525d0..866319d 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -919,10 +919,8 @@ mod tests { .build() .await .unwrap(); - let resolver = endpoint.resolver(); - let any: &dyn std::any::Any = resolver.as_ref(); - assert!(any.downcast_ref::().is_some()); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["marker resolver"]); } #[tokio::test] @@ -975,7 +973,10 @@ mod tests { &raw_network.quic().stun_resolver(), &external_resolver )); - assert!(Arc::ptr_eq(&endpoint.resolver(), &endpoint_resolver)); + assert_eq!( + endpoint_resolver_names(&endpoint), + vec!["counting resolver"] + ); } #[tokio::test] diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index e957e35..9b02855 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -61,20 +61,24 @@ pub(crate) struct BuiltEndpointDns { } impl EndpointDnsPlan { + #[cfg(test)] pub(crate) fn new() -> Self { Self::default() } + #[cfg(test)] pub(crate) fn with_dns(mut self, scheme: DnsScheme) -> Self { self.push_dns(scheme); self } + #[cfg(test)] pub(crate) fn with_resolver(mut self, resolver: ArcResolver) -> Self { self.push_resolver(resolver); self } + #[cfg(test)] pub(crate) fn with_publisher(mut self, scope: PublishScope, publisher: ArcPublisher) -> Self { self.push_publisher(scope, publisher); self From 0b666a2d4260d53ff1ac23688b6416a10c561890 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:30:39 +0800 Subject: [PATCH 08/12] docs: describe endpoint dns builder operations --- dhttp/src/endpoint.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 866319d..2c8848b 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -116,10 +116,16 @@ fn h3_resolver_arc_from_quic(quic: QuicEndpoint) -> ArcResolver { impl Endpoint { /// Construct a new endpoint with full configuration control. /// - /// Use the builder pattern (via [`Endpoint::builder`]) to configure - /// DNS schemes, network, identity, client/server config, and bind - /// patterns. For a simpler setup from a domain name, see + /// Use the builder pattern (via [`Endpoint::builder`]) to configure DNS + /// resolution, DNS publication, network, identity, client/server config, + /// and bind patterns. For a simpler setup from a domain name, see /// [`Endpoint::load`]. + /// + /// DNS defaults are used only when no DNS operations are configured. A + /// default endpoint resolves through H3 DNS, mDNS, and System DNS, and + /// publishes through H3 DNS and mDNS. Calling [`EndpointBuilder::dns`], + /// [`EndpointBuilder::resolver`], or [`EndpointBuilder::publisher`] makes + /// the DNS plan explicit and disables default DNS injection. #[builder] pub async fn new( #[builder(field)] dns_plan: EndpointDnsPlan, @@ -198,16 +204,32 @@ impl Endpoint { } impl EndpointBuilder { + /// Add a built-in DNS scheme to the endpoint DNS plan. + /// + /// Schemes are deduplicated by first occurrence. Schemes that support + /// publication add both resolver and publisher capabilities; System DNS + /// adds only resolver capability. pub fn dns(mut self, scheme: DnsScheme) -> Self { self.dns_plan.push_dns(scheme); self } + /// Add a custom resolver to the endpoint DNS plan. + /// + /// This appends resolver capability only. It does not add a DNS publisher + /// and it disables default DNS injection because the DNS plan is no longer + /// empty. pub fn resolver(mut self, resolver: Arc) -> Self { self.dns_plan.push_resolver(resolver); self } + /// Add a custom scoped DNS publisher to the endpoint DNS plan. + /// + /// This appends publisher capability only. It does not add a resolver and + /// it disables default DNS injection because the DNS plan is no longer + /// empty. A plan with publishers but no resolvers fails during endpoint + /// construction. pub fn publisher( mut self, scope: crate::ddns::publishers::PublishScope, From ca6547f7c9dbdbc44b775f88b97da371779f05a7 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 04:31:52 +0800 Subject: [PATCH 09/12] style: format endpoint dns changes --- dhttp/src/endpoint.rs | 5 ++++- dhttp/src/network.rs | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 2c8848b..6808b0e 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -927,7 +927,10 @@ mod tests { .await .expect("system plus custom publisher endpoint should build"); - assert_eq!(endpoint_resolver_names(&endpoint), vec!["System DNS Resolver"]); + assert_eq!( + endpoint_resolver_names(&endpoint), + vec!["System DNS Resolver"] + ); assert_eq!(endpoint.dns_publishers().iter().count(), 1); } diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 9b02855..201346d 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -282,7 +282,6 @@ fn has_system_dns(operations: &[EndpointDnsOp]) -> bool { .any(|operation| matches!(operation, EndpointDnsOp::Dns(DnsScheme::System))) } - #[derive(Clone)] pub(crate) struct ResolverPlan { schemes: Vec, @@ -807,5 +806,4 @@ mod tests { assert_eq!(resolver.to_string(), "counting resolver"); } - } From fe637fb081edbf2b490477bf15e385175edc7df7 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 05:55:44 +0800 Subject: [PATCH 10/12] feat: add dhttp dns construction helpers --- dhttp/src/ddns.rs | 582 +++++++++++++++++++++++++++++++- dhttp/src/endpoint.rs | 131 ++++---- dhttp/src/network.rs | 748 +++++++++--------------------------------- 3 files changed, 796 insertions(+), 665 deletions(-) diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index b427980..3f47405 100644 --- a/dhttp/src/ddns.rs +++ b/dhttp/src/ddns.rs @@ -1,3 +1,583 @@ //! Re-export of the ddns crate APIs used by DHTTP. -pub use ddns::{core, mdns, publishers, resolvers}; +use std::{future::Future, sync::Arc}; + +use snafu::ResultExt; + +use crate::{ + dquic::{ + Network, QuicEndpoint, + binds::BindPattern, + resolver::{Publish, Resolve}, + }, + h3x::endpoint::H3Endpoint, + network::{ArcResolvers, DeferredStunResolver, DhttpNetwork}, +}; + +pub use ::ddns::*; + +/// Resolver trait object used by DHTTP DNS construction. +pub type ArcResolver = Arc; + +/// Publisher trait object used by DHTTP DNS construction. +pub type ArcPublisher = Arc; + +#[derive(Clone)] +enum DhttpDnsOp { + Dns(resolvers::DnsScheme), + Resolver(ArcResolver), + Publisher(publishers::Publisher), +} + +/// Ordered DNS intent for DHTTP endpoint and network construction. +#[derive(Clone, Default)] +pub struct DhttpDnsPlan { + ops: Vec, +} + +impl DhttpDnsPlan { + /// Create an empty DNS plan. + /// + /// Empty plans use DHTTP's default DNS schemes when construction helpers + /// evaluate them. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Append a built-in DNS scheme to the plan. + /// + /// Built-in schemes are deduplicated by first occurrence when evaluated. + pub fn push_dns(&mut self, scheme: resolvers::DnsScheme) { + self.ops.push(DhttpDnsOp::Dns(scheme)); + } + + /// Append a custom resolver to the plan. + /// + /// Custom resolvers are not deduplicated. + pub fn push_resolver(&mut self, resolver: ArcResolver) { + self.ops.push(DhttpDnsOp::Resolver(resolver)); + } + + /// Append a custom scoped DNS publisher to the plan. + /// + /// Custom publishers are not deduplicated. + pub fn push_publisher(&mut self, scope: publishers::PublishScope, publisher: ArcPublisher) { + self.ops + .push(DhttpDnsOp::Publisher(publishers::Publisher::new( + scope, publisher, + ))); + } + + fn effective_ops(&self) -> Vec { + let source = if self.ops.is_empty() { + vec![ + DhttpDnsOp::Dns(resolvers::DnsScheme::H3), + DhttpDnsOp::Dns(resolvers::DnsScheme::Mdns), + DhttpDnsOp::Dns(resolvers::DnsScheme::System), + ] + } else { + self.ops.clone() + }; + + let mut seen = std::collections::BTreeSet::new(); + source + .into_iter() + .filter(|operation| match operation { + DhttpDnsOp::Dns(scheme) => seen.insert(*scheme), + DhttpDnsOp::Resolver(_) | DhttpDnsOp::Publisher(_) => true, + }) + .collect() + } + + #[cfg(test)] + fn effective_dns_schemes_for_test(&self) -> Vec { + self.effective_ops() + .into_iter() + .filter_map(|operation| match operation { + DhttpDnsOp::Dns(scheme) => Some(scheme), + DhttpDnsOp::Resolver(_) | DhttpDnsOp::Publisher(_) => None, + }) + .collect() + } + + #[cfg(test)] + fn effective_ops_len_for_test(&self) -> usize { + self.effective_ops().len() + } +} + +type DeferredEndpointResolver = resolvers::deferred::DeferredResolver; + +#[derive(Debug, snafu::Snafu)] +#[snafu(module(build_dhttp_network_with_dns_error))] +pub enum BuildDhttpNetworkWithDnsError { + #[snafu(display("network dns resolver set is empty"))] + EmptyResolver, + #[snafu(display("network deferred stun resolver was already initialized"))] + DeferredStunResolver { + source: resolvers::deferred::SetDeferredResolverError, + }, + #[snafu(display("h3 dns server url is invalid"))] + InvalidH3DnsServer { source: std::io::Error }, +} + +#[derive(Debug, snafu::Snafu)] +#[snafu(module(build_quic_endpoint_with_dns_error))] +pub enum BuildQuicEndpointWithDnsError { + #[snafu(display("endpoint dns resolver set is empty"))] + EmptyResolver, + #[snafu(display("endpoint deferred resolver was already initialized"))] + DeferredEndpointResolver { + source: resolvers::deferred::SetDeferredResolverError, + }, + #[snafu(display("h3 dns server url is invalid"))] + InvalidH3DnsServer { source: std::io::Error }, +} + +#[bon::builder(finish_fn = build)] +pub async fn dhttp_network_builder_with_dns( + #[builder(start_fn)] builder: F, + #[builder(start_fn)] dns_plan: &DhttpDnsPlan, + #[builder(default = Arc::new(Vec::new()))] bind: Arc>, + #[builder(default = Arc::::from(resolvers::DHTTP_H3_DNS_SERVER))] h3_dns_server: Arc, +) -> Result +where + F: FnOnce(ArcResolver) -> Arc, +{ + 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?; + + DhttpNetwork::from_deferred_stun_resolver(network, deferred_stun_resolver, final_resolver) + .context(build_dhttp_network_with_dns_error::DeferredStunResolverSnafu) +} + +#[bon::builder(finish_fn = build)] +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, +) -> Result<(QuicEndpoint, publishers::Publishers), BuildQuicEndpointWithDnsError> +where + F: FnOnce(ArcResolver) -> Fut, + Fut: Future, +{ + let deferred_endpoint_resolver = Arc::new(DeferredEndpointResolver::new()); + 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?; + + deferred_endpoint_resolver + .set(final_resolver) + .context(build_quic_endpoint_with_dns_error::DeferredEndpointResolverSnafu)?; + + Ok((endpoint, publishers)) +} + +async fn network_stun_resolver_from_plan( + dns_plan: &DhttpDnsPlan, + network: Arc, + bind: Arc>, + h3_dns_server: Arc, +) -> Result { + let operations = dns_plan.effective_ops(); + let h3_resolver = if uses_h3(&operations) { + let h3_underlay = network_h3_underlay(&operations, network.clone(), bind.clone()).await?; + let h3_quic = QuicEndpoint::builder() + .network(network.clone()) + .resolver(h3_underlay) + .bind(bind.clone()) + .build() + .await; + Some(Arc::new(h3_resolver_for_network( + h3_dns_server.as_ref(), + h3_quic, + )?)) + } else { + 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(); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { + builder = builder + .http() + .expect("BUG: DHTTP HTTP DNS server is a valid URL"); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::H3) => { + if let Some(h3_resolver) = h3_resolver.clone() { + builder = builder.resolver(h3_resolver); + } + } + DhttpDnsOp::Resolver(resolver) => { + builder = builder.resolver(resolver.clone()); + } + DhttpDnsOp::Publisher(_) => {} + } + } + + network_resolver_chain(builder.build()) +} + +async fn endpoint_dns_from_quic( + dns_plan: &DhttpDnsPlan, + endpoint: &QuicEndpoint, + h3_dns_server: Arc, +) -> Result<(resolvers::Resolvers, publishers::Publishers), BuildQuicEndpointWithDnsError> { + let operations = dns_plan.effective_ops(); + let endpoint_h3 = if uses_h3(&operations) { + let h3_underlay = endpoint_h3_underlay(&operations, endpoint).await?; + let mut h3_quic = endpoint.clone(); + h3_quic.set_resolver(h3_underlay); + Some(Arc::new(H3Endpoint::new(h3_quic))) + } else { + None + }; + + let mut resolver_builder = resolvers::Resolvers::builder(); + let mut publishers = publishers::Publishers::new(); + + 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, + ); + resolver_builder = resolver_builder.resolver(mdns.clone()); + publishers.push(publishers::Publisher::mdns(mdns)); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::System) => { + resolver_builder = resolver_builder.system(); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { + let http = Arc::new( + resolvers::HttpResolver::new(resolvers::DHTTP_HTTP_DNS_SERVER) + .expect("BUG: DHTTP HTTP DNS server is a valid URL"), + ); + resolver_builder = resolver_builder.resolver(http.clone()); + publishers.push(publishers::Publisher::http(http)); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::H3) => { + let h3_endpoint = endpoint_h3 + .clone() + .expect("BUG: endpoint H3 endpoint exists when H3 DNS is used"); + let h3 = Arc::new(h3_resolver_for_endpoint( + h3_dns_server.as_ref(), + h3_endpoint, + )?); + resolver_builder = resolver_builder.resolver(h3.clone()); + publishers.push(publishers::Publisher::new( + publishers::PublishScope::WideArea, + h3, + )); + } + DhttpDnsOp::Resolver(resolver) => { + resolver_builder = resolver_builder.resolver(resolver.clone()); + } + DhttpDnsOp::Publisher(publisher) => { + publishers.push(publisher.clone()); + } + } + } + + let resolvers = endpoint_resolver_chain(resolver_builder.build())?; + Ok((resolvers, publishers)) +} + +async fn endpoint_h3_underlay( + operations: &[DhttpDnsOp], + endpoint: &QuicEndpoint, +) -> Result { + let resolvers = non_h3_resolvers( + operations, + endpoint.network().clone(), + endpoint.bind_patterns().clone(), + ) + .await; + + endpoint_arc_resolver_chain(resolvers) +} + +async fn network_h3_underlay( + operations: &[DhttpDnsOp], + network: Arc, + bind: Arc>, +) -> Result { + let resolvers = non_h3_resolvers(operations, network, bind).await; + + network_arc_resolver_chain(resolvers) +} + +async fn non_h3_resolvers( + operations: &[DhttpDnsOp], + network: Arc, + bind: Arc>, +) -> resolvers::Resolvers { + 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(); + } + DhttpDnsOp::Dns(resolvers::DnsScheme::Http) => { + builder = builder + .http() + .expect("BUG: DHTTP HTTP DNS server is a valid URL"); + } + DhttpDnsOp::Dns(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() +} + +fn h3_resolver_for_network( + h3_dns_server: &str, + quic: QuicEndpoint, +) -> Result, BuildDhttpNetworkWithDnsError> { + let h3 = Arc::new(H3Endpoint::new(quic)); + resolvers::H3Resolver::from_endpoint(h3_dns_server, h3) + .context(build_dhttp_network_with_dns_error::InvalidH3DnsServerSnafu) +} + +fn h3_resolver_for_endpoint( + h3_dns_server: &str, + h3: Arc>, +) -> Result, BuildQuicEndpointWithDnsError> { + resolvers::H3Resolver::from_endpoint(h3_dns_server, h3) + .context(build_quic_endpoint_with_dns_error::InvalidH3DnsServerSnafu) +} + +fn endpoint_resolver_chain( + resolvers: resolvers::Resolvers, +) -> Result { + if resolvers.iter().next().is_none() { + build_quic_endpoint_with_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(resolvers) + } +} + +fn endpoint_arc_resolver_chain( + resolvers: resolvers::Resolvers, +) -> Result { + if resolvers.iter().next().is_none() { + build_quic_endpoint_with_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(Arc::new(resolvers)) + } +} + +fn network_resolver_chain( + resolvers: resolvers::Resolvers, +) -> Result { + if resolvers.iter().next().is_none() { + build_dhttp_network_with_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(Arc::new(resolvers)) + } +} + +fn network_arc_resolver_chain( + resolvers: resolvers::Resolvers, +) -> Result { + if resolvers.iter().next().is_none() { + build_dhttp_network_with_dns_error::EmptyResolverSnafu.fail() + } else { + Ok(Arc::new(resolvers)) + } +} + +fn uses_h3(operations: &[DhttpDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::H3))) +} + +fn has_custom_resolver(operations: &[DhttpDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, DhttpDnsOp::Resolver(_))) +} + +fn has_system_dns(operations: &[DhttpDnsOp]) -> bool { + operations + .iter() + .any(|operation| matches!(operation, DhttpDnsOp::Dns(resolvers::DnsScheme::System))) +} + +#[cfg(test)] +mod tests { + use std::{ + fmt, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + }; + + use futures::{FutureExt, StreamExt, stream}; + + use super::*; + use crate::dquic::resolver::{Publish, PublishFuture, Resolve}; + + #[derive(Debug)] + struct CountingResolver { + calls: Arc, + } + + impl fmt::Display for CountingResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("counting resolver") + } + } + + impl Resolve for CountingResolver { + fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { + self.calls.fetch_add(1, Ordering::SeqCst); + async move { Ok(stream::empty().boxed()) }.boxed() + } + } + + #[derive(Debug)] + struct CountingPublisher { + calls: Arc, + } + + impl fmt::Display for CountingPublisher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("counting publisher") + } + } + + impl Publish for CountingPublisher { + fn publish<'a>(&'a self, _name: &'a str, _packet: &'a [u8]) -> PublishFuture<'a> { + self.calls.fetch_add(1, Ordering::SeqCst); + async move { Ok(()) }.boxed() + } + } + + #[test] + fn dhttp_dns_plan_defaults_only_when_empty() { + let empty = DhttpDnsPlan::new(); + + assert_eq!( + empty.effective_dns_schemes_for_test(), + vec![ + resolvers::DnsScheme::H3, + resolvers::DnsScheme::Mdns, + resolvers::DnsScheme::System, + ] + ); + + let mut explicit = DhttpDnsPlan::new(); + explicit.push_resolver(Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + })); + + assert!(explicit.effective_dns_schemes_for_test().is_empty()); + } + + #[test] + fn dhttp_dns_plan_deduplicates_dns_schemes_not_custom_ops() { + let calls = Arc::new(AtomicUsize::new(0)); + let resolver: Arc = Arc::new(CountingResolver { calls }); + let publisher: Arc = Arc::new(CountingPublisher { + calls: Arc::new(AtomicUsize::new(0)), + }); + + let mut plan = DhttpDnsPlan::new(); + plan.push_dns(resolvers::DnsScheme::System); + plan.push_resolver(resolver.clone()); + plan.push_dns(resolvers::DnsScheme::System); + plan.push_resolver(resolver); + plan.push_publisher(publishers::PublishScope::WideArea, publisher.clone()); + plan.push_publisher(publishers::PublishScope::WideArea, publisher); + + assert_eq!(plan.effective_ops_len_for_test(), 5); + } + + #[tokio::test] + async fn dhttp_network_builder_with_dns_passes_deferred_resolver_to_builder() { + let mut plan = DhttpDnsPlan::new(); + plan.push_resolver(Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + })); + + let network = dhttp_network_builder_with_dns( + |resolver| { + assert!(resolver.to_string().starts_with("DeferredResolver(")); + crate::dquic::Network::builder() + .stun_resolver(resolver) + .build() + }, + &plan, + ) + .build() + .await + .expect("network helper should build"); + + assert!( + network + .network() + .quic() + .stun_resolver() + .to_string() + .starts_with("DeferredResolver(") + ); + } + + #[tokio::test] + async fn quic_endpoint_builder_with_dns_returns_endpoint_and_publishers() { + let mut plan = DhttpDnsPlan::new(); + plan.push_resolver(Arc::new(CountingResolver { + calls: Arc::new(AtomicUsize::new(0)), + })); + + let (endpoint, publishers) = quic_endpoint_builder_with_dns( + |resolver| async move { + crate::dquic::QuicEndpoint::builder() + .resolver(resolver) + .build() + .await + }, + &plan, + ) + .build() + .await + .expect("endpoint helper should build"); + + assert_eq!( + endpoint.resolver().to_string(), + "DeferredResolver(Resolvers(counting resolver))" + ); + assert!(publishers.iter().next().is_none()); + } +} diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 6808b0e..6f6d449 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -4,7 +4,10 @@ use bon::bon; use http::uri::Authority; use snafu::{OptionExt, ResultExt}; -use crate::ddns::resolvers::{DHTTP_H3_DNS_SERVER, DnsScheme, H3Resolver}; +use crate::ddns::{ + BuildDhttpNetworkWithDnsError, BuildQuicEndpointWithDnsError, DhttpDnsPlan, + resolvers::DnsScheme, +}; use crate::dquic::{ Identity, QuicEndpoint, binds::BindPattern, client::ClientQuicConfig, connection::Connection as QuicConnection, resolver::Resolve, server::ServerQuicConfig, @@ -20,9 +23,7 @@ pub mod client; pub mod server; use self::client::Request; -use crate::network::{ - ArcResolver, BuildEndpointDnsError, BuildStunDnsError, DhttpNetwork, EndpointDnsPlan, -}; +use crate::network::DhttpNetwork; /// A DHttp endpoint bound to a QUIC connection. /// @@ -74,9 +75,13 @@ pub enum BuildEndpointError { source: InvalidEndpointIdentityError, }, #[snafu(display("failed to build endpoint dns"))] - EndpointDns { source: BuildEndpointDnsError }, + EndpointDns { + source: BuildQuicEndpointWithDnsError, + }, #[snafu(display("failed to build stun dns"))] - StunDns { source: BuildStunDnsError }, + StunDns { + source: BuildDhttpNetworkWithDnsError, + }, } #[derive(Debug, snafu::Snafu)] @@ -102,16 +107,6 @@ fn normalize_bind(bind: Arc>) -> Arc> { } } -fn h3_resolver_from_quic(quic: QuicEndpoint) -> H3Resolver { - let h3 = Arc::new(H3Endpoint::new(quic)); - H3Resolver::from_endpoint(DHTTP_H3_DNS_SERVER, h3) - .expect("BUG: DHTTP H3 DNS server is a valid URL") -} - -fn h3_resolver_arc_from_quic(quic: QuicEndpoint) -> ArcResolver { - Arc::new(h3_resolver_from_quic(quic)) -} - #[bon] impl Endpoint { /// Construct a new endpoint with full configuration control. @@ -128,7 +123,7 @@ impl Endpoint { /// the DNS plan explicit and disables default DNS injection. #[builder] pub async fn new( - #[builder(field)] dns_plan: EndpointDnsPlan, + #[builder(field)] dns_plan: DhttpDnsPlan, identity: Option>, network: Option, @@ -142,54 +137,40 @@ impl Endpoint { .context(build_endpoint_error::InvalidIdentitySnafu)?; let bind = normalize_bind(bind); - let (mut network, owns_network) = match network { - Some(network) => (network, false), - None => (DhttpNetwork::endpoint_owned(), true), + let network = match network { + Some(network) => network, + None => DhttpNetwork::builder() + .dns_plan(dns_plan.clone()) + .bind(bind.clone()) + .build() + .await + .context(build_endpoint_error::StunDnsSnafu)?, }; let raw_network = network.network().clone(); - let built_dns = dns_plan - .build_endpoint_dns(raw_network.clone(), bind.clone()) - .await - .context(build_endpoint_error::EndpointDnsSnafu)?; - - let quic = QuicEndpoint::builder() - .network(raw_network.clone()) - .maybe_identity(identity) - .resolver(built_dns.endpoint_resolver.clone()) - .client(client.clone()) - .server(server) - .bind(bind.clone()) - .build() - .await; - - if let Some(endpoint_h3_deferred) = built_dns.endpoint_h3_deferred { - let mut dns_quic = quic.clone(); - dns_quic.set_resolver(built_dns.endpoint_h3_underlay.clone()); - endpoint_h3_deferred - .set(h3_resolver_from_quic(dns_quic)) - .expect("BUG: endpoint H3 resolver is set exactly once"); - } - - if owns_network { - let stun_h3_resolver = if dns_plan.uses_h3() { - let stun_quic = QuicEndpoint::builder() - .network(raw_network.clone()) - .resolver(built_dns.endpoint_h3_underlay) - .client(client) - .bind(bind.clone()) - .build() - .await; - Some(h3_resolver_arc_from_quic(stun_quic)) - } else { - None - }; - let stun_dns = dns_plan - .build_stun_dns(stun_h3_resolver, raw_network, bind.clone()) - .await - .context(build_endpoint_error::StunDnsSnafu)?; - network.finish_stun_resolver(stun_dns); - } + let (quic, publishers) = crate::ddns::quic_endpoint_builder_with_dns( + |resolver| { + let raw_network = raw_network.clone(); + let identity = identity.clone(); + let client = client.clone(); + let bind = bind.clone(); + async move { + QuicEndpoint::builder() + .network(raw_network) + .maybe_identity(identity) + .resolver(resolver) + .client(client) + .server(server) + .bind(bind) + .build() + .await + } + }, + &dns_plan, + ) + .build() + .await + .context(build_endpoint_error::EndpointDnsSnafu)?; let h3 = H3Endpoint::builder() .quic(quic) @@ -198,7 +179,7 @@ impl Endpoint { Ok(Self { inner: Arc::new(h3), network, - publishers: built_dns.endpoint_publishers, + publishers, }) } } @@ -590,7 +571,7 @@ impl crate::h3x::quic::Connect for Endpoint { mod tests { use super::*; use crate::{ - ddns::resolvers::{DnsScheme, Resolvers}, + ddns::resolvers::{DnsScheme, H3Resolver, Resolvers}, dquic::Network, network::DeferredStunResolver, }; @@ -875,10 +856,19 @@ mod tests { fn endpoint_resolver_names(endpoint: &Endpoint) -> Vec { let resolver = endpoint.resolver(); let any: &dyn Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("endpoint resolver should be an aggregate"); - resolvers + if let Some(resolvers) = any.downcast_ref::() { + return resolvers + .iter() + .map(|resolver| resolver.to_string()) + .collect(); + } + + let deferred = any + .downcast_ref::>() + .expect("endpoint resolver should be an aggregate or initialized deferred aggregate"); + deferred + .get() + .expect("endpoint deferred resolver should be initialized") .iter() .map(|resolver| resolver.to_string()) .collect() @@ -911,7 +901,10 @@ mod tests { panic!("publisher-only endpoint should fail without resolver"); }; - assert!(matches!(error, BuildEndpointError::EndpointDns { .. })); + assert!(matches!( + error, + BuildEndpointError::EndpointDns { .. } | BuildEndpointError::StunDns { .. } + )); } #[tokio::test] diff --git a/dhttp/src/network.rs b/dhttp/src/network.rs index 201346d..70d3c9f 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -1,322 +1,24 @@ use std::{ops::Deref, sync::Arc}; -use snafu::Snafu; - use crate::ddns::{ - mdns::MdnsResolvers, - publishers::{PublishScope, Publisher, Publishers}, - resolvers::{ - DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsScheme, H3Resolver, HttpResolver, Resolvers, - deferred::DeferredResolver, weak::WeakResolver, - }, + ArcPublisher, ArcResolver, BuildDhttpNetworkWithDnsError, DhttpDnsPlan, + dhttp_network_builder_with_dns, + publishers::PublishScope, + resolvers::{DnsScheme, Resolvers, deferred::DeferredResolver, weak::WeakResolver}, }; use crate::dquic::{ - Network, QuicEndpoint, + Network, binds::BindPattern, net::{Devices, InterfaceManager, Locations, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY}, - resolver::{Publish, Resolve, handy::SystemResolver}, }; -pub(crate) type DynResolver = dyn Resolve + Send + Sync; -pub(crate) type ArcResolver = Arc; -pub(crate) type DeferredStunResolver = DeferredResolver>; -pub(crate) type DeferredEndpointH3Resolver = DeferredResolver>; -pub(crate) type ArcEndpointH3Resolver = Arc; -pub(crate) type DynPublisher = dyn Publish + Send + Sync; -pub(crate) type ArcPublisher = Arc; pub(crate) type ArcResolvers = Arc; - -#[derive(Debug, Snafu)] -#[snafu(module(build_endpoint_dns_error))] -pub enum BuildEndpointDnsError { - #[snafu(display("endpoint dns resolver set is empty"))] - EmptyResolver, -} - -#[derive(Debug, Snafu)] -#[snafu(module(build_stun_dns_error))] -pub enum BuildStunDnsError { - #[snafu(display("stun dns resolver set is empty"))] - EmptyResolver, -} - -#[derive(Clone)] -pub(crate) enum EndpointDnsOp { - Dns(DnsScheme), - Resolver(ArcResolver), - Publisher(Publisher), -} - -#[derive(Clone, Default)] -pub(crate) struct EndpointDnsPlan { - ops: Vec, -} - -#[derive(Debug)] -pub(crate) struct BuiltEndpointDns { - pub(crate) endpoint_resolver: ArcResolver, - pub(crate) endpoint_publishers: Publishers, - pub(crate) endpoint_h3_deferred: Option, - pub(crate) endpoint_h3_underlay: ArcResolver, -} - -impl EndpointDnsPlan { - #[cfg(test)] - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(test)] - pub(crate) fn with_dns(mut self, scheme: DnsScheme) -> Self { - self.push_dns(scheme); - self - } - - #[cfg(test)] - pub(crate) fn with_resolver(mut self, resolver: ArcResolver) -> Self { - self.push_resolver(resolver); - self - } - - #[cfg(test)] - pub(crate) fn with_publisher(mut self, scope: PublishScope, publisher: ArcPublisher) -> Self { - self.push_publisher(scope, publisher); - self - } - - pub(crate) fn push_dns(&mut self, scheme: DnsScheme) { - self.ops.push(EndpointDnsOp::Dns(scheme)); - } - - pub(crate) fn push_resolver(&mut self, resolver: ArcResolver) { - self.ops.push(EndpointDnsOp::Resolver(resolver)); - } - - pub(crate) fn push_publisher(&mut self, scope: PublishScope, publisher: ArcPublisher) { - self.ops - .push(EndpointDnsOp::Publisher(Publisher::new(scope, publisher))); - } - - fn effective_ops(&self) -> Vec { - let source = if self.ops.is_empty() { - vec![ - EndpointDnsOp::Dns(DnsScheme::H3), - EndpointDnsOp::Dns(DnsScheme::Mdns), - EndpointDnsOp::Dns(DnsScheme::System), - ] - } else { - self.ops.clone() - }; - - let mut seen = std::collections::BTreeSet::new(); - source - .into_iter() - .filter(|operation| match operation { - EndpointDnsOp::Dns(scheme) => seen.insert(*scheme), - EndpointDnsOp::Resolver(_) | EndpointDnsOp::Publisher(_) => true, - }) - .collect() - } - - pub(crate) fn uses_h3(&self) -> bool { - self.effective_ops() - .iter() - .any(|operation| matches!(operation, EndpointDnsOp::Dns(DnsScheme::H3))) - } - - pub(crate) async fn build_endpoint_dns( - &self, - network: Arc, - bind: Arc>, - ) -> Result { - let operations = self.effective_ops(); - let mut resolver_builder = Resolvers::builder(); - let mut publishers = Publishers::new(); - let mut endpoint_h3_deferred = None; - - for operation in &operations { - match operation { - EndpointDnsOp::Dns(DnsScheme::Mdns) => { - let mdns = Arc::new( - MdnsResolvers::bind(network.clone(), bind.clone(), DHTTP_MDNS_SERVICE) - .await, - ); - resolver_builder = resolver_builder.resolver(mdns.clone()); - publishers.push(Publisher::mdns(mdns)); - } - EndpointDnsOp::Dns(DnsScheme::System) => { - resolver_builder = resolver_builder.system(); - } - EndpointDnsOp::Dns(DnsScheme::Http) => { - let http = Arc::new( - HttpResolver::new(DHTTP_HTTP_DNS_SERVER) - .expect("BUG: DHTTP HTTP DNS server is a valid URL"), - ); - resolver_builder = resolver_builder.resolver(http.clone()); - publishers.push(Publisher::http(http)); - } - EndpointDnsOp::Dns(DnsScheme::H3) => { - let h3 = Arc::new(DeferredEndpointH3Resolver::new()); - resolver_builder = resolver_builder.resolver(h3.clone()); - publishers.push(Publisher::new(PublishScope::WideArea, h3.clone())); - endpoint_h3_deferred = Some(h3); - } - EndpointDnsOp::Resolver(resolver) => { - resolver_builder = resolver_builder.resolver(resolver.clone()); - } - EndpointDnsOp::Publisher(publisher) => { - publishers.push(publisher.clone()); - } - } - } - - let endpoint_resolver = endpoint_resolver_chain(resolver_builder.build())?; - let endpoint_h3_underlay = self.build_endpoint_h3_underlay(network, bind).await?; - - Ok(BuiltEndpointDns { - endpoint_resolver, - endpoint_publishers: publishers, - endpoint_h3_deferred, - endpoint_h3_underlay, - }) - } - - async fn build_endpoint_h3_underlay( - &self, - network: Arc, - bind: Arc>, - ) -> Result { - let operations = self.effective_ops(); - let mut builder = Resolvers::builder(); - - for operation in &operations { - match operation { - EndpointDnsOp::Dns(DnsScheme::Mdns) => { - builder = builder.mdns(network.clone(), bind.clone()).await; - } - EndpointDnsOp::Dns(DnsScheme::System) => { - builder = builder.system(); - } - EndpointDnsOp::Dns(DnsScheme::Http) => { - builder = builder - .http() - .expect("BUG: DHTTP HTTP DNS server is a valid URL"); - } - EndpointDnsOp::Dns(DnsScheme::H3) | EndpointDnsOp::Publisher(_) => {} - EndpointDnsOp::Resolver(resolver) => { - builder = builder.resolver(resolver.clone()); - } - } - } - - if self.uses_h3() && !has_custom_resolver(&operations) && !has_system_dns(&operations) { - builder = builder.system(); - } - - endpoint_resolver_chain(builder.build()) - } - - pub(crate) async fn build_stun_dns( - &self, - h3_resolver: Option, - network: Arc, - bind: Arc>, - ) -> Result { - let operations = self.effective_ops(); - let mut builder = Resolvers::builder(); - - for operation in &operations { - match operation { - EndpointDnsOp::Dns(DnsScheme::Mdns) => { - builder = builder.mdns(network.clone(), bind.clone()).await; - } - EndpointDnsOp::Dns(DnsScheme::System) => { - builder = builder.system(); - } - EndpointDnsOp::Dns(DnsScheme::Http) => { - builder = builder - .http() - .expect("BUG: DHTTP HTTP DNS server is a valid URL"); - } - EndpointDnsOp::Dns(DnsScheme::H3) => { - if let Some(h3_resolver) = h3_resolver.clone() { - builder = builder.resolver(h3_resolver); - } - } - EndpointDnsOp::Resolver(resolver) => { - builder = builder.resolver(resolver.clone()); - } - EndpointDnsOp::Publisher(_) => {} - } - } - - stun_resolver_chain(builder.build()) - } -} - -fn endpoint_resolver_chain(resolvers: Resolvers) -> Result { - if resolvers.iter().next().is_none() { - build_endpoint_dns_error::EmptyResolverSnafu.fail() - } else { - Ok(Arc::new(resolvers)) - } -} - -fn stun_resolver_chain(resolvers: Resolvers) -> Result { - if resolvers.iter().next().is_none() { - build_stun_dns_error::EmptyResolverSnafu.fail() - } else { - Ok(Arc::new(resolvers)) - } -} - -fn has_custom_resolver(operations: &[EndpointDnsOp]) -> bool { - operations - .iter() - .any(|operation| matches!(operation, EndpointDnsOp::Resolver(_))) -} - -fn has_system_dns(operations: &[EndpointDnsOp]) -> bool { - operations - .iter() - .any(|operation| matches!(operation, EndpointDnsOp::Dns(DnsScheme::System))) -} - -#[derive(Clone)] -pub(crate) struct ResolverPlan { - schemes: Vec, - custom: Option, -} - -impl ResolverPlan { - pub(crate) fn new(schemes: Vec, custom: Option) -> Self { - let schemes = if schemes.is_empty() && custom.is_none() { - vec![DnsScheme::H3, DnsScheme::Mdns, DnsScheme::System] - } else { - schemes - }; - Self { schemes, custom } - } - - pub(crate) fn select_resolver(&self, resolvers: Resolvers) -> ArcResolver { - if self.schemes.is_empty() - && let Some(custom) = self.custom.clone() - { - custom - } else { - Arc::new(resolvers) - } - } - - pub(crate) fn final_resolver(&self, resolvers: Resolvers) -> ArcResolver { - self.select_resolver(resolvers) - } -} +pub(crate) type DeferredStunResolver = DeferredResolver>; #[derive(Clone)] pub struct DhttpNetwork { network: Arc, - deferred_stun_resolver: Option>, + _deferred_stun_resolver: Option>, _stun_resolver: Option, } @@ -326,29 +28,18 @@ impl DhttpNetwork { &self.network } - pub(crate) fn finish_stun_resolver(&mut self, stun_resolver: ArcResolvers) { - let Some(deferred) = self.deferred_stun_resolver.clone() else { - return; - }; - deferred - .set(WeakResolver::new(Arc::downgrade(&stun_resolver))) - .expect("BUG: network STUN resolver is set exactly once"); + pub(crate) fn from_deferred_stun_resolver( + network: Arc, + deferred_stun_resolver: Arc, + stun_resolver: ArcResolvers, + ) -> Result { + deferred_stun_resolver.set(WeakResolver::new(Arc::downgrade(&stun_resolver)))?; let keepalive: ArcResolver = stun_resolver; - self._stun_resolver = Some(keepalive); - } - - pub(crate) fn endpoint_owned() -> Self { - let deferred = Arc::new(DeferredStunResolver::new()); - let stun_resolver: ArcResolver = deferred.clone(); - let network = Network::builder() - .maybe_stun_server(Some(Arc::::from(crate::endpoint::STUN_SERVER))) - .stun_resolver(stun_resolver) - .build(); - Self { + Ok(Self { network, - deferred_stun_resolver: Some(deferred), - _stun_resolver: None, - } + _deferred_stun_resolver: Some(deferred_stun_resolver), + _stun_resolver: Some(keepalive), + }) } } @@ -370,22 +61,22 @@ impl From> for DhttpNetwork { fn from(network: Arc) -> Self { Self { network, - deferred_stun_resolver: None, + _deferred_stun_resolver: None, _stun_resolver: None, } } } impl DhttpNetwork { - pub fn new( + pub async fn new( stun_server: Option>, stun_resolver: Option, devices: &'static Devices, - ) -> Self { + ) -> Result { let builder = Self::builder().stun_server(stun_server).devices(devices); match stun_resolver { - Some(stun_resolver) => builder.stun_resolver(stun_resolver).build(), - None => builder.build(), + Some(stun_resolver) => builder.stun_resolver(stun_resolver).build().await, + None => builder.build().await, } } } @@ -397,14 +88,16 @@ impl DhttpNetwork { builder_type(vis = "pub"), finish_fn = build )] - fn with_options( + async fn with_options( + #[builder(field)] dns_plan: DhttpDnsPlan, // Bon reserves `Option` to mean "setter omitted". The outer option // carries that builder state; the inner option is the actual STUN // server setting, where `None` disables STUN. stun_server: Option>>, - #[builder(setters(vis = "pub(crate)"))] dns_schemes: Option>, - resolver: Option, stun_resolver: Option, + #[builder(default = Arc::new(Vec::new()))] bind: Arc>, + #[builder(default = Arc::::from(crate::ddns::resolvers::DHTTP_H3_DNS_SERVER))] + h3_dns_server: Arc, #[builder(default = Devices::global())] devices: &'static Devices, #[builder(default = Arc::new(InterfaceManager::new()))] iface_manager: Arc< InterfaceManager, @@ -412,285 +105,107 @@ impl DhttpNetwork { #[builder(default = Arc::new(DEFAULT_IO_FACTORY))] io_factory: Arc, #[builder(default = Arc::new(QuicRouter::new()))] quic_router: Arc, #[builder(default = Arc::new(Locations::new()))] locations: Arc, - ) -> Self { + ) -> Result { let stun_server = stun_server.unwrap_or_else(|| Some(Arc::::from(crate::endpoint::STUN_SERVER))); - let plan = dns_schemes - .or_else(|| resolver.as_ref().map(|_| Vec::new())) - .map(|schemes| ResolverPlan::new(schemes, resolver)); - let (stun_resolver, deferred_stun_resolver, keepalive_resolver) = - match (stun_resolver, plan) { - (Some(stun_resolver), _) => (stun_resolver.clone(), None, Some(stun_resolver)), - (None, Some(plan)) if plan.schemes.is_empty() => { - let stun_resolver = plan.final_resolver(Resolvers::new()); - (stun_resolver.clone(), None, Some(stun_resolver)) - } - (None, Some(_plan)) => { - let deferred = Arc::new(DeferredStunResolver::new()); - let stun_resolver: ArcResolver = deferred.clone(); - (stun_resolver, Some(deferred), None) - } - (None, None) => { - let stun_resolver: ArcResolver = Arc::new(SystemResolver); - (stun_resolver.clone(), None, Some(stun_resolver)) - } - }; - - let network = Network::builder() - .maybe_stun_server(stun_server) - .stun_resolver(stun_resolver) - .devices(devices) - .iface_manager(iface_manager) - .io_factory(io_factory) - .quic_router(quic_router) - .locations(locations) - .build(); - DhttpNetwork { - network, - deferred_stun_resolver, - _stun_resolver: keepalive_resolver, + + if let Some(stun_resolver) = stun_resolver { + let network = Network::builder() + .maybe_stun_server(stun_server) + .stun_resolver(stun_resolver.clone()) + .devices(devices) + .iface_manager(iface_manager) + .io_factory(io_factory) + .quic_router(quic_router) + .locations(locations) + .build(); + return Ok(Self { + network, + _deferred_stun_resolver: None, + _stun_resolver: Some(stun_resolver), + }); } + + dhttp_network_builder_with_dns( + |stun_resolver| { + Network::builder() + .maybe_stun_server(stun_server) + .stun_resolver(stun_resolver) + .devices(devices) + .iface_manager(iface_manager) + .io_factory(io_factory) + .quic_router(quic_router) + .locations(locations) + .build() + }, + &dns_plan, + ) + .bind(bind) + .h3_dns_server(h3_dns_server) + .build() + .await + } +} + +impl DhttpNetworkWithOptionsBuilder { + /// Replace the DNS plan used to construct the STUN resolver. + pub fn dns_plan(mut self, dns_plan: DhttpDnsPlan) -> Self { + self.dns_plan = dns_plan; + self + } + + /// Add a built-in DNS scheme to the STUN DNS plan. + pub fn dns(mut self, scheme: DnsScheme) -> Self { + self.dns_plan.push_dns(scheme); + self + } + + /// Add a custom resolver to the STUN DNS plan. + pub fn resolver(mut self, resolver: ArcResolver) -> Self { + self.dns_plan.push_resolver(resolver); + self + } + + /// Add a custom publisher operation to the DNS plan. + /// + /// Network construction ignores publisher operations while still treating + /// the DNS plan as explicit, matching endpoint DNS plan semantics. + pub fn publisher(mut self, scope: PublishScope, publisher: ArcPublisher) -> Self { + self.dns_plan.push_publisher(scope, publisher); + self } } #[cfg(test)] mod tests { use super::*; - use crate::ddns::publishers::PublishScope; - use crate::dquic::resolver::{Publish, PublishFuture}; + use crate::dquic::resolver::Resolve; use futures::FutureExt; - use std::sync::atomic::{AtomicUsize, Ordering}; + use std::{ + fmt, + sync::atomic::{AtomicUsize, Ordering}, + }; #[derive(Debug)] struct CountingResolver { calls: Arc, } - impl std::fmt::Display for CountingResolver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + impl fmt::Display for CountingResolver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("counting resolver") } } impl Resolve for CountingResolver { - fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { - use futures::{FutureExt, StreamExt, stream}; - - self.calls.fetch_add(1, Ordering::SeqCst); - async move { Ok(stream::empty().boxed()) }.boxed() - } - } - - #[derive(Debug)] - struct CountingPublisher { - calls: Arc, - } - - impl std::fmt::Display for CountingPublisher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("counting publisher") - } - } - - impl Publish for CountingPublisher { - fn publish<'a>(&'a self, _name: &'a str, _packet: &'a [u8]) -> PublishFuture<'a> { - self.calls.fetch_add(1, Ordering::SeqCst); - async move { Ok(()) }.boxed() - } - } - - #[derive(Debug)] - struct NamedResolver(&'static str); - - impl std::fmt::Display for NamedResolver { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.0) - } - } - - impl Resolve for NamedResolver { fn lookup<'a>(&'a self, _name: &'a str) -> crate::dquic::resolver::ResolveFuture<'a> { use futures::{StreamExt, stream}; + self.calls.fetch_add(1, Ordering::SeqCst); async move { Ok(stream::empty().boxed()) }.boxed() } } - fn resolver_names(resolver: &ArcResolver) -> Vec { - let any: &dyn std::any::Any = resolver.as_ref(); - let resolvers = any - .downcast_ref::() - .expect("resolver should be a resolver chain"); - resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect() - } - - fn arc_resolvers_names(resolvers: &ArcResolvers) -> Vec { - resolvers - .iter() - .map(|resolver| resolver.to_string()) - .collect() - } - - fn publisher_names(publishers: &Publishers) -> Vec { - publishers - .iter() - .map(|publisher| publisher.to_string()) - .collect() - } - - #[tokio::test] - async fn endpoint_dns_plan_defaults_when_no_ops() { - let network = Network::builder().build(); - let plan = EndpointDnsPlan::new(); - let built = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect("default plan should build"); - - let names = resolver_names(&built.endpoint_resolver); - assert!( - names - .iter() - .any(|name| name.starts_with("DeferredResolver(")) - ); - assert!(names.iter().any(|name| name == "mDNS resolvers")); - assert!(names.iter().any(|name| name == "System DNS Resolver")); - - let publisher_names = publisher_names(&built.endpoint_publishers); - assert!( - publisher_names - .iter() - .any(|name| name.starts_with("DeferredResolver(")) - ); - assert!(publisher_names.iter().any(|name| name == "mDNS resolvers")); - } - - #[tokio::test] - async fn endpoint_dns_plan_custom_resolver_only_has_no_publishers() { - let network = Network::builder().build(); - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = EndpointDnsPlan::new().with_resolver(custom); - let built = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect("custom resolver plan should build"); - - assert_eq!( - resolver_names(&built.endpoint_resolver), - vec!["counting resolver"] - ); - assert!(built.endpoint_publishers.iter().next().is_none()); - } - - #[tokio::test] - async fn endpoint_dns_plan_custom_publisher_only_is_empty_resolver_error() { - let network = Network::builder().build(); - let publisher = Arc::new(CountingPublisher { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = EndpointDnsPlan::new().with_publisher(PublishScope::WideArea, publisher); - let error = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect_err("publisher-only plan should not build a resolver"); - - assert!(matches!(error, BuildEndpointDnsError::EmptyResolver)); - } - - #[tokio::test] - async fn endpoint_dns_plan_deduplicates_dns_schemes_but_not_custom_resolvers() { - let network = Network::builder().build(); - let calls = Arc::new(AtomicUsize::new(0)); - let first: ArcResolver = Arc::new(CountingResolver { - calls: calls.clone(), - }); - let second = first.clone(); - let plan = EndpointDnsPlan::new() - .with_dns(DnsScheme::System) - .with_resolver(first) - .with_dns(DnsScheme::System) - .with_resolver(second); - let built = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect("mixed plan should build"); - - assert_eq!( - resolver_names(&built.endpoint_resolver), - vec![ - "System DNS Resolver".to_string(), - "counting resolver".to_string(), - "counting resolver".to_string(), - ] - ); - } - - #[tokio::test] - async fn endpoint_dns_plan_h3_underlay_uses_custom_without_system_fallback() { - let network = Network::builder().build(); - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = EndpointDnsPlan::new() - .with_dns(DnsScheme::H3) - .with_resolver(custom); - let built = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect("h3 plus custom resolver should build"); - - assert_eq!( - resolver_names(&built.endpoint_h3_underlay), - vec!["counting resolver"] - ); - } - - #[tokio::test] - async fn endpoint_dns_plan_h3_underlay_adds_system_without_custom_or_system() { - let network = Network::builder().build(); - let plan = EndpointDnsPlan::new().with_dns(DnsScheme::H3); - let built = plan - .build_endpoint_dns(network, Arc::new(Vec::new())) - .await - .expect("h3-only plan should build"); - - assert_eq!( - resolver_names(&built.endpoint_h3_underlay), - vec!["System DNS Resolver"] - ); - } - - #[tokio::test] - async fn build_stun_dns_keeps_h3_position_and_custom_resolvers() { - let network = Network::builder().build(); - let h3_marker: ArcResolver = Arc::new(NamedResolver("stun h3 marker")); - let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), - }); - let plan = EndpointDnsPlan::new() - .with_dns(DnsScheme::H3) - .with_resolver(custom) - .with_dns(DnsScheme::System); - let stun_dns = plan - .build_stun_dns(Some(h3_marker), network, Arc::new(Vec::new())) - .await - .expect("stun dns should build"); - - assert_eq!( - arc_resolvers_names(&stun_dns), - vec![ - "stun h3 marker".to_string(), - "counting resolver".to_string(), - "System DNS Resolver".to_string(), - ] - ); - } - #[tokio::test] async fn from_arc_network_preserves_external_network() { let network = Network::builder().build(); @@ -702,7 +217,10 @@ mod tests { #[tokio::test] async fn builder_defaults_stun_server_to_dhttp_constant() { - let dhttp_network = DhttpNetwork::builder().build(); + let dhttp_network = DhttpNetwork::builder() + .build() + .await + .expect("default network should build"); assert_eq!( dhttp_network.network().quic().stun_server().as_deref(), @@ -712,7 +230,11 @@ mod tests { #[tokio::test] async fn builder_allows_disabling_stun_server() { - let dhttp_network = DhttpNetwork::builder().stun_server(None).build(); + let dhttp_network = DhttpNetwork::builder() + .stun_server(None) + .build() + .await + .expect("network should build with disabled stun server"); assert_eq!(dhttp_network.network().quic().stun_server(), None); } @@ -721,7 +243,9 @@ mod tests { async fn builder_allows_custom_stun_server() { let dhttp_network = DhttpNetwork::builder() .stun_server(Some(Arc::from("custom.stun.example:3478"))) - .build(); + .build() + .await + .expect("network should build with custom stun server"); assert_eq!( dhttp_network.network().quic().stun_server().as_deref(), @@ -746,7 +270,9 @@ mod tests { .stun_server(Some(Arc::from("builder.stun.example:3478"))) .quic_router(quic_router.clone()) .locations(locations.clone()) - .build(); + .build() + .await + .expect("network should build with forwarded options"); let quic = dhttp_network.network().quic(); assert!(Arc::ptr_eq(&quic.iface_manager(), &iface_manager)); @@ -769,7 +295,11 @@ mod tests { calls: calls.clone(), }); - let dhttp_network = DhttpNetwork::builder().resolver(resolver).build(); + let dhttp_network = DhttpNetwork::builder() + .resolver(resolver) + .build() + .await + .expect("network should build with custom dns resolver"); let mut records = dhttp_network .network() .quic() @@ -783,15 +313,39 @@ mod tests { } #[tokio::test] - async fn h3_only_network_stun_resolver_starts_deferred_without_system_final_resolver() { + async fn h3_only_network_stun_resolver_uses_h3_without_system_final_resolver() { let dhttp_network = DhttpNetwork::builder() - .dns_schemes(vec![DnsScheme::H3]) - .build(); + .dns(DnsScheme::H3) + .build() + .await + .expect("h3-only network should build"); let stun_resolver = dhttp_network.network().quic().stun_resolver(); let any: &dyn std::any::Any = stun_resolver.as_ref(); + let deferred = any + .downcast_ref::() + .expect("h3-only network stun resolver is deferred"); + let weak_resolver = deferred + .get() + .expect("deferred STUN resolver is initialized"); + let actual = weak_resolver + .upgrade() + .expect("DhttpNetwork keeps the STUN resolver target alive"); + let resolver_names = actual + .iter() + .map(|resolver| resolver.to_string()) + .collect::>(); - assert!(any.downcast_ref::().is_some()); + assert!( + resolver_names + .iter() + .any(|name| name.starts_with("H3 DNS Resolver(")) + ); + assert!( + !resolver_names + .iter() + .any(|name| name == "System DNS Resolver") + ); } #[tokio::test] @@ -801,7 +355,11 @@ mod tests { calls: calls.clone(), }); - let dhttp_network = DhttpNetwork::builder().resolver(custom).build(); + let dhttp_network = DhttpNetwork::builder() + .stun_resolver(custom) + .build() + .await + .expect("network should build with explicit stun resolver"); let resolver = dhttp_network.network().quic().stun_resolver(); assert_eq!(resolver.to_string(), "counting resolver"); From d88e99f46bb712828e15b8f71df98709d67e199c Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 05:59:49 +0800 Subject: [PATCH 11/12] feat: add endpoint from_parts --- dhttp/src/endpoint.rs | 137 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 14 deletions(-) diff --git a/dhttp/src/endpoint.rs b/dhttp/src/endpoint.rs index 6f6d449..6fe5997 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -40,20 +40,6 @@ pub struct Endpoint { publishers: crate::ddns::publishers::Publishers, } -impl TryFrom> for Endpoint { - type Error = InvalidEndpointIdentityError; - - fn try_from(inner: Arc) -> Result { - Self::validate_identity(inner.quic().identity().as_deref())?; - let network = DhttpNetwork::from(inner.quic().network().clone()); - Ok(Self { - inner, - network, - publishers: crate::ddns::publishers::Publishers::new(), - }) - } -} - #[derive(Debug, snafu::Snafu)] #[snafu(module(invalid_endpoint_identity_error))] pub enum InvalidEndpointIdentityError { @@ -67,6 +53,17 @@ pub enum InvalidEndpointIdentityError { }, } +#[derive(Debug, snafu::Snafu)] +#[snafu(module(invalid_endpoint_parts_error))] +pub enum InvalidEndpointPartsError { + #[snafu(display("invalid endpoint identity"))] + InvalidIdentity { + source: InvalidEndpointIdentityError, + }, + #[snafu(display("endpoint parts use different networks"))] + NetworkMismatch, +} + #[derive(Debug, snafu::Snafu)] #[snafu(module(build_endpoint_error))] pub enum BuildEndpointError { @@ -272,6 +269,25 @@ pub enum ConnectError { } impl Endpoint { + pub fn from_parts( + h3: Arc, + publishers: crate::ddns::publishers::Publishers, + network: DhttpNetwork, + ) -> Result { + Self::validate_identity(h3.quic().identity().as_deref()) + .context(invalid_endpoint_parts_error::InvalidIdentitySnafu)?; + + if !Arc::ptr_eq(h3.quic().network(), network.network()) { + return invalid_endpoint_parts_error::NetworkMismatchSnafu.fail(); + } + + Ok(Self { + inner: h3, + network, + publishers, + }) + } + fn validate_identity(identity: Option<&Identity>) -> Result<(), InvalidEndpointIdentityError> { if let Some(identity) = identity { Self::name_from_identity(identity)?; @@ -684,6 +700,99 @@ mod tests { )); } + #[tokio::test] + async fn from_parts_preserves_matching_parts() { + let network = DhttpNetwork::builder() + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("network should build"); + let quic = QuicEndpoint::builder() + .network(network.network().clone()) + .resolver(Arc::new(MarkerResolver)) + .build() + .await; + let h3 = Arc::new(H3Endpoint::new(quic)); + let publisher: Arc = + Arc::new(CountingPublisher { + calls: Arc::new(AtomicUsize::new(0)), + }); + let publishers = crate::ddns::publishers::Publishers::new().with( + crate::ddns::publishers::Publisher::new( + crate::ddns::publishers::PublishScope::WideArea, + publisher, + ), + ); + + let endpoint = Endpoint::from_parts(h3.clone(), publishers, network.clone()) + .expect("matching endpoint parts should be accepted"); + + assert!(Arc::ptr_eq(&endpoint.as_h3(), &h3)); + assert!(Arc::ptr_eq(endpoint.network().network(), network.network())); + assert_eq!(endpoint.dns_publishers().iter().count(), 1); + } + + #[tokio::test] + async fn from_parts_rejects_network_mismatch() { + let endpoint_network = DhttpNetwork::builder() + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("endpoint network should build"); + let supplied_network = DhttpNetwork::builder() + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("supplied network should build"); + let quic = QuicEndpoint::builder() + .network(endpoint_network.network().clone()) + .resolver(Arc::new(MarkerResolver)) + .build() + .await; + let h3 = Arc::new(H3Endpoint::new(quic)); + + let error = match Endpoint::from_parts( + h3, + crate::ddns::publishers::Publishers::new(), + supplied_network, + ) { + Ok(_) => panic!("mismatched endpoint parts should be rejected"), + Err(error) => error, + }; + + assert!(matches!(error, InvalidEndpointPartsError::NetworkMismatch)); + } + + #[tokio::test] + async fn from_parts_rejects_invalid_identity() { + let network = DhttpNetwork::builder() + .resolver(Arc::new(MarkerResolver)) + .build() + .await + .expect("network should build"); + let identity = valid_dhttp_identity("example.com"); + let quic = QuicEndpoint::builder() + .network(network.network().clone()) + .identity(Arc::new(identity)) + .resolver(Arc::new(MarkerResolver)) + .build() + .await; + let h3 = Arc::new(H3Endpoint::new(quic)); + + let error = + match Endpoint::from_parts(h3, crate::ddns::publishers::Publishers::new(), network) { + Ok(_) => panic!("invalid identity should be rejected"), + Err(error) => error, + }; + + assert!(matches!( + error, + InvalidEndpointPartsError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidName { .. } + } + )); + } + #[test] fn endpoint_implements_quic_connect() { fn assert_connect() {} From 2e870efad4f969b9cfe696ca8e91035a02135b21 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 06:12:10 +0800 Subject: [PATCH 12/12] release: converge upstream registry dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f18dfd4..223aeeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ chrono = { version = "0.4", features = ["serde"] } # release dependencies resolve through crates.io in the formal release graph. dhttp-identity = "0.2.0" dhttp-home = { path = "home", version = "0.2.0" } -ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "dev/v0.4.0", version = "0.4.0", features = [ +ddns = { package = "dyns", version = "0.4.0", features = [ "resolvers", "publishers", "h3", @@ -48,7 +48,7 @@ ddns = { package = "dyns", git = "https://github.com/genmeta/ddns.git", branch = "mdns", "dquic-network", ] } -h3x = { git = "https://github.com/genmeta/h3x.git", branch = "dev/v0.4.0", version = "0.4.0", features = [ +h3x = { version = "0.4.0", features = [ "dquic", ] } dhttp = { path = "dhttp", version = "0.2.0" }