echoIRCd/src/modules/dnsbl.rs

103 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! DNSBL: DNS blocklist checks on connect. The resolver thread reverses the
//! client's IP under each configured blocklist zone and A-looks it up (see
//! [`crate::resolver`]); a listing triggers the configured action. Works for IPv4
//! (reversed octets) and IPv6 (reversed nibbles); a v4-only blocklist NXDOMAINs a
//! v6 query, which reads as "not listed".
//!
//! Actions (`dnsbl_action`): `mark` shows the notice and lets them in (default),
//! `kill` disconnects, `kline`/`gline`/`zline` add a 1-day ban and disconnect.
//! Driven from the connection lifecycle (`Server::add_conn` → `on_resolved`)
//! rather than as a hook `Module`.
use std::net::{IpAddr, Ipv4Addr};
use std::time::Duration;
use crate::resolver;
use crate::server::Server;
use crate::xline::XKind;
use crate::Uid;
/// Ban length applied by the `*line` actions on a hit.
const DNSBL_BAN: u64 = 86_400; // default ban length (1 day) if `dnsbl_duration` unset
/// Outcome of a DNSBL check for one connecting client.
pub enum Outcome {
/// No blocklists configured — the check didn't run.
Skipped,
/// Checked against every zone; the address is not listed.
Clean,
/// Listed: `zone` returned `reply` (`127.0.0.x`, last octet = reason code).
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
/// core thread (called from the resolver worker), so it may block on DNS.
pub fn check(ip: IpAddr, zones: &[String], timeout: Duration) -> Outcome {
if zones.is_empty() {
return Outcome::Skipped;
}
// 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 qname = format!("{}.{z}", resolver::reverse_labels(ip));
if let Some(reply) = resolver::a_lookup(&qname, timeout) {
return Outcome::Hit {
zone: zone.clone(),
reply,
};
}
}
Outcome::Clean
}
/// Emit the DNSBL notices for `outcome` and, on a hit, take the configured action.
/// Called from `Server::on_resolved` on the core thread.
pub fn report(s: &mut Server, uid: Uid, outcome: Outcome) {
match outcome {
Outcome::Skipped => {}
Outcome::Clean => {
s.notice_star(uid, "Checking for DNSBL");
s.notice_star(uid, "Checking for DNSBL done, no hit.");
}
Outcome::Hit { zone, reply } => {
s.notice_star(uid, "Checking for DNSBL");
s.notice_star(
uid,
&format!("Checking for DNSBL done — LISTED on {zone} ({reply})."),
);
act(s, uid, &zone, reply);
}
}
}
/// Act on a hit per `dnsbl_action`: `mark` just informs; the `*line` actions add a
/// temporary ban and close; `kill` closes without a persistent ban.
fn act(s: &mut Server, uid: Uid, zone: &str, reply: Ipv4Addr) {
let (mask, ip) = match s.users.get(&uid) {
Some(u) => (u.prefix(), u.addr.ip()),
None => return,
};
let action = s.dnsbl_action.clone();
s.snotice_c('d', &format!(
"DNSBL: {mask} is listed on {zone} ({reply}); action={action}"
));
let reason = format!("{} (listed on {zone})", s.dnsbl_reason);
let ipstr = ip.to_string();
let dur = s.conf_num("dnsbl_duration", DNSBL_BAN);
match action.as_str() {
"kline" => s.add_xline(XKind::Kline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
"gline" => s.add_xline(XKind::Gline, &format!("*@{ipstr}"), dur, "dnsbl", &reason),
"zline" => s.add_xline(XKind::Zline, &ipstr, dur, "dnsbl", &reason),
"kill" | "reject" => {}
_ => return, // "mark" or unknown: notify only, don't disconnect
}
// pre-registration users aren't caught by add_xline's enforce sweep, so close
// this connection explicitly (the ERROR flushes before the socket).
s.send(uid, format!("ERROR :Closing link: ({reason})"));
s.remove_user(uid, &reason);
}