IPv6-重定向、PMTU、GRE隧道这三个词被摆在一起,通常不是教材的章节安排,而是某次排查中被同时撞上的三个坑。我在处理跨站点网络互联时最怕遇到的组合就是:隧道建起来了、小包能 ping 通、路由表看着也没问题,但一传大文件就卡死,SSH 登录到一半没反应,某个业务页面转圈直到超时。这类现象背后,IPv6 的重定向机制、路径 MTU 发现(PMTU)以及 GRE 隧道封装开销三者往往是连在一起的:隧道改了路径,路径决定了 MTU,MTU 触发 ICMPv6 差错报文,而差错报文和重定向又都可能被策略拦掉或误信。
这篇内容讲的就是这三件事怎么咬合在一起,以及怎么把它们一个个拆开、验证、落地配置。适合已经能配通隧道、但对"为什么大包过不去"说不清楚的网络运维和系统工程师;也适合刚接触 IPv6、只会照抄配置模板的新手——我会把 MTU 的每一字节开销都算给你看,把验证命令按顺序列出来,照着敲就能复现。全文的地址一律使用标准文档保留段,配置基于通用开源系统上的 iproute2 工具集,实际环境里换掉地址和接口名即可。
1. 这三个词为什么会出现在同一个故障现场
1.1 一次"能通小包、通不了大包"的现场还原
先说一个典型的现场。两端各有出口设备,中间隔着一段不属于自己管辖的网络,为了把两侧的 IPv6 网段连起来,在两端设备之间拉了一条 GRE 隧道,隧道接口上配了 IPv6 地址,两侧内网路由指向隧道。配置完成,ping -6通,traceroute也正常,邻居关系一度看起来没问题。
然后业务上线,问题来了:SSH 能连上但敲命令没回显,HTTP 请求发出去了响应回不来,某些 API 调用超过几秒就断。抓包会看到一个很有规律的现象——握手阶段的小包全部成功,一旦进入数据传输阶段,特定大小的报文就开始消失,发送方反复重传,最后超时。这时候如果去看隧道接口的 MTU,多半还是默认值;如果去看防火墙策略,多半只放行了"必要的"协议,ICMPv6 差错报文被当作"诊断流量"挡在外面。
这三个词在这类现场里的分工非常明确。GRE 隧道决定了这条路径上真正可用的 MTU 是多少——封装要占字节,占完剩下的才是内层可用的;PMTU 决定了当报文超过这个值时,网络该怎么通知发送方;IPv6 重定向则可能在隧道建好之后,悄悄把某台主机的下一跳改回物理接口,让流量绕开隧道走一条你以为不存在的路径。三者任意一个出问题,表现出来的症状都可能是"时通时不通",非常容易被误判成链路质量或者应用层问题。
我个人的经验是:只要涉及隧道,先把 MTU 账算清楚,再把 ICMPv6 的放行策略理一遍,最后才去看路由和重定向。顺序反过来,往往会在路由表里绕很久,最后发现问题根本不在路由上。
1.2 三个机制在报文路径上的分工
把一条报文从主机 A 送到主机 B 的完整过程摊开看,三个机制作用的位置完全不同。
GRE 隧道作用在封装层。它在原始报文外面再套一层 IP 头和 GRE 头,外层头由隧道两端负责解释,中间网络只看外层。这意味着中间网络看到的最大报文长度限制,是针对"原始报文 + 封装开销"的,而不是针对原始报文本身。这是所有隧道类技术的共性,也是 MTU 问题的根源。
PMTU 作用在传输层与网络层的交界。IPv6 规定中间路由器不再对报文分片,只有源端可以分片。于是当一台路由器发现报文超过了出接口的 MTU,它唯一能做的是丢弃并回一个 ICMPv6 的"报文过大"报文,让源端自己把报文改小。这个机制能不能生效,完全取决于那个差错报文能不能回到源端。
IPv6 重定向作用在最后一跳的选择上。它不改封装,也不改 MTU,它改的是主机的下一跳缓存。当一台路由器发现某个主机把报文发给了自己,但报文的目的地其实有更直接的第一跳(同链路上的另一台路由器),它会发一个重定向报文告诉主机"下次直接发给那台"。在多出口、双上行、隧道旁挂的环境里,这个"好意"很容易让流量跑到预期之外的地方去。
三者的关系可以这么理解:隧道把路修窄了,PMTU 是路边的限高提示牌,重定向是路口的热心人给你指了另一条道。提示牌被别人摘了,你就会一直撞限高杆;热心人指错了道,你就绕远了还不知道。
2. IPv6重定向:善意的指路与它的代价
2.1 ICMPv6 Type 137 报文里装了什么
IPv6 的重定向由 ICMPv6 的 Type 137 报文承载,报文类型名叫 Redirect。它的结构里除了常规的 ICMPv6 头(类型、代码、校验和),还有三个关键字段:保留位、目标地址(Target Address)、目的地址(Destination Address),后面跟着可选的选项字段。
目标地址是"你下次应该发给谁"——也就是更优的下一跳;目的地址是"对于去往这个目的地的流量"——限定了这条建议的适用范围。这两个字段组合起来,等价于一条主机路由:去往 Destination Address 的流量,下一跳改成 Target Address。选项字段里通常包含目标链路层地址,让主机不用再发一次邻居请求就能直接封装。
这个设计很省事,但也意味着一条重定向报文本质上就是一次"远程修改主机路由表"的操作。主机收到之后如果直接采信,路由缓存就被改写了。所以在受控环境里,这个报文既可以用来优化路径,也可以被用来做流量牵引,甚至做中间人。这就是为什么边界设备、服务器、以及任何开启了转发的设备上,通常都建议把它关掉。
还有一点容易被忽略:ICMPv6 的重定向报文本身没有认证机制。主机无法验证这条建议是不是真的来自当前的第一跳路由器,只能靠源地址检查和跳数限制(Hop Limit 必须为 255)做基本校验。这两道校验能挡住跨网段伪造,但挡不住同链路上的伪造。
2.2 触发条件、主机接受策略与关闭场景
路由器什么时候会发重定向?条件比想象中严格。收到的报文必须是从某个接口进来、又从同一个接口出去(也就是这台的设备对这个目的地来说不是必经之路);报文源地址必须和它查到的更优下一跳在同一个链路上;那个更优下一跳不能就是报文源本身;而且这条重定向不能指向默认路由器自己。这几个条件同时满足,才会发。
主机侧接受与否由内核参数决定。在通用开源系统上,IPv6 的重定向接受开关是net.ipv6.conf.all.accept_redirects和net.ipv6.conf.<接口>.accept_redirects,默认通常是开启的。但这里有个隐含前提:一旦设备开启了转发(net.ipv6.conf.all.forwarding=1),内核在转发路径上就不会处理重定向报文了,因为它自己就是路由器,不应该再听别的路由器指路。
把常用参数和适用场景整理一下:
| 参数 | 常见默认值 | 建议值(主机) | 建议值(路由器/边界) |
|---|---|---|---|
net.ipv6.conf.all.accept_redirects | 1 | 1(受控内网)或 0 | 0 |
net.ipv6.conf.default.accept_redirects | 1 | 跟随 all | 0 |
net.ipv6.conf.all.forwarding | 0 | 0 | 1 |
net.ipv6.conf.all.accept_ra | 依发行版 | 1(需要自动配置时) | 0(手工配地址) |
关闭的操作很直接:
sysctl -w net.ipv6.conf.all.accept_redirects=0 sysctl -w net.ipv6.conf.default.accept_redirects=0 # 持久化写进 /etc/sysctl.d/ 下的配置文件注意:关闭重定向接受之后,如果网络里确实存在双出口且依赖重定向来优化路径,主机会一直走默认网关,可能造成次优路径。这不是"错",只是要多绕一跳。判断标准是:这条路径的额外延迟你能不能接受。能接受就关掉换取可预测性,不能接受就得用策略路由手工指定,而不是依赖重定向。
在隧道场景里,重定向最典型的副作用是"流量旁路"。假设主机 A 与隧道端点在同一网段,A 的默认网关是隧道端点 R1,但同网段里还有另一台路由器 R2 也能到达目的网段。R1 转发 A 的报文时会发现"你这个目的地让 R2 转更直接",于是发重定向。A 采信之后,去往该目的地的流量就绕过隧道走 R2 了。如果 R2 那条路径的 MTU 或策略与隧道不同,就会出现"一部分业务正常、一部分业务异常"的割裂现象,而且从 A 的路由表主表里根本看不出变化,因为改写的是缓存条目。
排查这类问题的办法是看主机的路由缓存或直接抓 ICMPv6 报文:
# 抓重定向报文 tcpdump -ni eth0 'icmp6 and ip6[40] == 137' # 查看去往某目的地实际选中的下一跳 ip -6 route get 2001:db8:200::10ip -6 route get的输出里会带上它最终选中的网关和出口,如果这个网关不是你预期的那个,那就是缓存被动过了。
3. PMTU:IPv6世界里路由器不再帮你分片
3.1 Packet Too Big 报文与源端的分片责任
IPv6 和 IPv4 在分片这件事上的态度完全不同。IPv4 里中间路由器遇到超出出接口 MTU 的报文,只要 DF 位没置位,它可以自己切一刀分片转发。IPv6 把这个能力拿掉了:中间路由器一律不分片,超了就丢,然后回一个 ICMPv6 Type 2,报文名就叫 Packet Too Big,里面带一个 MTU 字段,告诉源端"下次别超过这个数"。
这个设计的好处是把分片责任收敛到源端和目的端,中间设备不用维护分片状态,转发效率更高,也不会出现分片重组带来的攻击面。代价是把可靠性押在了差错报文能不能送达上。一旦差错报文被丢弃,源端就永远不知道要改小,会一直重传同一个超大报文,形成所谓的 PMTU 黑洞。
MTU 字段有个下限要求:不能小于 1280 字节。这是 IPv6 规定的最小链路 MTU,所有实现都必须能转发 1280 字节的报文。所以在 IPv6 里配隧道,隧道接口的 MTU 如果被算到 1280 以下,等于直接违反协议要求,很多实现会拒绝或者行为异常。这条红线在做嵌套隧道时必须反复确认。
源端收到 Packet Too Big 之后该做什么,RFC 里有明确的建议:更新该目的地的 PMTU 缓存,减小发送尺寸,并且后续报文继续按新的尺寸发。同时源端应该周期性地尝试增大,以便路径变化后能恢复吞吐。实际实现里这个"周期性尝试"做得很保守,很多系统只在新连接建立时才重新探测,所以一次错误的 PMTU 缓存可能影响很长时间。
3.2 PMTU黑洞的三种成因
PMTU 黑洞在实际网络里出现的频率远高于理论预期,成因基本可以归成三类。
第一类是差错报文被策略拦截。很多安全策略的思路是"只放行必要流量",而 ICMPv6 被笼统地当作诊断协议挡掉。问题在于 ICMPv6 不只是 ping,它承担了邻居发现、地址自动配置、差错通知等基础功能。把 Type 2 挡掉,等于把整条路径的 MTU 反馈通道切断了。IPv4 里类似的问题是挡 ICMP Type 3 Code 4(需要分片),IPv6 里就是 Type 2。
第二类是路径上存在非对称转发。报文去程走一条 MTU 较大的路径,回程走一条 MTU 较小的路径,差错报文从回程方向发回来,中间某一跳把它丢了。或者去程和回程的出口设备不同,差错报文回来的接口不是发出接口,被反向路径检查拦下。这类问题的特点是"单向大包不通",用 ping 测的时候如果只测一个方向,很容易漏掉。
第三类是应用层不响应。有些自定义协议或者老旧的 UDP 应用根本不处理 ICMP 差错,收到 Packet Too Big 也无动于衷,继续按原尺寸发。TCP 有内核兜底,UDP 就只能靠应用自己做。现在常见的基于 UDP 的传输协议大多实现了自己的探测机制,但并不是全部。
提示:判断是不是 PMTU 黑洞,最快的办法是找一个确定超大的报文,用不分片的方式发过去,同时在两端和中间关键节点抓 ICMPv6 Type 2。如果发送端一直在重传同样的尺寸、而中间节点确实发了 Type 2 但发送端没收到,那基本就锁定了。抓包过滤器写
icmp6 and ip6[40] == 2。
3.3 Linux侧影响PMTU行为的开关
在通用开源系统上,和 PMTU 相关的可调项主要有几个,理解它们各自的作用范围很重要,不要一股脑全改。
net.ipv4.tcp_mtu_probing控制 TCP 的 MTU 探测行为,取值 0/1/2。0 是关闭,完全依赖 ICMP 反馈;1 是在检测到重传超时后启用探测;2 是一开始就启用。开启探测的好处是在 ICMP 被挡的环境里 TCP 能自己找到合适的尺寸,代价是要多发一些探测报文,且探测过程本身有延迟。这个参数名字里带 v4,但对 IPv6 上的 TCP 连接同样生效,因为它是 TCP 层的行为。
路由上的mtu属性可以用来手工锁定某个目的地的 PMTU。当你知道某条路径的实际 MTU 且不指望 ICMP 反馈时,这是最直接的办法:
# 锁定去往某目的网段的 PMTU ip -6 route add 2001:db8:200::/64 via 2001:db8:100::2 dev gre1 mtu 1400 # 或者改一条已有路由 ip -6 route change 2001:db8:200::/64 mtu 1400接口的mtu属性决定这个接口发出的报文最大长度。隧道接口的 MTU 必须手工设成小于物理接口 MTU 减去封装开销的值,否则内核会按物理接口的 MTU 来发,封装之后直接被外层链路丢弃。
advmss是路由属性里控制 TCP MSS 通告值的,配合 MTU 使用能避免对端按过大的尺寸发。它只影响 TCP,对 UDP 无效。
# 查看当前某条路由的完整属性 ip -6 route show 2001:db8:200::/64 # 查看接口 MTU ip -d link show gre1| 参数/属性 | 作用层 | 影响范围 | 使用建议 |
|---|---|---|---|
tcp_mtu_probing | TCP | 全局 | ICMP 不可控时设为 1 |
接口mtu | 链路 | 单接口 | 隧道接口必须手工设 |
路由mtu | 网络 | 单个前缀 | 已知路径 MTU 时锁定 |
路由advmss | TCP | 单个前缀 | 配合 MTU 使用 |
tcp_base_mss | TCP | 全局 | 探测起始值,一般不动 |
一个常见的误区是把隧道接口 MTU 设得很小以求"保险"。设成 1280 确实不会丢包,但所有经过这条隧道的 TCP 连接 MSS 都会被压到 1220 左右,吞吐直接打折,而且大量小包让封装效率变得很难看。正确做法是算出精确值,宁可贴着上限也不要过度保守。
4. GRE隧道:先把封装开销算清楚再动手
4.1 GRE头长度与可选字段
GRE 的全称是通用路由封装,它的定位很朴素:把一种协议的报文当成载荷,塞进另一种协议的报文里传。外层可以是 IPv4 也可以是 IPv6,内层同样可以随意组合,这就给了四种基本组合。
基础的 GRE 头是 4 个字节,里面有一个标志位字段、一个协议类型字段。协议类型告诉对端"我里面装的是什么",比如 0x0800 是 IPv4,0x86DD 是 IPv6。除了这 4 个字节,GRE 还支持三个可选字段,每个 4 字节:校验和(C 位置位时出现)、密钥(K 位置位时出现)、序列号(S 位置位时出现)。这三个字段不是必须的,但对端必须能识别——如果一端发了带 Key 的报文而另一端没配 Key,报文会被丢弃,隧道看起来就是"起来了但不通"。
所以在 Linux 上配置时,key参数要么两端都配且一致,要么两端都不配。校验和字段在现代实现里基本不用,因为外层 IP 头本身就有校验和,再加一层纯属浪费。序列号字段用于乱序检测,在有多个外层路径的场景下有意义,但在点对点隧道里通常也不需要。
外层 IP 头是固定的:外层用 IPv4 时是 20 字节(不含选项),外层用 IPv6 时是 40 字节。这 20 字节的差异在 MTU 计算里非常关键,很多人算错就是因为默认按 IPv4 的 20 算,结果外层换成了 IPv6 就差了 20 字节。
所以最小开销公式是:
封装开销 = 外层IP头长度 + GRE基础头(4) + 可选字段长度之和举例,IPv6 over IPv4 的 GRE 隧道,不开可选字段,开销 = 20 + 4 = 24 字节。物理链路 MTU 是 1500,那么隧道接口可用的 MTU 就是 1476。
4.2 四种内外层组合的MTU对照
把四种组合的开销和隧道 MTU 都算出来,做成表放在手边,配置的时候直接查,比每次现算靠谱。
| 内层载荷 | 外层封装 | 外层IP头 | GRE头 | 总开销 | 物理1500时隧道MTU |
|---|---|---|---|---|---|
| IPv4 | IPv4 | 20 | 4 | 24 | 1476 |
| IPv6 | IPv4 | 20 | 4 | 24 | 1476 |
| IPv4 | IPv6 | 40 | 4 | 44 | 1456 |
| IPv6 | IPv6 | 40 | 4 | 44 | 1456 |
表里要注意一个容易混淆的点:隧道接口的 MTU 指的是内层报文的最大长度,也就是隧道接口"看起来"能发多大的包。它的计算只跟外层头和 GRE 头有关,跟内层是 IPv4 还是 IPv6 无关。内层是 IPv6 还是 IPv4 影响的是这个 MTU 够不够用——IPv6 要求至少 1280,IPv4 没有这个硬性要求。
如果开了 Key 字段,每种组合再减 4;同时开 Key 和序列号,减 8。这个减法必须做,否则大包会以极低的概率、在特定长度上丢,非常难查。
再往下算一层,TCP 的 MSS 该怎么设。MSS 是 TCP 载荷的最大长度,等于 MTU 减去 IP 头减去 TCP 头。以 IPv6 over IPv4 GRE、隧道 MTU 1476 为例:
MSS = 1476 - 40(IPv6头) - 20(TCP头) = 1416而如果主机不知道隧道存在,按物理口 1500 去算,MSS 会是 1500 - 40 - 20 = 1440,加上头就是 1500 字节,封装后变成 1524,超过物理链路 1500,直接被丢或者触发 Packet Too Big。这就是"握手正常、传数据卡死"的经典成因:握手包很小,怎么都能过;数据包填满 MSS,一过隧道就超。
4.3 隧道叠加与1280字节红线
比单层隧道更麻烦的是隧道叠加——在一条隧道里再跑一条隧道。这种结构在跨多个管理域的互联里并不罕见,每一层都要扣一次开销。
假设物理 1500,第一层是 IPv6 over IPv4 GRE,扣 24,剩 1476。第二层在这条隧道上再建一条 IPv4 over IPv6 GRE,外层是 IPv6(40 字节),扣 44,剩 1432。看起来还行。但如果第一层的外层换成 IPv6,第一层扣 44 剩 1456,第二层再扣 44 剩 1412。三层叠加就接近 1360 了。
再考虑某些链路本身 MTU 就不是 1500,比如某些承载网络给的是 1400 或者 1280。如果物理链路只有 1400,单层 IPv6 over IPv6 GRE 扣 44 剩 1356,勉强过 1280 红线;再叠一层就掉到 1312,第三层直接跌破 1280,IPv6 内层就跑不起来了。
注意:IPv6 的 1280 字节最小 MTU 是硬性要求,不是建议值。隧道接口 MTU 低于 1280 时,IPv6 的邻居发现、地址自动配置、甚至基本的 TCP 握手都可能出问题,表现为"隧道 up 但完全不通"。做多层设计时,每一层的 MTU 都要写进设计文档,而不是配完再看。
缓解手段有两个方向。一个是压缩封装:能不用 Key 就不用,外层尽量选 IPv4(省 20 字节)。另一个是降低内层协议的 MSS 期望值,同时对 UDP 类流量做应用层的尺寸控制。如果两条路都走不通,就得考虑换一种承载方式,而不是硬扛。
5. 实操:搭一条IPv6 over IPv4的GRE隧道并验证PMTU
5.1 拓扑、地址规划与前置检查
为了能完整复现,我按最常见的场景设计拓扑:两台设备 A 和 B 分处两个网络,中间隔着一段只能跑 IPv4 的承载网。两侧各有一个 IPv6 内网段,目标是让两个内网段互通。
地址规划全部使用标准文档保留段,实际部署时换成自己的:
| 用途 | 设备A | 设备B |
|---|---|---|
| 承载网接口(IPv4) | 192.0.2.1/24 | 198.51.100.2/24 |
| 承载网互联地址段 | 192.0.2.0/24 与 198.51.100.0/24 之间假设可达 | 同左 |
| 隧道接口(IPv6) | 2001:db8:100::1/64 | 2001:db8:100::2/64 |
| 内网段(IPv6) | 2001:db8:200::/64 | 2001:db8:300::/64 |
| 物理接口 MTU | 1500 | 1500 |
配置前先做三项前置检查,这三项漏掉任何一项,后面都会白折腾。
第一,确认两端 IPv4 地址互相可达,这是外层承载的前提。第二,确认物理接口实际的 MTU 是多少,不要假设一定是 1500,用ip link show看,承载网如果中间经过了别的隧道,实际值可能更小。第三,确认两端对 GRE 协议(IP 协议号 47)没有策略拦截,中间如果有策略设备,需要放行这个协议号,而不是某个端口。
# 前置检查 ip -4 addr show ip link show eth0 | grep -o 'mtu [0-9]*' ping -c 3 198.51.100.25.2 两端接口配置与MTU落地
先在 A 上建隧道接口。用 iproute2 的ip link add ... type gre语法,外层地址用local和remote指定,ttl显式设成 64,避免某些实现默认 0 导致外层 TTL 从 0 开始、被中间设备直接丢弃。
# 设备A ip link add gre1 type gre local 192.0.2.1 remote 198.51.100.2 ttl 64 ip link set gre1 mtu 1476 up ip -6 addr add 2001:db8:100::1/64 dev gre1# 设备B ip link add gre1 type gre local 198.51.100.2 remote 192.0.2.1 ttl 64 ip link set gre1 mtu 1476 up ip -6 addr add 2001:db8:100::2/64 dev gre1这里的 MTU 1476 是算出来的,不是拍的:1500 减去外层 IPv4 头 20 减去 GRE 基础头 4。如果配置里加了key,比如key 100,那 MTU 就要改成 1472。这一步必须在建接口之后、加地址之前做,否则内核会用默认值先走一遍邻居发现,可能留下错误的缓存。
还有个细节:ip link add默认会把隧道接口当作点对点设备,加 IPv6 地址时用 /64 是为了让两端在同一前缀下,实际点对点链路用 /127 更规范,能避免同网段地址扫描之类的干扰。如果改成 /127,两端地址分别是 2001:db8:100::0/127 和 2001:db8:100::1/127,邻居发现的开销也更小。
配完先确认状态:
ip -d link show gre1 ip -6 addr show dev gre1ip -d会把隧道参数全部打出来,包括 local、remote、ttl、key。这个输出建议存一份,后面排查时对照。
5.3 路由、MSS钳制、重定向开关
隧道接口通了,接下来要把内网路由指过去,同时处理 MSS 和重定向。
# 设备A:去往对端内网走隧道 ip -6 route add 2001:db8:300::/64 dev gre1 mtu 1476 advmss 1416 # 设备B:对称配置 ip -6 route add 2001:db8:200::/64 dev gre1 mtu 1476 advmss 1416mtu 1476是给这条路由锁定 PMTU,advmss 1416是让经过这条路由的 TCP 连接在 SYN 里通告 1416 的 MSS。1416 的算法是 1476 减 40(IPv6 头)减 20(TCP 头)。这两个值必须一致,改了 MTU 就要同步改 advmss,否则通告值和实际能发的大小对不上。
如果内网主机很多,逐条路由改 advmss 不现实,可以在转发路径上做 MSS 钳制:
# 通用开源系统上用 nftables 的写法 nft add table inet mangle nft add chain inet mangle forward '{ type filter hook forward priority mangle; }' nft add rule inet mangle forward tcp flags syn tcp option maxseg size set rt mtu这条规则的意思是把经过的 SYN 报文里的 MSS 选项,直接改成当前路由的 MTU 对应的值。好处是不用逐条路由配置,坏处是只在转发路径生效,本机发起的连接不经过 forward 钩子,还得靠advmss。
然后处理重定向。设备 A 和 B 都开启了 IPv6 转发(因为要转发内网流量),按照内核行为,它们在转发路径上不会处理重定向报文,这一点不用额外配。但它们自己作为主机发起的流量(比如管理流量)仍然可能接受重定向,所以建议显式关掉:
sysctl -w net.ipv6.conf.all.accept_redirects=0 sysctl -w net.ipv6.conf.default.accept_redirects=0内网主机侧则要评估:如果内网是单出口,直接关掉最省心;如果内网有双出口且依赖重定向,就得保留,但要确保重定向指的那条路径 MTU 和隧道一致,否则又回到老问题上。
5.4 四步验证:从ping到抓包
配完不能只看接口 up 就算完,必须按顺序验证四层。
第一步验证连通性,用小包:
ping -6 -c 3 2001:db8:100::2第二步验证大包。IPv6 没有 DF 位,源端本来就不分片,所以直接加大尺寸试到失败为止,失败的那个尺寸加 1 就是实际的 PMTU:
# 逐步加大,找到刚好能过的尺寸 ping -6 -c 3 -s 1400 2001:db8:100::2 ping -6 -c 3 -s 1436 2001:db8:100::2 ping -6 -c 3 -s 1437 2001:db8:100::21436 这个数字是算出来的:隧道 MTU 1476 减去 IPv6 头 40,等于 ICMPv6 载荷能带的最大值。如果 1436 能过、1437 不能过,说明隧道 MTU 设置正确,PMTU 反馈链路也是通的。
第三步验证端到端。从 A 的内网主机 ping B 的内网主机,同样加大尺寸。这一步能测出内网接口的 MTU 有没有也需要调整——如果内网接口还是 1500,而流量要经过隧道,那么大包在内网段就已经按 1500 发出去了,到了隧道口才被丢。这种情况要在内网侧也做 MSS 钳制,或者把内网接口的 MTU 一起调下来。
第四步抓包确认。在 A 的承载口上抓 ICMPv6 差错报文,看有没有 Type 2 发出来,以及发送频率:
tcpdump -ni eth0 'icmp6 and ip6[40] == 2' -c 20如果看到大量 Type 2 反复出现,说明发送端没有正确响应,或者响应了但下一次又按大尺寸发,通常是应用层没处理。如果一条都没有,说明要么 MTU 设置正确没有超限,要么差错报文在中间就被拦了——这时候要在中间节点上抓,确认到底在哪一段消失。
最后用ss看实际连接的 MSS 和 PMTU,这是最直观的:
ss -6 -tin dst 2001:db8:300::10输出里会带mss和pmtu两个字段,对照你配的值,对得上就说明整条链路是一致的。
6. 排查实录:六类典型故障的定位路径
6.1 症状—根因—处置速查表
隧道类问题最难的地方是症状高度相似,但根因分散在好几层。我把实际遇到过的情况整理成表,按症状反查,能省不少时间。
| 症状 | 可能根因 | 快速确认方法 | 处置 |
|---|---|---|---|
| 小包通、大包不通 | 隧道 MTU 未调或 MSS 未钳制 | ping -6 -s 1436试探 | 设隧道 MTU 与 advmss |
| 隧道 up 但完全不通 | 两端 Key 不一致或一端配了一端没配 | ip -d link show对比参数 | 两端参数对齐 |
| 间歇性丢大包 | 开了 Key 但 MTU 没减 4 | 计算开销是否含可选字段 | MTU 再减可选字段长度 |
| 首包通、后续全断 | PMTU 黑洞,ICMPv6 Type 2 被拦 | 中间节点抓ip6[40]==2 | 放行 ICMPv6 差错报文 |
| 某些目的地绕路 | 主机接受了重定向,缓存被改写 | ip -6 route get <dst>看网关 | 关闭 accept_redirects |
| 单向不通 | 非对称路由 + 反向路径检查 | 两端分别抓包 | 调整路由或放宽检查 |
| 邻居关系反复震荡 | 协议报文超过隧道 MTU | 抓协议报文尺寸 | 调 MTU 或调协议定时器 |
表里最值得展开的是"首包通、后续全断"这一类。它和"大包不通"看起来像,但成因不同:后者是尺寸问题,前者是反馈通道问题。判断方法是看失败的时间点——如果连接建立后第一个数据包就卡住,且重传次数不断累积,基本就是 PMTU 黑洞;如果只有特定大小的包失败,小包一直正常,那是 MTU 配置问题。
6.2 几个容易反复踩的坑
第一个坑是只改隧道接口 MTU,不改路由 advmss。隧道接口 MTU 改了,内核发出的报文尺寸确实变小了,但 TCP 的 MSS 是在握手时协商的,协商值来自路由的 advmss 或者出接口 MTU。如果路由上没有 advmss,内核会按接口 MTU 算,这时候如果流量是从内网主机发起、经过设备转发,主机算 MSS 用的是它自己的接口 MTU(1500),根本不知道隧道存在。结果就是主机按 1440 的 MSS 发,到隧道口被丢。解决办法要么在转发设备上做 MSS 钳制,要么把内网主机的接口 MTU 也调下来(后者影响面大,不推荐)。
第二个坑是抓包位置选错。查 PMTU 问题时,很多人只在两端抓,看不到中间的差错报文。实际上 Packet Too Big 是丢弃报文的那台设备发出的,如果丢弃发生在承载网中间,两端的抓包都看不到。正确做法是在隧道入口和出口分别抓,再在承载网的关键节点抓,三点对照才能定位。
第三个坑是忽略外层协议的 TTL 和跳数。GRE 隧道默认继承或者设一个固定的 TTL,如果设得太小,报文在承载网里跑几跳就过期了,表现出来是"距离近的对端能通,距离远的不能通"。设成 64 是常规做法,设成 255 也可以,但不要留默认的 0。
第四个坑是在虚拟化环境里忘了底层 MTU。物理机上是 1500,但虚机网卡可能被设成 1450 或者更低,容器网络的 overlay 还会再扣一层。这种情况下你在虚机里算出来的隧道 MTU 是基于错误的物理 MTU,怎么调都不对。上车之前先ip link show把所有接口的 MTU 打印一遍,从上往下捋。
第五个坑是用 ping 测通就认为没问题。ICMPv6 的 echo 报文默认尺寸很小,而且很多实现会对 echo 做特殊处理。真正能暴露问题的是大尺寸的 TCP 传输,或者带载荷的 UDP。验证时至少要跑一次实际的文件传输或者iperf之类的大流量测试,别只看 ping 结果。
提示:把所有涉及隧道的接口 MTU、路由 advmss、MSS 钳制规则整理成一张表,写进变更文档。这类配置最怕的是后人接手时只看到一半,改了一个值没改另一个,问题复现时又要从头查一遍。
我在实际做跨域互联时最深的体会是,隧道这东西的"配置"部分其实很简单,几条命令的事,真正花时间的是把开销算清楚、把反馈通道打通、把不该有的优化关掉。MTU 这件事没有"大概配一下"的空间,差 4 个字节就是差 4 个字节,表现出来就是大量连接卡在某个固定阶段。每次动手之前先把那张开销表填一遍,把 ICMPv6 的放行策略单独列一条,把重定向的开关状态记下来,后面出问题时能少绕很多路。至于多层隧道,我的建议是能不加就不加,每加一层就多一份需要长期维护的账,等到某天承载网悄悄把 MTU 从 1500 调到 1400,翻车的时候连是哪一层出的问题都要查半天。