AUTH Stage "local_port" condition doesn't match PROXY-protocol-forwarded port on multi-bind SMTP listener

Issue Description

Outbound mail gets rejected by strict recipients (Exim with verify = sender/callout) with a generic error, even though
SPF/DKIM/DMARC/PTR are all correctly configured:

  SMTP RCPT TO rejected ... code = 550, details = "Verification failed for <[email protected]>
  No Such User Here
  Sender verify failed"

This happens because the recipient’s MTA connects back to our own inbound port 25 to verify the sender address exists before
accepting the message — and that callback was itself getting rejected on our end (see root cause below), which the recipient’s Exim
reports back as a generic “Sender verify failed”.

Environment

  • stalwartlabs/stalwart:v0.16.17 (also reproduced on v0.16), Docker Compose

  • nginx in front of Stalwart as a stream proxy adding PROXY protocol, per the documented pattern
    (NGINX | Stalwart):

    stream {
    server {
    listen 25;
    proxy_pass stalwart:10025;
    proxy_protocol on;
    }
    }

  • smtp listener bound to both [::]:25 and [::]:10025 (public port + internal port nginx forwards to)

Expected Behavior

Port 25 (the public-facing inbound SMTP listener, forwarded internally to port 10025 via nginx’s PROXY protocol) should accept MAIL FROM/RCPT TO without requiring authentication, since inbound MX traffic from arbitrary external mail servers is never
authenticated. The AUTH Stage rule local_port != 25 was configured with this intent.

Actual Behavior

Because all real external traffic actually arrives with local_port = 10025 (not 25) due to the PROXY protocol forward, the
exclusion condition never matches, and Stalwart incorrectly demands authentication:

  503 5.5.1 You must authenticate first.
  503 5.5.1 MAIL is required first.

This causes external MTAs doing sender-verify callouts (e.g. Exim with verify = sender/callout) to fail their probe against us,
and they in turn reject our outbound mail with a generic 550 ... Sender verify failed / No Such User Here.

Reproduction Steps

  1. Deploy Stalwart behind an nginx stream proxy adding PROXY protocol, per the documented pattern
    (NGINX | Stalwart):

    stream {
    server {
    listen 25;
    proxy_pass stalwart:10025;
    proxy_protocol on;
    }
    }

  2. Bind the smtp listener to both [::]:25 and [::]:10025.

  3. Set Settings → MTA → Session → AUTH Stage → Require Authentication to local_port != 25.

  4. From any host, run:

    printf ‘EHLO test\r\nMAIL FROM:<>\r\nRCPT TO:[email protected]\r\nQUIT\r\n’ | nc mail.example.com 25

  5. Observe 503 5.5.1 You must authenticate first. instead of the expected 250 OK.

Relevant Log Output

SMTP RCPT TO rejected (delivery.rcpt-to-rejected) queueId = …, queueName = “remote”, from = “[email protected]”, to =
[“[email protected]”], hostname = “target-domain.com”, code = 550, details = “Verification failed for [email protected]
No Such User Here
Sender verify failed”, elapsed = 20040ms

— direct local test against our own port 25 (before fix) —
220 mail.example.com Stalwart ESMTP at your service
250-mail.example.com you had me at EHLO

250 8BITMIME
503 5.5.1 You must authenticate first.
503 5.5.1 MAIL is required first.
221 2.0.0 Bye.

— after changing the rule to local_port != 25 && local_port != 10025
250 2.1.0 OK
250 2.1.5 OK

Stalwart Version:
v0.16.17

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

I’m pretty sure vrfy is disabled by default

I worked around this by having stalwart listen to something like 127.255.255.1 on port 25, and having nginx listen to the public IPs

Fixed in v0.16.19 (yet to be released).