Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
1//! Boot-level golden: `OKICK` end-to-end through the real `leveva` binary (slice 294).
2//!
3//! `OKICK <channel> <user> [:comment]` lets an operator force-kick a member without being a
4//! chanop — or even a member of the channel. One booted server (`dline.kdl`, whose `root`
5//! operator suffices — OKICK needs only the oper bit), three clients:
6//!
7//! 1. `alice`, `bob`, `carol` register; `alice` opers up but never joins `#rust`.
8//! 2. `bob` JOINs `#rust` (→ `@op`), `carol` JOINs (a plain member).
9//! 3. `bob` (not an operator) `OKICK #rust carol` → `481 ERR_NOPRIVILEGES`.
10//! 4. `alice` (an oper, **not** a member) `OKICK #rust carol :spam` → `:leveva.test KICK #rust
11//! carol :spam`, echoed to `alice` and seen by `bob` (and `carol`, the victim).
12//! 5. `alice` `OKICK #ghost carol` (channel does not exist) → `403`.
13//!
14//! Lines are server- or numeric-prefixed and deterministic, so only the shared masks apply.
15
16mod harness;
17use harness::{boot_fixture, canonicalize, Client, PORT};
18
19fn register(name: &str) -> Client {
20 let mut c = Client::connect();
21 c.send(&format!("NICK {name}"));
22 c.send(&format!("USER {name} 0 * :{name} Tester"));
23 c.read_until(" 422 "); // drain the welcome burst (MOTD-missing 422)
24 c
25}
26
27#[test]
28fn okick_end_to_end() {
29 let _srv = boot_fixture("dline.kdl", PORT);
30
31 let mut alice = register("alice");
32 let mut bob = register("bob");
33 let mut carol = register("carol");
34
35 // alice opers up (any operator suffices for OKICK) but never joins #rust.
36 alice.send("OPER root hunter2");
37 alice.read_until(" 381 ");
38
39 // bob creates #rust (→ @op); carol joins as a plain member.
40 bob.send("JOIN #rust");
41 bob.read_until(" 366 ");
42 carol.send("JOIN #rust");
43 carol.read_until(" 366 ");
44 bob.read_until("JOIN #rust"); // bob sees carol's JOIN
45
46 // 3. non-oper bob → 481 (and the channel is untouched).
47 bob.send("OKICK #rust carol");
48 let bob_481 = bob.read_until(" 481 ");
49
50 // 4. alice (oper, not a member) force-kicks carol → server-sourced KICK, echoed + seen.
51 alice.send("OKICK #rust carol :spam");
52 let alice_kick = alice.read_until("KICK #rust");
53 let bob_kick = bob.read_until("KICK #rust");
54 let carol_kick = carol.read_until("KICK #rust");
55
56 // 5. nonexistent channel → 403.
57 alice.send("OKICK #ghost carol");
58 let alice_403 = alice.read_until(" 403 ");
59
60 let mut transcript = String::new();
61 let mut keep = |src: &str, needle: &str, label: &str| {
62 for line in canonicalize(src).lines() {
63 if line.contains(needle) {
64 transcript.push_str(label);
65 transcript.push_str(line);
66 transcript.push('\n');
67 }
68 }
69 };
70 keep(&bob_481, " 481 ", "bob 481: ");
71 keep(&alice_kick, "KICK #rust carol", "alice sees KICK: ");
72 keep(&bob_kick, "KICK #rust carol", "bob sees KICK: ");
73 keep(&carol_kick, "KICK #rust carol", "carol sees KICK: ");
74 keep(&alice_403, " 403 ", "alice 403: ");
75
76 insta::assert_snapshot!(transcript);
77}