很多人第一次真正被 IPv6 "咬"到,不是在读 RFC 的时候,而是在某台机器上执行完ip addr,看到一长串fe80::开头的地址,心里冒出一句:这玩意儿到底哪个能用?我最初接触 IPv6 也是这个状态——培训材料上讲得头头是道,什么 128 位、地址无限多、自动配置,但真到一台业务机器上要给它配一个固定地址、要从外面访问进来、要确认防火墙没漏,才发现基础概念不清楚,后面每一步都是瞎猜。IPv6 的基础概念不是背几条地址分类就完事,它真正决定的是你后面配置、排障、安全策略的每一个动作。这篇文章我想把 IPv6 从"地址长什么样"到"设备怎么自动拿到地址"、再到"报文头为什么这么设计""双栈阶段有哪些真实教训"这条线完整捋一遍,适合刚上手 IPv6 的运维和开发,也适合已经用过但一直没搞明白细节的人。
1. 从一次"地址不够用"的排查说起:IPv6到底在解决什么问题
几年前帮一个团队排查一个内网服务访问异常的问题,他们的机器数量其实不算多,几百台而已,但网段规划得一塌糊涂,多个业务挤在同一个 /24 里,后来想拆分,发现地址池已经紧到没法再切。当时有人提议上 IPv6,会议室里一片沉默——大部分人的第一反应是"我们又没缺到那个程度"。这种反应很典型,因为很多人对 IPv4 地址枯竭的理解停留在"全球地址用完了",觉得自己在内网用私有地址,跟这事没关系。实际上关系很大,只是体现得比较隐蔽。
1.1 32位地址空间的天花板是怎么被撞到的
IPv4 的地址长度是 32 位,理论总数是 2 的 32 次方,约 42.9 亿个。这个数字在 1980 年代设计时看起来是天文数字,因为当时接入网络的计算机可能只有几千台。问题在于分配方式极其粗糙:早期按类别划分,A 类地址一个网段就吃掉 1677 万个地址,分给了极少数机构,大量地址被浪费在"预留但没启用"的状态里。后来靠 CIDR(无类别域间路由)和 NAT 续命,把地址利用率拉高了一大截,但 NAT 本质上是把"地址不够"这个结构性问题变成了"端口和会话表不够"的新问题。
内网用私有地址确实缓解了压力,但私有地址只有三段:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,加起来约 1780 万个地址。听起来很多,可一旦公司做过多轮并购、历史网段没人敢动、每个机房都要留冗余段、每个云环境还要做地址重叠隔离,这 1780 万很快就不够分了。我见过最夸张的情况是三套不同的办公网用了完全相同的 192.168.1.0/24,做互联时只能在中间加一层地址转换,链路变成"转了三道",出问题时定位成本成倍上升。
所以 IPv4 的真正痛点不是"全球地址耗尽",而是地址规划的自由度被彻底压缩。你没法给每个机柜、每个虚拟机、每个容器分一段独立可路由的地址,只能不停地做 NAT、做重叠、做转换层。IPv6 要解决的核心问题,是把这个自由度还回来。
1.2 128位换来的不只是数量
IPv6 是 128 位,地址总数是 2 的 128 次方,约 3.4 乘以 10 的 38 次方。这个数字抽象到没有意义,换个算法:地球陆地表面积大约 1.49 亿平方公里,折算成平方米约 1.49 乘以 10 的 14 次方,平均下来每平方米能分到 2 乘以 10 的 24 次方个地址。也就是说,理论上地球上每一平方米的土地都可以分到上万亿个地址段,还绰绰有余。
但数量只是表面收益。真正有价值的是结构化的分配方式。IPv6 的标准前缀长度是 /64,一个 /64 里就有 2 的 64 次方个地址,约 1844 亿亿个,足够一个普通子网用一辈子。而一个 /48 可以切出 65536 个 /64,一个 /32 可以切出 65536 个 /48。这个层级意味着:一个中等规模的企业拿到一个 /32 之后,可以按"站点 /48 → 楼层或业务 /56 → 子网 /64"的方式逐层划分,每一层都有充足的余量,完全不需要考虑地址复用和 NAT。
另外两个容易被忽略的变化:IPv6 取消了广播地址,所有原来靠广播实现的机制改用组播;IPv6 的地址自动配置是协议内建能力,不需要额外的服务端就能让设备拿到可路由地址。这两点后面会详细讲,但它们直接改变的是网络运维的日常动作——你不再需要维护一张 DHCP 保留表来对应每台机器,也不用担心广播风暴把整个网段打瘫。
注意:拿到 IPv6 地址段之后,千万别按 IPv4 的习惯"省着用"。IPv6 的地址空间大到不需要节约,真正需要规划的是前缀层级,而不是单个地址。很多团队第一次上 IPv6 时把 /64 切得极碎,结果路由表膨胀、排障困难,这是典型的把 IPv4 思维带进了 IPv6。
2. 看懂一串IPv6地址:书写规则与压缩陷阱
IPv6 最劝退新人的地方就是地址写法。一长串十六进制加冒号,视觉上完全没有 IPv4 那种"四个数字点分"的直觉。但只要掌握几条固定规则,读地址会变得非常快,甚至能一眼看出这个地址是内网还是公网、是自动配置还是手工指定。我在排查问题时经常靠地址的前几位就能判断出问题大致出在哪一层,这个技能完全建立在熟悉书写规则的基础上。
2.1 八组十六进制的读法
IPv6 地址固定 128 位,被平均切成 8 组,每组 16 位。16 位用十六进制表示正好是 4 个字符,所以标准写法是 8 组、每组 4 个十六进制数字,组之间用冒号分隔。举个例子:
2001:0db8:0000:0000:0000:0000:0000:0001这就是一个完整的、没有做任何压缩的 IPv6 地址。每一组取值范围是 0000 到 ffff,也就是 0 到 65535。读的时候不要试图把它当成一个整体数字,而要理解成"8 段并列的编号",前 4 段通常承载网络前缀信息,后 4 段通常是接口标识。
这里有个思维转换很重要:IPv4 地址里"网络部分"和"主机部分"的边界是靠子网掩码隐含的,而 IPv6 里这个边界是靠前缀长度显式声明的,写法是地址后面加斜杠加数字,比如2001:db8:1:2::1/64。斜杠后面的 64 表示前 64 位是网络前缀,后 64 位是接口标识。这个划分方式在实践中几乎是默认标准,原因后面会讲。
2.2 双冒号压缩的三条铁律
标准写法太长了,所以协议规定了两条压缩规则。第一条:每组开头的连续零可以省略,0db8可以写成db8,0001可以写成1,但组内末尾的零不能省,0100不能写成01,否则位权就变了。第二条:连续的多组全零可以用一对冒号::代替。
第二条规则最容易出错,必须记住三条铁律:
::在一个地址里只能出现一次。因为它代表"这里是若干组零",出现两次就无法确定每组零到底有多少个,地址就产生歧义了。::可以压缩一组零,也可以压缩多组零,甚至可以是地址开头或结尾。::1表示前面 7 组全零、最后一组是 1,这就是环回地址;2001:db8::表示后面若干组全零。- 压缩之后要能唯一还原。判断方法是数一数剩下的组数,用 8 减去它,就是
::代表的组数。
我见过的最典型的书写错误是把2001:db8::1::2这种写法当真,实际上这完全非法。还有一种情况是脚本拼接地址时把::当成普通字符串处理,结果拼出两个双冒号,配置直接报错。
来看几个实际例子,对照着读会更容易建立感觉:
| 压缩写法 | 完整展开 | 说明 |
|---|---|---|
::1 | 0000:0000:0000:0000:0000:0000:0000:0001 | 环回地址,等价于 IPv4 的 127.0.0.1 |
:: | 全零 | 未指定地址,不能作为目的地址使用 |
fe80::1 | fe80:0000:0000:0000:0000:0000:0000:0001 | 链路本地地址 |
2001:db8::1 | 2001:0db8:0000:0000:0000:0000:0000:0001 | 文档示例前缀下的地址 |
注意:
2001:db8::/32是专门保留给文档和示例使用的地址段,任何时候都不要在真实环境里配这个前缀。它的作用和 IPv4 里的 192.0.2.0/24 一样,就是让你在写教程、写测试用例时不占用真实地址。
2.3 前缀长度与网段划分
前缀长度决定了这个地址所在网段的大小。常见的几个值要心里有数:/128 表示单个地址,/64 是标准子网,/56 和 /48 常用于站点或业务层级划分,/32 一般是运营商或大型企业拿到的整段。
为什么 /64 是标准?因为无状态地址自动配置(SLAAC)需要用后 64 位作为接口标识,如果前缀不是 /64,自动配置就无法按标准方式工作。早期有人试图用 /80 或者 /112 来"节约地址",结果导致 SLAAC 失效、邻居发现异常,最后只能改回来。这个教训在很多团队里都发生过,本质上是把 IPv4 的精打细算习惯带到了 IPv6。
实际划分的时候我习惯这样规划:拿到一个 /48,按业务线切成 /56,一条业务线有 256 个 /64 可用;再按机房或环境切成 /60,每个环境 16 个 /64。这个粒度对绝大多数中型规模的环境都足够,而且层级清晰,看地址前几位就能判断业务归属。如果拿到的是 /32,第一层直接切 /48,留出的空间足够未来几年扩张。
| 前缀长度 | 地址数量 | 典型用途 |
|---|---|---|
| /128 | 1 | 单个接口地址 |
| /64 | 约 1.84×10^19 | 标准子网,支持 SLAAC |
| /56 | 256 个 /64 | 业务线或单机房 |
| /48 | 65536 个 /64 | 站点级分配 |
| /32 | 65536 个 /48 | 企业或运营商整段 |
3. 地址不是只有一种:单播、组播、任播的分工
IPv4 时代大家对地址分类的印象是 A、B、C 类,IPv6 换了一套完全不同的分类逻辑:按"通信模式"分,而不是按"大小"分。这个划分方式直接对应了协议的运行机制,理解它才能看懂后面自动配置、邻居发现这些流程为什么那样设计。很多人卡在 IPv6 门口,就是因为把"地址类型"当成记忆题,而不是当成"这个地址用在什么场景"的问题。
3.1 单播内部的四种角色
单播就是一对一通信,这是最常见的类型。但 IPv6 的单播内部还细分成几种,各自有明确的使用边界:
全局单播地址,前缀范围是2000::/3,也就是前三位是 001。这个范围覆盖了目前所有已经分配给运营商的地址段,是可以全球路由的地址。看到以 2 或 3 开头的地址,基本可以判断它是公网地址。
链路本地地址,前缀fe80::/10。这个地址只在单个链路(也就是同一个二层广播域)内有效,路由器不会转发它。每块网卡启用 IPv6 后会自动生成一个链路本地地址,不需要任何配置。它的作用是支撑邻居发现、路由协议这些底层机制。注意fe80::/10这个范围意味着前 10 位固定,实际常见的地址前 16 位通常是fe80,但理论上fe80到febf都算。
唯一本地地址,前缀fc00::/7。这个范围相当于 IPv4 的私有地址,不会被全球路由。其中fc00::/8保留未定义,实际可用的是fd00::/8,也就是第二位是 d 的地址。使用时需要自己生成一个 40 位的全局 ID,保证不同站点之间不冲突。
环回地址和未指定地址,分别是::1和::。前者等价于 127.0.0.1,后者用于表示"还没有地址"的状态,比如主机在做地址重复检测时会用它作为源地址。
| 类型 | 前缀 | 是否可全球路由 | 典型出现位置 |
|---|---|---|---|
| 全局单播 | 2000::/3 | 是 | 对外提供服务的接口 |
| 链路本地 | fe80::/10 | 否 | 每块网卡自动生成 |
| 唯一本地 | fd00::/8 | 否 | 内网隔离环境 |
| 环回 | ::1/128 | 否 | 本机测试 |
| 未指定 | ::/128 | 否 | 地址配置过程中的临时占位 |
3.2 组播为什么在IPv6里变得重要
IPv6 明确取消了广播,原来靠广播实现的功能全部改用组播。这个改动解决的是"广播会把同一网段内所有设备都叫醒"这个老问题。组播的地址前缀是ff00::/8,第二位和第三位编码了标志位和作用域。
几个必须记住的组播地址:
ff02::1:链路上所有节点。作用类似 IPv4 的 255.255.255.255 广播,但只发给加入了该组的节点。ff02::2:链路上所有路由器。主机发送路由请求时用的就是它。ff02::1:ff00:0/104:请求节点组播地址段。每配置一个单播地址,系统会自动加入一个对应的请求节点组播组,地址构造方式是取单播地址的后 24 位,拼到ff02::1:ff00:0/104后面。
请求节点组播这个机制值得单独说一下,因为它替代了 IPv4 的 ARP。IPv4 里解析一个 IP 对应的 MAC 地址,要往整个网段广播 ARP 请求,所有设备都得处理一遍。IPv6 里改为把请求发到目标地址对应的请求节点组播组,只有真正相关的设备(理论上只有目标本身,以及配置了相同末 24 位的极少数设备)会响应。这个优化在设备密集的网段里效果非常明显。
3.3 任播的实际用法
任播是一对"最近的一个"通信:多个节点配置相同的任播地址,数据包会被路由到拓扑上最近的那个节点。IPv6 里任播不像单播和组播那样有独立的地址格式,而是通过在接口上把同一个地址配置到多台设备上来实现。
最常见的用法是"子网路由器任播地址":在某个子网前缀内,把"全零接口标识"的那个地址(比如2001:db8:1:2::)配置到该子网的所有路由器上。主机需要访问本子网外的网关时,可以直接把包发给这个地址,路由协议会自动选择最近的那台路由器。这个机制让主机不需要知道具体网关地址,配置上的耦合度更低。
不过在实践中,任播更多是用在 DNS 根服务器、CDN 节点这类场景——同一个地址在全球多个机房配置,用户请求自动落到最近的一台。对我这种做内网运维的人来说,任播的直接用处不大,但理解它的存在有助于看懂某些地址为什么"看起来像网络地址却能用"。
4. 插上网线就有地址:SLAAC与邻居发现的完整链路
这是 IPv6 最让人惊喜也最容易出问题的地方。一台设备接上网线,几秒钟之内就能拿到一个可用的全局地址,整个过程不需要任何服务器参与。很多人第一反应是"这么方便,那还要 DHCP 干什么",也有人第一次看到机器上突然多出一个地址时怀疑是不是被人配错了。把这条链路拆开看,每一步都有明确的用途,出问题时也能精确定位到停在哪一步。
4.1 RS/RA:路由通告怎么把前缀发下来
设备接入网络后,首先要生成一个链路本地地址。这个动作不依赖任何外部设备,是主机自己完成的:用某种方式生成 64 位接口标识,拼到fe80::/64前缀后面。生成之后不能立即使用,必须先做地址重复检测(DAD)——向该地址对应的请求节点组播组发一个邻居请求报文,如果没有收到响应,就认为地址可用。
链路本地地址可用之后,主机向ff02::2(所有路由器)发送路由请求报文(Router Solicitation,RS),意思是"这个链路上有没有路由器,请告诉我怎么配置"。路由器收到后回复路由通告报文(Router Advertisement,RA),内容包含几个关键信息:
- 网络前缀和前缀长度,通常是 /64。
- 标志位,其中 A 位表示允许主机用这个前缀做无状态配置,M 位表示地址要通过 DHCPv6 获取,O 位表示其他配置信息(比如 DNS)要通过 DHCPv6 获取。
- 生命周期,分有效生命周期和首选生命周期。首选生命周期内的地址可以正常用于新建连接,超过首选但未超有效的地址只能维持已有连接,不能新建。
- 默认路由信息,告诉主机本网段的默认网关是谁。
主机收到 RA 之后,如果 A 位置位,就用"前缀 + 自己的接口标识"拼出一个全局地址,再做一次 DAD,通过后就能正常通信了。整个过程通常在一两秒内完成。
路由器也会定期主动发送 RA(默认间隔大约 200 秒),保证新接入或者错过 RS 的设备也能收到。这里有个实操要点:RA 的发送间隔不能配得太短,某些设备对频繁的 RA 处理不过来,反过来如果完全关闭主动发送,只靠 RS 触发,某些老设备可能等不到。
4.2 接口标识的两种生成方式
后 64 位的接口标识怎么来,有两种主流方式,各有取舍。
第一种是基于 MAC 地址的 EUI-64 格式。做法是把 48 位 MAC 地址从中间劈开,插入fffe,然后把第一个字节的第 7 位(从高位数的 U/L 位)取反。举例来说,MAC 是00:1a:2b:3c:4d:5e,插入之后变成00:1a:2b:ff:fe:3c:4d:5e,再翻转 U/L 位,最终接口标识是021a:2bff:fe3c:4d5e。
这种方式的好处是稳定、可预测,同一个网卡在同一个前缀下永远是同一个地址。坏处也很明显:MAC 地址被暴露在公网地址里,任何人都能通过地址反推出设备厂商甚至具体型号,这被认为是隐私风险。另外设备换网卡地址就变,如果是做访问控制,维护起来很麻烦。
第二种是隐私扩展(RFC 4941),定期随机生成接口标识,并生成一组临时地址用于对外发起连接。默认情况下系统会优先使用临时地址出站,这样外部看到的源地址会定期变化,追踪难度大幅提升。代价是地址不再稳定,如果你的业务需要基于源地址做白名单,就会很痛苦。
实际部署里的常见做法是两者并存:稳定地址用于对外提供服务(配在 DNS 里),临时地址用于主动对外访问。我处理过的环境里,有些团队为了省事直接关掉隐私扩展,结果被人扫到地址段后做了资产测绘,事后追查才发现暴露面比想象的大得多。
4.3 NS/NA取代ARP之后的变化
邻居发现协议(NDP)用 ICMPv6 承载,核心是两类报文:邻居请求(NS)和邻居通告(NA)。作用相当于 IPv4 的 ARP 请求和应答,但机制上更精细。
当主机要发数据给同一链路上的某个地址时,先查邻居缓存。缓存里没有,就向目标地址对应的请求节点组播地址发 NS,源地址用自己的单播地址。目标收到后回复 NA,里面包含自己的 MAC 地址和一个标志位(O 位表示"请用这个新地址覆盖缓存里的旧值")。整个过程只影响组内的设备,不像 ARP 那样全链路广播。
这个改动带来两个实际影响。第一,抓包分析时不能再用arp过滤,要改用icmp6,命令是tcpdump -i eth0 icmp6,并且能看到neighbor solicitation和neighbor advertisement这两种类型。第二,ICMPv6 不能被整体封禁。很多团队沿用了 IPv4 时代"封掉所有 ICMP"的安全策略,结果 IPv6 上来之后邻居发现直接瘫痪,表现为"地址有、ping 不通、访问不了网关",排查半天才发现是防火墙规则。这个坑我在两个不同的环境里都遇到过。
另外 NDP 还有一个容易忽略的问题:邻居缓存表的大小和老化时间。IPv6 没有 ARP 表那种明确的容量概念,缓存条目过多时会挤掉旧的,如果链路上有大量短连接或者地址扫描,缓存频繁刷新会导致转发性能下降。部分系统提供了相关内核参数可以调,但更根本的办法是控制链路规模,别把上千台设备塞进同一个 /64。
5. 报文头的减法:40字节固定头与扩展头链
IPv6 的报文头设计思路和 IPv4 完全相反:IPv4 是"能塞进去的都塞",IPv6 是"能拿出去的全拿出去"。第一次看 IPv6 头结构的人往往觉得奇怪——怎么连校验和都没有了?分片字段去哪了?这些"缺失"不是疏忽,每一个都有明确的替代机制,理解这些取舍才能明白为什么 IPv6 在高速转发场景下效率更高。
5.1 去掉了哪些字段,为什么敢去
IPv6 的固定头是 40 字节,字段只有 8 个:版本(4 位)、流量类别(8 位)、流标签(20 位)、有效载荷长度(16 位)、下一个头(8 位)、跳数限制(8 位)、源地址(128 位)、目的地址(128 位)。对比 IPv4 的 20 字节基础头,少了这些:头长度、标识、标志、分片偏移、校验和、选项、填充。
校验和被去掉了,因为二层和四层都有校验,中间每一跳重新计算头部校验和是纯粹的浪费。这个改动在高端路由器上节省的处理开销相当可观——IPv4 转发时每跳都要重算,虽然硬件可以加速,但软件转发路径上这个成本很实在。
分片相关字段被去掉了,改为"只有源端可以分片"。中间路由器遇到超过 MTU 的包,不再自己分片,而是回一个 ICMPv6 的"包过大"报文(类型 2),里面带上链路的 MTU 值,让源端自己调整。这个机制叫路径 MTU 发现(PMTUD)。好处是路由器不用维护分片状态,转发逻辑简化;坏处是如果 ICMPv6 被防火墙拦掉,PMTUD 就断了,表现为"小包能通、大包卡死",比如 SSH 能连上但一传文件就挂。
头长度字段也没了,因为固定头就是 40 字节,没有选项就没有变长。
5.2 扩展头的链式结构怎么读
IPv4 的选项是塞在头里的,最长 40 字节,处理起来需要判断头长度,路径上的设备都得解析。IPv6 把扩展功能全部挪到固定头之后,用"下一个头"字段串成一条链。
每个扩展头自己也有一个"下一个头"字段,指向链上的下一个。链的终点是一个上层协议号,比如 6 表示 TCP、17 表示 UDP、58 表示 ICMPv6。常见的扩展头包括:
| 扩展头 | 下一个头值 | 用途 |
|---|---|---|
| 逐跳选项 | 0 | 需要每一跳都处理的选项,比如巨型载荷 |
| 路由 | 43 | 指定中间经过的节点 |
| 分片 | 44 | 源端分片时使用 |
| 目的选项 | 60 | 只给目的节点处理的选项 |
| 认证头 | 51 | IPSec 认证 |
| 封装安全载荷 | 50 | IPSec 加密 |
这个设计的巧妙之处在于:中间路由器只处理它需要处理的扩展头。绝大多数转发场景下,路由器只看固定头,直接按目的地址查表转发,不解析后面的链。只有逐跳选项头需要每一跳都看,所以它必须放在最前面。这种"按需解析"的思路,是 IPv6 在高吞吐场景下性能优势的来源之一。
不过链式结构也有代价。防火墙和抓包工具需要遍历整条链才能找到四层信息,如果链特别长(有些实现会插入多个扩展头),处理开销反而上升。更麻烦的是,某些设备对扩展头的处理不一致,导致本来合法的包被丢弃。实践中的建议是:除非必要,不要在路径上引入额外的扩展头,尤其是路由头,很多安全设备默认会丢弃带路由头的包。
注意:IPv6 规定链路最小 MTU 是 1280 字节。如果你的隧道或者虚拟网络封装导致实际 MTU 低于这个值,IPv6 会直接不可用,而不是像 IPv4 那样还能凑合。做 overlay 网络的时候这一条必须提前确认。
6. 手工配置与排查:命令行里最容易翻车的几个点
概念清楚了,落到操作上还是会有各种意外。我整理了几个在 Linux 环境下最常遇到的场景,都是实际排查中反复出现的,不是照抄手册能解决的问题。命令以iproute2工具集为准,ifconfig和route这些老工具在 IPv6 支持上不完整,不建议再用。
6.1 查看与临时配置的常用命令
先看当前状态:
# 查看所有接口的 IPv6 地址 ip -6 addr show # 只看某个接口 ip -6 addr show dev eth0 # 查看 IPv6 路由表 ip -6 route show # 查看邻居缓存(相当于 IPv6 的 ARP 表) ip -6 neigh show给接口临时加一个地址(重启失效):
ip -6 addr add 2001:db8:1:2::10/64 dev eth0加默认路由:
ip -6 route add default via 2001:db8:1:2::1 dev eth0测试连通性,ping在新版本系统里能自动识别 IPv6,老系统需要用ping6或者ping -6:
ping -6 -c 4 2001:db8:1:2::1追路由用traceroute -6或者tracepath6。注意 IPv6 的 traceroute 依赖 ICMPv6 的"超时"报文,如果中间设备把它拦了,结果会是一串星号。这不是链路断了,而是探测手段被挡住了,需要换用 TCP 模式或者其他方法验证。
排查时我习惯先确认三件事:地址有没有配上、路由有没有、邻居能不能解析。这三步对应三条命令,基本能定位到问题在哪一层。如果地址有、路由有、邻居有,但还是不通,那问题大概率在防火墙或者上层应用。
6.2 链路本地地址为什么要带接口名
这个细节几乎每个新手都会踩。链路本地地址fe80::开头的地址在同一个系统上可能出现在多个接口上,而且不同链路上的fe80::1是完全不同的地址。所以访问链路本地地址时必须指定接口,写法是在地址后面加百分号加接口名:
ping -6 fe80::1%eth0不加接口名,系统不知道你要走哪块网卡,直接报"无效参数"或者"网络不可达"。这个规则在配置文件里也一样,某些服务绑定链路本地地址时必须在地址后面带上%接口名。
另一个相关的问题是多宿主主机。一台机器有两块网卡,各自有链路本地地址,如果路由表里没有明确的条目,出站流量可能走错接口。这种情况在容器环境里特别常见——容器里的fe80::地址和宿主的完全不通,因为不在同一个链路。看到容器里ip addr有fe80地址就以为网络通了,是典型的误判。
6.3 防火墙与ICMPv6的取舍
这一块是最容易出安全事故的地方。IPv6 的防火墙规则和 IPv4 是分开的,ip6tables和iptables各自独立。很多团队上 IPv6 的时候只关注"能不能通",把ip6tables的默认策略设成 ACCEPT,结果所有服务在 IPv6 上都暴露了,而他们以为防火墙还在保护。
我的建议是反过来做:先把ip6tables的默认策略设为 DROP,然后按需要放行。必要放行的包括:
# 允许已建立的连接 ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许环回 ip6tables -A INPUT -i lo -j ACCEPT # 允许必要的 ICMPv6 类型(邻居发现、PMTUD 必需) ip6tables -A INPUT -p icmpv6 --icmpv6-type 133 -j ACCEPT # 路由请求 ip6tables -A INPUT -p icmpv6 --icmpv6-type 134 -j ACCEPT # 路由通告 ip6tables -A INPUT -p icmpv6 --icmpv6-type 135 -j ACCEPT # 邻居请求 ip6tables -A INPUT -p icmpv6 --icmpv6-type 136 -j ACCEPT # 邻居通告 ip6tables -A INPUT -p icmpv6 --icmpv6-type 2 -j ACCEPT # 包过大 ip6tables -A INPUT -p icmpv6 --icmpv6-type 1 -j ACCEPT # 目的不可达这几个类型是"封了必出问题"的。类型 133 到 136 支撑邻居发现,类型 2 支撑 PMTUD,类型 1 用于错误回传。至于回显请求(类型 128)和回显应答(类型 129),可以根据策略决定是否放行——很多人习惯封掉 ping,这在 IPv6 里同样可行,但要清楚封掉之后就少了最基础的连通性验证手段。
还有一个容易忽略的点:RA 报文应该只从预期的接口进来。如果一台主机同时接了内网和外网,攻击者可以在外网侧伪造 RA 把主机的默认路由改掉。防护手段是配置 RA Guard 或者在内核参数里限制接受 RA 的接口。这个风险在 IPv4 时代对应的是 ARP 欺骗,本质上是同一类问题。
7. 双栈共存阶段的几个真实教训
IPv6 不可能一夜之间替代 IPv4,绝大多数环境会长期处于双栈状态。双栈听起来简单——两个协议都开着,各走各的——实际上这里面的坑最多,因为两个协议栈的行为不完全一致,任何一个环节没对齐都会产生很难复现的问题。下面这几个教训都是我在实际环境里踩过的。
7.1 优先级与"能通但慢"
默认情况下,现代操作系统会优先使用 IPv6。这个策略本身合理,但前提是 IPv6 路径真的能通。我遇到过一个典型案例:某服务域名同时解析出 A 和 AAAA 记录,客户端的 IPv6 默认路由指向一个配置不全的网关,结果是"连接能建立但极慢",因为客户端先尝试 IPv6,超时之后回退到 IPv4,每次请求都多等几秒。用户感知就是"这系统怎么这么卡",但从监控上看各项指标都正常。
排查这类问题的关键是要能区分"协议层不通"和"应用层慢"。我的做法是先分别在纯 IPv4 和纯 IPv6 环境下测同样的请求,如果只有 IPv6 慢,那问题就在 IPv6 路径上。常见原因包括:默认路由指向的设备没有正确配置转发、路径 MTU 不一致导致分片、DNS 返回的 AAAA 地址对应的服务根本没监听。
反过来也有另一种情况:服务端只监听了 IPv4,但客户端优先走 IPv6,结果直接连不上。Linux 上可以通过net.ipv6.bindv6only参数控制监听套接字的行为,默认值在不同发行版上不一致,部署前必须确认。
7.2 只做了一半的安全策略
这是最危险的一类。团队花了很大精力把 IPv4 的防火墙规则梳理得很细,ACL、限流、入侵检测都配好了,然后"顺便"把 IPv6 打开。结果ip6tables是空的,默认策略是 ACCEPT,所有服务通过 IPv6 完全暴露。更糟的是,很多监控工具只采集 IPv4 的连接数,攻击流量走 IPv6 进来,监控上完全看不到。
我的做法是:在上 IPv6 之前,先把 IPv6 的默认策略设成 DROP,等安全策略补齐之后再逐个放行。这个顺序不能反。同时要把监控和日志的采集范围扩展到 IPv6,包括连接数、流量、访问日志,别让 IPv6 成为监控盲区。
还有一个细节是日志格式。有些日志系统在解析 IPv6 地址时会出错,因为地址里包含冒号,和某些分隔符冲突。日志采集链路上如果有这类问题,会导致 IPv6 的访问记录丢失或者错乱,事后追查时才发现没有数据。
7.3 自动配置带来的"地址太多不知道用哪个"
一台开了 IPv6 的机器,ip addr输出里可能同时有:链路本地地址、全局地址、临时地址、容器网桥地址,如果跑了虚拟化还有一堆veth接口的地址。新人看到这个会懵,不知道该用哪个。
判断逻辑其实很简单:对外提供服务用稳定的全局地址,主动访问外部用临时地址,链路本地地址只在本地链路内使用。具体来说,2000::/3开头的是全局地址,fe80开头的是链路本地,fd开头的是内网地址。带temporary标记的是隐私扩展生成的临时地址,deprecated标记表示该地址已过首选期不应再用于新建连接。
配置服务监听时,建议显式绑定到具体的全局地址,而不是::。绑定::会同时监听 IPv4 和 IPv6(在双栈套接字下),有时候会带来意料之外的暴露面。如果只想监听 IPv6,用::加bindv6only=1,或者直接绑定到具体的 IPv6 地址。
我在实际环境里还遇到过一个情况:某台机器的全局地址是通过 SLAAC 拿到的,前缀来自上游路由器的 RA,生命周期是 30 分钟。结果上游路由器重启后换了前缀,机器上旧地址进入 deprecated 状态,新地址还没做完 DAD,这中间的几十秒内新建连接全部失败。这类问题的根源是过度依赖自动配置,对于关键服务,静态配置地址加明确的默认路由是更稳的选择。
最后分享一个我觉得比较实用的小习惯:在给任何环境开 IPv6 之前,先用一台测试机完整跑一遍从接入到访问的全流程,包括地址获取、邻居解析、默认路由、DNS 解析(AAAA 记录)、应用监听、防火墙放行。整个链路走通之后再批量推开,比出问题之后回头补要省事得多。IPv6 的基础概念本身不复杂,复杂的是它和现有环境的每一个交接点,把这些交接点一个个确认清楚,后面的事情就顺了。