use std::net::ToSocketAddrs;
const DNS_BLOCKLISTS: &[(&str, &str)] = &[
("zen.spamhaus.org", "Spamhaus ZEN"),
("bl.spamcop.net", "SpamCop"),
("dnsbl.sorbs.net", "SORBS"),
];
pub fn check_dns_blocklist(domain: &str) -> Vec<String> {
let ips = match resolve_domain_ips(domain) {
Some(ips) => ips,
None => return Vec::new(),
};
let mut matches = Vec::new();
for ip in &ips {
let reversed = reverse_ipv4(ip);
let reversed = match reversed {
Some(r) => r,
None => continue, };
for &(zone, display_name) in DNS_BLOCKLISTS {
let query = format!("{reversed}.{zone}");
if dnsbl_lookup(&query) {
let entry = display_name.to_string();
if !matches.contains(&entry) {
matches.push(entry);
}
}
}
}
matches
}
fn resolve_domain_ips(domain: &str) -> Option<Vec<String>> {
let lookup = format!("{domain}:0");
let addrs: Vec<String> = lookup
.to_socket_addrs()
.ok()?
.filter_map(|addr| {
if addr.is_ipv4() {
Some(addr.ip().to_string())
} else {
None
}
})
.collect();
if addrs.is_empty() {
None
} else {
Some(addrs)
}
}
fn reverse_ipv4(ip: &str) -> Option<String> {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return None;
}
for part in &parts {
part.parse::<u8>().ok()?;
}
Some(format!(
"{}.{}.{}.{}",
parts[3], parts[2], parts[1], parts[0]
))
}
fn dnsbl_lookup(query: &str) -> bool {
let lookup = format!("{query}:0");
lookup.to_socket_addrs().is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverse_ipv4_valid() {
assert_eq!(reverse_ipv4("1.2.3.4"), Some("4.3.2.1".to_string()));
assert_eq!(
reverse_ipv4("192.168.1.100"),
Some("100.1.168.192".to_string())
);
assert_eq!(reverse_ipv4("10.0.0.1"), Some("1.0.0.10".to_string()));
}
#[test]
fn test_reverse_ipv4_invalid() {
assert_eq!(reverse_ipv4("not-an-ip"), None);
assert_eq!(reverse_ipv4("1.2.3"), None);
assert_eq!(reverse_ipv4("1.2.3.4.5"), None);
assert_eq!(reverse_ipv4("::1"), None);
assert_eq!(reverse_ipv4("256.1.2.3"), None);
}
}