共计 9263 个字符,预计需要花费 24 分钟才能阅读完成。
本文记录在已有 Docker WordPress 架构(Nginx 反代 + WordPress + MariaDB + phpMyAdmin)基础上启用 IPv6 双栈监听的全过程:conf.d 中每个站点配置需要添加哪些指令、Docker 层需要做哪些配套改造、DNS 与防火墙如何放行,以及一路上会踩的坑。
变更地图:先看清要改哪里
| 文件 | 需要添加的内容 |
|---|---|
conf.d/default.conf |
3 处:HTTP 默认 server 加 listen [::]:80 default_server;,HTTPS 默认 server 加 listen [::]:443 ssl default_server; |
conf.d/abc.com.conf |
3 个 server 块各加 v6 listen;主站额外加 listen [::]:443 quic reuseport; |
conf.d/pma.abc.com.conf |
2 个 server 块各加 v6 listen(不需要 quic 行) |
nginx.conf |
无需必改(resolver ... ipv6=off 保留);可选加固见第五节 |
docker-compose.yml |
ports 增加 [::]: 绑定;web 网络启用 IPv6 |
| Docker daemon | daemon.json 启用 ipv6 + ip6tables |
| DNS / 防火墙 | AAAA 记录;ip6tables / 云安全组放行 v6 |
核心结论一句话: conf.d 里要做的全部事情,就是给每个 listen 指令补一条对应的 listen [::]:...,并在主站补一条 v6 的 QUIC 监听;其余工作都在 Docker、DNS、防火墙层。
一、原理:双栈监听与 ipv6only
1.1 为什么加了 [::] 还要保留原来的 listen
Nginx 的 IPv6 监听 socket 默认带 ipv6only=on(Nginx 主动设置),即:
listen 443 ssl; → 创建 IPv4 socket,绑定 0.0.0.0:443
listen [::]:443 ssl; → 创建 IPv6 socket,仅绑定 [::]:443(不收 IPv4)
两者是两个独立的 socket,互不替代。只写 [::] 会丢掉 IPv4 用户;只写 IPv4 则 IPv6 用户连不上。所以是”补一行”,不是”改一行”。
1.2 QUIC 的 v6 监听与 reuseport
回顾讨论四的结论:reuseport 对同一个 address:port 只能声明一次。而 0.0.0.0:443/udp 与 [::]:443/udp 是不同的 address,因此:
listen 443 quic reuseport; # IPv4 UDP socket,声明一次 ✅
listen [::]:443 quic reuseport; # IPv6 UDP socket,再声明一次 ✅(不冲突)
但和之前一样,这两行只能写在主站 abc.com 的 server 块里,子站 pma.abc.com 依然不写——v6 的 QUIC socket 同样按 SNI 路由到所有 server 块。
1.3 default_server 必须 v4/v6 各一个
这是最容易漏的一点:如果只给 IPv4 加了 default_server,那么未知 SNI 的 IPv6 请求会匹配到第一个 listen [::]:443 ssl 的 server 块(即你的主站),导致证书和站点内容通过非法域名泄露。v6 的兜底拒绝必须显式声明。
二、default.conf 完整配置(IPv6 版)
# ─── HTTP 默认 server:未知 Host 一律拒绝 ───
# 只允许 /nginx-health 和 ACME HTTP-01 challenge。
server {
listen 80 default_server;
listen [::]:80 default_server; # ★ IPv6 兜底
server_name _;
# Docker 健康检查
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
# 未知 Host 直接拒绝,避免开放跳转。
# IPv6 字面量 Host(如 [2408:4003::1])不匹配任何 server_name,
# 同样落入这里返回 403。
location / {
return 403;
}
}
# ─── HTTPS 默认 server:未知 SNI 直接拒绝握手 ───
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server; # ★ IPv6 兜底(关键!)
server_name _;
# Nginx >= 1.19.4
ssl_reject_handshake on;
}
# ─── 显式禁止通过公网 IP 访问 HTTP ───
server {
listen 80;
server_name 服务器公网IP; # 替换为你的 IPv4
location / {
return 403;
}
}
# 说明:IPv6 公网 IP 直连无需单独 server 块——
# Host 为 [2408:xxxx::1] 的请求不匹配任何 server_name,
# 自动落入上面的 default_server 返回 403;
# HTTPS 侧 SNI 为 IP 字面量时直接被 ssl_reject_handshake 拒绝。
三、abc.com.conf 完整配置(IPv6 版)
# ─── HTTP → HTTPS 跳转 ───
server {
listen 80;
listen [::]:80; # ★ IPv6
server_name abc.com www.abc.com;
location / {
return 301 https://abc.com$request_uri;
}
}
# ─── www → 主域跳转(HTTPS) ───
server {
listen 443 ssl;
listen [::]:443 ssl; # ★ IPv6
http2 on;
server_name www.abc.com;
ssl_certificate /etc/ssl/fullchain.cer;
ssl_certificate_key /etc/ssl/abc.com.key;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
return 301 https://abc.com$request_uri;
}
}
# ─── 主站:abc.com ───
server {
listen 443 ssl;
listen [::]:443 ssl; # ★ IPv6 TCP
# HTTP/3 / QUIC:v4 与 v6 各声明一次 reuseport(仅主站写)
listen 443 quic reuseport;
listen [::]:443 quic reuseport; # ★ IPv6 UDP
http2 on;
server_name abc.com;
ssl_certificate /etc/ssl/fullchain.cer;
ssl_certificate_key /etc/ssl/abc.com.key;
# OCSP Stapling 保持注释(原因同前文:证书无 OCSP URL,
# 且额外 resolver 会干扰 Docker 内置 DNS)
# ssl_stapling on;
# ssl_stapling_verify on;
# ssl_trusted_certificate /etc/ssl/fullchain.cer;
# 安全响应头
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# 上传大小需与 PHP upload.ini 一致
client_max_body_size 100m;
# 健康检查
location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
# 禁止 XML-RPC
location = /xmlrpc.php {
return 444;
}
# 禁止访问 WordPress 敏感文件
location ~* /(wp-config\.php|readme\.html|license\.txt|wp-config-sample\.php)$ {
return 404;
}
# 禁止访问隐藏文件和敏感后缀
location ~* /\.(env|git|svn|htaccess|htpasswd|ini|log|sql|bak|old|save|swp)$ {
return 444;
}
location ~ /\. {
return 404;
}
# 禁止 uploads 目录执行 PHP
location ~* /wp-content/uploads/.*\.php$ {
return 403;
}
# WordPress 登录限流
location = /wp-login.php {
limit_req zone=login burst=5 nodelay;
limit_conn addr 8;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
# admin-ajax 限流
location = /wp-admin/admin-ajax.php {
limit_req zone=general burst=50 nodelay;
limit_conn addr 30;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
# 主站请求
location / {
limit_req zone=general burst=100 nodelay;
limit_conn addr 100;
include /etc/nginx/snippets/proxy-wordpress.conf;
}
}
四、pma.abc.com.conf 完整配置(IPv6 版)
# ─── phpMyAdmin HTTP 跳转到 HTTPS ───
server {
listen 80;
listen [::]:80; # ★ IPv6
server_name pma.abc.com;
location / {
return 301 https://pma.abc.com$request_uri;
}
}
# ─── phpMyAdmin HTTPS ───
server {
# 同讨论四:QUIC/reuseport 只需主站声明一次,
# v6 的 QUIC socket 同样按 SNI 自动路由到本 server 块,此处不写。
listen 443 ssl;
listen [::]:443 ssl; # ★ IPv6
http2 on;
server_name pma.abc.com;
ssl_certificate /etc/ssl/fullchain.cer;
ssl_certificate_key /etc/ssl/abc.com.key;
# 管理后台安全头
add_header X-Robots-Tag "noindex, nofollow" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Strict-Transport-Security "max-age=63072000" always;
# 访问控制(按需启用)。启用 IPv6 后白名单要同时写 v4 和 v6:
# allow 127.0.0.1;
# allow ::1; # ★ IPv6 本机
# allow 203.0.113.10; # 你的固定 IPv4
# allow 2408:4003:10ce::abcd; # 你的固定 IPv6
# deny all;
# Basic Auth(与白名单叠加使用):
# auth_basic "DB Admin";
# auth_basic_user_file /etc/nginx/htpasswd/dbadmin;
client_max_body_size 100m;
location / {
# 不要对 phpMyAdmin 启用 limit_req(静态资源会被拦截导致空白页)
limit_conn addr 5;
proxy_pass http://phpmyadmin_backend;
proxy_http_version 1.1;
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 X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_redirect off;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location ~ /\. {
return 404;
}
}
五、nginx.conf 需要改吗?
没有必改项,但有三处与 IPv6 相关的点需要理解:
5.1 resolver ... ipv6=off 保留不动
resolver 127.0.0.11 valid=10s ipv6=off;
这条只影响”运行时变量解析”(本方案未使用)。即使未来启用,ipv6=off 也是对的:容器间通信走 Docker 网络的 IPv4 地址最稳定,强制 AAAA 反而可能因 v6 路由未就绪而失败。对外双栈、对内纯 v4 是容器环境的最佳实践。
5.2 限流 zone 对 IPv6 天然兼容,但有一个语义陷阱
limit_req_zone $binary_remote_addr 对 IPv6 存 16 字节,zone 容量足够,无需修改。但要注意:
家庭/企业 IPv6 用户通常持有整个 /64 前缀,攻击者可以无限轮换地址绕过”按 /128 限流”。
可选加固:用 map 把 v6 地址截断到 /64 作为限流 key:
# nginx.conf http 块内(可选)
map $remote_addr $limit_key {
default $binary_remote_addr;
# 提取 IPv6 前四组(/64 前缀)作为 key
"~^(?P<v6pfx>[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}:[0-9a-fA-F]{1,4}):" $v6pfx;
}
limit_req_zone $limit_key zone=general:20m rate=50r/s;
limit_req_zone $limit_key zone=login:10m rate=10r/m;
limit_conn_zone $limit_key zone=addr:20m;
IPv4 地址不匹配正则,走 default 分支,行为不变。
5.3 若未来套 CDN,real_ip 要补 v6 段
# set_real_ip_from 173.245.48.0/20;
# set_real_ip_from 2400:cb00::/32; # ★ Cloudflare IPv6 段示例
# set_real_ip_from 2606:4700::/32;
# set_real_ip_from 2803:f800::/32;
# real_ip_header CF-Connecting-IP;
六、让 IPv6 流量真正到达容器:Docker 层改造
只在 conf.d 加 listen 是不够的。 容器在独立网络命名空间里,IPv6 流量要经过 Docker 的发布与 NAT 链才能进来。
6.1 Docker daemon 启用 IPv6(Engine ≥ 26 推荐)
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<'EOF'
{
"ipv6": true,
"fixed-cidr-v6": "fd00:d0c4:0001::/48",
"ip6tables": true,
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
EOF
sudo systemctl restart docker
ip6tables: true是关键:它让发布的端口走 ip6tables DNAT(保留客户端真实源 IPv6)。若不启用,IPv6 发布端口可能回退到 userland proxy,Nginx 日志和限流里看到的将全是网关地址。
6.2 docker-compose.yml 变更
services:
nginx:
# ...其余不变...
ports:
- "80:80/tcp"
- "[::]:80:80/tcp" # ★ IPv6 TCP
- "443:443/tcp"
- "[::]:443:443/tcp" # ★ IPv6 TCP
- "443:443/udp"
- "[::]:443:443/udp" # ★ IPv6 UDP(QUIC)
networks:
web:
enable_ipv6: true # ★ 让 nginx/wordpress 容器获得 v6 地址
ipam:
config:
- subnet: 172.28.0.0/16 # 显式固定 v4 子网(可选)
- subnet: fd00:c11c:0001::/64 # ULA v6 子网(入站靠 DNAT,无需公网段)
admin:
backend:
internal: true
说明:
- 只有
web网络需要 v6(nginx 入站所在网络);backend保持 internal 纯 v4,数据库隔离策略不变。 - ULA(
fd00::/8)子网即可:入站流量在宿主机边界被 DNAT 到容器 ULA 地址,源地址保留为客户端公网 IPv6。
6.3 重载
cd /web
docker compose down
docker compose up -d
docker exec nginx nginx -t && docker exec nginx nginx -s reload
七、DNS 与防火墙
7.1 DNS:添加 AAAA 记录
abc.com. AAAA 2408:4003:10ce::1
www.abc.com. AAAA 2408:4003:10ce::1
pma.abc.com. AAAA 2408:4003:10ce::1
没有 AAAA 记录,客户端根本不会发起 IPv6 连接——Nginx 配得再对也无人访问。
7.2 防火墙放行 v6
# UFW 确认 /etc/default/ufw 里 IPV6=yes
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 443/udp
# 或裸 ip6tables
sudo ip6tables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
sudo ip6tables -A INPUT -p udp --dport 443 -j ACCEPT
# 云服务器:安全组里 IPv6 规则是独立的一套,务必单独添加!
八、验证
# 1. 容器内确认双栈监听
docker exec nginx ss -tln | grep ':443'
# 期望同时看到 *:443 和 [::]:443
docker exec nginx ss -uln | grep ':443'
# 期望同时看到 v4/v6 的 UDP 443
# 2. DNS
dig AAAA abc.com +short
# 3. 从 IPv6 网络测试(本机无 v6 时用在线工具)
curl -6 -I https://abc.com
curl -6 -I https://pma.abc.com
curl --http3 -6 -I https://abc.com
# 4. 确认日志中真实客户端 IPv6
tail -1 /web/nginx/log/access.log
# 期望 $remote_addr 为 2408:xxxx::xxxx 形式,而非网关地址
# 5. 兜底验证:v6 直连 IP 应被拒
curl -6 -I http://[2408:4003:10ce::1]/ # 期望 403
在线检测:https://test-ipv6.com、https://ipv6-test.com(填入你的域名)。
九、踩坑清单
| 坑 | 表现 | 解决 |
|---|---|---|
漏掉 v6 default_server |
非法域名的 v6 请求命中主站,证书/内容泄露 | default.conf 补 listen [::]:443 ssl default_server; |
子站误加 quic reuseport |
nginx: [emerg] duplicate "reuseport" |
v4/v6 的 reuseport 都只写在主站 |
| 同一族地址重复 reuseport | 同上 emerg | 每个 address:port:族 只声明一次 |
| Docker 未启用 IPv6 | Nginx 正常但 curl -6 超时 |
daemon.json + compose enable_ipv6 + [::]: ports |
未启用 ip6tables |
日志/限流里源 IP 全是网关地址 | "ip6tables": true 后重启 Docker |
| 云安全组只开了 v4 | 本机 v6 通、外部 v6 不通 | 安全组 IPv6 规则单独添加 |
| 只有 A 记录 | 客户端从不走 v6 | 补 AAAA |
| 容器禁用了 IPv6 | nginx: [emerg] socket() [::]:443 failed (97: Address family not supported) |
检查 sysctl net.ipv6.conf.all.disable_ipv6,或移除 v6 listen |
| phpMyAdmin 白名单漏 v6 | 启用白名单后 v6 管理端 403 | 补 allow ::1; 与你的 v6 地址 |
| 误以为要改后端 | 给 wordpress/upstream 加 v6 | 不需要:对外双栈、对内纯 v4 |
本文内容在完整版教程 Docker WordPress 建站指南:从零到生产级部署 基础上进行讨论