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.
This commit is contained in:
Colin 2024-05-05 20:07:46 +00:00
parent ec4e22817a
commit 80f2a17bff
3 changed files with 224 additions and 50 deletions

View File

@ -40,35 +40,12 @@ 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<P: ConnectionProvider> {
pub struct Recursor<P: ConnectionProvider=TokioConnectionProvider> {
roots: RecursorPool<P>,
name_server_cache: Mutex<NameServerCache<P>>,
record_cache: DnsLru,
}
impl Recursor<GenericConnector<TokioRuntimeProvider>> {
/// 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<NameServerConfigGroup>,
ns_cache_size: usize,
record_cache_size: usize,
) -> Result<Self, ResolveError> {
// 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<P: ConnectionProvider> Recursor<P> {
/// Construct a new recursor using a custom name server pool.
@ -92,6 +69,27 @@ impl<P: ConnectionProvider> Recursor<P> {
}
impl<P: ConnectionProvider + Default> Recursor<P> {
/// 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<NameServerConfigGroup>,
ns_cache_size: usize,
record_cache_size: usize,
) -> Result<Self, ResolveError> {
// 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

View File

@ -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<O> = NameServer<MockConnProvider<O>>;
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<Result<DnsResponse, ProtoError>>,
) -> MockedNameServer<DefaultOnSend> {
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<O> = NameServer<HardcodedConnProvider>;
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");
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<Record>,
name_servers: Vec<Record>,
additionals: Vec<Record>,
) -> 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]);
// }

View File

@ -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!(