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
27pub(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
65pub const DHTTP_H3_DNS_SERVER: &str = crate::bootstrap::DHTTP_H3_DNS_SERVER;
67
68pub const DHTTP_BOOTSTRAP_URL: &str = crate::bootstrap::DHTTP_BOOTSTRAP_URL;
70
71pub 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}