diff --git a/Cargo.toml b/Cargo.toml index d495a20..223aeeb 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,15 +38,18 @@ 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 = [ - "h3x-resolver", - "mdns-resolver", - "http-resolver", +dhttp-identity = "0.2.0" +dhttp-home = { path = "home", version = "0.2.0" } +ddns = { package = "dyns", version = "0.4.0", features = [ + "resolvers", + "publishers", + "h3", + "http", + "mdns", + "dquic-network", ] } -h3x = { version = "0.3.1", features = [ +h3x = { 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/", diff --git a/dhttp/src/ddns.rs b/dhttp/src/ddns.rs index 9344718..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, publisher, 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 a8cb877..6fe5997 100644 --- a/dhttp/src/endpoint.rs +++ b/dhttp/src/endpoint.rs @@ -2,10 +2,11 @@ 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, +use crate::ddns::{ + BuildDhttpNetworkWithDnsError, BuildQuicEndpointWithDnsError, DhttpDnsPlan, + resolvers::DnsScheme, }; use crate::dquic::{ Identity, QuicEndpoint, binds::BindPattern, client::ClientQuicConfig, @@ -22,7 +23,7 @@ pub mod client; pub mod server; use self::client::Request; -use crate::network::{ArcResolver, DhttpNetwork, ResolverPlan}; +use crate::network::DhttpNetwork; /// A DHttp endpoint bound to a QUIC connection. /// @@ -36,16 +37,7 @@ use crate::network::{ArcResolver, DhttpNetwork, ResolverPlan}; pub struct Endpoint { inner: Arc, network: DhttpNetwork, -} - -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, } #[derive(Debug, snafu::Snafu)] @@ -61,6 +53,41 @@ 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 { + #[snafu(display("invalid endpoint identity"))] + InvalidIdentity { + source: InvalidEndpointIdentityError, + }, + #[snafu(display("failed to build endpoint dns"))] + EndpointDns { + source: BuildQuicEndpointWithDnsError, + }, + #[snafu(display("failed to build stun dns"))] + StunDns { + source: BuildDhttpNetworkWithDnsError, + }, +} + +#[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,33 +104,23 @@ fn normalize_bind(bind: Arc>) -> Arc> { } } -type DeferredH3Resolver = DeferredResolver>; - -fn deferred_h3_resolver() -> Arc { - 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) - .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. /// - /// 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_schemes: Vec, + #[builder(field)] dns_plan: DhttpDnsPlan, identity: Option>, network: Option, @@ -111,74 +128,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) - } + 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 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_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()) - .await; - let quic_resolver = resolver_plan.final_resolver(endpoint_resolvers); - - let quic = QuicEndpoint::builder() - .network(raw_network.clone()) - .maybe_identity(identity) - .resolver(quic_resolver) - .client(client.clone()) - .server(server) - .bind(bind.clone()) - .build() - .await; - - if let Some(endpoint_h3_deferred) = endpoint_h3_deferred { - let mut dns_quic = quic.clone(); - dns_quic.set_resolver(resolver_without_h3.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_quic = QuicEndpoint::builder() - .network(raw_network) - .resolver(resolver_without_h3) - .client(client) - .bind(bind.clone()) - .build() - .await; - Some(h3_resolver_arc_from_quic(stun_quic)) - } else { - None - }; - network.finish_stun_resolver(stun_h3_resolver, bind).await; - } + 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) @@ -187,13 +176,44 @@ impl Endpoint { Ok(Self { inner: Arc::new(h3), network, + publishers, }) } } 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_schemes.push(scheme); + 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, + publisher: Arc, + ) -> Self { + self.dns_plan.push_publisher(scope, publisher); self } } @@ -219,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)] @@ -236,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)] @@ -253,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)?; @@ -303,31 +338,42 @@ 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, + Option< + 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)?; + if self.publishers.iter().next().is_none() { + return Ok(None); + } + 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(Some(crate::ddns::publishers::EndpointPublicationLoop::new( + name, + self.publishers.clone(), + source, + ))) } /// Load an endpoint from a domain name. @@ -492,19 +538,24 @@ 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); } }; 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, } } } @@ -536,7 +587,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, }; @@ -597,7 +648,9 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidName { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidName { .. } + } )); } @@ -618,7 +671,9 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + } )); } @@ -639,7 +694,102 @@ mod tests { assert!(matches!( error, - InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + BuildEndpointError::InvalidIdentity { + source: InvalidEndpointIdentityError::InvalidCertificateMetadata { .. } + } + )); + } + + #[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 { .. } + } )); } @@ -711,6 +861,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; @@ -727,6 +896,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, @@ -747,6 +938,104 @@ 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(); + 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() + } + + #[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 { .. } | BuildEndpointError::StunDns { .. } + )); + } + + #[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 = @@ -757,26 +1046,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()); - } - - #[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"]); + assert_eq!(endpoint_resolver_names(&endpoint), vec!["marker resolver"]); } #[tokio::test] @@ -829,7 +1100,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 44bac8b..70d3c9f 100644 --- a/dhttp/src/network.rs +++ b/dhttp/src/network.rs @@ -1,109 +1,24 @@ use std::{ops::Deref, sync::Arc}; -use crate::ddns::resolvers::{ - DnsScheme, Resolvers, deferred::DeferredResolver, weak::WeakResolver, +use crate::ddns::{ + ArcPublisher, ArcResolver, BuildDhttpNetworkWithDnsError, DhttpDnsPlan, + dhttp_network_builder_with_dns, + publishers::PublishScope, + resolvers::{DnsScheme, Resolvers, deferred::DeferredResolver, weak::WeakResolver}, }; use crate::dquic::{ Network, binds::BindPattern, net::{Devices, InterfaceManager, Locations, ProductIO, QuicRouter, handy::DEFAULT_IO_FACTORY}, - resolver::{Resolve, handy::SystemResolver}, }; -pub(crate) type DynResolver = dyn Resolve + Send + Sync; -pub(crate) type ArcResolver = Arc; +pub(crate) type ArcResolvers = Arc; pub(crate) type DeferredStunResolver = DeferredResolver>; -#[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 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) 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) 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, + _deferred_stun_resolver: Option>, _stun_resolver: Option, } @@ -113,26 +28,18 @@ 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 { - 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); + 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; + Ok(Self { + network, + _deferred_stun_resolver: Some(deferred_stun_resolver), + _stun_resolver: Some(keepalive), + }) } } @@ -154,23 +61,22 @@ impl From> for DhttpNetwork { fn from(network: Arc) -> Self { Self { network, - deferred_stun_resolver: None, - stun_resolver_plan: 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, } } } @@ -182,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, @@ -197,69 +105,101 @@ 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, stun_resolver_plan, keepalive_resolver) = - match (stun_resolver, plan) { - (Some(stun_resolver), _) => { - (stun_resolver.clone(), None, 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)) - } - (None, Some(plan)) => { - let deferred = Arc::new(DeferredStunResolver::new()); - let stun_resolver: ArcResolver = deferred.clone(); - (stun_resolver, Some(deferred), Some(plan), None) - } - (None, None) => { - let stun_resolver: ArcResolver = Arc::new(SystemResolver); - (stun_resolver.clone(), None, 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_plan, - _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 std::sync::atomic::{AtomicUsize, Ordering}; + use crate::dquic::resolver::Resolve; + use futures::FutureExt; + 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}; + use futures::{StreamExt, stream}; self.calls.fetch_add(1, Ordering::SeqCst); async move { Ok(stream::empty().boxed()) }.boxed() @@ -277,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(), @@ -287,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); } @@ -296,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(), @@ -321,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)); @@ -344,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() @@ -358,142 +313,54 @@ 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(); - - assert!(any.downcast_ref::().is_some()); - } - - #[tokio::test] - async fn explicit_custom_network_stun_resolver_is_not_augmented_with_system() { - let calls = Arc::new(AtomicUsize::new(0)); - let custom: ArcResolver = Arc::new(CountingResolver { - calls: calls.clone(), - }); - - let dhttp_network = DhttpNetwork::builder().resolver(custom).build(); - let resolver = dhttp_network.network().quic().stun_resolver(); - - 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 + 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!(names.iter().any(|name| name == "System DNS Resolver")); - assert!(names.iter().any(|name| name == "counting resolver")); assert!( - !names + resolver_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 + !resolver_names .iter() - .any(|name| name.starts_with("H3 DNS Resolver(")) + .any(|name| name == "System 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() { + #[tokio::test] + async fn explicit_custom_network_stun_resolver_is_not_augmented_with_system() { + let calls = Arc::new(AtomicUsize::new(0)); let custom: ArcResolver = Arc::new(CountingResolver { - calls: Arc::new(AtomicUsize::new(0)), + calls: calls.clone(), }); - let plan = ResolverPlan::new(Vec::new(), Some(custom)); - let resolver = plan.resolver_without_h3(Resolvers::new()); + 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"); }