From 203a8775c9f32f42e8d8bacb529901ddd27950c6 Mon Sep 17 00:00:00 2001 From: Stephen Bennett Date: Sat, 10 Feb 2024 01:07:21 +0000 Subject: [PATCH] Support multiple ban types and actions Bans can match at registration (as for K:lines), immediately on new connections (as D:lines), or when SASL authentication is attempted. They can also require SASL, block SASL, or disconnect the user. --- Cargo.lock | 1 + sable_ircd/Cargo.toml | 1 + sable_ircd/src/command/handlers/ban.rs | 106 +++++++++++++++ sable_ircd/src/command/handlers/kline.rs | 2 + .../src/command/handlers/services/sasl.rs | 18 ++- sable_ircd/src/command/mod.rs | 1 + sable_ircd/src/server/command_action.rs | 18 +++ sable_ircd/src/server/mod.rs | 19 ++- sable_ircd/src/server/user_access.rs | 18 ++- sable_network/src/network/ban/mod.rs | 38 ++++-- sable_network/src/network/ban/repository.rs | 122 ++++++++++++++---- sable_network/src/network/event/details.rs | 2 + .../src/network/network/ban_state.rs | 2 + sable_network/src/network/state/bans.rs | 4 + 14 files changed, 317 insertions(+), 35 deletions(-) create mode 100644 sable_ircd/src/command/handlers/ban.rs diff --git a/Cargo.lock b/Cargo.lock index 6b98a938..fe7d0326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2066,6 +2066,7 @@ dependencies = [ "sable_network", "sable_server", "serde", + "serde_json", "serde_with 2.3.3", "sha256", "structopt", diff --git a/sable_ircd/Cargo.toml b/sable_ircd/Cargo.toml index 22d0a24e..adc78088 100644 --- a/sable_ircd/Cargo.toml +++ b/sable_ircd/Cargo.toml @@ -38,3 +38,4 @@ async-trait = "0.1.57" structopt = "0.3" base64 = "0.21" anyhow = "1.0" +serde_json = "1" diff --git a/sable_ircd/src/command/handlers/ban.rs b/sable_ircd/src/command/handlers/ban.rs new file mode 100644 index 00000000..0ff5113c --- /dev/null +++ b/sable_ircd/src/command/handlers/ban.rs @@ -0,0 +1,106 @@ +use super::*; +use sable_network::{chert, network::ban::*}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum NewBanAction { + RefuseConnection, + RequireSasl, +} + +#[derive(Debug, Deserialize)] +struct NewBanArguments { + #[serde(rename = "type")] + match_type: Option, + action: Option, + apply_existing: Option, + pattern: String, + duration: i64, + reason: String, + oper_reason: Option, +} + +#[command_handler("BAN")] +fn handle_ban( + server: &ClientServer, + source: UserSource, + response: &dyn CommandResponse, + new_ban_str: &str, +) -> CommandResult { + server.policy().require_oper(&source)?; + + let new_ban_details: NewBanArguments = match serde_json::from_str(new_ban_str) { + Ok(ban) => ban, + Err(e) => { + response.send(message::Fail::new("BAN", "INVALID_BAN", "", &e.to_string())); + return Ok(()); + } + }; + + let match_type = new_ban_details + .match_type + .unwrap_or(BanMatchType::PreRegistration); + + let action = match match_type { + BanMatchType::PreSasl => { + // Only valid action here is DenySasl + NetworkBanAction::DenySasl + } + _ => match new_ban_details.action { + Some(NewBanAction::RefuseConnection) => { + NetworkBanAction::RefuseConnection(new_ban_details.apply_existing.unwrap_or(true)) + } + Some(NewBanAction::RequireSasl) => { + NetworkBanAction::RequireSasl(new_ban_details.apply_existing.unwrap_or(true)) + } + None => NetworkBanAction::RefuseConnection(true), + }, + }; + + let pattern_parsed = match match_type { + BanMatchType::PreRegistration => { + chert::parse::(&new_ban_details.pattern) + .map(|ast| ast.get_root().clone()) + } + BanMatchType::NewConnection => { + chert::parse::(&new_ban_details.pattern) + .map(|ast| ast.get_root().clone()) + } + BanMatchType::PreSasl => chert::parse::(&new_ban_details.pattern) + .map(|ast| ast.get_root().clone()), + }; + + let pattern = match pattern_parsed { + Ok(node) => node, + Err(e) => { + response.send(message::Fail::new( + "BAN", + "INVALID_BAN_PATTERN", + "", + &format!("{:?}", e), + )); + return Ok(()); + } + }; + + let timestamp = sable_network::utils::now(); + let expires = timestamp + new_ban_details.duration * 60; + + let new_ban_id = server.ids().next_network_ban(); + + let new_ban = event::details::NewNetworkBan { + match_type, + pattern, + action, + timestamp, + expires, + reason: new_ban_details.reason, + oper_reason: new_ban_details.oper_reason, + setter_info: source.0.nuh(), + }; + + server.node().submit_event(new_ban_id, new_ban); + + Ok(()) +} diff --git a/sable_ircd/src/command/handlers/kline.rs b/sable_ircd/src/command/handlers/kline.rs index 037e49d9..ede6dca5 100644 --- a/sable_ircd/src/command/handlers/kline.rs +++ b/sable_ircd/src/command/handlers/kline.rs @@ -70,7 +70,9 @@ fn handle_kline( .log(); let new_kline = event::NewNetworkBan { + match_type: BanMatchType::PreRegistration, pattern, + action: NetworkBanAction::RefuseConnection(true), setter_info: source.0.nuh(), timestamp: sable_network::utils::now(), expires: sable_network::utils::now() + (duration * 60), diff --git a/sable_ircd/src/command/handlers/services/sasl.rs b/sable_ircd/src/command/handlers/services/sasl.rs index d527c95c..299c228c 100644 --- a/sable_ircd/src/command/handlers/services/sasl.rs +++ b/sable_ircd/src/command/handlers/services/sasl.rs @@ -1,4 +1,7 @@ -use sable_network::rpc::{RemoteServerRequestType, RemoteServerResponse}; +use sable_network::{ + network::ban::*, + rpc::{RemoteServerRequestType, RemoteServerResponse}, +}; use super::*; use base64::prelude::*; @@ -27,6 +30,19 @@ async fn handle_authenticate( } } else { // No session, so the argument is the mechanism name + // First check whether they're allowed to use SASL + let user_details = PreSaslBanSettings { + ip: cmd.connection().remote_addr(), + tls: cmd.connection().connection.is_tls(), + mechanism: text.to_owned(), + }; + + for ban in net.network_bans().find_pre_sasl(&user_details) { + if let NetworkBanAction::DenySasl = ban.action { + response.numeric(make_numeric!(SaslFail)); + return Ok(()); + } + } // Special case for EXTERNAL, which we can handle without going to services if text == "EXTERNAL" { diff --git a/sable_ircd/src/command/mod.rs b/sable_ircd/src/command/mod.rs index f8d06de8..26dc079a 100644 --- a/sable_ircd/src/command/mod.rs +++ b/sable_ircd/src/command/mod.rs @@ -39,6 +39,7 @@ mod handlers { mod admin; mod away; + mod ban; mod cap; mod chathistory; mod invite; diff --git a/sable_ircd/src/server/command_action.rs b/sable_ircd/src/server/command_action.rs index be90f02a..e6b767d7 100644 --- a/sable_ircd/src/server/command_action.rs +++ b/sable_ircd/src/server/command_action.rs @@ -9,6 +9,24 @@ impl ClientServer { Banned(reason) => { conn.send(make_numeric!(YoureBanned, &reason).format_for(self, &UnknownTarget)); } + SaslRequired(reason) => { + if reason.len() > 0 { + conn.send(message::Notice::new( + self, + &UnknownTarget, + &format!( + "You must authenticate via SASL to use this server ({})", + reason + ), + )); + } else { + conn.send(message::Notice::new( + self, + &UnknownTarget, + "You must authenticate via SASL to use this server", + )); + } + } InternalError => { tracing::error!(?conn, "Internal error checking access"); conn.send(message::Error::new("Internal error")); diff --git a/sable_ircd/src/server/mod.rs b/sable_ircd/src/server/mod.rs index e01f52aa..aaf54d48 100644 --- a/sable_ircd/src/server/mod.rs +++ b/sable_ircd/src/server/mod.rs @@ -5,7 +5,7 @@ use messages::*; use event::*; use rpc::*; -use sable_network::{config::TlsData, prelude::*}; +use sable_network::{config::TlsData, network::ban::NetworkBanAction, prelude::*}; use auth_client::*; use client_listener::*; @@ -244,6 +244,23 @@ impl ClientServer { match msg.detail { ConnectionEventDetail::NewConnection(conn) => { tracing::trace!("Got new connection"); + + let conn_details = ban::NewConnectionBanSettings { + ip: conn.remote_addr, + tls: conn.is_tls(), + }; + for ban in self + .network() + .network_bans() + .find_new_connection(&conn_details) + { + if let NetworkBanAction::RefuseConnection(_) = ban.action { + conn.send(format!("ERROR :*** Banned: {}\r\n", ban.reason)); + conn.close(); + return; + } + } + let conn = ClientConnection::new(conn); conn.send(message::Notice::new( diff --git a/sable_ircd/src/server/user_access.rs b/sable_ircd/src/server/user_access.rs index 2eb1f66e..4bb4f71f 100644 --- a/sable_ircd/src/server/user_access.rs +++ b/sable_ircd/src/server/user_access.rs @@ -7,6 +7,8 @@ use super::*; pub enum AccessError { /// User matched a network ban, with provided reason Banned(String), + /// User requires SASL but didn't use it + SaslRequired(String), /// An internal error occurred while attempting to verify access InternalError, } @@ -55,8 +57,20 @@ impl ClientServer { tls, }; - if let Some(ban) = net.network_bans().find(&user_details) { - return Err(AccessError::Banned(ban.reason.clone())); + for ban in net.network_bans().find_pre_registration(&user_details) { + match ban.action { + NetworkBanAction::RefuseConnection(_) => { + return Err(AccessError::Banned(ban.reason.clone())); + } + NetworkBanAction::RequireSasl(_) => { + if pre_client.sasl_account.get().is_none() { + return Err(AccessError::SaslRequired(ban.reason.clone())); + } + } + NetworkBanAction::DenySasl => { + // Doesn't make sense here and should have been rejected + } + } } } Ok(()) diff --git a/sable_network/src/network/ban/mod.rs b/sable_network/src/network/ban/mod.rs index 8efd4821..ae4cd7f9 100644 --- a/sable_network/src/network/ban/mod.rs +++ b/sable_network/src/network/ban/mod.rs @@ -10,7 +10,23 @@ use thiserror::Error; mod repository; pub use repository::*; -/// The set of user information that's available to a pre-registration network ban pattern +/// Describes when a network ban will be matched, and which set of information is available to it +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BanMatchType { + /// Matches immediately before registration, and also against existing connections + /// when newly added - approximately equivalent to old K:line. Match fields are those in + /// [`PreRegistrationBanSettings`]. + PreRegistration, + /// Matches as soon as a connection is received, before processing any messages, + /// and also against existing connections when added - approximately equivalent to old D:line. + /// Match fields are those in [`NewConnectionBanSettings`]. + NewConnection, + /// Matches when SASL authentication is initiated. Match fields are those in [`PreSaslBanSettings`]. + PreSasl, +} + +/// The set of user information that's available to a `PreRegistration` network ban pattern #[derive(Debug, Clone, chert::ChertStruct)] pub struct PreRegistrationBanSettings { #[chert(as_ref=str)] @@ -27,15 +43,23 @@ pub struct PreRegistrationBanSettings { pub tls: bool, } -/// The set of user information that's available to a pre-SASL-authentication network ban pattern +/// The set of user information that's available to a `NewConnection` network ban pattern +#[derive(Debug, Clone, chert::ChertStruct)] +pub struct NewConnectionBanSettings { + pub ip: IpAddr, + pub tls: bool, +} + +/// The set of user information that's available to a `PreSasl` network ban pattern #[derive(Debug, Clone, chert::ChertStruct)] pub struct PreSaslBanSettings { pub ip: IpAddr, pub tls: bool, + pub mechanism: String, } /// Actions that can be applied by a network ban -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[derive(PartialEq, Debug, Clone, Copy, Serialize, Deserialize)] pub enum NetworkBanAction { /// Refuse new connections that match these criteria. The boolean parameter /// determines whether existing connections that match will also be disconnected. @@ -44,16 +68,14 @@ pub enum NetworkBanAction { /// before registration. The boolean parameter determines whether existing matching /// connections that are not logged in to an account will be disconnected. RequireSasl(bool), - /// Refuse new connections instantly, without allowing exemptions from other config entries - /// (equivalent to legacy D:line). Only makes sense for a ban that matches only on - /// IP address; the other information won't be present at immediate-disconnection time. - DisconnectEarly, + /// Prevent matching connections from using SASL authentication + DenySasl, } /// Error type denoting an invalid ban mask was supplied #[derive(Debug, Clone, Error)] #[error("Invalid ban mask")] -pub struct InvalidBanMask; +pub struct InvalidBanPattern; /// Error type denoting that a duplicate ban was provided #[derive(Debug, Clone, Error)] diff --git a/sable_network/src/network/ban/repository.rs b/sable_network/src/network/ban/repository.rs index 420ba8bc..b2381832 100644 --- a/sable_network/src/network/ban/repository.rs +++ b/sable_network/src/network/ban/repository.rs @@ -1,67 +1,138 @@ +use chert::ChertStructTrait; + use super::*; use crate::network::*; use std::collections::HashMap; // Convenience alias for the engine type -type Engine = chert::compile::Engine; +type Engine = chert::compile::Engine; /// A collection of network bans, supporting efficient lookup based on /// (partial) user details #[derive(Debug, Clone)] pub struct BanRepository { - all_bans: HashMap, + pre_registration_bans: HashMap, + new_connection_bans: HashMap, + pre_sasl_bans: HashMap, - engine: Engine, + pre_registration_engine: Engine, + new_connection_engine: Engine, + pre_sasl_engine: Engine, } impl BanRepository { pub fn new() -> Self { - let all_bans = HashMap::new(); + let pre_registration_bans = HashMap::new(); + let new_connection_bans = HashMap::new(); + let pre_sasl_bans = HashMap::new(); Self { - engine: Self::compile_engine(&all_bans), - all_bans, + pre_registration_engine: Self::compile_engine(&pre_registration_bans), + new_connection_engine: Self::compile_engine(&new_connection_bans), + pre_sasl_engine: Self::compile_engine(&pre_sasl_bans), + pre_registration_bans, + new_connection_bans, + pre_sasl_bans, } } pub fn from_ban_set(bans: Vec) -> Self { - let mut all_bans = HashMap::new(); + let mut pre_registration_bans = HashMap::new(); + let mut new_connection_bans = HashMap::new(); + let mut pre_sasl_bans = HashMap::new(); for ban in bans { - all_bans.insert(ban.id, ban); + use BanMatchType::*; + match ban.match_type { + PreRegistration => pre_registration_bans.insert(ban.id, ban), + NewConnection => new_connection_bans.insert(ban.id, ban), + PreSasl => pre_sasl_bans.insert(ban.id, ban), + }; } - let engine = Self::compile_engine(&all_bans); - - Self { all_bans, engine } + Self { + pre_registration_engine: Self::compile_engine(&pre_registration_bans), + new_connection_engine: Self::compile_engine(&new_connection_bans), + pre_sasl_engine: Self::compile_engine(&pre_sasl_bans), + pre_registration_bans, + new_connection_bans, + pre_sasl_bans, + } } pub fn add(&mut self, ban: state::NetworkBan) { - self.all_bans.insert(ban.id, ban); - self.recompile(); + use BanMatchType::*; + match ban.match_type { + PreRegistration => { + self.pre_registration_bans.insert(ban.id, ban); + self.pre_registration_engine = Self::compile_engine(&self.pre_registration_bans); + } + NewConnection => { + self.new_connection_bans.insert(ban.id, ban); + self.new_connection_engine = Self::compile_engine(&self.new_connection_bans); + } + PreSasl => { + self.pre_sasl_bans.insert(ban.id, ban); + self.pre_sasl_engine = Self::compile_engine(&self.pre_sasl_bans); + } + }; } pub fn remove(&mut self, id: NetworkBanId) { - if self.all_bans.remove(&id).is_some() { - self.recompile(); + if self.pre_registration_bans.remove(&id).is_some() { + self.pre_registration_engine = Self::compile_engine(&self.pre_registration_bans); + } + if self.new_connection_bans.remove(&id).is_some() { + self.new_connection_engine = Self::compile_engine(&self.new_connection_bans); + } + if self.pre_sasl_bans.remove(&id).is_some() { + self.pre_sasl_engine = Self::compile_engine(&self.pre_sasl_bans); } } pub fn get(&self, id: &NetworkBanId) -> Option<&state::NetworkBan> { - self.all_bans.get(id) + self.pre_registration_bans + .get(id) + .or_else(|| self.new_connection_bans.get(id)) + .or_else(|| self.pre_sasl_bans.get(id)) } - pub fn find(&self, matching: &PreRegistrationBanSettings) -> Option<&state::NetworkBan> { - let matches = self.engine.eval(&matching); + pub fn find_pre_registration( + &self, + matching: &PreRegistrationBanSettings, + ) -> impl Iterator { + let matches = self.pre_registration_engine.eval(&matching); - matches.get(0).and_then(|id| self.all_bans.get(id)) + matches + .into_iter() + .filter_map(move |id| self.pre_registration_bans.get(&id)) } - fn recompile(&mut self) { - self.engine = Self::compile_engine(&self.all_bans); + pub fn find_new_connection( + &self, + matching: &NewConnectionBanSettings, + ) -> impl Iterator { + let matches = self.new_connection_engine.eval(&matching); + + matches + .into_iter() + .filter_map(move |id| self.new_connection_bans.get(&id)) + } + + pub fn find_pre_sasl( + &self, + matching: &PreSaslBanSettings, + ) -> impl Iterator { + let matches = self.pre_sasl_engine.eval(&matching); + + matches + .into_iter() + .filter_map(move |id| self.pre_sasl_bans.get(&id)) } - fn compile_engine(bans: &HashMap) -> Engine { + fn compile_engine( + bans: &HashMap, + ) -> Engine { chert::compile::compile_unsafe(bans.iter().map(|(k, v)| (*k, &v.pattern))) } } @@ -71,7 +142,12 @@ impl serde::ser::Serialize for BanRepository { where S: serde::Serializer, { - serializer.collect_seq(self.all_bans.values()) + serializer.collect_seq( + self.pre_registration_bans + .values() + .chain(self.new_connection_bans.values()) + .chain(self.pre_sasl_bans.values()), + ) } } diff --git a/sable_network/src/network/event/details.rs b/sable_network/src/network/event/details.rs index d4b1539c..b6bb8095 100644 --- a/sable_network/src/network/event/details.rs +++ b/sable_network/src/network/event/details.rs @@ -134,7 +134,9 @@ EventDetails => { #[target_type(NetworkBanId)] struct NewNetworkBan { + pub match_type: ban::BanMatchType, pub pattern: crate::chert::NodeBoolean, + pub action: ban::NetworkBanAction, pub timestamp: i64, pub expires: i64, diff --git a/sable_network/src/network/network/ban_state.rs b/sable_network/src/network/network/ban_state.rs index 47519fe0..11edc42c 100644 --- a/sable_network/src/network/network/ban_state.rs +++ b/sable_network/src/network/network/ban_state.rs @@ -14,7 +14,9 @@ impl Network { let ban = state::NetworkBan { id: target, created_by: event.id, + match_type: details.match_type, pattern: details.pattern.clone(), + action: details.action, timestamp: details.timestamp, expires: details.expires, reason: details.reason.clone(), diff --git a/sable_network/src/network/state/bans.rs b/sable_network/src/network/state/bans.rs index 7f75f66b..0138001b 100644 --- a/sable_network/src/network/state/bans.rs +++ b/sable_network/src/network/state/bans.rs @@ -2,13 +2,17 @@ use crate::prelude::*; use serde::{Deserialize, Serialize}; +use ban::*; + /// A network ban #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkBan { pub id: NetworkBanId, pub created_by: EventId, + pub match_type: BanMatchType, pub pattern: crate::chert::NodeBoolean, + pub action: NetworkBanAction, pub timestamp: i64, pub expires: i64,