1Panel 部署 邮箱服务 Stalwart + Roundcube 时遇到 Docker 网络回环及 Stalwart 误封 Docker Gateway 导致 502 的问题总结

1Panel 部署 Stalwart + Roundcube 遇到的两个网络问题总结

一、环境

服务器系统:

Debian 13
Kernel: 6.12.x
Docker: 29.8.1
Firewall Backend: iptables

通过 1Panel 应用商店部署:

Stalwart Mail Server
image: stalwartlabs/stalwart:v0.16.22-alpine

Roundcube
image: roundcube/roundcubemail:1.7.4-apache

PostgreSQL

OpenResty

Docker 网络:

1panel-network
Driver: bridge
Subnet: 172.18.0.0/16
Gateway: 172.18.0.1

当时容器 IP:

Stalwart:
172.18.0.2

PostgreSQL:
172.18.0.3

Roundcube:
172.18.0.4

OpenResty 使用:

NetworkMode=host

邮件域名:

mail.xfirco.com

Webmail:

webmail.xfirco.com

二、问题 1:Roundcube 无法通过公网邮件域名连接 Stalwart IMAP

Roundcube 在 1Panel 中配置:

IMAP:
ssl://mail.xfirco.com
993

SMTP:
tls://mail.xfirco.com
587

Roundcube 登录时出现:

连接至 IMAP 服务器失败

但从宿主机测试:

openssl s_client \
  -connect mail.xfirco.com:993 \
  -servername mail.xfirco.com

完全正常:

Verification: OK

* OK [CAPABILITY ...] Stalwart IMAP4rev2 at your service.

说明:

DNS 正常
993 正常
TLS 正常
证书正常
Stalwart IMAP 正常

但在 Roundcube 容器内:

openssl s_client \
  -connect mail.xfirco.com:993 \
  -servername mail.xfirco.com

返回:

SSL routines::unexpected eof while reading

PHP:

stream_socket_client("ssl://mail.xfirco.com:993")

也返回:

Failed to enable crypto
Unable to connect

原因定位

Roundcube:

172.18.0.4

Stalwart:

172.18.0.2

都处于:

br-8b523a9d6c9c

Docker 自动生成的 993 DNAT 规则:

-A DOCKER
! -i br-8b523a9d6c9c
-p tcp
--dport 993
-j DNAT
--to-destination 172.18.0.2:993

关键是:

! -i br-8b523a9d6c9c

也就是说:

从同一个 1panel-network bridge 进入的数据包,不使用宿主机 published port 的这条 DNAT 规则。

因此:

Roundcube
→ mail.xfirco.com
→ 公网 IP
→ 宿主机 993
→ Stalwart

这条路径不正常。

但直接走 Docker 内网:

openssl s_client \
  -connect stalwart-mail:993 \
  -servername mail.xfirco.com

完全正常。


当前解决方案

Roundcube 不再通过公网 IP 回环访问 Stalwart,而是:

Roundcube
→ Docker DNS
→ stalwart-mail
→ Stalwart

同时为了保持 TLS 证书验证正常:

实际 TCP 主机:
stalwart-mail

TLS SNI / peer_name:
mail.xfirco.com

Roundcube 自定义配置:

$config['imap_host'] = 'ssl://stalwart-mail:993';

$config['imap_conn_options'] = [
    'ssl' => [
        'verify_peer'      => true,
        'verify_peer_name' => true,
        'peer_name'        => 'mail.xfirco.com',
        'SNI_enabled'      => true,
    ],
];

$config['smtp_host'] = 'tls://stalwart-mail:587';

$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';

$config['smtp_conn_options'] = [
    'ssl' => [
        'verify_peer'      => true,
        'verify_peer_name' => true,
        'peer_name'        => 'mail.xfirco.com',
        'SNI_enabled'      => true,
    ],
];

实测:

Roundcube → Stalwart IMAP
正常

Roundcube → Stalwart SMTP
正常

三、问题 2:Stalwart Web 管理页面突然出现 502

访问:

https://mail.xfirco.com

出现:

502 Bad Gateway
openresty

1Panel 反向代理:

https://mail.xfirco.com
→ OpenResty
→ http://127.0.0.1:18080
→ Stalwart 8080

Stalwart Docker 映射:

127.0.0.1:18080 → 8080

现象

执行:

curl -v http://127.0.0.1:18080/

TCP 能连接:

Connected to 127.0.0.1 port 18080

但随后:

Empty reply from server

OpenResty 日志:

upstream prematurely closed connection
while reading response header from upstream

不是:

connection refused

所以不是 OpenResty 找不到端口,而是 Stalwart 主动关闭连接。


四、发现 Stalwart 把 Docker 网关封禁了

Stalwart 日志:

Blocked IP address
listenerId = "http"
localPort = 8080
remoteIp = 172.18.0.1

查询 BlockedIp

IP Address:
172.18.0.1

Reason:
Excessive port scanning attempts

Expiration:
<none>

也就是:

172.18.0.1

被永久封禁。

而这个地址正好是:

1panel-network Gateway
172.18.0.1

为什么发生

公网有大量扫描:

/.env
/wp-config.php
/phpinfo.php
/.git/config
/database.sql
/server.key
...

OpenResty 将请求反代给 Stalwart。

Stalwart 当时看到所有 HTTP 请求来源基本都是:

172.18.0.1

于是扫描器触发安全规则后,Stalwart 把:

172.18.0.1

当成攻击者封掉。

结果:

所有 OpenResty → Stalwart Web 请求
全部被关闭

→ 502

五、解除封禁

使用 Stalwart CLI Docker:

docker run --rm \
  --network container:1Panel-stalwart-mail-HzwT \
  -e STALWART_URL=http://127.0.0.1:8080 \
  -e STALWART_USER=管理员 \
  -e STALWART_PASSWORD \
  ghcr.io/stalwartlabs/cli:latest \
  query BlockedIp \
  --where address=172.18.0.1

得到:

jfbzyasgabae
172.18.0.1
Excessive port scanning attempts

删除:

delete BlockedIp --ids jfbzyasgabae

然后加入:

AllowedIp:
172.18.0.1

避免 OpenResty / Docker 网关再次被整体封禁。


六、1Panel 实际已经正确传递真实 IP Header

检查 1Panel 自动生成的:

/www/sites/mail.xfirco.com/proxy/root.conf

内容:

location ^~ / {
    proxy_pass http://127.0.0.1:18080;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_set_header REMOTE-HOST $remote_addr;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;

    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;

    proxy_http_version 1.1;

    add_header X-Cache $upstream_cache_status;

    proxy_ssl_server_name off;
    proxy_ssl_name $proxy_host;
}

因此 1Panel/OpenResty 本身已经发送:

X-Real-IP
X-Forwarded-For
X-Forwarded-Proto

七、Stalwart 开启 X-Forwarded-For

配置:

Http.useXForwarded = true

CLI:

update Http --field useXForwarded=true

确认:

Proxy
Obtain remote IP from Forwarded header: Yes

因此最终采用:

OpenResty
→ X-Forwarded-For
→ Stalwart

作为真实 IP 传递方式。


八、中途踩到的另一个坑:proxyTrustedNetworks

曾尝试设置:

proxyTrustedNetworks:
172.18.0.1

设置后:

curl http://127.0.0.1:18080/

再次出现:

Empty reply from server

OpenResty:

upstream prematurely closed connection

清空:

proxyTrustedNetworks = <none>

重启 Stalwart 后:

curl -v http://127.0.0.1:18080/

立即恢复:

HTTP/1.1 302 Found
location: /account

所以当前架构下:

useXForwarded = true

适用于 HTTP X-Forwarded-For

而:

proxyTrustedNetworks

会影响 PROXY Protocol 行为,不能和当前普通 HTTP 反代方式混用。


九、最终稳定配置

现在使用:

1Panel / OpenResty
    ↓
HTTP Reverse Proxy
    ↓
127.0.0.1:18080
    ↓
Stalwart :8080

OpenResty:

X-Real-IP
X-Forwarded-For
X-Forwarded-Proto

Stalwart:

Http.useXForwarded = true

proxyTrustedNetworks = <none>

AllowedIp:
172.18.0.1

目前:

Stalwart Web 正常
Roundcube 正常
IMAP 正常
SMTP 465 / 587 正常

十、希望 1Panel 官方考虑改进的地方

1. Stalwart 应用模板和反向代理联动

目前用户通过 1Panel 应用商店安装 Stalwart 后,很容易采用:

127.0.0.1:18080 → Stalwart 8080

再通过 1Panel OpenResty 反代。

建议在 Stalwart 应用说明中明确提示:

如果通过 1Panel OpenResty 反向代理 Stalwart:

应开启 Stalwart Http.useXForwarded

并确认 X-Forwarded-For 正确传递。

否则存在:

公网扫描器
→ OpenResty
→ Stalwart
→ Docker Gateway 被误判攻击源
→ 172.18.0.1 被封
→ 整个 WebUI 502

的问题。


2. 建议防止代理网关被永久 Auto-Ban

如果 1Panel 能识别应用是反代服务,可以考虑:

OpenResty / Docker Gateway

加入可信/允许地址,或者至少在应用安装说明中明确提醒。

尤其当前 Stalwart 对扫描封禁:

Expiration: <none>

意味着一旦误封代理地址,WebUI 会永久不可访问,必须 SSH 解封。


3. Roundcube + Mail Server 同网络场景

Roundcube 与 Stalwart 都挂在:

1panel-network

时,如果 Roundcube 配置:

ssl://mail.example.com:993

而这个域名又解析到宿主机公网 IP,可能遇到 Docker bridge → published port → 同 bridge 容器的 NAT 回环问题。

建议 1Panel 的邮件套件文档中明确推荐:

Roundcube
→ Docker service name
→ Stalwart

而不是:

Roundcube
→ 公网域名
→ 公网IP
→ 宿主机 published port
→ Stalwart

同时考虑 TLS SNI/peer_name 的处理。


十一、最小复现

Roundcube 问题

同一 Docker bridge:

Stalwart:
172.18.0.2

Roundcube:
172.18.0.4

Roundcube:

mail.example.com → 宿主机公网IP

执行:

docker exec roundcube \
openssl s_client \
-connect mail.example.com:993 \
-servername mail.example.com

失败。

执行:

docker exec roundcube \
openssl s_client \
-connect stalwart-mail:993 \
-servername mail.example.com

成功。


Stalwart 502 问题

OpenResty:

host network

Stalwart:

127.0.0.1:18080 → container:8080

Stalwart 将:

172.18.0.1

加入 BlockedIp 后:

curl http://127.0.0.1:18080/

表现为:

TCP connected
Empty reply from server

OpenResty:

upstream prematurely closed connection

最终:

502 Bad Gateway

删除 BlockedIp 后恢复。