Skip to content

Commit

Permalink
Support multiple ban types and actions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spb committed Feb 10, 2024
1 parent 7694ce8 commit 203a877
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 35 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sable_ircd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ async-trait = "0.1.57"
structopt = "0.3"
base64 = "0.21"
anyhow = "1.0"
serde_json = "1"
106 changes: 106 additions & 0 deletions sable_ircd/src/command/handlers/ban.rs
Original file line number Diff line number Diff line change
@@ -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<BanMatchType>,
action: Option<NewBanAction>,
apply_existing: Option<bool>,
pattern: String,
duration: i64,
reason: String,
oper_reason: Option<String>,
}

#[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::<PreRegistrationBanSettings>(&new_ban_details.pattern)
.map(|ast| ast.get_root().clone())
}
BanMatchType::NewConnection => {
chert::parse::<NewConnectionBanSettings>(&new_ban_details.pattern)
.map(|ast| ast.get_root().clone())
}
BanMatchType::PreSasl => chert::parse::<PreSaslBanSettings>(&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(())
}
2 changes: 2 additions & 0 deletions sable_ircd/src/command/handlers/kline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
18 changes: 17 additions & 1 deletion sable_ircd/src/command/handlers/services/sasl.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use sable_network::rpc::{RemoteServerRequestType, RemoteServerResponse};
use sable_network::{
network::ban::*,
rpc::{RemoteServerRequestType, RemoteServerResponse},
};

use super::*;
use base64::prelude::*;
Expand Down Expand Up @@ -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" {
Expand Down
1 change: 1 addition & 0 deletions sable_ircd/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod handlers {

mod admin;
mod away;
mod ban;
mod cap;
mod chathistory;
mod invite;
Expand Down
18 changes: 18 additions & 0 deletions sable_ircd/src/server/command_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
19 changes: 18 additions & 1 deletion sable_ircd/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 16 additions & 2 deletions sable_ircd/src/server/user_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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(())
Expand Down
38 changes: 30 additions & 8 deletions sable_network/src/network/ban/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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.
Expand All @@ -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)]
Expand Down
Loading

0 comments on commit 203a877

Please sign in to comment.