operserv: NOTIFY lines carry real IP, rdns, login and origin server for the oper feed
All checks were successful
CI / check (push) Successful in 4m4s

This commit is contained in:
Jean Chevronnet 2026-07-18 17:28:07 +00:00
parent 2fb1970030
commit 4d327c3f47
No known key found for this signature in database
2 changed files with 24 additions and 3 deletions

View file

@ -390,8 +390,28 @@ impl Engine {
if !self.db.notify_flags(Some(&target), chan).contains(flag) { if !self.db.notify_flags(Some(&target), chan).contains(flag) {
return None; return None;
} }
// The feed lands in the oper-only staff channel, so a watch line carries the
// forensic detail a cloak hides: the real IP (with rdns if it differs), the
// login, and the origin server. The connection port is not available — the
// ircd's s2s UID burst never sends it. `nick!ident@host` keeps the cloak so
// it still correlates with what ordinary users see.
let mut parts: Vec<String> = Vec::new();
if !target.ip.is_empty() {
let mut s = format!("\x02{}\x02", target.ip);
if !target.realhost.is_empty() && target.realhost != target.ip {
s.push_str(&format!(" ({})", target.realhost));
}
parts.push(s);
}
if let Some(account) = target.account {
parts.push(format!("acct \x02{account}\x02"));
}
if !target.server.is_empty() {
parts.push(format!("via {}", target.server));
}
let detail = if parts.is_empty() { String::new() } else { format!(" · {}", parts.join(" · ")) };
let who = format!("\x02{}\x02!{}@{}", target.nick, target.ident, target.host); let who = format!("\x02{}\x02!{}@{}", target.nick, target.ident, target.host);
self.feed("NOTIFY", format!("{who} {what}")) self.feed("NOTIFY", format!("{who} {what}{detail}"))
} }
// Inactivity-expiry thresholds (seconds); None leaves that kind never // Inactivity-expiry thresholds (seconds); None leaves that kind never

View file

@ -5408,8 +5408,9 @@ fn notify_hook_emits_feed_for_watched_user() {
let out = e.handle(NetEvent::UserAttrs { uid: "000AAAAAC".into(), ident: "x".into(), realhost: "h".into(), gecos: "g".into() }); let out = e.handle(NetEvent::UserAttrs { uid: "000AAAAAC".into(), ident: "x".into(), realhost: "h".into(), gecos: "g".into() });
assert!( assert!(
out.iter().any(|a| matches!(a, NetAction::Privmsg { from, to, text } out.iter().any(|a| matches!(a, NetAction::Privmsg { from, to, text }
if from == "42SAAAAAO" && to == "#services" && text.contains("[NOTIFY]") && text.contains("connected"))), if from == "42SAAAAAO" && to == "#services" && text.contains("[NOTIFY]") && text.contains("connected")
"expected a NOTIFY connect line, got {out:?}" && text.contains("1.2.3.4"))), // the real IP is carried, not just the cloak
"expected a NOTIFY connect line with the real IP, got {out:?}"
); );
// A clean user draws nothing. // A clean user draws nothing.
e.handle(NetEvent::UserConnect { uid: "000AAAAAD".into(), nick: "nice".into(), host: "h".into(), ip: "9.9.9.9".into() }); e.handle(NetEvent::UserConnect { uid: "000AAAAAD".into(), nick: "nice".into(), host: "h".into(), ip: "9.9.9.9".into() });