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_H3_DNS_SERVER: &str = crate::bootstrap::DHTTP_H3_DNS_SERVER;
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: &str = crate::bootstrap::DHTTP_MDNS_SERVICE;
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 = Arc::new(MdnsResolvers::bind(network, patterns, DHTTP_MDNS_SERVICE).await);
252        self.resolvers.push_candidate_resolver(mdns);
253        self
254    }
255
256    #[cfg(feature = "h3")]
257    pub fn h3<C>(
258        self,
259        endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
260    ) -> io::Result<Self>
261    where
262        C: h3x::quic::Connect + h3x::quic::WithLocalAuthority + Send + Sync + 'static,
263        C::Error: Send + Sync + 'static,
264        C::Connection: Send + 'static,
265    {
266        self.h3_with_base_url(DHTTP_H3_DNS_SERVER, endpoint)
267    }
268
269    #[cfg(feature = "h3")]
270    pub fn h3_with_base_url<C>(
271        mut self,
272        base_url: impl AsRef<str>,
273        endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
274    ) -> io::Result<Self>
275    where
276        C: h3x::quic::Connect + h3x::quic::WithLocalAuthority + Send + Sync + 'static,
277        C::Error: Send + Sync + 'static,
278        C::Connection: Send + 'static,
279    {
280        let resolver = Arc::new(H3Resolver::from_endpoint(base_url, endpoint)?);
281        self.resolvers.push_candidate_resolver(resolver);
282        Ok(self)
283    }
284
285    #[cfg(feature = "http")]
286    pub fn http(self) -> io::Result<Self> {
287        self.http_with_base_url(DHTTP_BOOTSTRAP_URL)
288    }
289
290    #[cfg(feature = "http")]
291    pub fn http_with_base_url(mut self, base_url: impl AsRef<str>) -> io::Result<Self> {
292        let resolver = Arc::new(HttpResolver::new(base_url.as_ref())?);
293        self.resolvers.push_candidate_resolver(resolver);
294        Ok(self)
295    }
296
297    pub fn system(mut self) -> Self {
298        self.resolvers
299            .push(Arc::new(dquic::qresolve::SystemResolver));
300        self
301    }
302
303    pub fn build(self) -> Resolvers {
304        self.resolvers
305    }
306}
307
308#[cfg(feature = "resolvers")]
309impl Resolvers {
310    pub fn builder() -> ResolversBuilder {
311        ResolversBuilder::default()
312    }
313
314    pub fn new() -> Self {
315        Self::default()
316    }
317
318    pub fn with(mut self, resolver: ArcResolver) -> Self {
319        self.push(resolver);
320        self
321    }
322
323    pub fn with_candidate_resolver<R>(mut self, resolver: Arc<R>) -> Self
324    where
325        R: crate::resolvers::endpoint_candidates::ResolveEndpointCandidates + Send + Sync + 'static,
326    {
327        self.push_candidate_resolver(resolver);
328        self
329    }
330
331    pub fn push(&mut self, resolver: ArcResolver) {
332        self.resolvers.push(ResolverEntry {
333            resolver,
334            endpoint_candidates: None,
335        });
336    }
337
338    pub fn push_candidate_resolver<R>(&mut self, resolver: Arc<R>)
339    where
340        R: crate::resolvers::endpoint_candidates::ResolveEndpointCandidates + Send + Sync + 'static,
341    {
342        let endpoint_candidates =
343            Some(resolver.clone()
344                as crate::resolvers::endpoint_candidates::ArcEndpointCandidateResolver);
345        let resolver = resolver as ArcResolver;
346        self.resolvers.push(ResolverEntry {
347            resolver,
348            endpoint_candidates,
349        });
350    }
351
352    pub fn iter(&self) -> impl Iterator<Item = &ArcResolver> {
353        self.resolvers.iter().map(|entry| &entry.resolver)
354    }
355
356    pub async fn lookup_endpoint_candidates(
357        &self,
358        name: &str,
359        lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
360    ) -> Result<crate::resolvers::endpoint_candidates::EndpointCandidates, ResolversError> {
361        let mut errors = vec![];
362        let mut groups =
363            Vec::<crate::resolvers::endpoint_candidates::EndpointCandidateGroup>::new();
364
365        for entry in self.resolvers.clone() {
366            let Some(candidate_resolver) = entry.endpoint_candidates else {
367                errors.push((
368                    entry.resolver.to_string(),
369                    io::Error::other("resolver does not support endpoint candidate lookup"),
370                ));
371                continue;
372            };
373
374            match candidate_resolver
375                .lookup_endpoint_candidates(name, lookup)
376                .await
377            {
378                Ok(candidates) => {
379                    for mut group in candidates.groups {
380                        if let Some(existing) = groups
381                            .iter_mut()
382                            .find(|existing| existing.chain == group.chain)
383                        {
384                            for endpoint in group.endpoints.drain(..) {
385                                if !existing.endpoints.contains(&endpoint) {
386                                    existing.endpoints.push(endpoint);
387                                }
388                            }
389                            for source in group.sources.drain(..) {
390                                if !existing.sources.contains(&source) {
391                                    existing.sources.push(source);
392                                }
393                            }
394                        } else {
395                            groups.push(group);
396                        }
397                    }
398                }
399                Err(error) => errors.push((entry.resolver.to_string(), error)),
400            }
401        }
402
403        if groups.is_empty() && !errors.is_empty() {
404            return Err(ResolversError { errors });
405        }
406
407        Ok(crate::resolvers::endpoint_candidates::EndpointCandidates { groups })
408    }
409
410    pub async fn lookup(
411        &self,
412        hostname: &str,
413        servname: &str,
414        family: Option<Family>,
415    ) -> Result<impl Stream<Item = (Source, EndpointAddr)> + use<>, ResolversError> {
416        let mut errors = vec![];
417
418        let mut lookups = stream::FuturesUnordered::from_iter(
419            (self.resolvers.clone().into_iter()).map(|entry| {
420                let resolver = entry.resolver.clone();
421                let hostname = hostname.to_owned();
422                let servname = servname.to_owned();
423                async move {
424                    (
425                        resolver.lookup(&hostname, &servname, family).await,
426                        resolver.clone(),
427                    )
428                }
429            }),
430        );
431
432        let endpoints = loop {
433            match lookups.next().await {
434                Some((Ok(endpoints), _)) => break endpoints,
435                Some((Err(error), resolver)) => errors.push((resolver.to_string(), error)),
436                None => return Err(ResolversError { errors }),
437            }
438        };
439
440        Ok(endpoints.chain(lookups.flat_map(|(endpoints, _)| stream::iter(endpoints).flatten())))
441    }
442}
443
444#[cfg(feature = "resolvers")]
445impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for Resolvers {
446    fn lookup_endpoint_candidates<'a>(
447        &'a self,
448        name: &'a str,
449        lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
450    ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
451        async move {
452            Resolvers::lookup_endpoint_candidates(self, name, lookup)
453                .await
454                .map_err(io::Error::other)
455        }
456        .boxed()
457    }
458}
459
460#[cfg(feature = "resolvers")]
461impl Resolve for Resolvers {
462    fn lookup<'l>(
463        &'l self,
464        hostname: &'l str,
465        servname: &'l str,
466        family: Option<Family>,
467    ) -> ResolveFuture<'l> {
468        Resolvers::lookup(self, hostname, servname, family)
469            .map_ok(StreamExt::boxed)
470            .map_err(io::Error::other)
471            .boxed()
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
478    use std::str::FromStr;
479    #[cfg(feature = "resolvers")]
480    use std::{error::Error as StdError, fmt, io, sync::Arc};
481
482    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
483    use super::MdnsResolvers;
484    #[cfg(feature = "resolvers")]
485    use super::Resolvers;
486    use super::{DHTTP_BOOTSTRAP_URL, DHTTP_H3_DNS_SERVER, DHTTP_MDNS_SERVICE, resolvable_name};
487    #[cfg(feature = "resolvers")]
488    use super::{DnsScheme, ResolversError};
489
490    #[cfg(feature = "resolvers")]
491    #[derive(Debug)]
492    struct TestSourceError {
493        message: &'static str,
494        source: Option<Box<TestSourceError>>,
495    }
496
497    #[cfg(feature = "resolvers")]
498    impl TestSourceError {
499        fn leaf(message: &'static str) -> Self {
500            Self {
501                message,
502                source: None,
503            }
504        }
505
506        fn with_source(message: &'static str, source: TestSourceError) -> Self {
507            Self {
508                message,
509                source: Some(Box::new(source)),
510            }
511        }
512    }
513
514    #[cfg(feature = "resolvers")]
515    impl fmt::Display for TestSourceError {
516        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517            f.write_str(self.message)
518        }
519    }
520
521    #[cfg(feature = "resolvers")]
522    impl StdError for TestSourceError {
523        fn source(&self) -> Option<&(dyn StdError + 'static)> {
524            self.source
525                .as_deref()
526                .map(|source| source as &(dyn StdError + 'static))
527        }
528    }
529
530    #[cfg(feature = "resolvers")]
531    fn other_error(message: &'static str) -> io::Error {
532        io::Error::other(message)
533    }
534
535    #[cfg(feature = "resolvers")]
536    fn chained_other_error(root: TestSourceError) -> io::Error {
537        io::Error::other(root)
538    }
539
540    #[test]
541    fn resolver_defaults_come_from_compile_time_environment() {
542        if let Some(expected) = option_env!("DHTTP_H3_DNS_SERVER") {
543            assert_eq!(DHTTP_H3_DNS_SERVER, expected);
544        }
545        if let Some(expected) = option_env!("DHTTP_BOOTSTRAP_URL") {
546            assert_eq!(DHTTP_BOOTSTRAP_URL, expected);
547        }
548        if let Some(expected) = option_env!("DHTTP_MDNS_SERVICE") {
549            assert_eq!(DHTTP_MDNS_SERVICE, expected);
550        }
551    }
552
553    #[test]
554    fn resolvable_name_accepts_dns_name_with_numeric_port() {
555        assert_eq!(
556            resolvable_name("example.dhttp.net:443"),
557            Some("example.dhttp.net")
558        );
559    }
560
561    #[test]
562    fn resolvable_name_accepts_stun_authority_with_numeric_port() {
563        assert_eq!(
564            resolvable_name("nat.genmeta.net:20004"),
565            Some("nat.genmeta.net")
566        );
567    }
568
569    #[test]
570    fn resolvable_name_rejects_ip_literals() {
571        assert_eq!(resolvable_name("127.0.0.1:443"), None);
572        assert_eq!(resolvable_name("[::1]:443"), None);
573    }
574
575    #[test]
576    fn endpoint_lookup_name_and_sequence_accepts_plain_name() {
577        let (name, sequence) =
578            super::endpoint_lookup_name_and_sequence("example.dhttp.net").expect("dns name");
579
580        assert_eq!(name, "example.dhttp.net");
581        assert_eq!(sequence, None);
582    }
583
584    #[test]
585    fn endpoint_lookup_name_and_sequence_parses_numeric_selector() {
586        let (name, sequence) =
587            super::endpoint_lookup_name_and_sequence("reimu.hakurei.dhttp.net:1")
588                .expect("dns name");
589
590        assert_eq!(name, "reimu.hakurei.dhttp.net");
591        assert_eq!(
592            sequence.map(dhttp_identity::certificate::CertificateSequence::get),
593            Some(1)
594        );
595    }
596
597    #[test]
598    fn endpoint_lookup_name_and_sequence_rejects_out_of_range_selector() {
599        let invalid = format!("example.dhttp.net:{}", (1u64 << 62) + 1);
600
601        assert_eq!(super::endpoint_lookup_name_and_sequence(&invalid), None);
602    }
603
604    #[cfg(feature = "resolvers")]
605    #[derive(Debug)]
606    struct CandidateResolver {
607        label: &'static str,
608        sequence: u8,
609    }
610
611    #[cfg(feature = "resolvers")]
612    impl fmt::Display for CandidateResolver {
613        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614            f.write_str(self.label)
615        }
616    }
617
618    #[cfg(feature = "resolvers")]
619    impl dquic::qresolve::Resolve for CandidateResolver {
620        fn lookup<'l>(
621            &'l self,
622            _hostname: &'l str,
623            _servname: &'l str,
624            _family: Option<dquic::qresolve::Family>,
625        ) -> dquic::qresolve::ResolveFuture<'l> {
626            use futures::{FutureExt, StreamExt, stream};
627            async { Ok(stream::empty().boxed()) }.boxed()
628        }
629    }
630
631    #[cfg(feature = "resolvers")]
632    impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for CandidateResolver {
633        fn lookup_endpoint_candidates<'a>(
634            &'a self,
635            _name: &'a str,
636            _lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
637        ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
638            use dhttp_identity::certificate::CertificateSequence;
639            use dquic::qresolve::Source;
640            use futures::FutureExt;
641
642            let sequence = self.sequence;
643            async move {
644                Ok(crate::resolvers::endpoint_candidates::EndpointCandidates {
645                    groups: vec![
646                        crate::resolvers::endpoint_candidates::EndpointCandidateGroup {
647                            chain: crate::core::certificate::primary_chain_key(
648                                CertificateSequence::from(sequence),
649                            ),
650                            endpoints: Vec::new(),
651                            sources: vec![Source::Dht],
652                        },
653                    ],
654                })
655            }
656            .boxed()
657        }
658    }
659
660    #[cfg(feature = "resolvers")]
661    #[tokio::test]
662    async fn aggregate_endpoint_candidates_preserve_resolver_order() {
663        let resolvers = Resolvers::new()
664            .with_candidate_resolver(Arc::new(CandidateResolver {
665                label: "a",
666                sequence: 1,
667            }))
668            .with_candidate_resolver(Arc::new(CandidateResolver {
669                label: "b",
670                sequence: 0,
671            }));
672
673        let candidates = resolvers
674            .lookup_endpoint_candidates(
675                "demo.dhttp.net",
676                crate::resolvers::endpoint_candidates::EndpointLookup::default(),
677            )
678            .await
679            .expect("candidate lookup succeeds");
680
681        assert_eq!(candidates.groups.len(), 2);
682        assert_eq!(candidates.groups[0].chain.usage().kind_flag(), "0");
683        assert_eq!(candidates.groups[0].chain.sequence().get(), 1);
684        assert_eq!(candidates.groups[1].chain.usage().kind_flag(), "0");
685        assert_eq!(candidates.groups[1].chain.sequence().get(), 0);
686    }
687
688    #[cfg(feature = "resolvers")]
689    #[derive(Debug)]
690    struct CandidateSetResolver {
691        label: &'static str,
692        groups: Vec<(u8, &'static str, dquic::qresolve::Source)>,
693    }
694
695    #[cfg(feature = "resolvers")]
696    impl fmt::Display for CandidateSetResolver {
697        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
698            f.write_str(self.label)
699        }
700    }
701
702    #[cfg(feature = "resolvers")]
703    impl dquic::qresolve::Resolve for CandidateSetResolver {
704        fn lookup<'l>(
705            &'l self,
706            _hostname: &'l str,
707            _servname: &'l str,
708            _family: Option<dquic::qresolve::Family>,
709        ) -> dquic::qresolve::ResolveFuture<'l> {
710            use futures::{FutureExt, StreamExt, stream};
711            async { Ok(stream::empty().boxed()) }.boxed()
712        }
713    }
714
715    #[cfg(feature = "resolvers")]
716    impl crate::resolvers::endpoint_candidates::ResolveEndpointCandidates for CandidateSetResolver {
717        fn lookup_endpoint_candidates<'a>(
718            &'a self,
719            _name: &'a str,
720            _lookup: crate::resolvers::endpoint_candidates::EndpointLookup,
721        ) -> crate::resolvers::endpoint_candidates::EndpointCandidateFuture<'a> {
722            use dhttp_identity::certificate::CertificateSequence;
723            use futures::FutureExt;
724
725            let groups = self
726                .groups
727                .iter()
728                .map(|(sequence, endpoint, source)| {
729                    crate::resolvers::endpoint_candidates::EndpointCandidateGroup {
730                        chain: crate::core::certificate::primary_chain_key(
731                            CertificateSequence::from(*sequence),
732                        ),
733                        endpoints: vec![dquic::qbase::net::addr::EndpointAddr::direct(
734                            endpoint.parse().unwrap(),
735                        )],
736                        sources: vec![source.clone()],
737                    }
738                })
739                .collect();
740            async move { Ok(crate::resolvers::endpoint_candidates::EndpointCandidates { groups }) }
741                .boxed()
742        }
743    }
744
745    #[cfg(feature = "resolvers")]
746    #[tokio::test]
747    async fn aggregate_endpoint_candidates_merge_duplicate_sequences_stably() {
748        use dquic::qresolve::Source;
749
750        let resolvers = Resolvers::new()
751            .with_candidate_resolver(Arc::new(CandidateSetResolver {
752                label: "a",
753                groups: vec![
754                    (2, "192.0.2.20:4433", Source::System),
755                    (1, "192.0.2.10:4433", Source::System),
756                ],
757            }))
758            .with_candidate_resolver(Arc::new(CandidateSetResolver {
759                label: "b",
760                groups: vec![
761                    (2, "192.0.2.21:4433", Source::Dht),
762                    (3, "192.0.2.30:4433", Source::Dht),
763                ],
764            }));
765
766        let candidates = resolvers
767            .lookup_endpoint_candidates(
768                "demo.dhttp.net",
769                crate::resolvers::endpoint_candidates::EndpointLookup::all(),
770            )
771            .await
772            .expect("candidate lookup succeeds");
773
774        let sequences = candidates
775            .groups
776            .iter()
777            .map(|group| group.chain.sequence().get())
778            .collect::<Vec<_>>();
779        assert_eq!(sequences, vec![2, 1, 3]);
780        assert_eq!(candidates.groups[0].endpoints.len(), 2);
781        assert_eq!(
782            candidates.groups[0].sources,
783            vec![Source::System, Source::Dht]
784        );
785    }
786
787    #[cfg(feature = "resolvers")]
788    #[test]
789    fn dns_scheme_round_trips_supported_schemes_and_rejects_dht() {
790        let cases = [
791            ("mdns", DnsScheme::Mdns),
792            ("http", DnsScheme::Http),
793            ("h3", DnsScheme::H3),
794            ("system", DnsScheme::System),
795        ];
796
797        for (text, scheme) in cases {
798            assert_eq!(DnsScheme::from_str(text).expect("supported scheme"), scheme);
799            assert_eq!(scheme.to_string(), text);
800        }
801
802        assert!(DnsScheme::from_str("dht").is_err());
803    }
804
805    #[cfg(feature = "resolvers")]
806    #[test]
807    fn resolvers_error_renders_no_resolvers_available_when_empty() {
808        let error = ResolversError { errors: vec![] };
809
810        assert_eq!(error.to_string(), "no DNS resolvers available");
811    }
812
813    #[cfg(feature = "resolvers")]
814    #[test]
815    fn resolvers_error_renders_resolver_bullets_in_stored_order() {
816        let error = ResolversError {
817            errors: vec![
818                (
819                    "System DNS Resolver".to_string(),
820                    other_error("invalid socket address"),
821                ),
822                ("mDNS resolvers".to_string(), other_error("timed out")),
823            ],
824        };
825
826        assert_eq!(
827            error.to_string(),
828            concat!(
829                "all DNS resolvers failed\n",
830                "  - System DNS Resolver: invalid socket address\n",
831                "  - mDNS resolvers: timed out"
832            )
833        );
834    }
835
836    #[cfg(feature = "resolvers")]
837    #[test]
838    fn resolvers_error_renders_numbered_source_chain_for_one_resolver() {
839        let error = ResolversError {
840            errors: vec![(
841                "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
842                chained_other_error(TestSourceError::with_source(
843                    "deferred resolver lookup failed",
844                    TestSourceError::leaf("no DNS record found"),
845                )),
846            )],
847        };
848
849        assert_eq!(
850            error.to_string(),
851            concat!(
852                "all DNS resolvers failed\n",
853                "  - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
854                "    1. no DNS record found"
855            )
856        );
857    }
858
859    #[cfg(feature = "resolvers")]
860    #[test]
861    fn resolvers_error_renders_repeated_source_messages_without_deduplication() {
862        let error = ResolversError {
863            errors: vec![(
864                "DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
865                chained_other_error(TestSourceError::with_source(
866                    "deferred resolver lookup failed",
867                    TestSourceError::with_source(
868                        "deferred resolver lookup failed",
869                        TestSourceError::leaf("no DNS record found"),
870                    ),
871                )),
872            )],
873        };
874
875        assert_eq!(
876            error.to_string(),
877            concat!(
878                "all DNS resolvers failed\n",
879                "  - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
880                "    1. deferred resolver lookup failed\n",
881                "    2. no DNS record found"
882            )
883        );
884    }
885
886    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
887    #[tokio::test]
888    async fn resolvers_builder_can_enable_mdns() {
889        use std::sync::Arc;
890
891        use h3x::dquic::{Network, binds::BindPattern};
892
893        let network = Network::builder().build();
894        let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
895
896        let resolvers = Resolvers::builder()
897            .mdns(network, Arc::new(vec![pattern]))
898            .await
899            .build();
900
901        assert!(resolvers.to_string().contains("mDNS resolvers"));
902    }
903
904    #[cfg(all(feature = "h3", feature = "resolvers", feature = "dquic-network"))]
905    #[tokio::test]
906    async fn resolvers_builder_accepts_custom_h3_base_url() {
907        use std::sync::Arc;
908
909        let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new(
910            h3x::dquic::QuicEndpoint::builder().build().await,
911        ));
912
913        let resolvers = Resolvers::builder()
914            .h3_with_base_url("https://custom-dns.example:4433", endpoint)
915            .expect("valid h3 dns url")
916            .build();
917
918        assert!(resolvers.to_string().contains("custom-dns.example"));
919    }
920
921    #[cfg(all(feature = "http", feature = "resolvers"))]
922    #[test]
923    fn resolvers_builder_accepts_custom_http_base_url() {
924        let resolvers = Resolvers::builder()
925            .http_with_base_url("https://custom-dns.example")
926            .expect("valid http dns url")
927            .build();
928
929        assert!(resolvers.to_string().contains("custom-dns.example"));
930    }
931
932    #[cfg(all(feature = "mdns", feature = "dquic-network", feature = "resolvers"))]
933    #[tokio::test]
934    async fn mdns_resolvers_bind_installs_mdns_on_null_io_binding() {
935        use std::sync::Arc;
936
937        use dquic::qinterface::io::IO;
938        use h3x::dquic::{Network, binds::BindPattern};
939
940        let network = Network::builder().build();
941        let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
942        let resolvers = MdnsResolvers::bind(
943            network.clone(),
944            Arc::new(vec![pattern.clone()]),
945            DHTTP_MDNS_SERVICE,
946        )
947        .await;
948
949        let ifaces = resolvers
950            .bound_interfaces(&pattern)
951            .expect("bound interfaces");
952        if ifaces.is_empty() {
953            return;
954        }
955        assert!(ifaces[0].borrow().bound_addr().is_err());
956        assert!(
957            ifaces[0]
958                .with_components(|components, _| components.exist::<crate::mdns::service::Mdns>())
959        );
960    }
961}