手边这台机器正好是个现成的样本,IPv6 在内核启动阶段就被关掉了。先看它现在的样子,再讨论该不该这么做。

彻底关闭之后,系统里少了什么

cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-6.8.0-149-generic root=UUID=c2450dfb-ab8c-4183-a93b-8201c9c7fa6c ro ipv6.disable=1
ls /proc/sys/net/ | tr '\n' ' '; echo
sysctl net.ipv6.conf.all.disable_ipv6
lsmod | grep -c ipv6
ip -6 addr show scope global
core ipv4 mptcp netfilter unix
sysctl: 无法获取/proc/sys/net/ipv6/conf/all/disable_ipv6 的文件状态(stat): 没有那个文件或目录
0

四条命令给出的是同一件事的四个侧面:/proc/sys/net/ 下面根本没有 ipv6 这一项,sysctl 读不到对应键,模块没加载,全局 v6 地址为空。ip -6 addr 那条什么都没输出。

副作用也会浮出来。用 systemd-analyze verify 检查任意一个单元文件,它都会附带抱怨一句:

systemd-analyze verify ./any.service 2>&1 | grep -i ipv6
Binding to IPv6 address not available since kernel does not support IPv6.

这是 systemd 在解析 ListenStream= 之类的指令时发现内核没有 v6 栈。它不是错误,verify 的退出码仍然是 0,但会一直在。看到这条不要以为是单元写错了。

三种做法

做法一:内核启动参数 ipv6.disable=1。 就是上面这台机器的状态。协议栈完全不编译进运行时,模块不加载,/proc/sys/net/ipv6 不存在。改回来必须重启。

做法二:sysctl 运行时关闭。

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1

模块还是加载的,/proc/sys/net/ipv6 还在,只是不再分配地址、不再参与邻居发现。想恢复就写回 0,不用重启。

做法三:留着协议栈,只关自动配置。

sysctl -w net.ipv6.conf.all.accept_ra=0
sysctl -w net.ipv6.conf.all.autoconf=0
sysctl -w net.ipv6.conf.all.addr_gen_mode=1

accept_ra=0 让主机忽略路由器通告,autoconf=0 关掉 SLAAC 无状态自动配置,addr_gen_mode=1 是不生成基于 MAC 的临时地址。结果是:栈在,link-local 可能还在,但不会自己长出全局地址。

什么时候真的该考虑关

我能想到站得住脚的场景不多,大致这几类。

上游完全没有 v6,但局域网里有设备在发 RA。 典型是某台打印机、某个家用路由器、某台开了 IPv6 却没人管的服务器,周期性发通告,导致一堆主机长出全局地址。这些地址不可路由,但应用会尝试用它们,失败之后回落到 v4,表现为连接慢。这时候关掉自动配置(做法三)通常就够了,不必动协议栈。

AAAA 记录存在但不可达。 DNS 返回了 v6 地址,客户端按 Happy Eyeballs 同时发起 v4/v6,v6 一路超时。现代客户端处理得不错,但一些老的库和自研客户端不会并发尝试,会串行等 v6 超时。这种情况我更愿意先修 DNS——把没有 v6 可达性的主机名的 AAAA 记录删掉——而不是在每台机器上关 IPv6。

双栈监听造成的绑定混乱。 应用监听 0.0.0.0,但容器或者反向代理那一层按 [::] 走,net.ipv6.bindv6only 的取值又和预期不一致,排查起来很费劲。如果这个环境短期内不会上 v6,关掉能减少一半的变量。

统一装机模板要求可预测。 批量交付的机器,不希望因为插在不同网段就自动长出不同地址。这种时候做法一是合理的,因为它不依赖运行时的配置管理有没有跑到。

什么时候不该关

有 v6 上游、容器网络是 dual-stack、Kubernetes 集群配了 v6 pod CIDR、跑 OSPFv3 或者 BGP over v6、用了依赖 link-local 的服务发现——这些场景关掉就是直接制造故障。

还有一类理由我不接受:“关掉 IPv6 能减小攻击面”。如果这台机器本来就没有 v6 路由、没有 v6 邻居、没有任何监听在 :: 上,那它的 v6 攻击面本来就接近零,关掉省不下什么。反过来,如果它真的有 v6 可达性而你只在 iptables 里写了 v4 规则,那问题不在 IPv6 存在,在于你的防火墙规则漏了 ip6tables/nft 的 inet 家族。用"关 IPv6"来掩盖"防火墙没配全",是把一个配置缺陷变成一个更难发现的配置缺陷。

我的选择顺序

先做法三,再做法二,最后才考虑做法一。

理由是可回退性。做法三和做法二都是运行时可改的,出问题一条命令写回去;做法一要重启,而重启一台生产机器的成本远高于它省下的那点确定性。

做法一我只在一个场景下用:装机模板,机器还没进生产,且明确知道这批机器整个生命周期都不会有 v6。这时候把不确定性在最早的时间点消掉是划算的。

分情况的地方在于配置管理工具。如果机器由 Ansible 或者类似的工具统一管,做法二其实比做法三更稳——一个 sysctl 键值,幂等、可断言、可回滚。做法三涉及三个键,其中 addr_gen_mode 是每接口的,写全了啰嗦,写漏了接口新增时又会回到默认。

两个我不确定的点

一是 sysctl 关闭和网络管理服务的交互。disable_ipv6=1 之后,NetworkManager 或者 systemd-networkd 里那些配了 v6 的 profile 会怎么反应,是报错、是忽略、还是反复尝试把值改回去,我没有在一个干净环境里逐一验证过。不同发行版的默认网络栈不一样,结论大概也不通用。

二是做法二的生效时机。disable_ipv6 对已经存在的地址是立刻撤销还是要等接口 down/up,我印象里不同内核版本表现不一致,但这属于需要查具体版本文档的东西,不适合凭记忆写在这里。真要用做法二,先在测试机上确认地址确实消失了,再往下推。

最后一条经验:不管选哪种做法,都要在改完之后跑一遍依赖 v6 的东西。最容易忘的是 DNS 解析——getaddrinfo 在只有 A 记录和同时有 A/AAAA 记录时返回的顺序不一样,有些应用会因此走到不同的分支。