IPv6 改造最容易出问题的阶段,往往不是地址申请或网卡加地址,而是服务已经同时拿到 A 与 AAAA 记录后:少数客户端优先走 IPv6,流量开始穿过一条此前没有被完整演练过的链路。应用看起来“偶发超时”,运维看到的却可能是 DNS、负载均衡、内核、访问控制、回调地址和监控体系多处同时失配。
本文以 Linux 服务器、Nginx 反向代理和 Kubernetes 工作负载为例,说明如何把改造限定为可验证、可灰度、可回退的双栈变更。文中的 <接口名>、、<域名>、<命名空间> 等为占位符,首次使用前应替换为本环境值。命令以 Debian/Ubuntu 和 RHEL 系常见工具为基础;发行版差异请先通过 --help 与软件版本确认。
双栈不等于给每台机器补一个 IPv6 地址。对一个外部 HTTP 服务而言,至少要同时确认:客户端到 DNS、DNS 到入口、入口到后端、后端到依赖、依赖回调到服务端这五段路径。任何一段只有 IPv4,或者对 IPv6 使用了另一套 ACL、路由、NAT、健康检查和证书策略,都会让“能 ping 通”掩盖真实风险。
改造前先记录基线。下面脚本只采集状态,不修改系统,适合在入口节点和后端节点分别执行并归档结果。
bash
#!/usr/bin/env bash set -euo pipefail OUT_DIR="./ipv6-baseline-$(hostname)-$(date +%Y%m%d%H%M%S)" mkdir -p "${OUT_DIR}" ip -br address >"${OUT_DIR}/ip-address.txt" ip -6 route show table all >"${OUT_DIR}/ipv6-routes.txt" sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.default.disable_ipv6 >"${OUT_DIR}/ipv6-sysctl.txt" ss -lntup >"${OUT_DIR}/listeners.txt" resolvectl status >"${OUT_DIR}/resolver.txt" 2>&1 || true tar -czf "${OUT_DIR}.tar.gz" "${OUT_DIR}"
ip -br address 用于确认地址落在哪个接口;ip -6 route show table all 能发现策略路由或错误的默认路由。不要只保存 ip addr 的截屏:后续回滚需要可比较的文本证据。
检查内核并没有在全局或单接口禁用 IPv6:
bash
sysctl -a 2>/dev/null | grep -E '^net.ipv6.conf..*.disable_ipv6'
结果中 all、default 以及实际承载地址的 <接口名> 都应为 0。需要注意,all=0 不能自动修复一个已被显式设为 1 的接口;反过来,临时 sysctl -w 也不会替代持久配置。
以下命令列出本机 IPv6 地址及有效期。若地址只有很短的 preferred lifetime,连接可能在地址过期或重编号后出现源地址选择异常。
bash
ip -6 address show dev <接口名>
示例输出应标注为“示例输出”,例如:inet6 2001100:12/64 scope global dynamic。重点判断 scope global、前缀长度、valid/preferred lifetime 和接口是否符合网络设计;不要将文档中的 2001:/32 用作真实生产地址。
现代客户端通常会通过 Happy Eyeballs 并发或快速回退,但回退并不保证用户无感。某些 SDK、旧系统、企业代理或自研解析器会先解析 AAAA 并等待连接超时;更糟的是递归 DNS 已经缓存 AAAA,业务却只完成了入口一半的放通。
先分别查询两种记录,不能用只显示合并结果的命令代替。
bash
dig +noall +answer A <域名> dig +noall +answer AAAA <域名> 发布前要在外部网络、办公网络和目标 VPC 内各执行一次。DNS 控制台显示记录存在,并不能证明用户实际使用的递归服务器已刷新。对关键域名可查询权威 DNS:
bash
dig +trace AAAA <域名>
+trace 会增加查询量,不要高频运行于生产探针;其价值是区分权威记录缺失、委派错误和递归缓存问题。
在固定入口地址验证 HTTP,避免 DNS 把问题混在一起:
bash
curl -6 --connect-timeout 3 --max-time 10 -sS -D - --resolve <域名>[<入口IPv6地址>] "https://<域名>/healthz" -o /dev/null
方括号是 URL/--resolve 中 IPv6 字面量必须的表示形式。响应状态、证书 SNI 和 Host 都应与 IPv4 一致。将 -6 改成 -4 可得到同条件对照,不应只比较连通性,还要比较 30x 跳转、认证和限流结果。
一份适合灰度发布的检查脚本如下。它不会写 DNS,只在候选 AAAA 已存在时验证两个协议族;任何一侧失败都以非零退出,方便接入发布流水线。
bash
#!/usr/bin/env bash set -euo pipefail DOMAIN="<域名>" PATH_NAME="/healthz" for family in 4 6; do echo "checking IPv${family}" curl "-${family}" --fail --silent --show-error --connect-timeout 3 --max-time 10 "https://${DOMAIN}${PATH_NAME}" >/dev/null done echo "dual-stack endpoint check passed" 灰度顺序建议是:先部署入口 IPv6 与监控,再用内部测试域名验证,再把 AAAA 记录用较短 TTL 发布给小流量子域名,观察一个完整 DNS 缓存周期,最后才合并到主域名。回滚动作不是删除服务器 IPv6,而是先撤销 AAAA 或将其指向已验证入口;撤销前确认没有仅 IPv6 的内部调用方。
服务监听 [::]:443 时是否同时接收 IPv4 连接,取决于 net.ipv6.bindv6only 和应用自身的 bind 行为。不能把某台机器上的结果推广到所有节点。Nginx 的 listen [::]:443 也不会替代 listen 443:生产配置应显式声明两者,并保持 TLS、HTTP/2、日志和访问控制参数一致。
先查看真实监听者与地址族:
bash
ss -H -lntp '( sport = :80 or sport = :443 )'
若只看到 *:443,要结合 ss -4、ss -6 分别确认。以下命令读取内核策略:
bash
sysctl net.ipv6.bindv6only 推荐将 Nginx 的监听配置写清楚,而非依赖 IPv4-mapped IPv6 行为:
nginx
server { listen 80; listen [::]:80; server_name <域名>; return 308 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name <域名>; ssl_certificate /etc/nginx/tls/<域名>.crt; ssl_certificate_key /etc/nginx/tls/<域名>.key; location = /healthz { return 200 "ok "; } }
配置修改前备份,之后先做语法检查再平滑生效。reload 会让新 worker 使用新配置,不能替代协议族验证。
bash
sudo cp -a /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.bak.$(date +%Y%m%d%H%M%S)" sudo nginx -t sudo systemctl reload nginx sudo ss -lntp '( sport = :80 or sport = :443 )'
如果语法检查失败,立即停止,不要重启服务。回滚使用刚才的备份文件,并再次执行 nginx -t 后 reload:
bash
sudo cp -a /etc/nginx/nginx.conf.bak.<时间戳> /etc/nginx/nginx.conf sudo nginx -t && sudo systemctl reload nginx
对容器化服务,应用只绑定 127.0.0.1 会让宿主 IPv6 入口无论如何都不可达。进入网络命名空间检查监听,而不是仅在宿主机检查端口映射:
bash
docker exec <容器名> sh -c 'ss -lntp || netstat -lntp'
Docker 是否支持 IPv6 还取决于 daemon 配置与网络创建方式。下面是专用 bridge 网络的示例;subnet 必须由网络团队分配,切勿从公网前缀随意挑选。
yaml
services: api: image: <镜像>:<标签> networks: [dualstack] networks: dualstack: enable_ipv6: true ipam: config: - subnet: "/64" 变更 Docker daemon 或默认网络会影响同机容器,必须先在独立节点灰度;不要为解决单个服务问题直接重启生产 Docker 守护进程。
IPv4 和 IPv6 是两套独立的过滤规则。常见误判是 ufw status 显示端口已允许,实际上配置文件 IPV6=no;或者云安全组规则只配置 CIDR 0.0.0.0/0。IPv6 没有 NAT 作为默认“遮挡层”,地址可路由也意味着入口规则必须更精确。
先收集主机侧规则,规则体系只能选本机实际启用的一种为准。使用 nftables 的系统可执行:
bash
sudo nft list ruleset
iptables-nft 兼容层环境要分别检查 v4/v6,不能以 iptables -S 代表 IPv6:
bash
sudo iptables -S sudo ip6tables -S 使用 UFW 时,确认其 IPv6 开关与实际规则:
bash
grep -E '^IPV6=' /etc/default/ufw sudo ufw status numbered
对互联网入口,建议只允许必要协议和经过评审的源前缀。下面 nftables 片段展示 IPv6 TCP 443 放通;合入前须与现有 inet filter 表结构合并,避免覆盖其他链。
nft
table inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iifname "lo" accept ip6 nexthdr ipv6-icmp accept tcp dport 443 ip6 saddr <允许的IPv6源前缀> accept } } 不要机械封禁所有 ICMPv6。邻居发现、路径 MTU 发现和错误通知依赖它;过度限制常表现为“小包正常、大响应卡住”。最终规则应由安全基线决定,至少保留网络团队批准的必要类型。
变更防火墙是高风险操作:先在带外控制台保持一条会话,使用定时回滚保护远程访问。以下脚本以 nft -c 检查候选规则,并在 5 分钟后恢复备份;只有验证完成才应取消定时任务。
bash
#!/usr/bin/env bash set -euo pipefail BACKUP="/root/nftables.before-ipv6.$(date +%Y%m%d%H%M%S)" sudo nft list ruleset | sudo tee "${BACKUP}" >/dev/null sudo nft -c -f /etc/nftables.conf echo "sudo nft -f '${BACKUP}'" | at now + 5 minutes sudo nft -f /etc/nftables.conf echo "验证完成后使用 atq/atrm 取消自动回滚任务"
执行前确认系统安装并启用了 atd;未启用时不要假定上述保护存在。验证时从授权的外部探针进行 IPv6 TLS 连接,再检查计数器或服务日志。云安全组、负载均衡 ACL、主机防火墙三处缺一不可。
IPv6 路由器不负责分片,路径 MTU 发现(PMTUD)被防火墙拦截后,典型现象是三次握手成功、短响应成功、下载或 TLS 握手中的较大报文超时。不要靠随意把网卡 MTU 改小“解决”;先定位实际路径和 ICMPv6 是否可达。
先查看接口 MTU 与默认路由:
bash
ip link show dev <接口名> ip -6 route get <对端IPv6地址>
Linux 的 tracepath6 可观察探测到的路径 MTU,目标可以是一个已授权的后端或入口:
bash
tracepath6 -n <对端IPv6地址> 对方不响应探测并不直接证明 MTU 有问题;应和服务失败的时间、抓包及 ICMPv6 Packet Too Big 证据结合。抓包时注意不要在包含敏感业务数据的接口长期运行:
bash
sudo timeout 30 tcpdump -ni <接口名> -vv 'icmp6 or (tcp port 443 and ip6)'
示例输出必须写作“示例输出”:ICMP6, packet too big, mtu 1280 表示设备在通知更小的可用 MTU;此时应检查中间防火墙是否转发该 ICMPv6,并确认隧道、VPN、Overlay 网络的封装开销。
若服务前有 Nginx,针对 TCP 代理可谨慎开启 MSS 钳制应由网络团队在边界设备实施。应用层不要以硬编码响应分块替代网络修复。HTTP 层可用一个足够大的但无敏感内容的测试对象验证传输:
bash
curl -6 --fail --http1.1 --max-time 30 -o /dev/null -w 'http=%{http_code} bytes=%{size_download} time=%{time_total} ' "https://<域名>/ipv6-test-1m.bin" 若明确要临时降低单个接口 MTU,应先确认该接口承载范围和重启后的持久化来源。下面命令只适合变更窗口内受控验证,断连风险必须提前告知业务方:
bash
sudo ip link set dev <接口名> mtu <临时MTU值> ip link show dev <接口名>
回滚为改动前记录的 MTU 值。不要把 1280 当作通用最优值,它只是 IPv6 最小链路 MTU,性能与封装场景需实测评估。
IPv6 文字地址内含冒号。把 $remote_addr:$remote_port 直接拼成日志字段,或把 IPv6 传给只接受 host:port 的旧解析器,会导致审计字段无法解析、白名单失效,甚至请求被错误路由。正确做法是使用结构化字段,或在 URI 中使用方括号。
检查 Nginx 是否真实将地址交给后端,而不是仅检查 access log:
nginx
log_format json_access escape=json '{"time":"$time_iso8601","remote_addr":"$remote_addr",' '"request":"$request","status":$status,"upstream":"$upstream_addr"}'; access_log /var/log/nginx/access.json json_access;
escape=json 能避免请求字段破坏 JSON;日志采集端仍需按 JSON 解析,不能继续按空格切分。修改后使用 nginx -t,再通过 IPv4、IPv6 各请求一次并检查字段。
反代到 IPv6 字面量上游必须用方括号,否则 Nginx 无法区分端口:
nginx
upstream api_backend { server [<后端IPv6地址>]:8080 max_fails=3 fail_timeout=10s; } server { listen 443 ssl; listen [::]:443 ssl; location /api/ { proxy_pass http://api_backend; } }
如果使用域名上游,并希望 Nginx 运行期解析 AAAA,需要指定支持 IPv6 的 resolver,并明确测试 DNS 变更后的行为。不同 Nginx 版本对动态解析的支持方式不同,应参考本机 nginx -V 与部署规范,而不在生产配置中盲目加变量。
对于真实客户端地址,只有在负载均衡器地址被严格信任时才接受 X-Forwarded-For 或 PROXY protocol。下面配置将可信代理限制为明确的 IPv4/IPv6 网段:
nginx
set_real_ip_from <负载均衡IPv4网段>; set_real_ip_from <负载均衡IPv6前缀>; real_ip_header X-Forwarded-For; real_ip_recursive on;
set_real_ip_from 放宽到 0.0.0.0/0 或 ::/0 会让任意客户端伪造来源地址,不能这样配置。验证时在后端日志中比较入口连接地址、转发头和 $realip_remote_addr,以证据确认链路。
应用数据库、缓存或 SMTP 回调配置也常有这个问题。URI 中必须为 IPv6 加方括号,例如:
text
postgresql://<用户名>:<密码>@[<数据库IPv6地址>]:5432/<数据库名> 这是配置示例,不应把密码写入命令历史、镜像或 Git 仓库。优先使用密钥管理系统或受限权限的环境变量文件。
Kubernetes 双栈需要控制面、节点、CNI、Pod CIDR、Service CIDR 与 kube-proxy/数据面协同支持。仅给 Ingress 增加 IPv6 地址,不代表 Pod 到 Pod、Pod 到公网、Service DNS 都具备 IPv6。先确认集群版本和地址族能力,变更前不要编辑 kubeadm 或 CNI 的生产配置。
bash
kubectl version --short kubectl get nodes -o wide kubectl -n <命名空间> get pods -o wide
检查指定 Service 的 ipFamilies、ipFamilyPolicy 和 clusterIP 列表:
bash
kubectl -n <命名空间> get service <服务名> -o yaml
双栈 Service 的示例清单如下。PreferDualStack 允许在集群能力不完整时退化,适合迁移期;真正要求双栈时才评估 RequireDualStack,它可能导致不支持的环境无法创建 Service。
yaml
apiVersion: v1 kind: Service metadata: name: api namespace: <命名空间> spec: selector: app: api ports: - name: http port: 80 targetPort: 8080 ipFamilyPolicy: PreferDualStack ipFamilies: - IPv4 - IPv6 应用侧应在 Pod 内分别解析和连接 Service,避免节点网络的成功掩盖 CNI 问题:
bash
kubectl -n <命名空间> exec <测试Pod名> -- sh -c ' getent ahostsv4 <服务名> && getent ahostsv6 <服务名> wget -4 -qO- http://<服务名>/healthz wget -6 -qO- http://<服务名>/healthz '
并非所有精简镜像都有 wget -4/-6,应在专用诊断镜像中执行,或改用镜像实际拥有的工具。不要为了排障在业务容器中临时安装软件并遗留状态。
NetworkPolicy 也必须检查 IPBlock 的地址族。下面片段仅说明 IPv6 来源限制的写法,合并前应核对已有 ingress 规则;空 ingress 或错误 selector 可能意外切断全部流量。
yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: api-ingress-ipv6 namespace: <命名空间> spec: podSelector: matchLabels: app: api policyTypes: [Ingress] ingress: - from: - ipBlock: cidr: <允许的IPv6前缀> ports: - protocol: TCP port: 8080 应用 NetworkPolicy 前先使用测试 namespace 与 canary Pod,记录现有策略:
bash
kubectl -n <命名空间> get networkpolicy -o yaml > networkpolicy.before.yaml kubectl -n <命名空间> apply --dry-run=server -f api-ingress-ipv6.yaml kubectl -n <命名空间> apply -f api-ingress-ipv6.yaml
--dry-run=server 只能校验 API 接受对象,不能证明 CNI 会按预期执行策略。上线后从允许和不允许来源分别探测;若业务受影响,使用之前导出的清单恢复并复验。
bash
kubectl -n <命名空间> apply -f networkpolicy.before.yaml kubectl -n <命名空间> get networkpolicy 监控应分地址族观察成功率和延迟。指标名称取决于 Ingress、Service Mesh 或 exporter,以下 PromQL 是对常见 HTTP 请求计数器的示意,必须以实际 exporter 暴露的指标和标签为准:
promql
sum by (ip_family, code) ( rate(http_requests_total{service="<服务名>"}[5m]) )
若系统没有 ip_family 标签,不要凭空在仪表盘中假设它存在;可从入口日志增加安全的协议族字段,再由日志平台聚合。节点层可采集 node_network_receive_bytes_total 等指标,但同样应以实际 node exporter 暴露的指标为准。
一次可接受的双栈上线,不是“IPv6 成功率没有明显告警”,而是有针对性证据证明两条链路都正确:DNS 的 A/AAAA 符合设计;入口对 v4/v6 都监听;安全组、负载均衡 ACL、主机防火墙一致;应用健康检查、认证、上传下载与回调路径均通过;按地址族切分的错误率与延迟在观察窗口内稳定。
下面巡检脚本可作为发布后定时任务。它同时验证解析、IPv4、IPv6 和 Nginx 配置,但不会代替外部真实用户探测。
bash
#!/usr/bin/env bash set -euo pipefail DOMAIN="<域名>" HEALTH_PATH="/healthz" test -n "$(dig +short A "${DOMAIN}")" test -n "$(dig +short AAAA "${DOMAIN}")" for family in 4 6; do curl "-${family}" -fsS --connect-timeout 3 --max-time 10 "https://${DOMAIN}${HEALTH_PATH}" >/dev/null done nginx -t echo "$(date -Is) dual-stack check ok" 回滚策略应按故障层次执行:若仅公网 IPv6 不可用,先将 AAAA 从流量入口撤出或在 DNS 管理面恢复上一个记录集;若入口配置错误,恢复已验证的 Nginx 配置;若 NetworkPolicy 阻断,恢复已导出的策略;若路由或 MTU 失败,恢复网络变更并保留抓包、路由和错误时间线。任何回滚后都应验证 IPv4 未受影响,再明确 IPv6 的现状,避免把“暂时只提供 IPv4”误报成“全量恢复”。
改造完成后建议把地址族作为容量、可用性与安全审计的一级维度。IPv6 不应该成为一条无人维护的旁路:变更评审、演练、日志解析、告警、WAF/ACL 和依赖方接入规范都要一起纳入日常运维。
双栈变更单至少应包含地址规划、所有暴露端口、DNS 记录集、入口与后端的 v4/v6 连通性结果、ACL 差异、证书校验结果、观测面板链接和回滚负责人。特别是回调型依赖:支付、对象存储、OAuth、Webhook、SMTP、消息队列和第三方 SaaS。它们可能解析 AAAA,但源站、代理或 allowlist 仍只允许 IPv4。
对服务端证书,分别在两个协议族上检查 SNI 和证书链;这能发现某个 IPv6 入口指向旧的 TLS 终止节点。
bash
for family in 4 6; do echo "IPv$family" openssl s_client -"$family" -connect <域名>:443 -servername <域名> -verify_return_error /dev/null | openssl x509 -noout -subject -issuer -dates done TLS 版本的 openssl 可能不支持 -4/-6 参数;若本机帮助信息没有该参数,可先用 getent 或 dig 获取地址,再以方括号 IPv6 字面量和 -connect 验证。命令不应输出或保存私钥。
发布窗口中应将 IPv6 错误独立告警。以下查询仅示意通过 Nginx 日志解析形成的指标;指标名和标签必须以实际日志采集器与 exporter 暴露为准。
promql
sum(rate(nginx_http_requests_total{server="<域名>",ip_family="ipv6",status=~"5.."}[5m])) / sum(rate(nginx_http_requests_total{server="<域名>",ip_family="ipv6"}[5m])) 如果没有协议族标签,不要用空结果证明 IPv6 无错误。应先确认日志字段产生、采集规则和 dashboard 查询的标签基数,再根据访问日志的 remote address 或入口 socket 采样验证。
对于出站依赖,应用选择 IPv4 或 IPv6 的行为也需要固定。不要依赖 DNS 返回顺序;客户端库、操作系统 RFC 6724 策略、代理环境变量和连接池缓存都可能改变实际选择。修改 gai.conf、系统全局优先级或禁用地址族影响面很大,应该作为独立网络变更审批,而不是业务上线时临时调整。
全部0条评论
快来发表一下你的评论吧 !