IPv6双栈服务器配置实战:一次踩坑全记录
发布日期: 2026/08/02 阅读总量: 0

凌晨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.addressesipv6.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'

输出里应该同时看到 :::800.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_ranet.ipv6.conf.ethX.accept_ra 都改成 2。这个 2 表示即使转发开启也接受 RA。
  • netplan 别同时开 dhcp6 和静态地址:有一次我在 /etc/netplan/01-netcfg.yaml 里同时写了 dhcp6: trueaddresses: [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 fluship -6 route flush 清理旧状态,然后重新 netplan applynmcli con up。重启服务器会引入不必要的业务中断。

没有高深的技术,IPv6 双栈的本质是内核收不收路由通告、防火墙放不放 NDP、服务有没有监听 v6 socket 三件事。按这个顺序查,十次有九次能解决。