From 80f2a17bff6cbfd130736e3e1fe4bbc182c36f8e Mon Sep 17 00:00:00 2001 From: Colin Date: Sun, 5 May 2024 20:07:46 +0000 Subject: [PATCH] recursor: make the test helpers more capable they did not previously allow any way to mock DNS query sequences in a manner compatible with the RecursorPool, which prefers to create new NameServers itself, rather than via anything injectable by the test. --- crates/recursor/src/recursor.rs | 46 ++-- .../integration-tests/tests/recursor_tests.rs | 226 ++++++++++++++++-- util/src/bin/recurse.rs | 2 +- 3 files changed, 224 insertions(+), 50 deletions(-) diff --git a/crates/recursor/src/recursor.rs b/crates/recursor/src/recursor.rs index 4fcb5628..76145ba7 100644 --- a/crates/recursor/src/recursor.rs +++ b/crates/recursor/src/recursor.rs @@ -40,35 +40,12 @@ 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 { +pub struct Recursor { roots: RecursorPool

, name_server_cache: Mutex>, record_cache: DnsLru, } -impl Recursor> { - /// Construct a new recursor using the list of NameServerConfigs for the root node list - /// - /// # Panics - /// - /// This will panic if the roots are empty. - pub fn new( - roots: impl Into, - ns_cache_size: usize, - record_cache_size: usize, - ) -> Result { - // configure the hickory-resolver - let roots: NameServerConfigGroup = roots.into(); - - assert!(!roots.is_empty(), "roots must not be empty"); - - 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. @@ -92,6 +69,27 @@ impl Recursor

{ } impl Recursor

{ + /// Construct a new recursor using the list of NameServerConfigs for the root node list + /// + /// # Panics + /// + /// This will panic if the roots are empty. + pub fn new( + roots: impl Into, + ns_cache_size: usize, + record_cache_size: usize, + ) -> Result { + // configure the hickory-resolver + let roots: NameServerConfigGroup = roots.into(); + + assert!(!roots.is_empty(), "roots must not be empty"); + + let opts = recursor_opts(); + let roots = NameServerPool::from_config(roots, opts, P::default()); + + Self::new_with_pool(roots, ns_cache_size, record_cache_size) + } + /// Perform a recursive resolution /// /// [RFC 1034](https://datatracker.ietf.org/doc/html/rfc1034#section-5.3.3), Domain Concepts and Facilities, November 1987 diff --git a/tests/integration-tests/tests/recursor_tests.rs b/tests/integration-tests/tests/recursor_tests.rs index 71c8ba0d..36244e46 100644 --- a/tests/integration-tests/tests/recursor_tests.rs +++ b/tests/integration-tests/tests/recursor_tests.rs @@ -8,37 +8,175 @@ //! 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::time::Instant; + +use futures::stream::{self, Stream}; +use futures::future; +use futures::StreamExt as _; use futures::executor::block_on; -use hickory_client::op::Query; -use hickory_proto::error::ProtoError; -use hickory_proto::xfer::DnsResponse; +use hickory_client::op::{Message, MessageType, Query}; +use hickory_client::rr::{Name, Record, RecordType}; use hickory_integration::mock_client::*; +use hickory_proto::DnsHandle; +use hickory_proto::error::ProtoError; +use hickory_proto::rr::LowerName; +use hickory_proto::serialize::txt::Parser; +use hickory_proto::xfer::{DnsRequest, DnsResponse}; use hickory_recursor::Recursor; -use hickory_resolver::name_server::{NameServer, NameServerPool}; use hickory_resolver::config::*; -use hickory_client::rr::{Name, RecordType}; +use hickory_resolver::name_server::ConnectionProvider; +use hickory_resolver::name_server::{NameServer, NameServerPool}; +use hickory_server::authority::{Authority, LookupOptions, ZoneType}; +use hickory_server::store::in_memory::InMemoryAuthority; -const DEFAULT_SERVER_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - -type MockedNameServer = NameServer>; +const NS_FAKEA: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); +const NS_FAKEB: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)); -fn mock_nameserver( - messages: Vec>, -) -> MockedNameServer { - let conn_provider = MockConnProvider { - on_send: DefaultOnSend, +const ZONE_FAKEA: &str = r#" +. IN SOA fakea action\.domains ( + 20 ; SERIAL + 7200 ; REFRESH + 600 ; RETRY + 3600000; EXPIRE + 60) ; MINIMUM + +. NS a.root-servers.net +a.root-servers.net A 10.0.0.2 +"#; + +const ZONE_FAKEB: &str = r#" +fakeb. IN SOA fakeb action\.domains ( + 20 ; SERIAL + 7200 ; REFRESH + 600 ; RETRY + 3600000; EXPIRE + 60) ; MINIMUM + +. NS fakea. +"#; +// net. NS b.gtld-servers.net. +// gtld-servers.net NS av1.nstld.com. +// com. NS b.gtld-servers.net. +// a.root-servers.net. A 1.2.3.4 + +// type MockedNameServer = NameServer; +type HardcodedNameServer = NameServer; + +/// 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>>>; + 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> + Send>>; + + fn send>(&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"); + let authority = make_authority_for(addr); + let mut lookup = authority.lookup( + &LowerName::new(query.name()), + query.query_type(), + LookupOptions::default(), + ).await.unwrap(); + println!("HardcodeDnsHandle: result {:?}", lookup); + let mut response = Message::new(); + response.add_query(query); + response.set_message_type(MessageType::Response); + response.insert_answers(lookup.iter().cloned().collect()); + if let Some(additionals) = lookup.take_additionals() { + response.insert_additionals(additionals.iter().cloned().collect()); + } + let resp = DnsResponse::from_message(response).unwrap(); + Ok(resp) + }) + }).collect(); + + Box::pin(stream::iter(response_futures).flatten()) + } +} + +fn make_authority_for(nameserver: SocketAddr) -> InMemoryAuthority { + println!("retrieving authority..."); + let (zone_text, name) = match nameserver { + s if s == SocketAddr::new(NS_FAKEA, 53) => { + (ZONE_FAKEA, ".") + }, + s if s == SocketAddr::new(NS_FAKEB, 53) => { + (ZONE_FAKEB, "fakeb.") + }, + ns => panic!("unexpected nameserver {:?}", ns), }; - let client = MockClientHandle::mock_on_send(messages, DefaultOnSend); + let (origin, records) = Parser::new(zone_text, None, Some(Name::from_str(name).unwrap())).parse().unwrap(); + InMemoryAuthority::new(origin, records, ZoneType::Primary, false /* allow_axfr */).unwrap() +} - NameServer::from_conn( +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(); + + tracing_subscriber::registry() + .with(formatter) + .with(subscriber) + .init(); +} + +pub fn response( + query: Query, + answers: Vec, + name_servers: Vec, + additionals: Vec, +) -> Message { + let mut message = Message::new(); + message.add_query(query); + message.set_message_type(MessageType::Response); + message.insert_answers(answers); + message.insert_name_servers(name_servers); + message.insert_additionals(additionals); + message +} + + +fn mock_nameserver(addr: IpAddr) -> HardcodedNameServer { + NameServer::new( NameServerConfig { - socket_addr: SocketAddr::new(DEFAULT_SERVER_ADDR, 0), + socket_addr: SocketAddr::new(addr, 53), protocol: Protocol::Udp, tls_dns_name: None, trust_negative_responses: false, @@ -46,9 +184,8 @@ fn mock_nameserver( tls_config: None, bind_addr: None, }, - Default::default(), - client, - conn_provider, + ResolverOpts::default(), + HardcodedConnProvider, ) } @@ -64,16 +201,16 @@ fn test_basic_recursion() { /// 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 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![ + mock_nameserver(NS_FAKEA), + // mock_nameserver(NS_FAKEB), + ], vec![], ); let recursor = Recursor::new_with_pool(roots, 1024, 1048576).unwrap(); @@ -81,5 +218,44 @@ fn test_root_ns() { let now = Instant::now(); let lookup = block_on(recursor.resolve(ns_query, now)).unwrap(); - assert_eq!(lookup.records()[0], ns_record); + assert_eq!(&*lookup.records().to_vec(), &[ns_record]); } + +// /// Query a top-level domain for its nameserver. `.` (NS) -> `com.` (NS) +// #[test] +// fn test_tld_ns() { +// logger("DEBUG"); +// +// let root_ns_query = Query::query(Name::from_str(".").unwrap(), RecordType::NS); +// let root_ns_record = ns_record(Name::from_str(".").unwrap(), Name::from_str("a.root-servers.net.").unwrap()); +// let root_ns_glue = v4_record(Name::from_str("a.root-servers.net.").unwrap(), Ipv4Addr::new(127, 0, 0, 2)); +// let root_ns_message = response(root_ns_query.clone(), vec![root_ns_record.clone()], vec![], vec![root_ns_glue]); +// +// let com_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("b.gtld-servers.net.").unwrap()); +// let com_ns_message = response(com_ns_query.clone(), vec![com_ns_record.clone()], vec![], vec![]); +// let root_nameserver = mock_nameserver( +// Ipv4Addr::new(127, 0, 0, 1), +// vec![ +// Ok(DnsResponse::from_message(root_ns_message).unwrap()), +// ], +// ); +// let com_nameserver = mock_nameserver( +// Ipv4Addr::new(127, 0, 0, 2), +// vec![ +// Ok(DnsResponse::from_message(com_ns_message).unwrap()), +// ], +// ); +// +// let roots = NameServerPool::from_nameservers( +// Default::default(), +// vec![mock_nameserver()], +// vec![], +// ); +// let recursor = Recursor::new(roots, 1024, 1048576).unwrap(); +// +// let now = Instant::now(); +// let lookup = block_on(recursor.resolve(com_ns_query, now)).unwrap(); +// +// assert_eq!(&*lookup.records().to_vec(), &[com_ns_record]); +// } diff --git a/util/src/bin/recurse.rs b/util/src/bin/recurse.rs index 5bf89460..a29c77c0 100644 --- a/util/src/bin/recurse.rs +++ b/util/src/bin/recurse.rs @@ -157,7 +157,7 @@ pub async fn main() -> Result<(), Box> { 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!(