protocol: verify echos pseudoclients request servprotect (+k, by name from CAPAB USERMODES) so they cannot be killed by operators; warn if service_modes lacks it
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
This commit is contained in:
parent
2f70b0c133
commit
b8a31244db
3 changed files with 44 additions and 0 deletions
|
|
@ -77,6 +77,10 @@ pub enum NetEvent {
|
||||||
ModulesAvailable { modules: Vec<String> },
|
ModulesAvailable { modules: Vec<String> },
|
||||||
// `CAPAB END` — the module lists are complete; verify dependencies now.
|
// `CAPAB END` — the module lists are complete; verify dependencies now.
|
||||||
CapabEnd,
|
CapabEnd,
|
||||||
|
// The ircd's `servprotect` user mode (from CAPAB USERMODES) and whether echo's
|
||||||
|
// configured service_modes request it — a services pseudoclient without it can
|
||||||
|
// be killed/hijacked by an operator.
|
||||||
|
ServProtect { letter: char, requested: bool },
|
||||||
// A server split away (SQUIT). `server` is its SID; every user behind it — and
|
// A server split away (SQUIT). `server` is its SID; every user behind it — and
|
||||||
// behind any server in its subtree — is gone, since a split is signalled once
|
// behind any server in its subtree — is gone, since a split is signalled once
|
||||||
// rather than as a QUIT per user.
|
// rather than as a QUIT per user.
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,14 @@ impl Protocol for InspIrcd {
|
||||||
vec![NetEvent::ModulesAvailable { modules }]
|
vec![NetEvent::ModulesAvailable { modules }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// CAPAB USERMODES: we only need servprotect (+k) — verify our own
|
||||||
|
// pseudoclients request it, so they can't be killed by operators.
|
||||||
|
Some(s) if s.eq_ignore_ascii_case("USERMODES") => {
|
||||||
|
match trailing(rest).split_whitespace().find_map(servprotect_letter) {
|
||||||
|
Some(letter) => vec![NetEvent::ServProtect { letter, requested: self.service_modes.contains(letter) }],
|
||||||
|
None => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(s) if s.eq_ignore_ascii_case("END") => vec![NetEvent::CapabEnd],
|
Some(s) if s.eq_ignore_ascii_case("END") => vec![NetEvent::CapabEnd],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
},
|
},
|
||||||
|
|
@ -585,6 +593,18 @@ fn parse_extban_cap(token: &str) -> Option<echo_api::ExtbanCap> {
|
||||||
(!name.is_empty()).then(|| echo_api::ExtbanCap { name: name.to_string(), letter, acting })
|
(!name.is_empty()).then(|| echo_api::ExtbanCap { name: name.to_string(), letter, acting })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The mode letter of the `servprotect` user mode in a CAPAB USERMODES token
|
||||||
|
// (`simple:servprotect=k`), or None for any other mode.
|
||||||
|
fn servprotect_letter(token: &str) -> Option<char> {
|
||||||
|
let (_ty, spec) = token.split_once(':')?;
|
||||||
|
let (name, val) = spec.split_once('=')?;
|
||||||
|
if name.eq_ignore_ascii_case("servprotect") {
|
||||||
|
val.chars().next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize a `CAPAB MODULES` token (`m_foo.so=data` / `foo=data` / `foo`) to the
|
// Normalize a `CAPAB MODULES` token (`m_foo.so=data` / `foo=data` / `foo`) to the
|
||||||
// bare module name, matching the ircd's own naming.
|
// bare module name, matching the ircd's own naming.
|
||||||
fn module_name(token: &str) -> String {
|
fn module_name(token: &str) -> String {
|
||||||
|
|
@ -682,6 +702,18 @@ mod tests {
|
||||||
assert!(p.parse("CAPAB CHANMODES :ban=b").is_empty(), "other CAPAB subcommands ignored");
|
assert!(p.parse("CAPAB CHANMODES :ban=b").is_empty(), "other CAPAB subcommands ignored");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CAPAB USERMODES surfaces servprotect and whether our service_modes request it
|
||||||
|
// (proto()'s service_modes includes k). Only servprotect is extracted.
|
||||||
|
#[test]
|
||||||
|
fn parses_capab_usermodes_servprotect() {
|
||||||
|
let mut p = proto();
|
||||||
|
assert!(matches!(p.parse("CAPAB USERMODES :simple:invisible=i simple:servprotect=k param:snomask=s").as_slice(),
|
||||||
|
[NetEvent::ServProtect { letter: 'k', requested: true }]));
|
||||||
|
assert!(matches!(p.parse("CAPAB USERMODES :simple:servprotect=Z").as_slice(),
|
||||||
|
[NetEvent::ServProtect { letter: 'Z', requested: false }], "char not in service_modes -> not requested"));
|
||||||
|
assert!(p.parse("CAPAB USERMODES :simple:invisible=i simple:wallops=w").is_empty(), "no servprotect -> nothing");
|
||||||
|
}
|
||||||
|
|
||||||
// CAPAB MODULES/MODSUPPORT surface normalized module names; CAPAB END triggers
|
// CAPAB MODULES/MODSUPPORT surface normalized module names; CAPAB END triggers
|
||||||
// the dependency check. Names are stripped of m_/.so/=data.
|
// the dependency check. Names are stripped of m_/.so/=data.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1124,6 +1124,14 @@ impl Engine {
|
||||||
verify_ircd_dependencies(&self.network);
|
verify_ircd_dependencies(&self.network);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
NetEvent::ServProtect { letter, requested } => {
|
||||||
|
if requested {
|
||||||
|
tracing::info!(mode = %letter, "services are servprotected (+{letter})");
|
||||||
|
} else {
|
||||||
|
tracing::warn!(mode = %letter, "service_modes does not request servprotect (+{letter}) — echo's pseudoclients can be killed or hijacked by operators; add it to [server] service_modes");
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
NetEvent::Casemapping { name } => {
|
NetEvent::Casemapping { name } => {
|
||||||
// echo folds identifiers as ascii. Verify the ircd agrees, rather than
|
// echo folds identifiers as ascii. Verify the ircd agrees, rather than
|
||||||
// assuming it silently — a mismatch would desync account/channel identity.
|
// assuming it silently — a mismatch would desync account/channel identity.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue