use std::net::Ipv4Addr;
use std::time::Duration;
use hickory_resolver::proto::op::{Message, OpCode, ResponseCode};
use hickory_resolver::proto::rr::rdata::{self as wire, sshfp, tlsa, CAA};
use hickory_resolver::proto::rr::{
Name, RData as HickoryRData, Record, RecordType as HickoryRecordType,
};
use tokio::net::UdpSocket;
use super::resolver::DnsResolver;
#[derive(Clone, Copy)]
pub(crate) enum MockMode {
Zone,
Nxdomain,
NoData,
Ignore,
}
fn name(s: &str) -> Name {
Name::from_ascii(s).expect("valid test name")
}
fn zone_answers(qname: &str, qtype: HickoryRecordType) -> Vec<HickoryRData> {
match (qname.trim_end_matches('.'), qtype) {
("seer.test", HickoryRecordType::A) => vec![
HickoryRData::A(wire::A(Ipv4Addr::new(192, 0, 2, 1))),
HickoryRData::A(wire::A(Ipv4Addr::new(192, 0, 2, 2))),
],
("seer.test", HickoryRecordType::AAAA) => vec![HickoryRData::AAAA(wire::AAAA(
"2001:db8::1".parse().expect("valid IPv6 literal"),
))],
("seer.test", HickoryRecordType::MX) => vec![
HickoryRData::MX(wire::MX::new(30, name("c.mail.seer.test."))),
HickoryRData::MX(wire::MX::new(10, name("a.mail.seer.test."))),
HickoryRData::MX(wire::MX::new(20, name("b.mail.seer.test."))),
],
("seer.test", HickoryRecordType::NS) => {
vec![HickoryRData::NS(wire::NS(name("ns1.seer.test.")))]
}
("seer.test", HickoryRecordType::TXT) => vec![HickoryRData::TXT(wire::TXT::new(vec![
"v=spf1 ".to_string(),
"-all".to_string(),
]))],
("seer.test", HickoryRecordType::SOA) => vec![HickoryRData::SOA(wire::SOA::new(
name("ns1.seer.test."),
name("hostmaster.seer.test."),
2026070101,
7200,
3600,
1209600,
300,
))],
("seer.test", HickoryRecordType::CAA) => vec![
HickoryRData::CAA(CAA::new_issue(false, Some(name("letsencrypt.org")), vec![])),
HickoryRData::CAA(CAA::new_iodef(
true,
url::Url::parse("mailto:security@seer.test").expect("valid iodef URL"),
)),
],
("_443._tcp.seer.test", HickoryRecordType::TLSA) => {
vec![HickoryRData::TLSA(wire::TLSA::new(
tlsa::CertUsage::from(3),
tlsa::Selector::from(1),
tlsa::Matching::from(1),
vec![0xAB, 0xCD, 0x01],
))]
}
("seer.test", HickoryRecordType::SSHFP) => vec![HickoryRData::SSHFP(wire::SSHFP::new(
sshfp::Algorithm::from(4),
sshfp::FingerprintType::from(2),
vec![0xDE, 0xAD, 0xBE, 0xEF],
))],
("seer.test", HickoryRecordType::NAPTR) => vec![HickoryRData::NAPTR(wire::NAPTR::new(
100,
50,
b"U".to_vec().into_boxed_slice(),
b"E2U+sip".to_vec().into_boxed_slice(),
b"!^.*$!sip:info@seer.test!".to_vec().into_boxed_slice(),
Name::root(),
))],
("_sip._tcp.seer.test", HickoryRecordType::SRV) => vec![HickoryRData::SRV(wire::SRV::new(
10,
5,
5060,
name("sipserver.seer.test."),
))],
("1.2.0.192.in-addr.arpa", HickoryRecordType::PTR) => {
vec![HickoryRData::PTR(wire::PTR(name("ptr.seer.test.")))]
}
("www.seer.test", HickoryRecordType::CNAME) => {
vec![HickoryRData::CNAME(wire::CNAME(name("edge.cdn.test.")))]
}
(
"1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa",
HickoryRecordType::PTR,
) => vec![HickoryRData::PTR(wire::PTR(name("one.one.one.one.")))],
_ => vec![],
}
}
fn response_skeleton(request: &Message) -> Message {
let mut response = Message::response(request.metadata.id, OpCode::Query);
response.metadata.recursion_desired = request.metadata.recursion_desired;
response.metadata.recursion_available = true;
for query in &request.queries {
response.add_query(query.clone());
}
response
}
pub(crate) async fn spawn_mock_dns(mode: MockMode) -> u16 {
spawn_mock_dns_fn(move |qname, qtype| match mode {
MockMode::Zone => MockReply::Answer(zone_answers(qname, qtype)),
MockMode::Nxdomain => MockReply::NxDomain,
MockMode::NoData => MockReply::NoData,
MockMode::Ignore => MockReply::NoReply,
})
.await
}
pub(crate) async fn spawn_mock_dns_sequence(answer_sets: Vec<Vec<HickoryRData>>) -> u16 {
let mut served = 0usize;
spawn_mock_dns_fn(move |_, _| {
let idx = served.min(answer_sets.len().saturating_sub(1));
served += 1;
MockReply::Answer(answer_sets.get(idx).cloned().unwrap_or_default())
})
.await
}
pub(crate) fn soa_rdata(zone: &str) -> HickoryRData {
HickoryRData::SOA(wire::SOA::new(
name(&format!("ns1.{zone}.")),
name(&format!("hostmaster.{zone}.")),
2026070101,
7200,
3600,
1209600,
300,
))
}
pub(crate) enum MockReply {
Answer(Vec<HickoryRData>),
AuthoritativeAnswer(Vec<HickoryRData>),
Referral(Vec<HickoryRData>),
NoData,
NoDataWithSoa(&'static str),
NxDomain,
ServFail,
Refused,
NoReply,
}
pub(crate) async fn spawn_mock_dns_fn<F>(mut handler: F) -> u16
where
F: FnMut(&str, HickoryRecordType) -> MockReply + Send + 'static,
{
let socket = UdpSocket::bind("127.0.0.1:0").await.expect("bind mock DNS");
let port = socket.local_addr().expect("mock DNS local addr").port();
tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
let Ok((len, src)) = socket.recv_from(&mut buf).await else {
return;
};
let Ok(request) = Message::from_vec(&buf[..len]) else {
continue;
};
let mut response = response_skeleton(&request);
if let Some(query) = request.queries.first() {
let qname = query.name.to_ascii().to_ascii_lowercase();
let owned = |rdata| Record::from_rdata(query.name.clone(), 300, rdata);
match handler(qname.trim_end_matches('.'), query.query_type) {
MockReply::Answer(answers) => {
response.add_answers(answers.into_iter().map(owned));
}
MockReply::AuthoritativeAnswer(answers) => {
response.metadata.authoritative = true;
response.add_answers(answers.into_iter().map(owned));
}
MockReply::Referral(records) => {
response.add_authorities(records.into_iter().map(owned));
}
MockReply::NoData => {}
MockReply::NoDataWithSoa(zone) => {
response.add_authority(Record::from_rdata(
name(&format!("{zone}.")),
300,
soa_rdata(zone),
));
}
MockReply::NxDomain => {
response.metadata.response_code = ResponseCode::NXDomain;
}
MockReply::ServFail => {
response.metadata.response_code = ResponseCode::ServFail;
}
MockReply::Refused => {
response.metadata.response_code = ResponseCode::Refused;
}
MockReply::NoReply => continue,
}
}
let Ok(bytes) = response.to_vec() else {
continue;
};
let _ = socket.send_to(&bytes, src).await;
}
});
port
}
pub(crate) fn mock_dns_resolver(port: u16) -> DnsResolver {
DnsResolver::new()
.with_timeout(Duration::from_millis(500))
.allowing_private_hosts()
.with_port(port)
}
pub(crate) fn mock_dns_resolver_default(port: u16) -> DnsResolver {
mock_dns_resolver(port).with_default_nameserver("127.0.0.1")
}