use std::{
error::Error,
fmt::{self, Debug, Display},
sync::Arc,
};
use dquic::{
qbase::net::addr::EndpointAddr,
qresolve::{Resolve, ResolveFuture, Source},
};
use futures::{FutureExt, Stream, StreamExt, TryFutureExt, stream};
use tokio::io;
#[cfg(feature = "h3x-resolver")]
pub mod h3;
#[cfg(feature = "http-resolver")]
pub mod http;
#[cfg(feature = "http-resolver")]
use http::HttpResolver;
#[cfg(feature = "mdns-resolver")]
use crate::mdns::resolvers::mdns::MdnsResolvers;
#[cfg_attr(
not(any(feature = "h3x-resolver", feature = "http-resolver")),
allow(dead_code)
)]
pub(crate) fn resolvable_name(name: &str) -> Option<&str> {
let host = match name.rsplit_once(':') {
Some((h, port)) if !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) => h,
_ => name,
};
rustls::pki_types::DnsName::try_from(host).ok()?;
Some(host)
}
pub const DHTTP_H3_DNS_SERVER: &str = crate::bootstrap::DHTTP_H3_DNS_SERVER;
pub const DHTTP_HTTP_DNS_SERVER: &str = crate::bootstrap::DHTTP_HTTP_DNS_SERVER;
pub const DHTTP_MDNS_SERVICE: &str = crate::bootstrap::DHTTP_MDNS_SERVICE;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum DnsScheme {
Mdns,
Http,
H3,
System,
}
impl Display for DnsScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Mdns => "mdns",
Self::Http => "http",
Self::H3 => "h3",
Self::System => "system",
})
}
}
#[derive(Debug, snafu::Snafu)]
#[snafu(display("unsupported dns scheme {scheme}"))]
pub struct ParseDnsSchemeError {
scheme: String,
}
impl std::str::FromStr for DnsScheme {
type Err = ParseDnsSchemeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mdns" => Ok(Self::Mdns),
"http" => Ok(Self::Http),
"h3" => Ok(Self::H3),
"system" => Ok(Self::System),
scheme => Err(ParseDnsSchemeError {
scheme: scheme.to_owned(),
}),
}
}
}
pub mod deferred;
pub(crate) mod selector;
pub mod weak;
type ArcResolver = Arc<dyn Resolve + Send + Sync + 'static>;
#[derive(Default, Clone, Debug)]
pub struct Resolvers {
resolvers: Vec<ArcResolver>,
}
impl Display for Resolvers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Resolvers(")?;
if self.resolvers.is_empty() {
f.write_str("empty")?;
} else {
for (i, resolver) in self.resolvers.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
fmt::Display::fmt(resolver.as_ref(), f)?;
}
}
f.write_str(")")
}
}
#[derive(Debug)]
pub struct DnsErrors {
errors: Vec<(String, io::Error)>,
}
fn format_dns_error_sources(
f: &mut fmt::Formatter<'_>,
error: &(dyn Error + 'static),
) -> fmt::Result {
let mut index = 1;
let mut current = error.source();
while let Some(source) = current {
write!(f, "\n {index}. {source}")?;
index += 1;
current = source.source();
}
Ok(())
}
fn format_dns_error_entry(
f: &mut fmt::Formatter<'_>,
resolver: &str,
error: &io::Error,
) -> fmt::Result {
write!(f, "\n - {resolver}: {error}")?;
format_dns_error_sources(f, error)
}
impl fmt::Display for DnsErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.errors.is_empty() {
return write!(f, "no DNS resolvers available");
}
write!(f, "all DNS resolvers failed")?;
for (resolver, error) in &self.errors {
format_dns_error_entry(f, resolver, error)?;
}
Ok(())
}
}
impl Error for DnsErrors {}
#[derive(Default)]
pub struct ResolversBuilder {
resolvers: Resolvers,
}
impl ResolversBuilder {
pub fn resolver(mut self, resolver: ArcResolver) -> Self {
self.resolvers.push(resolver);
self
}
#[cfg(feature = "mdns-resolver")]
pub async fn mdns(
mut self,
network: Arc<h3x::dquic::Network>,
patterns: Arc<Vec<h3x::dquic::binds::BindPattern>>,
) -> Self {
let mdns = Arc::new(MdnsResolvers::bind(network, patterns, DHTTP_MDNS_SERVICE).await);
self.resolvers.push(mdns);
self
}
#[cfg(feature = "h3x-resolver")]
pub fn h3<C>(
self,
endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
) -> io::Result<Self>
where
C: h3x::quic::Connect + Send + Sync + 'static,
C::Error: Send + Sync + 'static,
C::Connection: Send + 'static,
{
self.h3_with_base_url(DHTTP_H3_DNS_SERVER, endpoint)
}
#[cfg(feature = "h3x-resolver")]
pub fn h3_with_base_url<C>(
mut self,
base_url: impl AsRef<str>,
endpoint: Arc<h3x::endpoint::H3Endpoint<C, C::Connection>>,
) -> io::Result<Self>
where
C: h3x::quic::Connect + Send + Sync + 'static,
C::Error: Send + Sync + 'static,
C::Connection: Send + 'static,
{
let resolver = h3::H3Resolver::from_endpoint(base_url, endpoint)?;
self.resolvers.push(Arc::new(resolver));
Ok(self)
}
#[cfg(feature = "http-resolver")]
pub fn http(self) -> io::Result<Self> {
self.http_with_base_url(DHTTP_HTTP_DNS_SERVER)
}
#[cfg(feature = "http-resolver")]
pub fn http_with_base_url(mut self, base_url: impl AsRef<str>) -> io::Result<Self> {
let resolver = HttpResolver::new(base_url.as_ref())?;
self.resolvers.push(Arc::new(resolver));
Ok(self)
}
pub fn system(mut self) -> Self {
self.resolvers
.push(Arc::new(dquic::qresolve::SystemResolver));
self
}
pub fn build(self) -> Resolvers {
self.resolvers
}
}
impl Resolvers {
pub fn builder() -> ResolversBuilder {
ResolversBuilder::default()
}
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, resolver: ArcResolver) -> Self {
self.push(resolver);
self
}
pub fn push(&mut self, resolver: ArcResolver) {
self.resolvers.push(resolver);
}
pub fn iter(&self) -> impl Iterator<Item = &ArcResolver> {
self.resolvers.iter()
}
pub async fn lookup(
&self,
name: &str,
) -> Result<impl Stream<Item = (Source, EndpointAddr)> + use<>, DnsErrors> {
let mut errors = vec![];
let mut lookups = stream::FuturesUnordered::from_iter(
(self.resolvers.clone().into_iter()).map(|resolver| {
let resolver = resolver.clone();
let name = name.to_string();
async move { (resolver.lookup(&name).await, resolver.clone()) }
}),
);
let endpoints = loop {
match lookups.next().await {
Some((Ok(endpoints), _)) => break endpoints,
Some((Err(error), resolver)) => errors.push((resolver.to_string(), error)),
None => return Err(DnsErrors { errors }),
}
};
Ok(endpoints.chain(lookups.flat_map(|(endpoints, _)| stream::iter(endpoints).flatten())))
}
}
impl Resolve for Resolvers {
fn lookup<'l>(&'l self, name: &'l str) -> ResolveFuture<'l> {
self.lookup(name)
.map_ok(StreamExt::boxed)
.map_err(io::Error::other)
.boxed()
}
}
#[cfg(test)]
mod tests {
use std::{error::Error as StdError, fmt, io, str::FromStr};
#[cfg(feature = "mdns-resolver")]
use super::MdnsResolvers;
#[cfg(any(
feature = "h3x-resolver",
feature = "http-resolver",
feature = "mdns-resolver"
))]
use super::Resolvers;
use super::{
DHTTP_H3_DNS_SERVER, DHTTP_HTTP_DNS_SERVER, DHTTP_MDNS_SERVICE, DnsErrors, DnsScheme,
resolvable_name,
};
#[derive(Debug)]
struct TestSourceError {
message: &'static str,
source: Option<Box<TestSourceError>>,
}
impl TestSourceError {
fn leaf(message: &'static str) -> Self {
Self {
message,
source: None,
}
}
fn with_source(message: &'static str, source: TestSourceError) -> Self {
Self {
message,
source: Some(Box::new(source)),
}
}
}
impl fmt::Display for TestSourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.message)
}
}
impl StdError for TestSourceError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn StdError + 'static))
}
}
fn other_error(message: &'static str) -> io::Error {
io::Error::other(message)
}
fn chained_other_error(root: TestSourceError) -> io::Error {
io::Error::other(root)
}
#[test]
fn resolver_defaults_come_from_compile_time_environment() {
if let Some(expected) = option_env!("DHTTP_H3_DNS_SERVER") {
assert_eq!(DHTTP_H3_DNS_SERVER, expected);
}
if let Some(expected) = option_env!("DHTTP_HTTP_DNS_SERVER") {
assert_eq!(DHTTP_HTTP_DNS_SERVER, expected);
}
if let Some(expected) = option_env!("DHTTP_MDNS_SERVICE") {
assert_eq!(DHTTP_MDNS_SERVICE, expected);
}
}
#[test]
fn resolvable_name_accepts_dns_name_with_numeric_port() {
assert_eq!(
resolvable_name("example.dhttp.net:443"),
Some("example.dhttp.net")
);
}
#[test]
fn resolvable_name_accepts_stun_authority_with_numeric_port() {
assert_eq!(
resolvable_name("nat.genmeta.net:20004"),
Some("nat.genmeta.net")
);
}
#[test]
fn resolvable_name_rejects_ip_literals() {
assert_eq!(resolvable_name("127.0.0.1:443"), None);
assert_eq!(resolvable_name("[::1]:443"), None);
}
#[test]
fn dns_scheme_round_trips_supported_schemes_and_rejects_dht() {
let cases = [
("mdns", DnsScheme::Mdns),
("http", DnsScheme::Http),
("h3", DnsScheme::H3),
("system", DnsScheme::System),
];
for (text, scheme) in cases {
assert_eq!(DnsScheme::from_str(text).expect("supported scheme"), scheme);
assert_eq!(scheme.to_string(), text);
}
assert!(DnsScheme::from_str("dht").is_err());
}
#[test]
fn dns_errors_render_no_resolvers_available_when_empty() {
let error = DnsErrors { errors: vec![] };
assert_eq!(error.to_string(), "no DNS resolvers available");
}
#[test]
fn dns_errors_render_resolver_bullets_in_stored_order() {
let error = DnsErrors {
errors: vec![
(
"System DNS Resolver".to_string(),
other_error("invalid socket address"),
),
("mDNS resolvers".to_string(), other_error("timed out")),
],
};
assert_eq!(
error.to_string(),
concat!(
"all DNS resolvers failed\n",
" - System DNS Resolver: invalid socket address\n",
" - mDNS resolvers: timed out"
)
);
}
#[test]
fn dns_errors_render_numbered_source_chain_for_one_resolver() {
let error = DnsErrors {
errors: vec![(
"DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
chained_other_error(TestSourceError::with_source(
"deferred resolver lookup failed",
TestSourceError::leaf("no DNS record found"),
)),
)],
};
assert_eq!(
error.to_string(),
concat!(
"all DNS resolvers failed\n",
" - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
" 1. no DNS record found"
)
);
}
#[test]
fn dns_errors_render_repeated_source_messages_without_deduplication() {
let error = DnsErrors {
errors: vec![(
"DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/))".to_string(),
chained_other_error(TestSourceError::with_source(
"deferred resolver lookup failed",
TestSourceError::with_source(
"deferred resolver lookup failed",
TestSourceError::leaf("no DNS record found"),
),
)),
)],
};
assert_eq!(
error.to_string(),
concat!(
"all DNS resolvers failed\n",
" - DeferredResolver(H3 DNS Resolver(https://dns.genmeta.net:4433/)): deferred resolver lookup failed\n",
" 1. deferred resolver lookup failed\n",
" 2. no DNS record found"
)
);
}
#[cfg(feature = "mdns-resolver")]
#[tokio::test]
async fn resolvers_builder_can_enable_mdns() {
use std::sync::Arc;
use h3x::dquic::{Network, binds::BindPattern};
let network = Network::builder().build();
let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
let resolvers = Resolvers::builder()
.mdns(network, Arc::new(vec![pattern]))
.await
.build();
assert!(resolvers.to_string().contains("mDNS resolvers"));
}
#[cfg(feature = "h3x-resolver")]
#[tokio::test]
async fn resolvers_builder_accepts_custom_h3_base_url() {
use std::sync::Arc;
let endpoint = Arc::new(h3x::endpoint::H3Endpoint::new(
h3x::dquic::QuicEndpoint::builder().build().await,
));
let resolvers = Resolvers::builder()
.h3_with_base_url("https://custom-dns.example:4433", endpoint)
.expect("valid h3 dns url")
.build();
assert!(resolvers.to_string().contains("custom-dns.example"));
}
#[cfg(feature = "http-resolver")]
#[test]
fn resolvers_builder_accepts_custom_http_base_url() {
let resolvers = Resolvers::builder()
.http_with_base_url("https://custom-dns.example")
.expect("valid http dns url")
.build();
assert!(resolvers.to_string().contains("custom-dns.example"));
}
#[cfg(feature = "mdns-resolver")]
#[tokio::test]
async fn mdns_resolvers_bind_installs_mdns_on_null_io_binding() {
use std::sync::Arc;
use dquic::qinterface::io::IO;
use h3x::dquic::{Network, binds::BindPattern};
let network = Network::builder().build();
let pattern = BindPattern::from_str("iface://v4.lo:0").expect("valid pattern");
let resolvers = MdnsResolvers::bind(
network.clone(),
Arc::new(vec![pattern.clone()]),
DHTTP_MDNS_SERVICE,
)
.await;
let ifaces = resolvers
.bound_interfaces(&pattern)
.expect("bound interfaces");
assert!(!ifaces.is_empty());
assert!(ifaces[0].borrow().bound_addr().is_err());
assert!(
ifaces[0]
.with_components(|components, _| components.exist::<crate::mdns::service::Mdns>())
);
}
}