dnsbl: cap the number of blocklist zones checked per client at 16 — each zone is a serial blocking DNS lookup, so a long (mis)configured zone list could stall a connecting client's registration for zones.len()*timeout

This commit is contained in:
Jean Chevronnet 2026-08-19 01:21:40 +00:00
parent b63458816e
commit 8f5b37bf8d

View file

@ -30,13 +30,19 @@ pub enum Outcome {
Hit { zone: String, reply: Ipv4Addr }, Hit { zone: String, reply: Ipv4Addr },
} }
/// Most blocklist zones consulted per connecting client (latency bound).
const MAX_ZONES: usize = 16;
/// Check `ip` against every blocklist `zone`; the first listing wins. Runs off the /// Check `ip` against every blocklist `zone`; the first listing wins. Runs off the
/// core thread (called from the resolver worker), so it may block on DNS. /// core thread (called from the resolver worker), so it may block on DNS.
pub fn check(ip: IpAddr, zones: &[String], timeout: Duration) -> Outcome { pub fn check(ip: IpAddr, zones: &[String], timeout: Duration) -> Outcome {
if zones.is_empty() { if zones.is_empty() {
return Outcome::Skipped; return Outcome::Skipped;
} }
for zone in zones { // Each zone is a serial blocking lookup, so total latency is bounded by the
// number checked × timeout; cap it so a long (mis)configured zone list can't
// stall a client's registration for a very long time.
for zone in zones.iter().take(MAX_ZONES) {
let z = zone.trim().trim_end_matches('.'); let z = zone.trim().trim_end_matches('.');
let qname = format!("{}.{z}", resolver::reverse_labels(ip)); let qname = format!("{}.{z}", resolver::reverse_labels(ip));
if let Some(reply) = resolver::a_lookup(&qname, timeout) { if let Some(reply) = resolver::a_lookup(&qname, timeout) {