diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9babb9..61c6c49d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.16.12] - 2026-06-XX + +If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. + +## Added + +## Changed + +## Fixed +- DANE: Treat DNSSEC `bogus` as a temporary failures to prevent downgrade attacks. + ## [0.16.11] - 2026-06-25 If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions. diff --git a/Cargo.lock b/Cargo.lock index 436d6481..75955e32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,7 +1259,7 @@ dependencies = [ [[package]] name = "common" -version = "0.16.11" +version = "0.16.12" dependencies = [ "aes-gcm-siv", "ahash", @@ -1455,7 +1455,7 @@ checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "coordinator" -version = "0.16.11" +version = "0.16.12" dependencies = [ "async-nats", "futures", @@ -1808,7 +1808,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "dav" -version = "0.16.11" +version = "0.16.12" dependencies = [ "calcard", "chrono", @@ -1831,7 +1831,7 @@ dependencies = [ [[package]] name = "dav-proto" -version = "0.16.11" +version = "0.16.12" dependencies = [ "calcard", "chrono", @@ -2037,7 +2037,7 @@ dependencies = [ [[package]] name = "directory" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "argon2", @@ -2288,7 +2288,7 @@ dependencies = [ [[package]] name = "email" -version = "0.16.11" +version = "0.16.12" dependencies = [ "aes 0.9.1", "aes-gcm", @@ -2396,7 +2396,7 @@ dependencies = [ [[package]] name = "event_macro" -version = "0.16.11" +version = "0.16.12" dependencies = [ "proc-macro2", "quote", @@ -2912,7 +2912,7 @@ dependencies = [ [[package]] name = "groupware" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "calcard", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "http" -version = "0.16.11" +version = "0.16.12" dependencies = [ "async-stream", "base64 0.22.1", @@ -3295,7 +3295,7 @@ dependencies = [ [[package]] name = "http_proto" -version = "0.16.11" +version = "0.16.12" dependencies = [ "common", "compact_str", @@ -3592,7 +3592,7 @@ checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "imap" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "common", @@ -3619,7 +3619,7 @@ dependencies = [ [[package]] name = "imap_proto" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "chrono", @@ -3832,7 +3832,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.16.11" +version = "0.16.12" dependencies = [ "aes-gcm", "aes-gcm-siv", @@ -3920,7 +3920,7 @@ dependencies = [ [[package]] name = "jmap_proto" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "calcard", @@ -4379,9 +4379,9 @@ dependencies = [ [[package]] name = "mail-auth" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3c5a279d798bf41bfcf94ee593a34b545fb2010ebf5cfce3416d6b4019f697" +checksum = "9e533fed1dd496906478a2ebf4700525da7579ef116f3e90812ca5832aedd158" dependencies = [ "aws-lc-rs", "flate2", @@ -4422,7 +4422,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "common", @@ -4564,7 +4564,7 @@ checksum = "c797b9d6bb23aab2fc369c65f871be49214f5c759af65bde26ffaaa2b646b492" [[package]] name = "migration" -version = "0.16.11" +version = "0.16.12" dependencies = [ "base64 0.22.1", "common", @@ -4781,7 +4781,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "bincode 1.3.3", @@ -5585,7 +5585,7 @@ dependencies = [ [[package]] name = "pop3" -version = "0.16.11" +version = "0.16.12" dependencies = [ "common", "directory", @@ -5768,7 +5768,7 @@ dependencies = [ [[package]] name = "proc_macros" -version = "0.16.11" +version = "0.16.12" dependencies = [ "proc-macro2", "quote", @@ -6391,7 +6391,7 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "registry" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "hashify", @@ -7276,7 +7276,7 @@ dependencies = [ [[package]] name = "services" -version = "0.16.11" +version = "0.16.12" dependencies = [ "aes-gcm", "aes-gcm-siv", @@ -7534,7 +7534,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smtp" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "base64 0.22.1", @@ -7636,7 +7636,7 @@ dependencies = [ [[package]] name = "spam-filter" -version = "0.16.11" +version = "0.16.12" dependencies = [ "common", "compact_str", @@ -7745,7 +7745,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stalwart" -version = "0.16.11" +version = "0.16.12" dependencies = [ "common", "coordinator", @@ -7753,7 +7753,7 @@ dependencies = [ "directory", "email", "groupware", - "http 0.16.11", + "http 0.16.12", "http_proto", "imap", "jemallocator", @@ -7782,7 +7782,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "arc-swap", @@ -8022,7 +8022,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "async-trait", @@ -8046,7 +8046,7 @@ dependencies = [ "form_urlencoded", "futures", "groupware", - "http 0.16.11", + "http 0.16.12", "http-body-util", "http_proto", "hyper", @@ -8589,7 +8589,7 @@ dependencies = [ [[package]] name = "trc" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "base64 0.22.1", @@ -8682,7 +8682,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "types" -version = "0.16.11" +version = "0.16.12" dependencies = [ "blake3", "compact_str", @@ -8880,7 +8880,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utils" -version = "0.16.11" +version = "0.16.12" dependencies = [ "ahash", "arcstr", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 8e115eeb..66f8c99d 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "0.16.11" +version = "0.16.12" edition = "2024" build = "build.rs" @@ -18,7 +18,7 @@ imap_proto = { path = "../imap-proto" } sieve-rs = { version = "0.7", features = ["rkyv", "serde"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { version = "0.9", features = ["generate"] } +mail-auth = { version = "0.10", features = ["generate"] } smtp-proto = { version = "0.2", features = ["rkyv"] } dns-update = { version = "0.5" } calcard = { version = "0.3", features = ["rkyv"] } diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index cb2e7b20..86f71391 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -22,7 +22,7 @@ use crate::{ }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; -use mail_auth::{MX, Parameters, Txt}; +use mail_auth::{MX, Parameters, RecordSet, Txt}; use parking_lot::RwLock; use registry::schema::{prelude::ObjectType, structs}; use std::{ @@ -178,10 +178,6 @@ impl Caches { ((std::mem::size_of::() + 255) * 2) as u64, ), dns_tlsa: CacheWithTtl::new(cache.dns_tlsa, (std::mem::size_of::() + 255) as u64), - dns_dnssec: CacheWithTtl::new( - cache.dns_tlsa, - (std::mem::size_of::() + 255) as u64, - ), dns_mta_sts: CacheWithTtl::new( cache.dns_mta_sts, (std::mem::size_of::() + 255) as u64, @@ -203,10 +199,10 @@ impl Caches { '_, T, CacheWithTtl, Txt>, - CacheWithTtl, Arc<[MX]>>, - CacheWithTtl, Arc<[Ipv4Addr]>>, - CacheWithTtl, Arc<[Ipv6Addr]>>, - CacheWithTtl]>>, + CacheWithTtl, RecordSet>, + CacheWithTtl, RecordSet>, + CacheWithTtl, RecordSet>, + CacheWithTtl>>, > { Parameters { params, diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index aead9848..b66b99e0 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -218,6 +218,7 @@ impl Server { .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result + .rrset .iter() .flat_map(|mx| { mx.exchanges.iter().map(|host| { @@ -256,6 +257,7 @@ impl Server { .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result + .rrset .iter() .map(|host| Variable::from(host.to_compact_string())) .collect::>() @@ -271,6 +273,7 @@ impl Server { .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result + .rrset .iter() .map(|ip| Variable::from(ip.to_compact_string())) .collect::>() @@ -286,6 +289,7 @@ impl Server { .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result + .rrset .iter() .map(|ip| Variable::from(ip.to_compact_string())) .collect::>() diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a4205619..f200e83a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -39,7 +39,7 @@ use config::{ telemetry::Metrics, }; use ipc::{BroadcastEvent, PushEvent, QueueEvent, ReportingEvent}; -use mail_auth::{MX, Txt}; +use mail_auth::{MX, RecordSet, Txt}; use manager::application::Resource; use parking_lot::{Mutex, RwLock}; use rustls::sign::CertifiedKey; @@ -196,12 +196,11 @@ pub struct Caches { pub dkim_signers: Cache>, pub dns_txt: CacheWithTtl, Txt>, - pub dns_mx: CacheWithTtl, Arc<[MX]>>, - pub dns_ptr: CacheWithTtl]>>, - pub dns_ipv4: CacheWithTtl, Arc<[Ipv4Addr]>>, - pub dns_ipv6: CacheWithTtl, Arc<[Ipv6Addr]>>, + pub dns_mx: CacheWithTtl, RecordSet>, + pub dns_ptr: CacheWithTtl>>, + pub dns_ipv4: CacheWithTtl, RecordSet>, + pub dns_ipv6: CacheWithTtl, RecordSet>, pub dns_tlsa: CacheWithTtl, Arc>, - pub dns_dnssec: CacheWithTtl, bool>, pub dns_mta_sts: CacheWithTtl, Arc>, pub dns_rbl: CacheWithTtl, Option>>, diff --git a/crates/common/src/network/dns/resolve.rs b/crates/common/src/network/dns/resolve.rs index 1e81ace0..94c56c28 100644 --- a/crates/common/src/network/dns/resolve.rs +++ b/crates/common/src/network/dns/resolve.rs @@ -18,7 +18,7 @@ impl Server { .mx_lookup(entry, Some(&self.inner.cache.dns_mx)) .await { - Ok(result) => Ok(result.iter().any(|mx| !mx.exchanges.is_empty())), + Ok(result) => Ok(result.rrset.iter().any(|mx| !mx.exchanges.is_empty())), Err(Error::DnsRecordNotFound(_)) => Ok(false), Err(err) => Err(err.into()), } @@ -55,7 +55,7 @@ impl Server { .ptr_lookup(addr, Some(&self.inner.cache.dns_ptr)) .await { - Ok(result) => Ok(!result.is_empty()), + Ok(result) => Ok(!result.rrset.is_empty()), Err(Error::DnsRecordNotFound(_)) => Ok(false), Err(err) => Err(err.into()), } @@ -73,7 +73,7 @@ impl Server { .ipv4_lookup(entry, Some(&self.inner.cache.dns_ipv4)) .await { - Ok(result) => Ok(!result.is_empty()), + Ok(result) => Ok(!result.rrset.is_empty()), Err(Error::DnsRecordNotFound(_)) => Ok(false), Err(err) => Err(err.into()), } @@ -88,7 +88,7 @@ impl Server { .ipv6_lookup(entry, Some(&self.inner.cache.dns_ipv6)) .await { - Ok(result) => Ok(!result.is_empty()), + Ok(result) => Ok(!result.rrset.is_empty()), Err(Error::DnsRecordNotFound(_)) => Ok(false), Err(err) => Err(err.into()), } diff --git a/crates/common/src/scripts/plugins/dns.rs b/crates/common/src/scripts/plugins/dns.rs index 18d5816e..9301b553 100644 --- a/crates/common/src/scripts/plugins/dns.rs +++ b/crates/common/src/scripts/plugins/dns.rs @@ -57,6 +57,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result { .await { Ok(result) => result + .rrset .iter() .flat_map(|mx| { mx.exchanges @@ -99,6 +100,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result { .await { Ok(result) => result + .rrset .iter() .map(|host| Variable::from(host.to_string())) .collect::>() @@ -127,6 +129,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result { .await { Ok(result) => result + .rrset .iter() .map(|ip| Variable::from(ip.to_string())) .collect::>() @@ -144,6 +147,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result { .await { Ok(result) => result + .rrset .iter() .map(|ip| Variable::from(ip.to_string())) .collect::>() diff --git a/crates/coordinator/Cargo.toml b/crates/coordinator/Cargo.toml index e84503ae..bf7b149f 100644 --- a/crates/coordinator/Cargo.toml +++ b/crates/coordinator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coordinator" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 3adddcf1..2fdb038d 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav-proto" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 6ebf6f6f..218a877c 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dav" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 269a2893..693e390e 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "directory" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/email/Cargo.toml b/crates/email/Cargo.toml index 956c3b2d..4f33e6f5 100644 --- a/crates/email/Cargo.toml +++ b/crates/email/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "email" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 92b59c50..6db3480c 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "groupware" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index ba9c02cf..a0bf8a06 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_proto" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index c1294709..b37d9099 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -23,7 +23,7 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { version = "0.9", features = ["generate"] } +mail-auth = { version = "0.10", features = ["generate"] } tokio = { version = "1.47", features = ["rt"] } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1.1", features = ["tokio"] } diff --git a/crates/http/src/api/diagnose.rs b/crates/http/src/api/diagnose.rs index 55443b20..6e64a364 100644 --- a/crates/http/src/api/diagnose.rs +++ b/crates/http/src/api/diagnose.rs @@ -16,7 +16,10 @@ use mail_auth::{IpLookupStrategy, mta_sts::TlsRpt}; use serde::{Deserialize, Serialize}; use smtp::outbound::{ client::{SmtpClient, StartTlsResult}, - dane::{dnssec::TlsaLookup, verify::TlsaVerify}, + dane::{ + dnssec::{TlsaLookup, TlsaResult}, + verify::TlsaVerify, + }, error::ClientError, lookup::{DnsLookup, SourceIp, ToNextHop}, mta_sts::{lookup::MtaStsLookup, verify::VerifyPolicy}, @@ -249,13 +252,14 @@ async fn delivery_diagnose( // Obtain remote host list let mx_config = MxConfig { - max_mx: mxs.len(), + max_mx: mxs.rrset.len(), max_multi_homed: 10, ip_lookup_strategy: IpLookupStrategy::Ipv4thenIpv6, }; let hosts = if let Some(hosts) = mxs.to_remote_hosts(&domain, &mx_config) { tx.send(DeliveryStage::MxLookupSuccess { mxs: mxs + .rrset .iter() .map(|mx| MX { exchanges: mx.exchanges.iter().map(|e| e.to_string()).collect(), @@ -385,7 +389,7 @@ async fn delivery_diagnose( let now = Instant::now(); let dane_policy = match server.tlsa_lookup(format!("_25._tcp.{hostname}.")).await { - Ok(Some(tlsa)) if tlsa.has_end_entities => { + Ok(TlsaResult::Secure(tlsa)) if tlsa.has_end_entities => { tx.send(DeliveryStage::TlsaLookupSuccess { record: tlsa.as_ref().clone(), elapsed: now.elapsed_ms(), @@ -394,7 +398,7 @@ async fn delivery_diagnose( Some(tlsa) } - Ok(Some(_)) => { + Ok(TlsaResult::Secure(_)) => { tx.send(DeliveryStage::TlsaLookupError { elapsed: now.elapsed_ms(), reason: "TLSA record does not have end entities".to_string(), @@ -403,7 +407,16 @@ async fn delivery_diagnose( None } - Ok(None) => { + Ok(TlsaResult::Bogus) => { + tx.send(DeliveryStage::TlsaLookupError { + elapsed: now.elapsed_ms(), + reason: "Bogus TLSA record".to_string(), + }) + .await?; + + None + } + Ok(TlsaResult::Missing) => { tx.send(DeliveryStage::TlsaNotFound { elapsed: now.elapsed_ms(), reason: "No TLSA DNSSEC records found".to_string(), diff --git a/crates/imap-proto/Cargo.toml b/crates/imap-proto/Cargo.toml index b41e6085..93da1681 100644 --- a/crates/imap-proto/Cargo.toml +++ b/crates/imap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap_proto" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 8bae44ec..c21aa064 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index 1ffbf219..e0ea9383 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap_proto" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index e7de8197..e674567b 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -23,7 +23,7 @@ calcard = { version = "0.3" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { version = "0.9", features = ["generate"] } +mail-auth = { version = "0.10", features = ["generate"] } sieve-rs = { version = "0.7", features = ["rkyv"] } jmap-tools = { version = "0.1", features = ["rkyv"] } serde = { version = "1.0", features = ["derive"]} diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 50b5322a..65b4ad89 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.11" +version = "0.16.12" edition = "2024" [[bin]] diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index a427dd0d..43d62b89 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index 9f6bfb6d..cc6f16a8 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "migration" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index f493d89f..c6d96d98 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 73fb931b..e8ac5998 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pop3" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index 6ddfe434..243fdefa 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "registry" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -13,7 +13,7 @@ hashify = "0.2.7" ahash = { version = "0.8" } jmap-tools = { version = "0.1" } xxhash-rust = { version = "0.8.5", features = ["xxh3"] } -mail-auth = { version = "0.9" } +mail-auth = { version = "0.10" } tokio = { version = "1.47", features = ["fs"] } lz4_flex = { version = "0.13", default-features = false } diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 3eb2b63d..93b32749 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "services" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 5366b855..8bd988b5 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -21,7 +21,7 @@ email = { path = "../email" } registry = { path = "../registry" } spam-filter = { path = "../spam-filter" } trc = { path = "../trc" } -mail-auth = { version = "0.9", features = ["rkyv"] } +mail-auth = { version = "0.10", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } smtp-proto = { version = "0.2", features = ["rkyv", "serde"] } diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs index d1ce4e12..db396a0d 100644 --- a/crates/smtp/src/outbound/dane/dnssec.rs +++ b/crates/smtp/src/outbound/dane/dnssec.rs @@ -8,30 +8,63 @@ use common::{ Server, config::smtp::resolver::{Tlsa, TlsaEntry, TlsaMatching}, }; +pub use mail_auth::DnssecStatus; use mail_auth::{ + MX, RecordSet, common::resolver::ToFqdn, - hickory_resolver::proto::rr::{ - Name, RData, - rdata::tlsa::{CertUsage, Matching, Selector}, + hickory_resolver::{ + net::{DnsError, NetError}, + proto::{ + dnssec::Proof, + op::ResponseCode, + rr::{ + Name, RData, Record, RecordType, + rdata::tlsa::{CertUsage, Matching, Selector}, + }, + }, }, }; -use std::{future::Future, sync::Arc}; +use std::{ + future::Future, + sync::Arc, + time::{Duration, Instant}, +}; pub trait TlsaLookup: Sync + Send { + fn mx_lookup( + &self, + key: impl ToFqdn + Sync + Send, + ) -> impl Future>> + Send; + fn tlsa_lookup( &self, key: impl ToFqdn + Sync + Send, - ) -> impl Future>>> + Send; + ) -> impl Future> + Send; +} + +pub enum TlsaResult { + Secure(Arc), + Bogus, + Missing, } impl TlsaLookup for Server { - async fn tlsa_lookup( - &self, - key: impl ToFqdn + Sync + Send, - ) -> mail_auth::Result>> { + async fn mx_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result> { + if !self.core.smtp.resolvers.dnssec_available { + return self + .core + .smtp + .resolvers + .dns + .mx_lookup(key, Some(&self.inner.cache.dns_mx)) + .await; + } + let key = key.to_fqdn(); - if let Some(value) = self.inner.cache.dns_tlsa.get(key.as_ref()) { - return Ok(Some(value)); + if let Some(value) = self.inner.cache.dns_mx.get::(key.as_ref()) + && value.dnssec_status != DnssecStatus::Indeterminate + { + return Ok(value); } #[cfg(any(test, feature = "test_mode"))] @@ -39,78 +72,245 @@ impl TlsaLookup for Server { return mail_auth::common::resolver::mock_resolve(key.as_ref()); } - let mut entries = Vec::new(); - let tlsa_lookup = self + let mx_lookup = match self + .core + .smtp + .resolvers + .dnssec + .resolver + .mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?) + .await + { + Ok(mx_lookup) => mx_lookup, + Err(err) => { + if let Some(denial) = NegativeAnswer::from_error(&err) + && denial.response_code == ResponseCode::NoError + { + let records = RecordSet { + rrset: Arc::new([]), + dnssec_status: denial.dnssec_status, + }; + if let Some(valid_until) = denial.valid_until { + self.inner.cache.dns_mx.insert_with_expiry( + key, + records.clone(), + valid_until, + ); + } + return Ok(records); + } + return Err(err.into()); + } + }; + let mx_records = mx_lookup.answers(); + let mut dnssec_status: Option = None; + let mut records: Vec<(u16, Vec>)> = Vec::with_capacity(mx_records.len()); + for mx_record in mx_records { + if let RData::MX(mx) = &mx_record.data { + dnssec_status = Some(match dnssec_status { + Some(status) => least_secure(status, proof_to_dnssec_status(mx_record.proof)), + None => proof_to_dnssec_status(mx_record.proof), + }); + + let preference = mx.preference; + let exchange = mx.exchange.to_lowercase().to_string().into_boxed_str(); + + if let Some(record) = records.iter_mut().find(|r| r.0 == preference) { + record.1.push(exchange); + } else { + records.push((preference, vec![exchange])); + } + } + } + + records.sort_unstable_by_key(|a| a.0); + let rrset: Arc<[MX]> = records + .into_iter() + .map(|(preference, exchanges)| MX { + preference, + exchanges: exchanges.into_boxed_slice(), + }) + .collect::>(); + let records = RecordSet { + rrset, + dnssec_status: dnssec_status.unwrap_or(DnssecStatus::Indeterminate), + }; + + self.inner + .cache + .dns_mx + .insert_with_expiry(key, records.clone(), mx_lookup.valid_until()); + + Ok(records) + } + + async fn tlsa_lookup(&self, key: impl ToFqdn + Sync + Send) -> mail_auth::Result { + let key = key.to_fqdn(); + if let Some(value) = self.inner.cache.dns_tlsa.get(key.as_ref()) { + return Ok(TlsaResult::Secure(value)); + } + + #[cfg(any(test, feature = "test_mode"))] + if true { + if key.as_ref().contains("_dnssec_bogus.") { + return Ok(TlsaResult::Bogus); + } + return mail_auth::common::resolver::mock_resolve(key.as_ref()); + } + + let tlsa_lookup = match self .core .smtp .resolvers .dnssec .resolver .tlsa_lookup(Name::from_str_relaxed(key.as_ref())?) - .await?; + .await + { + Ok(tlsa_lookup) => tlsa_lookup, + Err(err) => { + if let Some(denial) = NegativeAnswer::from_error(&err) { + return Ok(if denial.dnssec_status == DnssecStatus::Bogus { + TlsaResult::Bogus + } else { + TlsaResult::Missing + }); + } + return Err(err.into()); + } + }; + let mut entries = Vec::new(); let mut has_end_entities = false; let mut has_intermediates = false; - let mut found_insecure = false; + let mut dnssec_status: Option = None; for record in tlsa_lookup.answers() { if let RData::TLSA(tlsa) = &record.data { - if record.proof.is_secure() { - let is_end_entity = match tlsa.cert_usage { - CertUsage::DaneEe => true, - CertUsage::DaneTa => false, - _ => continue, - }; - let matching = match tlsa.matching { - Matching::Raw => TlsaMatching::Full, - Matching::Sha256 => TlsaMatching::Sha256, - Matching::Sha512 => TlsaMatching::Sha512, - _ => continue, - }; - let is_spki = match tlsa.selector { - Selector::Spki => true, - Selector::Full => false, - _ => continue, - }; - if is_end_entity { - has_end_entities = true; - } else { - has_intermediates = true; - } - entries.push(TlsaEntry { - is_end_entity, - is_spki, - matching, - data: tlsa.cert_data.clone(), - }); - } else { - found_insecure = true; + dnssec_status = Some(match dnssec_status { + Some(status) => least_secure(status, proof_to_dnssec_status(record.proof)), + None => proof_to_dnssec_status(record.proof), + }); + + if !record.proof.is_secure() { + continue; } + + let is_end_entity = match tlsa.cert_usage { + CertUsage::DaneEe => true, + CertUsage::DaneTa => false, + _ => continue, + }; + let matching = match tlsa.matching { + Matching::Raw => TlsaMatching::Full, + Matching::Sha256 => TlsaMatching::Sha256, + Matching::Sha512 => TlsaMatching::Sha512, + _ => continue, + }; + let is_spki = match tlsa.selector { + Selector::Spki => true, + Selector::Full => false, + _ => continue, + }; + if is_end_entity { + has_end_entities = true; + } else { + has_intermediates = true; + } + entries.push(TlsaEntry { + is_end_entity, + is_spki, + matching, + data: tlsa.cert_data.clone(), + }); } } - if !entries.is_empty() || !found_insecure { - let tlsa = Arc::new(Tlsa { - entries, - has_end_entities, - has_intermediates, - }); + match dnssec_status { + Some(DnssecStatus::Bogus) => Ok(TlsaResult::Bogus), + Some(DnssecStatus::Secure) => { + let tlsa = Arc::new(Tlsa { + entries, + has_end_entities, + has_intermediates, + }); - self.inner.cache.dns_tlsa.insert_with_expiry( - key, - tlsa.clone(), - tlsa_lookup.valid_until(), - ); + self.inner.cache.dns_tlsa.insert_with_expiry( + key, + tlsa.clone(), + tlsa_lookup.valid_until(), + ); - Ok(Some(tlsa)) - } else { - Ok(None) + Ok(TlsaResult::Secure(tlsa)) + } + _ => Ok(TlsaResult::Missing), } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DnssecStatus { - Secure, - Insecure, +struct NegativeAnswer { + response_code: ResponseCode, + dnssec_status: DnssecStatus, + valid_until: Option, +} + +impl NegativeAnswer { + fn from_error(err: &NetError) -> Option { + let NetError::Dns(dns_error) = err else { + return None; + }; + + match dns_error { + DnsError::NoRecordsFound(no_records) => Some(NegativeAnswer { + response_code: no_records.response_code, + dnssec_status: no_records + .authorities + .as_deref() + .map(denial_dnssec_status) + .unwrap_or(DnssecStatus::Indeterminate), + valid_until: no_records + .negative_ttl + .map(|ttl| Instant::now() + Duration::from_secs(ttl as u64)), + }), + DnsError::Nsec { + response, proof, .. + } => Some(NegativeAnswer { + response_code: response.response_code, + dnssec_status: proof_to_dnssec_status(*proof), + valid_until: None, + }), + _ => None, + } + } +} + +fn denial_dnssec_status(authorities: &[Record]) -> DnssecStatus { + authorities + .iter() + .filter(|record| matches!(record.record_type(), RecordType::NSEC | RecordType::NSEC3)) + .map(|record| proof_to_dnssec_status(record.proof)) + .reduce(least_secure) + .unwrap_or(DnssecStatus::Indeterminate) +} + +fn proof_to_dnssec_status(proof: Proof) -> DnssecStatus { + match proof { + Proof::Secure => DnssecStatus::Secure, + Proof::Insecure => DnssecStatus::Insecure, + Proof::Bogus => DnssecStatus::Bogus, + Proof::Indeterminate => DnssecStatus::Indeterminate, + } +} + +fn least_secure(a: DnssecStatus, b: DnssecStatus) -> DnssecStatus { + fn rank(status: DnssecStatus) -> u8 { + match status { + DnssecStatus::Bogus => 0, + DnssecStatus::Indeterminate => 1, + DnssecStatus::Insecure => 2, + DnssecStatus::Secure => 3, + } + } + + if rank(a) <= rank(b) { a } else { b } } diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index ffbb4628..8e0ebfb5 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -9,7 +9,7 @@ use crate::outbound::DeliveryResult; use crate::outbound::client::{ SmtpClient, from_error_details, from_error_status, from_mail_send_error, }; -use crate::outbound::dane::dnssec::{DnssecStatus, TlsaLookup}; +use crate::outbound::dane::dnssec::{DnssecStatus, TlsaLookup, TlsaResult}; use crate::outbound::error::ClientError; use crate::outbound::lookup::{DnsLookup, SourceIp}; use crate::outbound::mta_sts::lookup::MtaStsLookup; @@ -29,6 +29,7 @@ use common::config::smtp::queue::RoutingStrategy; use common::config::{server::ServerProtocol, smtp::report::AggregateFrequency}; use common::ipc::{PolicyType, QueueEvent, QueueEventStatus, TlsEvent}; use compact_str::ToCompactString; +use mail_auth::RecordSet; use mail_auth::{ mta_sts::TlsRpt, report::tlsrpt::{FailureDetails, ResultType}, @@ -496,14 +497,7 @@ impl QueuedMessage { if let Some(mx_config) = mx_config { // Lookup MX let time = Instant::now(); - mx_list = match server - .core - .smtp - .resolvers - .dns - .mx_lookup(domain, Some(&server.inner.cache.dns_mx)) - .await - { + mx_list = match server.mx_lookup(domain).await { Ok(mx) => mx, Err(mail_auth::Error::DnsRecordNotFound(_)) => { trc::event!( @@ -514,7 +508,10 @@ impl QueuedMessage { Elapsed = time.elapsed(), ); - Arc::new([]) + RecordSet { + rrset: Arc::new([]), + dnssec_status: DnssecStatus::Indeterminate, + } } Err(err) => { trc::event!( @@ -640,22 +637,15 @@ impl QueuedMessage { ); // Obtain source and remote IPs - let dane_enabled = tls_strategy.try_dane() - && is_smtp - && server.core.smtp.resolvers.dnssec_available; let time = Instant::now(); - let resolve_result = match server - .resolve_host(remote_host, &envelope, dane_enabled) - .await - { - Ok(result) => { + let remote_ips = match server.resolve_host(remote_host, &envelope).await { + Ok(remote_ips) => { trc::event!( Delivery(DeliveryEvent::IpLookup), SpanId = message.span_id, Domain = domain.to_string(), Hostname = envelope.mx.to_string(), - Details = result - .remote_ips + Details = remote_ips .iter() .map(|ip| trc::Value::from(*ip)) .collect::>(), @@ -663,7 +653,7 @@ impl QueuedMessage { Elapsed = time.elapsed(), ); - result + remote_ips } Err(status) => { trc::event!( @@ -681,130 +671,96 @@ impl QueuedMessage { }; // Lookup DANE policy - let dane_policy = if dane_enabled { + let dane_policy = if tls_strategy.try_dane() && is_smtp { let time = Instant::now(); let strict = tls_strategy.is_dane_required(); - if resolve_result.dnssec_status == DnssecStatus::Insecure { - trc::event!( - Dane(DaneEvent::TlsaRecordNotDnssecSigned), - SpanId = message.span_id, - Domain = domain.to_string(), - Hostname = envelope.mx.to_string(), - Strict = strict, - Elapsed = time.elapsed(), - ); + match remote_host.dnssec_status() { + DnssecStatus::Secure => { + match server + .tlsa_lookup(format!("_25._tcp.{}.", envelope.mx)) + .await + { + Ok(TlsaResult::Secure(tlsa)) => { + if tlsa.has_end_entities || tlsa.has_intermediates { + trc::event!( + Dane(DaneEvent::TlsaRecordFetch), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Details = format!("{tlsa:?}"), + Strict = strict, + Elapsed = time.elapsed(), + ); - if strict { - // Report DANE required - if let Some(tls_report) = &tls_report { - server - .schedule_report(TlsEvent { - policy: PolicyType::Tlsa(None), - domain: domain.to_string(), - failure: FailureDetails::new(ResultType::DaneRequired) - .with_receiving_mx_hostname(envelope.mx) - .with_failure_reason_code( - "MX host is not in a DNSSEC signed zone.", - ) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - span_id: message.span_id, - }) - .await; - } + tlsa.into() + } else { + trc::event!( + Dane(DaneEvent::TlsaRecordInvalid), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Details = format!("{tlsa:?}"), + Strict = strict, + Elapsed = time.elapsed(), + ); - last_status = Status::PermanentFailure(ErrorDetails { - entity: envelope.mx.into(), - details: Error::DaneError("No TLSA DNSSEC records found".into()), - }); - continue 'next_host; - } - None - } else { - match server - .tlsa_lookup(format!("_25._tcp.{}.", envelope.mx)) - .await - { - Ok(Some(tlsa)) => { - if tlsa.has_end_entities || tlsa.has_intermediates { - trc::event!( - Dane(DaneEvent::TlsaRecordFetch), - SpanId = message.span_id, - Domain = domain.to_string(), - Hostname = envelope.mx.to_string(), - Details = format!("{tlsa:?}"), - Strict = strict, - Elapsed = time.elapsed(), - ); + // Report invalid TLSA record + if let Some(tls_report) = &tls_report { + server + .schedule_report(TlsEvent { + policy: tlsa.into(), + domain: domain.to_string(), + failure: FailureDetails::new( + ResultType::TlsaInvalid, + ) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code( + "Invalid TLSA record.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + span_id: message.span_id, + }) + .await; + } - tlsa.into() - } else { - trc::event!( - Dane(DaneEvent::TlsaRecordInvalid), - SpanId = message.span_id, - Domain = domain.to_string(), - Hostname = envelope.mx.to_string(), - Details = format!("{tlsa:?}"), - Strict = strict, - Elapsed = time.elapsed(), - ); - - // Report invalid TLSA record - if let Some(tls_report) = &tls_report { - server - .schedule_report(TlsEvent { - policy: tlsa.into(), - domain: domain.to_string(), - failure: FailureDetails::new( - ResultType::TlsaInvalid, - ) - .with_receiving_mx_hostname(envelope.mx) - .with_failure_reason_code("Invalid TLSA record.") - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - span_id: message.span_id, - }) - .await; + if strict { + last_status = Status::PermanentFailure(ErrorDetails { + entity: envelope.mx.into(), + details: Error::DaneError( + "No valid TLSA records were found".into(), + ), + }); + continue 'next_host; + } + None } - - if strict { - last_status = Status::PermanentFailure(ErrorDetails { - entity: envelope.mx.into(), - details: Error::DaneError( - "No valid TLSA records were found".into(), - ), - }); - continue 'next_host; - } - None } - } - Ok(None) => { - trc::event!( - Dane(DaneEvent::TlsaRecordNotDnssecSigned), - SpanId = message.span_id, - Domain = domain.to_string(), - Hostname = envelope.mx.to_string(), - Strict = strict, - Elapsed = time.elapsed(), - ); + Ok(TlsaResult::Bogus) => { + trc::event!( + Dane(DaneEvent::BogusDnssecRecord), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Details = "TLSA", + Strict = strict, + Elapsed = time.elapsed(), + ); - if strict { - // Report DANE required + // Report bogus TLSA record if let Some(tls_report) = &tls_report { server .schedule_report(TlsEvent { policy: PolicyType::Tlsa(None), domain: domain.to_string(), failure: FailureDetails::new( - ResultType::DaneRequired, + ResultType::DnssecInvalid, ) .with_receiving_mx_hostname(envelope.mx) .with_failure_reason_code( - "No TLSA DNSSEC records found.", + "Bogus TLSA records were found.", ) .into(), tls_record: tls_report.record.clone(), @@ -814,23 +770,17 @@ impl QueuedMessage { .await; } - last_status = Status::PermanentFailure(ErrorDetails { + last_status = Status::TemporaryFailure(ErrorDetails { entity: envelope.mx.into(), details: Error::DaneError( - "No TLSA DNSSEC records found".into(), + "Bogus TLSA records were found".into(), ), }); continue 'next_host; } - None - } - Err(err) => { - let not_found = - matches!(&err, mail_auth::Error::DnsRecordNotFound(_)); - - if not_found { + Ok(TlsaResult::Missing) => { trc::event!( - Dane(DaneEvent::TlsaRecordNotFound), + Dane(DaneEvent::TlsaRecordNotDnssecSigned), SpanId = message.span_id, Domain = domain.to_string(), Hostname = envelope.mx.to_string(), @@ -850,7 +800,7 @@ impl QueuedMessage { ) .with_receiving_mx_hostname(envelope.mx) .with_failure_reason_code( - "No TLSA records found for MX.", + "No TLSA DNSSEC records found.", ) .into(), tls_record: tls_report.record.clone(), @@ -863,35 +813,159 @@ impl QueuedMessage { last_status = Status::PermanentFailure(ErrorDetails { entity: envelope.mx.into(), details: Error::DaneError( - "No TLSA records found".into(), + "No TLSA DNSSEC records found".into(), ), }); continue 'next_host; } None - } else { - trc::event!( - Dane(DaneEvent::TlsaRecordFetchError), - SpanId = message.span_id, - Domain = domain.to_string(), - Hostname = envelope.mx.to_string(), - CausedBy = trc::Error::from(err.clone()), - Strict = strict, - Elapsed = time.elapsed(), - ); + } + Err(err) => { + let not_found = + matches!(&err, mail_auth::Error::DnsRecordNotFound(_)); - last_status = Status::from_mail_auth_error(envelope.mx, err); - continue 'next_host; + if not_found { + trc::event!( + Dane(DaneEvent::TlsaRecordNotFound), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Strict = strict, + Elapsed = time.elapsed(), + ); + + if strict { + // Report DANE required + if let Some(tls_report) = &tls_report { + server + .schedule_report(TlsEvent { + policy: PolicyType::Tlsa(None), + domain: domain.to_string(), + failure: FailureDetails::new( + ResultType::DaneRequired, + ) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code( + "No TLSA records found for MX.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + span_id: message.span_id, + }) + .await; + } + + last_status = Status::PermanentFailure(ErrorDetails { + entity: envelope.mx.into(), + details: Error::DaneError( + "No TLSA records found".into(), + ), + }); + continue 'next_host; + } + None + } else { + trc::event!( + Dane(DaneEvent::TlsaRecordFetchError), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + CausedBy = trc::Error::from(err.clone()), + Strict = strict, + Elapsed = time.elapsed(), + ); + + last_status = + Status::from_mail_auth_error(envelope.mx, err); + continue 'next_host; + } } } } + DnssecStatus::Bogus => { + trc::event!( + Dane(DaneEvent::BogusDnssecRecord), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Details = "MX", + Strict = strict, + Elapsed = time.elapsed(), + ); + + // Report bogus MX record + if let Some(tls_report) = &tls_report { + server + .schedule_report(TlsEvent { + policy: PolicyType::Tlsa(None), + domain: domain.to_string(), + failure: FailureDetails::new(ResultType::DnssecInvalid) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code( + "Bogus MX records were found.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + span_id: message.span_id, + }) + .await; + } + + last_status = Status::TemporaryFailure(ErrorDetails { + entity: envelope.mx.into(), + details: Error::DaneError("Bogus MX records were found".into()), + }); + continue 'next_host; + } + _ => { + trc::event!( + Dane(DaneEvent::TlsaRecordNotDnssecSigned), + SpanId = message.span_id, + Domain = domain.to_string(), + Hostname = envelope.mx.to_string(), + Strict = strict, + Elapsed = time.elapsed(), + ); + + if strict { + // Report DANE required + if let Some(tls_report) = &tls_report { + server + .schedule_report(TlsEvent { + policy: PolicyType::Tlsa(None), + domain: domain.to_string(), + failure: FailureDetails::new(ResultType::DaneRequired) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code( + "MX host is not in a DNSSEC signed zone.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + span_id: message.span_id, + }) + .await; + } + + last_status = Status::PermanentFailure(ErrorDetails { + entity: envelope.mx.into(), + details: Error::DaneError( + "No TLSA DNSSEC records found".into(), + ), + }); + continue 'next_host; + } + None + } } } else { None }; // Try each IP address - 'next_ip: for remote_ip in resolve_result.remote_ips { + 'next_ip: for remote_ip in remote_ips { // Throttle remote host envelope.remote_ip = remote_ip; for throttle in &queue_config.outbound_limiters.remote { diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index df7c62e7..f9fb9328 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -5,31 +5,16 @@ */ use super::NextHop; -use crate::outbound::dane::dnssec::DnssecStatus; use crate::queue::{Error, ErrorDetails, HostResponse, Status}; use common::{ Server, config::smtp::queue::{ConnectionStrategy, HostOrIp, IpAndHost, MxConfig}, expr::functions::ResolveVariable, }; -use mail_auth::{ - IpLookupStrategy, MX, - common::resolver::ToFqdn, - hickory_resolver::proto::rr::{Name, RData, RecordType}, -}; +use mail_auth::{IpLookupStrategy, MX, RecordSet}; use rand::{Rng, seq::SliceRandom}; use registry::schema::enums::ExpressionVariable; -use std::{ - future::Future, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - sync::Arc, - time::Instant, -}; - -pub struct IpLookupResult { - pub remote_ips: Vec, - pub dnssec_status: DnssecStatus, -} +use std::{future::Future, net::IpAddr, sync::Arc}; pub trait DnsLookup: Sync + Send { fn ip_lookup( @@ -39,19 +24,11 @@ pub trait DnsLookup: Sync + Send { max_results: usize, ) -> impl Future>> + Send; - fn dnssec_ip_lookup( - &self, - key: &str, - strategy: IpLookupStrategy, - max_results: usize, - ) -> impl Future, DnssecStatus)>> + Send; - fn resolve_host( &self, remote_host: &NextHop<'_>, envelope: &impl ResolveVariable, - use_dnssec: bool, - ) -> impl Future>, ErrorDetails>>> + Send; + ) -> impl Future, Status>, ErrorDetails>>> + Send; } impl DnsLookup for Server { @@ -76,7 +53,7 @@ impl DnsLookup for Server { .ipv4_lookup(key, Some(&self.inner.cache.dns_ipv4)) .await { - Ok(addrs) => addrs, + Ok(addrs) => addrs.rrset, Err(_) if has_ipv6 => Arc::new([]), Err(err) => return Err(err), } @@ -93,7 +70,7 @@ impl DnsLookup for Server { .ipv6_lookup(key, Some(&self.inner.cache.dns_ipv6)) .await { - Ok(addrs) => addrs, + Ok(addrs) => addrs.rrset, Err(_) if !ipv4_addrs.is_empty() => Arc::new([]), Err(err) => return Err(err), }; @@ -124,174 +101,21 @@ impl DnsLookup for Server { } } - async fn dnssec_ip_lookup( - &self, - key: &str, - strategy: IpLookupStrategy, - max_results: usize, - ) -> mail_auth::Result<(Vec, DnssecStatus)> { - let fqdn = key.to_fqdn(); - if let Some(secure) = self.inner.cache.dns_dnssec.get(fqdn.as_ref()) { - return Ok(( - self.ip_lookup(key, strategy, max_results).await?, - if secure { - DnssecStatus::Secure - } else { - DnssecStatus::Insecure - }, - )); - } - - #[cfg(any(test, feature = "test_mode"))] - if true { - return Ok(( - self.ip_lookup(key, strategy, max_results).await?, - DnssecStatus::Secure, - )); - } - - let (query_v4, query_v6, v4_first) = match strategy { - IpLookupStrategy::Ipv4Only => (true, false, true), - IpLookupStrategy::Ipv6Only => (false, true, false), - IpLookupStrategy::Ipv4thenIpv6 => (true, true, true), - IpLookupStrategy::Ipv6thenIpv4 => (true, true, false), - }; - let resolver = &self.core.smtp.resolvers.dnssec.resolver; - let name = Name::from_str_relaxed(fqdn.as_ref())?; - - let mut ipv4: Vec = Vec::new(); - let mut ipv6: Vec = Vec::new(); - let mut all_secure = true; - let mut v4_valid_until: Option = None; - let mut v6_valid_until: Option = None; - let mut not_found: Option = None; - - let mut record_types = Vec::with_capacity(2); - if query_v4 { - record_types.push(RecordType::A); - } - if query_v6 { - record_types.push(RecordType::AAAA); - } - - for record_type in record_types { - match resolver.lookup(name.clone(), record_type).await { - Ok(lookup) => { - let valid_until = lookup.valid_until(); - let mut found = false; - for record in lookup.answers() { - if !record.proof.is_secure() { - all_secure = false; - } - match &record.data { - RData::A(a) => { - ipv4.push(a.0); - found = true; - } - RData::AAAA(aaaa) => { - ipv6.push(aaaa.0); - found = true; - } - _ => {} - } - } - if found { - if record_type == RecordType::A { - v4_valid_until = Some(valid_until); - } else { - v6_valid_until = Some(valid_until); - } - } - } - Err(err) => { - let err: mail_auth::Error = err.into(); - if matches!(err, mail_auth::Error::DnsRecordNotFound(_)) { - not_found = Some(err); - } else { - return Err(err); - } - } - } - } - - if ipv4.is_empty() && ipv6.is_empty() { - return Err(not_found.unwrap_or(mail_auth::Error::DnsRecordNotFound( - mail_auth::hickory_resolver::proto::op::ResponseCode::NXDomain, - ))); - } - - if let Some(valid_until) = v4_valid_until { - self.inner.cache.dns_ipv4.insert_with_expiry( - fqdn.clone(), - Arc::from(ipv4.as_slice()), - valid_until, - ); - } - if let Some(valid_until) = v6_valid_until { - self.inner.cache.dns_ipv6.insert_with_expiry( - fqdn.clone(), - Arc::from(ipv6.as_slice()), - valid_until, - ); - } - if let Some(valid_until) = v4_valid_until.into_iter().chain(v6_valid_until).min() { - self.inner - .cache - .dns_dnssec - .insert_with_expiry(fqdn, all_secure, valid_until); - } - - let remote_ips: Vec = if v4_first { - ipv4.into_iter() - .map(IpAddr::from) - .chain(ipv6.into_iter().map(IpAddr::from)) - .take(max_results) - .collect() - } else { - ipv6.into_iter() - .map(IpAddr::from) - .chain(ipv4.into_iter().map(IpAddr::from)) - .take(max_results) - .collect() - }; - - Ok(( - remote_ips, - if all_secure { - DnssecStatus::Secure - } else { - DnssecStatus::Insecure - }, - )) - } - #[allow(unused_mut)] async fn resolve_host( &self, remote_host: &NextHop<'_>, envelope: &impl ResolveVariable, - use_dnssec: bool, - ) -> Result>, ErrorDetails>> { - let (mut remote_ips, dnssec_status) = match remote_host.fqdn_hostname() { - HostOrIp::Host(hostname) => { - let lookup = if use_dnssec { - self.dnssec_ip_lookup( - hostname.as_ref(), - remote_host.ip_lookup_strategy(), - remote_host.max_multi_homed(), - ) - .await - } else { - self.ip_lookup( - hostname.as_ref(), - remote_host.ip_lookup_strategy(), - remote_host.max_multi_homed(), - ) - .await - .map(|ips| (ips, DnssecStatus::Insecure)) - }; - - lookup.map_err(|err| { + ) -> Result, Status>, ErrorDetails>> { + let mut remote_ips = match remote_host.fqdn_hostname() { + HostOrIp::Host(hostname) => self + .ip_lookup( + hostname.as_ref(), + remote_host.ip_lookup_strategy(), + remote_host.max_multi_homed(), + ) + .await + .map_err(|err| { if let mail_auth::Error::DnsRecordNotFound(_) = &err { if matches!( remote_host, @@ -318,9 +142,8 @@ impl DnsLookup for Server { ), }) } - })? - } - HostOrIp::Ip(ip) => (vec![ip], DnssecStatus::Insecure), + })?, + HostOrIp::Ip(ip) => vec![ip], }; if !remote_ips.is_empty() { @@ -335,10 +158,7 @@ impl DnsLookup for Server { } } - Ok(IpLookupResult { - remote_ips, - dnssec_status, - }) + Ok(remote_ips) } else { Err(Status::TemporaryFailure(ErrorDetails { entity: remote_host.hostname().into(), @@ -383,17 +203,17 @@ pub trait ToNextHop { ) -> Option>>; } -impl ToNextHop for Arc<[MX]> { +impl ToNextHop for RecordSet { fn to_remote_hosts<'x, 'y: 'x>( &'x self, domain: &'y str, config: &'x MxConfig, ) -> Option>> { - if !self.is_empty() { + if !self.rrset.is_empty() { // Obtain max number of MX hosts to process let mut remote_hosts = Vec::with_capacity(config.max_mx); - 'outer: for mx in self.iter() { + 'outer: for mx in self.rrset.iter() { if mx.exchanges.len() > 1 { let mut slice = mx.exchanges.iter().collect::>(); slice.shuffle(&mut rand::rng()); @@ -401,6 +221,7 @@ impl ToNextHop for Arc<[MX]> { remote_hosts.push(NextHop::MX { host: remote_host.as_ref(), is_implicit: false, + dnssec_status: self.dnssec_status, config, }); if remote_hosts.len() == config.max_mx { @@ -415,6 +236,7 @@ impl ToNextHop for Arc<[MX]> { remote_hosts.push(NextHop::MX { host: remote_host.as_ref(), is_implicit: false, + dnssec_status: self.dnssec_status, config, }); if remote_hosts.len() == config.max_mx { @@ -429,6 +251,7 @@ impl ToNextHop for Arc<[MX]> { vec![NextHop::MX { host: domain, is_implicit: true, + dnssec_status: self.dnssec_status, config, }] .into() diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index 7587f844..22064995 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -13,7 +13,7 @@ use common::config::{ smtp::queue::{HostOrIp, MxConfig, RelayConfig}, }; use directory::Credentials; -use mail_auth::IpLookupStrategy; +use mail_auth::{DnssecStatus, IpLookupStrategy}; use smtp_proto::{Response, Severity}; use std::{borrow::Cow, net::IpAddr}; @@ -233,6 +233,7 @@ pub enum NextHop<'x> { is_implicit: bool, host: &'x str, config: &'x MxConfig, + dnssec_status: DnssecStatus, }, } @@ -334,6 +335,13 @@ impl NextHop<'_> { NextHop::Relay(host) => host.protocol == ServerProtocol::Smtp, } } + + fn dnssec_status(&self) -> DnssecStatus { + match self { + NextHop::MX { dnssec_status, .. } => *dnssec_status, + NextHop::Relay(_) => DnssecStatus::Indeterminate, + } + } } impl DeliveryResult { diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index 7191fadb..b545c182 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spam-filter" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -14,7 +14,7 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { version = "0.9" } +mail-auth = { version = "0.10" } tokio = { version = "1.47", features = ["net", "macros"] } psl = "2" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 56ee9520..031146fc 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "store" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/store/src/backend/postgres/main.rs b/crates/store/src/backend/postgres/main.rs index 722988a4..0e6dad6e 100644 --- a/crates/store/src/backend/postgres/main.rs +++ b/crates/store/src/backend/postgres/main.rs @@ -14,9 +14,7 @@ use crate::{ *, }; use ::registry::schema::{enums::PostgreSqlRecyclingMethod, structs}; -use deadpool_postgres::{ - Config, ManagerConfig, Object, PoolConfig, RecyclingMethod, Runtime, -}; +use deadpool_postgres::{Config, ManagerConfig, Object, PoolConfig, RecyclingMethod, Runtime}; use tokio_postgres::NoTls; use utils::tls::rustls_client_config; diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 0f3759c9..2bbe2282 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "trc" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] event_macro = { path = "./event-macro" } -mail-auth = { version = "0.9" } +mail-auth = { version = "0.10" } mail-parser = { version = "0.11", features = ["full_encoding"] } base64 = "0.22.1" serde = "1.0" diff --git a/crates/trc/event-macro/Cargo.toml b/crates/trc/event-macro/Cargo.toml index 66ab146c..da44d1c6 100644 --- a/crates/trc/event-macro/Cargo.toml +++ b/crates/trc/event-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "event_macro" -version = "0.16.11" +version = "0.16.12" edition = "2024" [lib] diff --git a/crates/trc/src/event/enums.rs b/crates/trc/src/event/enums.rs index 4120aa9a..53d883df 100644 --- a/crates/trc/src/event/enums.rs +++ b/crates/trc/src/event/enums.rs @@ -6,8 +6,8 @@ // This file is auto-generated. Do not edit directly. -pub const TOTAL_EVENT_COUNT: usize = 605; -pub const TOTAL_METRIC_COUNT: usize = 339; +pub const TOTAL_EVENT_COUNT: usize = 606; +pub const TOTAL_METRIC_COUNT: usize = 340; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum EventType { @@ -156,6 +156,7 @@ pub enum DaneEvent { TlsaRecordNotFound = 75, TlsaRecordNotDnssecSigned = 74, TlsaRecordInvalid = 72, + BogusDnssecRecord = 605, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -912,6 +913,7 @@ pub enum MetricType { DaneTlsaRecordNotFound = 61, DaneTlsaRecordNotDnssecSigned = 62, DaneTlsaRecordInvalid = 63, + DaneBogusDnssecRecord = 339, DeliveryTotalTime = 2, DeliveryAttemptTime = 3, DeliveryActiveConnections = 22, diff --git a/crates/trc/src/event/enums_impl.rs b/crates/trc/src/event/enums_impl.rs index 619ca607..56e1c715 100644 --- a/crates/trc/src/event/enums_impl.rs +++ b/crates/trc/src/event/enums_impl.rs @@ -81,6 +81,7 @@ impl EventType { b"dane.tlsa-record-not-found" => EventType::Dane(DaneEvent::TlsaRecordNotFound), b"dane.tlsa-record-not-dnssec-signed" => EventType::Dane(DaneEvent::TlsaRecordNotDnssecSigned), b"dane.tlsa-record-invalid" => EventType::Dane(DaneEvent::TlsaRecordInvalid), + b"dane.bogus-dnssec-record" => EventType::Dane(DaneEvent::BogusDnssecRecord), b"delivery.attempt-start" => EventType::Delivery(DeliveryEvent::AttemptStart), b"delivery.attempt-end" => EventType::Delivery(DeliveryEvent::AttemptEnd), b"delivery.completed" => EventType::Delivery(DeliveryEvent::Completed), @@ -702,6 +703,7 @@ impl EventType { "dane.tlsa-record-not-dnssec-signed" } EventType::Dane(DaneEvent::TlsaRecordInvalid) => "dane.tlsa-record-invalid", + EventType::Dane(DaneEvent::BogusDnssecRecord) => "dane.bogus-dnssec-record", EventType::Delivery(DeliveryEvent::AttemptStart) => "delivery.attempt-start", EventType::Delivery(DeliveryEvent::AttemptEnd) => "delivery.attempt-end", EventType::Delivery(DeliveryEvent::Completed) => "delivery.completed", @@ -1436,6 +1438,7 @@ impl EventType { EventType::Dane(DaneEvent::TlsaRecordNotFound) => 75, EventType::Dane(DaneEvent::TlsaRecordNotDnssecSigned) => 74, EventType::Dane(DaneEvent::TlsaRecordInvalid) => 72, + EventType::Dane(DaneEvent::BogusDnssecRecord) => 605, EventType::Delivery(DeliveryEvent::AttemptStart) => 77, EventType::Delivery(DeliveryEvent::AttemptEnd) => 76, EventType::Delivery(DeliveryEvent::Completed) => 80, @@ -2046,6 +2049,7 @@ impl EventType { 75 => Some(EventType::Dane(DaneEvent::TlsaRecordNotFound)), 74 => Some(EventType::Dane(DaneEvent::TlsaRecordNotDnssecSigned)), 72 => Some(EventType::Dane(DaneEvent::TlsaRecordInvalid)), + 605 => Some(EventType::Dane(DaneEvent::BogusDnssecRecord)), 77 => Some(EventType::Delivery(DeliveryEvent::AttemptStart)), 76 => Some(EventType::Delivery(DeliveryEvent::AttemptEnd)), 80 => Some(EventType::Delivery(DeliveryEvent::Completed)), @@ -2710,6 +2714,7 @@ impl EventType { EventType::Dane(DaneEvent::TlsaRecordNotFound) => Level::Info, EventType::Dane(DaneEvent::TlsaRecordNotDnssecSigned) => Level::Info, EventType::Dane(DaneEvent::TlsaRecordInvalid) => Level::Info, + EventType::Dane(DaneEvent::BogusDnssecRecord) => Level::Info, EventType::Delivery(DeliveryEvent::AttemptStart) => Level::Info, EventType::Delivery(DeliveryEvent::AttemptEnd) => Level::Info, EventType::Delivery(DeliveryEvent::Completed) => Level::Info, @@ -3059,6 +3064,7 @@ impl EventType { "TLSA record not DNSSEC signed" } EventType::Dane(DaneEvent::TlsaRecordInvalid) => "Invalid TLSA record", + EventType::Dane(DaneEvent::BogusDnssecRecord) => "Bogus DNSSEC record", EventType::Delivery(DeliveryEvent::AttemptStart) => "Delivery attempt started", EventType::Delivery(DeliveryEvent::AttemptEnd) => "Delivery attempt ended", EventType::Delivery(DeliveryEvent::Completed) => "Delivery completed", @@ -4072,6 +4078,7 @@ impl EventType { EventType::Dane(DaneEvent::TlsaRecordNotFound), EventType::Dane(DaneEvent::TlsaRecordNotDnssecSigned), EventType::Dane(DaneEvent::TlsaRecordInvalid), + EventType::Dane(DaneEvent::BogusDnssecRecord), EventType::Delivery(DeliveryEvent::AttemptStart), EventType::Delivery(DeliveryEvent::AttemptEnd), EventType::Delivery(DeliveryEvent::Completed), @@ -4652,6 +4659,7 @@ impl MetricType { b"dane.tlsa-record-not-found" => MetricType::DaneTlsaRecordNotFound, b"dane.tlsa-record-not-dnssec-signed" => MetricType::DaneTlsaRecordNotDnssecSigned, b"dane.tlsa-record-invalid" => MetricType::DaneTlsaRecordInvalid, + b"dane.bogus-dnssec-record" => MetricType::DaneBogusDnssecRecord, b"delivery.total-time" => MetricType::DeliveryTotalTime, b"delivery.attempt-time" => MetricType::DeliveryAttemptTime, b"delivery.active-connections" => MetricType::DeliveryActiveConnections, @@ -4997,6 +5005,7 @@ impl MetricType { MetricType::DaneTlsaRecordNotFound => "dane.tlsa-record-not-found", MetricType::DaneTlsaRecordNotDnssecSigned => "dane.tlsa-record-not-dnssec-signed", MetricType::DaneTlsaRecordInvalid => "dane.tlsa-record-invalid", + MetricType::DaneBogusDnssecRecord => "dane.bogus-dnssec-record", MetricType::DeliveryTotalTime => "delivery.total-time", MetricType::DeliveryAttemptTime => "delivery.attempt-time", MetricType::DeliveryActiveConnections => "delivery.active-connections", @@ -5353,6 +5362,7 @@ impl MetricType { MetricType::DaneTlsaRecordNotFound => 61, MetricType::DaneTlsaRecordNotDnssecSigned => 62, MetricType::DaneTlsaRecordInvalid => 63, + MetricType::DaneBogusDnssecRecord => 339, MetricType::DeliveryTotalTime => 2, MetricType::DeliveryAttemptTime => 3, MetricType::DeliveryActiveConnections => 22, @@ -5697,6 +5707,7 @@ impl MetricType { 61 => Some(MetricType::DaneTlsaRecordNotFound), 62 => Some(MetricType::DaneTlsaRecordNotDnssecSigned), 63 => Some(MetricType::DaneTlsaRecordInvalid), + 339 => Some(MetricType::DaneBogusDnssecRecord), 2 => Some(MetricType::DeliveryTotalTime), 3 => Some(MetricType::DeliveryAttemptTime), 22 => Some(MetricType::DeliveryActiveConnections), @@ -6042,6 +6053,7 @@ impl MetricType { MetricType::DaneTlsaRecordNotFound => 75, MetricType::DaneTlsaRecordNotDnssecSigned => 74, MetricType::DaneTlsaRecordInvalid => 72, + MetricType::DaneBogusDnssecRecord => 605, MetricType::DeliveryAttemptStart => 77, MetricType::DeliveryAttemptEnd => 76, MetricType::DeliveryCompleted => 80, @@ -6360,6 +6372,7 @@ impl MetricType { MetricType::DaneTlsaRecordNotFound => "TLSA record not found", MetricType::DaneTlsaRecordNotDnssecSigned => "TLSA record not DNSSEC signed", MetricType::DaneTlsaRecordInvalid => "Invalid TLSA record", + MetricType::DaneBogusDnssecRecord => "Bogus DNSSEC record", MetricType::DeliveryTotalTime => { "Total message delivery time from submission to delivery" } @@ -6726,6 +6739,7 @@ impl MetricType { | MetricType::DaneTlsaRecordNotFound | MetricType::DaneTlsaRecordNotDnssecSigned | MetricType::DaneTlsaRecordInvalid + | MetricType::DaneBogusDnssecRecord | MetricType::DeliveryAttemptStart | MetricType::DeliveryAttemptEnd | MetricType::DeliveryCompleted @@ -7060,6 +7074,7 @@ impl MetricType { MetricType::DaneTlsaRecordNotFound, MetricType::DaneTlsaRecordNotDnssecSigned, MetricType::DaneTlsaRecordInvalid, + MetricType::DaneBogusDnssecRecord, MetricType::DeliveryTotalTime, MetricType::DeliveryAttemptTime, MetricType::DeliveryActiveConnections, diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 2922a279..b5a5ba6b 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "types" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 3f9d2f83..b4863371 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.16.11" +version = "0.16.12" edition = "2024" [dependencies] @@ -12,7 +12,7 @@ rustls-platform-verifier = "0.7" tokio = { version = "1.47", features = ["net", "macros", "signal"] } tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } serde = { version = "1.0", features = ["derive"]} -mail-auth = { version = "0.9" } +mail-auth = { version = "0.10" } smtp-proto = { version = "0.2" } ahash = { version = "0.8", features = ["serde"] } chrono = "0.4" diff --git a/crates/utils/proc-macros/Cargo.toml b/crates/utils/proc-macros/Cargo.toml index f2ffb76d..3077e42a 100644 --- a/crates/utils/proc-macros/Cargo.toml +++ b/crates/utils/proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proc_macros" -version = "0.16.11" +version = "0.16.12" edition = "2024" [lib] diff --git a/crates/utils/src/cache.rs b/crates/utils/src/cache.rs index 3c3f540f..d747afcf 100644 --- a/crates/utils/src/cache.rs +++ b/crates/utils/src/cache.rs @@ -5,7 +5,7 @@ */ use arcstr::ArcStr; -use mail_auth::{MX, ResolverCache, Txt}; +use mail_auth::{DnssecStatus, MX, RecordSet, ResolverCache, Txt}; use quick_cache::{ Equivalent, Weighter, sync::{DefaultLifecycle, PlaceholderGuard}, @@ -258,6 +258,12 @@ impl CacheItemWeight for Arc<[T]> { } } +impl CacheItemWeight for RecordSet { + fn weight(&self) -> u64 { + self.rrset.weight() + std::mem::size_of::() as u64 + } +} + impl CacheItemWeight for u32 { fn weight(&self) -> u64 { std::mem::size_of::() as u64 diff --git a/resources/schema/schema.json.gz b/resources/schema/schema.json.gz index a8181418..634bb244 100644 Binary files a/resources/schema/schema.json.gz and b/resources/schema/schema.json.gz differ diff --git a/resources/schema/schema.json.sha256 b/resources/schema/schema.json.sha256 index 0c7a0eb8..d8237afb 100644 --- a/resources/schema/schema.json.sha256 +++ b/resources/schema/schema.json.sha256 @@ -1 +1 @@ -nvdIm9lmWr6eYd0LhUUTOCKQrZ6EP2w7aPmfNXw7qzI \ No newline at end of file +zeYWlKx8i_-qA0NG_5jMhgD9U2mcTmGFevxlNBSDDoI \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml index c94575fe..f594adc5 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tests" -version = "0.16.11" +version = "0.16.12" edition = "2024" [features] @@ -45,7 +45,7 @@ migration = { path = "../crates/migration", features = ["test_mode", "enterprise trc = { path = "../crates/trc", features = ["enterprise"] } managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] } smtp-proto = { version = "0.2" } -mail-auth = { version = "0.9", features = ["test"] } +mail-auth = { version = "0.10", features = ["test"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = "0.4.4" sieve-rs = { version = "0.7", features = ["rkyv"] } diff --git a/tests/resources/tls_cert.pem b/tests/resources/tls_cert.pem index 02b9b963..5c8104bd 100644 --- a/tests/resources/tls_cert.pem +++ b/tests/resources/tls_cert.pem @@ -1,29 +1,29 @@ -----BEGIN CERTIFICATE----- -MIIFCTCCAvGgAwIBAgIUCgHGQYUqtelbHGVSzCVwBL3fyEUwDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDUxNjExNDAzNFoXDTIzMDUx -NjExNDAzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF -AAOCAg8AMIICCgKCAgEAtwS0Fzl3SjaCuKEXgZ/fdWbDoj/qDphyNCAKNevQ0+D0 -STNkWCO04aFSH0zcL8zoD9gokNos0i7OU9//ZhZQmex4V6EFdZn8bFwUWN/scUvW -HEFXVjtHldO2isZgIxH9LuwRv7KAgkISuWahqerOVDhe7SeQUV0AJGNEh3cT9PZr -gSY931BxB7n+5k8eoSk8Z1gtBzQzL62kVGpHDKfw8yX8m65owF9eLUBrNzgxmXfC -xpuHwj7hmVhS09PPKeN/RsFS8PsYO7bo0u8jEKalteumjRT7RyUEbioqfo6ZFOGj -FHPIq/uKXS9zN1fpoyNh3ur5hMznQhrqlwBM9KlM7GdBJ0pZ3ad0YjT8IL/GnGKR -85J2WZdLqaQdUZo7nV67FhqdDlNE4MdwiykTMjfmLRXGAVhAzJHKyRKNwmkI2aqe -S7aqeNgvuDBwY80Q9a2rb5py1Aw+L8yCkUBuHboToDpxSVRDNN8DrWNmmsXnxsOG -wRDODy4GICKyxlP+RFSM8xWSQ6y9ktS2OfDBm+Eqcw+3pZKhdz2wgxLkUBJ8X1eh -kJrCA/6LTuhy6m6mMjAfoSOFU7fu88jxaWPgvP7GKyH+LM/t9eucobz2ks5rtSjz -V4Dc5DCS94/OpVRHwHdaFSPbJKBN9Ev8gnNrAyx/aBPGoHBPG/QUiU7dcUNIPt0C -AwEAAaNTMFEwHQYDVR0OBBYEFI167IxBmErB11EqiPPqFLa31ZaMMB8GA1UdIwQY -MBaAFI167IxBmErB11EqiPPqFLa31ZaMMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI -hvcNAQELBQADggIBALU00IOiH5ubEauVCmakms5ermNTZfculnhnDfWTLMeh2+a7 -G4cqADErfMhm/mmLbrw33t9s6tCAhQltvewKR40ST9uMPSyiQbYaCXd5DXnuI6Ox -JtNW+UOWIaMf8abnkdLvREOvb8dVQS1i3xq14tAjY5XgpGwCPP8m54b7N3Q7soLn -e5PDhPNTnhRIn2RLuYoZmQmMA5fcqEUDYff4epUww7PhrM1QckZligI3566NlGOf -j1G9JrivBtY0eaJtamIFnGMBT0ThDudxVja2Nv0C2Elry0p4T/o4nc4M67BJ/y1R -vjNLAgFhbxssemU3lZqSd+pykpJBwDBjFSPrZZmQcbk7H6Uz8V1xr/xuzfw6fA13 -NWZ5vLgP/DQ13sM+XFlxThKfbPMPVe/UCTvfGtNW+3XyBgPntEkR+fNEawQmzbYl -R+X1ymT9MZnEZqRMf7/UD/SYek1aUJefoew3upjMgxYVvh4F8dqJ+39F+xoFzIA2 -1dDAEMzXtjA3zKhZ2cycZbEzpJvYA3eGLuR16Suqfi4kPvfwK0mOhCxQmpayt7/X -vuEzW6dPCH8Hgbb0WvsSppGOvhdbDaZFNfFc5eNSxhyKzu3H3ACNImZRtZE+yixx -0fR8+xz9kDLf8xupV+X9heyFGHSyYU2Lveaevtr2Ij3weLRgJ6LbNALoeKXk +MIIFCzCCAvOgAwIBAgIUSL6bpA76cxsPc2XRu46iEcU2fccwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDYyNTE3MDgxOFoYDzIxMjYw +NjAxMTcwODE4WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC3BLQXOXdKNoK4oReBn991ZsOiP+oOmHI0IAo169DT +4PRJM2RYI7ThoVIfTNwvzOgP2CiQ2izSLs5T3/9mFlCZ7HhXoQV1mfxsXBRY3+xx +S9YcQVdWO0eV07aKxmAjEf0u7BG/soCCQhK5ZqGp6s5UOF7tJ5BRXQAkY0SHdxP0 +9muBJj3fUHEHuf7mTx6hKTxnWC0HNDMvraRUakcMp/DzJfybrmjAX14tQGs3ODGZ +d8LGm4fCPuGZWFLT088p439GwVLw+xg7tujS7yMQpqW166aNFPtHJQRuKip+jpkU +4aMUc8ir+4pdL3M3V+mjI2He6vmEzOdCGuqXAEz0qUzsZ0EnSlndp3RiNPwgv8ac +YpHzknZZl0uppB1RmjudXrsWGp0OU0Tgx3CLKRMyN+YtFcYBWEDMkcrJEo3CaQjZ +qp5Ltqp42C+4MHBjzRD1ratvmnLUDD4vzIKRQG4duhOgOnFJVEM03wOtY2aaxefG +w4bBEM4PLgYgIrLGU/5EVIzzFZJDrL2S1LY58MGb4SpzD7elkqF3PbCDEuRQEnxf +V6GQmsID/otO6HLqbqYyMB+hI4VTt+7zyPFpY+C8/sYrIf4sz+3165yhvPaSzmu1 +KPNXgNzkMJL3j86lVEfAd1oVI9skoE30S/yCc2sDLH9oE8agcE8b9BSJTt1xQ0g+ +3QIDAQABo1MwUTAdBgNVHQ4EFgQUjXrsjEGYSsHXUSqI8+oUtrfVlowwHwYDVR0j +BBgwFoAUjXrsjEGYSsHXUSqI8+oUtrfVlowwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAaXocvWeEiqOHhV6miozqNo3H1sGs9VrSRP8+mpIHpW5f +NulmMNgqfH+4MIzR5QMqj6n6do8F72s+K3xQDELT6468KtGfM8q0gGGhkvHxv5Wp +9Iibj5MGn3J8oZvtp1NKSLdURjHD+H46zSAHQszdkFUavidv7irAVDAgY4Yh+AGv +H4GA4VbRsjv5KFuMgXGbwNQxrbRT52+D5fzw9EObh6TBhEpxANyrjGtzRCVoTclS +R9N6KUd4cDjvif1Sdrnp3VONZ/Bf3BXij7PbgSSbzVdxEd82E0TQKLofeTlRcbT2 +IzIb8jqwAiOU4Q9BqjHB8tmJTg6DK4a2mU9q0pRjjtV34lzSTfM0ERnuwjIp0WK9 +Ot138lFOpblB4FNrpm+uxdMzti/wkaJ9JNfe11aLS7xtnDKiEFXdVatMhJ8S3MFf +DWb+hFdtvqYEOtc60ZGEsQTR6NdRgjg8JWLMWbN/gL1vH8FdyAmGZAa5m9OWmVn8 +lDrOewStid7o12nsyH0h8HSU3VkC9EpnAQG5MBMwNTVTPycYZluSx3EM1iOrNb/3 +keMF8oetq+LSAhpspD/l0OPIMFk53u62R8yAS/tKwG2q+li/k51ImD0fY+G3329n +2f0vMZDA+RjI8zIQIvrQWOr7AqkytZ/ETWz0c2GlWbJEgEA1Y2Lo/qGrLBji4wQ= -----END CERTIFICATE----- diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index b5300f2f..a099eb75 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -21,8 +21,8 @@ use common::{ use http_proto::{JsonResponse, ToHttpResponse}; use hyper::Method; use mail_auth::{ - ArcOutput, DkimOutput, DkimResult, DmarcResult, IprevOutput, IprevResult, MX, SpfOutput, - SpfResult, dkim::Signature, dmarc::Policy, + ArcOutput, DkimOutput, DkimResult, DmarcResult, DnssecStatus, IprevOutput, IprevResult, MX, + SpfOutput, SpfResult, dkim::Signature, dmarc::Policy, }; use mail_parser::MessageParser; use registry::{ @@ -214,6 +214,7 @@ async fn antispam() { exchanges: vec!["127.0.0.1".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(100), ); } diff --git a/tests/src/smtp/lookup/expressions.rs b/tests/src/smtp/lookup/expressions.rs index 691b238b..ad0f4c79 100644 --- a/tests/src/smtp/lookup/expressions.rs +++ b/tests/src/smtp/lookup/expressions.rs @@ -6,7 +6,7 @@ use crate::utils::{dns::DnsCache, server::TestServerBuilder}; use common::expr::{tokenizer::TokenMap, *}; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::schema::{ enums::ExpressionVariable, prelude::{ObjectType, Property}, @@ -75,6 +75,7 @@ async fn expressions() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index b415fa70..f7b05a55 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -11,7 +11,7 @@ use common::config::smtp::{ report::AggregateFrequency, resolver::{Mode, MxPattern, Policy}, }; -use mail_auth::{IpLookupStrategy, MX}; +use mail_auth::{DnssecStatus, IpLookupStrategy, MX, RecordSet}; use mail_parser::DateTime; use registry::{ schema::{ @@ -225,25 +225,28 @@ async fn strategies() { #[test] fn to_remote_hosts() { - let mx: Arc<[MX]> = Arc::from(vec![ - MX { - exchanges: vec!["mx1".into(), "mx2".into()].into_boxed_slice(), - preference: 10, - }, - MX { - exchanges: vec!["mx3".into(), "mx4".into(), "mx5".into(), "mx6".into()] - .into_boxed_slice(), - preference: 20, - }, - MX { - exchanges: vec!["mx7".into(), "mx8".into()].into_boxed_slice(), - preference: 10, - }, - MX { - exchanges: vec!["mx9".into(), "mxA".into()].into_boxed_slice(), - preference: 10, - }, - ]); + let mx: RecordSet = RecordSet { + rrset: Arc::from(vec![ + MX { + exchanges: vec!["mx1".into(), "mx2".into()].into_boxed_slice(), + preference: 10, + }, + MX { + exchanges: vec!["mx3".into(), "mx4".into(), "mx5".into(), "mx6".into()] + .into_boxed_slice(), + preference: 20, + }, + MX { + exchanges: vec!["mx7".into(), "mx8".into()].into_boxed_slice(), + preference: 10, + }, + MX { + exchanges: vec!["mx9".into(), "mxA".into()].into_boxed_slice(), + preference: 10, + }, + ]), + dnssec_status: DnssecStatus::Indeterminate, + }; let mx_config = MxConfig { max_mx: 7, max_multi_homed: 2, @@ -256,10 +259,13 @@ fn to_remote_hosts() { assert!((*host.as_bytes().last().unwrap() - b'0') <= 8); } } - let mx: Arc<[MX]> = Arc::from(vec![MX { - exchanges: vec![".".into()].into_boxed_slice(), - preference: 0, - }]); + let mx: RecordSet = RecordSet { + rrset: Arc::from(vec![MX { + exchanges: vec![".".into()].into_boxed_slice(), + preference: 0, + }]), + dnssec_status: DnssecStatus::Indeterminate, + }; assert!(mx.to_remote_hosts("domain", &mx_config).is_none()); } diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index cda46abd..f2e4ee9b 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -9,7 +9,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServerBuilder}, }; use ahash::{AHashMap, HashMap, HashSet}; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::NetworkListenerProtocol, @@ -134,6 +134,7 @@ async fn manage_queue() { exchanges: vec!["mx1.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index 953fbaf4..1d2d3b1c 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -16,7 +16,7 @@ use common::{ ipc::PolicyType, }; use mail_auth::{ - MX, + DnssecStatus, MX, common::parse::TxtRecordParser, mta_sts::{ReportUri, TlsRpt}, report::tlsrpt::ResultType, @@ -32,7 +32,10 @@ use registry::schema::{ }; use rustls_pki_types::CertificateDer; use sha2::{Digest, Sha256}; -use smtp::outbound::dane::{dnssec::TlsaLookup, verify::TlsaVerify}; +use smtp::outbound::dane::{ + dnssec::{TlsaLookup, TlsaResult}, + verify::TlsaVerify, +}; use smtp::queue::{Error, ErrorDetails, Status}; use std::{ collections::BTreeSet, @@ -118,6 +121,7 @@ async fn dane_verify() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( @@ -255,9 +259,13 @@ async fn dane_verify() { // An insecure (non-DNSSEC) MX zone must not honor TLSA records, // even when valid records are cached. - local.server.dnssec_add( - "mx.foobar.org", - false, + local.server.mx_add( + "foobar.org", + vec![MX { + exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), + preference: 10, + }], + DnssecStatus::Insecure, Instant::now() + Duration::from_secs(10), ); session @@ -347,6 +355,7 @@ async fn dane_downgrade_on_tlsa_servfail() { exchanges: vec!["mx._dns_error.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( @@ -372,6 +381,135 @@ async fn dane_downgrade_on_tlsa_servfail() { remote.assert_no_events(); } +#[tokio::test] +#[serial_test::serial] +async fn dane_bogus_dnssec_temp_fails() { + let mut local = TestServerBuilder::new("smtp_dane_bogus_local") + .await + .with_http_listener(19022) + .await + .disable_services() + .capture_queue() + .capture_reporting() + .build() + .await; + + let local_admin = local.account("admin"); + local_admin.mta_allow_relaying().await; + local_admin.mta_no_auth().await; + local_admin + .registry_create_object(TlsReportSettings { + send_frequency: Expression { + else_: "weekly".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + let (tls_strategy_id, mut tls_strategy) = local_admin + .registry_get_all::() + .await + .into_iter() + .find(|(_, s)| s.name == "default") + .unwrap(); + tls_strategy.dane = MtaRequiredOrOptional::Require; + tls_strategy.start_tls = MtaRequiredOrOptional::Require; + let mut tls_strategy = serde_json::to_value(tls_strategy).unwrap(); + tls_strategy + .as_object_mut() + .unwrap() + .retain(|k, _| k != "name"); + local_admin + .registry_update_object(ObjectType::MtaTlsStrategy, tls_strategy_id, tls_strategy) + .await; + local_admin.reload_settings().await; + local.reload_core(); + local.expect_reload_settings().await; + + local.server.txt_add( + "_smtp._tls.foobar.org", + TlsRpt::parse(b"v=TLSRPTv1; rua=mailto:reports@foobar.org").unwrap(), + Instant::now() + Duration::from_secs(30), + ); + local.server.ipv4_add( + "mx.foobar.org", + vec!["127.0.0.1".parse().unwrap()], + Instant::now() + Duration::from_secs(30), + ); + local.server.ipv4_add( + "mx._dnssec_bogus.foobar.org", + vec!["127.0.0.1".parse().unwrap()], + Instant::now() + Duration::from_secs(30), + ); + + let mut session = local.new_mta_session(); + session.data.remote_ip_str = "10.0.0.1".into(); + session.eval_session_params().await; + session.ehlo("mx.test.org").await; + + local.server.mx_add( + "foobar.org", + vec![MX { + exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), + preference: 10, + }], + DnssecStatus::Bogus, + Instant::now() + Duration::from_secs(30), + ); + session + .send_message("john@test.org", &["bill@foobar.org"], "test:no_dkim", "250") + .await; + local + .expect_message_then_deliver() + .await + .try_deliver(local.server.clone()); + let retry = local.expect_message().await; + assert!(retry.message.recipients[0].retry.due > now()); + + let report = local.read_report().await.unwrap_tls(); + assert_eq!(report.domain, "foobar.org"); + assert_eq!(report.policy, PolicyType::Tlsa(None)); + assert_eq!( + report.failure.as_ref().unwrap().result_type, + ResultType::DnssecInvalid + ); + assert_eq!( + report.failure.as_ref().unwrap().receiving_mx_hostname, + Some("mx.foobar.org".to_string()) + ); + + local.server.mx_add( + "foobar.org", + vec![MX { + exchanges: vec!["mx._dnssec_bogus.foobar.org".into()].into_boxed_slice(), + preference: 10, + }], + DnssecStatus::Secure, + Instant::now() + Duration::from_secs(30), + ); + session + .send_message("john@test.org", &["bill@foobar.org"], "test:no_dkim", "250") + .await; + local + .expect_message_then_deliver() + .await + .try_deliver(local.server.clone()); + let retry = local.expect_message().await; + assert!(retry.message.recipients[0].retry.due > now()); + + let report = local.read_report().await.unwrap_tls(); + assert_eq!(report.domain, "foobar.org"); + assert_eq!(report.policy, PolicyType::Tlsa(None)); + assert_eq!( + report.failure.as_ref().unwrap().result_type, + ResultType::DnssecInvalid + ); + assert_eq!( + report.failure.as_ref().unwrap().receiving_mx_hostname, + Some("mx._dnssec_bogus.foobar.org".to_string()) + ); +} + #[tokio::test] #[serial_test::serial] async fn dane_test() { @@ -462,12 +600,15 @@ async fn dane_test() { } // Successful DANE verification (end-entity match, RFC 7671 Section 5.1) - let tlsa = test + let tlsa = match test .server .tlsa_lookup(format!("_25._tcp.{host}.")) .await .unwrap() - .unwrap(); + { + TlsaResult::Secure(tlsa) => tlsa, + _ => panic!("expected secure TLSA records"), + }; assert_eq!( tlsa.verify(0, &host, &[host.as_str()], Some(&certs)), diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index f14be93e..07037d1f 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -11,7 +11,7 @@ use crate::{ }, utils::{dns::DnsCache, server::TestServerBuilder}, }; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::schema::structs::{Expression, MtaStageData}; use smtp_proto::{MAIL_REQUIRETLS, MAIL_RET_HDRS, MAIL_SMTPUTF8, RCPT_NOTIFY_NEVER}; use std::time::{Duration, Instant}; @@ -98,6 +98,7 @@ async fn extensions() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs index 4854f093..a6363990 100644 --- a/tests/src/smtp/outbound/fallback_relay.rs +++ b/tests/src/smtp/outbound/fallback_relay.rs @@ -8,7 +8,7 @@ use crate::{ smtp::session::TestSession, utils::{dns::DnsCache, server::TestServerBuilder}, }; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::MtaProtocol, @@ -102,6 +102,7 @@ async fn fallback_relay() { exchanges: vec!["_dns_error.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index 4beaf26f..4dd4a5d5 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -8,7 +8,7 @@ use crate::{ smtp::{inbound::TestQueueEvent, session::TestSession}, utils::{dns::DnsCache, server::TestServerBuilder}, }; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::MtaIpStrategy, @@ -84,6 +84,7 @@ async fn ip_lookup_strategy() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); if matches!(strategy, MtaIpStrategy::V6ThenV4) { diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index a3c6bfab..0248fec2 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -13,7 +13,7 @@ use crate::{ }; use common::{config::smtp::resolver::Policy, ipc::PolicyType}; use mail_auth::{ - MX, + DnssecStatus, MX, common::parse::TxtRecordParser, mta_sts::{MtaSts, ReportUri, TlsRpt}, report::tlsrpt::ResultType, @@ -102,6 +102,7 @@ async fn mta_sts_verify() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index 4d384577..334fbbdb 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -12,7 +12,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServerBuilder}, }; use common::{config::smtp::queue::QueueName, ipc::QueueEvent}; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::NetworkListenerProtocol, @@ -224,6 +224,7 @@ async fn smtp_delivery() { .into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index 4e250117..c601135b 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -12,7 +12,7 @@ use crate::{ }, utils::{dns::DnsCache, server::TestServerBuilder}, }; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::MtaOutboundThrottleKey, @@ -239,6 +239,7 @@ async fn throttle_outbound() { exchanges: vec!["mx.test.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( @@ -269,6 +270,7 @@ async fn throttle_outbound() { exchanges: vec!["mx.test.net".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index da402c03..fdf08e6f 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -11,7 +11,7 @@ use crate::{ }, utils::{dns::DnsCache, server::TestServerBuilder}, }; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::MtaRequiredOrOptional, @@ -119,6 +119,7 @@ async fn starttls_optional() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(10), ); local.server.ipv4_add( diff --git a/tests/src/smtp/queue/concurrent.rs b/tests/src/smtp/queue/concurrent.rs index 98e2b7b9..35a8de08 100644 --- a/tests/src/smtp/queue/concurrent.rs +++ b/tests/src/smtp/queue/concurrent.rs @@ -9,7 +9,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServerBuilder}, }; use common::{BuildServer, ipc::QueueEvent}; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::NetworkListenerProtocol, @@ -125,6 +125,7 @@ async fn concurrent_queue() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(100), ); local.server.ipv4_add( @@ -149,6 +150,7 @@ async fn concurrent_queue() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(100), ); server.ipv4_add( diff --git a/tests/src/smtp/queue/virtualq.rs b/tests/src/smtp/queue/virtualq.rs index 4b06a7ba..4cf110aa 100644 --- a/tests/src/smtp/queue/virtualq.rs +++ b/tests/src/smtp/queue/virtualq.rs @@ -9,7 +9,7 @@ use crate::{ utils::{dns::DnsCache, server::TestServerBuilder}, }; use common::{BuildServer, config::smtp::queue::QueueName, ipc::QueueEvent}; -use mail_auth::MX; +use mail_auth::{DnssecStatus, MX}; use registry::{ schema::{ enums::NetworkListenerProtocol, @@ -185,6 +185,7 @@ async fn virtual_queue() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(100), ); local.server.ipv4_add( @@ -209,6 +210,7 @@ async fn virtual_queue() { exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), preference: 10, }], + DnssecStatus::Secure, Instant::now() + Duration::from_secs(100), ); server.ipv4_add( diff --git a/tests/src/utils/dns.rs b/tests/src/utils/dns.rs index 31757622..5e01f631 100644 --- a/tests/src/utils/dns.rs +++ b/tests/src/utils/dns.rs @@ -8,7 +8,7 @@ use common::{ Server, config::{mailstore::spamfilter::IpResolver, smtp::resolver::Tlsa}, }; -use mail_auth::{MX, Txt, common::resolver::ToFqdn}; +use mail_auth::{DnssecStatus, MX, RecordSet, Txt, common::resolver::ToFqdn}; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, @@ -20,9 +20,14 @@ pub trait DnsCache { fn ipv6_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant); fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant); fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant); - fn mx_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant); + fn mx_add( + &self, + name: impl ToFqdn, + value: Vec, + dnssec_status: DnssecStatus, + valid_until: std::time::Instant, + ); fn tlsa_add(&self, name: impl ToFqdn, value: Arc, valid_until: std::time::Instant); - fn dnssec_add(&self, name: impl ToFqdn, secure: bool, valid_until: std::time::Instant); } impl DnsCache for Server { @@ -34,10 +39,14 @@ impl DnsCache for Server { } fn ipv4_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { - self.inner - .cache - .dns_ipv4 - .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + self.inner.cache.dns_ipv4.insert_with_expiry( + name.to_fqdn(), + RecordSet { + rrset: Arc::from(value), + dnssec_status: DnssecStatus::Indeterminate, + }, + valid_until, + ); } fn dnsbl_add(&self, name: &str, value: Vec, valid_until: std::time::Instant) { @@ -56,25 +65,42 @@ impl DnsCache for Server { } fn ipv6_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { - self.inner - .cache - .dns_ipv6 - .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + self.inner.cache.dns_ipv6.insert_with_expiry( + name.to_fqdn(), + RecordSet { + rrset: Arc::from(value), + dnssec_status: DnssecStatus::Indeterminate, + }, + valid_until, + ); } fn ptr_add(&self, name: IpAddr, value: Vec, valid_until: std::time::Instant) { self.inner.cache.dns_ptr.insert_with_expiry( name, - Arc::from(value.into_iter().map(Into::into).collect::>()), + RecordSet { + rrset: Arc::from(value.into_iter().map(Into::into).collect::>()), + dnssec_status: DnssecStatus::Indeterminate, + }, valid_until, ); } - fn mx_add(&self, name: impl ToFqdn, value: Vec, valid_until: std::time::Instant) { - self.inner - .cache - .dns_mx - .insert_with_expiry(name.to_fqdn(), Arc::from(value), valid_until); + fn mx_add( + &self, + name: impl ToFqdn, + value: Vec, + dnssec_status: DnssecStatus, + valid_until: std::time::Instant, + ) { + self.inner.cache.dns_mx.insert_with_expiry( + name.to_fqdn(), + RecordSet { + rrset: Arc::from(value), + dnssec_status, + }, + valid_until, + ); } fn tlsa_add(&self, name: impl ToFqdn, value: Arc, valid_until: std::time::Instant) { @@ -83,11 +109,4 @@ impl DnsCache for Server { .dns_tlsa .insert_with_expiry(name.to_fqdn(), value, valid_until); } - - fn dnssec_add(&self, name: impl ToFqdn, secure: bool, valid_until: std::time::Instant) { - self.inner - .cache - .dns_dnssec - .insert_with_expiry(name.to_fqdn(), secure, valid_until); - } }