multi-hop s2s: forward svs*/metadata/sasl to the target's server, route/flood encap by mask (Message::to_wire)
This commit is contained in:
parent
b92101616b
commit
d9433eb692
2 changed files with 146 additions and 56 deletions
143
src/link.rs
143
src/link.rs
|
|
@ -162,16 +162,16 @@ impl Server {
|
||||||
"MODE" | "FMODE" if registered => self.link_mode_recv(uid, msg),
|
"MODE" | "FMODE" if registered => self.link_mode_recv(uid, msg),
|
||||||
"FJOIN" if registered => self.link_fjoin_recv(uid, msg),
|
"FJOIN" if registered => self.link_fjoin_recv(uid, msg),
|
||||||
// services (SVS*) enforcement + account login, driven by a linked
|
// services (SVS*) enforcement + account login, driven by a linked
|
||||||
// services pseudoserver
|
// services pseudoserver (forwarded on if the target is on another server)
|
||||||
"SVSNICK" if registered => self.link_svsnick(msg),
|
"SVSNICK" if registered => self.link_svsnick(uid, msg),
|
||||||
"SVSJOIN" if registered => self.link_svsjoin(msg),
|
"SVSJOIN" if registered => self.link_svsjoin(uid, msg),
|
||||||
"SVSPART" if registered => self.link_svspart(msg),
|
"SVSPART" if registered => self.link_svspart(uid, msg),
|
||||||
"SVSMODE" if registered => self.link_svsmode(msg),
|
"SVSMODE" if registered => self.link_svsmode(uid, msg),
|
||||||
"SVSLOGIN" if registered => self.link_svslogin(msg),
|
"SVSLOGIN" if registered => self.link_svslogin(uid, msg),
|
||||||
"SVSLOGOUT" if registered => self.link_svslogout(msg),
|
"SVSLOGOUT" if registered => self.link_svslogout(uid, msg),
|
||||||
"ENCAP" if registered => self.link_encap(uid, msg),
|
"ENCAP" if registered => self.link_encap(uid, msg),
|
||||||
"METADATA" if registered => self.link_metadata(msg),
|
"METADATA" if registered => self.link_metadata(uid, msg),
|
||||||
"SASL" if registered => self.link_sasl(msg),
|
"SASL" if registered => self.link_sasl(uid, msg),
|
||||||
"BURST" => {
|
"BURST" => {
|
||||||
if let Some(l) = self.links.get_mut(&uid) {
|
if let Some(l) = self.links.get_mut(&uid) {
|
||||||
l.bursting = true;
|
l.bursting = true;
|
||||||
|
|
@ -387,11 +387,10 @@ impl Server {
|
||||||
|
|
||||||
// --- services (SVS*) over S2S ---------------------------------------------
|
// --- services (SVS*) over S2S ---------------------------------------------
|
||||||
// A linked services pseudoserver enforces nick/join/part/mode and account
|
// A linked services pseudoserver enforces nick/join/part/mode and account
|
||||||
// login on our users with these. Authority is the link itself — only
|
// login with these. Authority is the link itself — only registered peers
|
||||||
// registered peers reach `on_link`. Targets are network UUIDs or nicks; we
|
// reach `on_link`. Targets are network UUIDs or nicks: a locally-present
|
||||||
// act only on locally-present targets (multi-hop forwarding is still TODO,
|
// target is acted on directly (reusing the same primitive as the local SVS*
|
||||||
// like the SVSLOGIN/SASL note in the module header). Each reuses the same
|
// command); a target on another server is forwarded one hop toward it.
|
||||||
// primitive as the local SVS* command, so behaviour and propagation match.
|
|
||||||
|
|
||||||
/// Resolve an S2S target token (network UUID or nickname) to a local user.
|
/// Resolve an S2S target token (network UUID or nickname) to a local user.
|
||||||
fn link_local_target(&self, target: &str) -> Option<Uid> {
|
fn link_local_target(&self, target: &str) -> Option<Uid> {
|
||||||
|
|
@ -401,13 +400,37 @@ impl Server {
|
||||||
.or_else(|| self.find_nick(target))
|
.or_else(|| self.find_nick(target))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The local link toward the server that owns `target` (a UUID or nick), if the
|
||||||
|
/// user is remote and reachable. `None` when the target is local or unknown.
|
||||||
|
fn link_toward(&self, target: &str) -> Option<Uid> {
|
||||||
|
let uuid = if self.remote_users.contains_key(target) {
|
||||||
|
target.to_string()
|
||||||
|
} else {
|
||||||
|
self.remote_nick.get(&target.to_ascii_lowercase())?.clone()
|
||||||
|
};
|
||||||
|
self.remote_users.get(&uuid).map(|ru| ru.via)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route a services command aimed at a non-local `target` one hop onward.
|
||||||
|
/// Returns true if it was forwarded (never back down the link it came from).
|
||||||
|
fn forward_to_target(&self, target: &str, msg: &Message, from: Uid) -> bool {
|
||||||
|
match self.link_toward(target) {
|
||||||
|
Some(v) if v != from => {
|
||||||
|
self.link_out(v, msg.to_wire());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `:src SVSNICK <target> <newnick> [ts]` — force a nick change.
|
/// `:src SVSNICK <target> <newnick> [ts]` — force a nick change.
|
||||||
fn link_svsnick(&mut self, msg: &Message) {
|
fn link_svsnick(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (target, newnick) = (&msg.params[0], &msg.params[1]);
|
let (target, newnick) = (&msg.params[0], &msg.params[1]);
|
||||||
let Some(tuid) = self.link_local_target(target) else {
|
let Some(tuid) = self.link_local_target(target) else {
|
||||||
|
self.forward_to_target(target, msg, from);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !valid_nick(newnick)
|
if !valid_nick(newnick)
|
||||||
|
|
@ -420,21 +443,25 @@ impl Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src SVSJOIN <target> <channel>` — force a join.
|
/// `:src SVSJOIN <target> <channel>` — force a join.
|
||||||
fn link_svsjoin(&mut self, msg: &Message) {
|
fn link_svsjoin(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(tuid) = self.link_local_target(&msg.params[0]) {
|
match self.link_local_target(&msg.params[0]) {
|
||||||
self.join(tuid, &msg.params[1], None);
|
Some(tuid) => self.join(tuid, &msg.params[1], None),
|
||||||
|
None => {
|
||||||
|
self.forward_to_target(&msg.params[0], msg, from);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src SVSPART <target> <channel> [reason]` — force a part.
|
/// `:src SVSPART <target> <channel> [reason]` — force a part.
|
||||||
fn link_svspart(&mut self, msg: &Message) {
|
fn link_svspart(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(tuid) = self.link_local_target(&msg.params[0]) {
|
match self.link_local_target(&msg.params[0]) {
|
||||||
|
Some(tuid) => {
|
||||||
let reason = msg
|
let reason = msg
|
||||||
.params
|
.params
|
||||||
.get(2)
|
.get(2)
|
||||||
|
|
@ -442,26 +469,36 @@ impl Server {
|
||||||
.unwrap_or_else(|| "Services forced part".to_string());
|
.unwrap_or_else(|| "Services forced part".to_string());
|
||||||
self.force_part(tuid, &msg.params[1], &reason);
|
self.force_part(tuid, &msg.params[1], &reason);
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
self.forward_to_target(&msg.params[0], msg, from);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src SVSMODE <target> <modes>` — set a user's modes (e.g. `+r`). Channel
|
/// `:src SVSMODE <target> <modes>` — set a user's modes (e.g. `+r`). Channel
|
||||||
/// modes travel as (F)MODE, so a `#` target is ignored here.
|
/// modes travel as (F)MODE, so a `#` target is ignored here.
|
||||||
fn link_svsmode(&mut self, msg: &Message) {
|
fn link_svsmode(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 || msg.params[0].starts_with('#') {
|
if msg.params.len() < 2 || msg.params[0].starts_with('#') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(tuid) = self.link_local_target(&msg.params[0]) {
|
match self.link_local_target(&msg.params[0]) {
|
||||||
crate::coremods::core_mode::svs_set_user_modes(self, tuid, &msg.params[1]);
|
Some(tuid) => {
|
||||||
|
crate::coremods::core_mode::svs_set_user_modes(self, tuid, &msg.params[1])
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.forward_to_target(&msg.params[0], msg, from);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src SVSLOGIN <target> <account>` — log a user into (or, with `*`/`0`, out
|
/// `:src SVSLOGIN <target> <account>` — log a user into (or, with `*`/`0`, out
|
||||||
/// of) a services account.
|
/// of) a services account.
|
||||||
fn link_svslogin(&mut self, msg: &Message) {
|
fn link_svslogin(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(tuid) = self.link_local_target(&msg.params[0]) {
|
match self.link_local_target(&msg.params[0]) {
|
||||||
|
Some(tuid) => {
|
||||||
let account = &msg.params[1];
|
let account = &msg.params[1];
|
||||||
if account == "*" || account == "0" {
|
if account == "*" || account == "0" {
|
||||||
self.logout(tuid);
|
self.logout(tuid);
|
||||||
|
|
@ -469,28 +506,48 @@ impl Server {
|
||||||
self.set_login(tuid, account);
|
self.set_login(tuid, account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
self.forward_to_target(&msg.params[0], msg, from);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src SVSLOGOUT <target>` — log a user out of their account.
|
/// `:src SVSLOGOUT <target>` — log a user out of their account.
|
||||||
fn link_svslogout(&mut self, msg: &Message) {
|
fn link_svslogout(&mut self, from: Uid, msg: &Message) {
|
||||||
if let Some(t) = msg.params.first() {
|
if let Some(t) = msg.params.first() {
|
||||||
if let Some(tuid) = self.link_local_target(t) {
|
match self.link_local_target(t) {
|
||||||
self.logout(tuid);
|
Some(tuid) => self.logout(tuid),
|
||||||
|
None => {
|
||||||
|
self.forward_to_target(t, msg, from);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src ENCAP <servermask> <subcommand> [params...]` — a command encapsulated
|
/// `:src ENCAP <servermask> <subcommand> [params...]` — a command encapsulated
|
||||||
/// for specific server(s); services wrap SVS*/SASL this way. If the mask
|
/// for specific server(s); services wrap SVS*/SASL this way. If the mask
|
||||||
/// matches us, unwrap and dispatch the subcommand as if it arrived directly.
|
/// matches us we unwrap and dispatch the subcommand; a `*` mask is also flooded
|
||||||
/// (Multi-hop forwarding to other servers is still TODO.)
|
/// onward (minus the origin), and a specific other-server mask is routed to it.
|
||||||
fn link_encap(&mut self, via: Uid, msg: &Message) {
|
fn link_encap(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mask = msg.params[0].as_str();
|
let mask = msg.params[0].as_str();
|
||||||
let for_us = mask == "*" || mask == self.sid || glob_match(mask, &self.name);
|
if mask == "*" {
|
||||||
if for_us {
|
self.encap_unwrap(from, msg);
|
||||||
|
self.propagate(&msg.to_wire(), Some(from)); // spanning-tree flood
|
||||||
|
} else if mask == self.sid || glob_match(mask, &self.name) {
|
||||||
|
self.encap_unwrap(from, msg);
|
||||||
|
} else if let Some(v) = self.server_link(mask) {
|
||||||
|
if v != from {
|
||||||
|
self.link_out(v, msg.to_wire());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unwrap an ENCAP whose mask targets us and dispatch the inner subcommand as
|
||||||
|
/// if it had arrived directly on the link.
|
||||||
|
fn encap_unwrap(&mut self, from: Uid, msg: &Message) {
|
||||||
let sub = Message {
|
let sub = Message {
|
||||||
source: msg.source.clone(),
|
source: msg.source.clone(),
|
||||||
command: msg.params[1].to_ascii_uppercase(),
|
command: msg.params[1].to_ascii_uppercase(),
|
||||||
|
|
@ -498,19 +555,27 @@ impl Server {
|
||||||
ctags: String::new(),
|
ctags: String::new(),
|
||||||
label: None,
|
label: None,
|
||||||
};
|
};
|
||||||
self.on_link(via, &sub);
|
self.on_link(from, &sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The local link toward a server named/ided by `mask` (exact SID or name).
|
||||||
|
fn server_link(&self, mask: &str) -> Option<Uid> {
|
||||||
|
self.servers
|
||||||
|
.values()
|
||||||
|
.find(|sv| sv.sid == mask || sv.name.eq_ignore_ascii_case(mask))
|
||||||
|
.map(|sv| sv.via)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `:src METADATA <target> <key> :<value>` — services sync metadata onto a
|
/// `:src METADATA <target> <key> :<value>` — services sync metadata onto a
|
||||||
/// user. We apply `accountname` (login/logout); other keys are accepted and
|
/// user. We apply `accountname` (login/logout); other keys are accepted and
|
||||||
/// ignored for now.
|
/// ignored for now. Forwarded on if the target is remote.
|
||||||
fn link_metadata(&mut self, msg: &Message) {
|
fn link_metadata(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 3 {
|
if msg.params.len() < 3 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let (target, key, value) = (&msg.params[0], &msg.params[1], &msg.params[2]);
|
let (target, key, value) = (&msg.params[0], &msg.params[1], &msg.params[2]);
|
||||||
let Some(tuid) = self.link_local_target(target) else {
|
let Some(tuid) = self.link_local_target(target) else {
|
||||||
|
self.forward_to_target(target, msg, from);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if key == "accountname" {
|
if key == "accountname" {
|
||||||
|
|
@ -550,12 +615,14 @@ impl Server {
|
||||||
/// A SASL message from services: `:<svcsid> SASL <client-uuid> <type> …`.
|
/// A SASL message from services: `:<svcsid> SASL <client-uuid> <type> …`.
|
||||||
/// `C <data>` → relay a server challenge to the client as `AUTHENTICATE`;
|
/// `C <data>` → relay a server challenge to the client as `AUTHENTICATE`;
|
||||||
/// `D S [account]` → success (log in + 900/903); `D <other>` → fail (904).
|
/// `D S [account]` → success (log in + 900/903); `D <other>` → fail (904).
|
||||||
fn link_sasl(&mut self, msg: &Message) {
|
fn link_sasl(&mut self, from: Uid, msg: &Message) {
|
||||||
if msg.params.len() < 2 {
|
if msg.params.len() < 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(&uid) = self.uuid_local.get(&msg.params[0]) else {
|
let Some(&uid) = self.uuid_local.get(&msg.params[0]) else {
|
||||||
return; // not one of our clients
|
// not our client — route toward the server that owns them
|
||||||
|
self.forward_to_target(&msg.params[0], msg, from);
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
match msg.params[1].as_str() {
|
match msg.params[1].as_str() {
|
||||||
"C" => {
|
"C" => {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,29 @@ pub struct Message {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Re-serialise to a wire line, for forwarding across links. The last param is
|
||||||
|
/// emitted as a trailing `:param` when it's empty, has a space, or starts `:`.
|
||||||
|
pub fn to_wire(&self) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
if let Some(src) = &self.source {
|
||||||
|
out.push(':');
|
||||||
|
out.push_str(src);
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
out.push_str(&self.command);
|
||||||
|
let n = self.params.len();
|
||||||
|
for (i, p) in self.params.iter().enumerate() {
|
||||||
|
out.push(' ');
|
||||||
|
if i + 1 == n && (p.is_empty() || p.contains(' ') || p.starts_with(':')) {
|
||||||
|
out.push(':');
|
||||||
|
}
|
||||||
|
out.push_str(p);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse one wire line. Returns `None` for an empty/garbage line.
|
/// Parse one wire line. Returns `None` for an empty/garbage line.
|
||||||
pub fn parse(line: &str) -> Option<Message> {
|
pub fn parse(line: &str) -> Option<Message> {
|
||||||
let mut rest = line.trim_start();
|
let mut rest = line.trim_start();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue