Skip to main content

ddns/
resolvers.rs

1#[cfg(feature = "resolvers")]
2use std::{
3    error::Error,
4    fmt::{self, Display},
5    sync::Arc,
6};
7
8#[cfg(feature = "resolvers")]
9use dquic::{
10    qbase::net::addr::EndpointAddr,
11    qresolve::{Family, Resolve, ResolveFuture, Source},
12};
13#[cfg(feature = "resolvers")]
14use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream};
15#[cfg(feature = "resolvers")]
16use tokio::io;
17
18#[cfg(feature = "h3")]
19pub use crate::h3::H3Resolver;
20#[cfg(feature = "http")]
21pub use crate::http::HttpResolver;
22#[cfg(feature = "mdns")]
23pub use crate::mdns::MdnsResolver;
24#[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
25use crate::mdns::MdnsResolvers;
26
27/// Extract and validate the DNS host from `name`, which may include a `:port`
28/// suffix. Returns `Some(host)` if the host part is a valid RFC-compliant DNS
29/// name, or `None` for raw IP addresses, bracketed IPv6, or malformed input.
30pub(crate) fn resolvable_name(name: &str) -> Option<&str> {
31    let host = match name.rsplit_once(':') {
32        Some((h, port)) if !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) => h,
33        _ => name,
34    };
35    rustls::pki_types::DnsName::try_from(host).ok()?;
36    Some(host)
37}
38
39#[cfg_attr(
40    not(any(feature = "h3", feature = "http", feature = "mdns")),
41    allow(dead_code)
42)]
43pub(crate) fn endpoint_lookup_name_and_sequence(
44    name: &str,
45) -> Option<(
46    &str,
47    Option<dhttp_identity::certificate::CertificateSequence>,
48)> {
49    use dhttp_identity::certificate::CertificateSequence;
50
51    let (host, sequence) = match name.rsplit_once(':') {
52        Some((host, digits))
53            if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) =>
54        {
55            let sequence = digits.parse::<u64>().ok()?;
56            let sequence = CertificateSequence::try_from(sequence).ok()?;
57            (host, Some(sequence))
58        }
59        _ => (name, None),
60    };
61
62    Some((resolvable_name(host)?, sequence))
63}
64
65/// Default DNS-over-H3 server for DHTTP endpoints.
66pub const DHTTP_NAME_SERVICE: &str = crate::bootstrap::DHTTP_NAME_SERVICE;
67
68/// Default bootstrap service URL for DHTTP endpoints.
69pub const DHTTP_BOOTSTRAP_URL: &str = crate::bootstrap::DHTTP_BOOTSTRAP_URL;
70
71/// mDNS service type used by DHTTP endpoints.
72pub const DHTTP_MDNS_SERVICE_DOMAIN: &str = crate::bootstrap::DHTTP_MDNS_SERVICE_DOMAIN;
73
74#[cfg(feature = "resolvers")]
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub enum DnsScheme {
77    Mdns,
78    Http,
79    H3,
80    System,
81}
82
83#[cfg(feature = "resolvers")]
84impl Display for DnsScheme {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(match self {
87            Self::Mdns => "mdns",
88            Self::Http => "http",
89            Self::H3 => "h3",
90            Self::System => "system",
91        })
92    }
93}
94
95#[cfg(feature = "resolvers")]
96#[derive(Debug, snafu::Snafu)]
97#[snafu(display("unsupported dns scheme {scheme}"))]
98pub struct ParseDnsSchemeError {
99    scheme: String,
100}
101
102#[cfg(feature = "resolvers")]
103impl std::str::FromStr for DnsScheme {
104    type Err = ParseDnsSchemeError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        match s {
108            "mdns" => Ok(Self::Mdns),
109            "http" => Ok(Self::Http),
110            "h3" => Ok(Self::H3),
111            "system" => Ok(Self::System),
112            scheme => Err(ParseDnsSchemeError {
113                scheme: scheme.to_owned(),
114            }),
115        }
116    }
117}
118
119pub mod deferred;
120pub mod endpoint_candidates;
121#[cfg(any(feature = "mdns", test))]
122pub(crate) mod endpoint_group;
123pub mod weak;
124
125#[cfg(feature = "resolvers")]
126type ArcResolver = Arc<dyn Resolve + Send + Sync + 'static>;
127
128#[cfg(feature = "resolvers")]
129#[derive(Clone)]
130struct ResolverEntry {
131    resolver: ArcResolver,
132    endpoint_candidates:
133        Option<crate::resolvers::endpoint_candidates::ArcEndpointCandidateResolver>,
134}
135
136#[cfg(feature = "resolvers")]
137impl fmt::Debug for ResolverEntry {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.debug_struct("ResolverEntry")
140            .field("resolver", &self.resolver.to_string())
141            .field(
142                "supports_endpoint_candidates",
143                &self.endpoint_candidates.is_some(),
144            )
145            .finish()
146    }
147}
148
149#[cfg(feature = "resolvers")]
150#[derive(Default, Clone, Debug)]
151pub struct Resolvers {
152    resolvers: Vec<ResolverEntry>,
153}
154
155#[cfg(feature = "resolvers")]
156impl Display for Resolvers {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str("Resolvers(")?;
159        if self.resolvers.is_empty() {
160            f.write_str("empty")?;
161        } else {
162            for (i, entry) in self.resolvers.iter().enumerate() {
163                if i > 0 {
164                    f.write_str(", ")?;
165                }
166                fmt::Display::fmt(entry.resolver.as_ref(), f)?;
167            }
168        }
169        f.write_str(")")
170    }
171}
172
173#[cfg(feature = "resolvers")]
174#[derive(Debug)]
175pub struct ResolversError {
176    errors: Vec<(String, io::Error)>,
177}
178
179#[cfg(feature = "resolvers")]
180fn format_dns_error_sources(
181    f: &mut fmt::Formatter<'_>,
182    error: &(dyn Error + 'static),
183) -> fmt::Result {
184    let mut index = 1;
185    let mut current = error.source();
186
187    while let Some(source) = current {
188        write!(f, "\n    {index}. {source}")?;
189        index += 1;
190        current = source.source();
191    }
192
193    Ok(())
194}
195
196#[cfg(feature = "resolvers")]
197fn format_dns_error_entry(
198    f: &mut fmt::Formatter<'_>,
199    resolver: &str,
200    error: &io::Error,
201) -> fmt::Result {
202    write!(f, "\n  - {resolver}: {error}")?;
203    format_dns_error_sources(f, error)
204}
205
206#[cfg(feature = "resolvers")]
207impl fmt::Display for ResolversError {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        if self.errors.is_empty() {
210            return write!(f, "no DNS resolvers available");
211        }
212
213        write!(f, "all DNS resolvers failed")?;
214        for (resolver, error) in &self.errors {
215            format_dns_error_entry(f, resolver, error)?;
216        }
217        Ok(())
218    }
219}
220
221#[cfg(feature = "resolvers")]
222impl Error for ResolversError {}
223
224#[cfg(feature = "resolvers")]
225#[derive(Default)]
226pub struct ResolversBuilder {
227    resolvers: Resolvers,
228}
229
230#[cfg(feature = "resolvers")]
231impl ResolversBuilder {
232    pub fn resolver(mut self, resolver: ArcResolver) -> Self {
233        self.resolvers.push(resolver);
234        self
235    }
236
237    pub fn candidate_resolver<R>(mut self, resolver: Arc<R>) -> Self
238    where
239        R: crate::resolvers::endpoint_candidates::ResolveEndpointCandidates + Send + Sync + 'static,
240    {
241        self.resolvers.push_candidate_resolver(resolver);
242        self
243    }
244
245    #[cfg(all(feature = "mdns", feature = "dquic-network"))]
246    pub async fn mdns(
247        mut self,
248        network: Arc<h3x::dquic::Network>,
249        patterns: Arc<Vec<h3x::dquic::binds::BindPattern>>,
250    ) -> Self {
251        let mdns =
252            Arc::new(MdnsResolvers::bind(network, patterns, DHTTP_MDNS_SERVICE_DOMAIN).await);
253        self.resolvers.push_candidate_resolver(mdns);
254        self
255    }
256
257    #[cfg(feature = "h3")]
258    pub fn h3<C>(
259        self,
260        endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
261    ) -> io::Result<Self>
262    where
263        C: h3x::quic::Connect + h3x::quic::WithLocalAuthority + Send + Sync + 'static,
264        C::Error: Send + Sync + 'static,
265        C::Connection: Send + 'static,
266    {
267        self.h3_with_base_url(DHTTP_NAME_SERVICE, endpoint)
268    }
269
270    #[cfg(feature = "h3")]
271    pub fn h3_with_base_url<C>(
272        mut self,
273        base_url: impl AsRef<str>,
274        endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
275    ) -> io::Result<Self>
276    where
277        C: h3x::quic::Connect + h3x::quic::WithLocalAuthority + Send + Sync + 'static,
278        C::Error: Send + Sync + 'static,
279        C::Connection: Send + 'static,
280    {
281        let resolver = Arc::new(H3Resolver::from_endpoint(base_url, endpoint)?);
282        self.resolvers.push_candidate_resolver(resolver);
283        Ok(self)
284    }
285
286    #[cfg(feature = "http")]
287    pub fn http(self) -> io::Result<Self> {
288        self.http_with_base_url(DHTTP_BOOTSTRAP_URL)
289    }
290
291    #[cfg(feature = "http")]
292    pub fn http_with_base_url(mut self, base_url: impl AsRef<str>) -> io::Result<Self> {
293        let resolver = Arc::new(HttpResolver::new(base_url.as_ref())?);
294        self.resolvers.push_candidate_resolver(resolver);
295        Ok(self)
296    }
297
298    pub fn system(mut self) -> Self {
299        self.resolvers
300            .push(Arc::new(dquic::qresolve::SystemResolver));
301        self
302    }
303
304    pub fn build(self) -> Resolvers {
305        self.resolvers
306    }
307}
308
309#[cfg(feature = "resolvers")]
310impl Resolvers {
311    pub fn builder() -> ResolversBuilder {
312        ResolversBuilder::default()
313    }
314
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    pub fn with(mut self, resolver: ArcResolver) -> Self {
320        self.push(resolver);
321        self
322    }
323
324    pub fn with_candidate_resolver<R>(mut self, resolver: Arc<R>) -> Self
325    where
326        R: crate::resolvers::endpoint_candidates::ResolveEndpointCandidates + Send + Sync + 'static,
327    {
328        self.push_candidate_resolver(resolver);
329        self
330    }
331
332    pub fn push(&mut self, resolver: ArcResolver) {
333        self.resolvers.push(ResolverEntry {
334            resolver,
335            endpoint_candidates: None,
336        });
337    }
338
339    pub fn push_candidate_resolver<R>(&mut self, resolver: Arc<R>)
340    where
341        R: crate::resolvers::endpoint_candidates::ResolveEndpointCandidates + Send + Sync + 'static,
342    {
343        let endpoint_candidates =
344            Some(resolver.clone()
345                as crate::resolvers::endpoint_candidates::ArcEndpointCandidateResolver);
346        let resolver = resolver as ArcResolver;
347        self.resolvers.push(ResolverEntry {
348            resolver,
349            endpoint_candidates,
350        });
351    }
352
353    pub fn iter(&self) -> impl Iterator<Item = &ArcResolver> {
354        self.resolvers.iter().map(|entry| &entry.resolver)
355    }
356
357    pub async fn lookup_endpoint_candidates(
358        &self,
359        name: &str,
360        lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
361    ) -> Result<crate::resolvers::endpoint_candidates::EndpointCandidates, ResolversError> {
362        let mut errors = vec![];
363        let mut groups =
364            Vec::<crate::resolvers::endpoint_candidates::EndpointCandidateGroup>::new();
365
366        for entry in self.resolvers.clone() {
367            let Some(candidate_resolver) = entry.endpoint_candidates else {
368                errors.push((
369                    entry.resolver.to_string(),
370                    io::Error::other("resolver does not support endpoint candidate lookup"),
371                ));
372                continue;
373            };
374
375            match candidate_resolver
376                .lookup_endpoint_candidates(name, lookup)
377                .await
378            {
379                Ok(candidates) => {
380                    for mut group in candidates.groups {
381                        if let Some(existing) = groups
382                            .iter_mut()
383                            .find(|existing| existing.chain == group.chain)
384                        {
385                            for endpoint in group.endpoints.drain(..) {
386                                if !existing.endpoints.contains(&endpoint) {
387                                    existing.endpoints.push(endpoint);
388                                }
389                            }
390                            for source in group.sources.drain(..) {
391                                if !existing.sources.contains(&source) {
392                                    existing.sources.push(source);
393                                }
394                            }
395                        } else {
396                            groups.push(group);
397                        }
398                    }
399                }
400                Err(error) => errors.push((entry.resolver.to_string(), error)),
401            }
402        }
403
404        if groups.is_empty() && !errors.is_empty() {
405            return Err(ResolversError { errors });
406        }
407
408        Ok(crate::resolvers::endpoint_candidates::EndpointCandidates { groups })
409    }
410
411    pub async fn lookup(
412        &self,
413        hostname: &str,
414        servname: &str,
415        family: Option<Family>,
416    ) -> Result<impl Stream<Item = (Source, EndpointAddr)> + use<>, ResolversError> {
417        let mut errors = vec![];
418
419        let mut lookups = stream::FuturesUnordered::from_iter(
420            (self.resolvers.clone().into_iter()).map(|entry| {
421                let resolver = entry.resolver.clone();
422                let hostname = hostname.to_owned();
423                let servname = servname.to_owned();
424                async move {
425                    (
426                        resolver.lookup(&hostname, &servname, family).await,
427                        resolver.clone(),
428                    )
429                }
430            }),
431        );
432
433        let endpoints = loop {
434            match lookups.next().await {
435                Some((Ok(endpoints), _)) => break endpoints,
436                Some((Err(error), resolver)) => errors.push((resolver.to_string(), error)),
437                None => return Err(ResolversError { errors }),
438            }
439        };
440
441        Ok(endpoints.chain(lookups.flat_map(|(endpoints, _)| stream::iter(endpoints).flatten())))
442    }
443}
444
445#[cfg(feature = "resolvers")]
446impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for Resolvers {
447    fn lookup_endpoint_candidates<'a>(
448        &'a self,
449        name: &'a str,
450        lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
451    ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
452        async move {
453            Resolvers::lookup_endpoint_candidates(self, name, lookup)
454                .await
455                .map_err(io::Error::other)
456        }
457        .boxed()
458    }
459}
460
461#[cfg(feature = "resolvers")]
462impl Resolve for Resolvers {
463    fn lookup<'l>(
464        &'l self,
465        hostname: &'l str,
466        servname: &'l str,
467        family: Option<Family>,
468    ) -> ResolveFuture<'l> {
469        Resolvers::lookup(self, hostname, servname, family)
470            .map_ok(StreamExt::boxed)
471            .map_err(io::Error::other)
472            .boxed()
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
479    use std::str::FromStr;
480    #[cfg(feature = "resolvers")]
481    use std::{error::Error as StdError, fmt, io, sync::Arc};
482
483    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
484    use super::MdnsResolvers;
485    #[cfg(feature = "resolvers")]
486    use super::Resolvers;
487    use super::{
488        DHTTP_BOOTSTRAP_URL, DHTTP_MDNS_SERVICE_DOMAIN, DHTTP_NAME_SERVICE, resolvable_name,
489    };
490    #[cfg(feature = "resolvers")]
491    use super::{DnsScheme, ResolversError};
492
493    #[cfg(feature = "resolvers")]
494    #[derive(Debug)]
495    struct TestSourceError {
496        message: &'static str,
497        source: Option<Box<TestSourceError>>,
498    }
499
500    #[cfg(feature = "resolvers")]
501    impl TestSourceError {
502        fn leaf(message: &'static str) -> Self {
503            Self {
504                message,
505                source: None,
506            }
507        }
508
509        fn with_source(message: &'static str, source: TestSourceError) -> Self {
510            Self {
511                message,
512                source: Some(Box::new(source)),
513            }
514        }
515    }
516
517    #[cfg(feature = "resolvers")]
518    impl fmt::Display for TestSourceError {
519        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520            f.write_str(self.message)
521        }
522    }
523
524    #[cfg(feature = "resolvers")]
525    impl StdError for TestSourceError {
526        fn source(&self) -> Option<&(dyn StdError + 'static)> {
527            self.source
528                .as_deref()
529                .map(|source| source as &(dyn StdError + 'static))
530        }
531    }
532
533    #[cfg(feature = "resolvers")]
534    fn other_error(message: &'static str) -> io::Error {
535        io::Error::other(message)
536    }
537
538    #[cfg(feature = "resolvers")]
539    fn chained_other_error(root: TestSourceError) -> io::Error {
540        io::Error::other(root)
541    }
542
543    #[test]
544    fn resolver_defaults_come_from_compile_time_environment() {
545        if let Some(expected) = option_env!("DHTTP_NAME_SERVICE") {
546            assert_eq!(DHTTP_NAME_SERVICE, expected);
547        }
548        if let Some(expected) = option_env!("DHTTP_BOOTSTRAP_URL") {
549            assert_eq!(DHTTP_BOOTSTRAP_URL, expected);
550        }
551        if let Some(expected) = option_env!("DHTTP_MDNS_SERVICE_DOMAIN") {
552            assert_eq!(DHTTP_MDNS_SERVICE_DOMAIN, expected);
553        }
554    }
555
556    #[test]
557    fn resolvable_name_accepts_dns_name_with_numeric_port() {
558        assert_eq!(
559            resolvable_name("example.dhttp.net:443"),
560            Some("example.dhttp.net")
561        );
562    }
563
564    #[test]
565    fn resolvable_name_accepts_stun_authority_with_numeric_port() {
566        assert_eq!(
567            resolvable_name("nat.genmeta.net:20004"),
568            Some("nat.genmeta.net")
569        );
570    }
571
572    #[test]
573    fn resolvable_name_rejects_ip_literals() {
574        assert_eq!(resolvable_name("127.0.0.1:443"), None);
575        assert_eq!(resolvable_name("[::1]:443"), None);
576    }
577
578    #[test]
579    fn endpoint_lookup_name_and_sequence_accepts_plain_name() {
580        let (name, sequence) =
581            super::endpoint_lookup_name_and_sequence("example.dhttp.net").expect("dns name");
582
583        assert_eq!(name, "example.dhttp.net");
584        assert_eq!(sequence, None);
585    }
586
587    #[test]
588    fn endpoint_lookup_name_and_sequence_parses_numeric_selector() {
589        let (name, sequence) =
590            super::endpoint_lookup_name_and_sequence("reimu.hakurei.dhttp.net:1")
591                .expect("dns name");
592
593        assert_eq!(name, "reimu.hakurei.dhttp.net");
594        assert_eq!(
595            sequence.map(dhttp_identity::certificate::CertificateSequence::get),
596            Some(1)
597        );
598    }
599
600    #[test]
601    fn endpoint_lookup_name_and_sequence_rejects_out_of_range_selector() {
602        let invalid = format!("example.dhttp.net:{}", (1u64 << 62) + 1);
603
604        assert_eq!(super::endpoint_lookup_name_and_sequence(&invalid), None);
605    }
606
607    #[cfg(feature = "resolvers")]
608    #[derive(Debug)]
609    struct CandidateResolver {
610        label: &'static str,
611        sequence: u8,
612    }
613
614    #[cfg(feature = "resolvers")]
615    impl fmt::Display for CandidateResolver {
616        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617            f.write_str(self.label)
618        }
619    }
620
621    #[cfg(feature = "resolvers")]
622    impl dquic::qresolve::Resolve for CandidateResolver {
623        fn lookup<'l>(
624            &'l self,
625            _hostname: &'l str,
626            _servname: &'l str,
627            _family: Option<dquic::qresolve::Family>,
628        ) -> dquic::qresolve::ResolveFuture<'l> {
629            use futures::{FutureExt, StreamExt, stream};
630            async { Ok(stream::empty().boxed()) }.boxed()
631        }
632    }
633
634    #[cfg(feature = "resolvers")]
635    impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for CandidateResolver {
636        fn lookup_endpoint_candidates<'a>(
637            &'a self,
638            _name: &'a str,
639            _lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
640        ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
641            use dhttp_identity::certificate::CertificateSequence;
642            use dquic::qresolve::Source;
643            use futures::FutureExt;
644
645            let sequence = self.sequence;
646            async move {
647                Ok(crate::resolvers::endpoint_candidates::EndpointCandidates {
648                    groups: vec![
649                        crate::resolvers::endpoint_candidates::EndpointCandidateGroup {
650                            chain: crate::core::certificate::primary_chain_key(
651                                CertificateSequence::from(sequence),
652                            ),
653                            endpoints: Vec::new(),
654                            sources: vec![Source::Dht],
655                        },
656                    ],
657                })
658            }
659            .boxed()
660        }
661    }
662
663    #[cfg(feature = "resolvers")]
664    #[tokio::test]
665    async fn aggregate_endpoint_candidates_preserve_resolver_order() {
666        let resolvers = Resolvers::new()
667            .with_candidate_resolver(Arc::new(CandidateResolver {
668                label: "a",
669                sequence: 1,
670            }))
671            .with_candidate_resolver(Arc::new(CandidateResolver {
672                label: "b",
673                sequence: 0,
674            }));
675
676        let candidates = resolvers
677            .lookup_endpoint_candidates(
678                "demo.dhttp.net",
679                crate::resolvers::endpoint_candidates::EndpointLookup::default(),
680            )
681            .await
682            .expect("candidate lookup succeeds");
683
684        assert_eq!(candidates.groups.len(), 2);
685        assert_eq!(candidates.groups[0].chain.usage().kind_flag(), "0");
686        assert_eq!(candidates.groups[0].chain.sequence().get(), 1);
687        assert_eq!(candidates.groups[1].chain.usage().kind_flag(), "0");
688        assert_eq!(candidates.groups[1].chain.sequence().get(), 0);
689    }
690
691    #[cfg(feature = "resolvers")]
692    #[derive(Debug)]
693    struct CandidateSetResolver {
694        label: &'static str,
695        groups: Vec<(u8, &'static str, dquic::qresolve::Source)>,
696    }
697
698    #[cfg(feature = "resolvers")]
699    impl fmt::Display for CandidateSetResolver {
700        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
701            f.write_str(self.label)
702        }
703    }
704
705    #[cfg(feature = "resolvers")]
706    impl dquic::qresolve::Resolve for CandidateSetResolver {
707        fn lookup<'l>(
708            &'l self,
709            _hostname: &'l str,
710            _servname: &'l str,
711            _family: Option<dquic::qresolve::Family>,
712        ) -> dquic::qresolve::ResolveFuture<'l> {
713            use futures::{FutureExt, StreamExt, stream};
714            async { Ok(stream::empty().boxed()) }.boxed()
715        }
716    }
717
718    #[cfg(feature = "resolvers")]
719    impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for CandidateSetResolver {
720        fn lookup_endpoint_candidates<'a>(
721            &'a self,
722            _name: &'a str,
723            _lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
724        ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
725            use dhttp_identity::certificate::CertificateSequence;
726            use futures::FutureExt;
727
728            let groups = self
729                .groups
730                .iter()
731                .map(|(sequence, endpoint, source)| {
732                    crate::resolvers::endpoint_candidates::EndpointCandidateGroup {
733                        chain: crate::core::certificate::primary_chain_key(
734                            CertificateSequence::from(*sequence),
735                        ),
736                        endpoints: vec![dquic::qbase::net::addr::EndpointAddr::direct(
737                            endpoint.parse().unwrap(),
738                        )],
739                        sources: vec![source.clone()],
740                    }
741                })
742                .collect();
743            async move { Ok(crate::resolvers::endpoint_candidates::EndpointCandidates { groups }) }
744                .boxed()
745        }
746    }
747
748    #[cfg(feature = "resolvers")]
749    #[tokio::test]
750    async fn aggregate_endpoint_candidates_merge_duplicate_sequences_stably() {
751        use dquic::qresolve::Source;
752
753        let resolvers = Resolvers::new()
754            .with_candidate_resolver(Arc::new(CandidateSetResolver {
755                label: "a",
756                groups: vec![
757                    (2, "192.0.2.20:4433", Source::System),
758                    (1, "192.0.2.10:4433", Source::System),
759                ],
760            }))
761            .with_candidate_resolver(Arc::new(CandidateSetResolver {
762                label: "b",
763                groups: vec![
764                    (2, "192.0.2.21:4433", Source::Dht),
765                    (3, "192.0.2.30:4433", Source::Dht),
766                ],
767            }));
768
769        let candidates = resolvers
770            .lookup_endpoint_candidates(
771                "demo.dhttp.net",
772                crate::resolvers::endpoint_candidates::EndpointLookup::all(),
773            )
774            .await
775            .expect("candidate lookup succeeds");
776
777        let sequences = candidates
778            .groups
779            .iter()
780            .map(|group| group.chain.sequence().get())
781            .collect::<Vec<_>>();
782        assert_eq!(sequences, vec![2, 1, 3]);
783        assert_eq!(candidates.groups[0].endpoints.len(), 2);
784        assert_eq!(
785            candidates.groups[0].sources,
786            vec![Source::System, Source::Dht]
787        );
788    }
789
790    #[cfg(feature = "resolvers")]
791    #[test]
792    fn dns_scheme_round_trips_supported_schemes_and_rejects_dht() {
793        let cases = [
794            ("mdns", DnsScheme::Mdns),
795            ("http", DnsScheme::Http),
796            ("h3", DnsScheme::H3),
797            ("system", DnsScheme::System),
798        ];
799
800        for (text, scheme) in cases {
801            assert_eq!(DnsScheme::from_str(text).expect("supported scheme"), scheme);
802            assert_eq!(scheme.to_string(), text);
803        }
804
805        assert!(DnsScheme::from_str("dht").is_err());
806    }
807
808    #[cfg(feature = "resolvers")]
809    #[test]
810    fn resolvers_error_renders_no_resolvers_available_when_empty() {
811        let error = ResolversError { errors: vec![] };
812
813        assert_eq!(error.to_string(), "no DNS resolvers available");
814    }
815
816    #[cfg(feature = "resolvers")]
817    #[test]
818    fn resolvers_error_renders_resolver_bullets_in_stored_order() {
819        let error = ResolversError {
820            errors: vec![
821                (
822                    "System DNS Resolver".to_string(),
823                    other_error("invalid socket address"),
824                ),
825                ("mDNS resolvers".to_string(), other_error("timed out")),
826            ],
827        };
828
829        assert_eq!(
830            error.to_string(),
831            concat!(
832                "all DNS resolvers failed\n",
833                "  - System DNS Resolver: invalid socket address\n",
834                "  - mDNS resolvers: timed out"
835            )
836        );
837    }
838
839    #[cfg(feature = "resolvers")]
840    #[test]
841    fn resolvers_error_renders_numbered_source_chain_for_one_resolver() {
842        let error = ResolversError {
843            errors: vec![(
844                "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
845                chained_other_error(TestSourceError::with_source(
846                    "deferred resolver lookup failed",
847                    TestSourceError::leaf("no DNS record found"),
848                )),
849            )],
850        };
851
852        assert_eq!(
853            error.to_string(),
854            concat!(
855                "all DNS resolvers failed\n",
856                "  - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
857                "    1. no DNS record found"
858            )
859        );
860    }
861
862    #[cfg(feature = "resolvers")]
863    #[test]
864    fn resolvers_error_renders_repeated_source_messages_without_deduplication() {
865        let error = ResolversError {
866            errors: vec![(
867                "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
868                chained_other_error(TestSourceError::with_source(
869                    "deferred resolver lookup failed",
870                    TestSourceError::with_source(
871                        "deferred resolver lookup failed",
872                        TestSourceError::leaf("no DNS record found"),
873                    ),
874                )),
875            )],
876        };
877
878        assert_eq!(
879            error.to_string(),
880            concat!(
881                "all DNS resolvers failed\n",
882                "  - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
883                "    1. deferred resolver lookup failed\n",
884                "    2. no DNS record found"
885            )
886        );
887    }
888
889    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
890    #[tokio::test]
891    async fn resolvers_builder_can_enable_mdns() {
892        use std::sync::Arc;
893
894        use h3x::dquic::{Network, binds::BindPattern};
895
896        let network = Network::builder().build();
897        let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
898
899        let resolvers = Resolvers::builder()
900            .mdns(network, Arc::new(vec![pattern]))
901            .await
902            .build();
903
904        assert!(resolvers.to_string().contains("mDNS resolvers"));
905    }
906
907    #[cfg(all(feature = "h3", feature = "resolvers", feature = "dquic-network"))]
908    #[tokio::test]
909    async fn resolvers_builder_accepts_custom_h3_base_url() {
910        use std::sync::Arc;
911
912        let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new(
913            h3x::dquic::QuicEndpoint::builder().build().await,
914        ));
915
916        let resolvers = Resolvers::builder()
917            .h3_with_base_url("https://custom-dns.example:4433", endpoint)
918            .expect("valid h3 dns url")
919            .build();
920
921        assert!(resolvers.to_string().contains("custom-dns.example"));
922    }
923
924    #[cfg(all(feature = "http", feature = "resolvers"))]
925    #[test]
926    fn resolvers_builder_accepts_custom_http_base_url() {
927        let resolvers = Resolvers::builder()
928            .http_with_base_url("https://custom-dns.example")
929            .expect("valid http dns url")
930            .build();
931
932        assert!(resolvers.to_string().contains("custom-dns.example"));
933    }
934
935    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
936    #[tokio::test]
937    async fn mdns_resolvers_bind_installs_mdns_on_null_io_binding() {
938        use std::sync::Arc;
939
940        use dquic::qinterface::io::IO;
941        use h3x::dquic::{Network, binds::BindPattern};
942
943        let network = Network::builder().build();
944        let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
945        let resolvers = MdnsResolvers::bind(
946            network.clone(),
947            Arc::new(vec![pattern.clone()]),
948            DHTTP_MDNS_SERVICE_DOMAIN,
949        )
950        .await;
951
952        let ifaces = resolvers
953            .bound_interfaces(&pattern)
954            .expect("bound interfaces");
955        if ifaces.is_empty() {
956            return;
957        }
958        assert!(ifaces[0].borrow().bound_addr().is_err());
959        assert!(
960            ifaces[0]
961                .with_components(|components, _| components.exist::<crate::mdns::service::Mdns>())
962        );
963    }
964}