网站上IPv6前要过的五道关:AAAA、监听与防火墙

一、AAAA 记录带来的两个新问题

给站点加 IPv6,第一步通常是在 DNS 里加一条 AAAA 记录。这一步只花十秒钟,麻烦全在后面。AAAA 记录一旦发布,所有支持 IPv6 的客户端都会优先走 v6,包括爬虫、监控探针、CDN 回源探测,以及大量你根本没考虑过的客户端。如果服务端没准备好,这些访问就是直接失败,而且失败是静默的:IPv4 的监控一切正常,只有用户反馈「打不开」。

所以顺序必须是「先让服务端能接 v6,再发 AAAA」,而不是反过来。判断服务端是否就绪,不用猜,一条命令就够:

# 从外网(最好是另一台异地机器)测试
curl -6 -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/
curl -4 -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://example.com/

两个都返回 200,且耗时接近,才算真正双栈就绪。如果 v6 请求超时,说明链路上有环节没有监听 v6,可能是 Nginx、可能是防火墙、也可能是云厂商的安全组。

二、listen 指令:v6only 默认值是个陷阱

Linux 的 net.ipv6.bindv6only 在现代发行版上默认是 0,也就是「监听 :: 同时接受 v4 映射地址」。这导致很多人误以为机器已经在跑 IPv6,其实只是 v4 的连接通过 v6 socket 进来。

Nginx 的 listen 行为更需要注意。listen [::]:80 在未显式写 ipv6only 时的行为随版本而异,为了避免歧义,生产配置里一律显式写清楚:

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;
    server_name example.com;

    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server ipv6only=on;
    ssl_certificate     /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
}

ipv6only=on 意味着这个 listen 只接 v6,v4 由上面那行独立接管。这样的写法在排查时最清晰:两行配置对应两条独立链路,任何一边出问题都能立刻定位。反过来,如果只写 listen [::]:80 而不写 v4 那行,某些系统上会报 address already in use,另一些系统上则能启动但 v4 行为异常。

改完必须 nginx -t 再 reload,reload 之后用 ss 确认真实监听状态,而不是只看配置文件:

ss -lntp | grep -E ':(80|443)\b'

正常应该看到 *:80 或 [::]:80 这类双栈监听。如果只看到 0.0.0.0:80,说明 v4 那行生效、v6 没起来。

三、防火墙:v4 和 v6 是两套独立规则

这是 IPv6 部署里最高频的翻车点。iptables 只管 IPv4,ip6tables 管 IPv6,两者规则互不相干。你辛辛苦苦在 iptables 里把 22 端口限了来源,结果 ip6tables 是全开放 ACCEPT 的默认策略,等于给攻击者开了一扇完全没设防的门。

# 查看 v6 规则,如果输出只有默认策略没有具体规则,就是裸奔状态
ip6tables -L INPUT -n --line-numbers

# 查看默认策略
ip6tables -S INPUT | head -3

用 nftables 的系统同样要检查,因为很多发行版把 v4 和 v6 放在两张不同的表里:

nft list ruleset | grep -A5 'family ip6\|hook input'

判断原则很简单:v6 的放行规则必须比 v4 更严格,不能更宽松。因为 v6 的地址空间几乎无限,针对性的暴力破解不会再因为「IP 段就那么几个」而被简单地按段封禁。如果你的站点只需要 SSH 和 80/443,那么 v6 侧就只放行这三项,其余一律 reject,并且在 INPUT 链末尾用明确的 deny 收口,不要依赖默认策略。

云服务器的安全组是另一个坑。有些云厂商的安全组对 IPv6 是单独一个入口,另一些则要求先开通 IPv6 子网才出现。上线前一定在控制台确认:v6 的入站规则里 80 和 443 是放开的,出站也要检查——出站被限会导致 DNS 解析、外部 API 调用全部失败,症状是「页面能打开但很慢」。

四、日志、限流与统计全部失真

IPv6 地址长度是 39 个字符,日志格式里的 $remote_addr 会突然变长,于是那些用固定列宽或 awk 按字节位置切分的日志分析脚本全部错位。这个问题不报错,只是统计数据悄悄变错。上线前把所有解析日志的脚本过一遍,确认都是按分隔符切分而不是按位置。

更严重的是限流。原来的限流是按 $binary_remote_addr 做键,这个变量在 v4 下是 4 字节,在 v6 下是 16 字节,语法上兼容,但语义完全不同:一个 v6 用户通常拥有整段 /64,可以在这段里随意换地址。按完整地址限流,等于给攻击者无限量的免费额度。

# 按 /64 网段聚合再限流
geo $limit_key {
    default        $binary_remote_addr;
    2001:db8::/32  $binary_remote_addr;   # 谨慎:这只是示意
}

limit_req_zone $limit_key zone=api:10m rate=10r/s;

开源 Nginx 没有直接截断 v6 前缀的内置变量,比较实用的两种做法:一是用 map 正则把前 4 组(/64)提取出来作为限流键;二是在应用层做限流,使用「IP 前 64 位 + 用户 ID」作为键。第二种更可靠,也不依赖 Nginx 版本特性。

五、CDN 与回源:双栈最容易断的一环

开了 CDN 之后,DNS 的 AAAA 记录由 CDN 下发的为准,你源站的 AAAA 不再影响用户。但 CDN 回源时,如果回源地址配的是域名,它会同时解析 A 和 AAAA,然后可能优先走 v6 回源;这时源站 v6 没通,就表现为「部分节点 502、部分正常」这种非常难定位的形态。

排查方法:在 CDN 控制台或日志里看 502 的错误来源节点,把这些节点集中测试。如果确实是回源问题,最省事的处理是把回源地址写成明确的 IPv4 地址,或者关闭 CDN 侧的 v6 回源开关。这不是技术债,而是分层:CDN 到源站这一段完全受控,用 v4 没有任何劣势,而用户到 CDN 那一段已经由 CDN 负责双栈了。

# 在源站确认 v6 回源探测是否打得进来
journalctl -u nginx --since '10 min ago' | grep -c '2001:'
# 如果有大量 v6 请求但没有对应的业务量,可能是探测或扫描
ss -tn state established '( dport = :443 )' | awk '{print $5}' | grep ':' | head

顺带提醒:v6 地址在日志和监控面板里很难肉眼识别。建议在日志格式里加一个 $server_protocol 或者自建的 scheme 标记,方便后续按协议族拆分统计。

六、从双栈退回单栈:别慌,这样撤

如果上线后发现 v6 链路问题太多、一时又修不好,最有效的止血手段不是回滚代码,而是撤掉 AAAA 记录。DNS 层面撤掉之后,新解析的客户端会走 v4;但要注意 TTL 的缓存窗口,如果之前设的 TTL 是 3600 秒,那么最长一小时内仍会有用户走 v6。

撤 AAAA 是临时的,服务端配置不要跟着一起删。保留 Nginx 的 v6 listen 与防火墙规则,等链路修好之后只需重新加回 DNS 记录,不用再走一遍部署。这一点很关键:很多团队回滚时把配置也删了,第二次上线又要重来一遍,反而制造了第二次故障窗口。

撤掉之后要留一份检查清单,把这次踩到的点逐条记下来:防火墙表、CDN 回源、日志解析、限流键。下次再上 v6 时照着清单过一遍,通常半小时内就能确认干净。

七、容器与云主机里的三个额外坑

把应用放进 Docker 之后,IPv6 的行为和裸机差别很大,这一节把三个真实遇到过的坑单独列出来。

第一个坑是默认桥接网络根本没有 v6 地址。Docker 的 bridge 网络在未配置时只有 v4,容器里执行 ip -6 addr 是空的。这时候容器内的应用即使监听了 ::,也接不到外网的 v6 请求,因为宿主机虽然有 v6,但没有把它转发进来。要让容器具备 v6,必须在 /etc/docker/daemon.json 里开启 ipv6 并指定 fixed-cidr-v6,然后重启 Docker 服务。注意这个操作会重建 docker0 网桥,正在运行的容器会短暂断网,必须安排窗口。

{
  "ipv6": true,
  "fixed-cidr-v6": "fd00:dead:beef::/48",
  "experimental": true,
  "ip6tables": true
}

第二个坑是端口发布的默认行为。用 -p 80:80 发布端口时,Docker 默认只绑 v4,容器明明支持 v6 也接不进来。要双栈发布必须写成 -p [::]:80:80,或者更稳妥地写成两行:-p 0.0.0.0:80:80 加 -p [::]:80:80。这行小改动在 docker-compose.yml 里同样适用,ports 段里要写完整的两条,不要指望一条覆盖两种协议族。

第三个坑最隐蔽:宿主机关了 v6 转发。哪怕网络和端口都对,只要 net.ipv6.conf.all.forwarding 是 0,进来的 v6 包就出不去。检查一行就够:

sysctl net.ipv6.conf.all.forwarding net.ipv6.conf.default.forwarding
# 期望输出都是 1;是 0 时写进 /etc/sysctl.d/ 并 sysctl --system 生效

云主机还有一层:部分厂商的实例类型不支持 v6,或者只有部分可用区支持。上线前一定在控制台确认实例的 v6 地址已经分配且状态不是「待分配」,这一步没确认就去配防火墙,等于在给一个不存在的地址写规则,白费半小时。

八、小结

IPv6 不是「加一条 AAAA」这么简单,它牵动四层:Nginx 的 listen 语义、两套独立的防火墙、按地址做键的限流统计、以及 CDN 回源策略。上线顺序永远是先服务端就绪、再发记录;v6 的规则要比 v4 更严格;限流键必须按 /64 聚合;CDN 回源优先用明确的 v4 地址;出问题时先撤 AAAA 记录,保留服务端配置以便快速恢复。把这四点记住,双栈上线就能从「赌运气」变成「按步骤执行」。

Last modification:November 2nd, 2026 at 07:24 pm

Leave a Comment