This commit is contained in:
2025-10-26 00:33:21 +02:00
commit df417b4b66
4 changed files with 1224 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1091
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "spamlist_monitor"
version = "0.1.0"
edition = "2024"
[dependencies]
ipnet = "2.11.0"
thiserror = "2.0.17"
trust-dns-resolver = "0.23.2"

123
src/main.rs Normal file
View File

@@ -0,0 +1,123 @@
use ipnet::{Ipv6Net, PrefixLenError};
use std::net::{AddrParseError, IpAddr};
use thiserror::Error;
use trust_dns_resolver::{
Resolver,
config::{ResolverConfig, ResolverOpts},
error::ResolveError,
};
static BLACKLISTS_IPV4: &[&str] = &[
"zen.spamhaus.org",
"bl.spamcop.net",
"b.barracudacentral.org",
"dnsbl.sorbs.net",
];
static BLACKLISTS_IPV6: &[&str] = &["zen.spamhaus.org", "dnsbl.sorbs.net", "psbl.surriel.com"];
fn reverse_ipv4(ip: &str) -> String {
ip.split('.').rev().collect::<Vec<_>>().join(".")
}
#[derive(Error, Debug)]
enum ReverseIpv6Error {
#[error("failed to parse ip address")]
FailedToParse(#[from] AddrParseError),
#[error("ip address is not v6")]
NotIpv6(IpAddr),
#[error("ip address failed to expand to prefix len")]
FailedExpansion(#[from] PrefixLenError),
}
fn reverse_ipv6(ip: &str) -> Result<String, ReverseIpv6Error> {
let addr: IpAddr = ip.parse().map_err(ReverseIpv6Error::FailedToParse)?;
let IpAddr::V6(v6) = addr else {
return Err(ReverseIpv6Error::NotIpv6(addr));
};
let expanded = Ipv6Net::new(v6, 128)
.map_err(ReverseIpv6Error::FailedExpansion)?
.addr()
.to_string()
.replace(":", "");
Ok(expanded
.chars()
.rev()
.map(|c| format!("{}.{}", c, ""))
.collect::<String>()
.trim_end_matches('.')
.to_string())
}
fn check_blacklists(domain: &str) -> Result<Vec<(String, bool, String)>, ResolveError> {
let resolver = Resolver::new(ResolverConfig::default(), ResolverOpts::default())?;
let v4s: Vec<String> = resolver
.ipv4_lookup(domain)
.map(|ips| ips.iter().map(|ip| ip.to_string()).collect())?;
let v6s: Vec<String> = resolver
.ipv6_lookup(domain)
.map(|ips| ips.iter().map(|ip| ip.to_string()).collect())?;
Ok(BLACKLISTS_IPV4
.iter()
.map(|bl| {
v4s.iter().map(|v4| {
let query = format!("{}.{}", reverse_ipv4(v4), *bl);
let result = resolver.lookup_ip(query);
(bl.to_string(), result.is_ok(), v4.clone())
})
})
.flatten()
.chain(
BLACKLISTS_IPV6
.iter()
.map(|bl| {
v6s.iter().map(|v6| match reverse_ipv6(v6) {
Ok(rv6) => {
let query = format!("{}.{}", rv6, *bl);
let result = resolver.lookup_ip(query);
(bl.to_string(), result.is_ok(), v6.clone())
}
Err(e) => {
eprintln!("ERROR: Failed to reverse ipv6 {e}");
(String::new(), false, String::new())
}
})
})
.flatten(),
)
.collect())
}
fn main() {
let email_servers = ["microbel.pvv.ntnu.no"];
for domain in email_servers {
println!("Checking {domain}:");
match check_blacklists(domain) {
Ok(spamlist) => {
for (bl, listed, email_ip) in spamlist {
println!(
"email server ip: {email_ip}\t{:24} {}",
bl,
if listed { "LISTED" } else { "OK" }
);
}
}
Err(e) => {
eprintln!("ERROR: {e}");
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn reverse_ipv4() {
assert_eq!(super::reverse_ipv4("110.70.54.107"), "107.54.70.110");
}
}