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.

feat(leveva): user mode +C no-CTCP (P11 slice 306)

Port charybdis extensions/umode_noctcp.c to a leveva user mode +C: a
self-settable umode that refuses CTCP queries (other than ACTION) sent to the
user as a PRIVMSG, replying 531 ERR_CANNOTSENDTOUSER :+C set. NOTICE, CTCP
replies, and ACTION always pass. The recipient-side private-message counterpart
of the channel +C (slice 220), reusing the fuzzed channel::is_blocked_ctcp.

Pure seams mode::is_noctcp + mode::noctcp_user_blocks (the composite gate
decision), enforced home-server-local on both the local command/message.rs path
and the inbound-S2S s2s/relay::inbound_message path (new noctcp_reject_remote
routes 531 to the sender's uplink, the +R 248->249 split precedent). The bit
still propagates (slice-254 "no local-only umodes"), so WHOIS shows +C. Bit
0x80000, numeric 531; umode_diff/RENDERED/004-literal/snapshot wiring per the
user-mode checklist.

Tests: mode seam units, command self-settable + inverse, message gate +
inverses, s2s inbound + inverses, golden_noctcp_umode, noctcp_umode_proptest
(5 props). 5 welcome snapshots regenerated (004 gains C).

+504 -16
+7
leveva/help/USER_MODES.md
··· 14 14 | `+l` | locops | operators | Receive `LOCOPS` broadcasts (this server's operators only). Only operators may set it. | 15 15 | `+s` | snotices | operators | Receive server notices, filtered by a category mask (see below). Operators only. | 16 16 | `+B` | bot | you | Marks you as a bot (IRCv3): `WHOIS` adds numeric 335, `WHO` adds the `B` flag. | 17 + | `+C` | no-CTCP | you | Refuse CTCP queries (except `ACTION`) sent to you as a PRIVMSG; sender gets 531. | 17 18 | `+o` | operator | server (`OPER`) | Global IRC operator. | 18 19 | `+O` | local op | server (`OPER`) | Local-only IRC operator. | 19 20 | `+r` | restricted | server | Restricted connection: cannot op or change nick. | ··· 38 39 The bot flag (`+B`) is advertised in `ISUPPORT` as `BOT=B`, which is also the 39 40 flag character used in `WHO` replies. Messages you send while marked `+B` carry a 40 41 `bot` IRCv3 message tag, delivered only to clients that negotiated `message-tags`. 42 + 43 + The no-CTCP flag (`+C`) makes the server refuse CTCP queries addressed to you as 44 + a `PRIVMSG` — a `VERSION`, `PING`, `TIME`, `DCC`, and so on. The sender is told 45 + `531 ERR_CANNOTSENDTOUSER :+C set` and the query is never delivered. An `ACTION` 46 + (`/me`) is **not** a query and always passes, as do CTCP replies (which travel as 47 + `NOTICE`) and ordinary messages. Use it to stop CTCP scanners and DCC offers. 41 48 42 49 The server-notice flag (`+s`, operators only) carries a **snomask** — a set of 43 50 category letters selecting which notices you receive. `MODE <yournick> +s` with no
+75
leveva/src/command/message.rs
··· 530 530 } 531 531 continue; 532 532 } 533 + // No-CTCP (slice 306): a local target in `+C` refuses a CTCP query (other than 534 + // `ACTION`) addressed as a PRIVMSG — `531 ERR_CANNOTSENDTOUSER`. NOTICE 535 + // (`errs == false`), CTCP replies, and `ACTION` always pass. Self-messages are 536 + // exempt (handled by the `uid != client.uid` guard, matching the gates above). 537 + if uid != client.uid 538 + && crate::mode::noctcp_user_blocks(ctx.registry.modes_of(&uid), errs, text) 539 + { 540 + let tnick = ctx 541 + .registry 542 + .nick_of(&uid) 543 + .unwrap_or_else(|| target.to_string()); 544 + replies.push( 545 + Message::builder(Numeric::ErrCannotSendToUser) 546 + .prefix(&ctx.name) 547 + .param(&client.nick) 548 + .param(&tnick) 549 + .trailing("+C set") 550 + .build(), 551 + ); 552 + continue; 553 + } 533 554 ctx.registry 534 555 .deliver_uid(&uid, render(ctx.registry.caps_of(&uid))); 535 556 // PRIVMSG (never NOTICE, RFC 1459 §4.4.2) to an away user → `301 RPL_AWAY` ··· 1121 1142 let r = run(&ctx, "PRIVMSG bob :hi"); 1122 1143 assert!(r.is_empty()); 1123 1144 assert_eq!(delivered(&mut bob).command(), "PRIVMSG"); 1145 + } 1146 + 1147 + /// Set the `+C` no-CTCP **user** bit on a claimed target's registry record (distinct from the 1148 + /// channel `+C` helper `set_noctcp` above). 1149 + fn set_user_noctcp(ctx: &ServerContext, nick: &str) { 1150 + ctx.registry 1151 + .set_modes(&uid_for(nick), crate::UserMode::NoCtcp.bit()); 1152 + } 1153 + 1154 + #[test] 1155 + fn noctcp_umode_blocks_ctcp_privmsg_with_531() { 1156 + let ctx = ctx(); 1157 + let mut bob = claim_observed(&ctx, "bob"); 1158 + set_user_noctcp(&ctx, "bob"); 1159 + let r = run(&ctx, "PRIVMSG bob :\u{1}VERSION\u{1}"); 1160 + assert_eq!(codes(&r), &["531"]); 1161 + assert_eq!(r[0].params(), &["alice", "bob"]); 1162 + assert_eq!(r[0].trailing(), Some("+C set")); 1163 + assert!( 1164 + bob.try_recv().is_err(), 1165 + "a +C target receives no blocked CTCP" 1166 + ); 1167 + } 1168 + 1169 + #[test] 1170 + fn noctcp_umode_lets_action_and_plain_text_through() { 1171 + // Inverse: ACTION and ordinary text are never blocked by +C. 1172 + let ctx = ctx(); 1173 + let mut bob = claim_observed(&ctx, "bob"); 1174 + set_user_noctcp(&ctx, "bob"); 1175 + assert!(run(&ctx, "PRIVMSG bob :\u{1}ACTION waves\u{1}").is_empty()); 1176 + assert_eq!(delivered(&mut bob).trailing(), Some("\u{1}ACTION waves\u{1}")); 1177 + assert!(run(&ctx, "PRIVMSG bob :hello").is_empty()); 1178 + assert_eq!(delivered(&mut bob).trailing(), Some("hello")); 1179 + } 1180 + 1181 + #[test] 1182 + fn noctcp_umode_lets_notice_ctcp_through() { 1183 + // Inverse: a CTCP *reply* (NOTICE) is never blocked — charybdis returns early for NOTICE. 1184 + let ctx = ctx(); 1185 + let mut bob = claim_observed(&ctx, "bob"); 1186 + set_user_noctcp(&ctx, "bob"); 1187 + assert!(run(&ctx, "NOTICE bob :\u{1}VERSION reply\u{1}").is_empty()); 1188 + assert_eq!(delivered(&mut bob).trailing(), Some("\u{1}VERSION reply\u{1}")); 1189 + } 1190 + 1191 + #[test] 1192 + fn noctcp_plain_target_receives_ctcp() { 1193 + // Inverse: a target *without* +C receives a CTCP normally (the gate is not over-broad). 1194 + let ctx = ctx(); 1195 + let mut bob = claim_observed(&ctx, "bob"); 1196 + let r = run(&ctx, "PRIVMSG bob :\u{1}PING 1\u{1}"); 1197 + assert!(r.is_empty()); 1198 + assert_eq!(delivered(&mut bob).trailing(), Some("\u{1}PING 1\u{1}")); 1124 1199 } 1125 1200 1126 1201 #[test]
+28 -7
leveva/src/command/mode.rs
··· 781 781 } 782 782 } 783 783 } 784 - 'i' | 'w' | 'B' | 'x' | 'g' | 'G' | 'D' | 'R' | 'Q' => { 784 + 'i' | 'w' | 'B' | 'x' | 'g' | 'G' | 'D' | 'R' | 'Q' | 'C' => { 785 785 // `B` (IRCv3 bot mode), `x` (IP cloak), `g`/`G` (strict/soft caller-id, 786 786 // slices 216/218), `D` (deaf, slice 247), `R` (registered-only messages, 787 - // slice 248) and `Q` (no-forward, slice 250) are leveva-native but, unlike 788 - // `W`, a client *may* set/clear them on itself (`MODE <nick> 789 - // +B`/`+x`/`+g`/`+G`/`+D`/`+R`/`+Q`). The `+x` host change itself is applied 790 - // after the loop (it needs the net old→new diff, not the per-char step); the 791 - // caller-id notify-state reset is likewise applied after. 787 + // slice 248), `Q` (no-forward, slice 250) and `C` (no-CTCP, slice 306) are 788 + // leveva-native but, unlike `W`, a client *may* set/clear them on itself 789 + // (`MODE <nick> +B`/`+x`/`+g`/`+G`/`+D`/`+R`/`+Q`/`+C`). The `+x` host change 790 + // itself is applied after the loop (it needs the net old→new diff, not the 791 + // per-char step); the caller-id notify-state reset is likewise applied after. 792 792 let bit = UserMode::from_char(c) 793 - .expect("i/w/B/x/g/G/D/R/Q are user modes") 793 + .expect("i/w/B/x/g/G/D/R/Q/C are user modes") 794 794 .bit(); 795 795 if adding { 796 796 modes |= bit; ··· 969 969 UserMode::NoForward, 970 970 UserMode::Operwall, 971 971 UserMode::LocOps, 972 + UserMode::NoCtcp, 972 973 ] { 973 974 let was = old & m.bit() != 0; 974 975 let now = new & m.bit() != 0; ··· 1092 1093 assert_eq!(ctx.registry.get("alice").unwrap().modes, 0); 1093 1094 } 1094 1095 1096 + #[test] 1097 + fn plus_c_noctcp_is_self_settable_and_clears() { 1098 + let ctx = ctx(); 1099 + let mut c = setup(&ctx); // a plain non-oper user 1100 + let r = dispatch(&mut c, &Message::parse("MODE alice +C").unwrap(), &ctx); 1101 + assert_eq!(codes(&r), &["MODE"], "no 481/501 — any user may set +C"); 1102 + assert_eq!(r[0].trailing(), Some("+C"), "the +C set echoes (umode_diff)"); 1103 + assert_eq!(c.modes, UserMode::NoCtcp.bit()); 1104 + assert_eq!( 1105 + ctx.registry.get("alice").unwrap().modes, 1106 + UserMode::NoCtcp.bit() 1107 + ); 1108 + // Inverse: -C clears it back to empty and the registry mirrors that. 1109 + let r = dispatch(&mut c, &Message::parse("MODE alice -C").unwrap(), &ctx); 1110 + assert_eq!(r[0].trailing(), Some("-C")); 1111 + assert_eq!(c.modes, 0); 1112 + assert_eq!(ctx.registry.get("alice").unwrap().modes, 0); 1113 + } 1114 + 1095 1115 /// Slice 251 — the deferred `+G` echo fix. `+G` (soft caller-id, slice 218) is 1096 1116 /// client-settable (it is in the self-settable letter group), so a bare `±G` must echo 1097 1117 /// `MODE :+G` / `MODE :-G` exactly like `+g`. Before the fix `umode_diff` omitted ··· 1174 1194 UserMode::NoForward, 1175 1195 UserMode::Operwall, 1176 1196 UserMode::LocOps, 1197 + UserMode::NoCtcp, 1177 1198 ]; 1178 1199 1179 1200 /// Exhaustive regression for the slice-251 omission: **every** rendered umode, toggled alone,
+1 -1
leveva/src/isupport.rs
··· 203 203 204 204 #[test] 205 205 fn mode_strings_come_from_tables() { 206 - assert_eq!(user_modes_string(), "oOiwraWBxsgGZDRQzSl"); 206 + assert_eq!(user_modes_string(), "oOiwraWBxsgGZDRQzSlC"); 207 207 // ChanMode::ALL order: p s m n t i O C c S z T g o v q a h Y b e I R k l j (C=no-CTCP slice 208 208 // 220, c=no-color slice 232, S=TLS-only slice 233, z=op-moderation slice 235, 209 209 // j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240,
+89 -1
leveva/src/mode.rs
··· 202 202 /// local-only modes), so a remote `WHOIS` shows `+l`; only the LOCOPS *delivery* is local-server. 203 203 /// Appended **last** so all bits above are untouched. 204 204 LocOps, 205 + /// `+C` — no-CTCP: refuses CTCP queries (other than `ACTION`) sent to this user as a 206 + /// PRIVMSG (P11 slice 306). 207 + /// 208 + /// **leveva-native — a port of charybdis's `extensions/umode_noctcp.c`** (an *extension* 209 + /// umode, not part of the standard surface). When a user is `+C`, a CTCP query (a PRIVMSG 210 + /// whose body opens with `\x01` and is not `ACTION`) addressed to them is refused 211 + /// `531 ERR_CANNOTSENDTOUSER :+C set` and not delivered; NOTICE, CTCP replies, and `ACTION` 212 + /// always pass. The recipient-side private-message counterpart of the **channel** mode `+C` 213 + /// ([`ChanMode::NoCtcp`], slice 220) — the user/channel mode namespaces are disjoint. Like 214 + /// `+g`/`+R`/`+D` it is enforced on the target's **home server** (the local 215 + /// [`crate::command::message`] path and the inbound-S2S 216 + /// [`crate::s2s::relay::inbound_message`] path), but the *bit* still propagates 217 + /// ([`SEND_UMODES`](crate::s2s::umode::SEND_UMODES) is every umode), so a remote `WHOIS` 218 + /// shows `+C`. Client-settable. Appended **last** so all bits above are untouched. 219 + NoCtcp, 205 220 } 206 221 207 222 impl UserMode { 208 223 /// Every user mode, in `user_modes[]` table order (with the leveva-native 209 224 /// `+W` appended last). 210 - pub const ALL: [UserMode; 19] = [ 225 + pub const ALL: [UserMode; 20] = [ 211 226 UserMode::Oper, 212 227 UserMode::LocalOp, 213 228 UserMode::Invisible, ··· 227 242 UserMode::Operwall, 228 243 UserMode::Service, 229 244 UserMode::LocOps, 245 + UserMode::NoCtcp, 230 246 ]; 231 247 232 248 /// The mode letter, e.g. `'o'`. ··· 252 268 UserMode::Operwall => 'z', 253 269 UserMode::Service => 'S', 254 270 UserMode::LocOps => 'l', 271 + UserMode::NoCtcp => 'C', 255 272 } 256 273 } 257 274 ··· 279 296 UserMode::Operwall => 0x10000, // leveva-native (no C oracle, slice 252) 280 297 UserMode::Service => 0x20000, // leveva-native (charybdis UMODE_SERVICE, slice 253) 281 298 UserMode::LocOps => 0x40000, // leveva-native (charybdis UMODE_LOCOPS, slice 254) 299 + UserMode::NoCtcp => 0x80000, // leveva-native (charybdis umode_noctcp, slice 306) 282 300 } 283 301 } 284 302 ··· 306 324 UserMode::Operwall => "FLAGS_OPERWALL", 307 325 UserMode::Service => "FLAGS_SERVICE", 308 326 UserMode::LocOps => "FLAGS_LOCOPS", 327 + UserMode::NoCtcp => "FLAGS_NOCTCP", 309 328 } 310 329 } 311 330 ··· 332 351 'z' => UserMode::Operwall, 333 352 'S' => UserMode::Service, 334 353 'l' => UserMode::LocOps, 354 + 'C' => UserMode::NoCtcp, 335 355 _ => return None, 336 356 }) 337 357 } ··· 359 379 0x10000 => UserMode::Operwall, 360 380 0x20000 => UserMode::Service, 361 381 0x40000 => UserMode::LocOps, 382 + 0x80000 => UserMode::NoCtcp, 362 383 _ => return None, 363 384 }) 364 385 } ··· 422 443 modes & UserMode::NoForward.bit() != 0 423 444 } 424 445 446 + /// Whether `modes` carries the no-CTCP (`+C`, [`UserMode::NoCtcp`]) flag — the predicate that 447 + /// makes a user refuse CTCP queries (P11 slice 306). Exactly the `+C` bit test, independent of 448 + /// every other mode. (Distinct from the **channel** mode `+C` [`ChanMode::NoCtcp`], which refuses 449 + /// CTCP to a *channel*.) 450 + #[inline] 451 + pub fn is_noctcp(modes: u32) -> bool { 452 + modes & UserMode::NoCtcp.bit() != 0 453 + } 454 + 455 + /// Whether a private message must be refused by a `+C` ([`UserMode::NoCtcp`], slice 306) target — 456 + /// the composite delivery-gate decision and its fuzz seam, shared by the local 457 + /// ([`crate::command::message`]) and inbound-S2S ([`crate::s2s::relay::inbound_message`]) 458 + /// enforcement sites. 459 + /// 460 + /// A message is blocked iff it is a **PRIVMSG** (`is_privmsg`; a `NOTICE` never auto-replies and is 461 + /// never blocked here — charybdis returns early for `MESSAGE_TYPE_NOTICE`), the target is `+C`, and 462 + /// the body is a blockable CTCP query — [`crate::channel::is_blocked_ctcp`], i.e. it opens with the 463 + /// `\x01` delimiter and the leading token is not `ACTION` (case-insensitive). A blocked PRIVMSG 464 + /// gets `531 ERR_CANNOTSENDTOUSER`; everything else passes through to delivery. 465 + #[inline] 466 + pub fn noctcp_user_blocks(target_modes: u32, is_privmsg: bool, text: &str) -> bool { 467 + is_privmsg && is_noctcp(target_modes) && crate::channel::is_blocked_ctcp(text) 468 + } 469 + 425 470 /// Whether `modes` carries the operwall (`+z`, [`UserMode::Operwall`]) flag — the predicate that 426 471 /// makes a user a recipient of [`OPERWALL`](crate::command::operwall) broadcasts (P11 slice 252). 427 472 /// Exactly the `+z` bit test, independent of every other mode; the receiver-side delivery filter ··· 474 519 UserMode::Operwall => "z", 475 520 UserMode::Service => "S", 476 521 UserMode::LocOps => "l", 522 + UserMode::NoCtcp => "C", 477 523 }) 478 524 } 479 525 } ··· 1284 1330 assert!(!is_regonly_msg(0)); 1285 1331 assert!(!is_regonly_msg(UserMode::Invisible.bit() | UserMode::Deaf.bit())); 1286 1332 assert!(!is_regonly_msg(UserMode::CallerId.bit())); 1333 + } 1334 + 1335 + #[test] 1336 + fn is_noctcp_true_iff_the_c_bit_is_set() { 1337 + let c = UserMode::NoCtcp.bit(); 1338 + assert!(is_noctcp(c)); 1339 + assert!(is_noctcp(c | UserMode::Oper.bit() | UserMode::Deaf.bit())); 1340 + // Inverse: every other bit, and the empty set, are not +C. 1341 + assert!(!is_noctcp(0)); 1342 + assert!(!is_noctcp(UserMode::Invisible.bit() | UserMode::RegOnlyMsg.bit())); 1343 + // Disjoint from the channel +C: this is a *user* bit only. 1344 + assert!(!is_noctcp(UserMode::CallerId.bit())); 1345 + } 1346 + 1347 + #[test] 1348 + fn noctcp_user_blocks_truth_table() { 1349 + let c = UserMode::NoCtcp.bit(); 1350 + // Blocked: a PRIVMSG carrying a non-ACTION CTCP to a +C user. 1351 + assert!(noctcp_user_blocks(c, true, "\u{1}VERSION\u{1}")); 1352 + assert!(noctcp_user_blocks(c, true, "\u{1}PING 123\u{1}")); 1353 + // Inverse — NOTICE never blocks (the `is_privmsg == false` arm). 1354 + assert!(!noctcp_user_blocks(c, false, "\u{1}VERSION\u{1}")); 1355 + // Inverse — a non-+C target never blocks, whatever the body. 1356 + assert!(!noctcp_user_blocks(0, true, "\u{1}VERSION\u{1}")); 1357 + // Inverse — ACTION always passes (it is not a blockable CTCP). 1358 + assert!(!noctcp_user_blocks(c, true, "\u{1}ACTION waves\u{1}")); 1359 + // Inverse — plain text always passes. 1360 + assert!(!noctcp_user_blocks(c, true, "hello there")); 1361 + assert!(!noctcp_user_blocks(c, true, "")); 1362 + } 1363 + 1364 + #[test] 1365 + fn noctcp_mode_renders_last() { 1366 + // `+C` is appended after `+l`, so it renders after every other mode. 1367 + let bits = UserMode::Invisible.bit() | UserMode::NoCtcp.bit() | UserMode::Operwall.bit(); 1368 + assert_eq!(UserMode::render(bits), "izC", "C sorts last"); 1369 + // Inverse: no `+C` bit -> no C in the render. 1370 + assert!(!UserMode::render(UserMode::Invisible.bit()).contains('C')); 1371 + // Round-trip: the new letter survives from_char/as_char/from_bit. 1372 + assert_eq!(UserMode::from_char('C'), Some(UserMode::NoCtcp)); 1373 + assert_eq!(UserMode::NoCtcp.as_char(), 'C'); 1374 + assert_eq!(UserMode::from_bit(0x80000), Some(UserMode::NoCtcp)); 1287 1375 } 1288 1376 1289 1377 #[test]
+5
leveva/src/numeric.rs
··· 454 454 /// `ERR_CANTJOINOPERSONLY` — JOIN of an oper-only (`+O`) channel by a non-oper 455 455 /// (leveva-native; the InspIRCd/charybdis convention). 456 456 ErrCantjoinopersonly = 520, 457 + /// `ERR_CANNOTSENDTOUSER` — a private message was refused by the target's `+C` 458 + /// ([`crate::UserMode::NoCtcp`]) no-CTCP user mode (leveva-native; the charybdis 459 + /// `extensions/umode_noctcp.c` convention, slice 306). `<tnick> :+C set`. 460 + ErrCannotSendToUser = 531, 457 461 /// `RPL_WHOISSECURE` — WHOIS reports the target connected over a secure (TLS) transport: 458 462 /// `<reqnick> <tnick> :is using a secure connection`. leveva-native: the IRCnet 2.11 oracle 459 463 /// has no such numeric; `671` is the de-facto code used by charybdis/ratbox/InspIRCd. Fired ··· 777 781 Numeric::ErrUsersdontmatch => "ERR_USERSDONTMATCH", 778 782 Numeric::ErrSslonlychan => "ERR_SSLONLYCHAN", 779 783 Numeric::ErrCantjoinopersonly => "ERR_CANTJOINOPERSONLY", 784 + Numeric::ErrCannotSendToUser => "ERR_CANNOTSENDTOUSER", 780 785 Numeric::RplWhoissecure => "RPL_WHOISSECURE", 781 786 Numeric::RplEtracefull => "RPL_ETRACEFULL", 782 787 Numeric::RplMononline => "RPL_MONONLINE",
+2 -2
leveva/src/registration.rs
··· 1268 1268 // + s (server notices, re-introduced slice 184) + g/G (strict/soft caller-id, 216/218) 1269 1269 // + Z (secure/TLS, slice 227) + D (deaf, slice 247) + R (regonly-msg, slice 248) 1270 1270 // + Q (no-forward, slice 250) + z (operwall, slice 252) + S (service, slice 253) 1271 - // + l (locops, slice 254). 1272 - assert_eq!(p[3], "oOiwraWBxsgGZDRQzSl"); 1271 + // + l (locops, slice 254) + C (no-CTCP, slice 306). 1272 + assert_eq!(p[3], "oOiwraWBxsgGZDRQzSlC"); 1273 1273 // + the leveva-native elemental membership letters q/a/h (slice 158) and Y (OJOIN, 165) 1274 1274 // + C (no-CTCP, slice 220) + c (no-color, slice 232) + S (TLS-only, slice 233) 1275 1275 // + z (op-moderation, slice 235) + j (join-throttle, slice 234) + T (no-NOTICE, slice 239)
+137
leveva/src/s2s/relay.rs
··· 468 468 ); 469 469 } 470 470 471 + /// A remote sender's CTCP PRIVMSG was blocked by a local target's `+C` (no-CTCP, slice 306 — the 472 + /// S2S follow-on of the local plane in [`crate::command::message`], mirroring `+R` 248→249). Route 473 + /// `531 ERR_CANNOTSENDTOUSER` back to the sender's uplink, addressed by nick and prefixed with our 474 + /// server name (the inbound-numeric convention). Nothing is routed to the target; a blocked NOTICE 475 + /// never reaches here (the caller's `is_privmsg` guard). 476 + fn noctcp_reject_remote(ctx: &ServerContext, target: &Uid, sender: &Uid) { 477 + let tnick = ctx 478 + .registry 479 + .nick_of(target) 480 + .unwrap_or_else(|| target.as_str().to_string()); 481 + let snick = ctx 482 + .net 483 + .user(sender) 484 + .map(|u| u.nick) 485 + .or_else(|| ctx.registry.nick_of(sender)) 486 + .unwrap_or_else(|| sender.as_str().to_string()); 487 + route_numeric_to_remote( 488 + ctx, 489 + sender, 490 + Message::builder(crate::Numeric::ErrCannotSendToUser) 491 + .prefix(&ctx.name) 492 + .param(&snick) 493 + .param(&tnick) 494 + .trailing("+C set") 495 + .build() 496 + .to_wire(), 497 + ); 498 + } 499 + 471 500 /// Route a local client's tag-only `TAGMSG` **to a remote user** to its uplink. Like 472 501 /// [`route_message_to_remote`] but no text body — and, being a tag-only message, routed only 473 502 /// when the uplink speaks message-tags (a non-tag uplink is skipped). ··· 1112 1141 if verb.eq_ignore_ascii_case("PRIVMSG") { 1113 1142 regonly_msg_reject_remote(ctx, &uid, sender); 1114 1143 } 1144 + continue; 1145 + } 1146 + // No-CTCP (slice 306): the `+C` gate also runs on the target's home server 1147 + // (a per-server gate, like `+g`/`+R`). A CTCP query (other than `ACTION`) 1148 + // addressed as a remote PRIVMSG to a local `+C` user is refused — `531 1149 + // ERR_CANNOTSENDTOUSER` is routed back across the link to the sender; a NOTICE 1150 + // (and an `ACTION`) always passes. (Caller-id/`+R` take precedence above.) 1151 + if uid != *sender 1152 + && crate::mode::noctcp_user_blocks( 1153 + ctx.registry.modes_of(&uid), 1154 + verb.eq_ignore_ascii_case("PRIVMSG"), 1155 + text, 1156 + ) 1157 + { 1158 + noctcp_reject_remote(ctx, &uid, sender); 1115 1159 continue; 1116 1160 } 1117 1161 } ··· 3761 3805 "PRIVMSG", 3762 3806 ); 3763 3807 assert_eq!(next(&mut a_rx), ":rover!rover@rh PRIVMSG alice :hi\r\n"); 3808 + assert!(peer_rx.try_recv().is_err()); 3809 + } 3810 + 3811 + // ---- +C no-CTCP, remote-sender plane (slice 306) ---- 3812 + 3813 + /// A remote sender's CTCP PRIVMSG (other than ACTION) to a local `+C` target is blocked: 3814 + /// nothing is delivered and `531 ERR_CANNOTSENDTOUSER` routes back across the link. 3815 + #[test] 3816 + fn inbound_message_noctcp_blocks_a_remote_ctcp_privmsg() { 3817 + let ctx = ctx(); 3818 + let (mut link, mut peer_rx) = link_peer(&ctx); 3819 + introduce(&ctx, &link, "0XYZAAAAA", "rover"); 3820 + let (alice, mut a_rx) = local_client(&ctx, "0ABCAAAAA", "alice"); 3821 + ctx.registry.set_modes(&alice, crate::UserMode::NoCtcp.bit()); 3822 + 3823 + inbound_message( 3824 + &mut link, 3825 + &parse(":0XYZAAAAA PRIVMSG alice :\u{1}VERSION\u{1}"), 3826 + &ctx, 3827 + "PRIVMSG", 3828 + ); 3829 + assert!(a_rx.try_recv().is_err(), "no CTCP delivered to a +C target"); 3830 + let m531 = Message::parse(&next(&mut peer_rx)).unwrap(); 3831 + assert_eq!(m531.command(), "531"); 3832 + assert_eq!(m531.params().first().map(String::as_str), Some("rover")); 3833 + assert_eq!(m531.params().get(1).map(String::as_str), Some("alice")); 3834 + assert_eq!(m531.trailing(), Some("+C set")); 3835 + assert!(peer_rx.try_recv().is_err(), "531 only"); 3836 + } 3837 + 3838 + /// Inverse: a remote ACTION to a `+C` target is delivered (ACTION is not a blockable CTCP), 3839 + /// and no 531 is routed back. 3840 + #[test] 3841 + fn inbound_message_noctcp_lets_a_remote_action_through() { 3842 + let ctx = ctx(); 3843 + let (mut link, mut peer_rx) = link_peer(&ctx); 3844 + introduce(&ctx, &link, "0XYZAAAAA", "rover"); 3845 + let (alice, mut a_rx) = local_client(&ctx, "0ABCAAAAA", "alice"); 3846 + ctx.registry.set_modes(&alice, crate::UserMode::NoCtcp.bit()); 3847 + 3848 + inbound_message( 3849 + &mut link, 3850 + &parse(":0XYZAAAAA PRIVMSG alice :\u{1}ACTION waves\u{1}"), 3851 + &ctx, 3852 + "PRIVMSG", 3853 + ); 3854 + assert_eq!( 3855 + next(&mut a_rx), 3856 + ":rover!rover@rh PRIVMSG alice :\u{1}ACTION waves\u{1}\r\n" 3857 + ); 3858 + assert!(peer_rx.try_recv().is_err(), "ACTION → no 531 back"); 3859 + } 3860 + 3861 + /// Inverse: a remote CTCP NOTICE to a `+C` target is delivered (NOTICE never blocked), no 531. 3862 + #[test] 3863 + fn inbound_message_noctcp_remote_notice_ctcp_passes() { 3864 + let ctx = ctx(); 3865 + let (mut link, mut peer_rx) = link_peer(&ctx); 3866 + introduce(&ctx, &link, "0XYZAAAAA", "rover"); 3867 + let (alice, mut a_rx) = local_client(&ctx, "0ABCAAAAA", "alice"); 3868 + ctx.registry.set_modes(&alice, crate::UserMode::NoCtcp.bit()); 3869 + 3870 + inbound_message( 3871 + &mut link, 3872 + &parse(":0XYZAAAAA NOTICE alice :\u{1}VERSION reply\u{1}"), 3873 + &ctx, 3874 + "NOTICE", 3875 + ); 3876 + assert_eq!( 3877 + next(&mut a_rx), 3878 + ":rover!rover@rh NOTICE alice :\u{1}VERSION reply\u{1}\r\n" 3879 + ); 3880 + assert!(peer_rx.try_recv().is_err(), "NOTICE → no 531 back"); 3881 + } 3882 + 3883 + /// Inverse: a plain (non-`+C`) local target receives a remote CTCP normally — gate never fires. 3884 + #[test] 3885 + fn inbound_message_noctcp_plain_target_unaffected() { 3886 + let ctx = ctx(); 3887 + let (mut link, mut peer_rx) = link_peer(&ctx); 3888 + introduce(&ctx, &link, "0XYZAAAAA", "rover"); 3889 + let (_alice, mut a_rx) = local_client(&ctx, "0ABCAAAAA", "alice"); 3890 + 3891 + inbound_message( 3892 + &mut link, 3893 + &parse(":0XYZAAAAA PRIVMSG alice :\u{1}PING 1\u{1}"), 3894 + &ctx, 3895 + "PRIVMSG", 3896 + ); 3897 + assert_eq!( 3898 + next(&mut a_rx), 3899 + ":rover!rover@rh PRIVMSG alice :\u{1}PING 1\u{1}\r\n" 3900 + ); 3764 3901 assert!(peer_rx.try_recv().is_err()); 3765 3902 } 3766 3903
+88
leveva/tests/golden_noctcp_umode.rs
··· 1 + //! Boot-level golden: the `+C` no-CTCP **user mode** end-to-end through the real `leveva` binary 2 + //! (P11 slice 306, charybdis `extensions/umode_noctcp.c`). 3 + //! 4 + //! A `+C` user refuses CTCP queries (other than `ACTION`) addressed to them as a **PRIVMSG**: the 5 + //! sender gets `531 ERR_CANNOTSENDTOUSER :+C set` and the query is never delivered. `ACTION`, CTCP 6 + //! replies (NOTICE), and ordinary text always pass. `+C` is leveva-native (the charybdis 7 + //! extension convention), so this is a leveva-only golden — no differential against the port. 8 + //! 9 + //! "bob never received the blocked CTCP" is asserted by **sentinel ordering**: a later plain line 10 + //! that *does* arrive is read; if the blocked CTCP had been delivered it would sit ahead of it. 11 + 12 + mod harness; 13 + use harness::{boot, Client}; 14 + 15 + /// Register `name` and read through the welcome burst to the MOTD-missing 422. 16 + fn register(name: &str) -> Client { 17 + let mut c = Client::connect(); 18 + c.send(&format!("NICK {name}")); 19 + c.send(&format!("USER {name} 0 * :{name} Tester")); 20 + c.read_until(" 422 "); 21 + c 22 + } 23 + 24 + #[test] 25 + fn noctcp_blocks_ctcp_privmsg_then_passes_action_and_plain() { 26 + let _srv = boot(); 27 + 28 + let mut alice = register("alice"); 29 + let mut bob = register("bob"); 30 + 31 + // bob enables +C (no-CTCP). 32 + bob.send("MODE bob +C"); 33 + bob.read_until("+C"); 34 + 35 + // alice CTCPs bob → she gets 531, bob receives nothing. 36 + alice.send("PRIVMSG bob :\u{1}VERSION\u{1}"); 37 + let bounce = alice.read_until(" 531 "); 38 + assert!( 39 + bounce.contains(" 531 ") && bounce.contains("bob") && bounce.contains("+C set"), 40 + "a CTCP PRIVMSG to a +C user bounces 531 naming the target, got: {bounce}" 41 + ); 42 + 43 + // Inverse 1: an ACTION is still delivered (not a blockable CTCP). 44 + alice.send("PRIVMSG bob :\u{1}ACTION waves\u{1}"); 45 + let got_action = bob.read_until("ACTION waves"); 46 + assert!( 47 + got_action.contains("ACTION waves"), 48 + "+C must let an ACTION through, got: {got_action}" 49 + ); 50 + assert!( 51 + !got_action.contains("VERSION"), 52 + "the blocked CTCP must never have reached bob, got: {got_action}" 53 + ); 54 + 55 + // Inverse 2: a CTCP reply (NOTICE) is delivered. 56 + alice.send("NOTICE bob :\u{1}VERSION reply\u{1}"); 57 + let got_notice = bob.read_until("VERSION reply"); 58 + assert!( 59 + got_notice.contains("NOTICE") && got_notice.contains("VERSION reply"), 60 + "+C must let a CTCP NOTICE through, got: {got_notice}" 61 + ); 62 + 63 + // Inverse 3: clearing -C restores CTCP PRIVMSG delivery. 64 + bob.send("MODE bob -C"); 65 + bob.read_until("-C"); 66 + alice.send("PRIVMSG bob :\u{1}PING 42\u{1}"); 67 + let got_ping = bob.read_until("PING 42"); 68 + assert!( 69 + got_ping.contains("PING 42"), 70 + "clearing -C restores CTCP delivery, got: {got_ping}" 71 + ); 72 + } 73 + 74 + #[test] 75 + fn non_noctcp_user_receives_ctcp() { 76 + // Inverse baseline: a user without +C receives a CTCP query normally. 77 + let _srv = boot(); 78 + 79 + let mut alice = register("alice"); 80 + let mut bob = register("bob"); 81 + 82 + alice.send("PRIVMSG bob :\u{1}TIME\u{1}"); 83 + let got = bob.read_until("TIME"); 84 + assert!( 85 + got.contains("PRIVMSG") && got.contains("TIME"), 86 + "a non-+C user must receive the CTCP, got: {got}" 87 + ); 88 + }
+67
leveva/tests/noctcp_umode_proptest.rs
··· 1 + //! Property-based fuzzing of the `+C` no-CTCP **user mode** delivery gate 2 + //! ([`leveva::mode::noctcp_user_blocks`], P11 slice 306) — the composite decision both the local 3 + //! ([`leveva::command::message`]) and inbound-S2S ([`leveva::s2s::relay::inbound_message`]) 4 + //! enforcement sites read to refuse a CTCP query (other than `ACTION`) addressed to a `+C` user. 5 + //! 6 + //! The function must be **total** (never panic on any UTF-8 input or mode word) and equal the 7 + //! conjunction of its three independent gates: the message is a PRIVMSG, the target is `+C`, and 8 + //! the body is a blockable CTCP ([`leveva::channel::is_blocked_ctcp`]). It is monotone in each gate 9 + //! (clearing any one gate can only turn a `true` into `false`, never the reverse). 10 + 11 + use leveva::channel::is_blocked_ctcp; 12 + use leveva::mode::{is_noctcp, noctcp_user_blocks}; 13 + use leveva::UserMode; 14 + use proptest::prelude::*; 15 + 16 + /// An arbitrary umode word: any subset of the user-mode bits, plus arbitrary noise bits. 17 + fn modes() -> impl Strategy<Value = u32> { 18 + any::<u32>() 19 + } 20 + 21 + proptest! { 22 + /// Total, and exactly the conjunction of the three gates. 23 + #[test] 24 + fn equals_the_conjunction_of_the_three_gates( 25 + m in modes(), 26 + is_privmsg in any::<bool>(), 27 + text in ".*", 28 + ) { 29 + let expected = is_privmsg && is_noctcp(m) && is_blocked_ctcp(&text); 30 + prop_assert_eq!(noctcp_user_blocks(m, is_privmsg, &text), expected); 31 + } 32 + 33 + /// A NOTICE (`is_privmsg == false`) is never blocked, whatever the target/body. 34 + #[test] 35 + fn notice_is_never_blocked(m in modes(), text in ".*") { 36 + prop_assert!(!noctcp_user_blocks(m, false, &text)); 37 + } 38 + 39 + /// A target without the `+C` bit is never blocked, whatever the body. 40 + #[test] 41 + fn non_plus_c_target_is_never_blocked(m in modes(), is_privmsg in any::<bool>(), text in ".*") { 42 + let cleared = m & !UserMode::NoCtcp.bit(); 43 + prop_assert!(!noctcp_user_blocks(cleared, is_privmsg, &text)); 44 + } 45 + 46 + /// Monotonicity: if a message is blocked, then forcing the `+C` bit on (it already is) and 47 + /// keeping it a PRIVMSG keeps it blocked; and clearing `+C`, or flipping to NOTICE, unblocks it. 48 + #[test] 49 + fn clearing_any_gate_unblocks(m in modes(), text in ".*") { 50 + let with_c = m | UserMode::NoCtcp.bit(); 51 + if noctcp_user_blocks(with_c, true, &text) { 52 + // Clearing the +C bit unblocks. 53 + prop_assert!(!noctcp_user_blocks(with_c & !UserMode::NoCtcp.bit(), true, &text)); 54 + // Flipping to NOTICE unblocks. 55 + prop_assert!(!noctcp_user_blocks(with_c, false, &text)); 56 + } 57 + } 58 + 59 + /// A non-ACTION CTCP PRIVMSG to a `+C` target is always blocked (the positive direction). 60 + #[test] 61 + fn ctcp_privmsg_to_plus_c_is_blocked(cmd in "[A-Z][A-Z0-9]{0,15}", m in modes()) { 62 + prop_assume!(!cmd.eq_ignore_ascii_case("ACTION")); 63 + let with_c = m | UserMode::NoCtcp.bit(); 64 + let text = format!("\u{1}{cmd}\u{1}"); 65 + prop_assert!(noctcp_user_blocks(with_c, true, &text)); 66 + } 67 + }
+1 -1
leveva/tests/snapshots/golden_cap__cap_302_negotiation_suspends_then_resumes.snap
··· 7 7 :leveva.test 001 dan :Welcome to the Internet Relay Network dan!d@127.0.0.1 8 8 :leveva.test 002 dan :Your host is leveva.test, running version leveva-<VER> 9 9 :leveva.test 003 dan :This server was created <MASKED> 10 - :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSl psmntiOCcSzTgrQFPLovqahYbeIRkljf 10 + :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSlC psmntiOCcSzTgrQFPLovqahYbeIRkljf 11 11 :leveva.test 005 dan CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 12 12 :leveva.test 005 dan BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 13 13 :leveva.test 251 dan :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_cap__cap_ls_without_version_then_end.snap
··· 6 6 :leveva.test 001 jo :Welcome to the Internet Relay Network jo!j@127.0.0.1 7 7 :leveva.test 002 jo :Your host is leveva.test, running version leveva-<VER> 8 8 :leveva.test 003 jo :This server was created <MASKED> 9 - :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSl psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 + :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSlC psmntiOCcSzTgrQFPLovqahYbeIRkljf 10 10 :leveva.test 005 jo CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 11 11 :leveva.test 005 jo BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 12 12 :leveva.test 251 jo :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_pass__correct_password_welcomes.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSl psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSlC psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_registration__registration_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSl psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSlC psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers
+1 -1
leveva/tests/snapshots/golden_user_spaceless__spaceless_user_realname_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSl psmntiOCcSzTgrQFPLovqahYbeIRkljf 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZDRQzSlC psmntiOCcSzTgrQFPLovqahYbeIRkljf 9 9 :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQFPL PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers