inspircd: include membid in outbound IJOIN
All checks were successful
CI / check (push) Successful in 3m49s

Services IJOIN was ':<uid> IJOIN <chan>' with no membership id, but InspIRCd
requires <chan> <membid> (min 2 params). On link the ircd rejected the whole
server connection with 'Insufficient parameters' the moment a BotServ bot joined
its assigned channel, splitting services. Add a monotonic membid, matching how
InspIRCd itself sends IJOIN. Validated against a real InspIRCd 4 with production
data: full burst to ENDBURST, all bot IJOINs accepted, no split.
This commit is contained in:
Jean Chevronnet 2026-07-16 16:36:19 +00:00
parent ea7bba4f16
commit d1685f7e53
No known key found for this signature in database
2 changed files with 30 additions and 3 deletions

6
.gitignore vendored
View file

@ -11,3 +11,9 @@ fedserv.db.jsonl
/demo-stop.sh /demo-stop.sh
/test-net /test-net
/test-net-b /test-net-b
# Runtime data/logs
echo.db.jsonl
cutover-echo.log
test-net/echo.db.jsonl
test-net/*.log

View file

@ -11,11 +11,14 @@ pub struct InspIrcd {
password: String, password: String,
protocol: u32, protocol: u32,
ts: u64, ts: u64,
// Monotonic membership id for our own IJOINs. InspIRCd requires a membid on
// IJOIN (`IJOIN <chan> <membid>`); it need only be unique among our members.
membid: u64,
} }
impl InspIrcd { impl InspIrcd {
pub fn new(name: String, description: String, sid: String, password: String, protocol: u32, ts: u64) -> Self { pub fn new(name: String, description: String, sid: String, password: String, protocol: u32, ts: u64) -> Self {
Self { sid, name, description, password, protocol, ts } Self { sid, name, description, password, protocol, ts, membid: 0 }
} }
fn sourced(&self, cmd: String) -> String { fn sourced(&self, cmd: String) -> String {
@ -312,7 +315,12 @@ impl Protocol for InspIrcd {
// SVSNICK <uid> <newnick> <nickts> — the new nick takes the current // SVSNICK <uid> <newnick> <nickts> — the new nick takes the current
// time as its TS so it wins any collision resolution. // time as its TS so it wins any collision resolution.
NetAction::QuitUser { uid, reason } => vec![format!(":{} QUIT :{}", uid, reason)], NetAction::QuitUser { uid, reason } => vec![format!(":{} QUIT :{}", uid, reason)],
NetAction::ServiceJoin { uid, channel } => vec![format!(":{} IJOIN {}", uid, channel)], NetAction::ServiceJoin { uid, channel } => {
// IJOIN needs a membid (`IJOIN <chan> <membid>`); without it the
// ircd rejects the whole link with "Insufficient parameters".
self.membid += 1;
vec![format!(":{} IJOIN {} {}", uid, channel, self.membid)]
}
NetAction::ServicePart { uid, channel } => vec![format!(":{} PART {}", uid, channel)], NetAction::ServicePart { uid, channel } => vec![format!(":{} PART {}", uid, channel)],
// ENCAP the target's server: CHGHOST <uid> <newhost>, to set a vhost. // ENCAP the target's server: CHGHOST <uid> <newhost>, to set a vhost.
NetAction::SetHost { uid, host } => { NetAction::SetHost { uid, host } => {
@ -662,7 +670,20 @@ mod tests {
}).collect(); }).collect();
assert_eq!(joins, ["0IRAAAAAB", "0IRAAAAAC"]); assert_eq!(joins, ["0IRAAAAAB", "0IRAAAAAC"]);
let ev = proto().parse(":0IRAAAAAD IJOIN #chan"); // InspIRCd sends IJOIN with a membid (and optionally ts+modes) — tolerate it.
let ev = proto().parse(":0IRAAAAAD IJOIN #chan 42");
assert!(matches!(ev.as_slice(), [NetEvent::Join { uid, channel, op: false }] if uid == "0IRAAAAAD" && channel == "#chan"), "{ev:?}"); assert!(matches!(ev.as_slice(), [NetEvent::Join { uid, channel, op: false }] if uid == "0IRAAAAAD" && channel == "#chan"), "{ev:?}");
} }
// Our own IJOIN must carry a membid, or the ircd rejects the link with
// "Insufficient parameters" (the cutover crash on a bot-assigned channel).
#[test]
fn service_join_ijoin_includes_membid() {
let mut p = proto();
let a = p.serialize(&NetAction::ServiceJoin { uid: "00DB00000".into(), channel: "#taverne".into() });
assert_eq!(a, vec![":00DB00000 IJOIN #taverne 1".to_string()]);
// membid is monotonic across joins
let b = p.serialize(&NetAction::ServiceJoin { uid: "00DB00000".into(), channel: "#quizz".into() });
assert_eq!(b, vec![":00DB00000 IJOIN #quizz 2".to_string()]);
}
} }