Expand description
§mailrs-dnsbl
RFC 5782 DNS-based blocklist (DNSBL) lookup: reverse-IPv4 query construction, Spamhaus return-code interpretation, and an in-process TTL cache covering both positive and negative hits.
For inbound IP 1.2.3.4 against zone "zen.spamhaus.org":
query → 4.3.2.1.zen.spamhaus.org (A record)
result → 127.0.0.2 → DnsblResult::Sbl (Spamhaus Block List)
127.0.0.4 → DnsblResult::Xbl (Exploits Block List)
NXDOMAIN → None (not listed → Clean)Carved out of mailrs-shield’s dnsbl module — same code, same API,
its own crate so users who only want DNSBL don’t pull greylist + PTR
- Postgres dependencies.
§Quickstart
use hickory_resolver::TokioResolver;
use mailrs_dnsbl::DnsblCache;
use std::net::IpAddr;
use std::time::Duration;
let cache = DnsblCache::new(Duration::from_secs(300));
let zones = vec!["zen.spamhaus.org".into(), "bl.spamcop.net".into()];
match cache.check(resolver, ip, &zones).await {
Some((zone, result)) => {
eprintln!("listed in {zone}: {result:?}");
// Reject the connection / score it
}
None => {
// Not listed by any zone → accept
}
}§What this crate does
reverse_ipv4(ip)—1.2.3.4 → "4.3.2.1", the canonical RFC 5782 §2.1 query formdnsbl_query(reversed, zone)—"4.3.2.1.zen.spamhaus.org"interpret_spamhaus(reply)— map127.0.0.xreply codes to a typedDnsblResult. Sbl / Css / Xbl / Pbl + aListed(other)fallback for non-Spamhaus DNSBLs sharing the 127.0.0.x conventioncheck_dnsbl(resolver, ip, zones)— fan-out lookup, returns the FIRST zone that lists the IP (early exit, doesn’t continue)DnsblCache— TTL-cached wrapper aroundcheck_dnsbl. Caches both positive and negative lookups;cleanup()drops expired entriesis_ipv6_dnsbl_supported— stub returningfalse. Most DNSBL operators don’t support v6 lookups; the function is the extension point for the day they do.
§What this crate does not
- No DNS resolver of its own. Bring
hickory-resolverand pass it tocheck_dnsbl/DnsblCache::check. That’s a deliberate choice: if your app already has a configured resolver (with timeouts, retry policy, DoH/DoT, etc.), this crate uses it. Picking the resolver for you would be opinionated. - No URI BL (RFC 5782 §2.2 — querying for URIs found in body content). That’s a different lookup shape; out of scope for 1.x.
- No URIBL caching semantics beyond TTL. If you want pluggable
storage (Redis, Postgres, …) for the cache, wrap
check_dnsblyourself with whatever store you like.
§Performance
Measured (criterion, M-series Mac, release, 100-sample median):
| Operation | Median |
|---|---|
reverse_ipv4 | ~14 ns |
dnsbl_query (~20-char zone) | ~25 ns |
interpret_spamhaus (Sbl reply) | ~700 ps |
interpret_spamhaus (non-127.x → Clean) | ~700 ps |
DnsblCache::check (cached hit) | ~80 ns |
Note check_dnsbl itself is DNS-bound — milliseconds, not nanoseconds.
The bench numbers above are for the CPU pieces. The cache HIT path is
what matters for inbound throughput: a typical inbound run sees the
same IP repeatedly within a session, so cache hit ≈ 80 ns vs. cache
miss ≈ DNS RTT.
Reproduce: cargo bench -p mailrs-dnsbl --bench dnsbl. Workspace
PERFORMANCE.md carries the same table.
§License
Apache-2.0 OR MIT.
Structs§
- Dnsbl
Cache - TTL-cached DNSBL lookup. Avoids repeated DNS queries for IPs we’ve seen recently.
Enums§
- Dnsbl
Result - Spamhaus return codes (127.0.0.x) per https://www.spamhaus.org/zen/.
Functions§
- check_
dnsbl - Run the DNSBL lookup against each zone in order; return the first
zone that lists
ip, orNoneif no zone lists it. - dnsbl_
query - Build a DNSBL query hostname:
<reversed_ip>.<zone>per RFC 5782 §2.1. - interpret_
spamhaus - Interpret a Spamhaus-style A record response as a
DnsblResult. - is_
ipv6_ dnsbl_ supported - Stub: IPv6 isn’t supported by most DNSBL operators (the reverse-IP
scheme is impractical at IPv6 scale). Always returns
false. Kept as an extension point in case a future operator adopts IPv6 lookups. - reverse_
ipv4 - Reverse an IPv4 address into the dotted-octets form used for DNSBL
lookup per RFC 5782 §2.1:
1.2.3.4→"4.3.2.1".