sasl relay over s2s: authenticate plain forwarded to services (sasl_server), 900/903 on success; fails clean with no services
This commit is contained in:
parent
21f3bbd0d7
commit
fa84ba9f53
5 changed files with 125 additions and 15 deletions
|
|
@ -74,6 +74,7 @@ pub struct Config {
|
|||
pub dnsbl_zones: Vec<String>, // DNS blocklist zones to check on connect
|
||||
pub dnsbl_action: String, // mark | kline | gline | zline (on a hit)
|
||||
pub dnsbl_reason: String, // ban reason for a DNSBL hit
|
||||
pub sasl_server: String, // linked services server that handles SASL ("" = none)
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -100,6 +101,7 @@ impl Default for Config {
|
|||
dnsbl_zones: Vec::new(),
|
||||
dnsbl_action: "mark".to_string(),
|
||||
dnsbl_reason: "Your host is listed in a DNS blocklist".to_string(),
|
||||
sasl_server: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -232,6 +234,7 @@ impl Config {
|
|||
}
|
||||
"dnsbl_action" => c.dnsbl_action = v.to_ascii_lowercase(),
|
||||
"dnsbl_reason" => c.dnsbl_reason = v.to_string(),
|
||||
"sasl_server" | "sasl_target" => c.sasl_server = v.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ impl Command for Rehash {
|
|||
s.dnsbl_zones = fresh.dnsbl_zones;
|
||||
s.dnsbl_action = fresh.dnsbl_action;
|
||||
s.dnsbl_reason = fresh.dnsbl_reason;
|
||||
s.sasl_server = fresh.sasl_server;
|
||||
s.announce("Server configuration reloaded.");
|
||||
s.numeric(uid, RPL_REHASHING, &format!("{path} :Rehashing"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,18 +159,24 @@ impl Command for Authenticate {
|
|||
}
|
||||
let arg = ¶ms[0];
|
||||
let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone());
|
||||
// SASL is relayed to a linked services server (see `Server::sasl_relay`);
|
||||
// with none configured/linked it fails cleanly, exactly like InspIRCd.
|
||||
let have_services = s.sasl_link().is_some();
|
||||
match mech {
|
||||
// step 1 — the client picks a mechanism
|
||||
None => {
|
||||
if arg.eq_ignore_ascii_case("PLAIN") {
|
||||
if arg == "*" {
|
||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||
CmdResult::Ok
|
||||
} else if arg.eq_ignore_ascii_case("PLAIN") {
|
||||
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());
|
||||
CmdResult::Ok
|
||||
} else if arg == "*" {
|
||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||
CmdResult::Ok
|
||||
} else {
|
||||
s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms");
|
||||
s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism");
|
||||
|
|
@ -179,19 +185,32 @@ impl Command for Authenticate {
|
|||
}
|
||||
// step 2 — the client sends the base64 payload (or aborts with `*`)
|
||||
Some(_) => {
|
||||
if arg == "*" {
|
||||
if have_services {
|
||||
s.sasl_relay(uid, "D A");
|
||||
}
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = None;
|
||||
}
|
||||
if arg == "*" {
|
||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||
return CmdResult::Ok;
|
||||
}
|
||||
if arg.len() > 400 {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = None;
|
||||
}
|
||||
s.numeric(uid, ERR_SASLTOOLONG, ":SASL message too long");
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// base64(authzid \0 authcid \0 passwd) — would be relayed to services
|
||||
let _creds = openssl::base64::decode_block(arg).unwrap_or_default();
|
||||
if have_services {
|
||||
// relay the response; the verdict (900/903 or 904) comes back
|
||||
// over S2S in `Server::link_sasl`, which clears `sasl_mech`.
|
||||
s.sasl_relay(uid, &format!("C {arg}"));
|
||||
CmdResult::Ok
|
||||
} else {
|
||||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.sasl_mech = None;
|
||||
}
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_SASLFAIL,
|
||||
|
|
@ -202,6 +221,7 @@ impl Command for Authenticate {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SETNAME — change your realname (IRCv3). Broadcast to `setname`-capable peers.
|
||||
struct SetName;
|
||||
|
|
|
|||
84
src/link.rs
84
src/link.rs
|
|
@ -171,6 +171,7 @@ impl Server {
|
|||
"SVSLOGOUT" if registered => self.link_svslogout(msg),
|
||||
"ENCAP" if registered => self.link_encap(uid, msg),
|
||||
"METADATA" if registered => self.link_metadata(msg),
|
||||
"SASL" if registered => self.link_sasl(msg),
|
||||
"BURST" => {
|
||||
if let Some(l) = self.links.get_mut(&uid) {
|
||||
l.bursting = true;
|
||||
|
|
@ -520,6 +521,89 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
// --- SASL relay (client AUTHENTICATE ⇄ services) --------------------------
|
||||
|
||||
/// The local link toward the configured SASL services server, if connected.
|
||||
pub fn sasl_link(&self) -> Option<Uid> {
|
||||
if self.sasl_server.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.servers
|
||||
.values()
|
||||
.find(|sv| sv.name == self.sasl_server)
|
||||
.map(|sv| sv.via)
|
||||
}
|
||||
|
||||
/// Relay one SASL step for local client `uid` to the services server:
|
||||
/// `:<our-sid> SASL <client-uuid> <rest>`. No-op if SASL services aren't linked.
|
||||
pub fn sasl_relay(&self, uid: Uid, rest: &str) {
|
||||
let (Some(via), Some(uuid)) = (
|
||||
self.sasl_link(),
|
||||
self.users.get(&uid).map(|u| u.uuid.clone()),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.link_out(via, format!(":{} SASL {uuid} {rest}", self.sid));
|
||||
}
|
||||
|
||||
/// A SASL message from services: `:<svcsid> SASL <client-uuid> <type> …`.
|
||||
/// `C <data>` → relay a server challenge to the client as `AUTHENTICATE`;
|
||||
/// `D S [account]` → success (log in + 900/903); `D <other>` → fail (904).
|
||||
fn link_sasl(&mut self, msg: &Message) {
|
||||
if msg.params.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let Some(&uid) = self.uuid_local.get(&msg.params[0]) else {
|
||||
return; // not one of our clients
|
||||
};
|
||||
match msg.params[1].as_str() {
|
||||
"C" => {
|
||||
if let Some(data) = msg.params.get(2) {
|
||||
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);
|
||||
}
|
||||
self.sasl_done(uid, ok, &account);
|
||||
if let Some(u) = self.users.get_mut(&uid) {
|
||||
u.sasl_mech = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the SASL outcome to the client: 900 + 903 on success, 904 on failure.
|
||||
fn sasl_done(&self, uid: Uid, success: bool, account: &str) {
|
||||
if success {
|
||||
let mask = self
|
||||
.users
|
||||
.get(&uid)
|
||||
.map(|u| u.prefix())
|
||||
.unwrap_or_else(|| "*".to_string());
|
||||
self.numeric(
|
||||
uid,
|
||||
crate::numeric::RPL_LOGGEDIN,
|
||||
&format!("{mask} {account} :You are now logged in as {account}"),
|
||||
);
|
||||
self.numeric(
|
||||
uid,
|
||||
crate::numeric::RPL_SASLSUCCESS,
|
||||
":SASL authentication successful",
|
||||
);
|
||||
} else {
|
||||
self.numeric(
|
||||
uid,
|
||||
crate::numeric::ERR_SASLFAIL,
|
||||
":SASL authentication failed",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- inbound S2S records --------------------------------------------------
|
||||
|
||||
fn link_uid_recv(&mut self, via: Uid, msg: &Message) {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ pub struct Server {
|
|||
pub dnsbl_zones: Vec<String>, // DNS blocklist zones checked on connect
|
||||
pub dnsbl_action: String, // mark | kline | gline | zline
|
||||
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
||||
pub sasl_server: String, // services server that handles SASL
|
||||
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +143,7 @@ impl Server {
|
|||
dnsbl_zones: cfg.dnsbl_zones,
|
||||
dnsbl_action: cfg.dnsbl_action,
|
||||
dnsbl_reason: cfg.dnsbl_reason,
|
||||
sasl_server: cfg.sasl_server,
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue