Files
Stalwart/crates/smtp/src/core/throttle.rs
2026-03-11 17:00:40 +01:00

269 lines
8.4 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::Session;
use common::{
KV_RATE_LIMIT_SMTP, ThrottleKey, config::smtp::*, expr::functions::ResolveVariable,
network::SessionStream,
};
use queue::QueueQuota;
use registry::schema::{enums::ExpressionVariable, prelude::Property, structs::Rate};
use trc::SmtpEvent;
pub trait NewKey: Sized {
fn new_key(&self, e: &impl ResolveVariable, context: &str) -> ThrottleKey;
}
impl NewKey for QueueQuota {
fn new_key(&self, e: &impl ResolveVariable, _: &str) -> ThrottleKey {
let mut hasher = blake3::Hasher::new();
if (self.keys & THROTTLE_RCPT) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Rcpt)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RcptDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER) != 0 {
let sender = e.resolve_variable(ExpressionVariable::Sender).into_string();
hasher.update(
if !sender.is_empty() {
sender.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 {
let sender_domain = e
.resolve_variable(ExpressionVariable::SenderDomain)
.into_string();
hasher.update(
if !sender_domain.is_empty() {
sender_domain.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if let Some(messages) = &self.messages {
hasher.update(&messages.to_ne_bytes()[..]);
}
if let Some(size) = &self.size {
hasher.update(&size.to_ne_bytes()[..]);
}
ThrottleKey {
hash: hasher.finalize().into(),
}
}
}
impl NewKey for QueueRateLimiter {
fn new_key(&self, e: &impl ResolveVariable, context: &str) -> ThrottleKey {
let mut hasher = blake3::Hasher::new();
if (self.keys & THROTTLE_RCPT) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Rcpt)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RcptDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER) != 0 {
let sender = e.resolve_variable(ExpressionVariable::Sender).into_string();
hasher.update(
if !sender.is_empty() {
sender.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 {
let sender_domain = e
.resolve_variable(ExpressionVariable::SenderDomain)
.into_string();
hasher.update(
if !sender_domain.is_empty() {
sender_domain.as_ref()
} else {
"<>"
}
.as_bytes(),
);
}
if (self.keys & THROTTLE_HELO_DOMAIN) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::HeloDomain)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_AUTH_AS) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::AuthenticatedAs)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_LISTENER) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Listener)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_MX) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::Mx)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_REMOTE_IP) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::RemoteIp)
.to_string()
.as_bytes(),
);
}
if (self.keys & THROTTLE_LOCAL_IP) != 0 {
hasher.update(
e.resolve_variable(ExpressionVariable::LocalIp)
.to_string()
.as_bytes(),
);
}
hasher.update(&self.rate.period.as_secs().to_be_bytes()[..]);
hasher.update(&self.rate.count.to_be_bytes()[..]);
hasher.update(context.as_bytes());
ThrottleKey {
hash: hasher.finalize().into(),
}
}
}
impl<T: SessionStream> Session<T> {
pub async fn is_allowed(&mut self) -> bool {
let throttles = if !self.data.rcpt_to.is_empty() {
&self.server.core.smtp.queue.inbound_limiters.rcpt
} else if self.data.mail_from.is_some() {
&self.server.core.smtp.queue.inbound_limiters.sender
} else {
&self.server.core.smtp.queue.inbound_limiters.remote
};
for t in throttles {
if t.expr.is_empty()
|| self
.server
.eval_expr(&t.expr, self, t.id, Property::Match, self.data.session_id)
.await
.unwrap_or(false)
{
if (t.keys & THROTTLE_RCPT_DOMAIN) != 0 {
let d = self
.data
.rcpt_to
.last()
.map(|r| r.domain.as_str())
.unwrap_or_default();
if self.data.rcpt_to.iter().filter(|p| p.domain == d).count() > 1 {
continue;
}
}
// Build throttle key
let key = t.new_key(self, "inbound");
// Check rate
match self
.server
.in_memory_store()
.is_rate_allowed(KV_RATE_LIMIT_SMTP, key.hash.as_slice(), &t.rate, false)
.await
{
Ok(Some(_)) => {
trc::event!(
Smtp(SmtpEvent::RateLimitExceeded),
SpanId = self.data.session_id,
Id = t.id.to_string(),
Limit = vec![
trc::Value::from(t.rate.count),
trc::Value::from(t.rate.period.into_inner())
],
);
return false;
}
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
);
}
_ => (),
}
}
}
true
}
pub async fn throttle_rcpt(&self, rcpt: &str, rate: &Rate, ctx: &str) -> bool {
let mut hasher = blake3::Hasher::new();
hasher.update(rcpt.as_bytes());
hasher.update(ctx.as_bytes());
hasher.update(&rate.period.as_secs().to_ne_bytes()[..]);
hasher.update(&rate.count.to_ne_bytes()[..]);
match self
.server
.in_memory_store()
.is_rate_allowed(
KV_RATE_LIMIT_SMTP,
hasher.finalize().as_bytes(),
rate,
false,
)
.await
{
Ok(None) => true,
Ok(Some(_)) => false,
Err(err) => {
trc::error!(
err.span_id(self.data.session_id)
.caused_by(trc::location!())
);
true
}
}
}
}