listeners: make bind/bind_tls/bind_server repeatable (multiple addresses/ports) and normalize IPv4-mapped IPv6 peers back to plain IPv4 — a single [::] bind now serves IPv4+IPv6 with clean v4 addresses
This commit is contained in:
parent
08ed18bb96
commit
0ace39c882
4 changed files with 107 additions and 63 deletions
|
|
@ -5,7 +5,13 @@
|
||||||
|
|
||||||
servername = irc.example.net
|
servername = irc.example.net
|
||||||
network = ExampleNet
|
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 = 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.:
|
# TLS listener. Generate a cert/key first, e.g.:
|
||||||
# openssl req -x509 -newkey rsa:2048 -keyout tls/key.pem -out tls/cert.pem \
|
# openssl req -x509 -newkey rsa:2048 -keyout tls/key.pem -out tls/cert.pem \
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,8 @@ impl Default for AntiMixedCfg {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub servername: String,
|
pub servername: String,
|
||||||
pub network: String,
|
pub network: String,
|
||||||
pub bind: String,
|
pub bind: Vec<String>, // plaintext client listeners (repeatable; e.g. [::]:6667)
|
||||||
pub bind_tls: Option<String>, // e.g. 0.0.0.0:6697 — the TLS listener
|
pub bind_tls: Vec<String>, // TLS client listeners (repeatable; e.g. [::]:6697)
|
||||||
pub tls_cert: Option<String>, // PEM certificate chain
|
pub tls_cert: Option<String>, // PEM certificate chain
|
||||||
pub tls_key: Option<String>, // PEM private key
|
pub tls_key: Option<String>, // PEM private key
|
||||||
pub motd: Vec<String>,
|
pub motd: Vec<String>,
|
||||||
|
|
@ -74,7 +74,7 @@ pub struct Config {
|
||||||
pub cloak_key: Option<String>, // secret key for host cloaking (+x); None = off
|
pub cloak_key: Option<String>, // secret key for host cloaking (+x); None = off
|
||||||
pub sid: String, // this server's 3-char server id (S2S)
|
pub sid: String, // this server's 3-char server id (S2S)
|
||||||
pub serverdesc: String, // this server's description
|
pub serverdesc: String, // this server's description
|
||||||
pub bind_server: Option<String>, // the server-to-server link listener
|
pub bind_server: Vec<String>, // server-to-server link listeners (repeatable)
|
||||||
pub links: Vec<LinkBlock>, // peers we accept / dial
|
pub links: Vec<LinkBlock>, // peers we accept / dial
|
||||||
pub conf_path: String, // where this was loaded from (for REHASH)
|
pub conf_path: String, // where this was loaded from (for REHASH)
|
||||||
pub censor: Vec<(String, String)>, // +G bad words: (find, replace); empty replace = block
|
pub censor: Vec<(String, String)>, // +G bad words: (find, replace); empty replace = block
|
||||||
|
|
@ -96,8 +96,8 @@ impl Default for Config {
|
||||||
Config {
|
Config {
|
||||||
servername: "echo.local".to_string(),
|
servername: "echo.local".to_string(),
|
||||||
network: "echoNet".to_string(),
|
network: "echoNet".to_string(),
|
||||||
bind: "127.0.0.1:6767".to_string(),
|
bind: Vec::new(),
|
||||||
bind_tls: None,
|
bind_tls: Vec::new(),
|
||||||
tls_cert: None,
|
tls_cert: None,
|
||||||
tls_key: None,
|
tls_key: None,
|
||||||
motd: Vec::new(),
|
motd: Vec::new(),
|
||||||
|
|
@ -105,7 +105,7 @@ impl Default for Config {
|
||||||
cloak_key: None,
|
cloak_key: None,
|
||||||
sid: "0AA".to_string(),
|
sid: "0AA".to_string(),
|
||||||
serverdesc: "echoIRCd server".to_string(),
|
serverdesc: "echoIRCd server".to_string(),
|
||||||
bind_server: None,
|
bind_server: Vec::new(),
|
||||||
links: Vec::new(),
|
links: Vec::new(),
|
||||||
conf_path: "echoircd.conf".to_string(),
|
conf_path: "echoircd.conf".to_string(),
|
||||||
censor: Vec::new(),
|
censor: Vec::new(),
|
||||||
|
|
@ -167,14 +167,14 @@ impl Config {
|
||||||
match k {
|
match k {
|
||||||
"servername" | "server" => c.servername = v.to_string(),
|
"servername" | "server" => c.servername = v.to_string(),
|
||||||
"network" => c.network = v.to_string(),
|
"network" => c.network = v.to_string(),
|
||||||
"bind" => c.bind = v.to_string(),
|
"bind" => c.bind.push(v.to_string()),
|
||||||
"bind_tls" => c.bind_tls = Some(v.to_string()),
|
"bind_tls" => c.bind_tls.push(v.to_string()),
|
||||||
"tls_cert" => c.tls_cert = Some(v.to_string()),
|
"tls_cert" => c.tls_cert = Some(v.to_string()),
|
||||||
"tls_key" => c.tls_key = Some(v.to_string()),
|
"tls_key" => c.tls_key = Some(v.to_string()),
|
||||||
"cloak_key" => c.cloak_key = Some(v.to_string()),
|
"cloak_key" => c.cloak_key = Some(v.to_string()),
|
||||||
"sid" => c.sid = v.to_string(),
|
"sid" => c.sid = v.to_string(),
|
||||||
"serverdesc" | "description" => c.serverdesc = 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" => {
|
||||||
// link = <name> <ip> <port> <password> [autoconnect]
|
// link = <name> <ip> <port> <password> [autoconnect]
|
||||||
let t: Vec<&str> = v.split_whitespace().collect();
|
let t: Vec<&str> = v.split_whitespace().collect();
|
||||||
|
|
|
||||||
125
src/main.rs
125
src/main.rs
|
|
@ -24,29 +24,37 @@ fn main() {
|
||||||
// precompute the bcrypt constants off-thread so the first hash never stalls the core
|
// precompute the bcrypt constants off-thread so the first hash never stalls the core
|
||||||
thread::spawn(echoircd::bcrypt::warm);
|
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!(
|
eprintln!(
|
||||||
"echoircd {} on {} (network {}, server {})",
|
"echoircd {} (network {}, server {})",
|
||||||
env!("CARGO_PKG_VERSION"),
|
env!("CARGO_PKG_VERSION"),
|
||||||
cfg.bind,
|
|
||||||
cfg.network,
|
cfg.network,
|
||||||
cfg.servername
|
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<String> = if cfg.bind.is_empty() {
|
||||||
|
vec!["127.0.0.1:6767".to_string()]
|
||||||
|
} else {
|
||||||
|
cfg.bind.clone()
|
||||||
|
};
|
||||||
|
let mut client_listeners: Vec<mio::net::TcpListener> = Vec::new();
|
||||||
|
for b in &plaintext_binds {
|
||||||
|
match b.parse::<std::net::SocketAddr>() {
|
||||||
|
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)
|
// global queue limits (per-class overrides layer on top of these in the reactor)
|
||||||
let raw_num = |k: &str, d: usize| {
|
let raw_num = |k: &str, d: usize| {
|
||||||
|
|
@ -114,41 +122,49 @@ fn main() {
|
||||||
let reactors =
|
let reactors =
|
||||||
socketengine::spawn_reactors(tx.clone(), max_line, max_sendq, io_threads, handshake_timeout);
|
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
|
// optional TLS listeners (bind_tls, repeatable + tls_cert + tls_key). A cert/bind
|
||||||
// disables TLS but never takes the plaintext listener down.
|
// problem disables TLS but never takes the plaintext listeners down.
|
||||||
if let (Some(bind_tls), Some(cert), Some(key)) = (&cfg.bind_tls, &cfg.tls_cert, &cfg.tls_key) {
|
if !cfg.bind_tls.is_empty() {
|
||||||
match OpensslBackend::new(cert, key) {
|
match (&cfg.tls_cert, &cfg.tls_key) {
|
||||||
Ok(backend) => match TcpListener::bind(bind_tls) {
|
(Some(cert), Some(key)) => match OpensslBackend::new(cert, key) {
|
||||||
Ok(tls_listener) => {
|
Ok(backend) => {
|
||||||
eprintln!("echoircd TLS on {bind_tls} (openssl)");
|
|
||||||
let backend: Arc<dyn TlsBackend> = Arc::new(backend);
|
let backend: Arc<dyn TlsBackend> = Arc::new(backend);
|
||||||
let tls_tx = tx.clone();
|
for bind_tls in &cfg.bind_tls {
|
||||||
let tls_counter = counter.clone();
|
match TcpListener::bind(bind_tls) {
|
||||||
let tls_proxy_trust = proxy_trust.clone();
|
Ok(tls_listener) => {
|
||||||
let tls_reactors = reactors.clone();
|
eprintln!("echoircd TLS on {bind_tls} (openssl)");
|
||||||
let tls_limiter = accept_limiter.clone();
|
let tls_tx = tx.clone();
|
||||||
thread::spawn(move || {
|
let tls_counter = counter.clone();
|
||||||
socketengine::accept_loop(
|
let tls_proxy_trust = proxy_trust.clone();
|
||||||
tls_listener,
|
let tls_reactors = reactors.clone();
|
||||||
tls_tx,
|
let tls_limiter = accept_limiter.clone();
|
||||||
Some(backend),
|
let backend = backend.clone();
|
||||||
tls_counter,
|
thread::spawn(move || {
|
||||||
false,
|
socketengine::accept_loop(
|
||||||
max_line,
|
tls_listener,
|
||||||
tls_proxy_trust,
|
tls_tx,
|
||||||
tls_reactors,
|
Some(backend),
|
||||||
tls_limiter,
|
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)
|
// server-to-server link listeners (bind_server, repeatable — see crate::link)
|
||||||
if let Some(bind_srv) = &cfg.bind_server {
|
for bind_srv in &cfg.bind_server {
|
||||||
match TcpListener::bind(bind_srv) {
|
match TcpListener::bind(bind_srv) {
|
||||||
Ok(sl) => {
|
Ok(sl) => {
|
||||||
eprintln!("echoircd S2S link listener on {bind_srv} (sid {})", cfg.sid);
|
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
|
// client plaintext connections: one acceptor per listener, all round-robining
|
||||||
thread::spawn(move || {
|
// onto the shared reactor pool
|
||||||
socketengine::run_acceptor(client_listener, reactors, counter, proxy_trust, accept_limiter)
|
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();
|
let _ = core.join();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
/// How long a TLS thread blocks on a read before draining its write queue.
|
||||||
const TLS_POLL: Duration = Duration::from_millis(100);
|
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
|
/// 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
|
/// 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
|
/// [`OutSink`], e.g. on quit), or a per-connection queue-limit override (from the
|
||||||
|
|
@ -385,9 +397,11 @@ pub fn run_acceptor(
|
||||||
loop {
|
loop {
|
||||||
match listener.accept() {
|
match listener.accept() {
|
||||||
Ok((stream, _addr)) => {
|
Ok((stream, _addr)) => {
|
||||||
let addr = stream
|
let addr = normalize_addr(
|
||||||
.peer_addr()
|
stream
|
||||||
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap());
|
.peer_addr()
|
||||||
|
.unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap()),
|
||||||
|
);
|
||||||
// a connection from a trusted proxy leads with a PROXY header;
|
// a connection from a trusted proxy leads with a PROXY header;
|
||||||
// the worker holds its Connect until that header is consumed so
|
// the worker holds its Connect until that header is consumed so
|
||||||
// the core sees the real client IP.
|
// the core sees the real client IP.
|
||||||
|
|
@ -932,6 +946,7 @@ pub fn accept_loop(
|
||||||
let Ok(addr) = stream.peer_addr() else {
|
let Ok(addr) = stream.peer_addr() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let addr = normalize_addr(addr);
|
||||||
// rate-limit direct client connections at the edge (not S2S links, not proxied)
|
// rate-limit direct client connections at the edge (not S2S links, not proxied)
|
||||||
if !link {
|
if !link {
|
||||||
let via_proxy = proxy_trust
|
let via_proxy = proxy_trust
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue