#![forbid(unsafe_code)]
#![forbid(missing_docs)]
mod blocklist;
mod err;
mod join_all;
pub use crate::err::{Error, Result};
use crate::{blocklist::BlockList, join_all::join_all};
use dnsclientx::DNSClient;
use log::Level::Debug;
use log::{debug, log_enabled};
use smol::future::FutureExt;
use std::io::ErrorKind;
use std::{fs::File, io::Read, matches, net::IpAddr};
const RESOLV_CONF: &str = "/etc/resolv.conf";
#[derive(Clone)]
pub struct MxDns {
bootstrap: DNSClient,
blocklists: Vec<String>,
}
#[derive(Debug)]
pub enum FCrDNS {
NoReverse,
UnConfirmed(String),
Confirmed(String),
}
impl FCrDNS {
pub fn is_confirmed(&self) -> bool {
matches!(self, Self::Confirmed(_))
}
}
impl MxDns {
pub fn new<S>(blocklists_fqdn: S) -> Result<Self>
where
S: IntoIterator,
S::Item: Into<String>,
{
let mut buf = Vec::with_capacity(256);
let mut file = File::open(RESOLV_CONF)
.map_err(|e| Error::ResolvConfRead(RESOLV_CONF.to_string(), e))?;
file.read_to_end(&mut buf)
.map_err(|e| Error::ResolvConfRead(RESOLV_CONF.to_string(), e))?;
let conf = resolv_conf::Config::parse(&buf)
.map_err(|e| Error::ResolvConfParse(RESOLV_CONF.to_string(), e))?;
let nameservers = conf.get_nameservers_or_local();
if let Some(ip) = nameservers.first() {
let ip_addr: IpAddr = ip.into();
Ok(Self::with_dns(ip_addr, blocklists_fqdn))
} else {
Err(Error::NoNameservers(RESOLV_CONF.to_string()))
}
}
pub fn with_dns<I, S>(bootstrap_dns: I, blocklists_fqdn: S) -> Self
where
I: Into<IpAddr>,
S: IntoIterator,
S::Item: Into<String>,
{
let ip = bootstrap_dns.into();
let socket_addr = (ip, 53).into();
let bootstrap = DNSClient::new(vec![socket_addr]);
let blocklists: Vec<String> = blocklists_fqdn.into_iter().map(|i| i.into()).collect();
Self {
bootstrap,
blocklists,
}
}
pub fn on_blocklists<A>(&self, addr: A) -> Vec<Result<bool>>
where
A: Into<IpAddr>,
{
if self.blocklists.is_empty() {
return vec![];
}
let ip: IpAddr = addr.into();
let ret = smol::block_on({
let mut all_checks = Vec::new();
for blocklist in &self.blocklists {
let one_check = self.check_blocklist(blocklist, ip);
all_checks.push(one_check.boxed());
}
join_all(all_checks)
});
if log_enabled!(Debug) {
for i in ret.iter().enumerate() {
debug!("{} is blocked by {} = {:?}", ip, self.blocklists[i.0], i.1);
}
}
ret
}
async fn check_blocklist(&self, blocklist: &str, ip: IpAddr) -> Result<bool> {
let resolver = BlockList::lookup_ns(blocklist, &self.bootstrap).await?;
let blocklist_lookup = BlockList::new(resolver, blocklist);
blocklist_lookup.is_blocked(ip).await
}
pub fn is_blocked<A>(&self, addr: A) -> Result<bool>
where
A: Into<IpAddr>,
{
let mut res = self.on_blocklists(addr);
if res.is_empty() {
Ok(false)
} else if res.iter().all(|r| r.is_err()) {
res.pop().unwrap_or(Ok(false))
} else {
let is_blocked = res.into_iter().any(|r| r.unwrap_or(false));
Ok(is_blocked)
}
}
pub fn reverse_dns<A>(&self, ip: A) -> Result<Option<String>>
where
A: Into<IpAddr>,
{
let res = smol::block_on(self.bootstrap.query_ptr(ip.into()));
match res {
Ok(fqdn) => Ok(Some(fqdn)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Reverse("reverse_dns".into(), e)),
}
}
pub fn fcrdns<A>(&self, ip: A) -> Result<FCrDNS>
where
A: Into<IpAddr>,
{
let ipaddr = ip.into();
let fqdn = match self.reverse_dns(ipaddr)? {
None => return Ok(FCrDNS::NoReverse),
Some(s) => s,
};
debug!("reverse lookup for {} = {}", ipaddr, fqdn);
let forward = smol::block_on(self.bootstrap.query_a(&fqdn))
.map_err(|e| Error::DnsQuery("fcrdns".to_string(), e))?;
let is_confirmed = forward.contains(&ipaddr);
if is_confirmed {
Ok(FCrDNS::Confirmed(fqdn))
} else {
Ok(FCrDNS::UnConfirmed(fqdn))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
use std::net::Ipv4Addr;
const BOOTSTRAP_DNS: IpAddr = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
fn blocklists() -> Vec<(&'static str, bool)> {
vec![
("zen.spamhaus.org", true),
("bl.spamcop.net", false),
("dnsbl-1.uceprotect.net", true),
("b.barracuda.central.org", false),
("cbl.abuseat.org", true),
]
}
fn lookup_host(host: &str) -> Result<IpAddr> {
let socket_addr = (BOOTSTRAP_DNS, 53).into();
let dns = DNSClient::new(vec![socket_addr]);
smol::block_on(dns.query_a(host))
.and_then(|res| {
res.first()
.cloned()
.ok_or_else(|| io::Error::new(ErrorKind::Other, "no dns entries"))
})
.map_err(|e| Error::DnsQuery(host.to_string(), e))
}
fn build_mx_dns() -> MxDns {
let blocklists = blocklists()
.iter()
.map(|t| t.0)
.collect::<Vec<&'static str>>();
MxDns::with_dns(BOOTSTRAP_DNS, blocklists)
}
#[test]
fn empty_blocklists() {
let empty: Vec<String> = Vec::new();
let mxdns = MxDns::with_dns(BOOTSTRAP_DNS, empty);
let blocked = mxdns.is_blocked(Ipv4Addr::new(127, 0, 0, 2)).unwrap();
assert!(!blocked);
}
#[test]
fn blocklist_addrs() {
let mxdns = build_mx_dns();
let blocklists = blocklists();
for b in blocklists {
let ns = smol::block_on(mxdns.bootstrap.query_ns(b.0));
if b.1 {
assert!(matches!(ns, Ok(_)), "no NS for {}", b.0);
} else {
assert!(
matches!(&ns, Ok(v) if v.is_empty()),
"unexpected NS result {:?} for {}",
ns,
b.0
);
}
}
}
#[test]
fn not_blocked() {
let mxdns = build_mx_dns();
let blocked = mxdns.is_blocked([127, 0, 0, 1]).unwrap();
assert!(!blocked);
}
#[test]
fn blocked() {
let mxdns = build_mx_dns();
let blocked = mxdns.is_blocked([127, 0, 0, 2]).unwrap();
assert!(blocked);
}
#[test]
fn reverse_lookup() {
let alienscience_ip =
lookup_host("mail.alienscience.org").expect("Cannot lookup mailserver address");
let mxdns = build_mx_dns();
let reverse = mxdns.reverse_dns(alienscience_ip).unwrap().unwrap();
assert_eq!(reverse, "mail.alienscience.org");
}
#[test]
fn fcrdns_ok() {
let alienscience_ip =
lookup_host("mail.alienscience.org").expect("Cannot lookup mailserver address");
let mxdns = build_mx_dns();
let res = mxdns.fcrdns(alienscience_ip);
assert!(
matches!(res, Ok(FCrDNS::Confirmed(_))),
"Valid mail server failed fcrdns: {:?}",
res
);
}
#[test]
fn fcrdns_google_ok() {
let mxdns = build_mx_dns();
let res = mxdns.fcrdns([209, 85, 167, 66]);
assert!(
matches!(res, Ok(FCrDNS::Confirmed(_))),
"Valid google server failed fcrdns: {:?}",
res
);
}
#[test]
fn fcrdns_fail() {
let mxdns = build_mx_dns();
let res = mxdns.fcrdns([127, 0, 0, 2]);
assert!(
matches!(res, Ok(FCrDNS::NoReverse) | Ok(FCrDNS::UnConfirmed(_))),
"Known bad forward confirm failed: {:?}",
res
);
}
}