mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-11-28 00:56:45 +00:00
SYN flood, brute force fail2ban + session.mail.is-allowed expression (closes #482 closes #688 closes #609)
This commit is contained in:
parent
7e1b6bd06d
commit
36fd5797b7
35 changed files with 325 additions and 114 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -2,6 +2,25 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [0.9.3] - 2024-08-29
|
||||||
|
|
||||||
|
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
- Dashboard (Enterprise feature)
|
||||||
|
- Alerts (Enterprise feature)
|
||||||
|
- SYN Flood (session "loitering") attack protection (#482)
|
||||||
|
- Mailbox brute force protection (#688)
|
||||||
|
- Mail from is allowed (`session.mail.is-allowed`) expression (#609)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `authentication.fail2ban` setting renamed to `server.fail2ban.authentication`.
|
||||||
|
- Added elapsed times to message filtering events.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Include queueId in MTA Hooks (#708)
|
||||||
|
- Do not insert empty keywords in FTS index.
|
||||||
|
|
||||||
## [0.9.2] - 2024-08-21
|
## [0.9.2] - 2024-08-21
|
||||||
|
|
||||||
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
|
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin.
|
||||||
|
|
26
Cargo.lock
generated
26
Cargo.lock
generated
|
@ -1042,7 +1042,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
@ -1650,7 +1650,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "directory"
|
name = "directory"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
@ -2979,7 +2979,7 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "imap"
|
name = "imap"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"common",
|
"common",
|
||||||
|
@ -3191,7 +3191,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jmap"
|
name = "jmap"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
@ -3629,7 +3629,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mail-server"
|
name = "mail-server"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
"common",
|
||||||
"directory",
|
"directory",
|
||||||
|
@ -3648,7 +3648,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "managesieve"
|
name = "managesieve"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
@ -3947,7 +3947,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nlp"
|
name = "nlp"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
@ -4498,7 +4498,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pop3"
|
name = "pop3"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"common",
|
"common",
|
||||||
"imap",
|
"imap",
|
||||||
|
@ -6050,7 +6050,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smtp"
|
name = "smtp"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
@ -6166,7 +6166,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stalwart-cli"
|
name = "stalwart-cli"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"console",
|
"console",
|
||||||
|
@ -6197,7 +6197,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "store"
|
name = "store"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
@ -6824,7 +6824,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trc"
|
name = "trc"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
@ -7067,7 +7067,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utils"
|
name = "utils"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
|
@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <hello@stalw.art>"]
|
||||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||||
repository = "https://github.com/stalwartlabs/cli"
|
repository = "https://github.com/stalwartlabs/cli"
|
||||||
homepage = "https://github.com/stalwartlabs/cli"
|
homepage = "https://github.com/stalwartlabs/cli"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "common"
|
name = "common"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,7 @@ pub struct Auth {
|
||||||
pub struct Mail {
|
pub struct Mail {
|
||||||
pub script: IfBlock,
|
pub script: IfBlock,
|
||||||
pub rewrite: IfBlock,
|
pub rewrite: IfBlock,
|
||||||
|
pub is_allowed: IfBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -366,6 +367,11 @@ impl SessionConfig {
|
||||||
"session.mail.rewrite",
|
"session.mail.rewrite",
|
||||||
&has_sender_vars,
|
&has_sender_vars,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
&mut session.mail.is_allowed,
|
||||||
|
"session.mail.is-allowed",
|
||||||
|
&has_sender_vars,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
&mut session.rcpt.script,
|
&mut session.rcpt.script,
|
||||||
"session.rcpt.script",
|
"session.rcpt.script",
|
||||||
|
@ -761,6 +767,11 @@ impl Default for SessionConfig {
|
||||||
mail: Mail {
|
mail: Mail {
|
||||||
script: IfBlock::empty("session.mail.script"),
|
script: IfBlock::empty("session.mail.script"),
|
||||||
rewrite: IfBlock::empty("session.mail.rewrite"),
|
rewrite: IfBlock::empty("session.mail.rewrite"),
|
||||||
|
is_allowed: IfBlock::new::<()>(
|
||||||
|
"session.mail.is-allowed",
|
||||||
|
[],
|
||||||
|
"!is_empty(authenticated_as) || !key_exists('spam-block', sender_domain)",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
rcpt: Rcpt {
|
rcpt: Rcpt {
|
||||||
script: IfBlock::empty("session.rcpt.script"),
|
script: IfBlock::empty("session.rcpt.script"),
|
||||||
|
|
|
@ -291,10 +291,10 @@ impl Core {
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
Err(err)
|
Err(err)
|
||||||
} else if self.has_fail2ban() {
|
} else if self.has_auth_fail2ban() {
|
||||||
let login = credentials.login();
|
let login = credentials.login();
|
||||||
if self.is_fail2banned(remote_ip, login.to_string()).await? {
|
if self.is_auth_fail2banned(remote_ip, login).await? {
|
||||||
Err(trc::AuthEvent::Banned
|
Err(trc::SecurityEvent::AuthenticationBan
|
||||||
.into_err()
|
.into_err()
|
||||||
.ctx(trc::Key::RemoteIp, remote_ip)
|
.ctx(trc::Key::RemoteIp, remote_ip)
|
||||||
.ctx(trc::Key::AccountName, login.to_string()))
|
.ctx(trc::Key::AccountName, login.to_string()))
|
||||||
|
|
|
@ -21,7 +21,9 @@ pub struct BlockedIps {
|
||||||
pub version: AtomicU8,
|
pub version: AtomicU8,
|
||||||
ip_networks: Vec<IpAddrMask>,
|
ip_networks: Vec<IpAddrMask>,
|
||||||
has_networks: bool,
|
has_networks: bool,
|
||||||
limiter_rate: Option<Rate>,
|
auth_fail_rate: Option<Rate>,
|
||||||
|
rcpt_fail_rate: Option<Rate>,
|
||||||
|
loiter_fail_rate: Option<Rate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -63,7 +65,15 @@ impl BlockedIps {
|
||||||
ip_addresses: RwLock::new(ip_addresses),
|
ip_addresses: RwLock::new(ip_addresses),
|
||||||
has_networks: !ip_networks.is_empty(),
|
has_networks: !ip_networks.is_empty(),
|
||||||
ip_networks,
|
ip_networks,
|
||||||
limiter_rate: config.property_or_default::<Rate>("authentication.fail2ban", "100/1d"),
|
auth_fail_rate: config
|
||||||
|
.property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d")
|
||||||
|
.unwrap_or_default(),
|
||||||
|
rcpt_fail_rate: config
|
||||||
|
.property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d")
|
||||||
|
.unwrap_or_default(),
|
||||||
|
loiter_fail_rate: config
|
||||||
|
.property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d")
|
||||||
|
.unwrap_or_default(),
|
||||||
version: 0.into(),
|
version: 0.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,46 +118,86 @@ impl AllowedIps {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub async fn is_fail2banned(&self, ip: IpAddr, login: String) -> trc::Result<bool> {
|
pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
|
||||||
if let Some(rate) = &self.network.blocked_ips.limiter_rate {
|
if let Some(rate) = &self.network.blocked_ips.rcpt_fail_rate {
|
||||||
let is_allowed = self.is_ip_allowed(&ip)
|
let is_allowed = self.is_ip_allowed(&ip)
|
||||||
|| (self
|
|| self
|
||||||
.storage
|
.storage
|
||||||
.lookup
|
.lookup
|
||||||
.is_rate_allowed(format!("b:{}", ip).as_bytes(), rate, false)
|
.is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false)
|
||||||
.await?
|
.await?
|
||||||
.is_none()
|
.is_none();
|
||||||
&& self
|
|
||||||
.storage
|
|
||||||
.lookup
|
|
||||||
.is_rate_allowed(format!("b:{}", login).as_bytes(), rate, false)
|
|
||||||
.await?
|
|
||||||
.is_none());
|
|
||||||
if !is_allowed {
|
if !is_allowed {
|
||||||
// Add IP to blocked list
|
return self.block_ip(ip).await.map(|_| true);
|
||||||
self.network.blocked_ips.ip_addresses.write().insert(ip);
|
|
||||||
|
|
||||||
// Write blocked IP to config
|
|
||||||
self.storage
|
|
||||||
.config
|
|
||||||
.set([ConfigKey {
|
|
||||||
key: format!("{}.{}", BLOCKED_IP_KEY, ip),
|
|
||||||
value: String::new(),
|
|
||||||
}])
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Increment version
|
|
||||||
self.network.blocked_ips.increment_version();
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_fail2ban(&self) -> bool {
|
pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> {
|
||||||
self.network.blocked_ips.limiter_rate.is_some()
|
if let Some(rate) = &self.network.blocked_ips.loiter_fail_rate {
|
||||||
|
let is_allowed = self.is_ip_allowed(&ip)
|
||||||
|
|| self
|
||||||
|
.storage
|
||||||
|
.lookup
|
||||||
|
.is_rate_allowed(format!("l:{ip}").as_bytes(), rate, false)
|
||||||
|
.await?
|
||||||
|
.is_none();
|
||||||
|
|
||||||
|
if !is_allowed {
|
||||||
|
return self.block_ip(ip).await.map(|_| true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_auth_fail2banned(&self, ip: IpAddr, login: &str) -> trc::Result<bool> {
|
||||||
|
if let Some(rate) = &self.network.blocked_ips.auth_fail_rate {
|
||||||
|
let is_allowed = self.is_ip_allowed(&ip)
|
||||||
|
|| (self
|
||||||
|
.storage
|
||||||
|
.lookup
|
||||||
|
.is_rate_allowed(format!("b:{ip}").as_bytes(), rate, false)
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
|
&& self
|
||||||
|
.storage
|
||||||
|
.lookup
|
||||||
|
.is_rate_allowed(format!("b:{login}").as_bytes(), rate, false)
|
||||||
|
.await?
|
||||||
|
.is_none());
|
||||||
|
if !is_allowed {
|
||||||
|
return self.block_ip(ip).await.map(|_| true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn block_ip(&self, ip: IpAddr) -> trc::Result<()> {
|
||||||
|
// Add IP to blocked list
|
||||||
|
self.network.blocked_ips.ip_addresses.write().insert(ip);
|
||||||
|
|
||||||
|
// Write blocked IP to config
|
||||||
|
self.storage
|
||||||
|
.config
|
||||||
|
.set([ConfigKey {
|
||||||
|
key: format!("{}.{}", BLOCKED_IP_KEY, ip),
|
||||||
|
value: String::new(),
|
||||||
|
}])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Increment version
|
||||||
|
self.network.blocked_ips.increment_version();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_auth_fail2ban(&self) -> bool {
|
||||||
|
self.network.blocked_ips.auth_fail_rate.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool {
|
pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool {
|
||||||
|
@ -186,8 +236,10 @@ impl Default for BlockedIps {
|
||||||
ip_addresses: RwLock::new(AHashSet::new()),
|
ip_addresses: RwLock::new(AHashSet::new()),
|
||||||
ip_networks: Default::default(),
|
ip_networks: Default::default(),
|
||||||
has_networks: Default::default(),
|
has_networks: Default::default(),
|
||||||
limiter_rate: Default::default(),
|
|
||||||
version: Default::default(),
|
version: Default::default(),
|
||||||
|
auth_fail_rate: Default::default(),
|
||||||
|
rcpt_fail_rate: Default::default(),
|
||||||
|
loiter_fail_rate: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,11 +268,13 @@ impl Clone for BlockedIps {
|
||||||
ip_addresses: RwLock::new(self.ip_addresses.read().clone()),
|
ip_addresses: RwLock::new(self.ip_addresses.read().clone()),
|
||||||
ip_networks: self.ip_networks.clone(),
|
ip_networks: self.ip_networks.clone(),
|
||||||
has_networks: self.has_networks,
|
has_networks: self.has_networks,
|
||||||
limiter_rate: self.limiter_rate.clone(),
|
|
||||||
version: self
|
version: self
|
||||||
.version
|
.version
|
||||||
.load(std::sync::atomic::Ordering::Relaxed)
|
.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
.into(),
|
.into(),
|
||||||
|
auth_fail_rate: self.auth_fail_rate.clone(),
|
||||||
|
rcpt_fail_rate: self.rcpt_fail_rate.clone(),
|
||||||
|
loiter_fail_rate: self.loiter_fail_rate.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,7 +284,11 @@ impl Debug for BlockedIps {
|
||||||
f.debug_struct("BlockedIps")
|
f.debug_struct("BlockedIps")
|
||||||
.field("ip_addresses", &self.ip_addresses)
|
.field("ip_addresses", &self.ip_addresses)
|
||||||
.field("ip_networks", &self.ip_networks)
|
.field("ip_networks", &self.ip_networks)
|
||||||
.field("limiter_rate", &self.limiter_rate)
|
.field("has_networks", &self.has_networks)
|
||||||
|
.field("version", &self.version)
|
||||||
|
.field("auth_fail_rate", &self.auth_fail_rate)
|
||||||
|
.field("rcpt_fail_rate", &self.rcpt_fail_rate)
|
||||||
|
.field("loiter_fail_rate", &self.loiter_fail_rate)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,7 +230,7 @@ impl BuildSession for Arc<ServerInstance> {
|
||||||
// Check if blocked
|
// Check if blocked
|
||||||
if core.is_ip_blocked(&remote_ip) {
|
if core.is_ip_blocked(&remote_ip) {
|
||||||
trc::event!(
|
trc::event!(
|
||||||
Network(trc::NetworkEvent::DropBlocked),
|
Security(trc::SecurityEvent::IpBlocked),
|
||||||
ListenerId = self.id.clone(),
|
ListenerId = self.id.clone(),
|
||||||
LocalPort = local_addr.port(),
|
LocalPort = local_addr.port(),
|
||||||
RemoteIp = remote_ip,
|
RemoteIp = remote_ip,
|
||||||
|
|
|
@ -105,8 +105,10 @@ impl MetricsStore for Store {
|
||||||
EventType::MessageIngest(MessageIngestEvent::Ham),
|
EventType::MessageIngest(MessageIngestEvent::Ham),
|
||||||
EventType::MessageIngest(MessageIngestEvent::Spam),
|
EventType::MessageIngest(MessageIngestEvent::Spam),
|
||||||
EventType::Auth(AuthEvent::Failed),
|
EventType::Auth(AuthEvent::Failed),
|
||||||
EventType::Auth(AuthEvent::Banned),
|
EventType::Security(SecurityEvent::AuthenticationBan),
|
||||||
EventType::Network(NetworkEvent::DropBlocked),
|
EventType::Security(SecurityEvent::BruteForceBan),
|
||||||
|
EventType::Security(SecurityEvent::LoiterBan),
|
||||||
|
EventType::Security(SecurityEvent::IpBlocked),
|
||||||
EventType::IncomingReport(IncomingReportEvent::DmarcReport),
|
EventType::IncomingReport(IncomingReportEvent::DmarcReport),
|
||||||
EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings),
|
EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings),
|
||||||
EventType::IncomingReport(IncomingReportEvent::TlsReport),
|
EventType::IncomingReport(IncomingReportEvent::TlsReport),
|
||||||
|
|
|
@ -418,12 +418,12 @@ impl StoreTracer {
|
||||||
AuthEvent::Success
|
AuthEvent::Success
|
||||||
| AuthEvent::Failed
|
| AuthEvent::Failed
|
||||||
| AuthEvent::TooManyAttempts
|
| AuthEvent::TooManyAttempts
|
||||||
| AuthEvent::Banned
|
|
||||||
| AuthEvent::Error
|
| AuthEvent::Error
|
||||||
)
|
)
|
||||||
| EventType::Sieve(_)
|
| EventType::Sieve(_)
|
||||||
| EventType::Milter(_)
|
| EventType::Milter(_)
|
||||||
| EventType::MtaHook(_)
|
| EventType::MtaHook(_)
|
||||||
|
| EventType::Security(_)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "directory"
|
name = "directory"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "imap"
|
name = "imap"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "jmap"
|
name = "jmap"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -885,11 +885,10 @@ impl ToRequestError for trc::Error {
|
||||||
trc::AuthEvent::MissingTotp => {
|
trc::AuthEvent::MissingTotp => {
|
||||||
RequestError::blank(403, "TOTP code required", cause.message())
|
RequestError::blank(403, "TOTP code required", cause.message())
|
||||||
}
|
}
|
||||||
trc::AuthEvent::TooManyAttempts | trc::AuthEvent::Banned => {
|
trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(),
|
||||||
RequestError::too_many_auth_attempts()
|
|
||||||
}
|
|
||||||
_ => RequestError::unauthorized(),
|
_ => RequestError::unauthorized(),
|
||||||
},
|
},
|
||||||
|
trc::EventType::Security(_) => RequestError::too_many_auth_attempts(),
|
||||||
trc::EventType::Resource(cause) => match cause {
|
trc::EventType::Resource(cause) => match cause {
|
||||||
trc::ResourceEvent::NotFound => RequestError::not_found(),
|
trc::ResourceEvent::NotFound => RequestError::not_found(),
|
||||||
trc::ResourceEvent::BadParameters => RequestError::blank(
|
trc::ResourceEvent::BadParameters => RequestError::blank(
|
||||||
|
|
|
@ -7,7 +7,7 @@ homepage = "https://stalw.art"
|
||||||
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
|
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
|
||||||
categories = ["email"]
|
categories = ["email"]
|
||||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "managesieve"
|
name = "managesieve"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "nlp"
|
name = "nlp"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pop3"
|
name = "pop3"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
|
||||||
keywords = ["smtp", "email", "mail", "server"]
|
keywords = ["smtp", "email", "mail", "server"]
|
||||||
categories = ["email"]
|
categories = ["email"]
|
||||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,7 @@ impl<T: SessionStream> Session<T> {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
trc::EventType::Auth(trc::AuthEvent::Banned) => {
|
trc::EventType::Security(_) => {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
|
@ -120,6 +120,29 @@ impl<T: SessionStream> Session<T> {
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
// Check whether the address is allowed
|
||||||
|
if !self
|
||||||
|
.core
|
||||||
|
.core
|
||||||
|
.eval_if::<bool, _>(
|
||||||
|
&self.core.core.smtp.session.mail.is_allowed,
|
||||||
|
self,
|
||||||
|
self.data.session_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
let mail_from = self.data.mail_from.take().unwrap();
|
||||||
|
trc::event!(
|
||||||
|
Smtp(SmtpEvent::MailFromNotAllowed),
|
||||||
|
From = mail_from.address_lcase,
|
||||||
|
SpanId = self.data.session_id,
|
||||||
|
);
|
||||||
|
return self
|
||||||
|
.write(b"550 5.7.1 Sender address not allowed.\r\n")
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
// Sieve filtering
|
// Sieve filtering
|
||||||
if let Some((script, script_id)) = self
|
if let Some((script, script_id)) = self
|
||||||
.core
|
.core
|
||||||
|
|
|
@ -8,7 +8,7 @@ use common::{config::smtp::session::Stage, listener::SessionStream, scripts::Scr
|
||||||
use smtp_proto::{
|
use smtp_proto::{
|
||||||
RcptTo, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
|
RcptTo, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
|
||||||
};
|
};
|
||||||
use trc::SmtpEvent;
|
use trc::{SecurityEvent, SmtpEvent};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{Session, SessionAddress},
|
core::{Session, SessionAddress},
|
||||||
|
@ -315,11 +315,33 @@ impl<T: SessionStream> Session<T> {
|
||||||
if self.data.rcpt_errors < self.params.rcpt_errors_max {
|
if self.data.rcpt_errors < self.params.rcpt_errors_max {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
trc::event!(
|
match self
|
||||||
Smtp(SmtpEvent::TooManyInvalidRcpt),
|
.core
|
||||||
SpanId = self.data.session_id,
|
.core
|
||||||
Limit = self.params.rcpt_errors_max,
|
.is_rcpt_fail2banned(self.data.remote_ip)
|
||||||
);
|
.await
|
||||||
|
{
|
||||||
|
Ok(true) => {
|
||||||
|
trc::event!(
|
||||||
|
Security(SecurityEvent::BruteForceBan),
|
||||||
|
SpanId = self.data.session_id,
|
||||||
|
RemoteIp = self.data.remote_ip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
trc::event!(
|
||||||
|
Smtp(SmtpEvent::TooManyInvalidRcpt),
|
||||||
|
SpanId = self.data.session_id,
|
||||||
|
Limit = self.params.rcpt_errors_max,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
trc::error!(err
|
||||||
|
.span_id(self.data.session_id)
|
||||||
|
.caused_by(trc::location!())
|
||||||
|
.details("Failed to check if IP should be banned."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.write(b"421 4.3.0 Too many errors, disconnecting.\r\n")
|
self.write(b"421 4.3.0 Too many errors, disconnecting.\r\n")
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -11,7 +11,7 @@ use common::{
|
||||||
listener::{self, SessionManager, SessionStream},
|
listener::{self, SessionManager, SessionStream},
|
||||||
};
|
};
|
||||||
use tokio_rustls::server::TlsStream;
|
use tokio_rustls::server::TlsStream;
|
||||||
use trc::SmtpEvent;
|
use trc::{SecurityEvent, SmtpEvent};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{Session, SessionData, SessionParameters, SmtpSessionManager, State},
|
core::{Session, SessionData, SessionParameters, SmtpSessionManager, State},
|
||||||
|
@ -194,10 +194,32 @@ impl<T: SessionStream> Session<T> {
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
trc::event!(
|
match self
|
||||||
Smtp(SmtpEvent::TimeLimitExceeded),
|
.core
|
||||||
SpanId = self.data.session_id,
|
.core
|
||||||
);
|
.is_loiter_fail2banned(self.data.remote_ip)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(true) => {
|
||||||
|
trc::event!(
|
||||||
|
Security(SecurityEvent::LoiterBan),
|
||||||
|
SpanId = self.data.session_id,
|
||||||
|
RemoteIp = self.data.remote_ip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
trc::event!(
|
||||||
|
Smtp(SmtpEvent::TimeLimitExceeded),
|
||||||
|
SpanId = self.data.session_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
trc::error!(err
|
||||||
|
.span_id(self.data.session_id)
|
||||||
|
.caused_by(trc::location!())
|
||||||
|
.details("Failed to check if IP should be banned."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "store"
|
name = "store"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "trc"
|
name = "trc"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ impl EventType {
|
||||||
EventType::OutgoingReport(event) => event.description(),
|
EventType::OutgoingReport(event) => event.description(),
|
||||||
EventType::Telemetry(event) => event.description(),
|
EventType::Telemetry(event) => event.description(),
|
||||||
EventType::MessageIngest(event) => event.description(),
|
EventType::MessageIngest(event) => event.description(),
|
||||||
|
EventType::Security(event) => event.description(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +97,7 @@ impl EventType {
|
||||||
EventType::OutgoingReport(event) => event.explain(),
|
EventType::OutgoingReport(event) => event.explain(),
|
||||||
EventType::Telemetry(event) => event.explain(),
|
EventType::Telemetry(event) => event.explain(),
|
||||||
EventType::MessageIngest(event) => event.explain(),
|
EventType::MessageIngest(event) => event.explain(),
|
||||||
|
EventType::Security(event) => event.explain(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,6 +433,7 @@ impl SmtpEvent {
|
||||||
SmtpEvent::MailFromUnauthorized => "MAIL FROM unauthorized",
|
SmtpEvent::MailFromUnauthorized => "MAIL FROM unauthorized",
|
||||||
SmtpEvent::MailFromRewritten => "MAIL FROM address rewritten",
|
SmtpEvent::MailFromRewritten => "MAIL FROM address rewritten",
|
||||||
SmtpEvent::MailFromMissing => "MAIL FROM address missing",
|
SmtpEvent::MailFromMissing => "MAIL FROM address missing",
|
||||||
|
SmtpEvent::MailFromNotAllowed => "MAIL FROM not allowed",
|
||||||
SmtpEvent::MailFrom => "SMTP MAIL FROM command",
|
SmtpEvent::MailFrom => "SMTP MAIL FROM command",
|
||||||
SmtpEvent::MultipleMailFrom => "Multiple MAIL FROM commands",
|
SmtpEvent::MultipleMailFrom => "Multiple MAIL FROM commands",
|
||||||
SmtpEvent::MailboxDoesNotExist => "Mailbox does not exist",
|
SmtpEvent::MailboxDoesNotExist => "Mailbox does not exist",
|
||||||
|
@ -536,6 +539,9 @@ impl SmtpEvent {
|
||||||
SmtpEvent::MailFromMissing => {
|
SmtpEvent::MailFromMissing => {
|
||||||
"The remote client issued an RCPT TO command before MAIL FROM"
|
"The remote client issued an RCPT TO command before MAIL FROM"
|
||||||
}
|
}
|
||||||
|
SmtpEvent::MailFromNotAllowed => {
|
||||||
|
"The remote client is not allowed to send mail from this address"
|
||||||
|
}
|
||||||
SmtpEvent::MailFrom => "The remote client sent a MAIL FROM command",
|
SmtpEvent::MailFrom => "The remote client sent a MAIL FROM command",
|
||||||
SmtpEvent::MultipleMailFrom => "The remote client already sent a MAIL FROM command",
|
SmtpEvent::MultipleMailFrom => "The remote client already sent a MAIL FROM command",
|
||||||
SmtpEvent::MailboxDoesNotExist => "The mailbox does not exist on the server",
|
SmtpEvent::MailboxDoesNotExist => "The mailbox does not exist on the server",
|
||||||
|
@ -1114,7 +1120,6 @@ impl NetworkEvent {
|
||||||
NetworkEvent::Closed => "Network connection closed",
|
NetworkEvent::Closed => "Network connection closed",
|
||||||
NetworkEvent::ProxyError => "Proxy protocol error",
|
NetworkEvent::ProxyError => "Proxy protocol error",
|
||||||
NetworkEvent::SetOptError => "Network set option error",
|
NetworkEvent::SetOptError => "Network set option error",
|
||||||
NetworkEvent::DropBlocked => "Dropped connection from blocked IP address",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1133,7 +1138,6 @@ impl NetworkEvent {
|
||||||
NetworkEvent::Closed => "The network connection was closed",
|
NetworkEvent::Closed => "The network connection was closed",
|
||||||
NetworkEvent::ProxyError => "An error occurred with the proxy protocol",
|
NetworkEvent::ProxyError => "An error occurred with the proxy protocol",
|
||||||
NetworkEvent::SetOptError => "An error occurred while setting network options",
|
NetworkEvent::SetOptError => "An error occurred while setting network options",
|
||||||
NetworkEvent::DropBlocked => "The connection was dropped from a blocked IP address",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1736,7 +1740,6 @@ impl AuthEvent {
|
||||||
AuthEvent::Failed => "Authentication failed",
|
AuthEvent::Failed => "Authentication failed",
|
||||||
AuthEvent::MissingTotp => "Missing TOTP for authentication",
|
AuthEvent::MissingTotp => "Missing TOTP for authentication",
|
||||||
AuthEvent::TooManyAttempts => "Too many authentication attempts",
|
AuthEvent::TooManyAttempts => "Too many authentication attempts",
|
||||||
AuthEvent::Banned => "IP address banned after multiple authentication failures",
|
|
||||||
AuthEvent::Error => "Authentication error",
|
AuthEvent::Error => "Authentication error",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1747,9 +1750,6 @@ impl AuthEvent {
|
||||||
AuthEvent::Failed => "Failed authentication",
|
AuthEvent::Failed => "Failed authentication",
|
||||||
AuthEvent::MissingTotp => "TOTP is missing for authentication",
|
AuthEvent::MissingTotp => "TOTP is missing for authentication",
|
||||||
AuthEvent::TooManyAttempts => "Too many authentication attempts have been made",
|
AuthEvent::TooManyAttempts => "Too many authentication attempts have been made",
|
||||||
AuthEvent::Banned => {
|
|
||||||
"The IP address has been banned after multiple authentication failures"
|
|
||||||
}
|
|
||||||
AuthEvent::Error => "An error occurred with authentication",
|
AuthEvent::Error => "An error occurred with authentication",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1776,3 +1776,27 @@ impl ResourceEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SecurityEvent {
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SecurityEvent::AuthenticationBan => "Banned due to authentication errors",
|
||||||
|
SecurityEvent::BruteForceBan => "Banned due to brute force attack",
|
||||||
|
SecurityEvent::LoiterBan => "Banned due to loitering",
|
||||||
|
SecurityEvent::IpBlocked => "Blocked IP address",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn explain(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SecurityEvent::AuthenticationBan => {
|
||||||
|
"IP address was banned due to multiple authentication errors"
|
||||||
|
}
|
||||||
|
SecurityEvent::BruteForceBan => {
|
||||||
|
"IP address was banned due to possible brute force attack"
|
||||||
|
}
|
||||||
|
SecurityEvent::LoiterBan => "IP address was banned due to multiple loitering events",
|
||||||
|
SecurityEvent::IpBlocked => "Rejected connection from blocked IP address",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -126,6 +126,7 @@ impl EventType {
|
||||||
| SmtpEvent::MailFromRewritten
|
| SmtpEvent::MailFromRewritten
|
||||||
| SmtpEvent::MailFromMissing
|
| SmtpEvent::MailFromMissing
|
||||||
| SmtpEvent::MultipleMailFrom
|
| SmtpEvent::MultipleMailFrom
|
||||||
|
| SmtpEvent::MailFromNotAllowed
|
||||||
| SmtpEvent::RcptToDuplicate
|
| SmtpEvent::RcptToDuplicate
|
||||||
| SmtpEvent::RcptToRewritten
|
| SmtpEvent::RcptToRewritten
|
||||||
| SmtpEvent::RcptToMissing
|
| SmtpEvent::RcptToMissing
|
||||||
|
@ -203,9 +204,7 @@ impl EventType {
|
||||||
| NetworkEvent::FlushError
|
| NetworkEvent::FlushError
|
||||||
| NetworkEvent::Closed => Level::Trace,
|
| NetworkEvent::Closed => Level::Trace,
|
||||||
NetworkEvent::Timeout | NetworkEvent::AcceptError => Level::Debug,
|
NetworkEvent::Timeout | NetworkEvent::AcceptError => Level::Debug,
|
||||||
NetworkEvent::ListenStart
|
NetworkEvent::ListenStart | NetworkEvent::ListenStop => Level::Info,
|
||||||
| NetworkEvent::ListenStop
|
|
||||||
| NetworkEvent::DropBlocked => Level::Info,
|
|
||||||
NetworkEvent::ListenError
|
NetworkEvent::ListenError
|
||||||
| NetworkEvent::BindError
|
| NetworkEvent::BindError
|
||||||
| NetworkEvent::SetOptError
|
| NetworkEvent::SetOptError
|
||||||
|
@ -228,7 +227,6 @@ impl EventType {
|
||||||
AuthEvent::Failed => Level::Debug,
|
AuthEvent::Failed => Level::Debug,
|
||||||
AuthEvent::MissingTotp => Level::Trace,
|
AuthEvent::MissingTotp => Level::Trace,
|
||||||
AuthEvent::TooManyAttempts => Level::Warn,
|
AuthEvent::TooManyAttempts => Level::Warn,
|
||||||
AuthEvent::Banned => Level::Warn,
|
|
||||||
AuthEvent::Error => Level::Error,
|
AuthEvent::Error => Level::Error,
|
||||||
AuthEvent::Success => Level::Info,
|
AuthEvent::Success => Level::Info,
|
||||||
},
|
},
|
||||||
|
@ -275,9 +273,9 @@ impl EventType {
|
||||||
| PurgeEvent::TombstoneCleanup => Level::Debug,
|
| PurgeEvent::TombstoneCleanup => Level::Debug,
|
||||||
},
|
},
|
||||||
EventType::Eval(event) => match event {
|
EventType::Eval(event) => match event {
|
||||||
EvalEvent::Error => Level::Debug,
|
EvalEvent::Error | EvalEvent::StoreNotFound => Level::Debug,
|
||||||
EvalEvent::Result => Level::Trace,
|
EvalEvent::Result => Level::Trace,
|
||||||
EvalEvent::DirectoryNotFound | EvalEvent::StoreNotFound => Level::Warn,
|
EvalEvent::DirectoryNotFound => Level::Warn,
|
||||||
},
|
},
|
||||||
EventType::Server(event) => match event {
|
EventType::Server(event) => match event {
|
||||||
ServerEvent::Startup | ServerEvent::Shutdown | ServerEvent::Licensing => {
|
ServerEvent::Startup | ServerEvent::Shutdown | ServerEvent::Licensing => {
|
||||||
|
@ -536,6 +534,7 @@ impl EventType {
|
||||||
| MessageIngestEvent::Duplicate => Level::Info,
|
| MessageIngestEvent::Duplicate => Level::Info,
|
||||||
MessageIngestEvent::Error => Level::Error,
|
MessageIngestEvent::Error => Level::Error,
|
||||||
},
|
},
|
||||||
|
EventType::Security(_) => Level::Info,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,17 +155,15 @@ impl Event<EventType> {
|
||||||
matches!(
|
matches!(
|
||||||
self.inner,
|
self.inner,
|
||||||
EventType::Network(_)
|
EventType::Network(_)
|
||||||
| EventType::Auth(AuthEvent::TooManyAttempts | AuthEvent::Banned)
|
| EventType::Auth(AuthEvent::TooManyAttempts)
|
||||||
| EventType::Limit(LimitEvent::ConcurrentRequest | LimitEvent::TooManyRequests)
|
| EventType::Limit(LimitEvent::ConcurrentRequest | LimitEvent::TooManyRequests)
|
||||||
|
| EventType::Security(_)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn should_write_err(&self) -> bool {
|
pub fn should_write_err(&self) -> bool {
|
||||||
!matches!(
|
!matches!(self.inner, EventType::Network(_) | EventType::Security(_))
|
||||||
self.inner,
|
|
||||||
EventType::Network(_) | EventType::Auth(AuthEvent::Banned)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn corrupted_key(key: &[u8], value: Option<&[u8]>, caused_by: &'static str) -> Error {
|
pub fn corrupted_key(key: &[u8], value: Option<&[u8]>, caused_by: &'static str) -> Error {
|
||||||
|
@ -317,6 +315,13 @@ impl StoreEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SecurityEvent {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn into_err(self) -> Error {
|
||||||
|
Error::new(EventType::Security(self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AuthEvent {
|
impl AuthEvent {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||||
|
@ -346,7 +351,6 @@ impl AuthEvent {
|
||||||
"Try authenticating again using 'secret$totp_token'."
|
"Try authenticating again using 'secret$totp_token'."
|
||||||
),
|
),
|
||||||
Self::TooManyAttempts => "Too many authentication attempts",
|
Self::TooManyAttempts => "Too many authentication attempts",
|
||||||
Self::Banned => "Banned",
|
|
||||||
_ => "Authentication error",
|
_ => "Authentication error",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -525,14 +525,14 @@ impl EventType {
|
||||||
| HttpEvent::ResponseBody
|
| HttpEvent::ResponseBody
|
||||||
| HttpEvent::XForwardedMissing,
|
| HttpEvent::XForwardedMissing,
|
||||||
) => true,
|
) => true,
|
||||||
EventType::Network(NetworkEvent::Timeout | NetworkEvent::DropBlocked) => true,
|
EventType::Network(NetworkEvent::Timeout) => true,
|
||||||
|
EventType::Security(_) => true,
|
||||||
EventType::Limit(_) => true,
|
EventType::Limit(_) => true,
|
||||||
EventType::Manage(_) => false,
|
EventType::Manage(_) => false,
|
||||||
EventType::Auth(
|
EventType::Auth(
|
||||||
AuthEvent::Success
|
AuthEvent::Success
|
||||||
| AuthEvent::Failed
|
| AuthEvent::Failed
|
||||||
| AuthEvent::TooManyAttempts
|
| AuthEvent::TooManyAttempts
|
||||||
| AuthEvent::Banned
|
|
||||||
| AuthEvent::Error,
|
| AuthEvent::Error,
|
||||||
) => true,
|
) => true,
|
||||||
EventType::Config(_) => false,
|
EventType::Config(_) => false,
|
||||||
|
|
|
@ -182,6 +182,7 @@ pub enum EventType {
|
||||||
IncomingReport(IncomingReportEvent),
|
IncomingReport(IncomingReportEvent),
|
||||||
OutgoingReport(OutgoingReportEvent),
|
OutgoingReport(OutgoingReportEvent),
|
||||||
Telemetry(TelemetryEvent),
|
Telemetry(TelemetryEvent),
|
||||||
|
Security(SecurityEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[event_type]
|
#[event_type]
|
||||||
|
@ -195,6 +196,14 @@ pub enum HttpEvent {
|
||||||
XForwardedMissing,
|
XForwardedMissing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[event_type]
|
||||||
|
pub enum SecurityEvent {
|
||||||
|
AuthenticationBan,
|
||||||
|
BruteForceBan,
|
||||||
|
LoiterBan,
|
||||||
|
IpBlocked,
|
||||||
|
}
|
||||||
|
|
||||||
#[event_type]
|
#[event_type]
|
||||||
pub enum ClusterEvent {
|
pub enum ClusterEvent {
|
||||||
PeerAlive,
|
PeerAlive,
|
||||||
|
@ -371,6 +380,7 @@ pub enum SmtpEvent {
|
||||||
LhloExpected,
|
LhloExpected,
|
||||||
MailFromUnauthenticated,
|
MailFromUnauthenticated,
|
||||||
MailFromUnauthorized,
|
MailFromUnauthorized,
|
||||||
|
MailFromNotAllowed,
|
||||||
MailFromRewritten,
|
MailFromRewritten,
|
||||||
MailFromMissing,
|
MailFromMissing,
|
||||||
MailFrom,
|
MailFrom,
|
||||||
|
@ -639,7 +649,6 @@ pub enum NetworkEvent {
|
||||||
Closed,
|
Closed,
|
||||||
ProxyError,
|
ProxyError,
|
||||||
SetOptError,
|
SetOptError,
|
||||||
DropBlocked,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[event_type]
|
#[event_type]
|
||||||
|
@ -915,7 +924,6 @@ pub enum AuthEvent {
|
||||||
Failed,
|
Failed,
|
||||||
MissingTotp,
|
MissingTotp,
|
||||||
TooManyAttempts,
|
TooManyAttempts,
|
||||||
Banned,
|
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -339,7 +339,7 @@ impl EventType {
|
||||||
EventType::Arc(ArcEvent::InvalidCv) => 30,
|
EventType::Arc(ArcEvent::InvalidCv) => 30,
|
||||||
EventType::Arc(ArcEvent::InvalidInstance) => 31,
|
EventType::Arc(ArcEvent::InvalidInstance) => 31,
|
||||||
EventType::Arc(ArcEvent::SealerNotFound) => 32,
|
EventType::Arc(ArcEvent::SealerNotFound) => 32,
|
||||||
EventType::Auth(AuthEvent::Banned) => 33,
|
EventType::Security(SecurityEvent::AuthenticationBan) => 33,
|
||||||
EventType::Auth(AuthEvent::Error) => 34,
|
EventType::Auth(AuthEvent::Error) => 34,
|
||||||
EventType::Auth(AuthEvent::Failed) => 35,
|
EventType::Auth(AuthEvent::Failed) => 35,
|
||||||
EventType::Auth(AuthEvent::MissingTotp) => 36,
|
EventType::Auth(AuthEvent::MissingTotp) => 36,
|
||||||
|
@ -624,7 +624,7 @@ impl EventType {
|
||||||
EventType::Network(NetworkEvent::AcceptError) => 315,
|
EventType::Network(NetworkEvent::AcceptError) => 315,
|
||||||
EventType::Network(NetworkEvent::BindError) => 316,
|
EventType::Network(NetworkEvent::BindError) => 316,
|
||||||
EventType::Network(NetworkEvent::Closed) => 317,
|
EventType::Network(NetworkEvent::Closed) => 317,
|
||||||
EventType::Network(NetworkEvent::DropBlocked) => 318,
|
EventType::Security(SecurityEvent::IpBlocked) => 318,
|
||||||
EventType::Network(NetworkEvent::FlushError) => 319,
|
EventType::Network(NetworkEvent::FlushError) => 319,
|
||||||
EventType::Network(NetworkEvent::ListenError) => 320,
|
EventType::Network(NetworkEvent::ListenError) => 320,
|
||||||
EventType::Network(NetworkEvent::ListenStart) => 321,
|
EventType::Network(NetworkEvent::ListenStart) => 321,
|
||||||
|
@ -855,6 +855,9 @@ impl EventType {
|
||||||
EventType::Tls(TlsEvent::NoCertificatesAvailable) => 546,
|
EventType::Tls(TlsEvent::NoCertificatesAvailable) => 546,
|
||||||
EventType::Tls(TlsEvent::NotConfigured) => 547,
|
EventType::Tls(TlsEvent::NotConfigured) => 547,
|
||||||
EventType::Telemetry(TelemetryEvent::Alert) => 548,
|
EventType::Telemetry(TelemetryEvent::Alert) => 548,
|
||||||
|
EventType::Security(SecurityEvent::BruteForceBan) => 549,
|
||||||
|
EventType::Security(SecurityEvent::LoiterBan) => 550,
|
||||||
|
EventType::Smtp(SmtpEvent::MailFromNotAllowed) => 551,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -893,7 +896,7 @@ impl EventType {
|
||||||
30 => Some(EventType::Arc(ArcEvent::InvalidCv)),
|
30 => Some(EventType::Arc(ArcEvent::InvalidCv)),
|
||||||
31 => Some(EventType::Arc(ArcEvent::InvalidInstance)),
|
31 => Some(EventType::Arc(ArcEvent::InvalidInstance)),
|
||||||
32 => Some(EventType::Arc(ArcEvent::SealerNotFound)),
|
32 => Some(EventType::Arc(ArcEvent::SealerNotFound)),
|
||||||
33 => Some(EventType::Auth(AuthEvent::Banned)),
|
33 => Some(EventType::Security(SecurityEvent::AuthenticationBan)),
|
||||||
34 => Some(EventType::Auth(AuthEvent::Error)),
|
34 => Some(EventType::Auth(AuthEvent::Error)),
|
||||||
35 => Some(EventType::Auth(AuthEvent::Failed)),
|
35 => Some(EventType::Auth(AuthEvent::Failed)),
|
||||||
36 => Some(EventType::Auth(AuthEvent::MissingTotp)),
|
36 => Some(EventType::Auth(AuthEvent::MissingTotp)),
|
||||||
|
@ -1196,7 +1199,7 @@ impl EventType {
|
||||||
315 => Some(EventType::Network(NetworkEvent::AcceptError)),
|
315 => Some(EventType::Network(NetworkEvent::AcceptError)),
|
||||||
316 => Some(EventType::Network(NetworkEvent::BindError)),
|
316 => Some(EventType::Network(NetworkEvent::BindError)),
|
||||||
317 => Some(EventType::Network(NetworkEvent::Closed)),
|
317 => Some(EventType::Network(NetworkEvent::Closed)),
|
||||||
318 => Some(EventType::Network(NetworkEvent::DropBlocked)),
|
318 => Some(EventType::Security(SecurityEvent::IpBlocked)),
|
||||||
319 => Some(EventType::Network(NetworkEvent::FlushError)),
|
319 => Some(EventType::Network(NetworkEvent::FlushError)),
|
||||||
320 => Some(EventType::Network(NetworkEvent::ListenError)),
|
320 => Some(EventType::Network(NetworkEvent::ListenError)),
|
||||||
321 => Some(EventType::Network(NetworkEvent::ListenStart)),
|
321 => Some(EventType::Network(NetworkEvent::ListenStart)),
|
||||||
|
@ -1449,6 +1452,9 @@ impl EventType {
|
||||||
546 => Some(EventType::Tls(TlsEvent::NoCertificatesAvailable)),
|
546 => Some(EventType::Tls(TlsEvent::NoCertificatesAvailable)),
|
||||||
547 => Some(EventType::Tls(TlsEvent::NotConfigured)),
|
547 => Some(EventType::Tls(TlsEvent::NotConfigured)),
|
||||||
548 => Some(EventType::Telemetry(TelemetryEvent::Alert)),
|
548 => Some(EventType::Telemetry(TelemetryEvent::Alert)),
|
||||||
|
549 => Some(EventType::Security(SecurityEvent::BruteForceBan)),
|
||||||
|
550 => Some(EventType::Security(SecurityEvent::LoiterBan)),
|
||||||
|
551 => Some(EventType::Smtp(SmtpEvent::MailFromNotAllowed)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "utils"
|
name = "utils"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
@ -406,9 +406,11 @@ pub async fn insert_test_metrics(core: Arc<Core>) {
|
||||||
EventType::Queue(QueueEvent::QueueReport),
|
EventType::Queue(QueueEvent::QueueReport),
|
||||||
EventType::MessageIngest(MessageIngestEvent::Ham),
|
EventType::MessageIngest(MessageIngestEvent::Ham),
|
||||||
EventType::MessageIngest(MessageIngestEvent::Spam),
|
EventType::MessageIngest(MessageIngestEvent::Spam),
|
||||||
EventType::Auth(AuthEvent::Banned),
|
|
||||||
EventType::Auth(AuthEvent::Failed),
|
EventType::Auth(AuthEvent::Failed),
|
||||||
EventType::Network(NetworkEvent::DropBlocked),
|
EventType::Security(SecurityEvent::AuthenticationBan),
|
||||||
|
EventType::Security(SecurityEvent::BruteForceBan),
|
||||||
|
EventType::Security(SecurityEvent::LoiterBan),
|
||||||
|
EventType::Security(SecurityEvent::IpBlocked),
|
||||||
EventType::IncomingReport(IncomingReportEvent::DmarcReport),
|
EventType::IncomingReport(IncomingReportEvent::DmarcReport),
|
||||||
EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings),
|
EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings),
|
||||||
EventType::IncomingReport(IncomingReportEvent::TlsReport),
|
EventType::IncomingReport(IncomingReportEvent::TlsReport),
|
||||||
|
|
|
@ -100,8 +100,10 @@ enable = true
|
||||||
implicit = false
|
implicit = false
|
||||||
certificate = "default"
|
certificate = "default"
|
||||||
|
|
||||||
|
[server.fail2ban]
|
||||||
|
authentication = "101/5s"
|
||||||
|
|
||||||
[authentication]
|
[authentication]
|
||||||
fail2ban = "101/5s"
|
|
||||||
rate-limit = "100/2s"
|
rate-limit = "100/2s"
|
||||||
|
|
||||||
[session.ehlo]
|
[session.ehlo]
|
||||||
|
|
|
@ -56,6 +56,9 @@ requiretls = [{if = "remote_ip = '10.0.0.2'", then = true},
|
||||||
mt-priority = [{if = "remote_ip = '10.0.0.2'", then = 'nsep'},
|
mt-priority = [{if = "remote_ip = '10.0.0.2'", then = 'nsep'},
|
||||||
{else = false}]
|
{else = false}]
|
||||||
|
|
||||||
|
[session.mail]
|
||||||
|
is-allowed = "sender_domain != 'blocked.com'"
|
||||||
|
|
||||||
[session.data.limits]
|
[session.data.limits]
|
||||||
size = [{if = "remote_ip = '10.0.0.2'", then = 2048},
|
size = [{if = "remote_ip = '10.0.0.2'", then = 2048},
|
||||||
{else = 1024}]
|
{else = 1024}]
|
||||||
|
@ -70,8 +73,8 @@ enable = true
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn mail() {
|
async fn mail() {
|
||||||
// Enable logging
|
// Enable logging
|
||||||
crate::enable_logging();
|
crate::enable_logging();
|
||||||
|
|
||||||
let tmp_dir = TempDir::new("smtp_mail_test", true);
|
let tmp_dir = TempDir::new("smtp_mail_test", true);
|
||||||
let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap();
|
let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap();
|
||||||
|
@ -115,9 +118,16 @@ async fn mail() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
session.response().assert_code("503 5.5.1");
|
session.response().assert_code("503 5.5.1");
|
||||||
|
|
||||||
// Both IPREV and SPF should pass
|
// Test sender not allowed
|
||||||
session.ingest(b"EHLO mx1.foobar.org\r\n").await.unwrap();
|
session.ingest(b"EHLO mx1.foobar.org\r\n").await.unwrap();
|
||||||
session.response().assert_code("250");
|
session.response().assert_code("250");
|
||||||
|
session
|
||||||
|
.ingest(b"MAIL FROM:<bill@blocked.com>\r\n")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
session.response().assert_code("550 5.7.1");
|
||||||
|
|
||||||
|
// Both IPREV and SPF should pass
|
||||||
session
|
session
|
||||||
.ingest(b"MAIL FROM:<bill@foobar.org>\r\n")
|
.ingest(b"MAIL FROM:<bill@foobar.org>\r\n")
|
||||||
.await
|
.await
|
||||||
|
|
Loading…
Reference in a new issue