Akvicor
Akvicor
发布于 2026-07-23 / 0 阅读
0
0

Mailu 配置 Nginx 代理收发邮件

由于我的邮件服务器搭建在海外,无法正常接收国内的一些企业的邮件,因此使用这种方式在国内配置一个接收服务器,通过配置DNS的区域解析,不同区域解析到不同的服务器IP。

本教程中使用了WireGuard对涉及的服务器进行了组网,因此配置Nginx时使用的是组网IP,如果没有组网可以直接使用服务器的公网IP,但是要注意配置防火墙,防止被滥用。

本项目的示例服务器

如果你想使用公网就将下面的IP替换为你服务器的IP

  • 服务器A: 172.16.7.1,Mailu邮件服务器

  • 服务器B: 172.16.6.1,中转服务器

  • 服务器C: 172.16.5.3,Nginx代理收发服务器

目标

  • 两个地区使用相同的域名和端口号,通过智能解析按照区域解析到不同的IP,用户无需感知入口差异

  • 服务器A能识别经服务器C转发流量的真实来源 IP,保证 SPF、RBL、rspamd IP信誉、自定义 IP 黑白名单及审计日志全部基于真实 IP。

  • 服务器A原有的收发路径完全不变。

整体架构

                  C 地区用户 / 外部 MTA
                    智能解析 / MX → C 公网 IP
                    标准端口 25/465/587/993/995/143/110/4190
                              │
                ┌─────────────▼──────────────┐
                │ C: nginx stream 四层透传    │
                │ 连接开头附加 PROXY 协议头     │
                │ (携带真实客户端 IP)         │
                └─────────────┬──────────────┘
                              │ WireGuard 隧道
                              ▼
      A 专用监听(仅绑本机与内网接口,要求 PROXY 头,行为与标准端口一致)
        25   → smtp:125   (Postfix smtpd)
        110  → front:126  (dovecot pop3-login, STARTTLS)
        143  → front:127  (dovecot imap-login, STARTTLS)
        465  → front:128  (dovecot submission-login, implicit TLS)
        587  → front:129  (dovecot submission-login, STARTTLS)
        993  → front:130  (dovecot imap-login, implicit TLS)
        995  → front:131  (dovecot pop3-login, implicit TLS)
        4190 → front:132  (dovecot managesieve-login, STARTTLS)

  A 地区用户:智能解析 → A 公网 IP → 标准端口直连(原有路径,零改动)
  外部 MTA:    MX 兜底记录 → A front:25 直收(原有路径,零改动)

真实来源 IP 的传递机制是 PROXY 协议:C 的 nginx 在转发连接前先发送一个 PROXY 协议头(内含原始客户端 IP),A 侧专用监听解析该头并把其中的地址当作真实客户端地址,后续 rspamd 检查、认证限流、日志与邮件 Received 头均基于真实 IP。邮件数据为四层透传,TLS 始终在 A 端到端完成,证书与主机名不变。

专用监听与标准端口的关系:同一服务进程内并列的两组监听,配置、认证、后端转发完全一致,唯一区别是专用监听要求连接携带 PROXY 协议头。标准端口继续服务 A 地区直连流量(不带 PROXY 头),两组互不干扰。

端口映射一览

专用端口 125–132 连续编号,与公开端口按数值升序一一对应,方便记忆、映射和防火墙管理。这些低位端口未被 Mailu 与常见服务占用,容器内由 root 进程绑定;部署前在宿主机用 ss -lnt 确认空闲。

公开端口(A 直连 / C 监听)

A 内部专用监听

服务

25

smtp:125

Postfix(MX 收信)

110

front:126

POP3(STARTTLS)

143

front:127

IMAP(STARTTLS)

465

front:128

submission(implicit TLS)

587

front:129

submission(STARTTLS)

993

front:130

IMAP(implicit TLS)

995

front:131

POP3(implicit TLS)

4190

front:132

Sieve(STARTTLS)

A 配置

Mailu 数据目录为 /docker/mail/data,以下路径均基于此。

新建 Postfix 125 监听(MX 收信专用)

新建 /docker/mail/data/overrides/postfix/postfix.master,内容为单行(不可折行):

125/inet=125 inet n - n - - smtpd -o smtpd_upstream_proxy_protocol=haproxy

Mailu 启动时对该文件逐行执行 postconf -Me,其编辑语法为 服务/类型=完整master.cf行:等号左边 125/inet 是条目键,右边是注册进 master.cf 的完整服务行。该监听继承 main.cf 的全部限制(收件人校验、防开放中继、milter 链),仅额外要求连接携带 PROXY 协议头。端口号任选空闲端口即可(10025 已被 Mailu 内部提交服务占用),此处使用 125。overrides 仅在容器启动时加载,修改后需 docker compose restart smtp

smtpd_upstream_proxy_protocol=haproxy 中的 haproxy 是 Postfix 对 PROXY 协议的命名——该协议最早由 HAProxy 提出,此处表示接受 PROXY v1/v2 协议头。nginx 的 proxy_protocol on; 发送的正是这一协议,与实际使用的代理软件无关。

Postfix 启用 smtpd TLS

编辑 /docker/mail/data/overrides/postfix/postfix.cf,保留已有的 milter 行,追加三行 TLS 配置(注意第一行是我自定义的milter,与本项目无关):

smtpd_milters = inet:kmilter:1788 inet:antispam:11332
smtpd_tls_security_level = may
smtpd_tls_cert_file = /certs/cert.pem
smtpd_tls_key_file = /certs/key.pem

Mailu 的 TLS 默认由 front 容器终结,Postfix 自身不读证书。经 C 的MX 连接由 C 四层透传,STARTTLS 协商落在 Postfix 上,因此需要为Postfix 指定证书(与 front 使用同一套)。may 为机会性 TLS,front内部明文连接不受影响。

新建 front dovecot 专用监听(客户端端口专用)

front 容器内,465/587/993/995/110/143/4190 由 dovecot 代理进程监听,其配置模板末尾通过 !include_try /overrides/dovecot/proxy.conf 预留了官方扩展入口(front 的 /overrides 挂载自宿主机 data/overrides/nginx)。

新建 /docker/mail/data/overrides/nginx/dovecot/proxy.conf

# 代理中转专用监听:协议行为与标准端口一致,但要求连接携带 PROXY 协议头,
# 用于还原经入口节点(当前为 C,可扩展其他地区节点)转发流量的真实
# 客户端 IP。可信来源由主配置的 haproxy_trusted_networks(取自
# REAL_IP_FROM)限定。
# 专用端口 126-132 与公开端口按数值升序一一对应(110→126、143→127、
# 465→128、587→129、993→130、995→131、4190→132),另有 25→125 在
# Postfix 侧。这些端口在宿主机仅绑本机与内网接口,仅供入口节点经隧道访问。

service pop3-login {
  inet_listener pop3_proxy {
    port = 126
    haproxy = yes
  }
  inet_listener pop3s_proxy {
    port = 131
    ssl = yes
    haproxy = yes
  }
}

service imap-login {
  inet_listener imap_proxy {
    port = 127
    haproxy = yes
  }
  inet_listener imaps_proxy {
    port = 130
    ssl = yes
    haproxy = yes
  }
}

service submission-login {
  inet_listener submissions_proxy {
    port = 128
    ssl = yes
    haproxy = yes
  }
  inet_listener submission_proxy {
    port = 129
    haproxy = yes
  }
}

service managesieve-login {
  inet_listener sieve_proxy {
    port = 132
    haproxy = yes
  }
}

dovecot 会将同名 service 块与主配置合并,上述块只是在各服务中追加监听,不改变既有监听的任何行为。STARTTLS 类监听继承主配置的全局 ssl = required(认证前必须完成 TLS 升级);ssl = yes 的监听为 implicit TLS,与标准端口语义一致。

docker-compose 修改

由于我在front中使用的自己的证书文件,因此这里smtp直接相同的方式映射进去即可,如果你使用的其他方式则需要按照自己的情况调整。

smtp 服务:新增 125 端口映射与证书挂载(其余内容不变):

  smtp:
    image: ghcr.io/mailu/postfix:2024.06.55
    restart: always
    env_file: mail.env
    logging:
      driver: journald
      options:
        tag: mail-smtp
    ports:
      # 125 监听没有可信来源列表,绑定范围即伪造来源 IP 的信任边界,
      # 仅绑本机与内网接口,禁止绑公网
      - "127.0.0.1:125:125" # SMTP PROXY
      - "172.16.7.1:125:125"
      - "172.17.7.1:125:125"
    volumes:
      - "/docker/mail/data/mailqueue:/queue"
      - "/docker/mail/data/overrides/postfix:/overrides:ro"
      - "/docker/mail/data/custom/certs:/certs:ro"   # 与 front 同一套证书
    depends_on:
      - front
      - resolver
    dns:
      - 172.17.17.254

front 服务:在现有 ports 段追加专用监听映射(绑定接口与 smtp 的 125 保持一致,其余内容不变):

      # 代理中转专用监听(要求 PROXY 协议头),绑定范围即信任边界
      - "127.0.0.1:126:126" # POP3
      - "172.16.7.1:126:126"
      - "172.17.7.1:126:126"
      - "127.0.0.1:127:127" # IMAP4 (explicit TLS => STARTTLS)
      - "172.16.7.1:127:127"
      - "172.17.7.1:127:127"
      - "127.0.0.1:128:128" # ESMTP (implicit TLS)
      - "172.16.7.1:128:128"
      - "172.17.7.1:128:128"
      - "127.0.0.1:129:129" # ESMTP (explicit TLS => STARTTLS)
      - "172.16.7.1:129:129"
      - "172.17.7.1:129:129"
      - "127.0.0.1:130:130" # IMAP4 (implicit TLS)
      - "172.16.7.1:130:130"
      - "172.17.7.1:130:130"
      - "127.0.0.1:131:131" # POP3 (with TLS)
      - "172.16.7.1:131:131"
      - "172.17.7.1:131:131"
      - "127.0.0.1:132:132" # Sieve
      - "172.16.7.1:132:132"
      - "172.17.7.1:132:132"

mail.env 说明

本方案不需要 PROXY_PROTOCOL 设置(该设置的作用是把标准端口改为强制 PROXY 协议,与 A 地区直连不兼容;专用监听方案中标准端口保原样),否则标准 587/143/110 端口会要求 PROXY 头,A地区直连这三个端口的客户端将无法连接。

REAL_IP_FROM=172.16.0.0/12:同时作为专用监听 PROXY 头的信任边界(渲染为 dovecot haproxy_trusted_networks)。172.16.5.3 已在该范围内,功能上无需修改;建议收窄为实际代理地址(如 172.16.5.3/32 加 web 反代来源),缩小可伪造来源 IP 的主机范围。

REAL_IP_HEADER=X-Real-IP:仅作用于 HTTP 块,与邮件端口的 PROXY 协议无关,保持不变。

C 配置

C 的 nginx 需包含 stream 模块。以下配置按既有 stream 代理模板生成,放入 stream 配置目录(与 git-ssh 等现有端口代理同级):

  • upstream 多路径:v1-A~v2-A 对应到 A 的多条隧道(172.16~17 网段),含 TCP 健康检查与故障转移;如有 v3-A(172.18 路径)按同格式追加一行。

  • 相比模板的两处功能性增补:每个 server 块必须有 proxy_protocol on (传递真实客户端 IP,本方案核心);110/143/993/995/4190 增加 proxy_timeout 1h(IMAP IDLE 的空闲通知间隔为 29 分钟,超过 stream 默认 10 分钟超时会被切断)。

  • 邮件协议均为 TCP,PROXY 协议头也仅定义于 TCP,A 侧未发布 UDP,故不生成 UDP server 块。

  • 健康检查为 TCP 连接级探测(连上即断),A 侧 Postfix/dovecot 日志会出现"short protocol header"类记录,属预期噪音;Mailu 的 Postfix 启动脚本已内置过滤该模式。

upstream mail-smtp_stream {
  zone mail-smtp_stream 1M;
  server v1-A:125 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:125 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 25 so_keepalive=30:10:3;
  listen [::]:25 so_keepalive=30:10:3;
  proxy_pass mail-smtp_stream;
  proxy_protocol on;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00025-mail-smtp.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00025-mail-smtp.error.log;
}

upstream mail-pop3_stream {
  zone mail-pop3_stream 1M;
  server v1-A:126 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:126 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 110 so_keepalive=30:10:3;
  listen [::]:110 so_keepalive=30:10:3;
  proxy_pass mail-pop3_stream;
  proxy_protocol on;
  proxy_timeout 1h;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00110-mail-pop3.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00110-mail-pop3.error.log;
}

upstream mail-imap_stream {
  zone mail-imap_stream 1M;
  server v1-A:127 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:127 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 143 so_keepalive=30:10:3;
  listen [::]:143 so_keepalive=30:10:3;
  proxy_pass mail-imap_stream;
  proxy_protocol on;
  proxy_timeout 1h;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00143-mail-imap.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00143-mail-imap.error.log;
}

upstream mail-smtps_stream {
  zone mail-smtps_stream 1M;
  server v1-A:128 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:128 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 465 so_keepalive=30:10:3;
  listen [::]:465 so_keepalive=30:10:3;
  proxy_pass mail-smtps_stream;
  proxy_protocol on;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00465-mail-smtps.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00465-mail-smtps.error.log;
}

upstream mail-submission_stream {
  zone mail-submission_stream 1M;
  server v1-A:129 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:129 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 587 so_keepalive=30:10:3;
  listen [::]:587 so_keepalive=30:10:3;
  proxy_pass mail-submission_stream;
  proxy_protocol on;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00587-mail-submission.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00587-mail-submission.error.log;
}

upstream mail-imaps_stream {
  zone mail-imaps_stream 1M;
  server v1-A:130 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:130 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 993 so_keepalive=30:10:3;
  listen [::]:993 so_keepalive=30:10:3;
  proxy_pass mail-imaps_stream;
  proxy_protocol on;
  proxy_timeout 1h;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00993-mail-imaps.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00993-mail-imaps.error.log;
}

upstream mail-pop3s_stream {
  zone mail-pop3s_stream 1M;
  server v1-A:131 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:131 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 995 so_keepalive=30:10:3;
  listen [::]:995 so_keepalive=30:10:3;
  proxy_pass mail-pop3s_stream;
  proxy_protocol on;
  proxy_timeout 1h;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/00995-mail-pop3s.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/00995-mail-pop3s.error.log;
}

upstream mail-sieve_stream {
  zone mail-sieve_stream 1M;
  server v1-A:132 weight=1 max_fails=3 fail_timeout=30s backup;
  server v2-A:132 weight=1 max_fails=3 fail_timeout=30s;
  check interval=15000 rise=3 fall=2 timeout=2000 default_down=false type=tcp;
}

server {
  listen 4190 so_keepalive=30:10:3;
  listen [::]:4190 so_keepalive=30:10:3;
  proxy_pass mail-sieve_stream;
  proxy_protocol on;
  proxy_timeout 1h;
  include /etc/nginx/conf.d/stream-tcp-enabled/*.conf;
  access_log /nginx/logs/A/04190-mail-sieve.access.log main_tcp buffer=16k flush=1m;
  error_log  /nginx/logs/A/04190-mail-sieve.error.log;
}

DNS 配置

  • 客户端主机名mail.example.com 等做区域智能解析(华为云 DNS 智能线路:同一主机记录添加两条记录集,"默认"线路指向 A 公网 IP,"境外"或指定国家/地区线路指向 C 公网 IP)。两地区主机名与端口号完全一致,客户端配置无区域差异,证书为同一张。

  • SPF/DKIM/DMARC:发信仍从 A 发出,现有记录无需改动。

  • MX 记录:新增一条指向 C 的主机名(如 mx2.example.com → C 公网 IP),优先级数值小于 A 的现有 MX(如 10 对 20)。C 接收大部分正常邮件,A 直收作为兜底,两条路径均完整校验。MX 主机名同样可以按线路智能解析。

生效与验证

A 上重建容器(overrides 与新增端口映射在启动时加载):

cd /docker/mail && docker compose up -d

配置项验证:

# Postfix:125 已注册进 master.cf,TLS 已生效
docker compose exec smtp postconf -M | grep '^125 '
docker compose exec smtp postconf smtpd_tls_security_level smtpd_tls_cert_file

# 宿主机:专用监听均只绑 WireGuard IP
ss -lnt | grep -E ':(12[5-9]|13[0-2]) '

协议行为验证(在 A 上执行;若已收窄 REAL_IP_FROM,A 本机不在信任列表内,需改从 C 执行):

# 不带 PROXY 头连接专用监听:125 静默等待约 5 秒后断开(无 220 banner
# 即为协议强制生效);dovecot 侧明文端口(126/127/129/132)立即断开
nc 127.0.0.1 125

# 带 PROXY 头连接:应返回服务 banner(125 → 220 ESMTP,127 → * OK Dovecot)
(printf 'PROXY TCP4 203.0.113.9 172.16.7.1 51000 25\r\n'; sleep 8) | nc 127.0.0.1 125
printf 'PROXY TCP4 203.0.113.9 172.16.7.1 51000 143\r\n' | nc 127.0.0.1 127

# 日志确认会话来源为 PROXY 头中的地址(unknown 为无 PTR 反解,属正常)
docker compose logs --since 10m smtp | grep 'connect from'

# TLS 端口用 curl 验证(--haproxy-protocol 自动先发 PROXY 头再握手)
curl -v --insecure --haproxy-protocol 'imaps://127.0.0.1:130/' -u '用户@域名:密码'

端到端验证:

  1. 用 swaks 从外网经 C:25 发测试信,A 的 smtp 日志显示 connect from <真实IP>,rspamd 历史中 SPF/RBL 结果基于真实发件 IP。

  2. C 地区客户端经 465 或 587 提交邮件,smtp 日志与 Received 头显示真实客户端 IP;经 993 或 143 登录 IMAP,front 日志 rip= 显示真实 IP。

  3. A 地区用户照常收发,确认原有路径无变化。

安全注意事项

  • 专用监听的信任边界:任何能连到 125–132 端口的主机都可伪造 PROXY 头伪造来源 IP,且 Postfix 的 125 监听没有类似haproxy_trusted_networks 的可信来源列表,绑定范围即信任边界。这些端口只绑本机与必要的内网接口(C 走 172.16.7.1),禁止绑公网,并在 A 防火墙仅放行 172.16.5.3。

  • REAL_IP_FROM 的范围:该列表内的所有地址都可声明任意来源 IP(HTTP 的 X-Real-IP 与邮件端口的 PROXY 头共用此信任列表),按最小化原则配置。

  • C 机房端口:入方向 25 需开放(部分云厂商默认封锁;测试方法为在 C 监听 25 后从公网第三方机器 nc -vz,refused 表示端口畅通仅无监听,超时表示被过滤)。

  • 隧道中断:C 转发失败时外部发件 MTA 会自动重试,也可改走 A 的兜底 MX,邮件不会丢失;C 地区用户客户端会直连失败,此时 DNS 智能解析故障切换(调低 TTL)可作为应急手段。


评论