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_zones: Vec<String>, // DNS blocklist zones to check on connect
|
||||||
pub dnsbl_action: String, // mark | kline | gline | zline (on a hit)
|
pub dnsbl_action: String, // mark | kline | gline | zline (on a hit)
|
||||||
pub dnsbl_reason: String, // ban reason for a DNSBL 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 {
|
impl Default for Config {
|
||||||
|
|
@ -100,6 +101,7 @@ impl Default for Config {
|
||||||
dnsbl_zones: Vec::new(),
|
dnsbl_zones: Vec::new(),
|
||||||
dnsbl_action: "mark".to_string(),
|
dnsbl_action: "mark".to_string(),
|
||||||
dnsbl_reason: "Your host is listed in a DNS blocklist".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_action" => c.dnsbl_action = v.to_ascii_lowercase(),
|
||||||
"dnsbl_reason" => c.dnsbl_reason = v.to_string(),
|
"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_zones = fresh.dnsbl_zones;
|
||||||
s.dnsbl_action = fresh.dnsbl_action;
|
s.dnsbl_action = fresh.dnsbl_action;
|
||||||
s.dnsbl_reason = fresh.dnsbl_reason;
|
s.dnsbl_reason = fresh.dnsbl_reason;
|
||||||
|
s.sasl_server = fresh.sasl_server;
|
||||||
s.announce("Server configuration reloaded.");
|
s.announce("Server configuration reloaded.");
|
||||||
s.numeric(uid, RPL_REHASHING, &format!("{path} :Rehashing"));
|
s.numeric(uid, RPL_REHASHING, &format!("{path} :Rehashing"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,18 +159,24 @@ impl Command for Authenticate {
|
||||||
}
|
}
|
||||||
let arg = ¶ms[0];
|
let arg = ¶ms[0];
|
||||||
let mech = s.users.get(&uid).and_then(|u| u.sasl_mech.clone());
|
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 {
|
match mech {
|
||||||
// step 1 — the client picks a mechanism
|
// step 1 — the client picks a mechanism
|
||||||
None => {
|
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) {
|
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 {
|
||||||
|
s.sasl_relay(uid, "S PLAIN"); // start the exchange at services
|
||||||
|
}
|
||||||
s.send(uid, "AUTHENTICATE +".to_string());
|
s.send(uid, "AUTHENTICATE +".to_string());
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
} else if arg == "*" {
|
|
||||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
|
||||||
CmdResult::Ok
|
|
||||||
} else {
|
} else {
|
||||||
s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms");
|
s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms");
|
||||||
s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism");
|
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 `*`)
|
// step 2 — the client sends the base64 payload (or aborts with `*`)
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
|
if arg == "*" {
|
||||||
|
if have_services {
|
||||||
|
s.sasl_relay(uid, "D A");
|
||||||
|
}
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.sasl_mech = None;
|
u.sasl_mech = None;
|
||||||
}
|
}
|
||||||
if arg == "*" {
|
|
||||||
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted");
|
||||||
return CmdResult::Ok;
|
return CmdResult::Ok;
|
||||||
}
|
}
|
||||||
if arg.len() > 400 {
|
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");
|
s.numeric(uid, ERR_SASLTOOLONG, ":SASL message too long");
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
// base64(authzid \0 authcid \0 passwd) — would be relayed to services
|
if have_services {
|
||||||
let _creds = openssl::base64::decode_block(arg).unwrap_or_default();
|
// 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(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_SASLFAIL,
|
ERR_SASLFAIL,
|
||||||
|
|
@ -201,6 +220,7 @@ impl Command for Authenticate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SETNAME — change your realname (IRCv3). Broadcast to `setname`-capable peers.
|
/// SETNAME — change your realname (IRCv3). Broadcast to `setname`-capable peers.
|
||||||
|
|
|
||||||
84
src/link.rs
84
src/link.rs
|
|
@ -171,6 +171,7 @@ impl Server {
|
||||||
"SVSLOGOUT" if registered => self.link_svslogout(msg),
|
"SVSLOGOUT" if registered => self.link_svslogout(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(msg),
|
||||||
|
"SASL" if registered => self.link_sasl(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;
|
||||||
|
|
@ -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 --------------------------------------------------
|
// --- inbound S2S records --------------------------------------------------
|
||||||
|
|
||||||
fn link_uid_recv(&mut self, via: Uid, msg: &Message) {
|
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_zones: Vec<String>, // DNS blocklist zones checked on connect
|
||||||
pub dnsbl_action: String, // mark | kline | gline | zline
|
pub dnsbl_action: String, // mark | kline | gline | zline
|
||||||
pub dnsbl_reason: String, // ban reason on a DNSBL hit
|
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)
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +143,7 @@ impl Server {
|
||||||
dnsbl_zones: cfg.dnsbl_zones,
|
dnsbl_zones: cfg.dnsbl_zones,
|
||||||
dnsbl_action: cfg.dnsbl_action,
|
dnsbl_action: cfg.dnsbl_action,
|
||||||
dnsbl_reason: cfg.dnsbl_reason,
|
dnsbl_reason: cfg.dnsbl_reason,
|
||||||
|
sasl_server: cfg.sasl_server,
|
||||||
event_tx,
|
event_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue