资讯动态

IPv6重定向、PMTU与GRE隧道交叉故障排查指南

发布时间:2026/10/27 6:20:16 来源:尧图企业网站定制
IPv6 重定向、PMTU、GRE 隧道这三个词单拎出来任何一个都够写一篇长文凑在一起却往往出现在同一类故障单上链路看起来是通的ping 小包也回得干脆可一旦跑业务就时快时慢、TLS 握手卡在证书阶段、SSH 登录成功却打不出ls或者隧道两端明明是直连却绕了一大圈。我这些年处理过的 IPv6 隧道问题里十有八九最后都落在这三件事的交叉点上——GRE 把可用 MTU 压下去了PMTU 发现机制因为 ICMPv6 被挡而失效重定向又在隧道环境里悄悄改写了下一跳。这篇内容面向已经接触过 IPv6 基础、手上管着几条隧道或者准备上隧道的朋友也照顾刚接手 IPv6 运维、被大包不通折磨过的新人。我会把三个概念的报文细节、触发条件、封装开销算法、Linux 下的配置命令、探测手法和踩过的坑一次讲透你能直接抄走配置也能明白每一行为什么要这么写。1. 三个词摆在一起说的其实是一条因果链1.1 一条能 ping 通、打不开网页的链路先说个我印象最深的场景。两台主机之间拉了一条 GRE 隧道物理链路 MTU 1500隧道建好之后ping对端隧道地址秒回延迟个位数。可只要在上面跑 HTTP浏览器就转圈用curl -v看TCP 三次握手正常TLS ClientHello 发出去ServerHello 回来了然后卡在证书链传输上几十秒后超时。换成curl --http1.1访问一个纯文本小页面又能打开。这个现象非常有代表性。小包能过说明路由、邻居发现、基本连通性都没问题大包过不去说明路径上某一跳的 MTU 小于发送方认为的 MTU而发送方又没有收到包太大的通知。在 IPv4 时代中间路由器会帮你分片虽然效率低但至少能通到了 IPv6路由器不再分片源端如果不知道真实 PMTU就只能一直重传、一直丢。TLS 证书动辄两三 KB正好踩在这条线上。再往深挖一层为什么发送方会误判 MTU因为它收到的是隧道接口的 MTU而这个值是人配的配大了。人为什么配大因为 GRE 的封装开销容易被漏算。整条链路的因果关系就这么串起来了GRE 隧道改变了实际可用 MTUPMTU 发现负责把这个变化告知两端而 ICMPv6 报文一旦被过滤通知就到不了黑洞由此产生。至于重定向它是另一条支线上的变量后面单独说。1.2 因果链隧道压 MTUMTU 逼出 PMTU重定向改路径把这三个概念的逻辑关系摆清楚后面所有配置才有依据。GRE 隧道做的事情是封装把一个完整的 IPv6 或 IPv4 报文塞进一个新的外层 IPv6 报文里发出去。外层头 40 字节GRE 基本头 4 字节这 44 字节是纯开销不承载任何业务数据。物理链路 MTU 是 1500那么隧道里能过的最大内层报文就只有 1456。这个数字不是建议值是硬约束——超过它要么隧道入口自己分片IPv6 下会插 Fragment Header性能掉一大截要么外层包超过物理 MTU 被丢弃。PMTU 发现则是一套运行时的协商机制。源端先按自己认为的 MTU 发路径上任何一跳转不动了就回一个 ICMPv6 Type 2Packet Too Big把这一跳的 MTU 告诉源端源端降低缓存值重发。这套机制在 IPv6 里不是可选项是唯一能保证大包送达的手段因为路由器不再分片。重定向是路由器的一句好心提醒你把这个包交给我转其实同链路上有个更近的邻居能直接送到下次别绕我了。它优化的是路径不改变 MTU但在隧道场景下它可能把流量导向一个你自己都没预料到的出口让原本设计好的隧道走向失效甚至造成来回路径不一致进而让 PMTU 的探测结果在两条路径之间反复横跳。1.3 这三件事在什么规模下才会同时出现小规模环境下你可能一辈子碰不到三者叠加。但只要出现下面任意一种情况就得把三件事一起考虑一是跨域隧道两端分属不同管理域中间隔着你不控制的安全策略ICMPv6 能不能通、通哪些类型全靠猜。二是多宿主或双出口主机或路由器有两条以上出口路径重定向就有发挥空间。三是叠加封装GRE 之上再跑 VLAN、再跑 PPPoE每层都要扣字节扣到最后隧道 MTU 逼近 1280 这条红线。四是容器或虚拟化环境宿主机网卡 MTU 改了容器内的 veth、网桥、隧道接口不一定跟着改很容易出现宿主通、容器不通。我个人的判断标准很简单只要一条链路上出现了隧道并且隧道另一端不是你能完全掌控的设备就把 PMTU 和 ICMPv6 放行策略当成必查项而不是等出问题再回头翻。2. IPv6 重定向省一跳的优化也是事故的起点2.1 Type 137 报文里到底装了什么ICMPv6 重定向的 Type 字段值是 137Code 为 0。报文主体除了标准的校验和还有两个关键的 16 字节地址字段一个是Target Address指的是更优的下一跳也就是路由器建议你改用的那个邻居另一个是Destination Address指的是被重定向的目的地址也就是你原本想去的那个终点。后面可以跟两个选项Target Link-Layer Address 给出目标邻居的链路层地址省得主机再做一次邻居请求Redirected Header 则截取触发这次重定向的原始报文的一部分让主机能核对上下文。这两个地址字段的组合决定了一切。Target 必须是和主机在同一链路上的邻居Destination 必须不是链路本地地址、不是多播地址。换句话说重定向只对同一链路上存在更优出口这一种情形有意义。如果目标根本不在同一网段这条重定向就是伪造的主机必须丢弃。抓包看到 Type 137 的时候我习惯先看 Target 是不是真的是我邻居表里的设备。很多所谓的重定向攻击排查第一步就是把 Target 地址和 NDP 邻居缓存对一遍对不上基本就能定性。2.2 路由器什么时候发、主机什么时候收发送侧的门槛比想象中高。按 RFC 4861 的定义路由器要同时满足一串条件才会发重定向收到包的入接口和准备转发的出接口必须是同一个接口也就是这个包本来就该在本地链路上直接送达目的地址不能是本地链路上的邻居否则源端本来就该直接发转发接口的地址不能等于包的源地址目的地址不能是多播下一跳不能就是源地址本身。少一条都不发。接收侧的门槛更严。主机收到重定向后必须验证Target 地址与当前正在使用的默认路由器在同一链路上Target 不是多播地址这条报文的源地址确实是自己当前默认路由器的地址并且是目的地址当前的第一跳目的地址不能已经存在于邻居缓存里除非报文携带的 Redirected Header 与缓存内容不一致。这条不能在邻居缓存里的规则是为了防重定向循环——A 让你去 BB 让你回 A来回踢皮球。我在实际排障里发现一个很常见的误解很多人以为关掉主机的accept_redirects就万事大吉。但如果链路上有设备在发重定向而接收侧配置不一致比如一部分主机开着、一部分关着会出现同一网段内行为分裂——关掉的主机走次优路径但稳定开着的主机走最优路径但可能踩到某条链路的 MTU 问题。排查时看到同网段两台机器表现不同先查这个开关。2.3 隧道场景下为什么要主动关掉隧道接口本质上是点对点链路只有两个端点不存在同链路上还有第三个更优邻居这种情况。所以在隧道接口上接受重定向逻辑上就没有意义。更麻烦的是物理接口上的重定向可能影响隧道的承载流量。设想这样一张拓扑主机 A 的默认网关是 R1A 到远端要经过一条建在 R2 上的 GRE 隧道。R1 和 R2 在同一链路上。A 把包交给 R1R1 查表发现下一跳是 R2而 R2 和 A 同链路于是发一条重定向给 ATarget 写 R2。A 接受之后后续流量直接发给 R2。如果 R2 上的隧道配置是完整的这没什么问题甚至更快但如果 R2 只是作为转发节点、隧道其实建在 R1 上A 的流量就被导到了一个没有隧道出口的设备上直接黑洞。还有一种更隐蔽的情况隧道本身建在 A 上A 同时有物理接口和隧道接口。A 的默认路由指向 R1特定前缀走隧道。R1 看到 A 发来的隧道封装包外层目的地址是隧道对端如果 R1 判断该对端与 A 同链路就可能发重定向让 A 直接发——但隧道对端根本不在 A 的链路层可达范围内这条重定向是错的或者至少是不适用的。这种时候如果 A 盲目接受会污染路由决策。提示在承载隧道的设备上物理接口和隧道接口的accept_redirects都建议显式置 0不要依赖默认值。2.4 查看与关闭重定向的实操Linux 下三个 sysctl 控制这件事按接口粒度生效# 查看当前值1 表示接受0 表示忽略 sysctl net.ipv6.conf.all.accept_redirects sysctl net.ipv6.conf.eth0.accept_redirects # 全局关闭写入 /etc/sysctl.d/ 下的配置文件持久化 sysctl -w net.ipv6.conf.all.accept_redirects0 sysctl -w net.ipv6.conf.default.accept_redirects0 # 针对已存在的接口补一刀all 不覆盖已 up 的接口 for i in $(ls /sys/class/net); do sysctl -w net.ipv6.conf.${i}.accept_redirects0 done这里有个坑值得单独讲net.ipv6.conf.all.*只对之后新建的接口生效已经在跑的接口必须单独设或者用default加遍历。很多人改完all以为完事重启服务后新起的隧道接口又恢复了默认值 1问题复现。另一个坑是转发开关。net.ipv6.conf.all.forwarding1打开之后内核会把该接口的accept_redirects视为无效因为路由器角色不应该接受重定向但不同内核版本的行为不完全一致有些版本是忽略但值仍显示 1。所以我的习惯是显式写死不依赖内核的隐式行为。发送侧如果这台机器本身在转发还有send_redirectssysctl -w net.ipv6.conf.eth0.send_redirects0在隧道端点设备上关掉它可以避免对承载流量发出不适用的重定向。3. PMTUIPv6 里没有路上分片这回事3.1 路由器不再分片带来的连锁反应IPv4 的路由器遇到出接口 MTU 小于报文长度时如果 DF 位没置会就地分片。这让很多网络问题被掩盖了——反正能通只是慢一点。IPv6 把这个兜底机制直接砍掉了只有源节点可以分片中间路由器一律不分片。路由器转不动就丢包然后回一个 ICMPv6 Type 2 告诉源端太大了。这个设计让网络更干净但把责任全部压到了源端和 PMTU 发现机制上。源端必须做三件事维护每条路径的 PMTU 缓存、正确解析 ICMPv6 Type 2、在收到通知后降低缓存并重传。任何一环断了结果就是大包静默丢失。我在实际环境里见过的失效原因排前三的是安全策略把 ICMPv6 全类型一刀切封了中间设备丢弃 Type 2 是因为它不认这个报文类型老设备对 ICMPv6 的支持不完整源端应用自己设了IPV6_MTU_DISCOVER为关闭绕过 PMTU 直接按接口 MTU 发。第三条在自定义网络程序里特别常见开发者从 IPv4 代码搬过来没注意 IPv6 下这个选项的语义变了。3.2 ICMPv6 Type 2 与 PMTU 缓存的老化Type 2 报文的格式很简单Type2Code0校验和然后是 32 位的 MTU 字段后面附带触发报文的一部分。MTU 字段有约束——不能小于 1280也不能大于实际出接口 MTU防止恶意压低。源端收到之后把这条目的地址对应的 PMTU 降到报文里给的值然后重传。同时启动一个老化计时器Linux 下由net.ipv6.route.mtu_expires控制默认 600 秒。600 秒之后缓存失效重新按接口 MTU 试探如果路径变好了就提升变差了就再降。这个老化机制是把双刃剑。路径 MTU 恢复时它能自动跟上但如果路径上存在时好时坏的情况比如 ECMP 里有一条链路 MTU 更小缓存会在 600 秒周期里反复震荡表现为大包间歇性失败。遇到这种周期性故障先把mtu_expires调大让缓存稳定在一个保守值再排查路径。# 查看与调整 PMTU 缓存老化时间秒 sysctl net.ipv6.route.mtu_expires sysctl -w net.ipv6.route.mtu_expires1800还有一点PMTU 缓存是按目的地址精确说是按路由缓存条目维护的不是按连接。所以同一个目的地址上的多条 TCP 连接共享同一个 PMTU 值一条连接触发了下调其他连接立刻受益。这也是为什么有时候重连一下就好了——新连接复用了已经修正过的缓存值。3.3 1280 这条底线和 MSS 的换算IPv6 规定链路 MTU 不得小于 1280任何承载 IPv6 的链路层必须能传 1280 字节的报文。这条规定是所有隧道设计的锚点隧道接口的 MTU 绝对不能低于 1280否则隧道里跑的 IPv6 就无法正常工作你会看到邻居发现都失败。MSS 的换算是实操里最常算错的地方。记住公式场景MSS 计算MTU 1500 时IPv6 TCPMTU - 40 - 201440IPv4 TCPMTU - 20 - 201460GRE 隧道内 IPv6 TCPMTU 14561456 - 601396GRE 隧道内 IPv4 TCPMTU 14561456 - 401416IPv6 的固定头是 40 字节TCP 头最少 20 字节所以 IPv6 下的 MSS 比 IPv4 少 20。很多人习惯性按 IPv4 的 1460 去推隧道里就会多算 20 字节正好卡在边界上。实操里我一般会再留 20 到 40 字节余量。原因不是算错而是路径上可能存在你没发现的额外封装VLAN、MPLS、PPPoE或者某些设备对 IPv6 扩展头的处理比预期多耗字节。MSS 设 1396 和设 1360对吞吐的影响在千兆以下链路基本感知不到但能挡掉一批偶发故障。3.4 手工探测路径 MTU 的完整操作Linux 下的ping支持指定负载大小并禁止本地分片这是探测 PMTU 最直接的手段# -6 走 IPv6-M do 禁止本地分片-s 指定 ICMP 负载长度 # 总报文长度 40 (IPv6头) 8 (ICMPv6头) s ping -6 -M do -s 1452 2001:db8:200::1s1452对应总长 1500。如果返回Packet too big或者Message too long说明路径 MTU 小于 1500逐步往下调for s in 1452 1408 1372 1232; do ping -6 -M do -c 2 -W 1 -s $s 2001:db8:200::1 /dev/null 21 \ echo s$s OK, total$((s48)) \ || echo s$s FAIL, total$((s48)) dones1408对应隧道 MTU 1456 的满载测试s1232对应 1280 红线。这四个值覆盖了绝大多数隧道场景值得存成脚本随手跑。tracepath更省事它会自己二分查找并打印每一跳的 MTUtracepath -6 2001:db8:200::1输出里如果看到某一跳之后 PMTU 从 1500 掉到 1456那一跳就是瓶颈所在。注意tracepath依赖 ICMPv6 的 Time Exceeded如果中间设备不回这个结果会不完整。还有一个快速判断当前路由缓存值的方法ip -6 route get 2001:db8:200::1 # 输出里会带 mtu 字段如果没有则用接口 MTU ip -6 route get 2001:db8:200::1 from 2001:db8:1::14. GRE 隧道把封装开销一层层算清楚4.1 GRE 头到底占几个字节GRE 的基本头是 4 字节2 字节的标志位和版本号2 字节的协议类型。协议类型决定内层承载什么——0x0800 是 IPv40x86DD 是 IPv60x6558 是透明以太网桥接也就是 gretap。除了基本头GRE 还有两个可选字段Checksum4 字节和Key4 字节以及Sequence Number4 字节。按 RFC 2890Checksum 和 Sequence 不能同时启用C 位和 S 位互斥Key 可以和其中任意一个组合。Key 的作用是让接收端区分同一个源目的对之间的多条隧道实际部署里用得很普遍。所以在算开销时你要先确认这条隧道启用了什么GRE 配置GRE 头长度加 IPv6 外层40B总开销基本头444基本头 Key848基本头 Checksum848基本头 Key Sequence1252外层如果是 IPv4把 40 换成 20 重算。IPv4 外层 基本头 24 字节开销这也是老资料里最常见的GRE 开销 24的由来。现在跑 IPv6 隧道这个数字要改成 44很多人就是这么算错的。4.2 逐层扣减从物理 MTU 到隧道 MTU算隧道 MTU 的正确姿势是自上而下逐层扣而不是拍脑袋给个 1400。我按一条真实链路走一遍物理链路 MTU 1500。中间有一段 VLAN 子接口802.1Q 标签 4 字节实际可用 1496。再往上跑一条 GRE over IPv6 隧道外层 IPv6 头 40 字节GRE 基本头 4 字节合计 44 字节。那么隧道接口的 MTU 就是 1496 - 44 1452。如果这条 GRE 还启用了 Key再减 4得 1448。如果底层是 PPPoEMTU 从 1500 掉到 1492再叠 VLAN 得 1488再减 44 得 1444。这类环境里如果你按标准 1456 去配多出来的 12 字节就会在 PPPoE 那层被丢。我习惯在文档里画一张扣减表从最底层写到最上层每一层写清楚减多少、剩多少、谁负责。这张表比任何配置注释都有用尤其在交接或者排查时——只要发现某一层没人维护问题多半就出在那儿。注意隧道 MTU 设得比实际能力大会出现小包通大包不通设得太小会浪费带宽并可能低于 1280 这条红线。宁可略保守也不要冒险。还有一类容易忽略的开销如果隧道内层跑的是 IPv6而外层也是 IPv6那么内层 IPv6 的 40 字节头是算在隧道 MTU 之内的不需要额外扣。但如果隧道做的是二层桥接gretap内层是以太网帧还要考虑以太网头 14 字节和可能的 VLAN 标签。4.3 三种隧道 MTU 设法的取舍实际操作里有三种流派各有适用场景。第一种精确扣减。按上面算出来的 1452 或 1456 配置隧道 MTU然后把 MSS 钳到对应值。优点是带宽利用率最高缺点是对底层变更敏感——底层链路换了一段、多了一个封装层隧道 MTU 配置不跟着改就会出问题。第二种保守固定值。不管底层怎么算统一配 1400 或者 1280。这种方法在跨域环境里非常实用因为你根本不知道中间会不会有额外封装。1280 是下限配到这个值几乎不会出 MTU 问题代价是每个包少装 200 多字节数据大流量场景下吞吐会有可感知的下降。第三种隧道 MTU 配大 MSS 钳制兜底。隧道接口 MTU 保持 1500靠 MSS clamp 把 TCP 的段大小压下来。这种做法只对 TCP 有效UDP、QUIC、ICMP 大包照样会出问题。我一般只在过渡期用不作为长期方案。我自己的选择是隧道接口 MTU 按精确扣减配同时在转发路径上做 MSS clamp 作为第二道防线。两层保险出问题时至少 TCP 业务是通的能给你留出排查时间。4.4 Linux 下建一条 ip6gre 隧道并压 MTU下面是完整的搭建流程用文档专用前缀2001:db8::/32做示例。# 1. 建隧道本地 2001:db8:1::1对端 2001:db8:2::2承载在 eth0 上 ip -6 tunnel add gre1 mode ip6gre \ local 2001:db8:1::1 \ remote 2001:db8:2::2 \ ttl 64 \ dev eth0 # 2. 先把 MTU 压到算好的值再 up ip link set dev gre1 mtu 1452 ip link set dev gre1 up # 3. 配隧道内层地址 ip -6 addr add 2001:db8:ff::1/64 dev gre1 # 4. 加路由把目标网段指向隧道 ip -6 route add 2001:db8:aa::/64 dev gre1 # 5. 验证 ip -6 tunnel show gre1 ip -d link show gre1 ip -6 route get 2001:db8:aa::1几个细节值得说。mode ip6gre表示外层用 IPv6 承载 GRE内层可以是 IPv4 也可以是 IPv6。如果是二层桥接要用ip6gretap。ttl 64是给外层包的跳数限制不设的话内核会继承内层包的值容易出现内外层 TTL 语义混淆的问题。第 2 步的顺序很重要先设 MTU 再 up。如果先 up 再改 MTU中间那一小段时间里隧道按默认 1500 在跑正好有流量经过就会触发分片。我吃过一次这个亏脚本里把两条命令写反了重启网络的时候监控报了几分钟的大包丢弃。MSS 钳制用 nftables 做nft add table inet mssclamp nft add chain inet mssclamp post { type filter hook postrouting priority mangle; } nft add rule inet mssclamp post tcp flags syn tcp option maxseg size set rt mtuset rt mtu的意思是取路由表里的 MTU 值来设置 MSS也就是跟着 PMTU 走。如果 nft 版本不支持这个表达式退回到固定值nft add rule inet mssclamp post tcp flags syn tcp option maxseg size set 13921392 对应 1452 的隧道 MTU 再减 60留了 0 余量我一般写 1372留 20 字节余量。持久化方面ip命令是一次性的重启就没了。生产环境建议用 systemd-networkd 的.netdev加.network配置或者 NetworkManager 的连接配置把 MTU 和隧道参数都写进去。手工ip命令只在调试时用。5. 三者叠加后的故障现场与排查手册5.1 PMTU 黑洞最隐蔽的一类PMTU 黑洞的典型特征是小包通、大包不通且没有 ICMP 错误返回。判断方法很简单用ping -6 -M do -s从小到大扫找到第一个失败的尺寸然后看那个尺寸是否恰好等于某个已知的 MTU 减 48。如果s1452总长 1500失败s1400总长 1448成功那瓶颈大概在 1448 到 1500 之间很可能就是隧道 MTU 1452 没有正确配置或者没有被 PMTU 发现覆盖。反过来如果所有尺寸都成功但 TCP 大流量还是卡问题就不在 PMTU去看 TCP 窗口、丢包重传、或者应用层。排查黑洞时抓包是最直接的证据。在隧道两端同时抓 ICMPv6# 只看 ICMPv6 Type 2Packet Too Big tcpdump -ni any icmp6 and ip6[40] 2 -vv # 同时看隧道封装流量 tcpdump -ni eth0 ip6 proto 47 -vv如果入口侧发出了 Type 2 但出口侧没收到中间有设备在过滤如果出口侧根本没发出 Type 2说明它自己就没意识到包太大可能是配置问题。5.2 重定向和隧道打架来回路径不一致这类故障的表现是时通时不通重连就好。原因是重定向改变了某一方向的下一跳导致去程走隧道、回程走物理链路或者反过来。两条路径的 MTU 不同PMTU 缓存就会在两个值之间来回跳。诊断方法是看两端的路由和邻居状态# 看本机到对端的实际下一跳 ip -6 route get 2001:db8:aa::1 # 看邻居缓存里有没有预期之外的条目 ip -6 neigh show dev eth0 # 看重定向计数如果有 nstat -az | grep -i redirectnstat里的Ip6InRedirects和Ip6OutRedirects能直接告诉你有没有收到或发出重定向。这个计数器很多人不知道排查时非常好用。如果Ip6InRedirects在持续增长而你的网络设计里根本不应该有重定向那就是有设备在发不该发的报文。处理方式就是第 2 章说的两端都关掉accept_redirects和send_redirects让路径固定下来。隧道环境追求的是可预测不是最优。5.3 排查命令清单与速查表我把常用的命令整理成一张表出问题时按顺序跑一遍基本能定位到层目的命令关注点确认隧道状态ip -6 tunnel showlocal/remote/mtu 是否符合预期确认接口 MTUip -d link show gre1mtu 字段确认路由 MTUip -6 route get dst输出里的 mtu 值探测路径 MTUping -6 -M do -s N第一个失败点自动探测tracepath -6 dst每跳 MTU 变化看 TCP MSSss -6 -tin dst ipmss 字段看 ICMPv6tcpdump -ni any icmp6Type 2 / 137看重定向计数nstat -az | grep -i redirect是否异常增长看分片情况nstat -az | grep -i frag隧道内是否在分片再补一张症状对照表覆盖我遇到过的绝大多数组合症状最可能原因验证手段处理ping 通TCP 大响应超时PMTU 黑洞ping -M do -s 1452放行 ICMPv6 Type 2钳 MSS隧道内 iperf3 速率骤降隧道内分片nstat看 frag隧道 MTU 改小时通时不通重连恢复重定向改路径nstat看 Redirects关闭 accept_redirects只有 UDP 业务失败MSS 钳制对 UDP 无效抓包看包长应用层限制包大小容器内不通宿主通veth/网桥 MTU 未同步ip link show逐层看统一 MTU邻居发现失败隧道 MTU 1280ip -6 neigh提高隧道 MTU5.4 防火墙放行 ICMPv6 的正确姿势这一节单独拎出来讲因为它是黑洞的头号成因。很多安全基线把 ICMP 一刀切成只放 echo在 IPv4 下勉强能用有分片兜底在 IPv6 下就是灾难。必须放行的 ICMPv6 类型至少有这些Type 1Destination Unreachable路径不可达通知。Type 2Packet Too BigPMTU 的核心绝对不能挡。Type 3Time Exceededtraceroute 和环路检测依赖它。Type 4Parameter Problem扩展头处理出错时用。Type 128/129Echo Request/Reply基础连通性测试。Type 133/134Router Solicitation/Advertisement无状态地址配置必需。Type 135/136Neighbor Solicitation/Advertisement地址解析必需。Type 137Redirect按需放行隧道环境建议直接丢。nftables 下的最小放行规则nft add table inet icmpv6base nft add chain inet icmpv6base input { type filter hook input priority filter; } nft add rule inet icmpv6base input meta l4proto ipv6-icmp icmpv6 type { 1, 2, 3, 4 } accept nft add rule inet icmpv6base input meta l4proto ipv6-icmp icmpv6 type { 128, 129 } accept nft add rule inet icmpv6base input meta l4proto ipv6-icmp icmpv6 type { 133, 134, 135, 136 } accept nft add rule inet icmpv6base input meta l4proto ipv6-icmp icmpv6 type 137 drop注意Type 2 的放行要同时覆盖入方向和转发方向。很多配置只在 input 链放行forward 链还是默认 drop隧道流量照样黑洞。还有一条经验如果安全策略不允许你全类型放行至少保证 Type 2 的速率限制放宽。PMTU 发现在大流量路径上会产生密集的 Type 2 报文每个连接首次探测都可能触发限速设得太严比如每秒 10 个会导致一部分连接收不到通知。我一般把 Type 2 的限速设在每秒 200 个以上配合突发 500。6. 一份可以直接抄的基线配置与踩坑记录6.1 主机侧 sysctl 基线下面这份配置我在多个环境里用过覆盖了隧道端点、边界转发设备和普通主机三种角色。写到/etc/sysctl.d/90-ipv6-tunnel.conf里# 转发开关路由器角色打开 net.ipv6.conf.all.forwarding 1 # 重定向隧道环境统一关闭 net.ipv6.conf.all.accept_redirects 0 net.ipv6.conf.default.accept_redirects 0 net.ipv6.conf.all.send_redirects 0 net.ipv6.conf.default.send_redirects 0 # PMTU 缓存老化长路径建议拉长 net.ipv6.route.mtu_expires 1800 # 路由缓存的其他参数按需 net.ipv6.route.gc_timeout 60写完执行sysctl --system生效。别忘了第 2 章说的all不覆盖已存在接口新接口才用default。所以启动脚本里还要补一段遍历#!/bin/bash for i in /sys/class/net/*; do dev$(basename $i) [ -e $i/ifindex ] || continue sysctl -w net.ipv6.conf.$dev.accept_redirects0 /dev/null 21 sysctl -w net.ipv6.conf.$dev.send_redirects0 /dev/null 21 done这段跑在隧道接口创建之后。如果隧道是 systemd-networkd 管的可以用[Network]段的IPv6AcceptRAno加自定义 post-up 脚本或者直接用 networkd 的IPv6ProxyNDP之类参数间接控制。6.2 隧道侧配置模板一个可复用的隧道配置模板变量抽出来#!/bin/bash set -e UNDERLAY_DEVeth0 UNDERLAY_MTU$(cat /sys/class/net/${UNDERLAY_DEV}/mtu) TUN_NAMEgre1 LOCAL_ADDR2001:db8:1::1 REMOTE_ADDR2001:db8:2::2 INNER_ADDR2001:db8:ff::1/64 INNER_NET2001:db8:aa::/64 # GRE 基本头 4 外层 IPv6 头 40 44 OVERHEAD44 TUN_MTU$((UNDERLAY_MTU - OVERHEAD)) # 红线保护 if [ $TUN_MTU -lt 1280 ]; then echo ERROR: computed mtu ${TUN_MTU} below 1280, check underlay mtu 2 exit 1 fi ip -6 tunnel add $TUN_NAME mode ip6gre \ local $LOCAL_ADDR remote $REMOTE_ADDR ttl 64 dev $UNDERLAY_DEV ip link set dev $TUN_NAME mtu $TUN_MTU ip link set dev $TUN_NAME up ip -6 addr add $INNER_ADDR dev $TUN_NAME ip -6 route add $INNER_NET dev $TUN_NAME mtu $TUN_MTU echo tunnel ${TUN_NAME} up, mtu${TUN_MTU}, mss$((TUN_MTU - 60))这里有几个设计上的考量。第一MTU 是从底层接口动态读的底层变了模板自动跟着变不用手工改。第二加了 1280 红线检查宁可创建失败也不要建一条跑不起来的隧道。第三路由上加mtu参数把隧道 MTU 固化到路由条目里这样rt mtu的 MSS 钳制能直接读到正确值。第四MSS 只是打印出来供核对实际钳制交给 nftables 统一做。如果隧道要长期运行建议改用 systemd-networkd。一个.netdev文件大致是[NetDev] Namegre1 Kindip6gre MTUBytes1452 [Tunnel] Local2001:db8:1::1 Remote2001:db8:2::2 TTL64配套.network文件配内层地址和路由。这样开机自动建不用脚本。6.3 我踩过的几个坑第一个坑是隧道 MTU 和路由 MTU 不一致。有次隧道接口配 1452路由条目上被另一条静态路由覆盖成了默认 1500结果 PMTU 缓存读的是 1500MSS 钳制也按 1500 算实际包到了隧道就被丢。现在我的模板里两处都显式写 MTU并且加一条检查逻辑核对两者相等。第二个坑是IPv6 外层带扩展头导致开销多算。有一条隧道外层加了流标签相关的扩展头实际开销比 44 多了 8 字节1456 的隧道 MTU 就是过不去。这类情况很难提前算到我的应对是把 MSS 余量从 20 加到 40宁可少装一点。后来发现用tcpdump -v看外层包的总长度最直接抓几个满载包量一下实际封装长度比任何公式都准。第三个坑是容器网络没有继承宿主 MTU。宿主的隧道接口 1452但容器里的 veth 默认 1500容器内应用按 1500 发到宿主隧道口被丢。解决方式是在 CNI 配置里显式设置 MTU或者用ip link set dev vethX mtu 1452逐层设。这个问题在新环境部署时特别容易漏因为容器内ping小包完全正常。第四个坑是关闭重定向后某些老应用的行为变化。有一批依赖同网段最优路径的应用在重定向关闭后走次优路径延迟从 1ms 变成 3ms。这类业务感知非常敏锐会来投诉。处理方式是把它放到一个不关重定向的专用网段或者调整路由让最优路径显式生效。这提醒我安全基线不能无脑全量套得看业务特性。第五个坑是ICMPv6 限速误伤 PMTU。有次在中转设备上配了 ICMPv6 全类型限速每秒 50 个正常业务没问题但只要有一个大文件传输场景PMTU 探测报文一密集就被丢表现为传输速率锯齿状波动。后来把 Type 2 单独提出来放宽限速才解决。这个细节在配置文件里看不出问题只有结合流量特征才能定位。最后分享一个小技巧排查 MTU 相关问题时先别急着改配置用tcpdump在隧道两端各抓 100 个包把内层报文长度和外层报文长度的差值算出来。这个实测开销比任何公式都可靠尤其是路径上有你不了解的封装时。我现在的排查顺序是先抓包量开销再算隧道 MTU再看路由 MTU 是否一致最后才动防火墙。按这个顺序走大多数问题在第二步就能定位省下大量来回试错的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑