From 237d90f164566196cd7cdebae8a701bca67c15ef Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:28:07 +0200 Subject: [PATCH] Fix MTA: Relay routes are rejected with `host resolves loopback address`, which prevents relaying through a local proxy or tunnel --- CHANGELOG.md | 1 + crates/smtp/src/outbound/lookup.rs | 4 +--- crates/smtp/src/outbound/mod.rs | 8 ++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7815088..3cd2d0263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - MTA: - A domain `catchAllAddress` pointing to a mailing list or a sub-addressed mailbox is accepted at `RCPT TO` and then rejected at local delivery with `550 5.5.0 Mailbox not found`. - `is_local_address()` and `is_local_domain()` expression functions do not match an address or domain spelled with uppercase characters. + - Relay routes are rejected with `host resolves loopback address`, which prevents relaying through a local proxy or tunnel. - MySQL, MariaDB & PostgreSQL: Range scans, range deletions and store purges run as a single unbounded statement, so on servers that enforce a statement timeout they abort on large accounts and tasks such as account deletion can never complete. Scans now resume from the last key read and deletions fall back to bounded chunks when the server aborts a statement. - Network: `local_port` and `local_ip` report the address Stalwart is bound to rather than the address the client connected to when the connection arrives through a trusted proxy. - Search index: diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index ffe22ab11..6f6deb930 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -101,7 +101,6 @@ impl DnsLookup for Server { } } - #[allow(unused_mut)] async fn resolve_host( &self, remote_host: &NextHop<'_>, @@ -147,8 +146,7 @@ impl DnsLookup for Server { }; if !remote_ips.is_empty() { - #[cfg(not(feature = "test_mode"))] - if remote_ips.iter().any(|ip| ip.is_loopback()) { + if !remote_host.allow_loopback() && remote_ips.iter().any(|ip| ip.is_loopback()) { remote_ips.retain(|ip| !ip.is_loopback()); if remote_ips.is_empty() { return Err(Status::PermanentFailure(ErrorDetails { diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index 2c61c0f7b..d5e5d9578 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -305,6 +305,14 @@ impl NextHop<'_> { } } + #[inline(always)] + fn allow_loopback(&self) -> bool { + match self { + NextHop::MX { .. } => cfg!(feature = "test_mode"), + NextHop::Relay(_) => true, + } + } + #[inline(always)] fn credentials(&self) -> Option<&Credentials> { match self {