Distributed SMTP queues and Rate limiting passing tests
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user