From ec4e22817afe6cc8e290c15626b2754eb1e49f45 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 29 Apr 2024 19:39:47 +0000 Subject: [PATCH] recursor: define the bare minimum integration test --- Cargo.lock | 1 + crates/recursor/src/recursor.rs | 35 +++++++--- crates/recursor/src/recursor_pool.rs | 16 ++--- tests/integration-tests/Cargo.toml | 1 + tests/integration-tests/src/mock_client.rs | 10 ++- .../integration-tests/tests/recursor_tests.rs | 67 +++++++++++++++++++ 6 files changed, 107 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ff1b9c7..1eaa6be0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,6 +966,7 @@ dependencies = [ "futures", "hickory-client", "hickory-proto", + "hickory-recursor", "hickory-resolver", "hickory-server", "once_cell", diff --git a/crates/recursor/src/recursor.rs b/crates/recursor/src/recursor.rs index ff91e166..4fcb5628 100644 --- a/crates/recursor/src/recursor.rs +++ b/crates/recursor/src/recursor.rs @@ -28,7 +28,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 +40,13 @@ type NameServerCache

= LruCache>; /// 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, - name_server_cache: Mutex>, +pub struct Recursor { + roots: RecursorPool

, + name_server_cache: Mutex>, record_cache: DnsLru, } -impl Recursor { +impl Recursor> { /// Construct a new recursor using the list of NameServerConfigs for the root node list /// /// # Panics @@ -62,11 +62,24 @@ 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()); + + Self::new_with_pool(roots, ns_cache_size, record_cache_size) + } +} + +impl Recursor

{ + /// Construct a new recursor using a custom name server pool. + /// You likely want to use `new` instead. + pub fn new_with_pool( + roots: NameServerPool

, + ns_cache_size: usize, + record_cache_size: usize, + ) -> Result { 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()); @@ -76,7 +89,9 @@ impl Recursor { record_cache, }) } +} +impl Recursor

{ /// Perform a recursive resolution /// /// [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( &self, query: Query, - ns: RecursorPool, + ns: RecursorPool

, now: Instant, ) -> Result { if let Some(lookup) = self.record_cache.get(&query, now) { @@ -337,7 +352,7 @@ impl Recursor { &self, zone: Name, request_time: Instant, - ) -> Result, Error> { + ) -> Result, 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 +480,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); diff --git a/crates/recursor/src/recursor_pool.rs b/crates/recursor/src/recursor_pool.rs index 2611324c..9b133525 100644 --- a/crates/recursor/src/recursor_pool.rs +++ b/crates/recursor/src/recursor_pool.rs @@ -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 { +pub(crate) struct RecursorPool { zone: Name, - ns: GenericNameServerPool

, + ns: NameServerPool

, active_requests: Arc>, } -impl RecursorPool { - pub(crate) fn from(zone: Name, ns: GenericNameServerPool) -> Self { +impl RecursorPool

{ + pub(crate) fn from(zone: Name, ns: NameServerPool

) -> Self { let active_requests = Arc::new(Mutex::new(ActiveRequests::default())); Self { @@ -68,10 +67,7 @@ impl RecursorPool { } } -impl

RecursorPool

-where - P: RuntimeProvider + Send + 'static, -{ +impl RecursorPool

{ pub(crate) fn zone(&self) -> &Name { &self.zone } diff --git a/tests/integration-tests/Cargo.toml b/tests/integration-tests/Cargo.toml index 4945932b..dea8fe61 100644 --- a/tests/integration-tests/Cargo.toml +++ b/tests/integration-tests/Cargo.toml @@ -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 } diff --git a/tests/integration-tests/src/mock_client.rs b/tests/integration-tests/src/mock_client.rs index 8f288d6f..0400318b 100644 --- a/tests/integration-tests/src/mock_client.rs +++ b/tests/integration-tests/src/mock_client.rs @@ -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 { 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, @@ -245,7 +249,7 @@ pub trait OnSend: Clone + Send + Sync + 'static { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct DefaultOnSend; impl OnSend for DefaultOnSend {} diff --git a/tests/integration-tests/tests/recursor_tests.rs b/tests/integration-tests/tests/recursor_tests.rs index 4f135237..71c8ba0d 100644 --- a/tests/integration-tests/tests/recursor_tests.rs +++ b/tests/integration-tests/tests/recursor_tests.rs @@ -8,6 +8,50 @@ //! 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::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 = NameServer>; + + +fn mock_nameserver( + messages: Vec>, +) -> MockedNameServer { + 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.` /// /// 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() { // 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); +}