use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::AtomicU8;
use std::task::{Context, Poll};
use futures_util::{
FutureExt, Stream,
future::{self, BoxFuture},
lock::Mutex as AsyncMutex,
};
use tracing::debug;
#[cfg(feature = "__tls")]
use crate::connection_provider::TlsConfig;
#[cfg(feature = "tokio")]
use crate::net::runtime::TokioRuntimeProvider;
use crate::{
cache::{MAX_TTL, ResponseCache, TtlConfig},
caching_client::CachingClient,
config::{OpportunisticEncryption, ResolveHosts, ResolverConfig, ResolverOpts},
connection_provider::ConnectionProvider,
hosts::Hosts,
lookup::Lookup,
lookup_ip::{LookupIp, LookupIpFuture},
name_server_pool::{NameServerPool, NameServerTransportState, PoolContext},
net::{
NetError,
xfer::{DnsHandle, RetryDnsHandle},
},
proto::{
op::{DnsRequest, DnsRequestOptions, DnsResponse, Query},
rr::domain::usage::ONION,
rr::{IntoName, Name, RData, Record, RecordType},
},
};
#[cfg(feature = "__dnssec")]
use crate::{net::dnssec::DnssecDnsHandle, proto::dnssec::TrustAnchors};
macro_rules! lookup_fn {
($p:ident, $r:path) => {
pub async fn $p(&self, query: impl IntoName) -> Result<Lookup, NetError> {
self.inner_lookup(query.into_name()?, $r, self.request_options())
.await
}
};
}
#[cfg(feature = "tokio")]
pub type TokioResolver = Resolver<TokioRuntimeProvider>;
#[cfg(feature = "tokio")]
impl TokioResolver {
#[cfg(any(unix, target_os = "windows"))]
#[cfg(feature = "system-config")]
pub fn builder_tokio() -> Result<ResolverBuilder<TokioRuntimeProvider>, NetError> {
Self::builder(TokioRuntimeProvider::default())
}
}
#[derive(Clone)]
pub struct Resolver<P: ConnectionProvider> {
domain: Option<Name>,
search: Vec<Name>,
context: Arc<PoolContext>,
client_cache: CachingClient<LookupEither<P>>,
hosts: Arc<Hosts>,
}
impl<R: ConnectionProvider> Resolver<R> {
#[cfg(any(unix, target_os = "windows"))]
#[cfg(feature = "system-config")]
pub fn builder(provider: R) -> Result<ResolverBuilder<R>, NetError> {
let (config, options) = super::system_conf::read_system_conf()?;
let mut builder = Self::builder_with_config(config, provider);
*builder.options_mut() = options;
Ok(builder)
}
pub fn builder_with_config(config: ResolverConfig, provider: R) -> ResolverBuilder<R> {
ResolverBuilder {
config,
options: ResolverOpts::default(),
provider,
#[cfg(feature = "__tls")]
tls: None,
opportunistic_encryption: OpportunisticEncryption::default(),
encrypted_transport_state: NameServerTransportState::default(),
#[cfg(feature = "__dnssec")]
trust_anchor: None,
#[cfg(feature = "__dnssec")]
nsec3_soft_iteration_limit: None,
#[cfg(feature = "__dnssec")]
nsec3_hard_iteration_limit: None,
}
}
pub fn set_hosts(&mut self, hosts: Arc<Hosts>) {
self.hosts = hosts;
}
pub async fn lookup(
&self,
name: impl IntoName,
record_type: RecordType,
) -> Result<Lookup, NetError> {
self.inner_lookup(name.into_name()?, record_type, self.request_options())
.await
}
pub(crate) async fn inner_lookup<L>(
&self,
name: Name,
record_type: RecordType,
options: DnsRequestOptions,
) -> Result<L, NetError>
where
L: From<Lookup> + Send + Sync + 'static,
{
let names = self.build_names(name);
LookupFuture::lookup_with_hosts(
names,
record_type,
options,
self.client_cache.clone(),
self.hosts.clone(),
)
.await
.map(L::from)
}
pub async fn lookup_ip(&self, host: impl IntoName) -> Result<LookupIp, NetError> {
let mut finally_ip_addr = None;
let maybe_ip = host.to_ip().map(RData::from);
let maybe_name = host.into_name().map_err(NetError::from);
if let Some(ip_addr) = maybe_ip {
let name = maybe_name.clone().unwrap_or_default();
let record = Record::from_rdata(name.clone(), MAX_TTL, ip_addr.clone());
if self.context.options.ndots > 4 {
finally_ip_addr = Some(record);
} else {
let query = Query::query(name, ip_addr.record_type());
let lookup = Lookup::new_with_max_ttl(query, [record]);
return Ok(lookup.into());
}
}
let name = match (maybe_name, finally_ip_addr.as_ref()) {
(Ok(name), _) => name,
(Err(_), Some(ip_addr)) => {
let query = Query::query(ip_addr.name().clone(), ip_addr.record_type());
let lookup = Lookup::new_with_max_ttl(query, [ip_addr.clone()]);
return Ok(lookup.into());
}
(Err(err), None) => return Err(err),
};
let names = self.build_names(name);
let hosts = self.hosts.clone();
LookupIpFuture::lookup(
names,
self.context.options.ip_strategy,
self.client_cache.clone(),
self.request_options(),
hosts,
finally_ip_addr.map(Record::into_data),
)
.await
}
pub fn clear_lookup_cache(&self, name: impl IntoName, record_type: RecordType) {
let Ok(name) = name.into_name() else {
return;
};
for name in self.build_names(name) {
let query = Query::query(name, record_type);
self.client_cache.clear_cache_query(&query);
}
}
fn build_names(&self, name: Name) -> Vec<Name> {
if name.is_fqdn()
|| ONION.zone_of(&name)
&& name
.trim_to(2)
.iter()
.next()
.map(|name| name.len() == 56) .unwrap_or(false)
{
vec![name]
} else {
let mut names =
Vec::<Name>::with_capacity(1 + 1 + self.search.len());
let raw_name_first: bool =
name.num_labels() as usize > self.context.options.ndots || name.is_localhost();
if !raw_name_first {
let mut fqdn = name.clone();
fqdn.set_fqdn(true);
names.push(fqdn);
}
for search in self.search.iter().rev() {
let name_search = name.clone().append_domain(search);
match name_search {
Ok(name_search) => {
if !names.contains(&name_search) {
names.push(name_search);
}
}
Err(e) => debug!(
"Not adding {} to {} for search due to error: {}",
search, name, e
),
}
}
if let Some(domain) = &self.domain {
let name_search = name.clone().append_domain(domain);
match name_search {
Ok(name_search) => {
if !names.contains(&name_search) {
names.push(name_search);
}
}
Err(e) => debug!(
"Not adding {} to {} for search due to error: {}",
domain, name, e
),
}
}
if raw_name_first {
let mut fqdn = name.clone();
fqdn.set_fqdn(true);
names.push(fqdn);
}
names
}
}
lookup_fn!(reverse_lookup, RecordType::PTR);
lookup_fn!(ipv4_lookup, RecordType::A);
lookup_fn!(ipv6_lookup, RecordType::AAAA);
lookup_fn!(mx_lookup, RecordType::MX);
lookup_fn!(ns_lookup, RecordType::NS);
lookup_fn!(smimea_lookup, RecordType::SMIMEA);
lookup_fn!(soa_lookup, RecordType::SOA);
lookup_fn!(srv_lookup, RecordType::SRV);
lookup_fn!(tlsa_lookup, RecordType::TLSA);
lookup_fn!(txt_lookup, RecordType::TXT);
lookup_fn!(cert_lookup, RecordType::CERT);
pub fn clear_cache(&self) {
self.client_cache.clear_cache();
}
pub(crate) fn request_options(&self) -> DnsRequestOptions {
let mut request_opts = DnsRequestOptions::default();
request_opts.recursion_desired = self.context.options.recursion_desired;
request_opts.use_edns = self.context.options.edns0;
request_opts.edns_payload_len = self.context.options.edns_payload_len;
request_opts.case_randomization = self.context.options.case_randomization;
#[cfg(feature = "__dnssec")]
{
request_opts.edns_set_dnssec_ok = self.context.options.validate;
}
request_opts
}
pub fn options(&self) -> &ResolverOpts {
&self.context.options
}
}
impl<P: ConnectionProvider> fmt::Debug for Resolver<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Resolver").finish()
}
}
#[derive(Clone)]
enum LookupEither<P: ConnectionProvider> {
Retry(RetryDnsHandle<NameServerPool<P>>),
#[cfg(feature = "__dnssec")]
Secure(DnssecDnsHandle<RetryDnsHandle<NameServerPool<P>>>),
}
impl<P: ConnectionProvider> DnsHandle for LookupEither<P> {
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
type Runtime = P::RuntimeProvider;
fn is_verifying_dnssec(&self) -> bool {
match self {
Self::Retry(c) => c.is_verifying_dnssec(),
#[cfg(feature = "__dnssec")]
Self::Secure(c) => c.is_verifying_dnssec(),
}
}
fn send(&self, request: DnsRequest) -> Self::Response {
match self {
Self::Retry(c) => c.send(request),
#[cfg(feature = "__dnssec")]
Self::Secure(c) => c.send(request),
}
}
}
pub struct ResolverBuilder<P> {
config: ResolverConfig,
options: ResolverOpts,
provider: P,
#[cfg(feature = "__tls")]
tls: Option<rustls::ClientConfig>,
opportunistic_encryption: OpportunisticEncryption,
encrypted_transport_state: NameServerTransportState,
#[cfg(feature = "__dnssec")]
trust_anchor: Option<Arc<TrustAnchors>>,
#[cfg(feature = "__dnssec")]
nsec3_soft_iteration_limit: Option<u16>,
#[cfg(feature = "__dnssec")]
nsec3_hard_iteration_limit: Option<u16>,
}
impl<P: ConnectionProvider> ResolverBuilder<P> {
pub fn with_options(mut self, options: ResolverOpts) -> Self {
self.options = options;
self
}
pub fn options_mut(&mut self) -> &mut ResolverOpts {
&mut self.options
}
#[cfg(feature = "__dnssec")]
pub fn with_trust_anchor(mut self, trust_anchor: Arc<TrustAnchors>) -> Self {
self.trust_anchor = Some(trust_anchor);
self
}
#[cfg(feature = "__tls")]
pub fn with_tls_config(mut self, config: rustls::ClientConfig) -> Self {
self.tls = Some(config);
self
}
pub fn with_opportunistic_encryption(
mut self,
opportunistic_encryption: OpportunisticEncryption,
) -> Self {
self.opportunistic_encryption = opportunistic_encryption;
self
}
pub fn with_encrypted_transport_state(
mut self,
encrypted_transport_state: NameServerTransportState,
) -> Self {
self.encrypted_transport_state = encrypted_transport_state;
self
}
#[cfg(feature = "__dnssec")]
pub fn nsec3_iteration_limits(
mut self,
soft_limit: Option<u16>,
hard_limit: Option<u16>,
) -> Self {
self.nsec3_soft_iteration_limit = soft_limit;
self.nsec3_hard_iteration_limit = hard_limit;
self
}
pub fn build(self) -> Result<Resolver<P>, NetError> {
#[cfg_attr(not(feature = "__dnssec"), allow(unused_mut))]
let Self {
config:
ResolverConfig {
domain,
search,
name_servers,
},
mut options,
provider,
#[cfg(feature = "__tls")]
tls,
#[cfg(feature = "__dnssec")]
trust_anchor,
#[cfg(feature = "__dnssec")]
nsec3_soft_iteration_limit,
#[cfg(feature = "__dnssec")]
nsec3_hard_iteration_limit,
opportunistic_encryption,
encrypted_transport_state,
} = self;
#[cfg(feature = "__dnssec")]
if trust_anchor.is_some() || options.trust_anchor.is_some() {
options.validate = true;
}
let context = Arc::new(PoolContext {
answer_address_filter: options.answer_address_filter(),
options,
#[cfg(feature = "__tls")]
tls: match tls {
Some(config) => config,
None => TlsConfig::new()?.config,
},
opportunistic_probe_budget: AtomicU8::new(
opportunistic_encryption
.max_concurrent_probes()
.unwrap_or_default(),
),
opportunistic_encryption,
transport_state: AsyncMutex::new(encrypted_transport_state),
});
let pool = NameServerPool::from_config(name_servers, context.clone(), provider);
let client = RetryDnsHandle::new(pool, context.options.attempts);
#[cfg(feature = "__dnssec")]
let either = if context.options.validate {
let trust_anchor = trust_anchor.unwrap_or_else(|| Arc::new(TrustAnchors::default()));
LookupEither::Secure(
DnssecDnsHandle::with_trust_anchor(client, trust_anchor)
.nsec3_iteration_limits(nsec3_soft_iteration_limit, nsec3_hard_iteration_limit),
)
} else {
LookupEither::Retry(client)
};
#[cfg(not(feature = "__dnssec"))]
let either = LookupEither::Retry(client);
let cache = ResponseCache::new(
context.options.cache_size,
TtlConfig::from_opts(&context.options),
);
let client_cache =
CachingClient::with_cache(cache, either, context.options.preserve_intermediates);
let hosts = Arc::new(match context.options.use_hosts_file {
ResolveHosts::Always | ResolveHosts::Auto => Hosts::from_system().unwrap_or_default(),
ResolveHosts::Never => Hosts::default(),
});
Ok(Resolver {
domain,
search,
context,
client_cache,
hosts,
})
}
}
#[doc(hidden)]
pub struct LookupFuture<C>
where
C: DnsHandle + 'static,
{
client_cache: CachingClient<C>,
names: Vec<Name>,
record_type: RecordType,
options: DnsRequestOptions,
query: BoxFuture<'static, Result<Lookup, NetError>>,
}
impl<C> LookupFuture<C>
where
C: DnsHandle + 'static,
{
#[doc(hidden)]
pub fn lookup(
names: Vec<Name>,
record_type: RecordType,
options: DnsRequestOptions,
client_cache: CachingClient<C>,
) -> Self {
Self::lookup_with_hosts(
names,
record_type,
options,
client_cache,
Arc::new(Hosts::default()),
)
}
#[doc(hidden)]
pub fn lookup_with_hosts(
mut names: Vec<Name>,
record_type: RecordType,
options: DnsRequestOptions,
client_cache: CachingClient<C>,
hosts: Arc<Hosts>,
) -> Self {
let name = names
.pop()
.ok_or(NetError::Message("can not lookup for no names"));
let query = match name {
Ok(name) => {
let query = Query::query(name, record_type);
if let Some(lookup) = hosts.lookup_static_host(&query) {
future::ok(lookup).boxed()
} else {
client_cache.lookup(query, options).boxed()
}
}
Err(err) => future::err(err).boxed(),
};
Self {
client_cache,
names,
record_type,
options,
query,
}
}
}
impl<C> Future for LookupFuture<C>
where
C: DnsHandle + 'static,
{
type Output = Result<Lookup, NetError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
let query = self.query.as_mut().poll_unpin(cx);
let should_retry = match &query {
Poll::Pending => return Poll::Pending,
Poll::Ready(Ok(lookup)) => lookup.answers().is_empty(),
Poll::Ready(Err(_)) => true,
};
if should_retry {
if let Some(name) = self.names.pop() {
let record_type = self.record_type;
let options = self.options;
self.query = self
.client_cache
.lookup(Query::query(name, record_type), options)
.boxed();
continue;
}
}
return query;
}
}
}
#[cfg(all(test, feature = "tokio"))]
pub(crate) mod testing {
use std::{net::*, str::FromStr};
use tokio::runtime::Runtime;
use crate::config::{GOOGLE, LookupIpStrategy, NameServerConfig, ResolverConfig};
use crate::connection_provider::ConnectionProvider;
use crate::net::runtime::TokioRuntimeProvider;
use crate::proto::rr::Name;
use crate::resolver::Resolver;
pub(crate) async fn lookup_test<R: ConnectionProvider>(config: ResolverConfig, handle: R) {
let resolver = Resolver::<R>::builder_with_config(config, handle)
.build()
.unwrap();
let response = resolver
.lookup_ip("www.example.com.")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
}
pub(crate) async fn ip_lookup_test<R: ConnectionProvider>(handle: R) {
let resolver =
Resolver::<R>::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle)
.build()
.unwrap();
let response = resolver
.lookup_ip("10.1.0.2")
.await
.expect("failed to run lookup");
assert_eq!(
Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
response.iter().next()
);
let response = resolver
.lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c")
.await
.expect("failed to run lookup");
assert_eq!(
Some(IpAddr::V6(Ipv6Addr::new(
0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
))),
response.iter().next()
);
}
pub(crate) fn ip_lookup_across_threads_test(handle: TokioRuntimeProvider) {
use std::thread;
let resolver = Resolver::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle)
.build()
.unwrap();
let resolver_one = resolver.clone();
let resolver_two = resolver;
let test_fn = |resolver: Resolver<TokioRuntimeProvider>| {
let exec = Runtime::new().unwrap();
let response = exec
.block_on(resolver.lookup_ip("10.1.0.2"))
.expect("failed to run lookup");
assert_eq!(
Some(IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2))),
response.iter().next()
);
let response = exec
.block_on(resolver.lookup_ip("2606:2800:21f:cb07:6820:80da:af6b:8b2c"))
.expect("failed to run lookup");
assert_eq!(
Some(IpAddr::V6(Ipv6Addr::new(
0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
))),
response.iter().next()
);
};
let thread_one = thread::spawn(move || {
test_fn(resolver_one);
});
let thread_two = thread::spawn(move || {
test_fn(resolver_two);
});
thread_one.join().expect("thread_one failed");
thread_two.join().expect("thread_two failed");
}
#[cfg(feature = "__dnssec")]
#[allow(clippy::print_stdout)]
pub(crate) async fn sec_lookup_test<R: ConnectionProvider>(handle: R) {
let mut resolver_builder =
Resolver::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle);
resolver_builder.options_mut().validate = true;
resolver_builder.options_mut().try_tcp_on_error = true;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("cloudflare.com.")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
println!(
"{:?}",
response
.as_lookup()
.message()
.all_sections()
.collect::<Vec<_>>()
);
assert!(
response
.as_lookup()
.message()
.all_sections()
.any(|record| record.proof().is_secure())
);
}
#[cfg(feature = "__dnssec")]
#[allow(clippy::print_stdout)]
pub(crate) async fn sec_lookup_fails_test<R: ConnectionProvider>(handle: R) {
let mut resolver_builder =
Resolver::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle);
resolver_builder.options_mut().validate = true;
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver.lookup_ip("hickory-dns.org.").await;
let lookup_ip = response.unwrap();
println!(
"{:?}",
lookup_ip
.as_lookup()
.message()
.all_sections()
.collect::<Vec<_>>()
);
for record in lookup_ip.as_lookup().message().all_sections() {
assert!(record.proof().is_insecure());
}
}
#[cfg(feature = "system-config")]
pub(crate) async fn system_lookup_test<R: ConnectionProvider>(handle: R) {
let resolver = Resolver::<R>::builder(handle)
.expect("failed to create resolver")
.build()
.unwrap();
let response = resolver
.lookup_ip("www.example.com.")
.await
.expect("failed to run lookup");
assert_eq!(response.iter().count(), 2);
for address in response.iter() {
if address.is_ipv4() {
assert_eq!(address, IpAddr::V4(Ipv4Addr::new(93, 184, 215, 14)));
} else {
assert_eq!(
address,
IpAddr::V6(Ipv6Addr::new(
0x2606, 0x2800, 0x21f, 0xcb07, 0x6820, 0x80da, 0xaf6b, 0x8b2c,
))
);
}
}
}
#[cfg(all(unix, feature = "system-config"))]
pub(crate) async fn hosts_lookup_test<R: ConnectionProvider>(handle: R) {
let resolver = Resolver::<R>::builder(handle)
.expect("failed to create resolver")
.build()
.unwrap();
let response = resolver
.lookup_ip("a.com")
.await
.expect("failed to run lookup");
assert_eq!(response.iter().count(), 1);
for address in response.iter() {
if address.is_ipv4() {
assert_eq!(address, IpAddr::V4(Ipv4Addr::new(10, 1, 0, 104)));
} else {
panic!("failed to run lookup");
}
}
}
pub(crate) async fn fqdn_test<R: ConnectionProvider>(handle: R) {
let domain = Name::from_str("incorrect.example.com.").unwrap();
let search = vec![
Name::from_str("bad.example.com.").unwrap(),
Name::from_str("wrong.example.com.").unwrap(),
];
let name_servers: Vec<NameServerConfig> = ResolverConfig::udp_and_tcp(&GOOGLE)
.name_servers()
.to_owned();
let mut resolver_builder = Resolver::<R>::builder_with_config(
ResolverConfig::from_parts(Some(domain), search, name_servers),
handle,
);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("www.example.com.")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
for address in response.iter() {
assert!(address.is_ipv4(), "should only be looking up IPv4");
}
}
pub(crate) async fn ndots_test<R: ConnectionProvider>(handle: R) {
let domain = Name::from_str("incorrect.example.com.").unwrap();
let search = vec![
Name::from_str("bad.example.com.").unwrap(),
Name::from_str("wrong.example.com.").unwrap(),
];
let name_servers: Vec<NameServerConfig> = ResolverConfig::udp_and_tcp(&GOOGLE)
.name_servers()
.to_owned();
let mut resolver_builder = Resolver::<R>::builder_with_config(
ResolverConfig::from_parts(Some(domain), search, name_servers),
handle,
);
resolver_builder.options_mut().ndots = 2;
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("www.example.com")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
for address in response.iter() {
assert!(address.is_ipv4(), "should only be looking up IPv4");
}
}
pub(crate) async fn large_ndots_test<R: ConnectionProvider>(handle: R) {
let domain = Name::from_str("incorrect.example.com.").unwrap();
let search = vec![
Name::from_str("bad.example.com.").unwrap(),
Name::from_str("wrong.example.com.").unwrap(),
];
let name_servers: Vec<NameServerConfig> = ResolverConfig::udp_and_tcp(&GOOGLE)
.name_servers()
.to_owned();
let mut resolver_builder = Resolver::<R>::builder_with_config(
ResolverConfig::from_parts(Some(domain), search, name_servers),
handle,
);
resolver_builder.options_mut().ndots = 5;
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("www.example.com")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
for address in response.iter() {
assert!(address.is_ipv4(), "should only be looking up IPv4");
}
}
pub(crate) async fn domain_search_test<R: ConnectionProvider>(handle: R) {
let domain = Name::from_str("example.com.").unwrap();
let search = vec![
Name::from_str("bad.example.com.").unwrap(),
Name::from_str("wrong.example.com.").unwrap(),
];
let name_servers: Vec<NameServerConfig> = ResolverConfig::udp_and_tcp(&GOOGLE)
.name_servers()
.to_owned();
let mut resolver_builder = Resolver::<R>::builder_with_config(
ResolverConfig::from_parts(Some(domain), search, name_servers),
handle,
);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("www")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
for address in response.iter() {
assert!(address.is_ipv4(), "should only be looking up IPv4");
}
}
pub(crate) async fn search_list_test<R: ConnectionProvider>(handle: R) {
let domain = Name::from_str("incorrect.example.com.").unwrap();
let search = vec![
Name::from_str("bad.example.com.").unwrap(),
Name::from_str("example.com.").unwrap(),
];
let name_servers: Vec<NameServerConfig> = ResolverConfig::udp_and_tcp(&GOOGLE)
.name_servers()
.to_owned();
let mut resolver_builder = Resolver::<R>::builder_with_config(
ResolverConfig::from_parts(Some(domain), search, name_servers),
handle,
);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("www")
.await
.expect("failed to run lookup");
assert_ne!(response.iter().count(), 0);
for address in response.iter() {
assert!(address.is_ipv4(), "should only be looking up IPv4");
}
}
pub(crate) async fn idna_test<R: ConnectionProvider>(handle: R) {
let resolver =
Resolver::<R>::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle)
.build()
.unwrap();
let response = resolver
.lookup_ip("ä¸å›½.icom.museum.")
.await
.expect("failed to run lookup");
assert!(response.iter().next().is_some());
}
pub(crate) async fn localhost_ipv4_test<R: ConnectionProvider>(handle: R) {
let mut resolver_builder =
Resolver::<R>::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4thenIpv6;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("localhost")
.await
.expect("failed to run lookup");
let mut iter = response.iter();
assert_eq!(iter.next().expect("no A"), IpAddr::V4(Ipv4Addr::LOCALHOST));
}
pub(crate) async fn localhost_ipv6_test<R: ConnectionProvider>(handle: R) {
let mut resolver_builder =
Resolver::<R>::builder_with_config(ResolverConfig::udp_and_tcp(&GOOGLE), handle);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv6thenIpv4;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("localhost")
.await
.expect("failed to run lookup");
let mut iter = response.iter();
assert_eq!(
iter.next().expect("no AAAA"),
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1,))
);
}
pub(crate) async fn search_ipv4_large_ndots_test<R: ConnectionProvider>(handle: R) {
let mut config = ResolverConfig::udp_and_tcp(&GOOGLE);
config.add_search(Name::from_str("example.com").unwrap());
let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
resolver_builder.options_mut().ndots = 5;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("198.51.100.35")
.await
.expect("failed to run lookup");
let mut iter = response.iter();
assert_eq!(
iter.next().expect("no rdatas"),
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 35))
);
}
pub(crate) async fn search_ipv6_large_ndots_test<R: ConnectionProvider>(handle: R) {
let mut config = ResolverConfig::udp_and_tcp(&GOOGLE);
config.add_search(Name::from_str("example.com").unwrap());
let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
resolver_builder.options_mut().ndots = 5;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("2001:db8::c633:6423")
.await
.expect("failed to run lookup");
let mut iter = response.iter();
assert_eq!(
iter.next().expect("no rdatas"),
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
);
}
pub(crate) async fn search_ipv6_name_parse_fails_test<R: ConnectionProvider>(handle: R) {
let mut config = ResolverConfig::udp_and_tcp(&GOOGLE);
config.add_search(Name::from_str("example.com").unwrap());
let mut resolver_builder = Resolver::<R>::builder_with_config(config, handle);
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
resolver_builder.options_mut().ndots = 5;
let resolver = resolver_builder.build().unwrap();
let response = resolver
.lookup_ip("2001:db8::198.51.100.35")
.await
.expect("failed to run lookup");
let mut iter = response.iter();
assert_eq!(
iter.next().expect("no rdatas"),
IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc633, 0x6423))
);
}
}
#[cfg(test)]
#[cfg(feature = "tokio")]
#[allow(clippy::extra_unused_type_parameters)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Mutex;
use futures_util::stream::once;
use futures_util::{Stream, future};
use test_support::subscribe;
#[cfg(all(unix, feature = "system-config"))]
use super::testing::hosts_lookup_test;
#[cfg(feature = "system-config")]
use super::testing::system_lookup_test;
use super::testing::{
domain_search_test, fqdn_test, idna_test, ip_lookup_across_threads_test, ip_lookup_test,
large_ndots_test, localhost_ipv4_test, localhost_ipv6_test, lookup_test, ndots_test,
search_ipv4_large_ndots_test, search_ipv6_large_ndots_test,
search_ipv6_name_parse_fails_test, search_list_test,
};
#[cfg(feature = "__dnssec")]
use super::testing::{sec_lookup_fails_test, sec_lookup_test};
use super::*;
use crate::config::{CLOUDFLARE, GOOGLE, ResolverConfig, ResolverOpts};
use crate::net::DnsError;
use crate::net::xfer::DnsExchange;
use crate::proto::op::{DnsRequest, DnsResponse, Message};
use crate::proto::rr::rdata::A;
fn is_send_t<T: Send>() -> bool {
true
}
fn is_sync_t<T: Sync>() -> bool {
true
}
#[test]
fn test_send_sync() {
assert!(is_send_t::<ResolverConfig>());
assert!(is_sync_t::<ResolverConfig>());
assert!(is_send_t::<ResolverOpts>());
assert!(is_sync_t::<ResolverOpts>());
assert!(is_send_t::<Resolver<TokioRuntimeProvider>>());
assert!(is_sync_t::<Resolver<TokioRuntimeProvider>>());
assert!(is_send_t::<DnsRequest>());
assert!(is_send_t::<LookupIpFuture<DnsExchange<TokioRuntimeProvider>>>());
assert!(is_send_t::<LookupFuture<DnsExchange<TokioRuntimeProvider>>>());
}
#[tokio::test]
async fn test_lookup_google() {
subscribe();
let handle = TokioRuntimeProvider::default();
lookup_test(ResolverConfig::udp_and_tcp(&GOOGLE), handle).await;
}
#[tokio::test]
async fn test_lookup_cloudflare() {
subscribe();
let handle = TokioRuntimeProvider::default();
lookup_test(ResolverConfig::udp_and_tcp(&CLOUDFLARE), handle).await;
}
#[tokio::test]
async fn test_ip_lookup() {
subscribe();
let handle = TokioRuntimeProvider::default();
ip_lookup_test(handle).await;
}
#[test]
fn test_ip_lookup_across_threads() {
subscribe();
let handle = TokioRuntimeProvider::default();
ip_lookup_across_threads_test(handle);
}
#[tokio::test]
#[cfg(feature = "__dnssec")]
#[ignore = "flaky test against internet server"]
async fn test_sec_lookup() {
subscribe();
let handle = TokioRuntimeProvider::default();
sec_lookup_test(handle).await;
}
#[tokio::test]
#[cfg(feature = "__dnssec")]
#[ignore = "flaky test against internet server"]
async fn test_sec_lookup_fails() {
subscribe();
let handle = TokioRuntimeProvider::default();
sec_lookup_fails_test(handle).await;
}
#[tokio::test]
#[ignore]
#[cfg(any(unix, target_os = "windows"))]
#[cfg(feature = "system-config")]
async fn test_system_lookup() {
subscribe();
let handle = TokioRuntimeProvider::default();
system_lookup_test(handle).await;
}
#[tokio::test]
#[ignore]
#[cfg(all(unix, feature = "system-config"))]
async fn test_hosts_lookup() {
subscribe();
let handle = TokioRuntimeProvider::default();
hosts_lookup_test(handle).await;
}
#[tokio::test]
async fn test_fqdn() {
subscribe();
let handle = TokioRuntimeProvider::default();
fqdn_test(handle).await;
}
#[tokio::test]
async fn test_ndots() {
subscribe();
let handle = TokioRuntimeProvider::default();
ndots_test(handle).await;
}
#[tokio::test]
async fn test_large_ndots() {
subscribe();
let handle = TokioRuntimeProvider::default();
large_ndots_test(handle).await;
}
#[tokio::test]
async fn test_domain_search() {
subscribe();
let handle = TokioRuntimeProvider::default();
domain_search_test(handle).await;
}
#[tokio::test]
async fn test_search_list() {
subscribe();
let handle = TokioRuntimeProvider::default();
search_list_test(handle).await;
}
#[tokio::test]
async fn test_idna() {
subscribe();
let handle = TokioRuntimeProvider::default();
idna_test(handle).await;
}
#[tokio::test]
async fn test_localhost_ipv4() {
subscribe();
let handle = TokioRuntimeProvider::default();
localhost_ipv4_test(handle).await;
}
#[tokio::test]
async fn test_localhost_ipv6() {
subscribe();
let handle = TokioRuntimeProvider::default();
localhost_ipv6_test(handle).await;
}
#[tokio::test]
async fn test_search_ipv4_large_ndots() {
subscribe();
let handle = TokioRuntimeProvider::default();
search_ipv4_large_ndots_test(handle).await;
}
#[tokio::test]
async fn test_search_ipv6_large_ndots() {
subscribe();
let handle = TokioRuntimeProvider::default();
search_ipv6_large_ndots_test(handle).await;
}
#[tokio::test]
async fn test_search_ipv6_name_parse_fails() {
subscribe();
let handle = TokioRuntimeProvider::default();
search_ipv6_name_parse_fails_test(handle).await;
}
#[test]
fn test_build_names() {
use std::str::FromStr;
let handle = TokioRuntimeProvider::default();
let mut config = ResolverConfig::udp_and_tcp(&GOOGLE);
config.add_search(Name::from_ascii("example.com.").unwrap());
let resolver = Resolver::builder_with_config(config, handle)
.build()
.unwrap();
assert_eq!(resolver.build_names(Name::from_str("").unwrap()).len(), 2);
assert_eq!(resolver.build_names(Name::from_str(".").unwrap()).len(), 1);
let fqdn = Name::from_str("foo.example.com.").unwrap();
let name_list = resolver.build_names(Name::from_str("foo").unwrap());
assert!(name_list.contains(&fqdn));
let name_list = resolver.build_names(fqdn.clone());
assert_eq!(name_list.len(), 1);
assert_eq!(name_list.first(), Some(&fqdn));
}
#[test]
fn test_build_names_onion() {
let handle = TokioRuntimeProvider::default();
let mut config = ResolverConfig::udp_and_tcp(&GOOGLE);
config.add_search(Name::from_ascii("example.com.").unwrap());
let resolver = Resolver::builder_with_config(config, handle)
.build()
.unwrap();
let tor_address = [
Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
.unwrap(),
Name::from_ascii("www.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion")
.unwrap(), ];
let not_tor_address = [
Name::from_ascii("onion").unwrap(),
Name::from_ascii("www.onion").unwrap(),
Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.www.onion")
.unwrap(), Name::from_ascii("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion.to")
.unwrap(), ];
for name in &tor_address {
assert_eq!(resolver.build_names(name.clone()).len(), 1);
}
for name in ¬_tor_address {
assert_eq!(resolver.build_names(name.clone()).len(), 2);
}
}
#[tokio::test]
async fn test_lookup() {
assert_eq!(
LookupFuture::lookup(
vec![Name::root()],
RecordType::A,
DnsRequestOptions::default(),
CachingClient::new(0, mock(vec![v4_message()]), false),
)
.await
.unwrap()
.answers()
.iter()
.map(|r| r.data().ip_addr().unwrap())
.collect::<Vec<IpAddr>>(),
vec![Ipv4Addr::LOCALHOST]
);
}
#[tokio::test]
async fn test_lookup_slice() {
assert_eq!(
Record::data(
&LookupFuture::lookup(
vec![Name::root()],
RecordType::A,
DnsRequestOptions::default(),
CachingClient::new(0, mock(vec![v4_message()]), false),
)
.await
.unwrap()
.answers()[0]
)
.ip_addr()
.unwrap(),
Ipv4Addr::LOCALHOST
);
}
#[tokio::test]
async fn test_lookup_into_iter() {
assert_eq!(
LookupFuture::lookup(
vec![Name::root()],
RecordType::A,
DnsRequestOptions::default(),
CachingClient::new(0, mock(vec![v4_message()]), false),
)
.await
.unwrap()
.answers()
.iter()
.map(|r| r.data().ip_addr().unwrap())
.collect::<Vec<IpAddr>>(),
vec![Ipv4Addr::LOCALHOST]
);
}
#[tokio::test]
async fn test_error() {
assert!(
LookupFuture::lookup(
vec![Name::root()],
RecordType::A,
DnsRequestOptions::default(),
CachingClient::new(0, mock(vec![error()]), false),
)
.await
.is_err()
);
}
#[tokio::test]
async fn test_empty_no_response() {
let error = LookupFuture::lookup(
vec![Name::root()],
RecordType::A,
DnsRequestOptions::default(),
CachingClient::new(0, mock(vec![empty()]), false),
)
.await
.expect_err("this should have been a NoRecordsFound");
let NetError::Dns(DnsError::NoRecordsFound(no_records)) = error else {
panic!("wrong error received");
};
assert_eq!(*no_records.query, Query::query(Name::root(), RecordType::A));
assert_eq!(no_records.negative_ttl, None);
}
#[derive(Clone)]
struct MockDnsHandle {
messages: Arc<Mutex<Vec<Result<DnsResponse, NetError>>>>,
}
impl DnsHandle for MockDnsHandle {
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, NetError>> + Send>>;
type Runtime = TokioRuntimeProvider;
fn send(&self, _: DnsRequest) -> Self::Response {
Box::pin(once(future::ready(
self.messages.lock().unwrap().pop().unwrap_or_else(empty),
)))
}
}
fn v4_message() -> Result<DnsResponse, NetError> {
let mut message = Message::query();
message.add_query(Query::query(Name::root(), RecordType::A));
message.insert_answers(vec![Record::from_rdata(
Name::root(),
86400,
RData::A(A::new(127, 0, 0, 1)),
)]);
let resp = DnsResponse::from_message(message.into_response()).unwrap();
assert!(resp.contains_answer());
Ok(resp)
}
fn empty() -> Result<DnsResponse, NetError> {
Ok(DnsResponse::from_message(Message::query().into_response()).unwrap())
}
fn error() -> Result<DnsResponse, NetError> {
Err(NetError::from(std::io::Error::from(
std::io::ErrorKind::Other,
)))
}
fn mock(messages: Vec<Result<DnsResponse, NetError>>) -> MockDnsHandle {
MockDnsHandle {
messages: Arc::new(Mutex::new(messages)),
}
}
}