Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

ircd.rs / leveva / src / connstats.rs
14 kB 322 lines
1//! Per-connection traffic counters (P11 slice 145). 2//! 3//! Each accepted connection owns a [`ConnStats`]: monotonic byte/message counters for 4//! data sent to and received from the socket, plus the connection's birth instant. The 5//! struct is shared between two tasks via `Arc`: 6//! 7//! - the connection's own serve loop (`serve_client` in `main.rs`) is the **writer** — it 8//! bumps the counters at every socket read/write; and 9//! - any *other* connection running `STATS l` is a **reader** — it enumerates the registry 10//! ([`crate::registry::Registry::link_info`]) and snapshots each link's counters into a 11//! `211 RPL_STATSLINKINFO` line. 12//! 13//! The counters are plain [`AtomicU64`]s with `Relaxed` ordering: the reader only needs an 14//! eventually-consistent snapshot for a stats dump, never a happens-before relationship 15//! with the wire, so the cheapest ordering is correct. The struct is lock-free, so the 16//! pure render path ([`crate::command::stats`]) and the fuzz tests drive it directly. 17 18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 19 20/// The default SendQ ceiling (P11 slice 147) applied to a local client whose connection class 21/// does not set a `sendq` (the open-server / `sendq 0` case). 1 MiB — a generous client 22/// ceiling, above leveva's typical configured values. A SendQ ceiling is a safety mechanism 23/// (it bounds the unbounded mailbox under a non-draining peer), so unlike the slice-136 24/// per-class connection caps it is **always** on for a real client; only remote-user / test 25/// claims (which have no serve loop draining their mailbox) opt out with `sendq_max == 0`. 26pub const DEFAULT_SENDQ: u64 = 1_048_576; 27 28/// The default SendQ ceiling applied to a **server link** (P11 slice 148). 8 MiB — far above 29/// the client [`DEFAULT_SENDQ`] because a peer legitimately queues a whole network burst 30/// (every server + user + channel) on link-up, so a client-sized ceiling would kill a healthy 31/// burst (the `golden_s2s_mass_burst_drop` trap). It is the safety bound that keeps a 32/// non-draining peer's mailbox from growing without limit, not a per-connect tuning knob — 33/// resolving the matched `connect`/`class` block's `sendq` is a follow-on. Matches the shipped 34/// `class "servers" { sendq 8000000 }` in `dist/ircd.kdl`. 35pub const DEFAULT_SERVER_SENDQ: u64 = 8_000_000; 36 37/// The outcome of offering one frame to a connection's SendQ ([`ConnStats::sendq_admit`]). 38#[derive(Debug, Clone, Copy, PartialEq, Eq)] 39pub enum SendqVerdict { 40 /// Under the limit — the frame's length was added to the queue depth; send it. 41 Send, 42 /// This frame pushes the depth over `max`. The add was backed out and the connection is 43 /// now latched dead — the caller queues **one** terminal eject instead, then drops the 44 /// rest. Returned to exactly one producer (the latch is a `swap`). 45 Overflow, 46 /// The connection is already latched dead (the terminal eject is in flight) — drop the 47 /// frame silently, without accounting, so a wedged socket can't grow the mailbox. 48 Dropped, 49} 50 51/// Live, lock-free traffic counters for one connection. Created at connection birth (in 52/// [`crate::session::Session::new`]); a clone of the `Arc` is handed to the registry at 53/// registration finalize so `STATS l` can read it from another task. 54#[derive(Debug)] 55pub struct ConnStats { 56 sent_messages: AtomicU64, 57 sent_bytes: AtomicU64, 58 recv_messages: AtomicU64, 59 recv_bytes: AtomicU64, 60 /// Bytes currently queued on the connection's mailbox: incremented by the registry when a 61 /// producer pushes a frame, decremented by the serve loop when it drains one (P11 slice 62 /// 147). The live send-queue depth `STATS l` reports. 63 sendq: AtomicU64, 64 /// High-water mark of [`Self::sendq`] (informational; not wire-rendered). 65 sendq_peak: AtomicU64, 66 /// Latched once the SendQ overflows its ceiling, so exactly one terminal eject is queued 67 /// and every later frame is dropped. 68 sendq_dead: AtomicBool, 69 /// Connection birth, unix **nanoseconds** ([`crate::clock::unixnano`]). Seeded once and 70 /// never moved; rendered as "seconds open" in `211`. 71 connected_at: u64, 72} 73 74/// A `Copy` point-in-time view of a [`ConnStats`], taken under no lock. The numbers may be 75/// momentarily stale relative to the live socket — fine for a stats dump. 76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 77pub struct ConnStatsSnapshot { 78 pub sent_messages: u64, 79 pub sent_bytes: u64, 80 pub recv_messages: u64, 81 pub recv_bytes: u64, 82 /// Live send-queue depth in bytes (P11 slice 147) — what `STATS l`'s `<sendq>` reports. 83 pub sendq: u64, 84 pub connected_at: u64, 85} 86 87impl ConnStats { 88 /// A fresh counter set born at `connected_at` (unix-nanos). All four counters start at 89 /// zero. 90 pub fn new(connected_at: u64) -> Self { 91 Self { 92 sent_messages: AtomicU64::new(0), 93 sent_bytes: AtomicU64::new(0), 94 recv_messages: AtomicU64::new(0), 95 recv_bytes: AtomicU64::new(0), 96 sendq: AtomicU64::new(0), 97 sendq_peak: AtomicU64::new(0), 98 sendq_dead: AtomicBool::new(false), 99 connected_at, 100 } 101 } 102 103 /// Offer one `len`-byte frame to the send queue under ceiling `max` (P11 slice 147). 104 /// `max == 0` disables the kill (account-only — used for remote-user / test links that 105 /// have no serve loop draining their mailbox). See [`SendqVerdict`]. 106 pub fn sendq_admit(&self, len: u64, max: u64) -> SendqVerdict { 107 if self.sendq_dead.load(Ordering::Relaxed) { 108 return SendqVerdict::Dropped; 109 } 110 let new = self.sendq.fetch_add(len, Ordering::Relaxed) + len; 111 if max != 0 && new > max { 112 // Over the ceiling: back the add out and latch dead. `swap` makes exactly one 113 // racing producer observe the transition (it emits the terminal eject); any other 114 // simultaneously-overflowing producer sees the latch already set and drops. 115 self.sendq.fetch_sub(len, Ordering::Relaxed); 116 if self.sendq_dead.swap(true, Ordering::Relaxed) { 117 return SendqVerdict::Dropped; 118 } 119 return SendqVerdict::Overflow; 120 } 121 self.sendq_peak.fetch_max(new, Ordering::Relaxed); 122 SendqVerdict::Send 123 } 124 125 /// Account `len` bytes onto the queue **unconditionally**, bypassing the dead latch — for 126 /// the terminal eject itself and for control-plane frames (KILL / collision SAVE) that are 127 /// always delivered (P11 slice 147). 128 pub fn sendq_force_add(&self, len: u64) { 129 let new = self.sendq.fetch_add(len, Ordering::Relaxed) + len; 130 self.sendq_peak.fetch_max(new, Ordering::Relaxed); 131 } 132 133 /// The serve loop drained one `len`-byte frame off the mailbox; drop the queue depth. 134 /// Saturating, so any accounting imbalance can never wrap the unsigned counter. 135 pub fn sendq_drain(&self, len: u64) { 136 let mut cur = self.sendq.load(Ordering::Relaxed); 137 loop { 138 let next = cur.saturating_sub(len); 139 match self 140 .sendq 141 .compare_exchange_weak(cur, next, Ordering::Relaxed, Ordering::Relaxed) 142 { 143 Ok(_) => return, 144 Err(actual) => cur = actual, 145 } 146 } 147 } 148 149 /// The current send-queue depth in bytes. 150 pub fn sendq_len(&self) -> u64 { 151 self.sendq.load(Ordering::Relaxed) 152 } 153 154 /// Whether this connection has been latched dead by a SendQ overflow. 155 pub fn sendq_is_dead(&self) -> bool { 156 self.sendq_dead.load(Ordering::Relaxed) 157 } 158 159 /// Account a chunk written to the socket: `bytes.len()` bytes and 160 /// [`count_messages`]`(bytes)` messages. 161 pub fn record_sent(&self, bytes: &[u8]) { 162 self.sent_bytes 163 .fetch_add(bytes.len() as u64, Ordering::Relaxed); 164 self.sent_messages 165 .fetch_add(count_messages(bytes), Ordering::Relaxed); 166 } 167 168 /// Account a chunk read from the socket: `bytes.len()` bytes and 169 /// [`count_messages`]`(bytes)` messages. 170 pub fn record_recv(&self, bytes: &[u8]) { 171 self.recv_bytes 172 .fetch_add(bytes.len() as u64, Ordering::Relaxed); 173 self.recv_messages 174 .fetch_add(count_messages(bytes), Ordering::Relaxed); 175 } 176 177 /// Take a consistent-enough snapshot for a stats line. 178 pub fn snapshot(&self) -> ConnStatsSnapshot { 179 ConnStatsSnapshot { 180 sent_messages: self.sent_messages.load(Ordering::Relaxed), 181 sent_bytes: self.sent_bytes.load(Ordering::Relaxed), 182 recv_messages: self.recv_messages.load(Ordering::Relaxed), 183 recv_bytes: self.recv_bytes.load(Ordering::Relaxed), 184 sendq: self.sendq.load(Ordering::Relaxed), 185 connected_at: self.connected_at, 186 } 187 } 188} 189 190/// Count IRC messages in a wire chunk: the number of `\n` line terminators. A partial line 191/// with no terminator yet contributes nothing (it is counted when its `\n` arrives in a 192/// later chunk), matching how the framer completes a line. Well-formed leveva output is 193/// CRLF-terminated, so this equals the line count. 194pub fn count_messages(bytes: &[u8]) -> u64 { 195 bytes.iter().filter(|&&b| b == b'\n').count() as u64 196} 197 198#[cfg(test)] 199mod tests { 200 use super::*; 201 202 #[test] 203 fn fresh_stats_are_all_zero() { 204 let s = ConnStats::new(42); 205 let snap = s.snapshot(); 206 assert_eq!(snap.sent_messages, 0); 207 assert_eq!(snap.sent_bytes, 0); 208 assert_eq!(snap.recv_messages, 0); 209 assert_eq!(snap.recv_bytes, 0); 210 assert_eq!(snap.connected_at, 42); 211 } 212 213 #[test] 214 fn record_sent_accumulates_bytes_and_messages() { 215 let s = ConnStats::new(0); 216 s.record_sent(b"PING :x\r\n"); 217 s.record_sent(b"PONG :y\r\nNOTICE a :hi\r\n"); // two lines 218 let snap = s.snapshot(); 219 assert_eq!(snap.sent_bytes, 9 + 23); 220 assert_eq!(snap.sent_messages, 1 + 2); 221 // recv untouched — the two directions are independent. 222 assert_eq!(snap.recv_bytes, 0); 223 assert_eq!(snap.recv_messages, 0); 224 } 225 226 #[test] 227 fn record_recv_accumulates_independently() { 228 let s = ConnStats::new(0); 229 s.record_recv(b"NICK alice\r\n"); 230 let snap = s.snapshot(); 231 assert_eq!(snap.recv_bytes, 12); 232 assert_eq!(snap.recv_messages, 1); 233 assert_eq!(snap.sent_bytes, 0); 234 } 235 236 #[test] 237 fn sendq_admit_under_limit_accounts_and_sends() { 238 let s = ConnStats::new(0); 239 assert_eq!(s.sendq_admit(100, 1000), SendqVerdict::Send); 240 assert_eq!(s.sendq_admit(100, 1000), SendqVerdict::Send); 241 assert_eq!(s.sendq_len(), 200); 242 assert!(!s.sendq_is_dead()); 243 assert_eq!(s.snapshot().sendq, 200); 244 } 245 246 #[test] 247 fn sendq_admit_over_limit_overflows_latches_and_backs_out_the_add() { 248 let s = ConnStats::new(0); 249 assert_eq!(s.sendq_admit(800, 1000), SendqVerdict::Send); 250 // 800 + 300 = 1100 > 1000 → Overflow; the 300 is backed out (depth stays 800). 251 assert_eq!(s.sendq_admit(300, 1000), SendqVerdict::Overflow); 252 assert_eq!(s.sendq_len(), 800); 253 assert!(s.sendq_is_dead()); 254 // Once dead, every later frame is Dropped without accounting. 255 assert_eq!(s.sendq_admit(1, 1000), SendqVerdict::Dropped); 256 assert_eq!(s.sendq_len(), 800); 257 } 258 259 #[test] 260 fn sendq_admit_exactly_at_limit_is_send() { 261 let s = ConnStats::new(0); 262 // new == max is allowed (only `> max` overflows). 263 assert_eq!(s.sendq_admit(1000, 1000), SendqVerdict::Send); 264 assert!(!s.sendq_is_dead()); 265 assert_eq!(s.sendq_admit(1, 1000), SendqVerdict::Overflow); 266 } 267 268 #[test] 269 fn sendq_max_zero_never_overflows() { 270 let s = ConnStats::new(0); 271 for _ in 0..1000 { 272 assert_eq!(s.sendq_admit(10_000, 0), SendqVerdict::Send); 273 } 274 assert!(!s.sendq_is_dead()); 275 assert_eq!(s.sendq_len(), 10_000_000); 276 } 277 278 #[test] 279 fn sendq_drain_decrements_and_saturates() { 280 let s = ConnStats::new(0); 281 s.sendq_admit(500, 0); 282 s.sendq_drain(200); 283 assert_eq!(s.sendq_len(), 300); 284 // Inverse: draining more than is queued floors at zero (never wraps). 285 s.sendq_drain(10_000); 286 assert_eq!(s.sendq_len(), 0); 287 } 288 289 #[test] 290 fn sendq_force_add_bypasses_the_dead_latch() { 291 let s = ConnStats::new(0); 292 s.sendq_admit(1000, 1000); 293 s.sendq_admit(1, 1000); // Overflow → latched dead 294 assert!(s.sendq_is_dead()); 295 // A control-plane frame still accounts even past death. 296 s.sendq_force_add(50); 297 assert_eq!(s.sendq_len(), 1050); 298 } 299 300 #[test] 301 fn sendq_enqueue_drain_round_trips_to_zero() { 302 // Inverse invariant: a balanced enqueue/drain sequence returns depth to exactly 0. 303 let s = ConnStats::new(0); 304 for n in [10u64, 20, 30, 40] { 305 s.sendq_admit(n, 0); 306 } 307 for n in [10u64, 20, 30, 40] { 308 s.sendq_drain(n); 309 } 310 assert_eq!(s.sendq_len(), 0); 311 } 312 313 #[test] 314 fn count_messages_is_the_newline_count() { 315 assert_eq!(count_messages(b""), 0); 316 assert_eq!(count_messages(b"no terminator"), 0); // partial line, not yet a message 317 assert_eq!(count_messages(b"one\r\n"), 1); 318 assert_eq!(count_messages(b"a\r\nb\r\nc\r\n"), 3); 319 // a trailing partial after a complete line counts only the complete one 320 assert_eq!(count_messages(b"done\r\npartial"), 1); 321 } 322}