Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
70cc19bf67 | |||
67649863fa | |||
338b35bc25 | |||
6e251e348d | |||
c669e3d397 | |||
999cdf4950 | |||
fd265a9ae4 | |||
c43bef87f9 | |||
2aa98d0799 | |||
50e0653373 | |||
d95b4202b2 | |||
9c6b064dba | |||
80f2a17bff | |||
ec4e22817a | |||
591a4a9fb2 | |||
f6b7fc1287 | |||
6e4af5c549 |
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -966,6 +966,7 @@ dependencies = [
|
|||
"futures",
|
||||
"hickory-client",
|
||||
"hickory-proto",
|
||||
"hickory-recursor",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"once_cell",
|
||||
|
|
|
@ -511,7 +511,7 @@ mod tests {
|
|||
priority: 1,
|
||||
params: vec![(
|
||||
SvcParamKey::Key(667),
|
||||
SvcParamValue::Unknown(Unknown(b"hello".into())),
|
||||
SvcParamValue::Unknown(Unknown((*b"hello").into())),
|
||||
)],
|
||||
},
|
||||
// Figure 6: A Generic Key and Quoted Value with a Decimal Escape
|
||||
|
@ -522,7 +522,7 @@ mod tests {
|
|||
priority: 1,
|
||||
params: vec![(
|
||||
SvcParamKey::Key(667),
|
||||
SvcParamValue::Unknown(Unknown(b"hello\\210qoo".into())),
|
||||
SvcParamValue::Unknown(Unknown((*b"hello\\210qoo").into())),
|
||||
)],
|
||||
},
|
||||
// Figure 7: Two Quoted IPv6 Hints
|
||||
|
|
|
@ -12,6 +12,7 @@ use futures_util::{future::select_all, FutureExt};
|
|||
use hickory_resolver::name_server::TokioConnectionProvider;
|
||||
use lru_cache::LruCache;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -28,7 +29,7 @@ use crate::{
|
|||
dns_lru::{DnsLru, TtlConfig},
|
||||
error::ResolveError,
|
||||
lookup::Lookup,
|
||||
name_server::{GenericNameServerPool, TokioRuntimeProvider},
|
||||
name_server::{ConnectionProvider, GenericConnector, GenericNameServerPool, NameServerPool, TokioRuntimeProvider},
|
||||
Name,
|
||||
},
|
||||
Error, ErrorKind,
|
||||
|
@ -40,13 +41,35 @@ type NameServerCache<P> = LruCache<Name, RecursorPool<P>>;
|
|||
/// A top down recursive resolver which operates off a list of roots for initial recursive requests.
|
||||
///
|
||||
/// This is the well known root nodes, referred to as hints in RFCs. See the IANA [Root Servers](https://www.iana.org/domains/root/servers) list.
|
||||
pub struct Recursor {
|
||||
roots: RecursorPool<TokioRuntimeProvider>,
|
||||
name_server_cache: Mutex<NameServerCache<TokioRuntimeProvider>>,
|
||||
pub struct Recursor<P: ConnectionProvider=TokioConnectionProvider> {
|
||||
roots: RecursorPool<P>,
|
||||
name_server_cache: Mutex<NameServerCache<P>>,
|
||||
record_cache: DnsLru,
|
||||
}
|
||||
|
||||
impl Recursor {
|
||||
|
||||
impl<P: ConnectionProvider> Recursor<P> {
|
||||
/// Construct a new recursor using a custom name server pool.
|
||||
/// You likely want to use `new` instead.
|
||||
pub fn new_with_pool(
|
||||
roots: NameServerPool<P>,
|
||||
ns_cache_size: usize,
|
||||
record_cache_size: usize,
|
||||
) -> Result<Self, ResolveError> {
|
||||
let roots = RecursorPool::from(Name::root(), roots);
|
||||
debug!("Using cache sizes {}/{}", ns_cache_size, record_cache_size);
|
||||
let name_server_cache = Mutex::new(NameServerCache::new(ns_cache_size));
|
||||
let record_cache = DnsLru::new(record_cache_size, TtlConfig::default());
|
||||
|
||||
Ok(Self {
|
||||
roots,
|
||||
name_server_cache,
|
||||
record_cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: ConnectionProvider + Default> Recursor<P> {
|
||||
/// Construct a new recursor using the list of NameServerConfigs for the root node list
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -62,19 +85,10 @@ impl Recursor {
|
|||
|
||||
assert!(!roots.is_empty(), "roots must not be empty");
|
||||
|
||||
debug!("Using cache sizes {}/{}", ns_cache_size, record_cache_size);
|
||||
let opts = recursor_opts();
|
||||
let roots =
|
||||
GenericNameServerPool::from_config(roots, opts, TokioConnectionProvider::default());
|
||||
let roots = RecursorPool::from(Name::root(), roots);
|
||||
let name_server_cache = Mutex::new(NameServerCache::new(ns_cache_size));
|
||||
let record_cache = DnsLru::new(record_cache_size, TtlConfig::default());
|
||||
let roots = NameServerPool::from_config(roots, opts, P::default());
|
||||
|
||||
Ok(Self {
|
||||
roots,
|
||||
name_server_cache,
|
||||
record_cache,
|
||||
})
|
||||
Self::new_with_pool(roots, ns_cache_size, record_cache_size)
|
||||
}
|
||||
|
||||
/// Perform a recursive resolution
|
||||
|
@ -239,8 +253,45 @@ impl Recursor {
|
|||
/// contiguous zone at ISI.EDU.
|
||||
/// ```
|
||||
pub async fn resolve(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
|
||||
if let Some(lookup) = self.record_cache.get(&query, request_time) {
|
||||
return lookup.map_err(Into::into);
|
||||
let mut all_found = Lookup::new_with_max_ttl(query.clone(), Arc::new([]));
|
||||
let mut active_query = query.clone();
|
||||
// max number of CNAMEs to traverse
|
||||
const MAX_FOLLOWS: usize = 20;
|
||||
for iter in 0..MAX_FOLLOWS {
|
||||
let lookup = self.resolve_once(active_query.clone(), request_time).await?;
|
||||
|
||||
if lookup.query() == &query {
|
||||
// Resolved as expected: done!
|
||||
all_found = all_found.append(lookup);
|
||||
debug!("resolve complete in iteration {}", iter);
|
||||
return Ok(all_found);
|
||||
}
|
||||
// Extract whichever CNAME the server told us of
|
||||
let mut cnames = lookup.records().iter().filter_map(|r| match r.data() {
|
||||
Some(RData::CNAME(target)) if r.name() == active_query.name() => Some(target),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(cname_data) = cnames.next() {
|
||||
debug!("resolve to CNAME target: {}", *cname_data);
|
||||
active_query.set_name((**cname_data).clone());
|
||||
all_found = all_found.append(lookup);
|
||||
} else {
|
||||
// The server gave us neither the expected record, nor any CNAMEs to follow.
|
||||
// This is probably an error.
|
||||
all_found = all_found.append(lookup);
|
||||
debug!("resolve complete in iteration {}: no more CNAMEs to follow", iter);
|
||||
return Ok(all_found);
|
||||
}
|
||||
}
|
||||
debug!("giving up after following {} CNAMEs", MAX_FOLLOWS);
|
||||
Err(ErrorKind::Timeout.into())
|
||||
}
|
||||
|
||||
/// Recursively resolve a query until we find the record, or reach a CNAME that must be
|
||||
/// manually dereferenced.
|
||||
async fn resolve_once(&self, query: Query, request_time: Instant) -> Result<Lookup, Error> {
|
||||
if let Some(lookup) = self.get_cached_query_or_cname(&query, request_time) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
// not in cache, let's look for an ns record for lookup
|
||||
|
@ -284,15 +335,17 @@ impl Recursor {
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
/// Perform a single DNS query, checking/updating the cache.
|
||||
/// In case that the nameserver answers to a non-CNAME query with a CNAME, this function will
|
||||
/// return the CNAME. It's up to the caller to expand the answer further.
|
||||
async fn lookup(
|
||||
&self,
|
||||
query: Query,
|
||||
ns: RecursorPool<TokioRuntimeProvider>,
|
||||
ns: RecursorPool<P>,
|
||||
now: Instant,
|
||||
) -> Result<Lookup, Error> {
|
||||
if let Some(lookup) = self.record_cache.get(&query, now) {
|
||||
debug!("cached data {lookup:?}");
|
||||
return lookup.map_err(Into::into);
|
||||
if let Some(lookup) = self.get_cached_query_or_cname(&query, now) {
|
||||
return lookup;
|
||||
}
|
||||
|
||||
let response = ns.lookup(query.clone());
|
||||
|
@ -321,7 +374,17 @@ impl Recursor {
|
|||
}
|
||||
});
|
||||
|
||||
let lookup = self.record_cache.insert_records(query, records, now);
|
||||
let lookup = self.record_cache.insert_records(query.clone(), records, now);
|
||||
let lookup = lookup.or_else(|| {
|
||||
if query.query_type().is_cname() || query.query_type().is_any() {
|
||||
None
|
||||
} else {
|
||||
debug!("no records for {}: checking for cached CNAME", query.name());
|
||||
let mut cname_query = query;
|
||||
cname_query.set_query_type(RecordType::CNAME);
|
||||
self.record_cache.get(&cname_query, now).and_then(Result::ok)
|
||||
}
|
||||
});
|
||||
|
||||
lookup.ok_or_else(|| Error::from("no records found"))
|
||||
}
|
||||
|
@ -337,7 +400,7 @@ impl Recursor {
|
|||
&self,
|
||||
zone: Name,
|
||||
request_time: Instant,
|
||||
) -> Result<RecursorPool<TokioRuntimeProvider>, Error> {
|
||||
) -> Result<RecursorPool<P>, Error> {
|
||||
// TODO: need to check TTLs here.
|
||||
if let Some(ns) = self.name_server_cache.lock().get_mut(&zone) {
|
||||
return Ok(ns.clone());
|
||||
|
@ -465,10 +528,10 @@ impl Recursor {
|
|||
}
|
||||
|
||||
// now construct a namesever pool based off the NS and glue records
|
||||
let ns = GenericNameServerPool::from_config(
|
||||
let ns = NameServerPool::from_config(
|
||||
config_group,
|
||||
recursor_opts(),
|
||||
TokioConnectionProvider::default(),
|
||||
Default::default(),
|
||||
);
|
||||
let ns = RecursorPool::from(zone.clone(), ns);
|
||||
|
||||
|
@ -477,6 +540,28 @@ impl Recursor {
|
|||
self.name_server_cache.lock().insert(zone, ns.clone());
|
||||
Ok(ns)
|
||||
}
|
||||
|
||||
/// Return cached data for the precise query, if present,
|
||||
/// falling back to aliased data if an answer compatible with the given query exists.
|
||||
///
|
||||
/// For example, a query for `www.example.com. A` could yield `www.example.com. A 1.2.3.4`
|
||||
/// OR it could yield `www.example.com. CNAME other.example.com.`.
|
||||
fn get_cached_query_or_cname(&self, query: &Query, request_time: Instant) -> Option<Result<Lookup, Error>> {
|
||||
if let Some(lookup) = self.record_cache.get(query, request_time) {
|
||||
debug!("cached data {lookup:?}");
|
||||
return Some(lookup.map_err(Into::into));
|
||||
}
|
||||
if !query.query_type().is_cname() && !query.query_type().is_any() {
|
||||
// If trying to lookup e.g. an A record, check if it's actually a record we've looked
|
||||
// up before, behind a CNAME.
|
||||
let mut cname_query = query.clone();
|
||||
cname_query.set_query_type(RecordType::CNAME);
|
||||
if let Some(lookup) = self.record_cache.get(&cname_query, request_time) {
|
||||
return Some(lookup.map_err(Into::into));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn recursor_opts() -> ResolverOpts {
|
||||
|
|
|
@ -18,10 +18,9 @@ use hickory_proto::{
|
|||
xfer::{DnsRequestOptions, DnsResponse},
|
||||
DnsHandle,
|
||||
};
|
||||
use hickory_resolver::name_server::{RuntimeProvider, TokioRuntimeProvider};
|
||||
use hickory_resolver::{
|
||||
error::{ResolveError, ResolveErrorKind},
|
||||
name_server::GenericNameServerPool,
|
||||
name_server::{ConnectionProvider, NameServerPool},
|
||||
Name,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
@ -50,14 +49,14 @@ impl Future for SharedLookup {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RecursorPool<P: RuntimeProvider + Send + 'static> {
|
||||
pub(crate) struct RecursorPool<P: ConnectionProvider> {
|
||||
zone: Name,
|
||||
ns: GenericNameServerPool<P>,
|
||||
ns: NameServerPool<P>,
|
||||
active_requests: Arc<Mutex<ActiveRequests>>,
|
||||
}
|
||||
|
||||
impl RecursorPool<TokioRuntimeProvider> {
|
||||
pub(crate) fn from(zone: Name, ns: GenericNameServerPool<TokioRuntimeProvider>) -> Self {
|
||||
impl<P: ConnectionProvider> RecursorPool<P> {
|
||||
pub(crate) fn from(zone: Name, ns: NameServerPool<P>) -> Self {
|
||||
let active_requests = Arc::new(Mutex::new(ActiveRequests::default()));
|
||||
|
||||
Self {
|
||||
|
@ -68,10 +67,7 @@ impl RecursorPool<TokioRuntimeProvider> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<P> RecursorPool<P>
|
||||
where
|
||||
P: RuntimeProvider + Send + 'static,
|
||||
{
|
||||
impl<P: ConnectionProvider> RecursorPool<P> {
|
||||
pub(crate) fn zone(&self) -> &Name {
|
||||
&self.zone
|
||||
}
|
||||
|
|
|
@ -129,7 +129,8 @@ impl Lookup {
|
|||
}
|
||||
|
||||
/// Clones the inner vec, appends the other vec
|
||||
pub(crate) fn append(&self, other: Self) -> Self {
|
||||
#[doc(hidden)]
|
||||
pub fn append(&self, other: Self) -> Self {
|
||||
let mut records = Vec::with_capacity(self.len() + other.len());
|
||||
records.extend_from_slice(&self.records);
|
||||
records.extend_from_slice(&other.records);
|
||||
|
|
26
flake.lock
Normal file
26
flake.lock
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1719234068,
|
||||
"narHash": "sha256-1AjSIedDC/aERt24KsCUftLpVppW61S7awfjGe7bMio=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "90bd1b26e23760742fdcb6152369919098f05417",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-23.11",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
22
flake.nix
Normal file
22
flake.nix
Normal file
|
@ -0,0 +1,22 @@
|
|||
# enter a dev shell with: `nix develop`
|
||||
{
|
||||
inputs.nixpkgs.url = "nixpkgs/nixos-23.11";
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
in with self.outputs.packages.x86_64-linux;
|
||||
{
|
||||
devShells.x86_64-linux.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
openssl
|
||||
pkg-config
|
||||
rustc
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ tokio = { workspace = true, features = ["time", "rt"] }
|
|||
tracing.workspace = true
|
||||
hickory-client.workspace = true
|
||||
hickory-proto = { workspace = true, features = ["testing"] }
|
||||
hickory-recursor = { workspace = true }
|
||||
hickory-resolver = { workspace = true, features = ["tokio-runtime"] }
|
||||
hickory-server = { workspace = true, features = ["testing"] }
|
||||
webpki-roots = { workspace = true, optional = true }
|
||||
|
|
|
@ -14,7 +14,7 @@ use futures::stream::{once, Stream};
|
|||
use futures::{future, AsyncRead, AsyncWrite, Future};
|
||||
|
||||
use hickory_client::op::{Message, Query};
|
||||
use hickory_client::rr::rdata::{CNAME, SOA};
|
||||
use hickory_client::rr::rdata::{CNAME, NS, SOA};
|
||||
use hickory_client::rr::{Name, RData, Record};
|
||||
use hickory_proto::error::ProtoError;
|
||||
use hickory_proto::tcp::DnsTcpStream;
|
||||
|
@ -127,7 +127,7 @@ impl RuntimeProvider for MockRuntimeProvider {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MockConnProvider<O: OnSend + Unpin> {
|
||||
pub on_send: O,
|
||||
}
|
||||
|
@ -211,6 +211,10 @@ pub fn soa_record(name: Name, mname: Name) -> Record {
|
|||
Record::from_rdata(name, 86400, RData::SOA(soa))
|
||||
}
|
||||
|
||||
pub fn ns_record(name: Name, ns: Name) -> Record {
|
||||
Record::from_rdata(name, 86400, RData::NS(NS(ns)))
|
||||
}
|
||||
|
||||
pub fn message(
|
||||
query: Query,
|
||||
answers: Vec<Record>,
|
||||
|
@ -245,7 +249,7 @@ pub trait OnSend: Clone + Send + Sync + 'static {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DefaultOnSend;
|
||||
|
||||
impl OnSend for DefaultOnSend {}
|
||||
|
|
635
tests/integration-tests/tests/recursor_tests.rs
Normal file
635
tests/integration-tests/tests/recursor_tests.rs
Normal file
|
@ -0,0 +1,635 @@
|
|||
// Copyright 2015-2022 Benjamin Fry <benjaminfry@me.com>
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
||||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
||||
// http://opensource.org/licenses/MIT>, at your option. This file may not be
|
||||
// copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! Integration tests for the recursor. These integration tests setup scenarios to verify that the recursor is able
|
||||
//! to recursively resolve various real world scenarios. As new scenarios are discovered, they should be added here.
|
||||
|
||||
use std::future::Future;
|
||||
use std::net::*;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use futures::stream::{self, Stream};
|
||||
use futures::future;
|
||||
use futures::StreamExt as _;
|
||||
use futures::executor::block_on;
|
||||
|
||||
use hickory_client::op::{Message, MessageType, Query};
|
||||
use hickory_client::rr::{rdata, Name, RData, Record, RecordType};
|
||||
use hickory_integration::mock_client::*;
|
||||
use hickory_proto::DnsHandle;
|
||||
use hickory_proto::error::{ProtoError, ProtoErrorKind};
|
||||
use hickory_proto::rr::LowerName;
|
||||
use hickory_proto::serialize::txt::Parser;
|
||||
use hickory_proto::xfer::{DnsRequest, DnsResponse};
|
||||
use hickory_recursor::{ErrorKind, Recursor};
|
||||
use hickory_resolver::config::*;
|
||||
use hickory_resolver::error::ResolveErrorKind;
|
||||
use hickory_resolver::name_server::ConnectionProvider;
|
||||
use hickory_resolver::name_server::{NameServer, NameServerPool};
|
||||
use hickory_server::authority::{Catalog, LookupError, LookupOptions, ZoneType};
|
||||
use hickory_server::store::in_memory::InMemoryAuthority;
|
||||
|
||||
|
||||
const NS_ROOT: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
const NS_TLDS: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2));
|
||||
const NS_IANA: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3));
|
||||
const NS_ORG: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 4));
|
||||
|
||||
|
||||
const ZONE_ROOT: &str = r#"
|
||||
@ IN SOA a.root-servers.net. nstld.verisign-grs.com. (
|
||||
20 ; SERIAL
|
||||
7200 ; REFRESH
|
||||
600 ; RETRY
|
||||
3600000; EXPIRE
|
||||
60) ; MINIMUM
|
||||
|
||||
. NS a.root-servers.net.
|
||||
a.root-servers.net A 10.0.0.1
|
||||
|
||||
com NS a.gtld-servers.net.
|
||||
a.gtld-servers.net A 10.0.0.2
|
||||
|
||||
net NS a.gtld-servers.net.
|
||||
|
||||
org NS b0.org.afilias-nst.org.
|
||||
b0.org.afilias-nst.org. A 10.0.0.4
|
||||
|
||||
us NS b.cctld.us.
|
||||
"#;
|
||||
|
||||
const ZONE_TLDS: &str = r#"
|
||||
@ IN SOA av4.nstld.com. nstld.verisign-grs.com. (
|
||||
20 ; SERIAL
|
||||
7200 ; REFRESH
|
||||
600 ; RETRY
|
||||
3600000; EXPIRE
|
||||
60) ; MINIMUM
|
||||
|
||||
com. TXT my-text-record
|
||||
example.com. NS a.iana-servers.net.
|
||||
example.org. NS a.iana-servers.net.
|
||||
iana-servers.net. NS a.iana-servers.net.
|
||||
a.iana-servers.net. A 10.0.0.3
|
||||
"#;
|
||||
|
||||
|
||||
const ZONE_IANA: &str = r#"
|
||||
@ IN SOA a.iana-servers.net. noc.dns.icann.org. (
|
||||
20 ; SERIAL
|
||||
7200 ; REFRESH
|
||||
600 ; RETRY
|
||||
3600000; EXPIRE
|
||||
60) ; MINIMUM
|
||||
|
||||
a.iana-servers.net. A 10.0.0.3
|
||||
"#;
|
||||
|
||||
const ZONE_EXAMPLE_COM: &str = r#"
|
||||
@ IN SOA a.iana-servers.net. noc.dns.icann.org. (
|
||||
20 ; SERIAL
|
||||
7200 ; REFRESH
|
||||
600 ; RETRY
|
||||
3600000; EXPIRE
|
||||
60) ; MINIMUM
|
||||
|
||||
example.com. A 10.0.100.1
|
||||
www.example.com. A 10.0.100.1
|
||||
cname.sub.example.com. CNAME www.example.com.
|
||||
cname.example.com. CNAME www.example.com.
|
||||
example-org-cname.example.com. CNAME example.org.
|
||||
inline-org-cname.example.com. CNAME inline.org.
|
||||
double-cname.sub.example.com. CNAME inline-org-cname.example.org.
|
||||
cycle-a.example.com. CNAME cycle-b.org.
|
||||
intractable-cname.example.com. CNAME nonexistent.org.
|
||||
"#;
|
||||
|
||||
const ZONE_ORG: &str = r#"
|
||||
@ IN SOA a0.org.afilias-nst.info. hostmaster.donuts.email. 20 7200 600 3600000 60
|
||||
|
||||
example.org. NS a.iana-servers.net.
|
||||
inline.org. A 10.0.100.3
|
||||
cycle-b.org. CNAME cycle-a.example.com.
|
||||
"#;
|
||||
|
||||
const ZONE_EXAMPLE_ORG: &str = r#"
|
||||
@ IN SOA a.iana-servers.net. noc.dns.icann.org. (
|
||||
20 ; SERIAL
|
||||
7200 ; REFRESH
|
||||
600 ; RETRY
|
||||
3600000; EXPIRE
|
||||
60) ; MINIMUM
|
||||
|
||||
example.org. A 10.0.100.2
|
||||
inline-org-cname.example.org. CNAME inline.org.
|
||||
"#;
|
||||
|
||||
type HardcodedNameServer = NameServer<HardcodedConnProvider>;
|
||||
|
||||
/// A ConnectionProvider which includes hard-coded DNS test data shared across tests.
|
||||
#[derive(Clone, Default)]
|
||||
struct HardcodedConnProvider;
|
||||
impl ConnectionProvider for HardcodedConnProvider {
|
||||
type Conn = HardcodedDnsHandle;
|
||||
type FutureConn = Pin<Box<dyn Send + Future<Output = Result<Self::Conn, ProtoError>>>>;
|
||||
type RuntimeProvider = MockRuntimeProvider;
|
||||
|
||||
fn new_connection(
|
||||
&self,
|
||||
config: &NameServerConfig,
|
||||
_options: &ResolverOpts,
|
||||
) -> Self::FutureConn {
|
||||
println!("HardcodedConnProvider::new_connection");
|
||||
Box::pin(future::ok(HardcodedDnsHandle(config.socket_addr)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HardcodedDnsHandle(SocketAddr);
|
||||
|
||||
impl DnsHandle for HardcodedDnsHandle {
|
||||
type Response = Pin<Box<dyn Stream<Item = Result<DnsResponse, ProtoError>> + Send>>;
|
||||
|
||||
fn send<R: Into<DnsRequest>>(&self, request: R) -> Self::Response {
|
||||
let addr = self.0;
|
||||
let request = request.into();
|
||||
// let queries: Vec<_> = request.queries().iter().collect();
|
||||
println!("HardcodedDnsHandle::send request: {:?}", request.queries());
|
||||
let response_futures: Vec<_> = request.queries().iter().map(move |query| {
|
||||
let query = query.clone();
|
||||
stream::once(async move {
|
||||
println!("HardcodedDnsHandle: fielding query: {:?} {:?}", query.name(), query.query_type());
|
||||
let mut response = Message::new();
|
||||
response.add_query(query.clone());
|
||||
response.set_message_type(MessageType::Response);
|
||||
let query_lower = LowerName::new(query.name());
|
||||
let catalog = make_catalog_for(addr);
|
||||
let authority = catalog.find(&query_lower);
|
||||
if let Some(authority) = authority {
|
||||
let lookup = authority.lookup(
|
||||
&query_lower,
|
||||
query.query_type(),
|
||||
LookupOptions::default(),
|
||||
).await;
|
||||
// println!("HardcodeDnsHandle: result {:?}", lookup);
|
||||
match lookup {
|
||||
Ok(mut lookup_obj) => {
|
||||
response.add_answers(lookup_obj.iter().cloned());
|
||||
if let Some(additionals_obj) = lookup_obj.take_additionals() {
|
||||
response.add_additionals(additionals_obj.iter().cloned());
|
||||
}
|
||||
},
|
||||
Err(LookupError::ResponseCode(code)) => {
|
||||
response.set_response_code(code);
|
||||
},
|
||||
Err(LookupError::NameExists) => {
|
||||
if !query.query_type().is_soa() {
|
||||
println!("nx_domain and not soa, lookup: {:?} SOA", authority.origin());
|
||||
let soa_lookup = authority.lookup(
|
||||
authority.origin(),
|
||||
RecordType::SOA,
|
||||
LookupOptions::default(),
|
||||
).await;
|
||||
// println!("HardcodeDnsHandle: revised result {:?}", lookup);
|
||||
if let Ok(r) = soa_lookup {
|
||||
response.add_name_servers(r.iter().cloned());
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
// response.set_header(*response.header().clone()
|
||||
// .set_query_count(response.queries().len() as _)
|
||||
// .set_answer_count(response.answers().len() as _)
|
||||
// .set_name_server_count(response.name_servers().len() as _)
|
||||
// .set_additional_count(response.additionals().len() as _)
|
||||
// );
|
||||
let resp = DnsResponse::from_message(response).unwrap();
|
||||
Ok(resp)
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Box::pin(stream::iter(response_futures).flatten())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn make_catalog_for(nameserver: SocketAddr) -> Catalog {
|
||||
println!("retrieving catalog for {:?}...", nameserver);
|
||||
let zones = match nameserver {
|
||||
s if s == SocketAddr::new(NS_ROOT, 53) => vec![
|
||||
(".", ZONE_ROOT),
|
||||
],
|
||||
s if s == SocketAddr::new(NS_TLDS, 53) => vec![
|
||||
(".", ZONE_TLDS),
|
||||
],
|
||||
s if s == SocketAddr::new(NS_IANA, 53) => vec![
|
||||
("iana-servers.net.", ZONE_IANA),
|
||||
("example.com.", ZONE_EXAMPLE_COM),
|
||||
("example.org.", ZONE_EXAMPLE_ORG),
|
||||
],
|
||||
s if s == SocketAddr::new(NS_ORG, 53) => vec![
|
||||
("org.", ZONE_ORG),
|
||||
],
|
||||
ns => panic!("unexpected nameserver {:?}", ns),
|
||||
};
|
||||
let mut catalog = Catalog::new();
|
||||
for (name, zone_text) in zones {
|
||||
let (origin, records) = Parser::new(zone_text, None, Some(Name::from_str(name).unwrap())).parse().unwrap();
|
||||
let authority = InMemoryAuthority::new(origin.clone(), records, ZoneType::Primary, false /* allow_axfr */).unwrap();
|
||||
catalog.upsert(
|
||||
LowerName::new(&origin),
|
||||
Box::new(Arc::new(authority)),
|
||||
)
|
||||
}
|
||||
catalog
|
||||
}
|
||||
|
||||
pub fn logger(level: &str) {
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
// Setup tracing for logging based on input
|
||||
let subscriber = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::metadata::LevelFilter::OFF.into())
|
||||
.parse(level)
|
||||
.expect("failed to configure tracing/logging");
|
||||
|
||||
let formatter = tracing_subscriber::fmt::layer().compact();
|
||||
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(formatter)
|
||||
.with(subscriber)
|
||||
.try_init();
|
||||
}
|
||||
|
||||
fn mock_nameserver(addr: IpAddr) -> HardcodedNameServer {
|
||||
NameServer::new(
|
||||
NameServerConfig {
|
||||
socket_addr: SocketAddr::new(addr, 53),
|
||||
protocol: Protocol::Udp,
|
||||
tls_dns_name: None,
|
||||
trust_negative_responses: false,
|
||||
#[cfg(any(feature = "dns-over-rustls", feature = "dns-over-https-rustls"))]
|
||||
tls_config: None,
|
||||
bind_addr: None,
|
||||
},
|
||||
ResolverOpts::default(),
|
||||
HardcodedConnProvider,
|
||||
)
|
||||
}
|
||||
|
||||
/// Tests a basic recursive resolution `a.recursive.test.` , `.` -> `test.` -> `recursive.test.` -> `a.recursive.test.`
|
||||
///
|
||||
/// There are three authorities needed for this test `.` which contains the `test` nameserver, `recursive.test` which is
|
||||
/// target zone containing `a.recursive.test.`.
|
||||
#[test]
|
||||
fn test_basic_recursion() {
|
||||
// TBD
|
||||
}
|
||||
|
||||
/// Query the root for its nameserver. This is a single DNS request, no recursion necessary.
|
||||
#[test]
|
||||
fn test_root_ns() {
|
||||
logger("DEBUG");
|
||||
let ns_query = Query::query(Name::from_str(".").unwrap(), RecordType::NS);
|
||||
let ns_record = ns_record(Name::from_str(".").unwrap(), Name::from_str("a.root-servers.net.").unwrap());
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(ns_query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[ns_record]);
|
||||
}
|
||||
|
||||
/// Query a top-level domain for its nameserver. `.` (NS) -> `com.` (NS).
|
||||
/// This requires two DNS requests/iterations, but doesn't actually cross NS boundaries.
|
||||
#[test]
|
||||
fn test_tld_ns() {
|
||||
logger("DEBUG");
|
||||
|
||||
let ns_query = Query::query(Name::from_str("com.").unwrap(), RecordType::NS);
|
||||
let com_ns_record = ns_record(Name::from_str("com.").unwrap(), Name::from_str("a.gtld-servers.net").unwrap());
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(ns_query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[com_ns_record]);
|
||||
}
|
||||
|
||||
/// Query a top-level domain for a non-NS record. `.` (NS) -> `com.` (NS) -> `com.` (TXT)
|
||||
/// This requires two DNS requests/iterations, but doesn't actually cross NS boundaries.
|
||||
#[test]
|
||||
fn test_tld_txt() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("com.").unwrap(), RecordType::TXT);
|
||||
let expected_record = Record::from_rdata(
|
||||
Name::from_str("com.").unwrap(),
|
||||
86400,
|
||||
RData::TXT(rdata::TXT::new(vec!["my-text-record".to_string()])),
|
||||
);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_v4_domain() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("example.com.").unwrap(), RecordType::A);
|
||||
let expected_record = v4_record(
|
||||
Name::from_str("example.com.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 1),
|
||||
);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||
}
|
||||
|
||||
/// Query a top-level domain for a record several layers deep.
|
||||
/// `.` (NS) -> `com.` (NS) -> `example.com.` (NS) -> `www.example.com.` (NS, NXDomain), `www.example.com` (A)
|
||||
#[test]
|
||||
fn test_recurse() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("www.example.com.").unwrap(), RecordType::A);
|
||||
let expected_record = v4_record(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 1),
|
||||
);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||
}
|
||||
|
||||
/// If `example.com.` is a NS and `sub.example.com.` doesn't exist, then `cname.sub.example.com.`
|
||||
/// triggers a different codepath than `cname.example.com.`. The recursor tries to query the
|
||||
/// latter as a NS (and receives a CNAME); the former is queried directly as a CNAME.
|
||||
#[test]
|
||||
fn test_cname_below_nonexistent_parent() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("cname.sub.example.com.").unwrap(), RecordType::CNAME);
|
||||
let expected_record = cname_record(
|
||||
Name::from_str("cname.sub.example.com.").unwrap(),
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||
}
|
||||
|
||||
/// `.` (NS) -> `com.` (NS) -> `example.com.` (NS) -> `cname.example.com.`.
|
||||
/// This results in a lookup for `cname.example.com. NS`, even though the record is actually CNAME.
|
||||
#[test]
|
||||
fn test_cname_under_ns() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("cname.example.com.").unwrap(), RecordType::CNAME);
|
||||
let expected_record = cname_record(
|
||||
Name::from_str("cname.example.com.").unwrap(),
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &[expected_record]);
|
||||
}
|
||||
|
||||
/// Query for an A record which is actually a CNAME record.
|
||||
/// The server should answer with CNAME, and (since the target is in the same zone) an additional
|
||||
/// A record.
|
||||
#[test]
|
||||
fn test_cname_queried_as_v4() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("cname.example.com.").unwrap(), RecordType::A);
|
||||
let expected_records = [
|
||||
cname_record(
|
||||
Name::from_str("cname.example.com.").unwrap(),
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
),
|
||||
v4_record(
|
||||
Name::from_str("www.example.com.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 1),
|
||||
),
|
||||
];
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &expected_records);
|
||||
}
|
||||
|
||||
/// Follow a CNAME from one zone to another, but where the target is still served by the same NS.
|
||||
#[test]
|
||||
fn test_cname_to_other_zone() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("example-org-cname.example.com.").unwrap(), RecordType::A);
|
||||
let expected_records = [
|
||||
cname_record(
|
||||
Name::from_str("example-org-cname.example.com.").unwrap(),
|
||||
Name::from_str("example.org.").unwrap(),
|
||||
),
|
||||
v4_record(
|
||||
Name::from_str("example.org.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 2),
|
||||
),
|
||||
];
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &expected_records);
|
||||
}
|
||||
|
||||
/// Follow a CNAME from one zone to another zone, served by a different NS.
|
||||
#[test]
|
||||
fn test_cname_to_other_ns() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("inline-org-cname.example.com.").unwrap(), RecordType::A);
|
||||
let expected_records = [
|
||||
cname_record(
|
||||
Name::from_str("inline-org-cname.example.com.").unwrap(),
|
||||
Name::from_str("inline.org.").unwrap(),
|
||||
),
|
||||
v4_record(
|
||||
Name::from_str("inline.org.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 3),
|
||||
),
|
||||
];
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &expected_records);
|
||||
}
|
||||
|
||||
/// Follow more than one CNAME to an existing record.
|
||||
#[test]
|
||||
fn test_double_cname() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("double-cname.sub.example.com.").unwrap(), RecordType::A);
|
||||
let expected_records = [
|
||||
cname_record(
|
||||
Name::from_str("double-cname.sub.example.com.").unwrap(),
|
||||
Name::from_str("inline-org-cname.example.org.").unwrap(),
|
||||
),
|
||||
cname_record(
|
||||
Name::from_str("inline-org-cname.example.org.").unwrap(),
|
||||
Name::from_str("inline.org.").unwrap(),
|
||||
),
|
||||
v4_record(
|
||||
Name::from_str("inline.org.").unwrap(),
|
||||
Ipv4Addr::new(10, 0, 100, 3),
|
||||
),
|
||||
];
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup = block_on(recursor.resolve(query, now)).unwrap();
|
||||
|
||||
assert_eq!(&*lookup.records().to_vec(), &expected_records);
|
||||
}
|
||||
|
||||
/// Test that the recursor does not get stuck when presented with an infinite cycle of CNAMEs.
|
||||
#[test]
|
||||
fn test_cname_cycle() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("cycle-a.example.com.").unwrap(), RecordType::A);
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup_err = block_on(recursor.resolve(query, now)).unwrap_err();
|
||||
|
||||
assert!(matches!(lookup_err.kind(), ErrorKind::Timeout));
|
||||
}
|
||||
|
||||
/// Test that the recursor responds with an error when following CNAMEs to non-existent records,
|
||||
/// rather than (for example) returning partial results.
|
||||
#[test]
|
||||
fn test_cname_to_nonexistent_record() {
|
||||
logger("DEBUG");
|
||||
|
||||
let query = Query::query(Name::from_str("intractable-cname.example.com.").unwrap(), RecordType::A);
|
||||
|
||||
let roots = NameServerPool::from_nameservers(
|
||||
Default::default(),
|
||||
vec![mock_nameserver(NS_ROOT)],
|
||||
vec![],
|
||||
);
|
||||
let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap();
|
||||
|
||||
let now = Instant::now();
|
||||
let lookup_err = block_on(recursor.resolve(query, now)).unwrap_err();
|
||||
|
||||
assert!(matches!(lookup_err.kind(),
|
||||
ErrorKind::Resolve(resolve_error) if matches!(resolve_error.kind(),
|
||||
ResolveErrorKind::Proto(proto_error) if matches!(proto_error.kind(),
|
||||
ProtoErrorKind::NoRecordsFound { .. }
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
|
@ -157,7 +157,7 @@ pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let name = opts.domainname;
|
||||
let ty = opts.ty;
|
||||
|
||||
let recursor = Recursor::new(roots, 1024, 1048576)?;
|
||||
let recursor: Recursor = Recursor::new(roots, 1024, 1048576)?;
|
||||
|
||||
// execute query
|
||||
println!(
|
||||
|
|
Loading…
Reference in New Issue
Block a user