diff --git a/echoircd.conf.example b/echoircd.conf.example index 7ac9006..59a3007 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -144,6 +144,7 @@ amu_target = both # threads that frame lines / run TLS crypto off the single state core. One acceptor # round-robins connections across the pool; the core stays single-threaded and lock-free. # io_threads = 0 # reactor workers; 0 = auto (one per core, capped at 4) +# tls_handshake_timeout = 15 # drop a TLS conn that stalls mid-handshake (secs; 0 = off) # --- ident (RFC1413): off by default; a connection class can also enable it --- # useident = yes # look up every client's ident (adds connect latency) diff --git a/src/main.rs b/src/main.rs index d8b58eb..ac54d75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,8 +58,12 @@ fn main() { }; let max_line = raw_num("max_line", socketengine::DEFAULT_MAX_LINE); let max_sendq = raw_num("max_sendq", socketengine::DEFAULT_MAX_SENDQ); - // plaintext reactor-pool size (0 = auto: one worker per core, capped) + // reactor-pool size (0 = auto: one worker per core, capped) let io_threads = raw_num("io_threads", 0); + // reap a TLS handshake that stalls this long (0 = never); guards the TLS port + // against connections that open but never negotiate + let hs = raw_num("tls_handshake_timeout", 15); + let handshake_timeout = (hs > 0).then(|| Duration::from_secs(hs as u64)); // trusted PROXY-protocol source globs (reactor rewrites the client IP from them) let proxy_trust: Vec = cfg.raw.get("proxy").cloned().unwrap_or_default(); @@ -104,7 +108,8 @@ fn main() { // reactor worker pool: shared by the plaintext acceptor and the direct-TLS // acceptor, so client I/O (framing + TLS crypto) spreads across cores. - let reactors = socketengine::spawn_reactors(tx.clone(), max_line, max_sendq, io_threads); + let reactors = + socketengine::spawn_reactors(tx.clone(), max_line, max_sendq, io_threads, handshake_timeout); // optional TLS listener (bind_tls + tls_cert + tls_key). A cert/bind problem // disables TLS but never takes the plaintext listener down. diff --git a/src/socketengine.rs b/src/socketengine.rs index f79b39f..4791258 100644 --- a/src/socketengine.rs +++ b/src/socketengine.rs @@ -22,7 +22,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::sync::Arc; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use mio::net::{TcpListener as MioListener, TcpStream as MioStream}; use mio::{Events, Interest, Poll, Token, Waker}; @@ -250,11 +250,12 @@ pub fn spawn_reactors( max_line: usize, max_sendq: usize, io_threads: usize, + handshake_timeout: Option, ) -> Vec { let workers = resolve_io_threads(io_threads); let mut reactors = Vec::with_capacity(workers); for _ in 0..workers { - match spawn_reactor(core.clone(), max_line, max_sendq) { + match spawn_reactor(core.clone(), max_line, max_sendq, handshake_timeout) { Ok(h) => reactors.push(h), Err(e) => eprintln!("reactor: cannot start a worker: {e}"), } @@ -354,6 +355,7 @@ fn spawn_reactor( core: Sender, max_line: usize, max_sendq: usize, + handshake_timeout: Option, ) -> io::Result { let poll = Poll::new()?; let waker = Arc::new(Waker::new(poll.registry(), WAKE)?); @@ -364,7 +366,17 @@ fn spawn_reactor( waker: waker.clone(), }; thread::spawn(move || { - reactor_loop(poll, waker, handoff_rx, out_tx, out_rx, core, max_line, max_sendq) + reactor_loop( + poll, + waker, + handoff_rx, + out_tx, + out_rx, + core, + max_line, + max_sendq, + handshake_timeout, + ) }); Ok(handle) } @@ -380,15 +392,40 @@ fn reactor_loop( core: Sender, max_line: usize, max_sendq: usize, + handshake_timeout: Option, ) { let mut conns: HashMap = HashMap::new(); let mut next_token = FIRST_CONN; let mut events = Events::with_capacity(1024); + // TLS conns still negotiating, with the deadline by which they must finish; a + // stalled handshake holds no uid so nothing else would ever reap it. + let mut pending_hs: Vec<(usize, Instant)> = Vec::new(); loop { - if poll.poll(&mut events, None).is_err() { + // block indefinitely when idle; while handshakes are pending, wake ~1s to reap + // any that blew their deadline (slow-loris on the TLS port). + let timeout = (!pending_hs.is_empty()).then(|| Duration::from_millis(1000)); + if poll.poll(&mut events, timeout).is_err() { continue; } + if !pending_hs.is_empty() { + let now = Instant::now(); + let mut expired = Vec::new(); + pending_hs.retain(|&(tok, dl)| match conns.get(&tok) { + Some(c) if c.handshaking => { + if now >= dl { + expired.push(tok); + false + } else { + true + } + } + _ => false, // handshake finished, or the conn is already gone + }); + for tok in expired { + close_conn(&mut poll, &mut conns, tok, &core); + } + } for event in events.iter() { match event.token() { WAKE => { @@ -445,6 +482,11 @@ fn reactor_loop( pending_out: Some(out), }, ); + if handshaking { + if let Some(d) = handshake_timeout { + pending_hs.push((token, Instant::now() + d)); + } + } // announce now only if nothing defers it: a TLS conn waits for // its handshake, a proxy conn for its header. if !a.via_proxy && !handshaking {