IPv6 喊了这么多年,很多站长的第一反应还是"我用不上"。但只要你的网站接了 Cloudflare、用了 CDN、或者用户里有相当一部分是移动网络,你其实早就在跟 IPv6 打交道了——只是它跑在 CDN 那一层,你没感知到。而一旦你想把源站也切到双栈(IPv4 + IPv6 同时提供),或者想给后端服务之间用 IPv6 直连省掉 NAT,问题就来了:为什么有的客户端能打开有的打不开?为什么加了 AAAA 记录反而变慢?这篇文章就是讲清楚 IPv6 双栈从服务器配置到 DNS、再到 Nginx 和防火墙的完整落地流程,以及那些"看起来配好了、实际是半通"的陷阱。
一、先理解为什么 IPv6 不只是"更长的地址"
很多人把 IPv6 当作"IP 不够用了所以地址变长",这只是表象。真正影响运维方式的是这几个结构差异:
- 地址是 128 位、写成 8 组十六进制,比如
2001:db8::1。连续为 0 的段可以用::缩写一次。 - 没有广播,只有组播和任播。ARP 的角色由 NDP(邻居发现)接管。
- 子网建议是 /64。也就是哪怕你只有几台机器,也通常分到一个 /64——这包含 2 的 64 次方个地址,大得离谱。这不是浪费,而是为了让"无状态地址自动配置(SLAAC)"能工作。
- 默认支持无状态自动配置(SLAAC):客户端根据路由器通告(RA)里的前缀,自己拼一个地址,不需要 DHCP 服务器。这也是为什么 IPv6 环境下你可能看到"服务器明明没配地址却能上网"的诡异现象。
- NAT 不再是必需品:IPv6 的设计哲学是端到端可达,因此防火墙的角色比 NAT 更重要——没有 NAT 这层"天然隔离",你的内网服务如果防火墙配错,是真会直接暴露到公网的。这一点极其关键,后面会专门讲。
二、云服务器上拿到 IPv6 并配上地址
多数主流云厂商(含 Vultr、Hetzner 等)在控制台里可以为实例分配一个 IPv6 地址或用一段 /64、/56 前缀。分配完之后,登录服务器确认接口是否已经拿到地址:
# 看接口的地址,inet = IPv4,inet6 = IPv6
ip addr show
# 只看 IPv6
ip -6 addr show
# 看 IPv6 路由(默认路由是 ::/0 才对)
ip -6 route show如果 ip -6 addr 里只有 ::1 那个回环和 fe80:: 开头的链路本地地址(link-local),说明公网 IPv6 还没配好。常见有两种配法:
方式 A:云控制台分配的单地址(静态)
直接把云厂商给的地址加到接口上。用 netplan(Ubuntu 18.04+ 的标准)或 networkd 写成持久配置,重启不丢:
# 临时加(重启失效,仅用于测试)
ip -6 addr add 2001:db8:1234::100/64 dev eth0
ip -6 route add default via 2001:db8:1234::1 dev eth0
# 验证能出网(ping 一个公共 IPv6)
ping6 -c 3 2606:4700:4700::1111 # Cloudflare 的公共 DNS,走 IPv6测试通了之后,一定要落到持久配置里。/etc/network/interfaces(Debian 传统写法)或 netplan 的 YAML 里,把 address 和 gateway6 补齐。忘记持久化是新手最常见的"重启后 IPv6 又没了"的原因。
方式 B:SLAAC 自动获取(有一整段前缀时)
如果云厂商给的是 /64 前缀并且上游有 RA,接口一般会自动拿到地址。检查 sysctl net.ipv6.conf.eth0.accept_ra 是否为 1。服务器上通常建议关闭 SLAAC、用静态地址,避免地址变成 EUI-64 那种难记的形式,也避免前缀变化导致地址漂移。
三、DNS:AAAA 记录到底该不该加
这是双栈最关键、也最常翻车的一环。规则很简单但后果很重:
只有当你确认服务器和全链路(防火墙、Nginx、后端)都真正支持 IPv6 时,才加 AAAA 记录。 一旦加了 AAAA 而服务器其实没通,支持 IPv6 的客户端会优先走 IPv6、然后连不上——而这类客户端比例不低(大量移动网络、部分家宽),结果就是"一部分人永远打不开你的站"。
加 AAAA 之前,用外部工具验证你的 IPv6 通畅性:ping6 从外部打、或者用在线 IPv6 测试工具。确认无误后再加。加 AAAA 的正确姿势:
# 用 dig 查自己的 AAAA 是否生效、TTL 是多少
dig AAAA www.example.com +short
dig A www.example.com +short # A 和 AAAA 应并存
# 从外部验证 IPv6 可达(在别的、有 IPv6 的机器上执行)
curl -6 -s -o /dev/null -w "%{http_code}" https://www.example.com/; echocurl -6 强制走 IPv6,返回 200 才说明 IPv6 真的能提供 HTTPS 服务。
双栈优先级:Happy Eyeballs 与 DNS 顺序
支持双栈的客户端(现代浏览器、curl)用 Happy Eyeballs(RFC 8305) 算法处理 A 和 AAAA:它同时尝试 IPv4 和 IPv6,谁先回应用谁,避免纯 IPv6 挂了拖累整体。所以理论上"IPv6 打不通但 IPv4 通"应该能被兜住。但现实里,如果IPv6 的丢包是部分丢而不是全断(比如防火墙放行了 TCP 443 但没放行 ICMPv6,导致路径 MTU 发现失败),Happy Eyeballs 救不了你——连接会建上但传大数据时卡死。这正是"有时能开有时打不开"的经典成因,见下面第七节。
四、Nginx 监听 IPv6
Nginx 默认的 listen 80; 只监听 IPv4。要同时服务两栈,最简洁的写法是:
server {
# IPv4 和 IPv6 都监听
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
...
}注意 IPv6 地址在 listen 里要用方括号包起来,[::] 表示所有 IPv6 地址(相当于 IPv4 的 0.0.0.0)。改完 nginx -t 验证语法。
还有一个容易忽略的点:日志里的 $remote_addr 会变成 IPv6 地址,你原来的日志分析脚本、fail2ban 规则、按 IP 限流的 map 表,可能只认 IPv4 格式,需要同步更新。access log 里混着 IPv4 和 IPv6 是正常的,别以为是攻击。
五、防火墙:IPv6 和 IPv4 是两套独立规则
这是双栈最大的坑,没有之一。iptables 只管 IPv4,ip6tables 只管 IPv6,它们是两张完全独立的规则表。你在 iptables 里精心写好的"默认 DROP、只放行 22/80/443",对 IPv6 流量毫无影响——如果 ip6tables 是空的默认 ACCEPT,那么所有 IPv6 端口都是敞开的。
# 看 IPv4 和 IPv6 规则分别是啥
iptables -L -n
ip6tables -L -n
# 用 nftables 的话,一张表可以同时管两栈(更推荐)
nft list ruleset用 nftables 的话有个好处:可以定义 inet 家族的表,一条规则同时作用于 IPv4 和 IPv6,避免两套规则不同步。示例:
# inet 家族 = 同时管 IPv4 和 IPv6
nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0 ; policy drop ; }'
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input iif lo accept
nft add rule inet filter input tcp dport '{ 22, 80, 443 }' accept
# 重要:ICMPv6 必须放行,否则路径 MTU 发现会失败
nft add rule inet filter input ip6 nexthdr icmpv6 accept
nft add rule inet filter input icmp type '{ echo-request, echo-reply }' acceptICMPv6:IPv6 世界里不能关的东西
IPv4 时代"反射 ping"是常识,很多人习惯性地把 ICMP 全禁掉。但IPv6 依赖 ICMPv6 工作:NDP(邻居发现)靠它、路径 MTU 发现靠它,如果全禁了,后果是"能连上但大包传不动"。正确的做法是:放行 ICMPv6 的关键类型(尤其是 1、2、3、4 这几类差错报文和管理报文),只对 ping(echo)按需限制。一刀切封 ICMPv6 是 IPv6 故障的头号人为原因。
六、后端服务之间用 IPv6:省掉 NAT 的收益与代价
对多机架构,IPv6 的一个实际好处是:后端服务之间可以用全球可路由的 IPv6 地址直连,不需要 NAT、不需要端口映射。比如 Web 服务器直连数据库:
# 应用里连接数据库,用 IPv6 地址要加方括号
# postgresql://user:pass@[2001:db8:1234::20]:5432/db
# 命令行测试
nc -6 -vz 2001:db8:1234::20 5432
ss -6 -tlnp | grep 5432 # 看数据库是不是监听在 IPv6(:: 表示所有地址)代价是:没有 NAT 挡着,数据库的 IPv6 地址如果被扫到,防火墙再没配好,就直接裸奔了。所以用 IPv6 内网互联时,必须靠防火墙(或云安全组的 IPv6 规则)严格限制来源。别忘了——云安全组通常也有独立的 IPv6 规则列表,很多人只加了 IPv4 的入站规则,IPv6 这边一片空白。
七、真实故障复盘:为什么"小请求能开,刷新几次就卡死"
一个非常典型的案例:站长给源站加了 AAAA 记录,测 curl -6 首页返回 200,以为成功了。上线后收到投诉:"网站有时候转圈半天打不开。" 他自己刷新又正常。
症状:首页能出,但大图片、大 JS 文件加载到一半卡住;重试有时成功有时失败。
排查动作:
# 1. 先确认 IPv6 路径是否真的通、有没有 MTU 问题
# 用固定小包和大小包分别 ping,看大包是否丢
ping6 -c 3 -s 1400 2001:db8:1234::100 # 接近 MTU 的包
ping6 -c 3 -s 100 2001:db8:1234::100 # 小包
# 2. 查防火墙有没有放行 ICMPv6
nft list ruleset | grep -i icmpv6
ip6tables -L -n | grep -i icmp
# 3. 用 traceroute6 看在哪一跳断开
traceroute6 www.example.com根因:他的防火墙只放行了 IPv6 的 TCP 80/443,把 ICMPv6 全禁了。结果 IPv6 的路径 MTU 发现(PMTUD)失败——服务器发大包时收不到"包太大"的 ICMPv6 差错报文回执,无法把包拆小,于是大包在路径上被静默丢弃。小请求(首页 HTML)能过,大文件就卡死。
前后对比:放行 ICMPv6(特别是 type 2 "Packet Too Big")之后,大文件立刻正常。整个过程里改的只是防火墙一行规则,但如果没有认真看 ICMPv6 这一环,会一直误以为"是 CDN 或网络商的锅"。
方法论提炼:IPv6 故障"部分通"的时候,先怀疑 ICMPv6 和 MTU,而不是应用本身。放行 ICMPv6 是 IPv6 运维的第一原则。
八、上线检查清单
- 地址:
ip -6 addr有公网地址,ip -6 route有::/0默认路由,且已持久化。 - 出网:
ping6或curl -6能到外部。 - 服务:Nginx 有
listen [::]:443,ss -6 -tlnp能看到监听。 - 防火墙:
ip6tables -L -n/ nftables 规则确实放行了 80/443,并且放行了必要的 ICMPv6;默认策略不是 ACCEPT。 - DNS:只在服务真的通之后才加 AAAA;加了立刻从外部 IPv6 环境验证
curl -6返回 200。 - 监控:给 IPv6 单独做一次可达性拨测,别只监控 IPv4。
IPv6 不是"要不要做"的问题,而是"什么时候必须做"的问题。移动网络和大厂服务商的 IPv6 占比年年上升,早一天把双栈配好、把 ICMPv6 和防火墙这两个坑趟平,就早一天不用在半夜被"部分用户打不开"的用户投诉叫醒。它的配置量其实不大,难的是那些反直觉的地方——尤其是"IPv6 的防火墙是另一套、ICMPv6 不能封"这两条,记住它们,就绕开了 90% 的坑。