sasl: wrap the s2s sasl relay in encap (client/agent/mode/data) so services drive authenticate, account via metadata

This commit is contained in:
Jean Chevronnet 2026-08-15 12:04:09 +00:00
parent 1c8799d785
commit 62f901bdc8
2 changed files with 41 additions and 20 deletions

View file

@ -284,13 +284,20 @@ impl Command for Authenticate {
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted"); s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
CmdResult::Ok CmdResult::Ok
} else if arg.eq_ignore_ascii_case("PLAIN") { } 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) { if let Some(u) = s.users.get_mut(&uid) {
u.sasl_mech = Some("PLAIN".to_string()); u.sasl_mech = Some("PLAIN".to_string());
} }
if have_services { // start the exchange at services; its `C` challenge is relayed
s.sasl_relay(uid, "S PLAIN"); // start the exchange at services // back to the client as the `AUTHENTICATE +` prompt
} s.sasl_relay(uid, "S PLAIN");
s.send(uid, "AUTHENTICATE +".to_string());
CmdResult::Ok CmdResult::Ok
} else if arg.eq_ignore_ascii_case("EXTERNAL") { } else if arg.eq_ignore_ascii_case("EXTERNAL") {
// CertFP: only works on TLS with a client cert; the fingerprint // 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) { if let Some(u) = s.users.get_mut(&uid) {
u.sasl_mech = Some("EXTERNAL".to_string()); 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.sasl_relay(uid, &format!("S EXTERNAL {fp}"));
s.send(uid, "AUTHENTICATE +".to_string());
CmdResult::Ok CmdResult::Ok
} }
_ => { _ => {

View file

@ -758,8 +758,11 @@ impl Server {
.map(|sv| sv.via) .map(|sv| sv.via)
} }
/// Relay one SASL step for local client `uid` to the services server: /// Relay one SASL step for local client `uid` to the services server, wrapped
/// `:<our-sid> SASL <client-uuid> <rest>`. No-op if SASL services aren't linked. /// as `:<our-sid> ENCAP <svc> SASL <client-uuid> * <mode> [data...]`. `rest` is
/// the mode letter and its data (e.g. `S PLAIN`, `C <b64>`). 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) { pub fn sasl_relay(&self, uid: Uid, rest: &str) {
let (Some(via), Some(uuid)) = ( let (Some(via), Some(uuid)) = (
self.sasl_link(), self.sasl_link(),
@ -767,33 +770,43 @@ impl Server {
) else { ) else {
return; 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: `:<svcsid> SASL <client-uuid> <type> …`. /// A SASL step from services, unwrapped from its ENCAP: params are
/// `<agent> <client-uuid> <mode> [data...]`.
/// `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` → success (the account was set by a preceding `METADATA accountname`,
/// so we emit 900/903 for it); `D <other>` → fail (904).
fn link_sasl(&mut self, from: Uid, msg: &Message) { fn link_sasl(&mut self, from: Uid, msg: &Message) {
if msg.params.len() < 2 { if msg.params.len() < 3 {
return; 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 // 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; return;
}; };
match msg.params[1].as_str() { match msg.params[2].as_str() {
"C" => { "C" => {
if let Some(data) = msg.params.get(2) { if let Some(data) = msg.params.get(3) {
self.send(uid, format!("AUTHENTICATE {data}")); self.send(uid, format!("AUTHENTICATE {data}"));
} }
} }
"D" => { "D" => {
let ok = msg.params.get(2).map(|t| t == "S").unwrap_or(false); let ok = msg.params.get(3).map(|t| t == "S").unwrap_or(false);
let account = msg.params.get(3).cloned().unwrap_or_default(); let account = self
if ok && !account.is_empty() { .users
self.set_login(uid, &account); .get(&uid)
} .and_then(|u| u.account.clone())
.unwrap_or_default();
self.sasl_done(uid, ok, &account); self.sasl_done(uid, ok, &account);
if let Some(u) = self.users.get_mut(&uid) { if let Some(u) = self.users.get_mut(&uid) {
u.sasl_mech = None; u.sasl_mech = None;