diff --git a/echoircd.conf.example b/echoircd.conf.example index b680fc7..b549d9e 100644 --- a/echoircd.conf.example +++ b/echoircd.conf.example @@ -5,7 +5,13 @@ servername = irc.example.net network = ExampleNet +# Listener addresses. `bind`, `bind_tls` and `bind_server` are all REPEATABLE — add a +# line per address/port. A bare `[::]` binds IPv4 and IPv6 at once (dual-stack), and an +# IPv4 client on it is normalized back to its real v4 address (not ::ffff:...). So a +# single `[::]` line serves both families; use explicit lines to pin specific IPs/ports. bind = 0.0.0.0:6667 +# bind = [::]:6667 # dual-stack (IPv4 + IPv6) on one line +# bind = 0.0.0.0:6668 # an extra plaintext port # TLS listener. Generate a cert/key first, e.g.: # openssl req -x509 -newkey rsa:2048 -keyout tls/key.pem -out tls/cert.pem \ diff --git a/src/config.rs b/src/config.rs index bd351cf..e5aa2b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,8 +65,8 @@ impl Default for AntiMixedCfg { pub struct Config { pub servername: String, pub network: String, - pub bind: String, - pub bind_tls: Option, // e.g. 0.0.0.0:6697 — the TLS listener + pub bind: Vec, // plaintext client listeners (repeatable; e.g. [::]:6667) + pub bind_tls: Vec, // TLS client listeners (repeatable; e.g. [::]:6697) pub tls_cert: Option, // PEM certificate chain pub tls_key: Option, // PEM private key pub motd: Vec, @@ -74,7 +74,7 @@ pub struct Config { pub cloak_key: Option, // secret key for host cloaking (+x); None = off pub sid: String, // this server's 3-char server id (S2S) pub serverdesc: String, // this server's description - pub bind_server: Option, // the server-to-server link listener + pub bind_server: Vec, // server-to-server link listeners (repeatable) pub links: Vec, // peers we accept / dial pub conf_path: String, // where this was loaded from (for REHASH) pub censor: Vec<(String, String)>, // +G bad words: (find, replace); empty replace = block @@ -96,8 +96,8 @@ impl Default for Config { Config { servername: "echo.local".to_string(), network: "echoNet".to_string(), - bind: "127.0.0.1:6767".to_string(), - bind_tls: None, + bind: Vec::new(), + bind_tls: Vec::new(), tls_cert: None, tls_key: None, motd: Vec::new(), @@ -105,7 +105,7 @@ impl Default for Config { cloak_key: None, sid: "0AA".to_string(), serverdesc: "echoIRCd server".to_string(), - bind_server: None, + bind_server: Vec::new(), links: Vec::new(), conf_path: "echoircd.conf".to_string(), censor: Vec::new(), @@ -167,14 +167,14 @@ impl Config { match k { "servername" | "server" => c.servername = v.to_string(), "network" => c.network = v.to_string(), - "bind" => c.bind = v.to_string(), - "bind_tls" => c.bind_tls = Some(v.to_string()), + "bind" => c.bind.push(v.to_string()), + "bind_tls" => c.bind_tls.push(v.to_string()), "tls_cert" => c.tls_cert = Some(v.to_string()), "tls_key" => c.tls_key = Some(v.to_string()), "cloak_key" => c.cloak_key = Some(v.to_string()), "sid" => c.sid = v.to_string(), "serverdesc" | "description" => c.serverdesc = v.to_string(), - "bind_server" => c.bind_server = Some(v.to_string()), + "bind_server" => c.bind_server.push(v.to_string()), "link" => { // link = [autoconnect] let t: Vec<&str> = v.split_whitespace().collect(); diff --git a/src/main.rs b/src/main.rs index 7286bba..d2da6b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,29 +24,37 @@ fn main() { // precompute the bcrypt constants off-thread so the first hash never stalls the core thread::spawn(echoircd::bcrypt::warm); - // Client plaintext connections run on the mio reactor, so bind a mio listener - // (fail fast if the main port is taken). - let bind_addr: std::net::SocketAddr = match cfg.bind.parse() { - Ok(a) => a, - Err(e) => { - eprintln!("echoircd: bad bind address {}: {e}", cfg.bind); - std::process::exit(1); - } - }; - let client_listener = match mio::net::TcpListener::bind(bind_addr) { - Ok(l) => l, - Err(e) => { - eprintln!("echoircd: cannot bind {}: {e}", cfg.bind); - std::process::exit(1); - } - }; eprintln!( - "echoircd {} on {} (network {}, server {})", + "echoircd {} (network {}, server {})", env!("CARGO_PKG_VERSION"), - cfg.bind, cfg.network, cfg.servername ); + // Plaintext client listeners. `bind` is repeatable; a bare `[::]` binds IPv4+IPv6 + // (dual-stack). Bind each as a mio listener and exit only if none could bind, so an + // unavailable family (e.g. no IPv6) degrades gracefully instead of taking us down. + let plaintext_binds: Vec = if cfg.bind.is_empty() { + vec!["127.0.0.1:6767".to_string()] + } else { + cfg.bind.clone() + }; + let mut client_listeners: Vec = Vec::new(); + for b in &plaintext_binds { + match b.parse::() { + Ok(a) => match mio::net::TcpListener::bind(a) { + Ok(l) => { + eprintln!("echoircd plaintext on {b}"); + client_listeners.push(l); + } + Err(e) => eprintln!("echoircd: cannot bind {b}: {e}"), + }, + Err(e) => eprintln!("echoircd: bad bind address {b}: {e}"), + } + } + if client_listeners.is_empty() { + eprintln!("echoircd: no plaintext listener could bind; exiting"); + std::process::exit(1); + } // global queue limits (per-class overrides layer on top of these in the reactor) let raw_num = |k: &str, d: usize| { @@ -114,41 +122,49 @@ fn main() { 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. - if let (Some(bind_tls), Some(cert), Some(key)) = (&cfg.bind_tls, &cfg.tls_cert, &cfg.tls_key) { - match OpensslBackend::new(cert, key) { - Ok(backend) => match TcpListener::bind(bind_tls) { - Ok(tls_listener) => { - eprintln!("echoircd TLS on {bind_tls} (openssl)"); + // optional TLS listeners (bind_tls, repeatable + tls_cert + tls_key). A cert/bind + // problem disables TLS but never takes the plaintext listeners down. + if !cfg.bind_tls.is_empty() { + match (&cfg.tls_cert, &cfg.tls_key) { + (Some(cert), Some(key)) => match OpensslBackend::new(cert, key) { + Ok(backend) => { let backend: Arc = Arc::new(backend); - let tls_tx = tx.clone(); - let tls_counter = counter.clone(); - let tls_proxy_trust = proxy_trust.clone(); - let tls_reactors = reactors.clone(); - let tls_limiter = accept_limiter.clone(); - thread::spawn(move || { - socketengine::accept_loop( - tls_listener, - tls_tx, - Some(backend), - tls_counter, - false, - max_line, - tls_proxy_trust, - tls_reactors, - tls_limiter, - ) - }); + for bind_tls in &cfg.bind_tls { + match TcpListener::bind(bind_tls) { + Ok(tls_listener) => { + eprintln!("echoircd TLS on {bind_tls} (openssl)"); + let tls_tx = tx.clone(); + let tls_counter = counter.clone(); + let tls_proxy_trust = proxy_trust.clone(); + let tls_reactors = reactors.clone(); + let tls_limiter = accept_limiter.clone(); + let backend = backend.clone(); + thread::spawn(move || { + socketengine::accept_loop( + tls_listener, + tls_tx, + Some(backend), + tls_counter, + false, + max_line, + tls_proxy_trust, + tls_reactors, + tls_limiter, + ) + }); + } + Err(e) => eprintln!("echoircd: cannot bind TLS {bind_tls}: {e}"), + } + } } - Err(e) => eprintln!("echoircd: cannot bind TLS {bind_tls}: {e}"), + Err(e) => eprintln!("echoircd: TLS disabled (cert/key error): {e}"), }, - Err(e) => eprintln!("echoircd: TLS disabled (cert/key error): {e}"), + _ => eprintln!("echoircd: bind_tls set but tls_cert/tls_key missing; TLS disabled"), } } - // server-to-server link listener (see crate::link) - if let Some(bind_srv) = &cfg.bind_server { + // server-to-server link listeners (bind_server, repeatable — see crate::link) + for bind_srv in &cfg.bind_server { match TcpListener::bind(bind_srv) { Ok(sl) => { eprintln!("echoircd S2S link listener on {bind_srv} (sid {})", cfg.sid); @@ -190,9 +206,16 @@ fn main() { }); } - // client plaintext connections: the acceptor round-robins them across the pool - thread::spawn(move || { - socketengine::run_acceptor(client_listener, reactors, counter, proxy_trust, accept_limiter) - }); + // client plaintext connections: one acceptor per listener, all round-robining + // onto the shared reactor pool + for listener in client_listeners { + let (r, c, pt, lim) = ( + reactors.clone(), + counter.clone(), + proxy_trust.clone(), + accept_limiter.clone(), + ); + thread::spawn(move || socketengine::run_acceptor(listener, r, c, pt, lim)); + } let _ = core.join(); } diff --git a/src/socketengine.rs b/src/socketengine.rs index b42cfff..2f5e9ab 100644 --- a/src/socketengine.rs +++ b/src/socketengine.rs @@ -40,6 +40,18 @@ pub const DEFAULT_MAX_SENDQ: usize = 1 << 20; // 1 MiB /// How long a TLS thread blocks on a read before draining its write queue. const TLS_POLL: Duration = Duration::from_millis(100); +/// Collapse an IPv4-mapped IPv6 peer address (`::ffff:1.2.3.4`, which is how an IPv4 +/// client shows up on a dual-stack `[::]` listener) back to a plain IPv4 `SocketAddr`, +/// so cloaking, bans, GeoIP, DNSBL and host display all see the real IPv4 address. +fn normalize_addr(a: SocketAddr) -> SocketAddr { + if let SocketAddr::V6(v6) = a { + if let Some(v4) = v6.ip().to_ipv4_mapped() { + return SocketAddr::new(IpAddr::V4(v4), a.port()); + } + } + a +} + /// A queued output action the core hands the reactor: a line to write to a /// connection, a request to flush-then-close it (sent when the core drops the /// [`OutSink`], e.g. on quit), or a per-connection queue-limit override (from the @@ -385,9 +397,11 @@ pub fn run_acceptor( loop { match listener.accept() { Ok((stream, _addr)) => { - let addr = stream - .peer_addr() - .unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap()); + let addr = normalize_addr( + stream + .peer_addr() + .unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap()), + ); // a connection from a trusted proxy leads with a PROXY header; // the worker holds its Connect until that header is consumed so // the core sees the real client IP. @@ -932,6 +946,7 @@ pub fn accept_loop( let Ok(addr) = stream.peer_addr() else { continue; }; + let addr = normalize_addr(addr); // rate-limit direct client connections at the edge (not S2S links, not proxied) if !link { let via_proxy = proxy_trust