1. IPv6解决的问题:地址短缺之外的重构
1.1 地址焦虑与NAT的隐性成本
十年前我在一个机房里做设备上架,那时候给每台机器配地址是一件不太需要动脑的事情:一段私有网段,一个默认网关,剩下的交给地址转换设备。真正让人难受的是后面的事情——外网要访问内网某个服务,得做端口映射;内网几台机器要互相访问,还需要把映射规则想清楚;日志里全是转换后的地址,出问题排查时,先得搞清楚哪台机器对应哪条映射。地址转换确实把地址复用做到了极致,但代价是把"端到端可达"这个网络最朴素的能力给拆掉了。
这也是我理解IPv6的起点。很多人以为它只是把地址从32位拉长到128位,其实地址长度只是表象,真正变化的是整个网络的设计前提:每一台设备都可以拥有一个全球唯一的地址,而不必躲在转换设备后面。端点之间重新变成可以直接对话的两端,中间设备只需要做转发,不需要拆包改地址。这带来的直接好处是链路可追踪、配置可自动化、协议扩展不需要再叠一层封装。
从工程角度看,地址转换设备在家庭和小型办公场景里还会长期存在,但在数据中心、园区网、物联网设备接入这些场景里,转换层带来的状态维护、连接数上限、协议兼容问题(比如某些协议把地址写进了载荷)会持续消耗运维精力。IPv6的设计目标就是把这些隐性成本从根上去掉,而不是打补丁。
1.2 128位到底有多大,用数字感受一下
2的128次方约等于3.4乘以10的38次方,这个数字大到没法用日常单位形容。换个算法:地球陆地面积约1.5亿平方公里,折合约1.5乘以10的14次方平方米,把全部地址平摊下去,每平方米能分到大约2.2乘以10的24次方个地址。就算把每个网段都按每台设备一个地址来规划,也用不完。
但对我们做规划的人来说,真正有意义的不是总量,而是可分配的层级结构。IPv6的地址规划惯例是按位切分:运营商拿 /32,一个站点拿 /48,站点内部再按业务或楼层切 /56,单个子网固定用 /64。这套惯例不是强迫的,但沿用它能避免很多坑,后面讲自动配置的时候会看到,/64这个长度几乎是硬性要求。
具体换算一下:一个 /48 前缀里包含 2的16次方,也就是65536个 /64 子网。一个 /56 里有256个 /64。对一个中等规模的园区来说,一个 /48 完全够用,甚至还能给未来的新楼宇、新业务预留大量空间。我在实际规划里通常会把第一个 /64 留作基础设施网段,第二个留给办公终端,第三个开始按业务顺序往后排,留出一段空号便于以后插入。
提醒一句:不要试图在一个 /64 内部再做"子网划分"。除了少数点对点链路可以用 /127(RFC 6164)之外,把 /64 切成更小的段会直接破坏无状态地址自动配置,客户端拿不到可用地址,这类问题排查起来非常费时间。
1.3 协议层面的三处关键改动
地址变长只是第一层。第二层是首部简化:IPv6的基本首部长度固定为40字节,去掉了校验和字段,去掉了可选字段和填充,把分片信息挪到了扩展首部。中间路由器转发时只需要看基本首部,处理负担更轻。校验和之所以能去掉,是因为链路层和传输层本身已经有校验机制,重复校验的意义不大,反而增加了每跳的计算开销。
第三层是邻居发现机制的重构。原来的地址解析协议在IPv6里被邻居发现取代,走的是ICMPv6报文,功能也从"地址解析"扩展到了路由器发现、前缀通告、重复地址检测、重定向。这一改动让"插上网线就能拿到地址"成为协议内置能力,而不是靠额外的服务端。
第四层是组播成为基础能力。IPv6取消了广播,所有原本靠广播完成的事情都改由组播承担。这个变化在小网络里看不出什么,但在大型二层网络里能显著减少无关终端被打扰的情况。
这四点加起来,才是IPv6真正区别于前代的地方。我在给团队做内部培训时反复强调:不要把它当成"地址更长的IPv4",它有自己的一套运行逻辑,用旧习惯去配新协议,八成会踩坑。
2. 地址表示法:把一串冒号读明白
2.1 完整写法、压缩规则与合法边界
IPv6地址是128位,写成8组、每组4位十六进制数,用冒号分隔。一个完整形态长这样:2001:0db8:0000:0000:0000:0000:0000:0001。这么写太啰嗦,于是有两条压缩规则。
第一条,每组内的前导零可以省略,0db8写成db8,0001写成1。第二条,连续的一段全零组可以用一对冒号代替,但全文只能出现一次。上面那个地址压缩后是2001:db8::1。
这两条规则看似简单,但第一次写配置的时候特别容易出错。我见过最常见的三个错误:
- 在同一个地址里用了两次
::,比如2001::db8::1,这是非法写法,解析器会直接报错。真遇到两段不连续的全零,只能压缩其中一段,另一段老实写全零组。 - 把
::当成"省略了任意个组"的万能符号,结果数不清到底省略了几组。判断办法很简单:把::展开成若干个0000,直到总组数正好是8。 - 把结尾的单个组省略,写成
2001:db8:1:,这是不合法的。
还有一个细节值得记:地址里字母大小写不敏感,2001:DB8::1和2001:db8::1是同一个地址。设备上显示的时候通常统一小写,做日志比对时记得先归一化。
带前缀的写法是在地址后面加斜杠加位数,比如2001:db8:1::/64。这个位数表示前多少位是网络部分,剩下的位是接口标识。做地址规划时,前缀长度是我们唯一需要精心设计的参数,接口标识部分基本交给自动生成或者手工指定。
2.2 前缀长度怎么定:/32、/48、/56、/64 的分工
前缀长度的选择有一套事实标准,虽然协议层面没有强制,但全行业都这么用,跟着走最省事。
| 前缀长度 | 典型用途 | 内含 /64 子网数 | 说明 |
|---|---|---|---|
| /32 | 分配给运营商或大型组织 | 约43亿 | 通常由地址分配机构下发 |
| /48 | 一个站点、一个分支机构 | 65536 | 中小企业建议的最小规划单位 |
| /56 | 一个家庭或小型办公 | 256 | 部分接入场景的常见下发长度 |
| /64 | 单个二层子网 | 1 | 自动配置的硬性边界 |
| /127 | 点对点链路 | 不适用 | 避免地址浪费,见常用实践 |
| /128 | 单个主机地址 | 不适用 | 回环、主机路由 |
这里需要解释几个"为什么"。为什么单个子网一定要 /64?因为无状态地址自动配置的接口标识是基于64位设计的,如果子网前缀不是64位,客户端算出来的地址就无法正常拼装,很多实现会直接放弃配置。为什么点对点用 /127 而不是 /64?一条链路两端各一个地址,用 /64 会浪费掉一整段,虽然地址不值钱,但在做访问控制和路由聚合统计时,一堆只用了两个地址的 /64 会很碍眼。RFC 6164专门讨论了这个用法,主流实现都支持。
我自己做规划时会留一段"预留区":比如拿到2001:db8:abcd::/48,前16个 /64 留给基础设施(环回、链路互联、管理网),接着32个留给服务器区,再往后按业务线顺序切分,最后留出大约四分之一的空间不分配。这个习惯是从一次扩容踩坑之后养成的——当时业务增长需要新开网段,发现地址段被切得七零八落,中间没有连续空间,只能重新规划,连带改了一大堆访问控制规则。
2.3 常见地址段速查表
真正上手配置的时候,脑子里要有一张特殊地址的速查表,否则看到日志里冒出来的地址会一脸茫然。
| 地址段 | 名称 | 用途与注意事项 |
|---|---|---|
::/128 | 未指定地址 | 表示"还没有地址",不能配置在接口上 |
::1/128 | 环回地址 | 等价于 127.0.0.1,只有这一个 |
2000::/3 | 全球单播 | 当前实际分配使用的空间,约占全部地址的八分之一 |
fc00::/7 | 唯一本地地址 | 私有地址用途,实际常用fd00::/8 |
fe80::/10 | 链路本地地址 | 每个接口自动生成,只在单条链路上有效 |
ff00::/8 | 组播地址 | 替代广播,所有组播都在这段内 |
2001:db8::/32 | 文档示例 | 写文章、做实验专用,不可用于生产 |
64:ff9b::/96 | 协议转换知名前缀 | 与转换机制配合使用 |
2001:db8::/32这一段要特别记牢。它是专门保留给文档和示例的,在任何正式环境里看到这个前缀,基本可以判定配置没改干净。我做过一次配置审计,就在一台测试机转生产的过程中发现了残留的示例地址,虽然不影响功能,但会让排障的人误判归属。
链路本地地址则是最容易被忽略又最常用的一段。它的特点是自动生成、不需要任何配置、只在本地链路有效,不能跨路由器转发。后面讲邻居发现和路由通告时,几乎所有控制报文都是靠它来承载的。
3. 地址类型全解析:三个层次要看清楚
3.1 单播、组播、任播的差别
IPv6的地址在"通信模式"这个维度上分三类,理解这三类的差别比记住具体地址段更重要。
单播是一对一,最常见,标识一个具体的接口。前面说的全球单播、链路本地、唯一本地都属于这一类。
组播是一对多,一个地址标识一组接口,发往这个地址的报文会被组内所有成员收到。IPv6里没有广播,所有需要"通知所有人"的场景都用组播实现。这个设计的好处是,没有被订阅的终端根本不会被打扰,网卡层面就可以过滤掉。
任播是一对最近,一个地址被配置在多个不同位置的接口上,报文会被路由到其中"最近"的那一个。这个概念在旧协议里也有,但IPv6把它做成了常规能力。任播最典型的用途是让多个节点提供同一个服务入口,客户端不需要知道具体连的是哪一台。判断"最近"的标准由路由协议决定,通常是跳数或度量值。
这里有个实操上的认知差异值得说清楚:任播地址在语法上和单播地址完全一样,无法从地址本身判断它是不是任播。它只是一种使用方式,不是一个独立的地址段。所以做地址规划的时候,如果你想给某个服务用任播,只需要在多个节点上配置同一个地址,然后确保路由能把流量导向正确的节点,不需要申请特殊前缀。
3.2 全球单播与运营商分配链路
全球单播地址目前只使用了2000::/3这个范围,前缀前三位固定为001。在地址分配体系里,顶层机构向下分配的一般是 /12 或 /23 这样的块,运营商拿到 /32 左右,再往下给客户分 /48 或 /56。
这个层级结构对我们配置的影响是:看到地址的前几位就能大致判断它的来源和用途。比如2001:0:开头的地址块,分配历史比较久,很多早期参与方拿到的都是这个范围;2400:开头的在亚太区域比较常见。
但真正需要关心的不是这些背景知识,而是多前缀共存这件事。一台机器同时拥有运营商下发的全球地址、链路本地地址、可能还有唯一本地地址,这是完全正常的。应用层在绑定监听地址时如果只指定了其中一个,就会出现"从某个网络能通、从另一个网络不通"的怪现象。后面排查章节会专门讲这个问题。
另一个实际问题是前缀变更。部分接入场景下,运营商下发的前缀可能会变化,如果服务端把地址写死在配置里,前缀一变服务就失联。稳妥的做法是让服务监听在通配地址上,或者用脚本在地址变化时更新配置和域名记录。我在一个小型服务上就吃过这个亏,当时图省事把地址写进了反向代理配置,结果某次链路调整后整晚都在排查"为什么域名解析对了但连不上"。
3.3 链路本地地址:每台设备的"身份证"
链路本地地址固定在fe80::/10范围内,每个启用IPv6的接口都会自动生成一个。生成方式有两种:基于接口的物理地址推导(传统做法),或者随机生成(隐私保护做法)。它有几个非常实用的特性。
第一,不需要任何配置就能用。插上网线,接口起来,链路本地地址就有了。这在做设备调试时极其方便——两台机器直连,不用管地址规划,直接用fe80::开头的地址就能互相ping通。
第二,它是所有控制协议的基础。路由器通告、邻居请求、邻居通告,源地址基本都是链路本地地址。这意味着即使路由器还没有配置全球地址,网络也已经在正常工作了。
第三,它不能跨路由器转发。路由器收到目的地址是链路本地的报文会直接丢弃。所以它不是"内网地址"的替代品,只在单条链路内有效。
使用时的关键细节是作用域标识。因为同一个链路本地地址可能出现在多个接口上,所以必须用%接口名明确指定走哪个接口,比如ping fe80::1%eth0。这一点在脚本里特别容易漏,漏了就会报"地址无效"或者"网络不可达"。我在写自动化脚本时养成的习惯是:凡是链路本地地址,一律带上作用域后缀,宁可多写几个字符。
注意:部分系统在配置静态地址时会提示"链路本地地址由系统自动管理",手工添加会被拒绝。这不是bug,是设计如此。确实需要自定义时,通常要改接口标识生成策略,而不是直接加地址。
3.4 唯一本地地址与组播的实用细节
唯一本地地址在fc00::/7范围内,实际使用中基本都是fd00::/8。它的定位类似私有地址,用于内部网络,不会被路由到公网。区别在于,旧协议的私有地址需要配合地址转换才能出网,而唯一本地地址可以和全球地址同时存在于一个接口上,由系统根据目的地址自动选择源地址,不需要转换层。
我在内部服务互联的场景里比较喜欢用它:地址段稳定、不受运营商前缀变更影响、内部互通规则好写。代价是如果服务需要对外提供访问,还得额外配一个全球地址,或者在前置层做转发。
组播地址这一段,新手最容易记住的是两个固定地址:ff02::1表示链路上所有节点,ff02::2表示链路上所有路由器。这两个在抓包时出现频率极高,看到它们不用紧张,就是正常的发现通告。
还有一个必须提的组播地址叫被请求节点组播地址,格式是ff02::1:ff加上目标地址的后24位。它的作用是让邻居请求报文只发给"可能拥有这个地址"的那一小部分终端,而不是全链路广播。这正是组播替代广播的核心机制:把"喊给所有人听"改成"只喊给相关的人听"。在一个有几千台终端的二层网络里,这个设计能显著降低无关终端的中断处理次数。
组播地址的生存时间字段也有讲究:ff02里的2表示链路本地范围,ff05表示站点范围。范围字段决定了报文能被转发多远,写错范围会导致该到的地方没到,或者不该到的地方收到了。
4. 地址自动配置:插上网线就有地址
4.1 无状态自动配置的完整流程
这是IPv6里我认为最值得花时间理解的一块,因为它直接决定了"为什么我的机器拿不到地址"。
流程大致分四步。第一步,接口起来后,主机先生成一个链路本地地址,向链路上发一个路由器请求报文,问"谁是路由器"。第二步,路由器回应路由器通告,里面带着前缀、默认网关信息、生存时间、以及各种标志位。第三步,主机拿到前缀后,把自己的接口标识拼上去,形成一个候选的全球地址。第四步,在正式使用之前,主机做一次重复地址检测:向候选地址对应的被请求节点组播地址发一个邻居请求,如果没有别人回应"这个地址是我的",就认为地址可用,正式启用。
整个过程不需要任何服务端,也不需要在路由器上维护每个客户端的状态,"无状态"这个名字就是这么来的。
几个参数直接决定行为:
- 通告间隔:路由器定期主动发通告,默认周期在几百秒量级,同时有最大和最小间隔限制,避免所有路由器同时发包造成拥塞。
- 生存时间:地址的有效期和首选期是两个独立字段。首选期过了之后,地址还能用但不再用于新建连接;有效期过了才彻底失效。做地址迁移时这个机制很有用,可以让旧地址平滑退出。
- 标志位:有一个位告诉主机"地址可以自己配",另一个位告诉主机"其他配置(比如域名解析服务器)要找专门的服务获取"。这两个位组合出的四种情况,是排查问题的关键线索。
实际踩过的坑是:路由器上改了前缀,但没调整旧前缀的生存时间,导致客户端同时保留新旧两个地址,旧地址一直不失效。这时候从外部看,域名解析可能返回旧地址,连接自然失败。解决办法是迁移时先把旧前缀的生存时间调短,让它快速过期,再下线。
4.2 邻居发现:替代地址解析的那套机制
邻居发现本质上是一组ICMPv6报文,一共五类,分工明确。
| 报文类型 | 编号 | 作用 |
|---|---|---|
| 路由器请求 | 133 | 主机主动询问路由器信息 |
| 路由器通告 | 134 | 路由器下发前缀与参数 |
| 邻居请求 | 135 | 询问某地址对应哪个链路层地址,也用于重复检测 |
| 邻居通告 | 136 | 回应请求,或主动通告地址变更 |
| 重定向 | 137 | 告诉主机有更优的下一跳 |
和旧协议相比,最大的改进是地址解析不再用广播。旧协议里,要找一个地址对应的物理地址,得向全网喊一嗓子,所有终端都要处理这个请求。新的做法是算出一个被请求节点组播地址,只把请求发给可能相关的那一小撮终端,网卡层面就能过滤掉其余报文。
另一个改进是把路由器发现、前缀通告、地址解析、重复检测统一到一套机制里,不再需要额外的协议配合。代价是这套机制完全依赖ICMPv6,如果防火墙把ICMPv6全拦了,整个网络会瘫痪。这是我见过最典型的配置事故之一:有人按照旧习惯写了一条"禁止所有ICMP"的规则,结果所有终端拿不到地址,路由也建不起来,现象看起来像是"网络时通时不通",排查方向很容易跑偏。
正确的做法是按类型放行必要的ICMPv6报文,至少要让133、134、135、136、137这几类通过,另外路径MTU发现相关的"数据包过大"报文也必须放行,否则大包会被静默丢弃,表现为"网页能打开但传文件卡住"。
4.3 有状态配置与隐私扩展
无状态配置能解决地址问题,但解决不了"下发域名解析服务器地址"这类需求。这时候就需要有状态配置服务出场,它能在下发地址的同时把各种参数一并推给客户端。
两种方式不是互斥的,实际部署里常见三种组合:
- 纯无状态:只靠路由器通告,简单,适合终端多、变化快的场景。
- 纯有状态:全部参数由服务端下发,控制力强,适合需要精确管理租期的场景。
- 混合:地址自己配,其他参数找服务端要。这是很多园区的实际选择,兼顾了自动化和管理需要。
选择的时候想清楚一件事:是否需要按终端做精细的策略。如果需要给不同终端分配不同的解析服务器或者不同的租期,那只能走有状态;如果只需要"能用就行",无状态最省事。
隐私扩展是另一个绕不开的话题。传统做法里,接口标识由物理地址推导而来,这意味着一台设备的地址在全球范围内是稳定的,只要它换到另一个网络,前缀变了但后半段不变。这带来一个现实问题:行为可以被长期关联。隐私扩展的做法是定期生成随机的接口标识,并保留多个地址同时可用,让外部观察者难以长期跟踪同一台设备。
代价是地址不再稳定,做访问控制或者日志关联时不能靠接口标识来识别设备。我的建议是分场景:终端设备开启隐私扩展;服务器和网络设备用固定地址,便于管理和审计。不要一刀切。
5. 动手配一遍:从主机到路由
5.1 主机侧的基础检查命令
理论说再多不如自己配一遍。下面这套命令是我在任何一台机器上排查IPv6问题的固定流程,按顺序执行,基本能定位到问题在哪一层。
先确认协议栈是否启用。有些系统镜像默认关闭了IPv6,这时候所有后续操作都是白费:
# 查看是否被禁用,返回1表示已禁用 cat /proc/sys/net/ipv6/conf/all/disable_ipv6 # 临时启用(重启失效) sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 sudo sysctl -w net.ipv6.conf.default.disable_ipv6=0然后是地址和路由:
# 查看所有接口的IPv6地址 ip -6 addr show # 只看某个接口 ip -6 addr show dev eth0 # 查看IPv6路由表 ip -6 route show # 查看邻居缓存(相当于旧的地址解析表) ip -6 neigh show一个健康的接口至少应该有链路本地地址。如果连这个都没有,说明协议栈没起来或者接口没启用IPv6。如果有链路本地但没全球地址,说明路由器通告没收到,或者前缀没下发。
手工配置地址的写法:
# 添加地址,前缀64位 sudo ip -6 addr add 2001:db8:1::10/64 dev eth0 # 添加默认路由,网关用链路本地地址时需要指定接口 sudo ip -6 route add default via fe80::1 dev eth0 # 删除地址 sudo ip -6 addr del 2001:db8:1::10/64 dev eth0连通性测试用ping -6或者独立的ping6命令:
# 测链路本地地址必须带接口名 ping -6 fe80::1%eth0 # 测全球地址 ping -6 2001:db8:1::1 # 指定源地址,多地址场景下很有用 ping -6 -I 2001:db8:1::10 2001:db8:2::1-I这个参数在多前缀环境下是救命的。有些机器同时有多个地址,不加指定时系统会按一套选源规则挑一个,挑错了就会出现"目标能通但源地址不对,导致对端访问控制拒绝"的情况。
5.2 路由器侧的通告配置
要让下面的终端自动拿到地址,路由器或者三层设备需要发通告。Linux上最常用的是一个小工具,配置文件不长,但每一项都有讲究。
# 安装(不同发行版的包名略有差异) sudo apt install radvd配置文件大致长这样:
interface eth0 { AdvSendAdvert on; # 通告间隔,单位秒 MinRtrAdvInterval 200; MaxRtrAdvInterval 600; # 告诉客户端可以自己配地址 AdvManagedFlag off; AdvOtherConfigFlag on; prefix 2001:db8:1::/64 { AdvOnLink on; AdvAutonomous on; # 有效期与首选期,单位秒 AdvValidLifetime 86400; AdvPreferredLifetime 14400; }; # 下发解析服务器 RDNSS 2001:db8:1::53 { AdvRDNSSLifetime 600; }; };几个配置项的解释。AdvManagedFlag off配合AdvOtherConfigFlag on就是前面说的混合模式:地址自己配,其他参数找服务端。AdvAutonomous on表示这个前缀可以用于自动配置,关掉它客户端就不会用这个前缀生成地址。生存时间的设置决定了地址多久过期,做迁移时把它调小能加速旧地址退出。
这里有个前提必须满足:转发功能要打开,否则路由器不会发通告。这是内核的默认保护机制,防止一台普通主机被误当成路由器乱发通告。
# 开启转发 sudo sysctl -w net.ipv6.conf.all.forwarding=1 # 如果希望本机在转发开启的情况下仍然接受通告,需要单独设置 sudo sysctl -w net.ipv6.conf.eth0.accept_ra=2第二个参数是我踩过的最深的坑之一。默认情况下,一旦forwarding=1,内核就不再接受路由器通告。这在容器宿主或者多网卡机器上会导致"网卡明明连着,就是没有默认路由",现象非常迷惑。加一行accept_ra=2就能解决。
配置完后重启服务,然后在客户端上用ip -6 addr看有没有拿到地址,用ip -6 route看有没有默认路由。两样都有了,说明通告链路是通的。
5.3 服务端监听与访问控制
地址配通只是第一步,服务能不能被访问是另一回事。这里有两个高频问题。
第一个是监听的地址族。很多服务的默认配置写的是0.0.0.0,这只监听IPv4。要同时支持,得改成::,或者写两个监听指令。在Linux上,::默认是双栈的,同时接受v4和v6连接(前提是net.ipv6.bindv6only为0,这是默认值)。所以把监听地址从0.0.0.0改成::通常就够了。
验证监听状态:
# 查看IPv6监听端口 ss -6 -lntp # 对比看IPv4 ss -4 -lntp如果ss -6里没有对应端口,那就是服务没监听IPv6,改配置重启即可。这个检查放在"服务连不上"排查的第一步,能省掉大量时间。
第二个是访问控制规则。很多人写防火墙规则时只写了IPv4那套,IPv6默认策略是放行或者拒绝,取决于系统默认。稳妥的做法是两种地址族都显式配置,不要依赖默认值。
# 查看现有规则 sudo ip6tables -L -n -v # 用新一点的工具 sudo nft list ruleset必须放行的ICMPv6类型前面提过,这里给个最小集合:邻居请求、邻居通告、路由器请求、路由器通告、重定向、目的地不可达、数据包过大、超时、参数问题。前五类关系到能不能通信,后四类关系到能不能稳定通信。
sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 133 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 134 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 135 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 136 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 137 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 2 -j ACCEPT sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 3 -j ACCEPT数量不多,写一次基本一劳永逸。我一般会把这些规则打包成一个脚本,新机器部署时直接跑一遍,比事后排查省事得多。
6. 共存期的三种过渡思路
6.1 双栈:最稳也最主流的做法
双栈的意思是设备同时运行两套协议栈,同时拥有两种地址,应用根据目标地址选择走哪条路。这是目前绝大多数网络的实际状态,也是最不容易出问题的方案。
双栈的优势很直接:任何一方出问题,另一方还能兜底;不需要额外的转换设备;应用层不需要改代码,只要正确使用域名解析就行。
代价是运维复杂度翻倍:两套地址规划、两套访问控制规则、两套监控采集。而且要注意两者之间的优先级问题。当域名同时解析出两种地址时,客户端会按一套规则选择先用哪个。这套规则在多数系统上是可配置的,默认倾向于先用新协议。如果你的新协议链路质量不如旧的,可能会出现"能连通但体验差"的情况,这时候可以考虑调整优先级,或者干脆先把新协议链路质量调好。
我在实际部署里遵循的原则是:先把新协议的链路质量做到和旧的一致甚至更好,再考虑调整优先级。顺序反了,用户会先感受到问题,然后对新协议产生抵触,后面再推就难了。
6.2 隧道与协议转换:只在必要的时候用
隧道解决的是"中间一段网络不支持新协议"的问题。思路是把新协议的报文封装在旧协议里,穿过不支持的区域,到了对端再解封。常见的封装方式有几种,配置上大同小异,都是在两端建立一条虚拟链路,然后在这条链路上跑新协议。
隧道的问题在于它会引入额外的封装开销,降低有效载荷,同时增加了故障点:两端任意一端配置不一致,链路就断。而且封装后的报文在网络中间设备看来是另一种协议,可能被访问控制规则误拦,也可能触发分片问题。我在实际项目里对隧道的态度是:只在没有其他办法的时候用,而且要在两端做好监控,一旦发现质量下降就尽快切回双栈。
协议转换的思路则是在两种协议之间做翻译,让只支持一种协议的终端能访问另一种协议的服务。这需要在中间设备上做地址和报文的映射,配置上有专门的知名前缀。它的好处是不需要终端改造,坏处是有些协议会把地址写进应用层载荷里,翻译之后对不上,导致部分功能异常。所以这类方案通常只用于"让老终端访问少数几个服务"的过渡场景,不作为长期架构。
6.3 迁移顺序怎么排
迁移不是一次性的开关,而是一个有先后的过程。我习惯的顺序是:先做基础设施的可达性(地址规划、路由、监控),再做内部服务的双栈,然后是出口,最后是终端。
理由是这样的。基础设施先行,后面所有步骤都有观察手段;内部服务双栈,可以先在内部验证应用兼容性,发现问题影响范围可控;出口最后做,因为一旦出口通了,外部的访问路径就变了,回退成本最高;终端放最后,因为终端数量大、场景杂,需要前面几步都稳定之后再推。
每一步都要设定明确的验收标准和回退方案。验收标准不能只是"能ping通",至少要看:新建连接成功率、连接建立时延、大包传输是否正常、域名解析返回的地址族是否正确。回退方案要提前写好,别等到出事才想怎么退。
7. 常见问题与排查实录
7.1 问题速查表
下面这张表是我这几年攒下来的,基本覆盖了日常遇到的八成情况。排查顺序建议从上往下,先排除最简单的。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 接口没有链路本地地址 | 协议栈被禁用或接口未启用 | 查内核参数,查接口状态 |
| 有链路本地地址但无全球地址 | 未收到通告,或前缀配置错误 | 抓包看有无通告报文,检查路由器配置 |
| 有地址但无默认路由 | 通告里未带路由信息,或转发未开启 | 查内核转发开关,查通告配置 |
| 能ping通地址但域名访问失败 | 解析返回了不可达的地址族 | 检查解析结果,检查源地址选择 |
| 小包正常大包卡死 | 路径MTU发现报文被拦截 | 放行"数据包过大"类ICMPv6 |
| 服务本机能连外部连不上 | 监听地址只绑了单一地址族 | 查看监听端口,改成通配地址 |
| 时通时不通 | 存在重复地址,或通告冲突 | 查邻居缓存,查是否有多个通告源 |
| 网关是链路本地地址时路由不通 | 路由未指定出接口 | 路由条目补上接口名 |
7.2 抓包与分层排查思路
遇到说不清的问题,抓包是最快的。我的习惯是一次抓两类:控制面报文和业务面报文,分开看。
# 抓取ICMPv6控制报文,看发现流程是否正常 sudo tcpdump -i eth0 -n 'icmp6' # 抓取某个主机的全部流量 sudo tcpdump -i eth0 -n host 2001:db8:1::10 # 抓取某端口,验证握手是否完成 sudo tcpdump -i eth0 -n 'ip6 and tcp port 443'看控制面报文的时候,重点看三件事:通告报文有没有周期性出现、前缀是不是预期的、标志位和生存时间是不是合理。如果通告压根没出现,问题在路由器侧;如果出现了但客户端没用,问题在客户端侧或者中间有拦截。
看业务面报文的时候,重点看握手有没有完成。如果看到请求发出去了但没有回应,说明对端没收到或者回不来;如果看到回应了但连接没建立,多半是中间有设备在干扰。这时候对比两个方向的报文,很容易定位到断点在哪一跳。
分层排查的顺序我一般这样走:先确认本机地址和路由,再确认同链路可达,再确认跨链路可达,最后确认应用层可达。每一层用最直接的工具验证,不要跳步。跳过中间层直接查应用,往往会在一堆无关的方向上浪费时间。
7.3 几个踩过的坑和对应的习惯
第一个坑是默认关闭。很多云主机镜像或者精简系统默认禁用了IPv6,表现出来就是"怎么配都不生效"。现在我拿到新机器第一件事就是查那个内核参数,确认是0才开始后面的操作。花十秒钟,省半小时。
第二个坑是转发与通告的互相影响。前面提过,开启转发会关闭通告接收,这个默认行为坑过我不止一次。现在的习惯是:凡是配置了转发的机器,都在配置文件里显式写上accept_ra=2,并且写注释说明原因,免得后来的人看到觉得多余给删了。
第三个坑是多地址场景下的源地址选择。一台机器同时有多个地址,系统按一套规则挑选源地址,挑错了会触发对端的访问控制。排查这类问题的办法是在目标端看日志里记录的源地址,然后回本源端核对。彻底解决要靠调整地址的优先级标记,让期望的地址优先被选中。命令是ip -6 addr change加上preferred_lft或者优先级参数,具体写法各系统略有差异,配之前先查一下当前优先级。
第四个坑是日志里的地址格式不统一。同样一个地址,有的系统输出压缩形式,有的输出完整形式,有的大小写不一致。做日志关联之前先做归一化处理,否则匹配不上。写脚本的时候我会加一步标准化:全部转小写、展开压缩、去掉前导零,然后再比对。
第五个坑是监控采集缺失。新协议上线后,原来的监控只采集旧协议指标,导致新链路出问题完全看不到。现在我推动任何网络变更,第一件事是把监控埋点加上,包括地址存活、路由数量、连接成功率、时延分布。没有观测的变更等于在黑暗里开车。
一个容易被忽略的细节:做地址规划时,把每一段的用途、负责人、分配时间记在一张表里,并且纳入版本管理。我见过太多"这段地址是谁在用"的疑问,最后靠翻邮件和问人才搞清楚。表格不复杂,几列就够,但它能省掉未来无数次扯皮。
最后分享一个我自己的检查习惯:任何IPv6配置改完之后,不看配置文件,直接用命令查实际生效的状态。配置文件和实际状态不一致的情况太常见了——配置写对了但服务没重启、参数被更高优先级的配置覆盖、模板渲染时变量没替换。以实际状态为准,是唯一靠谱的验收方式。