conn_waitpong: optionally require a PONG cookie before registration (anti-bot)

This commit is contained in:
Jean Chevronnet 2026-08-09 23:04:14 +00:00
parent e0d849b4c9
commit 61954f6c5c
7 changed files with 70 additions and 2 deletions

View file

@ -117,6 +117,11 @@ amu_target = both
# hidewhois_hide_server = yes # hide 312
# hidewhois_hide_idle = yes # hide 317
# hidewhois_hide_secure = yes # hide 671
# --- conn_waitpong (m_conn_waitpong): hold registration until the client answers
# a server PING with the exact cookie — filters bots that never PONG. Real
# clients auto-reply, so it's transparent to them.
# conn_waitpong = yes
# conn_waitpong_killonbadreply = yes # drop on a wrong pong (default: keep waiting)
# --- showfile (m_showfile): serve a text file as its own command. One line per
# file: `showfile = <COMMAND> <path>`. The file is read fresh each use, so
# edits show without a rehash. e.g. make /RULES stream a rules file:

View file

@ -555,8 +555,10 @@ impl Command for Pong {
fn before_reg(&self) -> bool {
true
}
fn handle(&self, _s: &mut Server, _uid: Uid, _params: &[String]) -> CmdResult {
CmdResult::Ok // keepalive; nothing to do yet
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
// conn_waitpong: a pre-registration PONG may be answering our cookie
crate::modules::conn_waitpong::on_pong(s, uid, params);
CmdResult::Ok // otherwise just a keepalive
}
}

View file

@ -384,6 +384,7 @@ impl Ircd {
&& !u.ident.is_empty()
&& !u.cap
&& !u.dns_pending
&& u.waitpong.is_none()
})
.unwrap_or(false);
if ready {

View file

@ -0,0 +1,54 @@
//! conn_waitpong — optionally hold registration until the client answers a server
//! 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)
//! ```
//!
//! The gate itself is the core `User.waitpong` field (checked in `try_register`,
//! like `dns_pending`); this module just arms it at connect and clears it on the
//! matching PONG. Behaviour reference: InspIRCd's `m_conn_waitpong`. Native Rust.
use crate::server::Server;
use crate::Uid;
/// A short random cookie the client must echo back in its PONG.
fn cookie() -> String {
let mut b = [0u8; 8];
let _ = openssl::rand::rand_bytes(&mut b);
b.iter().map(|x| format!("{x:02x}")).collect()
}
/// At connect: if enabled, stash a cookie on the user and PING it. `try_register`
/// will not complete while `User.waitpong` is set.
pub fn arm(s: &mut Server, uid: Uid) {
if !s.conf_bool("conn_waitpong", false) {
return;
}
let c = cookie();
if let Some(u) = s.users.get_mut(&uid) {
u.waitpong = Some(c.clone());
}
s.send(uid, format!(":{} PING :{c}", s.name));
}
/// On PONG: clear the gate if the cookie matches. On a wrong reply, optionally drop
/// the client (else keep waiting — a real client will retry on the next PING).
pub fn on_pong(s: &mut Server, uid: Uid, params: &[String]) {
let Some(want) = s.users.get(&uid).and_then(|u| u.waitpong.clone()) else {
return; // not waiting (already satisfied, or feature off)
};
let got = params.last().map(String::as_str).unwrap_or("");
if got == want {
if let Some(u) = s.users.get_mut(&uid) {
u.waitpong = None;
}
} else if s.conf_bool("conn_waitpong_killonbadreply", false) {
s.send(
uid,
"ERROR :Closing link (incorrect ping reply)".to_string(),
);
s.remove_user(uid, "Incorrect ping reply");
}
}

View file

@ -12,6 +12,7 @@ pub mod channelban;
pub mod chathistory;
pub mod cloak;
pub mod cloudflare_challenge;
pub mod conn_waitpong;
pub mod connectban;
pub mod connflood;
pub mod customtitle;

View file

@ -318,6 +318,7 @@ impl Server {
addr,
registered: false,
dns_pending: false,
waitpong: None,
deferred: Vec::new(),
cap: false,
cap_302: false,
@ -393,6 +394,8 @@ impl Server {
"Couldn't look up your hostname; using your IP address instead",
);
}
// conn_waitpong: optionally hold registration until the client PONGs a cookie
crate::modules::conn_waitpong::arm(self, uid);
}
/// Fire an HTTP POST on a worker thread and deliver `(status, body)` back to
@ -1039,6 +1042,7 @@ mod tests {
addr: "127.0.0.1:1".parse().unwrap(),
registered: true,
dns_pending: false,
waitpong: None,
deferred: Vec::new(),
cap: false,
cap_302: false,

View file

@ -286,6 +286,7 @@ pub struct User {
pub addr: SocketAddr,
pub registered: bool,
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
pub waitpong: Option<String>, // conn_waitpong: cookie the client must PONG before registering
pub deferred: Vec<String>, // handshake lines held while dns_pending (replayed after)
pub cap: bool, // CAP negotiation in progress (holds registration)
pub cap_302: bool, // client sent CAP LS 302 (cap-notify aware)