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_NAME_SERVICE: &str = crate::bootstrap::DHTTP_NAME_SERVICE;
67
68pub const DHTTP_BOOTSTRAP_URL: &str = crate::bootstrap::DHTTP_BOOTSTRAP_URL;
70
71pub 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}