//! ident — optional RFC 1413 (Ident) lookups. When a connection class (or the //! global `useident = yes`) asks for one, we ask the client's host (port 113) who //! owns the connection; a confirmed reply becomes the visible username without the //! `~` that marks an unverified ident. `requireident` refuses clients whose ident //! can't be confirmed. The lookup runs on a short-lived worker thread (like the //! resolver) and reports back as `Event::Ident`, so the core never blocks. use std::io::{Read, Write}; use std::net::{IpAddr, SocketAddr, TcpStream}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; use crate::ircd::Event; use crate::modules::connclass; use crate::server::Server; use crate::users::ident_of; use crate::Uid; /// Default per-lookup timeout; overridable with `ident_timeout` (seconds). const IDENT_TIMEOUT: u64 = 5; /// Cap on concurrent lookups so a connection flood can't spawn unbounded threads. const MAX_ACTIVE: usize = 256; static ACTIVE: AtomicUsize = AtomicUsize::new(0); /// Per-user lookup result stored on `User.ext`: `Some(name)` confirmed, `None` not. struct IdentResult(Option); /// `(useident, requireident)` combining the client's class with the global default. fn policy(s: &Server, uid: Uid) -> (bool, bool) { let (cu, cr) = connclass::ident_policy(s, uid); ( cu || s.conf_bool("useident", false), cr || s.conf_bool("requireident", false), ) } /// Start an ident lookup for a freshly-connected client if its class (or the global /// config) wants one. Sets `ident_pending` to hold registration until the reply /// arrives. Returns whether a lookup was started. pub fn dispatch(s: &mut Server, uid: Uid) -> bool { let (useident, requireident) = policy(s, uid); if !useident && !requireident { return false; } let (ip, their_port, our_port) = match s.users.get(&uid) { Some(u) => (u.addr.ip(), u.addr.port(), u.port), None => return false, }; if ACTIVE.fetch_add(1, Ordering::Relaxed) >= MAX_ACTIVE { ACTIVE.fetch_sub(1, Ordering::Relaxed); return false; // too many in flight: skip (treated as no ident) } let timeout = Duration::from_secs(s.conf_num("ident_timeout", IDENT_TIMEOUT)); if let Some(u) = s.users.get_mut(&uid) { u.ident_pending = true; } s.notice_star(uid, "Checking Ident"); let tx = s.event_tx.clone(); std::thread::spawn(move || { let ident = lookup(ip, their_port, our_port, timeout); ACTIVE.fetch_sub(1, Ordering::Relaxed); let _ = tx.send(Event::Ident { uid, ident }); }); true } /// The blocking RFC 1413 exchange: connect to `:113`, ask about the connection /// pair, and return the confirmed username (unsanitised) or `None`. fn lookup(ip: IpAddr, their_port: u16, our_port: u16, timeout: Duration) -> Option { let mut stream = TcpStream::connect_timeout(&SocketAddr::new(ip, 113), timeout).ok()?; stream.set_read_timeout(Some(timeout)).ok()?; stream.set_write_timeout(Some(timeout)).ok()?; // the query is ", " let query = format!("{their_port}, {our_port}\r\n"); stream.write_all(query.as_bytes()).ok()?; let mut buf = Vec::new(); let mut chunk = [0u8; 256]; loop { match stream.read(&mut chunk) { Ok(0) => break, Ok(n) => { buf.extend_from_slice(&chunk[..n]); if buf.len() > 512 || buf.contains(&b'\n') { break; } } Err(_) => break, } } parse_reply(&String::from_utf8_lossy(&buf)) } /// Parse an ident reply: `, : USERID : : `. fn parse_reply(reply: &str) -> Option { let fields: Vec<&str> = reply.split(':').collect(); if fields.len() < 4 || !fields[1].trim().eq_ignore_ascii_case("USERID") { return None; } let name = fields[3].trim(); (!name.is_empty()).then(|| name.to_string()) } /// A lookup finished: stash the result and clear the registration hold. The result /// is applied to the visible ident at registration (so a late USER can't clobber it). pub fn on_result(s: &mut Server, uid: Uid, ident: Option) { match &ident { Some(name) => s.notice_star(uid, &format!("Received Ident response: {name}")), None => s.notice_star(uid, "No Ident response"), } if let Some(u) = s.users.get_mut(&uid) { *u.ext.get_or_insert_with(|| IdentResult(None)) = IdentResult(ident); u.ident_pending = false; } } /// At registration, apply a confirmed ident (dropping the leading `~`) and enforce /// `requireident`. Returns `Some(reason)` to reject. pub fn finalize(s: &mut Server, uid: Uid) -> Option { let confirmed = s .users .get(&uid) .and_then(|u| u.ext.get::().map(|r| r.0.clone())); if let Some(Some(name)) = confirmed { let clean = ident_of(&name); // sanitised, no `~` if let Some(u) = s.users.get_mut(&uid) { u.ident = clean; } } let (_, requireident) = policy(s, uid); if requireident { let unverified = s .users .get(&uid) .map(|u| u.ident.is_empty() || u.ident.starts_with('~')) .unwrap_or(true); if unverified { return Some("Your connection class requires a valid ident response".to_string()); } } None } #[cfg(test)] mod tests { use super::*; #[test] fn parses_userid_and_error() { assert_eq!( parse_reply("6193, 6667 : USERID : UNIX : reverse\r\n").as_deref(), Some("reverse") ); assert_eq!(parse_reply("6193, 6667 : ERROR : NO-USER"), None); assert_eq!(parse_reply("garbage"), None); // case-insensitive reply type, trailing charset field tolerated assert_eq!( parse_reply("1,2 : userid : UNIX,US-ASCII : bob").as_deref(), Some("bob") ); } }