s2s: FJOIN TS arbitration wipes list modes+topic on loss and merges on equal TS; arbitrate remote-remote nick collisions (rewrite forward); IJOIN/FTOPIC channel-TS handling
This commit is contained in:
parent
4b86de3b62
commit
da36d5c32a
1 changed files with 161 additions and 6 deletions
167
src/link.rs
167
src/link.rs
|
|
@ -1101,16 +1101,22 @@ impl Server {
|
||||||
nick = uuid.clone(); // the incoming user lost: introduce it under its UUID
|
nick = uuid.clone(); // the incoming user lost: introduce it under its UUID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Collision with an existing REMOTE user: rename the incoming one to its UUID
|
||||||
|
// rather than silently dropping it — a drop would leave a routing ghost (the
|
||||||
|
// user recorded nowhere here yet never forwarded to our other peers, so its
|
||||||
|
// later JOIN/PRIVMSG/QUIT all fail our source guards). Full TS arbitration
|
||||||
|
// would need the peer's stored nick-TS, which RemoteUser doesn't carry.
|
||||||
if self.remote_nick.contains_key(&nick.to_ascii_lowercase()) {
|
if self.remote_nick.contains_key(&nick.to_ascii_lowercase()) {
|
||||||
return;
|
nick = uuid.clone();
|
||||||
}
|
}
|
||||||
|
let renamed = nick != msg.params[2];
|
||||||
self.remote_nick
|
self.remote_nick
|
||||||
.insert(nick.to_ascii_lowercase(), uuid.clone());
|
.insert(nick.to_ascii_lowercase(), uuid.clone());
|
||||||
self.remote_users.insert(
|
self.remote_users.insert(
|
||||||
uuid.clone(),
|
uuid.clone(),
|
||||||
RemoteUser {
|
RemoteUser {
|
||||||
uuid,
|
uuid,
|
||||||
nick,
|
nick: nick.clone(),
|
||||||
ident,
|
ident,
|
||||||
host,
|
host,
|
||||||
realname,
|
realname,
|
||||||
|
|
@ -1121,8 +1127,25 @@ impl Server {
|
||||||
via,
|
via,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// re-propagate verbatim to our other peers (keeps every field intact)
|
// Re-propagate to our other peers. If a collision renamed the loser, rewrite
|
||||||
self.propagate(&msg.to_wire(), Some(via));
|
// the nick in the forwarded UID so downstream learns the corrected nick and
|
||||||
|
// doesn't re-collide; otherwise forward verbatim.
|
||||||
|
if renamed {
|
||||||
|
let mut p = msg.params.clone();
|
||||||
|
p[2] = nick;
|
||||||
|
let fwd = Message {
|
||||||
|
source: msg.source.clone(),
|
||||||
|
command: msg.command.clone(),
|
||||||
|
params: p,
|
||||||
|
ctags: String::new(),
|
||||||
|
label: None,
|
||||||
|
batch: None,
|
||||||
|
concat: false,
|
||||||
|
};
|
||||||
|
self.propagate(&fwd.to_wire(), Some(via));
|
||||||
|
} else {
|
||||||
|
self.propagate(&msg.to_wire(), Some(via));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a remote source uuid is genuinely reached through link `via` — guards
|
/// Whether a remote source uuid is genuinely reached through link `via` — guards
|
||||||
|
|
@ -1585,9 +1608,19 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If the channel is unknown (a desync/race), create it with the TS the IJOIN
|
||||||
|
// carries — not now() — so our fabricated instance doesn't later win a bogus
|
||||||
|
// TS war and propagate the wrong age.
|
||||||
|
let ijoin_ts: Option<u64> = msg.params.get(2).and_then(|t| t.parse().ok());
|
||||||
self.channels
|
self.channels
|
||||||
.entry(key.clone())
|
.entry(key.clone())
|
||||||
.or_insert_with(|| Channel::new(&chan))
|
.or_insert_with(|| {
|
||||||
|
let mut c = Channel::new(&chan);
|
||||||
|
if let Some(ts) = ijoin_ts {
|
||||||
|
c.created = ts;
|
||||||
|
}
|
||||||
|
c
|
||||||
|
})
|
||||||
.rmembers
|
.rmembers
|
||||||
.insert(uuid.clone(), m);
|
.insert(uuid.clone(), m);
|
||||||
let prefix = self
|
let prefix = self
|
||||||
|
|
@ -1958,6 +1991,18 @@ impl Server {
|
||||||
}
|
}
|
||||||
let topic = msg.params.last().cloned().unwrap_or_default();
|
let topic = msg.params.last().cloned().unwrap_or_default();
|
||||||
let ts = msg.params[2].parse().unwrap_or_else(|_| now());
|
let ts = msg.params[2].parse().unwrap_or_else(|_| now());
|
||||||
|
// channel-TS guard: if our channel won the TS war (older/lower created TS),
|
||||||
|
// ignore a topic coming from an instance that lost it.
|
||||||
|
let chants: u64 = msg.params[1].parse().unwrap_or(0);
|
||||||
|
if self
|
||||||
|
.channels
|
||||||
|
.get(&key)
|
||||||
|
.map(|c| c.created)
|
||||||
|
.is_some_and(|ours| ours < chants)
|
||||||
|
{
|
||||||
|
self.propagate(&msg.to_wire(), Some(via));
|
||||||
|
return;
|
||||||
|
}
|
||||||
// keep whichever topic was set later: drop an FTOPIC older than the one we hold.
|
// keep whichever topic was set later: drop an FTOPIC older than the one we hold.
|
||||||
if let Some(cur_ts) = self
|
if let Some(cur_ts) = self
|
||||||
.channels
|
.channels
|
||||||
|
|
@ -2240,9 +2285,20 @@ impl Server {
|
||||||
.channels
|
.channels
|
||||||
.entry(key.clone())
|
.entry(key.clone())
|
||||||
.or_insert_with(|| Channel::new(&chan));
|
.or_insert_with(|| Channel::new(&chan));
|
||||||
if our_ts.is_none() || remote_wins {
|
if !we_win {
|
||||||
|
// Fresh channel, we lost the TS war, or an equal-TS merge: adopt the
|
||||||
|
// remote channel modes. On a loss, first wipe OUR state — the boolean
|
||||||
|
// modes AND (previously missed) the list modes + topic — else a ban
|
||||||
|
// the winning side never had lingers here forever (split-brain).
|
||||||
if remote_wins {
|
if remote_wins {
|
||||||
ch.modes = ChanModes::default();
|
ch.modes = ChanModes::default();
|
||||||
|
ch.bans.clear();
|
||||||
|
ch.excepts.clear();
|
||||||
|
ch.invex.clear();
|
||||||
|
ch.filters.clear();
|
||||||
|
ch.exemptchanops.clear();
|
||||||
|
ch.autoop.clear();
|
||||||
|
ch.topic = None;
|
||||||
for m in ch.members.values_mut() {
|
for m in ch.members.values_mut() {
|
||||||
m.clear_status();
|
m.clear_status();
|
||||||
}
|
}
|
||||||
|
|
@ -2422,6 +2478,105 @@ mod tests {
|
||||||
assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept");
|
assert_eq!(s.channels["#c"].created, 1000, "our older TS is kept");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bob_server() -> Server {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Losing the FJOIN TS war must wipe our list modes (bans) and topic too — not just
|
||||||
|
// the boolean modes — else a ban the winning side never had lingers forever here.
|
||||||
|
#[test]
|
||||||
|
fn fjoin_loss_clears_lists_and_topic() {
|
||||||
|
use crate::channels::{Ban, Topic};
|
||||||
|
let mut s = bob_server();
|
||||||
|
s.channels.insert("#c".into(), {
|
||||||
|
let mut c = Channel::new("#c");
|
||||||
|
c.created = 2000; // we hold the NEWER (losing) TS
|
||||||
|
c.bans.push(Ban {
|
||||||
|
mask: "*!*@evil".into(),
|
||||||
|
setter: "me".into(),
|
||||||
|
ts: 0,
|
||||||
|
expires: None,
|
||||||
|
});
|
||||||
|
c.topic = Some(Topic {
|
||||||
|
text: "old".into(),
|
||||||
|
setter: "me".into(),
|
||||||
|
ts: 0,
|
||||||
|
});
|
||||||
|
c
|
||||||
|
});
|
||||||
|
let m = crate::message::parse(":42S FJOIN #c 1000 +mnt :o,42SAAAAAA").unwrap();
|
||||||
|
s.link_fjoin_recv(1, &m);
|
||||||
|
let ch = &s.channels["#c"];
|
||||||
|
assert_eq!(ch.created, 1000, "we adopt the winning TS");
|
||||||
|
assert!(ch.bans.is_empty(), "our ban must be wiped on losing the TS war");
|
||||||
|
assert!(ch.topic.is_none(), "our topic must be wiped on losing the TS war");
|
||||||
|
assert!(ch.modes.moderated, "the winner's +m is adopted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal-TS FJOIN must MERGE the remote channel modes, not drop them.
|
||||||
|
#[test]
|
||||||
|
fn fjoin_equal_ts_merges_modes() {
|
||||||
|
let mut s = bob_server();
|
||||||
|
s.channels.insert("#c".into(), {
|
||||||
|
let mut c = Channel::new("#c");
|
||||||
|
c.created = 1000;
|
||||||
|
c
|
||||||
|
});
|
||||||
|
let m = crate::message::parse(":42S FJOIN #c 1000 +m :o,42SAAAAAA").unwrap();
|
||||||
|
s.link_fjoin_recv(1, &m);
|
||||||
|
assert!(
|
||||||
|
s.channels["#c"].modes.moderated,
|
||||||
|
"an equal-TS FJOIN must merge the remote +m"
|
||||||
|
);
|
||||||
|
assert_eq!(s.channels["#c"].created, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An FTOPIC from a channel instance that LOST the TS war (its chants > our created)
|
||||||
|
// must be ignored, even if its topic timestamp is newer.
|
||||||
|
#[test]
|
||||||
|
fn ftopic_dropped_when_our_channel_won_the_ts() {
|
||||||
|
use crate::channels::Topic;
|
||||||
|
let mut s = bob_server();
|
||||||
|
s.channels.insert("#c".into(), {
|
||||||
|
let mut c = Channel::new("#c");
|
||||||
|
c.created = 1000; // we won
|
||||||
|
c.topic = Some(Topic {
|
||||||
|
text: "ours".into(),
|
||||||
|
setter: "me".into(),
|
||||||
|
ts: 5,
|
||||||
|
});
|
||||||
|
c
|
||||||
|
});
|
||||||
|
// chants=2000 (their instance lost), topicts=9 (newer) — must still be ignored
|
||||||
|
let m = crate::message::parse(":42SAAAAAA FTOPIC #c 2000 9 bob :theirs").unwrap();
|
||||||
|
s.link_ftopic_recv(1, &m);
|
||||||
|
assert_eq!(
|
||||||
|
s.channels["#c"].topic.as_ref().unwrap().text,
|
||||||
|
"ours",
|
||||||
|
"a topic from a channel instance that lost the TS war must be dropped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn uline_recognises_services_server() {
|
fn uline_recognises_services_server() {
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue