From 62f901bdc8e7d0265e7fc300ed013d47687e7f0e Mon Sep 17 00:00:00 2001 From: reverse Date: Sat, 15 Aug 2026 12:04:09 +0000 Subject: [PATCH] sasl: wrap the s2s sasl relay in encap (client/agent/mode/data) so services drive authenticate, account via metadata --- src/coremods/core_user.rs | 18 +++++++++++----- src/link.rs | 43 +++++++++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 5611c61..81ac1f2 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -284,13 +284,20 @@ impl Command for Authenticate { s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted"); CmdResult::Ok } else if arg.eq_ignore_ascii_case("PLAIN") { + if !have_services { + s.numeric( + uid, + ERR_SASLFAIL, + ":SASL authentication failed (services are not available)", + ); + return CmdResult::Fail; + } if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = Some("PLAIN".to_string()); } - if have_services { - s.sasl_relay(uid, "S PLAIN"); // start the exchange at services - } - s.send(uid, "AUTHENTICATE +".to_string()); + // start the exchange at services; its `C` challenge is relayed + // back to the client as the `AUTHENTICATE +` prompt + s.sasl_relay(uid, "S PLAIN"); CmdResult::Ok } else if arg.eq_ignore_ascii_case("EXTERNAL") { // CertFP: only works on TLS with a client cert; the fingerprint @@ -301,8 +308,9 @@ impl Command for Authenticate { if let Some(u) = s.users.get_mut(&uid) { u.sasl_mech = Some("EXTERNAL".to_string()); } + // services replies with a `C` challenge we relay as the + // client's `AUTHENTICATE +` prompt s.sasl_relay(uid, &format!("S EXTERNAL {fp}")); - s.send(uid, "AUTHENTICATE +".to_string()); CmdResult::Ok } _ => { diff --git a/src/link.rs b/src/link.rs index 88c9d0c..0d848f6 100644 --- a/src/link.rs +++ b/src/link.rs @@ -758,8 +758,11 @@ impl Server { .map(|sv| sv.via) } - /// Relay one SASL step for local client `uid` to the services server: - /// `: SASL `. No-op if SASL services aren't linked. + /// Relay one SASL step for local client `uid` to the services server, wrapped + /// as `: ENCAP SASL * [data...]`. `rest` is + /// the mode letter and its data (e.g. `S PLAIN`, `C `). The agent field is + /// `*` — services accept it, so we needn't track their agent id. No-op with no + /// SASL services linked. pub fn sasl_relay(&self, uid: Uid, rest: &str) { let (Some(via), Some(uuid)) = ( self.sasl_link(), @@ -767,33 +770,43 @@ impl Server { ) else { return; }; - self.link_out(via, format!(":{} SASL {uuid} {rest}", self.sid)); + let mask = self + .servers + .values() + .find(|s| s.via == via) + .map(|s| s.sid.clone()) + .unwrap_or_else(|| "*".to_string()); + self.link_out(via, format!(":{} ENCAP {mask} SASL {uuid} * {rest}", self.sid)); } - /// A SASL message from services: `: SASL …`. + /// A SASL step from services, unwrapped from its ENCAP: params are + /// ` [data...]`. /// `C ` → relay a server challenge to the client as `AUTHENTICATE`; - /// `D S [account]` → success (log in + 900/903); `D ` → fail (904). + /// `D S` → success (the account was set by a preceding `METADATA accountname`, + /// so we emit 900/903 for it); `D ` → fail (904). fn link_sasl(&mut self, from: Uid, msg: &Message) { - if msg.params.len() < 2 { + if msg.params.len() < 3 { return; } - let Some(&uid) = self.uuid_local.get(&msg.params[0]) else { + let client = msg.params[1].clone(); + let Some(&uid) = self.uuid_local.get(&client) else { // not our client — route toward the server that owns them - self.forward_to_target(&msg.params[0], msg, from); + self.forward_to_target(&client, msg, from); return; }; - match msg.params[1].as_str() { + match msg.params[2].as_str() { "C" => { - if let Some(data) = msg.params.get(2) { + if let Some(data) = msg.params.get(3) { self.send(uid, format!("AUTHENTICATE {data}")); } } "D" => { - let ok = msg.params.get(2).map(|t| t == "S").unwrap_or(false); - let account = msg.params.get(3).cloned().unwrap_or_default(); - if ok && !account.is_empty() { - self.set_login(uid, &account); - } + let ok = msg.params.get(3).map(|t| t == "S").unwrap_or(false); + let account = self + .users + .get(&uid) + .and_then(|u| u.account.clone()) + .unwrap_or_default(); self.sasl_done(uid, ok, &account); if let Some(u) = self.users.get_mut(&uid) { u.sasl_mech = None;