use futures::future::join_all;
use hickory_resolver::TokioResolver;
use std::collections::HashSet;
use std::net::IpAddr;
const MAX_WILDCARD_PROBES: usize = 64;
const STABLE_BATCHES: usize = 3;
pub async fn detect_wildcards(
domain: &str,
resolver: &TokioResolver,
probes: usize,
) -> HashSet<IpAddr> {
let batch_size = probes.max(1);
let mut ips = HashSet::new();
let mut total = 0usize;
let mut stable = 0usize;
while total < MAX_WILDCARD_PROBES && stable < STABLE_BATCHES {
let n = batch_size.min(MAX_WILDCARD_PROBES - total);
let futs = (0..n).map(|_| {
let probe = format!("gossan-wildcard-{}.{domain}", fastrand::u32(..));
async move {
let mut found = HashSet::new();
if let Ok(lookup) = resolver.lookup_ip(probe.as_str()).await {
for ip in lookup.iter() {
found.insert(ip);
}
}
found
}
});
let results = join_all(futs).await;
total += n;
let mut grew = false;
for set in results {
for ip in set {
if ips.insert(ip) {
grew = true;
}
}
}
if ips.is_empty() {
break;
}
if grew {
stable = 0;
} else {
stable += 1;
}
}
ips
}
#[cfg(test)]
mod tests {
use super::*;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use hickory_resolver::name_server::TokioConnectionProvider;
use hickory_resolver::TokioResolver;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use tokio::net::UdpSocket;
use tokio::task::JoinHandle;
async fn mock_dns_server(bind: SocketAddr) -> JoinHandle<()> {
let socket = Arc::new(UdpSocket::bind(bind).await.unwrap());
tokio::spawn(async move {
let mut buf = [0u8; 512];
loop {
let (len, peer) = match socket.recv_from(&mut buf).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("wildcard DNS mock recv_from failed: error={}", e);
continue;
}
};
let mut resp = Vec::from(&buf[..len]);
if resp.len() < 12 {
continue;
}
resp[2] = 0x81;
resp[3] = 0x80;
resp[6] = 0x00;
resp[7] = 0x01;
let mut i = 12usize;
while i < len && buf[i] != 0 {
i += 1 + buf[i] as usize;
}
i += 5; let _ = i;
resp.push(0xC0);
resp.push(0x0C);
resp.push(0x00);
resp.push(0x01);
resp.push(0x00);
resp.push(0x01);
resp.extend_from_slice(&300u32.to_be_bytes());
resp.push(0x00);
resp.push(0x04);
resp.extend_from_slice(&Ipv4Addr::new(1, 2, 3, 4).octets());
let _ = socket.send_to(&resp, peer).await;
}
})
}
async fn mock_rotating_dns_server(bind: SocketAddr, pool: Vec<Ipv4Addr>) -> JoinHandle<()> {
let socket = Arc::new(UdpSocket::bind(bind).await.unwrap());
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
tokio::spawn(async move {
let mut buf = [0u8; 512];
loop {
let (len, peer) = match socket.recv_from(&mut buf).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("wildcard DNS mock recv_from failed: error={}", e);
continue;
}
};
let mut resp = Vec::from(&buf[..len]);
if resp.len() < 12 {
continue;
}
resp[2] = 0x81;
resp[3] = 0x80;
resp[6] = 0x00;
resp[7] = 0x01;
let idx = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % pool.len();
let ip = pool[idx];
resp.push(0xC0);
resp.push(0x0C);
resp.push(0x00);
resp.push(0x01);
resp.push(0x00);
resp.push(0x01);
resp.extend_from_slice(&300u32.to_be_bytes());
resp.push(0x00);
resp.push(0x04);
resp.extend_from_slice(&ip.octets());
let _ = socket.send_to(&resp, peer).await;
}
})
}
fn resolver_for(addr: SocketAddr) -> TokioResolver {
let mut config = ResolverConfig::new();
let group = hickory_resolver::config::NameServerConfigGroup::from_ips_clear(
&[addr.ip()],
addr.port(),
false,
);
if let Some(ns) = group.into_inner().into_iter().next() {
config.add_name_server(ns);
}
let mut opts = ResolverOpts::default();
opts.timeout = std::time::Duration::from_secs(2);
opts.attempts = 1;
TokioResolver::builder_with_config(config, TokioConnectionProvider::default())
.with_options(opts)
.build()
}
#[tokio::test]
async fn wildcard_detects_mock_wildcard() {
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let actual_addr = socket.local_addr().unwrap();
drop(socket);
let server = mock_dns_server(actual_addr).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let resolver = resolver_for(actual_addr);
let ips = detect_wildcards("example.com", &resolver, 3).await;
server.abort();
assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
}
#[tokio::test]
async fn wildcard_covers_rotating_ip_pool() {
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let actual_addr = socket.local_addr().unwrap();
drop(socket);
let pool = vec![
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(2, 2, 2, 2),
Ipv4Addr::new(3, 3, 3, 3),
Ipv4Addr::new(4, 4, 4, 4),
Ipv4Addr::new(5, 5, 5, 5),
Ipv4Addr::new(6, 6, 6, 6),
Ipv4Addr::new(7, 7, 7, 7),
Ipv4Addr::new(8, 8, 8, 8),
];
let server = mock_rotating_dns_server(actual_addr, pool.clone()).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let resolver = resolver_for(actual_addr);
let ips = detect_wildcards("example.com", &resolver, 3).await;
server.abort();
for ip in pool {
assert!(
ips.contains(&IpAddr::V4(ip)),
"rotating pool IP {ip} must be sampled via stabilization"
);
}
}
}