recursor: define the bare minimum integration test

This commit is contained in:
Colin 2024-04-29 19:39:47 +00:00
parent 591a4a9fb2
commit ec4e22817a
6 changed files with 107 additions and 23 deletions

1
Cargo.lock generated
View File

@ -966,6 +966,7 @@ dependencies = [
"futures", "futures",
"hickory-client", "hickory-client",
"hickory-proto", "hickory-proto",
"hickory-recursor",
"hickory-resolver", "hickory-resolver",
"hickory-server", "hickory-server",
"once_cell", "once_cell",

View File

@ -28,7 +28,7 @@ use crate::{
dns_lru::{DnsLru, TtlConfig}, dns_lru::{DnsLru, TtlConfig},
error::ResolveError, error::ResolveError,
lookup::Lookup, lookup::Lookup,
name_server::{GenericNameServerPool, TokioRuntimeProvider}, name_server::{ConnectionProvider, GenericConnector, GenericNameServerPool, NameServerPool, TokioRuntimeProvider},
Name, Name,
}, },
Error, ErrorKind, Error, ErrorKind,
@ -40,13 +40,13 @@ type NameServerCache<P> = LruCache<Name, RecursorPool<P>>;
/// A top down recursive resolver which operates off a list of roots for initial recursive requests. /// 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. /// 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 { pub struct Recursor<P: ConnectionProvider> {
roots: RecursorPool<TokioRuntimeProvider>, roots: RecursorPool<P>,
name_server_cache: Mutex<NameServerCache<TokioRuntimeProvider>>, name_server_cache: Mutex<NameServerCache<P>>,
record_cache: DnsLru, record_cache: DnsLru,
} }
impl Recursor { impl Recursor<GenericConnector<TokioRuntimeProvider>> {
/// Construct a new recursor using the list of NameServerConfigs for the root node list /// Construct a new recursor using the list of NameServerConfigs for the root node list
/// ///
/// # Panics /// # Panics
@ -62,11 +62,24 @@ impl Recursor {
assert!(!roots.is_empty(), "roots must not be empty"); assert!(!roots.is_empty(), "roots must not be empty");
debug!("Using cache sizes {}/{}", ns_cache_size, record_cache_size);
let opts = recursor_opts(); let opts = recursor_opts();
let roots = let roots =
GenericNameServerPool::from_config(roots, opts, TokioConnectionProvider::default()); GenericNameServerPool::from_config(roots, opts, TokioConnectionProvider::default());
Self::new_with_pool(roots, ns_cache_size, record_cache_size)
}
}
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); 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 name_server_cache = Mutex::new(NameServerCache::new(ns_cache_size));
let record_cache = DnsLru::new(record_cache_size, TtlConfig::default()); let record_cache = DnsLru::new(record_cache_size, TtlConfig::default());
@ -76,7 +89,9 @@ impl Recursor {
record_cache, record_cache,
}) })
} }
}
impl<P: ConnectionProvider + Default> Recursor<P> {
/// Perform a recursive resolution /// Perform a recursive resolution
/// ///
/// [RFC 1034](https://datatracker.ietf.org/doc/html/rfc1034#section-5.3.3), Domain Concepts and Facilities, November 1987 /// [RFC 1034](https://datatracker.ietf.org/doc/html/rfc1034#section-5.3.3), Domain Concepts and Facilities, November 1987
@ -287,7 +302,7 @@ impl Recursor {
async fn lookup( async fn lookup(
&self, &self,
query: Query, query: Query,
ns: RecursorPool<TokioRuntimeProvider>, ns: RecursorPool<P>,
now: Instant, now: Instant,
) -> Result<Lookup, Error> { ) -> Result<Lookup, Error> {
if let Some(lookup) = self.record_cache.get(&query, now) { if let Some(lookup) = self.record_cache.get(&query, now) {
@ -337,7 +352,7 @@ impl Recursor {
&self, &self,
zone: Name, zone: Name,
request_time: Instant, request_time: Instant,
) -> Result<RecursorPool<TokioRuntimeProvider>, Error> { ) -> Result<RecursorPool<P>, Error> {
// TODO: need to check TTLs here. // TODO: need to check TTLs here.
if let Some(ns) = self.name_server_cache.lock().get_mut(&zone) { if let Some(ns) = self.name_server_cache.lock().get_mut(&zone) {
return Ok(ns.clone()); return Ok(ns.clone());
@ -465,10 +480,10 @@ impl Recursor {
} }
// now construct a namesever pool based off the NS and glue records // now construct a namesever pool based off the NS and glue records
let ns = GenericNameServerPool::from_config( let ns = NameServerPool::from_config(
config_group, config_group,
recursor_opts(), recursor_opts(),
TokioConnectionProvider::default(), Default::default(),
); );
let ns = RecursorPool::from(zone.clone(), ns); let ns = RecursorPool::from(zone.clone(), ns);

View File

@ -18,10 +18,9 @@ use hickory_proto::{
xfer::{DnsRequestOptions, DnsResponse}, xfer::{DnsRequestOptions, DnsResponse},
DnsHandle, DnsHandle,
}; };
use hickory_resolver::name_server::{RuntimeProvider, TokioRuntimeProvider};
use hickory_resolver::{ use hickory_resolver::{
error::{ResolveError, ResolveErrorKind}, error::{ResolveError, ResolveErrorKind},
name_server::GenericNameServerPool, name_server::{ConnectionProvider, NameServerPool},
Name, Name,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
@ -50,14 +49,14 @@ impl Future for SharedLookup {
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct RecursorPool<P: RuntimeProvider + Send + 'static> { pub(crate) struct RecursorPool<P: ConnectionProvider> {
zone: Name, zone: Name,
ns: GenericNameServerPool<P>, ns: NameServerPool<P>,
active_requests: Arc<Mutex<ActiveRequests>>, active_requests: Arc<Mutex<ActiveRequests>>,
} }
impl RecursorPool<TokioRuntimeProvider> { impl<P: ConnectionProvider> RecursorPool<P> {
pub(crate) fn from(zone: Name, ns: GenericNameServerPool<TokioRuntimeProvider>) -> Self { pub(crate) fn from(zone: Name, ns: NameServerPool<P>) -> Self {
let active_requests = Arc::new(Mutex::new(ActiveRequests::default())); let active_requests = Arc::new(Mutex::new(ActiveRequests::default()));
Self { Self {
@ -68,10 +67,7 @@ impl RecursorPool<TokioRuntimeProvider> {
} }
} }
impl<P> RecursorPool<P> impl<P: ConnectionProvider> RecursorPool<P> {
where
P: RuntimeProvider + Send + 'static,
{
pub(crate) fn zone(&self) -> &Name { pub(crate) fn zone(&self) -> &Name {
&self.zone &self.zone
} }

View File

@ -108,6 +108,7 @@ tokio = { workspace = true, features = ["time", "rt"] }
tracing.workspace = true tracing.workspace = true
hickory-client.workspace = true hickory-client.workspace = true
hickory-proto = { workspace = true, features = ["testing"] } hickory-proto = { workspace = true, features = ["testing"] }
hickory-recursor = { workspace = true }
hickory-resolver = { workspace = true, features = ["tokio-runtime"] } hickory-resolver = { workspace = true, features = ["tokio-runtime"] }
hickory-server = { workspace = true, features = ["testing"] } hickory-server = { workspace = true, features = ["testing"] }
webpki-roots = { workspace = true, optional = true } webpki-roots = { workspace = true, optional = true }

View File

@ -14,7 +14,7 @@ use futures::stream::{once, Stream};
use futures::{future, AsyncRead, AsyncWrite, Future}; use futures::{future, AsyncRead, AsyncWrite, Future};
use hickory_client::op::{Message, Query}; 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_client::rr::{Name, RData, Record};
use hickory_proto::error::ProtoError; use hickory_proto::error::ProtoError;
use hickory_proto::tcp::DnsTcpStream; 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 struct MockConnProvider<O: OnSend + Unpin> {
pub on_send: O, 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)) 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( pub fn message(
query: Query, query: Query,
answers: Vec<Record>, answers: Vec<Record>,
@ -245,7 +249,7 @@ pub trait OnSend: Clone + Send + Sync + 'static {
} }
} }
#[derive(Clone)] #[derive(Clone, Default)]
pub struct DefaultOnSend; pub struct DefaultOnSend;
impl OnSend for DefaultOnSend {} impl OnSend for DefaultOnSend {}

View File

@ -8,6 +8,50 @@
//! Integration tests for the recursor. These integration tests setup scenarios to verify that the recursor is able //! 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. //! to recursively resolve various real world scenarios. As new scenarios are discovered, they should be added here.
use std::net::*;
use std::str::FromStr as _;
use std::time::Instant;
use futures::executor::block_on;
use hickory_client::op::Query;
use hickory_proto::error::ProtoError;
use hickory_proto::xfer::DnsResponse;
use hickory_integration::mock_client::*;
use hickory_recursor::Recursor;
use hickory_resolver::name_server::{NameServer, NameServerPool};
use hickory_resolver::config::*;
use hickory_client::rr::{Name, RecordType};
const DEFAULT_SERVER_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
type MockedNameServer<O> = NameServer<MockConnProvider<O>>;
fn mock_nameserver(
messages: Vec<Result<DnsResponse, ProtoError>>,
) -> MockedNameServer<DefaultOnSend> {
let conn_provider = MockConnProvider {
on_send: DefaultOnSend,
};
let client = MockClientHandle::mock_on_send(messages, DefaultOnSend);
NameServer::from_conn(
NameServerConfig {
socket_addr: SocketAddr::new(DEFAULT_SERVER_ADDR, 0),
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,
},
Default::default(),
client,
conn_provider,
)
}
/// Tests a basic recursive resolution `a.recursive.test.` , `.` -> `test.` -> `recursive.test.` -> `a.recursive.test.` /// 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 /// There are three authorities needed for this test `.` which contains the `test` nameserver, `recursive.test` which is
@ -16,3 +60,26 @@
fn test_basic_recursion() { fn test_basic_recursion() {
// TBD // TBD
} }
/// Query the root for its nameserver. This is a single DNS request, no recursion necessary.
#[test]
fn test_root_ns() {
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 ns_message = message(ns_query.clone(), vec![ns_record.clone()], vec![], vec![]);
let nameserver = mock_nameserver(
vec![Ok(DnsResponse::from_message(ns_message).unwrap())],
);
let roots = NameServerPool::from_nameservers(
Default::default(),
vec![nameserver],
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()[0], ns_record);
}