Gmail 报 550-5.7.25 缺 PTR:docker-mailserver 出站发信的源 IP、反向解析与 EHLO 怎么对齐
出站被退时,问题通常不在“邮件内容”,而在收件方看到的那条身份链:连接用的源 IP → 该 IP 的 PTR 与正向 A/AAAA → EHLO 里的 FQDN → 25 端口是否可达 → 是否走 relay。任何一环对不上,Gmail 这类只认硬指标的 MTA 就直接在 DATA 阶段拒绝。
项目地址:docker-mailserver
相关文档:Bind outbound SMTP to a specific network、Understanding the Ports、Debugging、Usage
现象与先取证
典型报错来自 issue #2991,配置是 hostname: mail、domainname: xxxx.com:
host gmail-smtp-in.l.google.com[108.177.15.27] said:
550-5.7.25 [144.76.109.34] The IP address sending this message does not
550-5.7.25 have a PTR record setup, or the corresponding forward DNS entry
550-5.7.25 does not point to the sending IP. As a policy, Gmail does not
550-5.7.25 accept messages from IPs with missing PTR records. ...
550 5.7.25 ... (in reply to end of DATA command)
先别急着改配置,把日志打开取全证据:环境变量 LOG_LEVEL=debug 或 trace,容器内日志在 /var/log/mail/*.log 与 /var/log/supervisor/*.log,用 docker logs -f 跟实时输出。同时确认所有已发布端口真的开放、没有被 ISP 或主机商在链路上封掉(Debugging 页的 Preliminary checks)。
确认收件方实际看到的源地址
报错里方括号中的 144.76.109.34 就是答案:那是收件方 TCP 连接的源地址,不是你以为的那块网卡地址。宿主有多块公网 IPv4/IPv6 时,Docker 默认走默认路由,出站 SNAT 到哪个地址不由 DMS 决定。
容器内查当前实际生效的绑定:
docker exec postconf -nf | grep smtp_bind_address
docker exec postconf -Mf # master.cf 各服务活动选项
postconf -nf 看的是 main.cf 生效值,postconf -Mf 看的是 master.cf 里各服务的实际选项——后面那个继承坑就藏在这里。
PTR 与正向确认:必须成对
Gmail 的要求写在报错原文里:PTR 必须存在,且该 IP 的正向 DNS 记录要指回同一个地址。issue 里用户已经给 xxxx.com 加了 PTR 仍全拒,维护者 casperklein 的答复是:PTR 应指向 mail.xxxx.com,并且刚加完 PTR 后 DNS 传播需要时间。
两条命令即可自证:
dig +short -x 192.0.2.10 @1.1.1.1
dig +short A mail.example.com @1.1.1.1
dig +short AAAA mail.example.com @1.1.1.1
+short -x 拿反解,A/AAAA 拿正向,两边必须闭合。Usage 页给的最佳情况就是:MX 指向 mail.example.com,A 记录把 mail.example.com 指到 192.0.2.3,PTR 把 192.0.2.3 反解回 mail.example.com。
几个常踩的边界:PTR 只能由 IP 归属方(VPS 商控制台或工单)设置,普通 DNS 面板写不了反向区;Cloudflare 橙云代理不是 SMTP 代理,mail 主机名必须保持 DNS-only;MX 记录应指向主机名而不是 IP。IPv6 是独立的发信身份,需要独立的 PTR 和对应 AAAA,不要因为 VPS 分到了地址就发 AAAA。
EHLO 身份:别和 DNS 打架
出站时 DMS 用 EHLO 报出自己的 FQDN,收件方会验证该名字解析到的地址是否就是连接源 IP。容器内确认当前值:
docker compose exec mailserver hostname --fqdn
docker compose exec mailserver postconf myhostname # 若 smtp_helo_name 未设,Postfix 通常用 myhostname
DMS 的 FQDN 默认取自容器 hostname,除非设置 OVERRIDE_HOSTNAME。hostname、OVERRIDE_HOSTNAME 与自定义 Postfix 覆盖三者互相矛盾时,EHLO 报出来的名字和 PTR 指向的名字就不是同一个,安全检查照样不过。
25 端口:出站那条路
端口表里 25 是 ESMTP 传输,默认启用;587(explicit TLS)与 465(implicit TLS)是 submission,也默认启用,但它们的用途是先认证到第三方 relay,再由 relay 用 25 端口替你投递。25 端口不要求认证,STARTTLS 失败时仍可明文收信。
问题在于出站 25 常被服务商封掉以防滥用,官方列出的常见受害宿主是 Azure、AWS EC2、Vultr。若端口无法解封,只能 relay 出站。另外 Docker 发布端口会绕过 UFW/firewalld 前端规则,别把“防火墙放行了”当成“端口通”。
外部确认 SMTP 可达与证书:
openssl s_client -starttls smtp -connect mail.example.com:25 -servername mail.example.com -crlf
绑定出口 IP 的两种网络
直接绑定:目标源 IP 已在 DMS 容器的网络命名空间内,比如 network_mode: host,或 macvlan 网络把该地址分给了容器(注意 macvlan 容器不会继承只属于宿主的地址)。在 config volume(docker-data/dms/config)里放 postfix-main.cf:
smtp_bind_address = 198.51.100.42
smtp_bind_address6 = 2001:DB8::42
桥接网络:出站走默认路由,需要在宿主上手工改路由,或用 Docker 网络的 host_ipv4 驱动选项强制桥接网络出站时的 SNAT 源地址;该 IP 必须属于某块网络接口。IPv6 走 host_ipv6,要求至少 Docker v25。compose.yaml:
networks:
default:
driver_opts:
com.docker.network.bridge.host_binding_ipv4: 198.51.100.42
com.docker.network.host_ipv4: 198.51.100.42
坑:smtp_bind_address 写 main.cf 会被 smtp-amavis 继承
把 smtp_bind_address 写进 main.cf,所有在 master.cf 中扩展 smtp transport 的服务都会继承它——包括 smtp-amavis,而它本来是显式配置为走 loopback(localhost/127.0.0.1)的。结果就是内部 Amavis 投递也被绑到公网地址上,链路的假设被破坏。
postfix-master.cf 覆盖写法:
smtp-amavis/unix/smtp_bind_address=127.0.0.1
smtp-amavis/unix/smtp_bind_address6=::1
更干净的做法是只在 smtp transport 上显式设置:
smtp/inet/smtp_bind_address = 198.51.100.42
smtp/inet/smtp_bind_address6 = 2001:DB8::42
这样避开了 smtp-amavis 的问题,但如果配置了 relay 出站,还需要对 relay transport 单独覆盖一次。
验证清单
docker compose exec mailserver postconf relayhost
docker exec postconf -nf | grep smtp_bind_address
docker exec postconf -Mf
dig +short -x @1.1.1.1
dig +short A mail.example.com @1.1.1.1
openssl s_client -starttls smtp -connect mail.example.com:25 -servername mail.example.com -crlf
再从外部用 MX Toolbox、mail-tester.com、multiRBL.valli.org、internet.nl 交叉看 PTR、SPF 与黑名单。SPF 侧的判断基于连接源 IP 是否被 envelope sender 域名的 SPF TXT 授权;如果策略用了 mx、a 这类 DNS 机制,对应记录必须与所选发信 IP 一致。Postfix 侧还有 reject_unknown_sender 之类的检查,具体行为需自行验证。
什么时候该换 relay
三种情况不要继续和 25 端口死磕:服务商明确不解封出站 25;宿主拿不到对应的 PTR 修改权限;多台机器共享出口、源 IP 无法稳定绑定。此时改用 587/465 认证到 relay 服务,由它用 25 端口投递,身份链的责任转移到 relay 侧——此时 relayhost 与 relay transport 的绑定覆盖要一起核对。
IPv6 相关部分在 2023 年仍属实验性,官方建议至少 Docker Engine v23,并需额外配置以保留客户端 IP;host_ipv6 则要求 Docker v25 以上。