diff --git a/src/modules/conn_waitpong.rs b/src/modules/conn_waitpong.rs index 607d5e6..66c1499 100644 --- a/src/modules/conn_waitpong.rs +++ b/src/modules/conn_waitpong.rs @@ -2,13 +2,18 @@ //! PING with the exact cookie we sent, filtering bots that never PONG. Config: //! //! ```text -//! conn_waitpong = yes # require the pong before registering (default off) -//! conn_waitpong_killonbadreply = yes # disconnect on a wrong pong (default: keep waiting) +//! conn_waitpong = yes # require the pong before registering (default off) +//! conn_waitpong_killonbadreply = yes # disconnect on a wrong pong (default: keep waiting) +//! conn_waitpong_exempt_localhost4 = yes # skip the cookie for 127.0.0.0/8 (default off) +//! conn_waitpong_exempt_localhost6 = yes # skip the cookie for ::1 (default off) +//! connectclass ... waitpongexempt=yes # skip the cookie for a whole class //! ``` //! //! The gate is the core `User.waitpong` field (checked in `try_register`); this //! module arms it at connect and clears it on the matching PONG. +use std::net::IpAddr; + use crate::server::Server; use crate::Uid; @@ -19,10 +24,29 @@ fn cookie() -> String { b.iter().map(|x| format!("{x:02x}")).collect() } +/// Whether this client is exempt from the cookie: a per-class opt-out, or a +/// loopback address whose family the config trusts. Both default off, so the +/// challenge still applies everywhere unless explicitly relaxed. +fn exempt(s: &Server, uid: Uid) -> bool { + if crate::modules::connclass::waitpong_exempt(s, uid) { + return true; + } + let Some(ip) = s.users.get(&uid).map(|u| u.addr.ip()) else { + return false; + }; + match ip { + IpAddr::V4(a) if a.is_loopback() => s.conf_bool("conn_waitpong_exempt_localhost4", false), + IpAddr::V6(a) if a.is_loopback() => s.conf_bool("conn_waitpong_exempt_localhost6", false), + // a loopback client on an IPv6 listener can arrive v4-mapped (::ffff:127.0.0.1) + IpAddr::V6(a) => a.to_ipv4_mapped().is_some_and(|m| m.is_loopback()) && s.conf_bool("conn_waitpong_exempt_localhost4", false), + _ => false, + } +} + /// At connect: if enabled, stash a cookie on the user and PING it. `try_register` -/// will not complete while `User.waitpong` is set. +/// will not complete while `User.waitpong` is set. Exempt sources skip it. pub fn arm(s: &mut Server, uid: Uid) { - if !s.conf_bool("conn_waitpong", false) { + if !s.conf_bool("conn_waitpong", false) || exempt(s, uid) { return; } let c = cookie(); diff --git a/src/modules/connclass.rs b/src/modules/connclass.rs index 399b039..e773cb2 100644 --- a/src/modules/connclass.rs +++ b/src/modules/connclass.rs @@ -52,6 +52,7 @@ pub struct ConnClass { pub requireident: bool, // refuse if the ident lookup fails pub resolvehostnames: bool, // resolve rDNS for this class (default yes) pub maxconnwarn: bool, // snotice opers when a limit refuses a client + pub waitpongexempt: bool, // skip the conn_waitpong cookie for this class } /// Split a `key=value` value on commas into non-empty pieces. @@ -91,6 +92,7 @@ fn apply(c: &mut ConnClass, k: &str, v: &str) { "requireident" => c.requireident = v.eq_ignore_ascii_case("yes"), "resolvehostnames" => c.resolvehostnames = !v.eq_ignore_ascii_case("no"), "maxconnwarn" => c.maxconnwarn = v.eq_ignore_ascii_case("yes"), + "waitpongexempt" => c.waitpongexempt = v.eq_ignore_ascii_case("yes"), _ => {} } } @@ -512,6 +514,10 @@ pub fn flood_over(s: &Server, uid: Uid) -> Option<(Option, Option, b pub fn resolve_hostnames(s: &Server, uid: Uid) -> bool { class_of(s, uid).map(|c| c.resolvehostnames).unwrap_or(true) } +/// Whether this client's class opts out of the conn_waitpong cookie. +pub fn waitpong_exempt(s: &Server, uid: Uid) -> bool { + class_of(s, uid).map(|c| c.waitpongexempt).unwrap_or(false) +} /// `(useident, requireident)` for this client's class. pub fn ident_policy(s: &Server, uid: Uid) -> (bool, bool) { class_of(s, uid)