From 1ab643c87086bd2dd2b86a52cd3eb57169d5d3e8 Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 16 Aug 2026 18:06:03 +0000 Subject: [PATCH] =?UTF-8?q?s2s:=20timestamp=20arbitration=20for=20fjoin=20?= =?UTF-8?q?(lower=20TS=20wins=20=E2=80=94=20strip=20or=20wipe=20losing=20s?= =?UTF-8?q?tatus),=20fmode=20and=20ftopic=20(drop=20changes=20stamped=20ne?= =?UTF-8?q?wer=20than=20ours)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channels.rs | 10 ++++++++++ src/link.rs | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/channels.rs b/src/channels.rs index a2101b5..385d421 100644 --- a/src/channels.rs +++ b/src/channels.rs @@ -118,6 +118,16 @@ impl Member { /// Set/clear a prefix mode by its letter — built-in booleans or, for a /// config-defined letter, the custom-prefix set (used by the S2S mode applier). + /// Drop the standard status modes (q/a/o/h/v) — used when this side loses a + /// channel-timestamp war and every member must be de-statused. + pub fn clear_status(&mut self) { + self.owner = false; + self.admin = false; + self.op = false; + self.halfop = false; + self.voice = false; + } + pub fn set_prefix(&mut self, letter: char, on: bool) { match letter { 'y' => self.oprefix = on, diff --git a/src/link.rs b/src/link.rs index 4671c5e..850a134 100644 --- a/src/link.rs +++ b/src/link.rs @@ -20,7 +20,7 @@ use std::net::{SocketAddr, TcpStream}; use std::collections::HashSet; -use crate::channels::{glob_match, Ban, Channel, Member, Topic}; +use crate::channels::{glob_match, Ban, ChanModes, Channel, Member, Topic}; use crate::message::Message; use crate::server::{now, Server}; use crate::socketengine::OutSink; @@ -2104,6 +2104,44 @@ mod tests { // A server is a service iff its NAME matches the sasl_server or a `uline` config // entry (case-insensitive); `silent` is honoured. + // FJOIN timestamp arbitration: the lower channel TS wins. A member bursted with + // a NEWER TS than ours joins stripped of status; an OLDER TS wipes our side. + #[test] + fn fjoin_ts_arbitration_strips_losing_status() { + use crate::config::Config; + use std::sync::atomic::AtomicU64; + use std::sync::{mpsc, Arc}; + let (tx, _rx) = mpsc::channel(); + let mut s = Server::new(Config::default(), tx, Arc::new(AtomicU64::new(1))); + s.remote_users.insert( + "42SAAAAAA".to_string(), + RemoteUser { + uuid: "42SAAAAAA".to_string(), + nick: "bob".into(), + ident: "b".into(), + host: "h".into(), + realname: "b".into(), + account: None, + ip: String::new(), + modes: String::new(), + sid: "42S".into(), + via: 1, + }, + ); + // we already hold #c at an OLD (winning) TS + s.channels.insert("#c".into(), { + let mut c = Channel::new("#c"); + c.created = 1000; + c + }); + // a peer bursts #c with a NEWER TS, opping bob — bob must join WITHOUT +o + let m = crate::message::parse(":42S FJOIN #c 2000 +nt :o,42SAAAAAA").unwrap(); + s.link_fjoin_recv(1, &m); + let opped = s.channels["#c"].rmembers["42SAAAAAA"].op; + assert!(!opped, "a member bursted with a newer (losing) TS must be de-statused"); + assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept"); + } + #[test] fn uline_recognises_services_server() { use crate::config::Config;