Distributed SMTP queues and Rate limiting passing tests

This commit is contained in:
mdecimus
2024-02-13 14:35:28 +01:00
parent 66669545ff
commit 44db67cc2a
105 changed files with 2124 additions and 1678 deletions

View File

@@ -43,7 +43,6 @@ use tokio_rustls::TlsAcceptor;
use crate::{
acme::{directory::ACME_TLS_ALPN_NAME, AcmeManager},
listener::{
blocked::BlockedIps,
tls::{Certificate, CertificateResolver},
TcpAcceptor,
},
@@ -66,8 +65,7 @@ impl Config {
// Parse servers
for (internal_id, id) in self.sub_keys("server.listener", ".protocol").enumerate() {
let mut server =
self.parse_server(id, &certificates, &acmes, servers.blocked_ips.clone())?;
let mut server = self.parse_server(id, &certificates, &acmes)?;
if !servers.inner.iter().any(|s| s.id == server.id) {
server.internal_id = internal_id as u16;
servers.inner.push(server);
@@ -116,7 +114,6 @@ impl Config {
id: &str,
certificates: &AHashMap<String, Arc<Certificate>>,
acmes: &AHashMap<String, Arc<AcmeManager>>,
blocked_ips: Arc<BlockedIps>,
) -> super::Result<Server> {
// Build listeners
let mut listeners = Vec::new();
@@ -378,7 +375,6 @@ impl Config {
acceptor,
tls_implicit,
proxy_networks,
blocked_ips,
})
}
}

View File

@@ -37,7 +37,7 @@ use tokio::net::TcpSocket;
use crate::{
acme::AcmeManager,
failed,
listener::{blocked::BlockedIps, tls::Certificate, TcpAcceptor},
listener::{tls::Certificate, TcpAcceptor},
UnwrapFailure,
};
@@ -63,7 +63,6 @@ pub struct Server {
pub protocol: ServerProtocol,
pub listeners: Vec<Listener>,
pub proxy_networks: Vec<IpAddrMask>,
pub blocked_ips: Arc<BlockedIps>,
pub acceptor: TcpAcceptor,
pub tls_implicit: bool,
pub max_connections: u64,
@@ -74,7 +73,6 @@ pub struct Servers {
pub inner: Vec<Server>,
pub certificates: Vec<Arc<Certificate>>,
pub acme_managers: Vec<Arc<AcmeManager>>,
pub blocked_ips: Arc<BlockedIps>,
}
#[derive(Debug)]

View File

@@ -1,160 +0,0 @@
/*
* Copyright (c) 2023 Stalwart Labs Ltd.
*
* This file is part of the Stalwart Mail Server.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* in the LICENSE file at the top-level directory of this distribution.
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the AGPLv3 license by
* purchasing a commercial license. Please contact licensing@stalw.art
* for more details.
*/
use std::{
fmt::Debug,
net::IpAddr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use ahash::{AHashMap, AHashSet};
use arc_swap::{ArcSwap, ArcSwapOption};
use parking_lot::{Mutex, RwLock};
use crate::config::{ipmask::IpAddrMask, utils::ParseKey, Config, ConfigKey, Rate};
use super::limiter::RateLimiter;
pub struct BlockedIps {
ip_addresses: RwLock<AHashSet<IpAddr>>,
ip_networks: ArcSwap<Vec<IpAddrMask>>,
has_networks: AtomicBool,
limiters: Mutex<AHashMap<LimitBy, RateLimiter>>,
limiter_rate: ArcSwapOption<Rate>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
enum LimitBy {
IpAddr(IpAddr),
Login(String),
}
pub const BLOCKED_IP_KEY: &str = "server.security.blocked-networks";
impl BlockedIps {
pub fn new() -> Self {
Self {
ip_addresses: RwLock::new(AHashSet::new()),
ip_networks: ArcSwap::new(Arc::new(Vec::new())),
limiters: Mutex::new(Default::default()),
limiter_rate: ArcSwapOption::empty(),
has_networks: AtomicBool::new(false),
}
}
pub fn reload(&self, config: &Config) -> crate::config::Result<()> {
self.limiter_rate.store(
config
.property::<Rate>("server.security.fail2ban")?
.map(Arc::new),
);
self.reload_blocked_ips(config)
}
pub fn reload_blocked_ips(&self, config: &Config) -> crate::config::Result<()> {
let mut ip_addresses = AHashSet::new();
let mut ip_networks = Vec::new();
for ip in config.set_values(BLOCKED_IP_KEY) {
if ip.contains('/') {
ip_networks.push(ip.parse_key(BLOCKED_IP_KEY)?);
} else {
ip_addresses.insert(ip.parse_key(BLOCKED_IP_KEY)?);
}
}
self.has_networks
.store(!ip_networks.is_empty(), Ordering::Relaxed);
*self.ip_addresses.write() = ip_addresses;
self.ip_networks.store(Arc::new(ip_networks));
Ok(())
}
pub fn is_fail2banned(&self, ip: IpAddr, login: String) -> Option<ConfigKey> {
if let Some(rate) = self.limiter_rate.load().as_ref() {
let is_allowed = self
.limiters
.lock()
.entry(LimitBy::IpAddr(ip))
.or_insert_with(|| RateLimiter::new(rate))
.is_allowed(rate)
&& self
.limiters
.lock()
.entry(LimitBy::Login(login))
.or_insert_with(|| RateLimiter::new(rate))
.is_allowed(rate);
if !is_allowed {
self.ip_addresses.write().insert(ip);
return Some(ConfigKey {
key: format!("{}.{}", BLOCKED_IP_KEY, ip),
value: String::new(),
});
}
}
None
}
pub fn has_fail2ban(&self) -> bool {
self.limiter_rate.load().is_some()
}
pub fn cleanup(&self) {
self.limiters
.lock()
.retain(|_, limiter| limiter.is_active());
}
pub fn is_blocked(&self, ip: &IpAddr) -> bool {
self.ip_addresses.read().contains(ip)
|| (self.has_networks.load(Ordering::Relaxed)
&& self
.ip_networks
.load()
.iter()
.any(|network| network.matches(ip)))
}
}
impl Debug for BlockedIps {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BlockedIps")
.field("ip_addresses", &self.ip_addresses)
.field("ip_networks", &self.ip_networks)
.field("limiters", &self.limiters)
.field("limiter_rate", &self.limiter_rate)
.finish()
}
}
impl Default for BlockedIps {
fn default() -> Self {
Self::new()
}
}

View File

@@ -63,7 +63,6 @@ impl Server {
hostname: self.hostname,
acceptor: self.acceptor,
proxy_networks: self.proxy_networks,
blocked_ips: self.blocked_ips,
limiter: ConcurrencyLimiter::new(self.max_connections),
shutdown_rx,
});
@@ -116,7 +115,7 @@ impl Server {
.proxied_address()
.map(|addr| addr.source)
.unwrap_or(remote_addr);
if let Some(session) = instance.build_session(stream, local_ip, remote_addr) {
if let Some(session) = instance.build_session(stream, local_ip, remote_addr, &manager) {
// Spawn session
manager.spawn(session, is_tls);
}
@@ -131,7 +130,7 @@ impl Server {
}
}
});
} else if let Some(session) = instance.build_session(stream, local_ip, remote_addr) {
} else if let Some(session) = instance.build_session(stream, local_ip, remote_addr, &manager) {
// Set socket options
opts.apply(&session.stream);
@@ -165,20 +164,22 @@ impl Server {
}
trait BuildSession {
fn build_session<T: SessionStream>(
fn build_session<T: SessionStream, M: SessionManager>(
&self,
stream: T,
local_ip: IpAddr,
remote_addr: SocketAddr,
manager: &M,
) -> Option<SessionData<T>>;
}
impl BuildSession for Arc<ServerInstance> {
fn build_session<T: SessionStream>(
fn build_session<T: SessionStream, M: SessionManager>(
&self,
stream: T,
local_ip: IpAddr,
remote_addr: SocketAddr,
manager: &M,
) -> Option<SessionData<T>> {
// Convert mapped IPv6 addresses to IPv4
let remote_ip = match remote_addr.ip() {
@@ -191,7 +192,7 @@ impl BuildSession for Arc<ServerInstance> {
let remote_port = remote_addr.port();
// Check if blocked
if self.blocked_ips.is_blocked(&remote_ip) {
if manager.is_ip_blocked(&remote_ip) {
tracing::debug!(
context = "listener",
event = "blocked",

View File

@@ -35,12 +35,8 @@ use tokio::{
};
use tokio_rustls::{Accept, TlsAcceptor};
use self::{
blocked::BlockedIps,
limiter::{ConcurrencyLimiter, InFlight},
};
use self::limiter::{ConcurrencyLimiter, InFlight};
pub mod blocked;
pub mod limiter;
pub mod listen;
pub mod stream;
@@ -55,7 +51,6 @@ pub struct ServerInstance {
pub acceptor: TcpAcceptor,
pub limiter: ConcurrencyLimiter,
pub proxy_networks: Vec<IpAddrMask>,
pub blocked_ips: Arc<BlockedIps>,
pub shutdown_rx: watch::Receiver<bool>,
}
@@ -144,6 +139,7 @@ pub trait SessionManager: Sync + Send + 'static + Clone {
self,
session: SessionData<T>,
) -> impl std::future::Future<Output = ()> + Send;
fn is_ip_blocked(&self, addr: &IpAddr) -> bool;
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send;
}