use std::{
net::{IpAddr, Ipv4Addr},
sync::{
Arc,
atomic::{AtomicU8, Ordering},
},
time::{Duration, Instant},
};
use test_support::{MockNetworkHandler, MockProvider, MockRecord, MockResponseSection, subscribe};
use tokio::time as TokioTime;
use super::{Recursor, RecursorError, RecursorMode, RecursorOptions, is_subzone};
use crate::{
cache::TtlConfig,
config::ResolverOpts,
net::{NetError, runtime::TokioRuntimeProvider, xfer::Protocol},
proto::{
op::{Message, Query, ResponseCode},
rr::{Name, Record, RecordType},
},
};
#[tokio::test]
async fn recursor_connection_deduplication() -> Result<(), NetError> {
subscribe();
let query_name = Name::from_ascii("host.hickory-dns.testing.")?;
let dup_query_name = Name::from_ascii("host.hickory-dns-dup.testing.")?;
let (provider, options) = test_fixture()?;
let recursor = Recursor::with_options(&[ROOT_IP], options, provider.clone())?;
for query in [query_name, dup_query_name] {
let response = recursor
.resolve(Query::query(query, RecordType::A), Instant::now(), false)
.await?;
assert_eq!(response.response_code, ResponseCode::NoError);
assert_eq!(
provider.count_new_connection_calls(ROOT_IP, Protocol::Tcp),
1
);
assert_eq!(
provider.count_new_connection_calls(TLD_IP, Protocol::Tcp),
1
);
assert_eq!(
provider.count_new_connection_calls(LEAF_IP, Protocol::Tcp),
1
);
}
Ok(())
}
#[tokio::test]
async fn recursor_connection_deduplication_non_cached() -> Result<(), NetError> {
subscribe();
let query_name = Name::from_ascii("host.hickory-dns.testing.")?;
let dup_query_name = Name::from_ascii("host.hickory-dns-dup.testing.")?;
let (provider, options) = test_fixture()?;
let recursor = Recursor::with_options(
&[ROOT_IP],
RecursorOptions {
ns_cache_size: 1,
..options
},
provider.clone(),
)?;
let response = recursor
.resolve(
Query::query(query_name, RecordType::A),
Instant::now(),
false,
)
.await?;
assert_eq!(response.response_code, ResponseCode::NoError);
assert_eq!(
provider.count_new_connection_calls(ROOT_IP, Protocol::Tcp),
1
);
assert_eq!(
provider.count_new_connection_calls(TLD_IP, Protocol::Tcp),
1
);
assert_eq!(
provider.count_new_connection_calls(LEAF_IP, Protocol::Tcp),
1
);
let response = recursor
.resolve(
Query::query(dup_query_name, RecordType::A),
Instant::now(),
false,
)
.await
.unwrap();
assert_eq!(response.response_code, ResponseCode::NoError);
assert_eq!(
provider.count_new_connection_calls(ROOT_IP, Protocol::Tcp),
1
);
assert_eq!(
provider.count_new_connection_calls(TLD_IP, Protocol::Tcp),
2
);
assert_eq!(
provider.count_new_connection_calls(LEAF_IP, Protocol::Tcp),
2
);
Ok(())
}
#[tokio::test(start_paused = true)]
async fn name_server_cache_ttl() -> Result<(), NetError> {
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let zone_ttl = 60;
let recursor = ns_cache_test_fixture(zone_ttl, zone_ttl, TtlConfig::default(), false)?;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs((zone_ttl * 2) as u64)).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_2));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_2));
Ok(())
}
#[tokio::test(start_paused = true)]
async fn name_server_cache_ttl_clamp_min() -> Result<(), NetError> {
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let zone_ttl = 60;
let recursor_min_ttl: u32 = 240;
let recursor_max_ttl = 86400;
assert!(zone_ttl * 2 < recursor_min_ttl); let opts = ResolverOpts {
positive_min_ttl: Some(Duration::from_secs(recursor_min_ttl as u64)),
positive_max_ttl: Some(Duration::from_secs(recursor_max_ttl)),
..ResolverOpts::default()
};
let recursor = ns_cache_test_fixture(zone_ttl, zone_ttl, TtlConfig::from_opts(&opts), false)?;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs(u64::from(zone_ttl * 2))).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs(recursor_min_ttl as u64)).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_2));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_2));
Ok(())
}
#[tokio::test(start_paused = true)]
async fn name_server_cache_ttl_clamp_max() -> Result<(), NetError> {
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let zone_ttl: u32 = 3600;
let recursor_min_ttl = 1;
let recursor_max_ttl = 60;
assert!(zone_ttl > recursor_max_ttl * 2);
let opts = ResolverOpts {
positive_min_ttl: Some(Duration::from_secs(recursor_min_ttl)),
positive_max_ttl: Some(Duration::from_secs(recursor_max_ttl as u64)),
..ResolverOpts::default()
};
let recursor = ns_cache_test_fixture(zone_ttl, zone_ttl, TtlConfig::from_opts(&opts), false)?;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs(u64::from(recursor_max_ttl * 2))).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_2));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_2));
Ok(())
}
#[tokio::test(start_paused = true)]
async fn name_server_cache_ttl_glue() -> Result<(), NetError> {
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let zone_ttl = 60;
let ns_ttl = 15;
assert!(zone_ttl > ns_ttl * 2); let recursor = ns_cache_test_fixture(zone_ttl, ns_ttl, TtlConfig::default(), false)?;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs((ns_ttl * 2) as u64)).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_2));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_2));
Ok(())
}
#[tokio::test(start_paused = true)]
async fn name_server_cache_ttl_glue_off_domain() -> Result<(), NetError> {
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let zone_ttl = 60;
let ns_ttl = 15;
assert!(zone_ttl > ns_ttl * 2); let recursor = ns_cache_test_fixture(zone_ttl, ns_ttl, TtlConfig::default(), true)?;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_1));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_1));
let _ = TokioTime::advance(Duration::from_secs((ns_ttl * 2) as u64)).await;
let response = ttl_lookup(&recursor, &query_1_name).await?;
assert!(validate_response(response, &query_1_name, target_1_ip_2));
let response = ttl_lookup(&recursor, &query_2_name).await?;
assert!(validate_response(response, &query_2_name, target_2_ip_2));
Ok(())
}
#[tokio::test]
async fn ns_pool_zone_name_test() -> Result<(), NetError> {
subscribe();
let query_name = Name::from_ascii("host.hickory-dns.testing.")?;
let nx_query_name = Name::from_ascii("invalid.hickory-dns.testing.")?;
let delegated_query_name = Name::from_ascii("host.delegated.hickory-dns.testing.")?;
let ent_query_name = Name::from_ascii("ent.hickory-dns.testing.")?;
let ent_delegated_query_name = Name::from_ascii("host.delegated.ent.hickory-dns.testing.")?;
let tld_zone = Name::from_ascii("testing.")?;
let tld_ns = Name::from_ascii("testing.testing.")?;
let leaf_zone = Name::from_ascii("hickory-dns.testing.")?;
let leaf_ns = Name::from_ascii("ns.hickory-dns.testing.")?;
let delegated_leaf_zone = Name::from_ascii("delegated.hickory-dns.testing.")?;
let delegated_leaf_ns = Name::from_ascii("ns.delegated.hickory-dns.testing.")?;
let ent_delegated_leaf_zone = Name::from_ascii("delegated.ent.hickory-dns.testing.")?;
let ent_delegated_leaf_ns = Name::from_ascii("ns.delegated.ent.hickory-dns.testing.")?;
let responses = vec![
MockRecord::ns(ROOT_IP, &tld_zone, &tld_ns),
MockRecord::a(ROOT_IP, &tld_ns, TLD_IP)
.with_query_name(&tld_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(TLD_IP, &leaf_zone, &leaf_ns),
MockRecord::a(TLD_IP, &leaf_ns, LEAF_IP)
.with_query_name(&leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(LEAF_IP, &delegated_leaf_zone, &delegated_leaf_ns),
MockRecord::a(LEAF_IP, &delegated_leaf_ns, DELEGATED_LEAF_IP)
.with_query_name(&delegated_leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(LEAF_IP, &ent_delegated_leaf_zone, &ent_delegated_leaf_ns),
MockRecord::a(LEAF_IP, &ent_delegated_leaf_ns, ENT_DELEGATED_LEAF_IP)
.with_query_name(&ent_delegated_leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::a(LEAF_IP, &query_name, LEAF_IP),
MockRecord::a(LEAF_IP, &ent_query_name, LEAF_IP),
MockRecord::a(DELEGATED_LEAF_IP, &delegated_query_name, DELEGATED_LEAF_IP),
MockRecord::a(
ENT_DELEGATED_LEAF_IP,
&ent_delegated_query_name,
ENT_DELEGATED_LEAF_IP,
),
];
let recursor_no_cache = Recursor::with_options(
&[ROOT_IP],
RecursorOptions {
deny_server: Vec::new(),
ns_cache_size: 1,
..RecursorOptions::default()
},
MockProvider::new(MockNetworkHandler::new(responses.clone())),
)?;
let recursor_cache = Recursor::with_options(
&[ROOT_IP],
RecursorOptions {
deny_server: Vec::new(),
ns_cache_size: 1024,
..RecursorOptions::default()
},
MockProvider::new(MockNetworkHandler::new(responses.clone())),
)?;
for recursor in [recursor_no_cache, recursor_cache] {
assert_eq!(
get_zone_name(&recursor, &query_name).await?,
Some(leaf_zone.clone())
);
assert_eq!(
get_zone_name(&recursor, &leaf_zone).await?,
Some(leaf_zone.clone())
);
assert_eq!(
get_zone_name(&recursor, &nx_query_name).await?,
Some(leaf_zone.clone())
);
assert_eq!(
get_zone_name(&recursor, &delegated_query_name).await?,
Some(delegated_leaf_zone.clone())
);
assert_eq!(
get_zone_name(&recursor, &ent_query_name).await?,
Some(leaf_zone.clone())
);
assert_eq!(
get_zone_name(&recursor, &ent_delegated_query_name).await?,
Some(ent_delegated_leaf_zone.clone())
);
assert!(validate_response(
ttl_lookup(&recursor, &query_name).await?,
&query_name,
LEAF_IP
));
assert!(validate_response(
ttl_lookup(&recursor, &delegated_query_name).await?,
&delegated_query_name,
DELEGATED_LEAF_IP
));
assert!(validate_response(
ttl_lookup(&recursor, &ent_delegated_query_name).await?,
&ent_delegated_query_name,
ENT_DELEGATED_LEAF_IP
));
}
Ok(())
}
#[tokio::test]
async fn not_fully_qualified_domain_name_in_query() -> Result<(), NetError> {
subscribe();
let j_root_servers_net_ip = IpAddr::from([192, 58, 128, 30]);
let recursor = Recursor::with_options(
&[j_root_servers_net_ip],
RecursorOptions::default(),
TokioRuntimeProvider::default(),
)?;
let name = Name::from_ascii("example.com")?;
assert!(!name.is_fqdn());
let query = Query::query(name, RecordType::A);
let res = recursor
.resolve(query, Instant::now(), false)
.await
.unwrap_err();
assert!(res.to_string().contains("fully qualified"));
Ok(())
}
#[tokio::test]
async fn cache_negative_responses() -> Result<(), NetError> {
subscribe();
let exists_name = Name::from_ascii("exists.hickory-dns.testing.")?;
let no_exist_name = Name::from_ascii("doesnotexist.hickory-dns.testing.")?;
let tld_zone = Name::from_ascii("testing.")?;
let tld_ns = Name::from_ascii("testing.testing.")?;
let leaf_zone = Name::from_ascii("hickory-dns.testing.")?;
let leaf_ns = Name::from_ascii("ns.hickory-dns.testing.")?;
let responses = vec![
MockRecord::ns(ROOT_IP, &tld_zone, &tld_ns),
MockRecord::a(ROOT_IP, &tld_ns, TLD_IP)
.with_query_name(&tld_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(TLD_IP, &leaf_zone, &leaf_ns),
MockRecord::a(TLD_IP, &leaf_ns, LEAF_IP)
.with_query_name(&leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::a(LEAF_IP, &exists_name, LEAF_IP),
MockRecord::soa(LEAF_IP, &leaf_ns, &leaf_zone, &leaf_ns)
.with_query_name(&no_exist_name)
.with_query_type(RecordType::A)
.with_ttl(3600),
MockRecord::soa(LEAF_IP, &leaf_ns, &leaf_zone, &leaf_ns)
.with_query_name(&no_exist_name)
.with_query_type(RecordType::NS)
.with_ttl(3600),
];
let provider = MockProvider::new(MockNetworkHandler::new(responses));
let recursor = Recursor::with_options(
&[ROOT_IP],
RecursorOptions {
deny_server: Vec::new(), ..RecursorOptions::default()
},
provider.clone(),
)?;
for _ in 0..2 {
let response = recursor
.resolve(
Query::query(no_exist_name.clone(), RecordType::A),
Instant::now(),
false,
)
.await;
assert!(response.is_err());
assert_eq!(
provider
.queries(&LEAF_IP)
.iter()
.filter(|x| x.name() == &no_exist_name && x.query_type() == RecordType::A)
.count(),
1,
);
}
Ok(())
}
#[test]
fn is_subzone_test() {
use core::str::FromStr;
assert!(is_subzone(
&Name::from_str(".").unwrap(),
&Name::from_str("com.").unwrap(),
));
assert!(is_subzone(
&Name::from_str("com.").unwrap(),
&Name::from_str("example.com.").unwrap(),
));
assert!(is_subzone(
&Name::from_str("example.com.").unwrap(),
&Name::from_str("host.example.com.").unwrap(),
));
assert!(is_subzone(
&Name::from_str("example.com.").unwrap(),
&Name::from_str("host.multilevel.example.com.").unwrap(),
));
assert!(!is_subzone(
&Name::from_str("").unwrap(),
&Name::from_str("example.com.").unwrap(),
));
assert!(!is_subzone(
&Name::from_str("com.").unwrap(),
&Name::from_str("example.net.").unwrap(),
));
assert!(!is_subzone(
&Name::from_str("example.com.").unwrap(),
&Name::from_str("otherdomain.com.").unwrap(),
));
assert!(!is_subzone(
&Name::from_str("com").unwrap(),
&Name::from_str("example.com.").unwrap(),
));
}
async fn get_zone_name(
recursor: &Recursor<MockProvider>,
query: &Name,
) -> Result<Option<Name>, NetError> {
match recursor.mode {
RecursorMode::NonValidating { ref handle } => {
let ns_pool = handle
.ns_pool_for_name(query.clone(), Instant::now(), 0)
.await?
.1;
Ok(ns_pool.zone().cloned())
}
#[cfg(feature = "__dnssec")]
_ => panic!("test doesn't support validating mode"),
}
}
fn ns_cache_test_fixture(
zone_ttl: u32,
ns_ttl: u32,
ttl_config: TtlConfig,
off_domain: bool,
) -> Result<Recursor<MockProvider>, RecursorError> {
subscribe();
let query_1_name = Name::from_ascii("host.hickory-dns.testing.")?;
let query_2_name = Name::from_ascii("host2.hickory-dns.testing.")?;
let tld_zone = Name::from_ascii("testing.")?;
let tld_ns = Name::from_ascii("testing.testing.")?;
let leaf_zone = Name::from_ascii("hickory-dns.testing.")?;
let leaf_ns = Name::from_ascii("ns.hickory-dns.testing.")?;
let off_domain_zone = Name::from_ascii("otherdomain.testing.")?;
let off_domain_ns = Name::from_ascii("ns.otherdomain.testing.")?;
let off_domain_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 5, 1));
let leaf_2_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 4, 1));
let target_1_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_1_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let target_2_ip_1 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1));
let target_2_ip_2 = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 2));
let mut responses = vec![
MockRecord::ns(ROOT_IP, &tld_zone, &tld_ns),
MockRecord::a(ROOT_IP, &tld_ns, TLD_IP)
.with_query_name(&tld_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::a(LEAF_IP, &query_1_name, target_1_ip_1).with_ttl(0),
MockRecord::a(leaf_2_ip, &query_1_name, target_1_ip_2).with_ttl(0),
MockRecord::a(LEAF_IP, &query_2_name, target_2_ip_1).with_ttl(0),
MockRecord::a(leaf_2_ip, &query_2_name, target_2_ip_2).with_ttl(0),
];
if off_domain {
responses.append(
&mut [
MockRecord::ns(TLD_IP, &leaf_zone, &off_domain_ns).with_ttl(zone_ttl),
MockRecord::ns(TLD_IP, &off_domain_zone, &off_domain_ns).with_ttl(zone_ttl),
MockRecord::a(TLD_IP, &off_domain_ns, off_domain_ip)
.with_query_name(&off_domain_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional)
.with_ttl(zone_ttl),
MockRecord::soa(
off_domain_ip,
&off_domain_ns,
&off_domain_zone,
&off_domain_ns,
)
.with_query_name(&off_domain_ns)
.with_query_type(RecordType::NS),
MockRecord::a(off_domain_ip, &off_domain_ns, LEAF_IP).with_ttl(ns_ttl),
]
.into_iter()
.collect::<Vec<MockRecord>>(),
);
} else {
responses.append(
&mut [
MockRecord::ns(TLD_IP, &leaf_zone, &leaf_ns).with_ttl(zone_ttl),
MockRecord::a(TLD_IP, &leaf_ns, LEAF_IP)
.with_query_name(&leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional)
.with_ttl(ns_ttl),
]
.into_iter()
.collect::<Vec<MockRecord>>(),
);
}
let counter = Arc::new(AtomicU8::new(0));
let handler = MockNetworkHandler::new(responses).with_mutation(Box::new(
move |destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
let leaf_ns = leaf_ns.clone();
let query_name = msg.queries[0].name();
let query_type = msg.queries[0].query_type();
if !off_domain {
if destination == TLD_IP && *query_name == leaf_zone && query_type == RecordType::NS
{
let count = counter.fetch_add(1, Ordering::Relaxed);
if count > 0 {
msg.additionals.clear();
msg.add_additional(Record::from_rdata(leaf_ns, ns_ttl, leaf_2_ip.into()));
}
}
} else if destination == off_domain_ip
&& *query_name == off_domain_ns
&& query_type == RecordType::A
{
let count = counter.fetch_add(1, Ordering::Relaxed);
if count > 0 {
msg.answers.clear();
msg.add_answer(Record::from_rdata(leaf_ns, zone_ttl, leaf_2_ip.into()));
}
}
},
));
Recursor::with_options(
&[ROOT_IP],
RecursorOptions {
deny_server: Vec::new(),
cache_policy: ttl_config,
..RecursorOptions::default()
},
MockProvider::new(handler),
)
}
async fn ttl_lookup(
recursor: &Recursor<MockProvider>,
name: &Name,
) -> Result<Message, RecursorError> {
recursor
.resolve(
Query::query(name.clone(), RecordType::A),
TokioTime::Instant::now().into(),
false,
)
.await
}
fn validate_response(response: Message, name: &Name, ip: IpAddr) -> bool {
response.response_code == ResponseCode::NoError
&& response.answers == [Record::from_rdata(name.clone(), 0, ip.into())]
}
fn test_fixture() -> Result<(MockProvider, RecursorOptions), NetError> {
let query_name = Name::from_ascii("host.hickory-dns.testing.")?;
let dup_query_name = Name::from_ascii("host.hickory-dns-dup.testing.")?;
let tld_zone = Name::from_ascii("testing.")?;
let tld_ns = Name::from_ascii("testing.testing.")?;
let leaf_zone = Name::from_ascii("hickory-dns.testing.")?;
let leaf_ns = Name::from_ascii("ns.hickory-dns.testing.")?;
let dup_leaf_zone = Name::from_ascii("hickory-dns-dup.testing.")?;
let dup_leaf_ns = Name::from_ascii("ns.hickory-dns-dup.testing.")?;
let responses = vec![
MockRecord::ns(ROOT_IP, &tld_zone, &tld_ns),
MockRecord::a(ROOT_IP, &tld_ns, TLD_IP)
.with_query_name(&tld_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(TLD_IP, &leaf_zone, &leaf_ns),
MockRecord::a(TLD_IP, &leaf_ns, LEAF_IP)
.with_query_name(&leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(TLD_IP, &dup_leaf_zone, &dup_leaf_ns),
MockRecord::a(TLD_IP, &dup_leaf_ns, LEAF_IP)
.with_query_name(&dup_leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::a(LEAF_IP, &query_name, LEAF_IP),
MockRecord::a(LEAF_IP, &dup_query_name, LEAF_IP),
];
let handler = MockNetworkHandler::new(responses).with_mutation(Box::new(
|_destination: IpAddr, protocol: Protocol, msg: &mut Message| {
if protocol == Protocol::Udp {
msg.metadata.truncation = true;
}
},
));
let provider = MockProvider::new(handler);
let options = RecursorOptions {
deny_server: Vec::new(),
..RecursorOptions::default()
};
Ok((provider, options))
}
#[cfg(feature = "metrics")]
mod metrics {
use ::metrics::{Unit, with_local_recorder};
use metrics_util::debugging::DebuggingRecorder;
use test_support::{assert_counter_eq, assert_gauge_eq, assert_histogram_sample_count_eq};
use tokio::runtime::Builder;
use super::*;
#[cfg(feature = "__dnssec")]
use crate::metrics::recursor::{
BOGUS_ANSWERS_TOTAL, INDETERMINATE_ANSWERS_TOTAL, INSECURE_ANSWERS_TOTAL,
SECURE_ANSWERS_TOTAL, VALIDATED_RESPONSE_CACHE_SIZE,
};
use crate::metrics::recursor::{
CACHE_HIT_DURATION, CACHE_HIT_TOTAL, CACHE_MISS_DURATION, CACHE_MISS_TOTAL,
CONNECTION_CACHE_SIZE, IN_FLIGHT_QUERIES, NAME_SERVER_CACHE_SIZE, OUTGOING_QUERIES_TOTAL,
RESPONSE_CACHE_SIZE,
};
#[cfg(feature = "__dnssec")]
use crate::recursor::DnssecConfig;
use crate::recursor::DnssecPolicy;
#[test]
fn test_recursor_metrics() {
subscribe();
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
let query_name = Name::parse("hickory-dns.testing.", None).unwrap();
with_local_recorder(&recorder, || {
let runtime = Builder::new_current_thread().enable_all().build().unwrap();
let tld_zone = Name::from_ascii("testing.").unwrap();
let tld_ns = Name::from_ascii("testing.testing.").unwrap();
let leaf_zone = Name::from_ascii("hickory-dns.testing.").unwrap();
let leaf_ns = Name::from_ascii("leaf.testing.").unwrap();
let handler = MockNetworkHandler::new(vec![
MockRecord::ns(ROOT_IP, &tld_zone, &tld_ns),
MockRecord::a(ROOT_IP, &tld_ns, TLD_IP)
.with_query_name(&tld_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::ns(TLD_IP, &leaf_zone, &leaf_ns),
MockRecord::a(TLD_IP, &leaf_ns, LEAF_IP)
.with_query_name(&leaf_zone)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional),
MockRecord::a(LEAF_IP, &leaf_zone, A_RR_IP),
]);
let provider = MockProvider::new(handler);
#[cfg(not(feature = "__dnssec"))]
let policy = DnssecPolicy::SecurityUnaware;
#[cfg(feature = "__dnssec")]
let policy = DnssecPolicy::ValidateWithStaticKey(DnssecConfig::default());
let recursor = Recursor::new(
&[ROOT_IP],
policy,
None,
RecursorOptions {
deny_server: Vec::new(),
..RecursorOptions::default()
},
provider,
)
.unwrap();
runtime.block_on(async {
for _ in 0..3 {
let response = recursor
.resolve(
Query::query(query_name.clone(), RecordType::A),
Instant::now(),
false,
)
.await
.unwrap();
assert_eq!(response.response_code, ResponseCode::NoError);
}
});
});
#[allow(clippy::mutable_key_type)]
let map = snapshotter.snapshot().into_hashmap();
#[cfg(not(feature = "__dnssec"))]
{
assert_counter_eq(&map, OUTGOING_QUERIES_TOTAL, vec![], 3);
assert_counter_eq(&map, CACHE_HIT_TOTAL, vec![], 2);
assert_counter_eq(&map, CACHE_MISS_TOTAL, vec![], 1);
assert_histogram_sample_count_eq(&map, CACHE_HIT_DURATION, vec![], 2, Unit::Seconds);
assert_histogram_sample_count_eq(&map, CACHE_MISS_DURATION, vec![], 1, Unit::Seconds);
assert_gauge_eq(&map, RESPONSE_CACHE_SIZE, vec![], 3);
assert_gauge_eq(&map, NAME_SERVER_CACHE_SIZE, vec![], 2);
assert_gauge_eq(&map, CONNECTION_CACHE_SIZE, vec![], 2);
assert_gauge_eq(&map, IN_FLIGHT_QUERIES, vec![], 0);
}
#[cfg(feature = "__dnssec")]
{
assert_counter_eq(&map, OUTGOING_QUERIES_TOTAL, vec![], 4);
assert_counter_eq(&map, CACHE_HIT_TOTAL, vec![], 5);
assert_counter_eq(&map, CACHE_MISS_TOTAL, vec![], 2);
assert_histogram_sample_count_eq(&map, CACHE_HIT_DURATION, vec![], 3, Unit::Seconds);
assert_histogram_sample_count_eq(&map, CACHE_MISS_DURATION, vec![], 1, Unit::Seconds);
assert_counter_eq(&map, SECURE_ANSWERS_TOTAL, vec![], 0);
assert_counter_eq(&map, INSECURE_ANSWERS_TOTAL, vec![], 0);
assert_counter_eq(&map, BOGUS_ANSWERS_TOTAL, vec![], 5);
assert_counter_eq(&map, INDETERMINATE_ANSWERS_TOTAL, vec![], 0);
assert_gauge_eq(&map, RESPONSE_CACHE_SIZE, vec![], 3);
assert_gauge_eq(&map, VALIDATED_RESPONSE_CACHE_SIZE, vec![], 1);
assert_gauge_eq(&map, NAME_SERVER_CACHE_SIZE, vec![], 2);
assert_gauge_eq(&map, CONNECTION_CACHE_SIZE, vec![], 2);
assert_gauge_eq(&map, IN_FLIGHT_QUERIES, vec![], 0);
}
}
const A_RR_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
}
#[cfg(all(
test,
feature = "serde",
feature = "toml",
any(feature = "__tls", feature = "__quic")
))]
mod config {
use std::path::Path;
use crate::{
config::{OpportunisticEncryption, OpportunisticEncryptionConfig},
recursor::{DnssecPolicyConfig, RecursiveConfig},
};
#[test]
fn can_parse_recursive_config() {
let input = r#"roots = "/etc/root.hints"
dnssec_policy.ValidateWithStaticKey.path = "/etc/trusted-key.key""#;
let config = toml::from_str::<RecursiveConfig>(input).unwrap();
if let DnssecPolicyConfig::ValidateWithStaticKey { path, .. } = config.dnssec_policy {
assert_eq!(Some(Path::new("/etc/trusted-key.key")), path.as_deref());
} else {
unreachable!()
}
}
#[test]
fn can_parse_recursor_cache_policy() {
use std::time::Duration;
use hickory_proto::rr::RecordType;
let input = r#"roots = "/etc/root.hints"
[cache_policy.default]
positive_max_ttl = 14400
[cache_policy.A]
positive_max_ttl = 3600"#;
let config = toml::from_str::<RecursiveConfig>(input).unwrap();
assert_eq!(
*config
.options
.cache_policy
.positive_response_ttl_bounds(RecordType::MX)
.end(),
Duration::from_secs(14400)
);
assert_eq!(
*config
.options
.cache_policy
.positive_response_ttl_bounds(RecordType::A)
.end(),
Duration::from_secs(3600)
)
}
#[test]
fn can_parse_recursor_opportunistic_enc_policy() {
let input = r#"roots = "/etc/root.hints"
[opportunistic_encryption]
enabled = {}
"#;
let config = toml::from_str::<RecursiveConfig>(input).unwrap();
assert_eq!(
config.options.opportunistic_encryption,
OpportunisticEncryption::Enabled {
config: OpportunisticEncryptionConfig::default()
}
);
}
}
const ROOT_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 1, 1));
const TLD_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 2, 1));
const LEAF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 3, 1));
const DELEGATED_LEAF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 4, 1));
const ENT_DELEGATED_LEAF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 0, 5, 1));