From 7694ce807b7da003696c04794727568e4bf7dd54 Mon Sep 17 00:00:00 2001 From: Stephen Bennett Date: Wed, 7 Feb 2024 19:39:56 +0000 Subject: [PATCH] Use chert for klines --- Cargo.lock | 178 ++++++++++--- client_listener/src/connection.rs | 1 + sable_ircd/src/capability/mod.rs | 1 + sable_ircd/src/client.rs | 4 + sable_ircd/src/command/handlers/kline.rs | 51 +++- sable_ircd/src/command/handlers/user.rs | 8 +- sable_ircd/src/command/mod.rs | 2 +- sable_ircd/src/messages/source_target.rs | 18 ++ sable_ircd/src/movable.rs | 1 + sable_ircd/src/server/command_action.rs | 7 +- sable_ircd/src/server/user_access.rs | 44 +++- sable_network/Cargo.toml | 1 + sable_network/src/lib.rs | 6 + sable_network/src/network/ban/mod.rs | 244 +++--------------- sable_network/src/network/ban/repository.rs | 152 ++--------- sable_network/src/network/event/details.rs | 3 +- .../src/network/network/ban_state.rs | 18 +- sable_network/src/network/state/bans.rs | 4 +- sable_network/src/network/wrapper/bans.rs | 13 +- 19 files changed, 333 insertions(+), 423 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 144b6229..6b98a938 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "aliasable" version = "0.1.3" @@ -135,7 +144,7 @@ checksum = "7b2d0f03b3640e3a630367e40c468cb7f309529c708ed1d88597047b0e7c6ef7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -269,6 +278,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bincode" version = "1.3.3" @@ -384,6 +399,39 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chert" +version = "0.2.1" +source = "git+https://github.com/jesopo/chert?branch=spb/compile-node-references#473be24f7bb7dbadfc9e988cdec2ecba18536d7f" +dependencies = [ + "chert_accessor", + "chert_derive", + "cidr", + "logos", + "regex", + "serde", + "serde_regex", +] + +[[package]] +name = "chert_accessor" +version = "0.2.0" +source = "git+https://github.com/jesopo/chert?branch=spb/compile-node-references#473be24f7bb7dbadfc9e988cdec2ecba18536d7f" +dependencies = [ + "cidr", + "regex", +] + +[[package]] +name = "chert_derive" +version = "0.2.0" +source = "git+https://github.com/jesopo/chert?branch=spb/compile-node-references#473be24f7bb7dbadfc9e988cdec2ecba18536d7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "chrono" version = "0.4.26" @@ -400,6 +448,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cidr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d18b093eba54c9aaa1e3784d4361eb2ba944cf7d0a932a830132238f483e8d8" +dependencies = [ + "serde", +] + [[package]] name = "cipher" version = "0.2.5" @@ -635,7 +692,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -657,7 +714,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core 0.20.1", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -720,7 +777,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -822,7 +879,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1293,6 +1350,38 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "logos" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 2.0.48", +] + +[[package]] +name = "logos-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" +dependencies = [ + "logos-codegen", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -1314,7 +1403,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] @@ -1342,9 +1431,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memfd" @@ -1624,7 +1713,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1655,7 +1744,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -1708,9 +1797,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -1770,9 +1859,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.29" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1827,11 +1916,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ - "regex-syntax 0.7.2", + "aho-corasick", + "memchr", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", ] [[package]] @@ -1843,6 +1935,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -1851,9 +1954,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "resolv-conf" @@ -1993,6 +2096,7 @@ dependencies = [ "backoff", "bitflags", "built", + "chert", "chrono", "concurrent_log", "futures", @@ -2099,22 +2203,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.166" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.166" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -2128,6 +2232,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_with" version = "1.14.0" @@ -2175,7 +2289,7 @@ dependencies = [ "darling 0.20.1", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -2379,9 +2493,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2432,7 +2546,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -2537,7 +2651,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -2690,7 +2804,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", ] [[package]] @@ -2917,7 +3031,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -2939,7 +3053,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/client_listener/src/connection.rs b/client_listener/src/connection.rs index 49da3f4e..f3021d4f 100644 --- a/client_listener/src/connection.rs +++ b/client_listener/src/connection.rs @@ -6,6 +6,7 @@ use std::net::IpAddr; use tokio::sync::mpsc::UnboundedSender; /// A connection being managed by the worker process. +#[derive(Debug)] pub struct Connection { pub id: ConnectionId, pub tls_info: Option, diff --git a/sable_ircd/src/capability/mod.rs b/sable_ircd/src/capability/mod.rs index cd247b9a..63929644 100644 --- a/sable_ircd/src/capability/mod.rs +++ b/sable_ircd/src/capability/mod.rs @@ -123,6 +123,7 @@ impl ClientCapabilitySet { } } +#[derive(Debug)] pub struct AtomicCapabilitySet(AtomicU64); impl AtomicCapabilitySet { diff --git a/sable_ircd/src/client.rs b/sable_ircd/src/client.rs index 945c2ad9..ffee7450 100644 --- a/sable_ircd/src/client.rs +++ b/sable_ircd/src/client.rs @@ -22,6 +22,7 @@ use std::sync::OnceLock; use tokio::time::Instant; /// A client protocol connection +#[derive(Debug)] pub struct ClientConnection { /// The underlying network connection pub connection: Movable, @@ -71,6 +72,8 @@ pub struct PreClient { pub attach_user_id: OnceLock, #[serde_as(as = "WrapOption")] pub user: OnceLock, + #[serde_as(as = "WrapOption<(String,String)>")] + pub extra_user_params: OnceLock<(String, String)>, #[serde_as(as = "WrapOption")] pub nick: OnceLock, #[serde_as(as = "WrapOption")] @@ -233,6 +236,7 @@ impl PreClient { connected_at: Instant::now(), attach_user_id: OnceLock::new(), user: OnceLock::new(), + extra_user_params: OnceLock::new(), nick: OnceLock::new(), realname: OnceLock::new(), hostname: OnceLock::new(), diff --git a/sable_ircd/src/command/handlers/kline.rs b/sable_ircd/src/command/handlers/kline.rs index ad25d8d1..037e49d9 100644 --- a/sable_ircd/src/command/handlers/kline.rs +++ b/sable_ircd/src/command/handlers/kline.rs @@ -1,4 +1,5 @@ use super::*; +use sable_network::chert; use sable_network::network::ban::*; const DEFAULT_KLINE_DURATION: u32 = 1440; @@ -24,24 +25,52 @@ fn handle_kline( let mask_parts: Vec<_> = mask.split('@').collect(); if let [user, host] = mask_parts[..] { + let user_condition = if user == "*" { + None + } else { + Some(format!("user == \"{}\"", user)) + }; + + let host_condition = if host.parse::().is_ok() { + format!("ip == {}", host) + } else if let Some((first, second)) = host.rsplit_once('/') { + if second.parse::().is_ok() && first.parse::().is_ok() { + format!("ip in {}", host) + } else { + format!("host == \"{}\"", host) + } + } else { + format!("host == \"{}\"", host) + }; + + let condition = if let Some(user_condition) = user_condition { + format!("{} && {}", user_condition, host_condition) + } else { + host_condition + }; + + let pattern = match chert::parse::(&condition) { + Err(err) => { + tracing::error!(condition, ?err, "Translated ban condition failed to parse"); + response.send(message::Notice::new( + server, + &source, + "Internal error: condition failed to parse", + )); + return Ok(()); + } + Ok(parsed) => parsed.get_root().clone(), + }; + audit .ban() - .target_str(mask.to_string()) + .target_str(condition) .target_duration(duration) .reason(message.to_string()) .log(); - let matcher = match NetworkBanMatch::from_user_host(user, host) { - Ok(matcher) => matcher, - Err(_) => { - response.notice(&format!("A network ban is already set on {}", mask)); - return Ok(()); - } - }; - let new_kline = event::NewNetworkBan { - matcher, - action: NetworkBanAction::RefuseConnection(true), + pattern, 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/user.rs b/sable_ircd/src/command/handlers/user.rs index db42d070..bf8bff83 100644 --- a/sable_ircd/src/command/handlers/user.rs +++ b/sable_ircd/src/command/handlers/user.rs @@ -6,8 +6,8 @@ fn handle_user( source: PreClientSource, cmd: &dyn Command, username: Username, - _unused1: &str, - _unused2: &str, + unused1: &str, + unused2: &str, realname: Result, ) -> CommandResult { let Ok(realname) = realname else { @@ -22,6 +22,10 @@ fn handle_user( // from this pre-client. If that happens we silently ignore the new values. source.user.set(username).ok(); source.realname.set(realname).ok(); + source + .extra_user_params + .set((unused1.to_owned(), unused2.to_owned())) + .ok(); if source.can_register() { server.add_action(CommandAction::RegisterClient(cmd.connection_id())); diff --git a/sable_ircd/src/command/mod.rs b/sable_ircd/src/command/mod.rs index c424399c..f8d06de8 100644 --- a/sable_ircd/src/command/mod.rs +++ b/sable_ircd/src/command/mod.rs @@ -22,7 +22,7 @@ mod dispatcher; pub use dispatcher::*; mod plumbing; -pub use plumbing::{ArgListIter, Command}; +pub use plumbing::{ArgListIter, Command, LoggedInUserSource, PreClientSource, UserSource}; /// A convenience definition for the result type returned from command handlers pub type CommandResult = Result<(), CommandError>; diff --git a/sable_ircd/src/messages/source_target.rs b/sable_ircd/src/messages/source_target.rs index 491062a5..ddffda5f 100644 --- a/sable_ircd/src/messages/source_target.rs +++ b/sable_ircd/src/messages/source_target.rs @@ -154,3 +154,21 @@ impl MessageTarget for crate::command::CommandSource<'_> { } } } + +impl MessageTarget for crate::command::UserSource<'_> { + fn format(&self) -> String { + ::format(&self.0) + } +} + +impl MessageTarget for crate::command::LoggedInUserSource<'_> { + fn format(&self) -> String { + ::format(&self.user) + } +} + +impl MessageTarget for crate::command::PreClientSource { + fn format(&self) -> String { + "*".to_string() + } +} diff --git a/sable_ircd/src/movable.rs b/sable_ircd/src/movable.rs index c5a9ac69..7afb0117 100644 --- a/sable_ircd/src/movable.rs +++ b/sable_ircd/src/movable.rs @@ -4,6 +4,7 @@ /// /// Implements [`Deref`](std::ops::Deref) for transparent access to the underlying /// value; this operation will panic if the `Movable` has been emptied. +#[derive(Debug)] pub enum Movable { Empty, Full(T), diff --git a/sable_ircd/src/server/command_action.rs b/sable_ircd/src/server/command_action.rs index fc8c0c01..be90f02a 100644 --- a/sable_ircd/src/server/command_action.rs +++ b/sable_ircd/src/server/command_action.rs @@ -4,10 +4,15 @@ use parking_lot::RwLockUpgradableReadGuard; impl ClientServer { fn notify_access_error(&self, err: &user_access::AccessError, conn: &ClientConnection) { + use user_access::AccessError::*; match err { - user_access::AccessError::Banned(reason) => { + Banned(reason) => { conn.send(make_numeric!(YoureBanned, &reason).format_for(self, &UnknownTarget)); } + InternalError => { + tracing::error!(?conn, "Internal error checking access"); + conn.send(message::Error::new("Internal error")); + } } } diff --git a/sable_ircd/src/server/user_access.rs b/sable_ircd/src/server/user_access.rs index 8ac42d63..2eb1f66e 100644 --- a/sable_ircd/src/server/user_access.rs +++ b/sable_ircd/src/server/user_access.rs @@ -1,4 +1,4 @@ -use sable_network::prelude::ban::UserDetails; +use sable_network::prelude::ban::*; use super::*; @@ -7,22 +7,52 @@ use super::*; pub enum AccessError { /// User matched a network ban, with provided reason Banned(String), + /// An internal error occurred while attempting to verify access + InternalError, } impl ClientServer { + #[tracing::instrument(skip(self, net))] pub(super) fn check_user_access( &self, net: &Network, client: &ClientConnection, ) -> Result<(), AccessError> { if let Some(pre_client) = client.pre_client() { + let Some(nick) = pre_client.nick.get().cloned() else { + tracing::error!("PreClient nickname not set"); + return Err(AccessError::InternalError); + }; + let Some(user) = pre_client.user.get().cloned() else { + tracing::error!("PreClient username not set"); + return Err(AccessError::InternalError); + }; + let Some(host) = pre_client.hostname.get().cloned() else { + tracing::error!("PreClient hostname not set"); + return Err(AccessError::InternalError); + }; + let Some(realname) = pre_client.realname.get().cloned() else { + tracing::error!("PreClient realname not set"); + return Err(AccessError::InternalError); + }; + let Some((user_param_1, user_param_2)) = pre_client.extra_user_params.get().cloned() + else { + tracing::error!("PreClient user parameters not set"); + return Err(AccessError::InternalError); + }; + let ip = client.remote_addr(); - let user_details = UserDetails { - nick: pre_client.nick.get().map(Nickname::as_ref), - ident: pre_client.user.get().map(Username::as_ref), - host: pre_client.hostname.get().map(Hostname::as_ref), - ip: Some(&ip), - realname: pre_client.realname.get().map(Realname::as_ref), + let tls = client.connection.is_tls(); + + let user_details = PreRegistrationBanSettings { + nick, + user, + host, + realname, + ip, + user_param_1, + user_param_2, + tls, }; if let Some(ban) = net.network_bans().find(&user_details) { diff --git a/sable_network/Cargo.toml b/sable_network/Cargo.toml index 21d32d30..aa05a8d6 100644 --- a/sable_network/Cargo.toml +++ b/sable_network/Cargo.toml @@ -42,6 +42,7 @@ concurrent_log = { version = "0.2.4", features = [ "serde" ] } ipnet = { version = "2", features = [ "serde" ] } anyhow = "1.0" backoff = { version = "0.4.0", features = ["tokio"] } +chert = { git = "https://github.com/jesopo/chert", branch = "spb/compile-node-references" } [dependencies.serde] version = "1" diff --git a/sable_network/src/lib.rs b/sable_network/src/lib.rs index 8bcb53f0..10e5899f 100644 --- a/sable_network/src/lib.rs +++ b/sable_network/src/lib.rs @@ -32,6 +32,12 @@ pub mod types { pub mod utils; +pub mod chert { + pub use chert::compile::Engine; + pub use chert::parse::nodes::boolean::NodeBoolean; + pub use chert::*; +} + mod build_data { include!(concat!(env!("OUT_DIR"), "/built.rs")); } diff --git a/sable_network/src/network/ban/mod.rs b/sable_network/src/network/ban/mod.rs index c48aed37..8efd4821 100644 --- a/sable_network/src/network/ban/mod.rs +++ b/sable_network/src/network/ban/mod.rs @@ -1,16 +1,39 @@ -use crate::{id::*, types::Pattern, validated::*}; +use crate::{id::*, validated::*}; -use ipnet::IpNet; use serde::{Deserialize, Serialize}; -use std::{convert::TryInto, str::FromStr}; +use std::net::IpAddr; use thiserror::Error; -mod user_details; -pub use user_details::*; +//mod user_details; +//pub use user_details::*; mod repository; pub use repository::*; +/// The set of user information that's available to a pre-registration network ban pattern +#[derive(Debug, Clone, chert::ChertStruct)] +pub struct PreRegistrationBanSettings { + #[chert(as_ref=str)] + pub nick: Nickname, + #[chert(as_ref=str)] + pub user: Username, + #[chert(as_ref=str)] + pub host: Hostname, + #[chert(as_ref=str)] + pub realname: Realname, + pub ip: IpAddr, + pub user_param_1: String, + pub user_param_2: String, + pub tls: bool, +} + +/// The set of user information that's available to a pre-SASL-authentication network ban pattern +#[derive(Debug, Clone, chert::ChertStruct)] +pub struct PreSaslBanSettings { + pub ip: IpAddr, + pub tls: bool, +} + /// Actions that can be applied by a network ban #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] pub enum NetworkBanAction { @@ -27,21 +50,6 @@ pub enum NetworkBanAction { DisconnectEarly, } -/// Methods by which a network ban can match a user's host -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] -pub enum NetworkBanHostMatch { - /// Exact IP address match - ExactIp(std::net::IpAddr), - /// IP address range (i.e. CIDR mask) - IpRange(ipnet::IpNet), - /// Exact resolved hostname match - ExactHostname(Hostname), - /// Resolved hostname range (i.e. *.suffix) - HostnameRange(String), - /// Freeform pattern (e.g. ip-8-8-*.dynamic-isp.com, 192.*.*.1) - HostnameMask(Pattern), -} - /// Error type denoting an invalid ban mask was supplied #[derive(Debug, Clone, Error)] #[error("Invalid ban mask")] @@ -56,197 +64,5 @@ pub struct DuplicateNetworkBan { pub ban: crate::network::state::NetworkBan, } -/// Criteria for matching a network ban. -/// -/// All fields except host are optional, and the combination will match if all its -/// fields that are present match individually. -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] -pub struct NetworkBanMatch { - pub host: NetworkBanHostMatch, - pub ident: Option, - pub realname: Option, - pub nickname: Option, -} - -impl FromStr for NetworkBanHostMatch { - type Err = InvalidBanMask; - - fn from_str(s: &str) -> Result { - let ipv4_chars = "0123456789."; - let ipv6_chars = "0123456789abcdef:"; - let wildcard_chars = "*?"; - - if let Ok(ip) = std::net::IpAddr::from_str(s) { - Ok(Self::ExactIp(ip)) - } else if let Ok(ip_net) = ipnet::IpNet::from_str(s) { - Ok(Self::IpRange(ip_net)) - } else if s.chars().any(|c| wildcard_chars.contains(c)) { - // There's a wildcard involved somewhere. *.host.name and 1.2.* or aa:bb:* - // patterns are handled specially, so look for those - - if s.ends_with(".*") - && s.split_at(s.len() - 1) - .0 - .chars() - .all(|c| ipv4_chars.contains(c)) - { - // This is an IPv4 prefix mask, which we can turn into a CIDR-based IpNet - - let mut components = s.split('.').collect::>(); - - if components.len() > 4 { - return Err(InvalidBanMask); - } - - // We know from the surrounding if that the last component is * - components.pop(); - let prefix_len = (components.len() * 8) as u8; // Can't overflow because of length test above - - let mut numbers = Vec::::new(); - for c in components { - numbers.push(c.parse().map_err(|_| InvalidBanMask)?); - } - while numbers.len() < 4 { - numbers.push(0); - } - - let ip_addr = - std::net::Ipv4Addr::new(numbers[0], numbers[1], numbers[2], numbers[3]); - let ip_net = IpNet::new(ip_addr.into(), prefix_len).map_err(|_| InvalidBanMask)?; - - Ok(Self::IpRange(ip_net)) - } else if s.ends_with(":*") - && s.split_at(s.len() - 1) - .0 - .chars() - .all(|c| ipv6_chars.contains(c)) - { - // This is an IPv6 prefix mask, which we can turn into a CIDR-based IpNet - - let mut components = s.split(':').collect::>(); - - if components.len() > 8 { - return Err(InvalidBanMask); - } - - // We know from the surrounding if that the last component is * - components.pop(); - let prefix_len = (components.len() * 16) as u8; // Can't overflow because of length test above - - let mut numbers = Vec::::new(); - for c in components { - numbers.push(u16::from_str_radix(c, 16).map_err(|_| InvalidBanMask)?); - } - while numbers.len() < 8 { - numbers.push(0); - } - - let ip_addr = std::net::Ipv6Addr::new( - numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5], - numbers[6], numbers[7], - ); - let ip_net = IpNet::new(ip_addr.into(), prefix_len).map_err(|_| InvalidBanMask)?; - - Ok(Self::IpRange(ip_net)) - } else if s.starts_with("*.") && !s[2..].chars().any(|c| wildcard_chars.contains(c)) { - // If the first character is * and there are no other wildcards, then - // it's a suffix-based range - Ok(Self::HostnameRange(s[2..].to_owned())) - } else { - // If we didn't match any of the above, treat it as a free-form wildcard pattern - // that's not amenable to quick lookup - Ok(Self::HostnameMask(Pattern::new(s.to_owned()))) - } - } else { - Ok(Self::ExactHostname( - s.try_into().map_err(|_| InvalidBanMask)?, - )) - } - } -} - -impl NetworkBanHostMatch { - pub fn matches(&self, user_details: &UserDetails) -> bool { - match self { - NetworkBanHostMatch::ExactIp(ip) => user_details.ip == Some(ip), - NetworkBanHostMatch::IpRange(ip_net) => { - if let Some(ip) = user_details.ip { - ip_net.contains(ip) - } else { - false - } - } - NetworkBanHostMatch::ExactHostname(host) => user_details.host == Some(host.as_ref()), - NetworkBanHostMatch::HostnameRange(host_suffix) => { - if let Some(host) = user_details.host { - host.ends_with(host_suffix) - } else { - false - } - } - NetworkBanHostMatch::HostnameMask(mask) => { - let host_match = if let Some(host) = user_details.host { - mask.matches(host) - } else { - false - }; - - let ip_match = if let Some(ip) = user_details.ip { - mask.matches(&ip.to_string()) - } else { - false - }; - - host_match || ip_match - } - } - } -} - -impl NetworkBanMatch { - pub fn from_user_host(user: &str, host: &str) -> Result { - let ident_match = if user == "*" { - None - } else { - Some(Pattern::new(user.to_owned())) - }; - - let host_match = NetworkBanHostMatch::from_str(host)?; - - Ok(Self { - host: host_match, - ident: ident_match, - realname: None, - nickname: None, - }) - } - - pub fn matches(&self, user_details: &UserDetails) -> bool { - if !self.host.matches(user_details) { - return false; - } - - if let (Some(ident), Some(user_ident)) = (self.ident.as_ref(), user_details.ident) { - if !ident.matches(user_ident) { - return false; - } - } - if let (Some(realname), Some(user_realname)) = - (self.realname.as_ref(), user_details.realname) - { - if !realname.matches(user_realname.as_ref()) { - return false; - } - } - if let (Some(nickname), Some(user_nickname)) = (self.nickname.as_ref(), user_details.nick) { - if !nickname.matches(user_nickname.as_ref()) { - return false; - } - } - - return true; - } -} - -#[cfg(test)] -mod test; +//#[cfg(test)] +//mod test; diff --git a/sable_network/src/network/ban/repository.rs b/sable_network/src/network/ban/repository.rs index 096ee421..420ba8bc 100644 --- a/sable_network/src/network/ban/repository.rs +++ b/sable_network/src/network/ban/repository.rs @@ -2,7 +2,9 @@ use super::*; use crate::network::*; use std::collections::HashMap; -use std::net::IpAddr; + +// Convenience alias for the engine type +type Engine = chert::compile::Engine; /// A collection of network bans, supporting efficient lookup based on /// (partial) user details @@ -10,94 +12,38 @@ use std::net::IpAddr; pub struct BanRepository { all_bans: HashMap, - exact_host_bans: HashMap>, - exact_ip_bans: HashMap>, - - host_range_bans: HashMap>, - ip_net_bans: HashMap>, - - // Freeform host bans aren't indexable and just have to all be tested - freeform_hostmask_bans: Vec, + engine: Engine, } impl BanRepository { pub fn new() -> Self { + let all_bans = HashMap::new(); Self { - all_bans: HashMap::new(), - exact_host_bans: HashMap::new(), - exact_ip_bans: HashMap::new(), - host_range_bans: HashMap::new(), - ip_net_bans: HashMap::new(), - freeform_hostmask_bans: Vec::new(), + engine: Self::compile_engine(&all_bans), + all_bans, } } - pub fn from_ban_set(bans: Vec) -> Result { - let mut ret = Self::new(); + pub fn from_ban_set(bans: Vec) -> Self { + let mut all_bans = HashMap::new(); for ban in bans { - ret.add(ban)?; + all_bans.insert(ban.id, ban); } - Ok(ret) - } + let engine = Self::compile_engine(&all_bans); - fn add_index_for(&mut self, ban: &state::NetworkBan) -> Result<(), NetworkBanId> { - let ban_vec = match &ban.matcher.host { - NetworkBanHostMatch::ExactIp(ip) => self.exact_ip_bans.entry(*ip).or_default(), - NetworkBanHostMatch::IpRange(ip_net) => { - self.ip_net_bans.entry(ip_net.network()).or_default() - } - NetworkBanHostMatch::ExactHostname(host) => { - self.exact_host_bans.entry(*host).or_default() - } - NetworkBanHostMatch::HostnameRange(host_suffix) => { - self.host_range_bans.entry(host_suffix.clone()).or_default() - } - NetworkBanHostMatch::HostnameMask(_) => &mut self.freeform_hostmask_bans, - }; - - // We don't strictly need to do this if we just created the vec, but it won't take - // long to iterate an empty vector and the code's clearer this way. - - // We can't use `self` in the closure because it's already borrowed mutably; declaring this - // here lets the closure access a single field - let all_bans = &self.all_bans; - if let Some(existing) = ban_vec.iter().find(|id| { - if let Some(other_ban) = all_bans.get(id) { - ban.matcher == other_ban.matcher - } else { - false - } - }) { - Err(*existing) - } else { - ban_vec.push(ban.id); - Ok(()) - } + Self { all_bans, engine } } - pub fn add(&mut self, ban: state::NetworkBan) -> Result<(), DuplicateNetworkBan> { - if let Err(existing_id) = self.add_index_for(&ban) { - Err(DuplicateNetworkBan { existing_id, ban }) - } else { - self.all_bans.insert(ban.id, ban); - Ok(()) - } + pub fn add(&mut self, ban: state::NetworkBan) { + self.all_bans.insert(ban.id, ban); + self.recompile(); } pub fn remove(&mut self, id: NetworkBanId) { - if let Some(ban) = self.all_bans.remove(&id) { - let search_vec = match &ban.matcher.host { - NetworkBanHostMatch::ExactIp(ip) => self.exact_ip_bans.get_mut(&ip), - NetworkBanHostMatch::IpRange(ip_net) => self.ip_net_bans.get_mut(&ip_net.network()), - NetworkBanHostMatch::ExactHostname(host) => self.exact_host_bans.get_mut(host), - NetworkBanHostMatch::HostnameRange(host) => self.host_range_bans.get_mut(host), - NetworkBanHostMatch::HostnameMask(_) => Some(&mut self.freeform_hostmask_bans), - }; - if let Some(search_vec) = search_vec { - search_vec.retain(|id| id != &ban.id) - } + if self.all_bans.remove(&id).is_some() { + self.recompile(); } } @@ -105,62 +51,18 @@ impl BanRepository { self.all_bans.get(id) } - pub fn find(&self, user_details: &UserDetails) -> Option<&state::NetworkBan> { - let mut candidates = Vec::new(); - - // First look for an exact IP match - if let Some(vec) = user_details.ip.and_then(|ip| self.exact_ip_bans.get(ip)) { - candidates.push(vec); - } - - // Then an exact hostname match - if let Some(vec) = user_details - .host - .and_then(|host| self.exact_host_bans.get(host)) - { - candidates.push(vec); - } - - // Then run through each prefix length checking for range bans - if let Some(ip) = user_details.ip { - let mut next_net: Option = Some((*ip).into()); - - while let Some(ref net) = next_net { - if let Some(vec) = self.ip_net_bans.get(&net.network()) { - candidates.push(vec) - } - next_net = net.supernet(); - } - } - - // Then go through each possible hostname suffix - if let Some(host) = user_details.host { - let mut host_part = host; + pub fn find(&self, matching: &PreRegistrationBanSettings) -> Option<&state::NetworkBan> { + let matches = self.engine.eval(&matching); - while let Some((_, suffix)) = host_part.split_once('.') { - if let Some(vec) = self.host_range_bans.get(suffix) { - candidates.push(vec); - } - host_part = suffix; - } - } + matches.get(0).and_then(|id| self.all_bans.get(id)) + } - candidates.push(&self.freeform_hostmask_bans); - - // `candidates` is now ordered: exact IP match first, then exact hostname, - // then CIDR range bans from most specific to least specific prefix length, - // then hostname suffix bans from most to least specific, then freeform mask bans. - // - // The first of these that matches on its other criteria is the one we'll use. - for id in candidates.into_iter().flatten() { - if let Some(candidate_ban) = self.all_bans.get(&id) { - if candidate_ban.matcher.matches(user_details) { - return Some(candidate_ban); - } - } - } + fn recompile(&mut self) { + self.engine = Self::compile_engine(&self.all_bans); + } - None + fn compile_engine(bans: &HashMap) -> Engine { + chert::compile::compile_unsafe(bans.iter().map(|(k, v)| (*k, &v.pattern))) } } @@ -179,6 +81,6 @@ impl<'de> serde::de::Deserialize<'de> for BanRepository { D: serde::Deserializer<'de>, { let bans = Vec::deserialize(deserializer)?; - Self::from_ban_set(bans).map_err(serde::de::Error::custom) + Ok(Self::from_ban_set(bans)) } } diff --git a/sable_network/src/network/event/details.rs b/sable_network/src/network/event/details.rs index 0fd24ad0..d4b1539c 100644 --- a/sable_network/src/network/event/details.rs +++ b/sable_network/src/network/event/details.rs @@ -134,8 +134,7 @@ EventDetails => { #[target_type(NetworkBanId)] struct NewNetworkBan { - pub matcher: ban::NetworkBanMatch, - pub action: ban::NetworkBanAction, + pub pattern: crate::chert::NodeBoolean, 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 5b7f6189..47519fe0 100644 --- a/sable_network/src/network/network/ban_state.rs +++ b/sable_network/src/network/network/ban_state.rs @@ -14,8 +14,7 @@ impl Network { let ban = state::NetworkBan { id: target, created_by: event.id, - matcher: details.matcher.clone(), - action: details.action.clone(), + pattern: details.pattern.clone(), timestamp: details.timestamp, expires: details.expires, reason: details.reason.clone(), @@ -23,20 +22,7 @@ impl Network { setter_info: details.setter_info.clone(), }; - if let Err(e) = self.network_bans.add(ban) { - let ban = e.ban; - if let Some(existing) = self.network_bans.get(&e.existing_id) { - // Two separate bans with identical matchers - we choose one arbitrarily - if ban.timestamp < existing.timestamp - || (ban.timestamp == existing.timestamp && ban.created_by < existing.created_by) - { - self.network_bans.remove(existing.id); - if self.network_bans.add(ban).is_err() { - todo!("handle this error, or at least report it"); - } - } - } - } + self.network_bans.add(ban); } pub(super) fn remove_ban( diff --git a/sable_network/src/network/state/bans.rs b/sable_network/src/network/state/bans.rs index 1e5bb6a3..7f75f66b 100644 --- a/sable_network/src/network/state/bans.rs +++ b/sable_network/src/network/state/bans.rs @@ -1,4 +1,3 @@ -use crate::network::ban::*; use crate::prelude::*; use serde::{Deserialize, Serialize}; @@ -9,8 +8,7 @@ pub struct NetworkBan { pub id: NetworkBanId, pub created_by: EventId, - pub matcher: NetworkBanMatch, - pub action: NetworkBanAction, + pub pattern: crate::chert::NodeBoolean, pub timestamp: i64, pub expires: i64, diff --git a/sable_network/src/network/wrapper/bans.rs b/sable_network/src/network/wrapper/bans.rs index 5c48ecb8..876c250b 100644 --- a/sable_network/src/network/wrapper/bans.rs +++ b/sable_network/src/network/wrapper/bans.rs @@ -1,4 +1,4 @@ -use crate::{network::ban::*, prelude::*}; +use crate::prelude::*; /// A wrapper around a [`state::NetworkBan`] pub struct NetworkBan<'a> { @@ -12,14 +12,9 @@ impl NetworkBan<'_> { self.data.id } - /// The ban match criteria - pub fn matcher(&self) -> &NetworkBanMatch { - &self.data.matcher - } - - /// The action to be applied by the ban - pub fn action(&self) -> &NetworkBanAction { - &self.data.action + /// Return the pattern expression which matches + pub fn pattern(&self) -> &crate::chert::NodeBoolean { + &self.data.pattern } /// Details of who set this ban