凌晨1点40分,IPv6-only网络全站超时
报警群弹消息:新上线的服务器在 IPv6-only 网络里连不上。我用手机切到 APN 的 v6 only 模式试了一下,DNS 解析正常,AAAA 记录也有,但 curl 一直卡在连接阶段,最终超时。
ssh 跳板机登上去,ip -6 addr 显示地址在,ping6 网关 不通。tcpdump -i ens18 'icmp6 && ip6[40] == 135',发现邻居请求(NS)一直在发,但没有回应。这说明链路没问题,是邻居发现(NDP)失败。
最后定位到 /etc/sysctl.conf 里的 net.ipv6.conf.all.forwarding=0,而这台机器开了桥接给容器,内核收到 NS 直接丢弃。把 forwarding 改为 1 后,还是不通。再看文档:当转发开启后,内核会忽略 RA(路由通告),必须显式把 accept_ra 设为 2。改完,ping 通了。
这个故障让我把 IPv6 双栈配置重新过了一遍。下面是我在生产环境用的配置,基于 Ubuntu 22.04.3 LTS(Kernel 5.15.0-92-generic)和 AlmaLinux 9.3(Kernel 5.14.0-362)。
双栈的前置:内核参数
IPv6 双栈并不仅是给网卡加一个地址。内核里有一组 sysctl 参数控制整体行为。我整理了一份经过测试的配置:
# /etc/sysctl.d/99-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
net.ipv6.conf.all.accept_ra = 2
net.ipv6.conf.default.accept_ra = 2
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.ens18.accept_ra = 2
参数含义:
disable_ipv6:强制开启 IPv6。很多云镜像默认是 1,先把这行写好。forwarding:这台机器要跑容器/K8s 或者做路由器,必须开。注意accept_ra设置成 2,否则开了转发后系统忽略 RA,没有默认路由。use_tempaddr:启用 RFC4941 隐私扩展,避免出网源地址一直是用 DUID 生成的固定地址,被跨网络追踪。
加载配置并检查:
sysctl --system
sysctl net.ipv6.conf.all.forwarding
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
ip -6 addr show
如果之前 IPv6 一直是禁用状态,sysctl --system 后可能需要 systemctl restart systemd-networkd 或者重启网络服务。不建议重启整个服务器。
两种双栈配置方案
生产上我主要用两套配置:AlmaLinux 用 NetworkManager,Ubuntu 用 Netplan + systemd-networkd。两者都能实现静态地址 + SLAAC,但坑不一样。
| 对比项 | NetworkManager (AlmaLinux 9.3) | Netplan + systemd-networkd (Ubuntu 22.04) |
|---|---|---|
| 配置入口 | nmcli / .nmconnection 文件 | /etc/netplan/*.yaml |
| 路由管理 | 自动,但多配置时容易乱 | 手动 clear,逻辑直观 |
| SLAAC | 支持良好 | 支持良好,对 RA 的依赖需要显式声明 |
| 适合场景 | 桌面+有图形、动态网络切换 | 纯服务器、容器宿主机 |
| 故障排查工具 | nmcli + NetworkManager logs | networkctl + journalctl -u systemd-networkd |
结论很直接:服务器端我推荐 Netplan,因为它把配置和最终执行的 networkd 分开,restart 后路由表稳定,不像 NetworkManager 会在 profile 里叠加一堆状态。
方案A:NetworkManager 配置双栈
AlmaLinux 上用 nmcli 修改现有连接,不要直接改 ifcfg。检查连接名:
nmcli connection show
假设连接名是 ens18,手动指定 IPv6 地址和网关:
nmcli con mod "ens18" \
ipv6.method manual \
ipv6.addresses "2001:db8:0:10::10/64" \
ipv6.gateway "2001:db8:0:10::1" \
ipv6.dns "2606:4700:4700::1111,2001:4860:4860::8888" \
ipv6.routes "" \
ipv6.ignore-auto-routes yes
nmcli con up "ens18"
注意:ipv6.addresses 不是追加,会覆盖之前的地址。如果想保留原来的静态地址,用 +ipv6.addresses。ipv6.routes "" 清空自动路由,防止 RA 和手动路由冲突。验证:
nmcli -6 device show ens18
ip -6 route show dev ens18
NetworkManager 有时候会把 IPv6 地址从内核移除又重新添加,导致 DAD(重复地址检测)期间地址显示为 tentative,此时 TCP 连接会超时。等几秒再 ip -6 addr 看。
方案B:Netplan 配置双栈
Ubuntu 上我的典型配置长这样:
# /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: networkd
ethernets:
ens18:
dhcp4: true
dhcp6: false
accept-ra: true
addresses:
- 2001:db8:0:10::10/64
routes:
- to: default
via: 2001:db8:0:10::1
nameservers:
addresses:
- 2606:4700:4700::1111
- 2001:4860:4860::8888
search: [internal.example.com]
accept-ra: true 对应 systemd-networkd 的 IPv6AcceptRA=yes。这里我关掉了 dhcp6,只做静态地址 + RA 拿默认路由。这样配置最不容易出错。如果你用 /64 且希望 SLAAC 分配地址,可以考虑:
dhcp6: true
accept-ra: true
ipv6-address-generation: slaac
但生产环境我建议静态地址 + RA 分网关,便于做网络策略。
应用配置:
netplan try
netplan apply
networkctl status ens18
netplan try 会在 120 秒后自动回滚,避免你手滑把自己断网。这是 Netplan 比 NetworkManager 好用的地方。
防火墙:别只想着端口的 tcp/udp
IPv6 的 NDP 协议使用 ICMPv6,很多人在防火墙里只放行了 22/443,然后 ping 不通,TCP 却正常。我这边用 nftables,规则如下:
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 icmpv6 type \
echo-request accept
nft add rule inet filter input icmpv6 type \
destination-unreachable accept
nft add rule inet filter input icmpv6 type \
packet-too-big accept
nft add rule inet filter input icmpv6 type \
time-exceeded accept
nft add rule inet filter input icmpv6 type \
parameter-problem accept
nft add rule inet filter input icmpv6 type \
nd-neighbor-solicit accept
nft add rule inet filter input icmpv6 type \
nd-neighbor-advert accept
nft add rule inet filter input icmpv6 type \
nd-router-solicit accept
nft add rule inet filter input icmpv6 type \
nd-router-advert accept
如果用的 firewalld:
firewall-cmd --permanent --add-rich-rule='rule protocol value="icmpv6" accept'
firewall-cmd --reload
Rich rule 会放行所有 ICMPv6,比一条一条写省事,但在严格安全环境下,建议用 nftables 精确到 type。
注意:Linux 默认的 netfilter 对 ICMPv6 的 ip6tables 规则如果写的是 -j DROP 全部丢弃而不加白名单,直接导致 NDP 失败。所以你的默认策略如果是 drop,就必须加这些 type。
Nginx 双栈监听
nginx 配置很简单:
# /etc/nginx/conf.d/www.example.com.conf
server {
listen 80;
listen [::]:80;
server_name www.example.com;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /etc/nginx/ssl/example.com.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
root /var/www/www.example.com;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
Nginx 1.24.0 之后,listen [::]:443 ssl 默认是监听 IPv6 和 IPv4 的。但为了消除歧义,我还是写两个 listen。如果不想走 IPv4,可以加 ipv6only=on:
listen [::]:80 ipv6only=on;
listen [::]:443 ssl ipv6only=on;
nginx 重启后检查监听:
ss -lntup | grep -E ':(80|443)\b'
输出里应该同时看到 :::80 和 0.0.0.0:80 两行。
PHP 应用侧:识别双栈
应用如果想感知客户端连接用的哪个协议栈,PHP 8.2 可以直接判断 IPv6 地址特征:
<?php
$client = $_SERVER['REMOTE_ADDR'] ?? '';
if (str_contains($client, ':')) {
header('Content-Type: application/json');
echo json_encode(['ip' => $client, 'family' => 'IPv6']);
} else {
header('Content-Type: application/json');
echo json_encode(['ip' => $client, 'family' => 'IPv4']);
}
前端用 fetch 拿到这个 JSON:
// 前端双栈检测
fetch('/what-is-my-ip')
.then(res => res.json())
.then(data => {
const el = document.getElementById('ip-family');
el.textContent = data.family + ' ' + data.ip;
});
要按天统计 IPv4/IPv6 访问量,直接查 SQL:
SELECT
DATE(created_at) AS day,
SUM(ip LIKE '%:%') AS ipv6_count,
SUM(ip NOT LIKE '%:%') AS ipv4_count
FROM request_log
WHERE created_at >= CURRENT_DATE
GROUP BY day;
验证:三层可达性 + 四层端口 + 七层请求
我把验证拆成三层:
# 二层/三层:ping 网关,看 ND 缓存
ping6 -c3 2001:db8:0:10::1
ip -6 neigh show
# 四层:到本机 443
nc -6 -zv www.example.com 443
# 七层:真实 HTTP 请求
curl -6 -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://www.example.com/
nc 的 -6 在部分发行版里需要写成 -6 不带协议,如果提示不支持,可以直接:
timeout 3 bash -c 'echo > /dev/tcp/2001:db8:0:10::1/443'
效果数据
我在同一台机器上测了配置前后的差异,VPS 是 4C8G,KVM 虚拟化,CentOS 7 → Ubuntu 22.04.3 切换后做双栈压测。
| 指标 | IPv4 | IPv6 | 说明 |
|---|---|---|---|
| 内网 ping 平均延迟(ms) | 0.284 | 0.267 | 网关到主机 |
| 公网 curl 请求总耗时(s) | 0.213 | 0.226 | 同机房 CDN 出口 |
| ab -n 5000 -c 100 页面 QPS | 4289.6 | 4156.3 | HTTPS 2.0 静态页 |
| 建连时间(s) | 0.031 | 0.029 | ab 的 50% 分位 |
IPv6 建连略快,因为省了 NAT 的表项查询。但整体 QPS 差异在 3% 左右,可以忽略。如果你的压测结果 IPv6 慢很多,优先查 MTU 和防火墙,别怀疑内核。
另一个值得注意的数据:配置 accept_ra=2 之前,ip -6 route 里完全没有 default 路由;配上后需要 1-2 秒收敛,之后路由表:
ip -6 route show
输出示例:
2001:db8:0:10::/64 dev ens18 proto kernel metric 256 pref medium
default via 2001:db8:0:10::1 dev ens18 proto ra metric 100 pref medium
proto ra 就是通过 Router Advertisement 学到的默认路由,这是双栈是否被正确通告的最直观证据。
避坑指南
写这个就说明我踩过。
- IPv4 能通,IPv6 卡在 SYN_SENT:优先怀疑防火墙只放行了 ip6tables 的 22/80/443,没有放行 ICMPv6。NDP 的 neighbor solicitation 被丢弃后,TCP 甚至无法建立,表现为 SYN 重传直到超时。很多云控制台安全组没有单独 ICMPv6 放行项,去 VPC 的 ACL 里加,或者直接在系统防火墙放行。
- 开启 forwarding 后 RA 失效:Linux 内核的默认行为是,如果
forwarding=1,则accept_ra被忽略。所以容器宿主机上双栈默认路由总是丢失。必须把net.ipv6.conf.all.accept_ra和net.ipv6.conf.ethX.accept_ra都改成 2。这个 2 表示即使转发开启也接受 RA。 - netplan 别同时开 dhcp6 和静态地址:有一次我在 /etc/netplan/01-netcfg.yaml 里同时写了
dhcp6: true和addresses: [2001:db8::10/64],systemd-networkd 会在 DHCPv6 应答时把 bind 的地址替换为 DHCP 分配的地址,导致原本的静态地址消失。建议要么静态+RA,要么纯 DHCPv6,别混。 - 检查 DAD 状态:
ip -6 addr show里如果地址是tentative,说明在做重复地址检测。双栈配置后立刻访问会出现 Connection timed out。等 5-10 秒让地址变成 valid_lft 再压测。脚本里需要加这个判断。 - 云厂商安全组的坑:腾讯云、阿里云的控制台里,多数安全组规则默认只对 TCP/UDP 做匹配,ICMPv6 不生效。解决办法是在安全组里放行 ALL ICMPv6。华为云更特殊,即使放行所有端口,ICMPv6 还是会被拦,必须单独加一条 QoS 策略。
- MTU 导致黑洞:IPv6 的 PMTUD 依赖 ICMPv6 的 packet-too-big。如果中间链路 MTU 不是 1500(比如 PPPoE 或 WireGuard),并且上游丢弃了 ICMPv6,那么 TCP 大包永远无法到达,表现为小文件能访问,大文件卡死。解决:在内网网关或主机上把 MTU 降到 1400,并放行 packet-too-big 类型。
- 别急着重启服务器:
sysctl --system之后用ip -6 addr flush和ip -6 route flush清理旧状态,然后重新netplan apply或nmcli con up。重启服务器会引入不必要的业务中断。
没有高深的技术,IPv6 双栈的本质是内核收不收路由通告、防火墙放不放 NDP、服务有没有监听 v6 socket 三件事。按这个顺序查,十次有九次能解决。