How to connect to a local LMTP port

Your question

Hi, I want to redirect one domain to a local LMTP server using a port, but stalwart refuses with “connection to ‘127.0.0.1’ failed: host resolves loopback address”

how can I allow lmtp connection to localhost LMTP Port?

The source code reveals the issue. This is a hardcoded security check in the outbound SMTP/LMTP delivery engine - when the resolved address is a loopback address (127.0.0.1, ::1), those IPs are stripped and if nothing remains, a permanent failure is returned. This check is unconditional (wrapped in #[cfg(not(feature = "test_mode"))] only, meaning it applies in all production builds) and there is currently no configuration knob to bypass it.

The correct approach is to use an IP address literal directly in the relay route’s address field rather than a hostname like localhost or 127.0.0.1 as a DNS name. Looking at the code:

HostOrIp::Ip(ip) => vec![ip],

When the address in the MtaRoute is already an IP address (not a hostname), it skips the DNS resolution path entirely - and the loopback check only applies when IPs come from DNS resolution. So if you supply 127.0.0.1 as a literal IP in the route config it goes through HostOrIp::Ip, bypassing the loopback guard.

In your MtaRoute Relay object, make sure address is set as a bare IP literal, for example:

{
  "@type": "Relay",
  "name": "lmtp",
  "address": "127.0.0.1",
  "port": 2424,
  "protocol": "lmtp",
  "implicitTls": false,
  "allowInvalidCerts": true,
  "authSecret": {"@type": "None"}
}

The key is that 127.0.0.1 here is parsed as an IP address literal, not a hostname to be resolved via DNS - so the loopback rejection code is never reached. Using the hostname localhost triggers a DNS lookup which resolves to the loopback address and then hits the guard; using the IP literal sidesteps the lookup entirely.

See the routing docs for full LMTP delivery configuration context: Routing | Stalwart

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

Even with 127.0.0.1 it says “connection to ‘127.0.0.1’ failed: host resolves loopback address”

Let me look more carefully at the code to understand when the IP literal path is taken vs the DNS path.

The source code confirms the previous analysis was correct on the code path: when address is an IP literal, fqdn_hostname() returns HostOrIp::Ip(ip) which maps directly to vec![ip] in lookup.rs, bypassing DNS. However, the loopback check then runs on that resolved IP list regardless. Let me look more carefully at the full resolve_host flow:

Looking at lookup.rs again - the loopback check at lines 155-162 runs on remote_ips after they’re populated, regardless of whether they came from DNS or from an IP literal. So HostOrIp::Ip(ip) => vec![ip] still puts the loopback IP into the list, and then the check strips it out and returns the error.

This means the error occurs with both localhost and 127.0.0.1. The loopback guard is hardcoded for all production builds and there is currently no configuration option to bypass it.

The only workaround is to not use a loopback address at all. Some options:

  1. Use a non-loopback local IP: Bind your LMTP server to the machine’s actual LAN/private IP (e.g. 192.168.x.x or 10.x.x.x) and use that in the route’s address field instead of 127.0.0.1.

  2. Use a Unix socket or similar: Not currently supported by the Relay route type.

  3. If Stalwart is the final mail store: Use the Local route variant instead of relaying to an LMTP server. This is the intended pattern when Stalwart itself handles final delivery.

This is a hardcoded security measure in the outbound delivery engine with no current configuration knob to disable it. A community member or staff member may be able to confirm if there is a planned override.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

This is currently not supported, as a workaround you could create a local internal IP address so Stalwart accepts routing to localhost.