MTA-STS Policy management
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use mail_auth::{
|
||||
@@ -13,7 +16,12 @@ use mail_auth::{
|
||||
Resolver,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use utils::{config::Config, suffixlist::PublicSuffix};
|
||||
use utils::{
|
||||
config::{utils::ParseValue, Config},
|
||||
suffixlist::PublicSuffix,
|
||||
};
|
||||
|
||||
use crate::Core;
|
||||
|
||||
pub struct Resolvers {
|
||||
pub dns: Resolver,
|
||||
@@ -47,20 +55,21 @@ pub struct Tlsa {
|
||||
pub has_intermediates: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Default, Clone, Copy)]
|
||||
pub enum Mode {
|
||||
Enforce,
|
||||
Testing,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Clone)]
|
||||
pub enum MxPattern {
|
||||
Equals(String),
|
||||
StartsWith(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub struct Policy {
|
||||
pub id: String,
|
||||
pub mode: Mode,
|
||||
@@ -227,6 +236,97 @@ impl Resolvers {
|
||||
}
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn try_parse(config: &mut Config) -> Option<Self> {
|
||||
let mode = config
|
||||
.property_or_default::<Option<Mode>>("session.mta-sts.mode", "testing")
|
||||
.unwrap_or_default()?;
|
||||
let max_age = config
|
||||
.property_or_default::<Duration>("session.mta-sts.max-age", "7d")
|
||||
.unwrap_or_else(|| Duration::from_secs(604800))
|
||||
.as_secs();
|
||||
let mut mx = Vec::new();
|
||||
|
||||
for (_, item) in config.values("session.mta-sts.mx") {
|
||||
if let Some(item) = item.strip_prefix("*.") {
|
||||
mx.push(MxPattern::StartsWith(item.to_string()));
|
||||
} else {
|
||||
mx.push(MxPattern::Equals(item.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut policy = Self {
|
||||
id: Default::default(),
|
||||
mode,
|
||||
mx,
|
||||
max_age,
|
||||
};
|
||||
|
||||
if !policy.mx.is_empty() {
|
||||
policy.mx.sort_unstable();
|
||||
policy.id = policy.hash().to_string();
|
||||
}
|
||||
|
||||
policy.into()
|
||||
}
|
||||
|
||||
pub fn try_build<I, T>(mut self, names: I) -> Option<Self>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
if self.mx.is_empty() {
|
||||
for name in names {
|
||||
let name = name.as_ref();
|
||||
if let Some(domain) = name.strip_prefix('.') {
|
||||
self.mx.push(MxPattern::StartsWith(domain.to_string()));
|
||||
} else if name != "*" && !name.is_empty() {
|
||||
self.mx.push(MxPattern::Equals(name.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
if !self.mx.is_empty() {
|
||||
self.mx.sort_unstable();
|
||||
self.id = self.hash().to_string();
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
self.mode.hash(&mut s);
|
||||
self.max_age.hash(&mut s);
|
||||
self.mx.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Core {
|
||||
pub fn build_mta_sts_policy(&self) -> Option<Policy> {
|
||||
self.smtp
|
||||
.session
|
||||
.mta_sts_policy
|
||||
.clone()
|
||||
.and_then(|policy| policy.try_build(self.tls.certificates.load().keys()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseValue for Mode {
|
||||
fn parse_value(value: &str) -> utils::config::Result<Self> {
|
||||
match value {
|
||||
"enforce" => Ok(Self::Enforce),
|
||||
"testing" | "test" => Ok(Self::Testing),
|
||||
"none" => Ok(Self::None),
|
||||
_ => Err(format!("Invalid mode value {value:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Resolvers {
|
||||
fn default() -> Self {
|
||||
let (config, opts) = match read_system_conf() {
|
||||
@@ -253,6 +353,36 @@ impl Default for Resolvers {
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Policy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("version: STSv1\r\n")?;
|
||||
f.write_str("mode: ")?;
|
||||
match self.mode {
|
||||
Mode::Enforce => f.write_str("enforce")?,
|
||||
Mode::Testing => f.write_str("testing")?,
|
||||
Mode::None => unreachable!(),
|
||||
}
|
||||
f.write_str("\r\nmax_age: ")?;
|
||||
self.max_age.fmt(f)?;
|
||||
f.write_str("\r\n")?;
|
||||
|
||||
for mx in &self.mx {
|
||||
f.write_str("mx: ")?;
|
||||
let mx = match mx {
|
||||
MxPattern::StartsWith(mx) => {
|
||||
f.write_str("*.")?;
|
||||
mx
|
||||
}
|
||||
MxPattern::Equals(mx) => mx,
|
||||
};
|
||||
f.write_str(mx)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Resolvers {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
expr::{if_block::IfBlock, tokenizer::TokenMap, *},
|
||||
};
|
||||
|
||||
use self::throttle::parse_throttle;
|
||||
use self::{resolver::Policy, throttle::parse_throttle};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct SessionConfig {
|
||||
pub rcpt: Rcpt,
|
||||
pub data: Data,
|
||||
pub extensions: Extensions,
|
||||
pub mta_sts_policy: Option<Policy>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -188,6 +189,7 @@ impl SessionConfig {
|
||||
.filter_map(|id| parse_pipe(config, &id, &has_rcpt_vars))
|
||||
.collect();
|
||||
session.throttle = SessionThrottle::parse(config);
|
||||
session.mta_sts_policy = Policy::try_parse(config);
|
||||
|
||||
for (value, key, token_map) in [
|
||||
(&mut session.duration, "session.duration", &has_conn_vars),
|
||||
@@ -713,6 +715,7 @@ impl Default for SessionConfig {
|
||||
"false",
|
||||
),
|
||||
},
|
||||
mta_sts_policy: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use store::{
|
||||
write::{BatchBuilder, ValueClass},
|
||||
@@ -124,6 +125,32 @@ impl ConfigManager {
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn group(
|
||||
&self,
|
||||
prefix: &str,
|
||||
suffix: &str,
|
||||
) -> store::Result<AHashMap<String, AHashMap<String, String>>> {
|
||||
let mut grouped = AHashMap::new();
|
||||
|
||||
let mut list = self.list(prefix, true).await?;
|
||||
for (key, _) in &list {
|
||||
if let Some(key) = key.strip_suffix(suffix) {
|
||||
grouped.insert(key.to_string(), AHashMap::new());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, entries) in &mut grouped {
|
||||
let prefix = format!("{name}.");
|
||||
for (key, value) in &mut list {
|
||||
if let Some(key) = key.strip_prefix(&prefix) {
|
||||
entries.insert(key.to_string(), std::mem::take(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(grouped)
|
||||
}
|
||||
|
||||
async fn db_list(
|
||||
&self,
|
||||
prefix: &str,
|
||||
@@ -385,6 +412,46 @@ impl ConfigManager {
|
||||
Err("External configuration file does not contain a version key".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_services(&self) -> store::Result<Vec<(String, u16, bool)>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for listener in self
|
||||
.group("server.listener.", ".protocol")
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_values()
|
||||
{
|
||||
let is_tls = listener
|
||||
.get("tls.implicit")
|
||||
.map_or(false, |tls| tls == "true");
|
||||
let protocol = listener
|
||||
.get("protocol")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default();
|
||||
let port = listener
|
||||
.get("bind")
|
||||
.or_else(|| {
|
||||
listener.iter().find_map(|(key, value)| {
|
||||
if key.starts_with("bind.") {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.and_then(|s| s.rsplit_once(':').and_then(|(_, p)| p.parse::<u16>().ok()))
|
||||
.unwrap_or_default();
|
||||
|
||||
if port > 0 {
|
||||
result.push((protocol.to_string(), port, is_tls));
|
||||
}
|
||||
}
|
||||
|
||||
result.sort_unstable();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Patterns {
|
||||
|
||||
Reference in New Issue
Block a user