use std::{
cell::Cell,
future::Future,
io,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use tracing::info;
use hickory_net::{
runtime::{RuntimeProvider, Time, TokioHandle},
xfer::Protocol,
};
use hickory_proto::{
op::{Message, Query, ResponseCode},
rr::{
Name, RData, Record, RecordType,
rdata::{A, NS, TXT},
},
};
use test_support::{
MockNetworkHandler, MockProvider, MockRecord, MockResponseSection, MockTcpStream,
MockUdpSocket, subscribe,
};
use crate::recursor::{DnssecPolicy, Recursor, RecursorOptions};
#[tokio::test]
async fn test_ghost_domain() {
subscribe();
let root_ns_ip = Ipv4Addr::new(10, 1, 0, 1);
let old_ns_ip = Ipv4Addr::new(10, 2, 0, 1);
let new_ns_ip = Ipv4Addr::new(10, 3, 0, 1);
let zone_name = Name::parse("testing.", None).unwrap();
let query_name = Name::parse("www.testing.", None).unwrap();
let old_ns_name = Name::parse("old-ns.", None).unwrap();
let new_ns_name = Name::parse("new-ns.", None).unwrap();
let root_ns_name = Name::parse("root-ns.", None).unwrap();
let rname = Name::parse("admin.example.", None).unwrap();
let switch_referral = Arc::new(AtomicBool::new(false));
let ttl = 1000;
let handler = MockNetworkHandler::new(vec![
MockRecord::ns(root_ns_ip.into(), &zone_name, &old_ns_name).with_ttl(ttl),
MockRecord::a(root_ns_ip.into(), &old_ns_name, old_ns_ip.into())
.with_query_name(&zone_name)
.with_query_type(RecordType::NS)
.with_section(MockResponseSection::Additional)
.with_ttl(ttl),
MockRecord::a(root_ns_ip.into(), &old_ns_name, old_ns_ip.into()).with_ttl(ttl),
MockRecord::a(root_ns_ip.into(), &new_ns_name, new_ns_ip.into()).with_ttl(ttl),
MockRecord::ns(old_ns_ip.into(), &zone_name, &old_ns_name)
.with_section(MockResponseSection::Answer)
.with_ttl(ttl),
MockRecord::ns(new_ns_ip.into(), &zone_name, &new_ns_name)
.with_section(MockResponseSection::Answer)
.with_ttl(ttl),
MockRecord::txt(old_ns_ip.into(), &query_name, vec!["old".to_string()]).with_ttl(ttl),
MockRecord::txt(new_ns_ip.into(), &query_name, vec!["new".to_string()]).with_ttl(ttl),
MockRecord::soa(old_ns_ip.into(), &query_name, &old_ns_name, &rname)
.with_query_type(RecordType::NS)
.with_ttl(ttl),
MockRecord::soa(new_ns_ip.into(), &query_name, &new_ns_name, &rname)
.with_query_type(RecordType::NS)
.with_ttl(ttl),
MockRecord::soa(root_ns_ip.into(), &Name::root(), &root_ns_name, &rname)
.with_query_name(&old_ns_name)
.with_query_type(RecordType::NS)
.with_ttl(ttl),
MockRecord::soa(root_ns_ip.into(), &Name::root(), &root_ns_name, &rname)
.with_query_name(&new_ns_name)
.with_query_type(RecordType::NS)
.with_ttl(ttl),
MockRecord::soa(root_ns_ip.into(), &Name::root(), &root_ns_name, &rname)
.with_query_name(&old_ns_name)
.with_query_type(RecordType::AAAA)
.with_ttl(ttl),
MockRecord::soa(root_ns_ip.into(), &Name::root(), &root_ns_name, &rname)
.with_query_name(&new_ns_name)
.with_query_type(RecordType::AAAA)
.with_ttl(ttl),
])
.with_mutation(Box::new({
let zone_name = zone_name.clone();
let switch_referral = switch_referral.clone();
move |destination: IpAddr, _protocol: Protocol, msg: &mut Message| {
let query = &msg.queries[0];
if destination == root_ns_ip && query.name == zone_name {
msg.metadata.authoritative = false;
if switch_referral.load(Ordering::SeqCst) {
info!("responding with new referral");
msg.answers.clear();
msg.authorities.clear();
msg.additionals.clear();
msg.authorities.push(Record::from_rdata(
zone_name.clone(),
ttl,
RData::NS(NS(new_ns_name.clone())),
));
msg.additionals.push(Record::from_rdata(
new_ns_name.clone(),
ttl,
RData::A(A(new_ns_ip)),
));
} else {
info!("responding with old referral");
}
}
}
}));
let provider = TestRuntimeProvider(MockProvider::new(handler));
let recursor = Recursor::new(
&[root_ns_ip.into()],
DnssecPolicy::SecurityUnaware,
None,
RecursorOptions {
deny_server: Vec::new(),
..Default::default()
},
provider,
)
.unwrap();
TIME.set(Some(0));
let cache_t1 = Instant::now();
tokio::time::pause();
let tokio_start = tokio::time::Instant::now();
info!("first request");
let response_1 = recursor
.resolve(
Query::query(query_name.clone(), RecordType::TXT),
cache_t1,
false,
)
.await
.unwrap();
assert_eq!(response_1.response_code, ResponseCode::NoError);
assert!(response_1.answers.iter().any(|record| {
let RData::TXT(TXT {
txt_data: strings, ..
}) = &record.data
else {
return false;
};
strings[0].as_ref() == b"old"
}));
switch_referral.store(true, Ordering::SeqCst);
TIME.set(Some(750));
let cache_t2 = cache_t1 + Duration::from_secs(750);
tokio::time::advance(Duration::from_secs(750)).await;
info!("second request");
let response_2 = recursor
.resolve(
Query::query(zone_name.clone(), RecordType::NS),
cache_t2,
false,
)
.await
.unwrap();
assert_eq!(response_2.response_code, ResponseCode::NoError);
assert!(
response_2
.all_sections()
.any(|record| record.record_type() == RecordType::NS)
);
TIME.set(Some(1500));
let cache_t3 = cache_t1 + Duration::from_secs(1500);
tokio::time::advance(Duration::from_secs(750)).await;
info!("third request");
let response_3 = recursor
.resolve(
Query::query(query_name.clone(), RecordType::TXT),
cache_t3,
false,
)
.await
.unwrap();
info!(
elapsed = ?tokio_start.elapsed(),
"Tokio elapsed time after third request"
);
assert_eq!(response_3.response_code, ResponseCode::NoError);
assert!(
response_3.answers.iter().any(|record| {
let RData::TXT(TXT {
txt_data: strings, ..
}) = &record.data
else {
return false;
};
strings[0].as_ref() == b"new"
}),
"Did not get new zone's data: {response_3:?}"
);
}
#[derive(Clone)]
struct TestRuntimeProvider(MockProvider);
impl RuntimeProvider for TestRuntimeProvider {
type Handle = TokioHandle;
type Timer = TestTimer;
type Udp = MockUdpSocket;
type Tcp = MockTcpStream;
fn create_handle(&self) -> Self::Handle {
self.0.create_handle()
}
fn connect_tcp(
&self,
server_addr: SocketAddr,
bind_addr: Option<SocketAddr>,
timeout: Option<Duration>,
) -> std::pin::Pin<Box<dyn Send + Future<Output = Result<Self::Tcp, io::Error>>>> {
self.0.connect_tcp(server_addr, bind_addr, timeout)
}
fn bind_udp(
&self,
local_addr: SocketAddr,
server_addr: SocketAddr,
) -> std::pin::Pin<Box<dyn Send + Future<Output = Result<Self::Udp, io::Error>>>> {
self.0.bind_udp(local_addr, server_addr)
}
}
struct TestTimer;
#[async_trait::async_trait]
impl Time for TestTimer {
async fn delay_for(_duration: Duration) {}
async fn timeout<F: 'static + Future + Send>(
_duration: Duration,
future: F,
) -> Result<F::Output, io::Error> {
Ok(future.await)
}
fn current_time() -> u64 {
TIME.get()
.expect("Mock time has not been set on current threads")
}
}
thread_local! {
static TIME: Cell<Option<u64>> = const{ Cell::new(None) };
}