Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
1use crate::command::*;
2use crate::UserMode;
3
4/// `OKICK <channel> <user> [:comment]` — leveva's port of charybdis `extensions/m_okick.c` (P11
5/// slice 294). An IRC **operator** force-kicks `user` from `channel` **without** being a chanop —
6/// or even a member of the channel — sourced from the server so the channel sees `:<server> KICK
7/// <chan> <user> :<comment>`. It is the oper-override sibling of [`KICK`](crate::command::kick)
8/// (chanop-gated) and [`REMOVE`](crate::command::remove) (chanop force-*part*), and the
9/// kick-shaped member of the charybdis operator channel-intervention family
10/// (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`/`OPME`/`REMOVE`).
11///
12/// Gate order:
13/// - Not an IRC operator (`+o`/`+O`) → `481 ERR_NOPRIVILEGES` (OPME precedent: a plain oper bit,
14/// not a per-command [`OperPrivilege`](crate::config::OperPrivilege); charybdis `m_okick` is
15/// plain `mg_not_oper`).
16/// - Missing channel or user → `461 ERR_NEEDMOREPARAMS`.
17/// - Unknown victim nick → `401 ERR_NOSUCHNICK`.
18/// - Channel does not exist → `403 ERR_NOSUCHCHANNEL`.
19/// - Victim not on the channel → `441 ERR_USERNOTINCHANNEL`.
20/// - Victim holds `+Y` (official-join) → `482 ERR_CHANOPRIVSNEEDED` "official network business"
21/// (a leveva divergence from charybdis's raw removal — the `+Y` immunity stays absolute).
22/// - Success → remove the victim, broadcast `:<server> KICK <chan> <victim> :<comment>` to every
23/// local member (incl. the victim), **echo the KICK back to the oper** (always — the oper need
24/// not be a member; excluded from the audience fan to avoid a duplicate), relay server-sourced
25/// to the network ([`crate::s2s::relay::server_kick`]), and post a `+s` audit snomask.
26///
27/// Divergences (documented): leveva-native (no oracle — 2.11 has no OKICK); the `+Y` immunity is
28/// respected (charybdis raw-removes); the oper always receives the KICK echo even when not a
29/// member; the audit notice rides the `+s` snomask, not `+w` wallops (SA*/OJOIN/OPME family,
30/// slices 184/186/292); local-only. The comment defaults to the oper's nick (KICK precedent).
31pub(crate) fn okick(client: &Registered, msg: &Message, ctx: &ServerContext) -> Vec<Message> {
32 if client.modes & (UserMode::Oper.bit() | UserMode::LocalOp.bit()) == 0 {
33 return vec![no_privileges(ctx, &client.nick)];
34 }
35 let Some(chan) = msg.params().first().filter(|s| !s.is_empty()) else {
36 return vec![need_more_params(ctx, &client.nick, "OKICK")];
37 };
38 let Some(victim) = msg.params().get(1).filter(|s| !s.is_empty()) else {
39 return vec![need_more_params(ctx, &client.nick, "OKICK")];
40 };
41 // Comment is the trailing or a third middle param; defaults to the oper's nick (KICK precedent).
42 let comment = msg
43 .trailing()
44 .or_else(|| msg.params().get(2).map(String::as_str))
45 .filter(|s| !s.is_empty())
46 .unwrap_or(&client.nick)
47 .to_string();
48
49 let Some(uid) = ctx.registry.uid_of(victim) else {
50 return vec![no_such_nick(ctx, &client.nick, victim)];
51 };
52
53 match ctx.channels.okick(chan, &uid) {
54 OkickOutcome::NoSuchChannel => vec![no_such_channel(ctx, &client.nick, chan)],
55 OkickOutcome::NotInChannel { display } => vec![Message::builder(Numeric::ErrUsernotinchannel)
56 .prefix(&ctx.name)
57 .param(&client.nick)
58 .param(victim)
59 .param(&display)
60 .trailing("They aren't on that channel")
61 .build()],
62 OkickOutcome::OfficialBusiness { display } => {
63 let victim_nick = ctx
64 .registry
65 .nick_of(&uid)
66 .unwrap_or_else(|| victim.to_string());
67 vec![Message::builder(Numeric::ErrChanoprivsneeded)
68 .prefix(&ctx.name)
69 .param(&client.nick)
70 .param(&display)
71 .trailing(format!(
72 "Can't kick {victim_nick} as they're on official network business"
73 ))
74 .build()]
75 }
76 OkickOutcome::Kicked { display, audience } => {
77 // The canonical victim nick (resolved live) keeps the broadcast consistent with NAMES.
78 let victim_nick = ctx
79 .registry
80 .nick_of(&uid)
81 .unwrap_or_else(|| victim.to_string());
82 // Server-sourced KICK (charybdis `:me.name KICK`), echoed to the oper (always — they
83 // may not be a member) and fanned to every other local member incl. the victim.
84 let echo = Message::builder("KICK")
85 .prefix(&ctx.name)
86 .param(&display)
87 .param(&victim_nick)
88 .trailing(&comment)
89 .build();
90 let wire = echo.to_wire();
91 for m in &audience {
92 if *m != client.uid && ctx.is_local_uid(m) {
93 ctx.registry.deliver_uid(m, wire.clone());
94 }
95 }
96 crate::s2s::relay::server_kick(ctx, &display, &uid, &comment);
97 crate::snotice::server_notice(
98 ctx,
99 &format!("{} used OKICK on {victim_nick} in {display}", client.nick),
100 );
101 vec![echo]
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::command::testutil::*;
110
111 /// An opered `alice` connection (claimed in the registry) carrying `+o`.
112 fn opered(ctx: &ServerContext) -> Registered {
113 let _ = claim_observed(ctx, "alice");
114 let mut c = client();
115 c.modes |= UserMode::Oper.bit();
116 c
117 }
118
119 fn drive(ctx: &ServerContext, c: &Registered, line: &str) -> Vec<Message> {
120 dispatch(&mut c.clone(), &Message::parse(line).unwrap(), ctx)
121 }
122
123 /// The gate (and its inverse): a non-operator's OKICK is `481` and removes nothing.
124 #[test]
125 fn okick_requires_oper() {
126 let ctx = ctx();
127 let _bob = claim_observed(&ctx, "bob");
128 ctx.channels.join("#rust", &uid_for("bob"), 0); // bob the chanop/victim
129 let r = drive(&ctx, &client(), "OKICK #rust bob");
130 assert_eq!(codes(&r), &["481"]);
131 assert!(
132 ctx.channels.is_member("#rust", &uid_for("bob")),
133 "a gated OKICK removes nothing"
134 );
135 }
136
137 /// Operand / lookup errors land on the oper.
138 #[test]
139 fn okick_operand_and_lookup_errors() {
140 let ctx = ctx();
141 let oper = opered(&ctx);
142 assert_eq!(codes(&drive(&ctx, &oper, "OKICK")), &["461"]);
143 assert_eq!(codes(&drive(&ctx, &oper, "OKICK #rust")), &["461"]);
144 // Unknown nick → 401 (checked before the channel exists).
145 assert_eq!(codes(&drive(&ctx, &oper, "OKICK #rust nobody")), &["401"]);
146 // Known nick, ghost channel → 403.
147 let _bob = claim_observed(&ctx, "bob");
148 assert_eq!(codes(&drive(&ctx, &oper, "OKICK #ghost bob")), &["403"]);
149 // Known nick not on an existing channel → 441.
150 let _ = claim_observed(&ctx, "carol");
151 ctx.channels.join("#real", &uid_for("carol"), 0);
152 let r = drive(&ctx, &oper, "OKICK #real bob");
153 assert_eq!(codes(&r), &["441"]);
154 assert_eq!(r[0].params(), &["alice", "bob", "#real"]);
155 }
156
157 /// Success: an oper who is **not a member** force-kicks; the KICK is server-sourced, echoed to
158 /// the oper, seen by a co-member, and the victim is gone.
159 #[test]
160 fn okick_removes_member_as_a_non_member_oper() {
161 let ctx = ctx();
162 let oper = opered(&ctx); // alice, an oper, never joins #rust
163 let _bob = claim_observed(&ctx, "bob");
164 let mut carol = claim_observed(&ctx, "carol");
165 ctx.channels.join("#rust", &uid_for("bob"), 0); // bob the chanop/victim
166 ctx.channels.join("#rust", &uid_for("carol"), 0); // carol a bystander
167
168 let r = drive(&ctx, &oper, "OKICK #rust bob :spam");
169 // The oper gets the server-sourced KICK echoed back (even though not a member).
170 assert_eq!(codes(&r), &["KICK"]);
171 assert_eq!(r[0].prefix(), Some(ctx.name.as_str()));
172 assert_eq!(r[0].params(), &["#rust", "bob"]);
173 assert_eq!(r[0].trailing(), Some("spam"));
174 // carol witnesses the same KICK.
175 let seen = delivered(&mut carol);
176 assert_eq!(seen.command(), "KICK");
177 assert_eq!(seen.params(), &["#rust", "bob"]);
178 assert!(!ctx.channels.is_member("#rust", &uid_for("bob")), "bob gone");
179 assert!(ctx.channels.is_member("#rust", &uid_for("carol")));
180 }
181
182 /// Default comment is the oper's nick (no reason supplied).
183 #[test]
184 fn okick_default_comment_is_oper_nick() {
185 let ctx = ctx();
186 let oper = opered(&ctx);
187 let _bob = claim_observed(&ctx, "bob");
188 ctx.channels.join("#rust", &uid_for("bob"), 0);
189 let r = drive(&ctx, &oper, "OKICK #rust bob");
190 assert_eq!(r[0].command(), "KICK");
191 assert_eq!(r[0].trailing(), Some("alice"));
192 }
193
194 /// A `+Y` (official-join) member is un-okick-able → `482`, and nothing changes.
195 #[test]
196 fn okick_refuses_official_join_member() {
197 let ctx = ctx();
198 let oper = opered(&ctx);
199 let _bob = claim_observed(&ctx, "bob");
200 ctx.channels.join("#rust", &uid_for("bob"), 0);
201 ctx.channels.apply_modes_as_server(
202 "#rust",
203 &[ModeChange::Member {
204 add: true,
205 mode: ChanMode::OfficialJoin,
206 uid: uid_for("bob"),
207 }],
208 );
209 let r = drive(&ctx, &oper, "OKICK #rust bob :try");
210 assert_eq!(codes(&r), &["482"]);
211 assert!(r[0].trailing().unwrap().contains("official network business"));
212 assert!(
213 ctx.channels.is_member("#rust", &uid_for("bob")),
214 "the +Y member stays"
215 );
216 }
217}