Skip to content

隧道基础:GRE

前九章的 VPP 一直在改写报文。这一章讲另一件事:把报文包起来

问题:私网地址过不了公网

两个站点各有一段私网地址,中间隔着只认公网地址的 WAN。 hA 想访问 hB,包发出去就死在 WAN 上 —— 没人知道 10.2.0.0/24 往哪走。

NAT 解决的是"内网访问公网", 这里的需求不一样:两个私网要互相访问,而且要保留真实地址

隧道的思路:在外面再套一层

text
  ┌──────────────┬─────────┬────────────────────────────────┐
  │ 外层 IP (20)  │ GRE (4) │ 原始 IP 包(内层,一个字节没动)    │
  │ 198.51.100.1 │         │ 10.1.0.11 → 10.2.0.11          │
  │ → .2         │         │                                │
  └──────────────┴─────────┴────────────────────────────────┘
     ↑ WAN 只看这一层                ↑ 对端剥掉外层后原样还原

WAN 上跑的是一个普通的、源目都是公网地址的 IP 包。 私网地址被整个装进了载荷里,WAN 完全不知情。

实验拓扑

启动两站点底座

bash
./labctl up site
text
  站点 A                     WAN                      站点 B
  hA 10.1.0.11 ── nat1 ══════════════════════ nat2 ── hB 10.2.0.11
                 .1   198.51.100.0/30   .2

底座起来后有个刻意保持的初始状态:

text
✓ WAN 链路 198.51.100.1 ↔ .2 通
✓ hA → hB 不通(预期如此,WAN 上没有私网路由)

这就是隧道要解决的问题。

这个底座跑两台 VPP,不开 polling

HA 底座一样的取舍:两台 VPP 各吃一个核会压垮 4 核实验机。 时延会高一些,但隧道实验不看时延。

配置

应用配置

bash
./labctl apply site 01
./labctl verify site 01

三步,两端对称。

bash
# 1. 建隧道口。src/dst 是**外层**地址,也就是 WAN 上互通的那对
vppctl create gre tunnel src 198.51.100.1 dst 198.51.100.2   # nat1
vppctl create gre tunnel src 198.51.100.2 dst 198.51.100.1   # nat2

# 2. 给隧道口配地址(隧道内部的下一跳)
vppctl set interface state gre0 up
vppctl set interface ip address gre0 10.99.1.1/30            # nat1
vppctl set interface ip address gre0 10.99.1.2/30            # nat2

# 3. 把对端站点的路由指进隧道
vppctl ip route add 10.2.0.0/24 via 10.99.1.2                # nat1
vppctl ip route add 10.1.0.0/24 via 10.99.1.1                # nat2

通了:

text
  hA → hB
  3 packets transmitted, 3 packets received, 0% packet loss
  round-trip min/avg/max = 0.987/1.048/1.110 ms

WAN 的路由表一个字都没改。

封装在图上的样子

bash
./labctl vppctl site clear trace
./labctl vppctl site trace add af-packet-input 4
# ...制造流量...
./labctl vppctl site show trace
text
  → af-packet-input
  → ethernet-input
  → ip4-input                       内层包进来:10.1.0.11 → 10.2.0.11
  → ip4-lookup                      查路由,命中 10.2.0.0/24 via gre0
  → ip4-load-balance
  → ip4-midchain                    ★ 隧道的"中间链"节点
  → tunnel-output                   ★ 套上外层 IP 头 + GRE 头
  → ip4-rewrite                     按外层地址改写链路层头
  → host-eth2-output
    GRE: 198.51.100.1 -> 198.51.100.2
    GRE ip4

ip4-midchain 是理解 VPP 隧道的关键概念。

普通转发是"查路由 → 改链路层头 → 发出去"。隧道多了一步: 查路由的结果不是一个物理接口,而是一个需要先封装再重新查一次路由的虚拟接口。 midchain 就是把这两段接起来的地方 —— 先做封装,再把封装好的包 交给外层地址的转发链。

这解释了隧道的性能特征

一个包穿过隧道要查两次路由:一次内层(决定进哪个隧道), 一次外层(决定隧道包从哪个物理口出去)。

所以隧道的转发成本天然高于普通转发。VPP 用 midchain 把外层的转发链 预先"堆叠"好,避免每个包真的查两遍表 —— 这是它比朴素实现快的地方。

隧道最大的坑:MTU

这一节值得慢读,因为它是隧道故障里出现频率最高的一类。

症状:小包能通,大包不通

bash
./labctl vppctl site show interface | grep -E "gre0|host-eth2"
text
  gre0         MTU 9000
  host-eth2    MTU 1500

gre0 声称能收 9000 字节,但物理口只有 1500。

主机看到网关那边的路径能走大包,于是发了 1500 字节的包。 VPP 加上 24 字节的外层头,变成 1524 —— 物理口塞不下。

text
  发一个 1472 字节的 ping(加上头正好 1500,封装后就超了)
    2 packets transmitted, 2 packets received, 0% packet loss
    8 ip4-frag number of sent fragments info
✓ 包通了,但 VPP 不得不做分片

通了,靠的是分片。这就是症状隐蔽的原因 —— ping 默认包很小, 测不出问题;只有传大文件、跑 TLS 握手(证书链很大)时才暴露。

分片的代价

text
  · 每个大包变成两个,pps 翻倍
  · 接收端要重组,占内存和 CPU
  · 中间丢了任一片,整个包就废了
  · 很多防火墙对分片包处理不当,直接丢弃

更糟的是应用设置了 DF(不允许分片) 位的情况 —— 那样包会被直接丢掉。症状就是经典的"小包能通、大包不通", 而且 ping 测不出来。

改隧道 MTU?不行

bash
vppctl set interface mtu 1476 gre0
text
set interface mtu: not supported

VPP 不允许改 GRE 隧道口的 MTU。所以得换个思路。

解法:MSS clamping

不去修 MTU,而是让 TCP 自己少发

TCP 建连时双方在 SYN 里协商 MSS(最大报文段长度)。 隧道设备转发 SYN 时把这个值改小,两端就会自觉地只发小包, 根本不会产生需要分片的大包。

bash
vppctl set interface tcp-mss-clamp gre0 ip4 rx ip4-mss 1400 ip6 disable ip6-mss 0
vppctl show interface tcp-mss-clamp
text
gre0: ip4: 1400 [RX] ip6: disabled

必须指定方向

bash
vppctl set interface tcp-mss-clamp gre0 ip4 enable ip4-mss 1400
text
set interface tcp-mss-clamp: Please specify the MSS clamping direction for ip4 and ip6

要写 rxtx,不能写 enable。而且 ip4 和 ip6 都要给值 (不用的那个写 disable ip6-mss 0)。

效果在 trace 里直接可见:

text
  不做 clamping    trace 里的 MSS:mss 1460
  做了 clamping    trace 里的 MSS:mss 1400 mss 1460
                                   ↑ 改写后   ↑ 入口处的原始值

为什么 MSS clamping 比 PMTUD 可靠

理论上 TCP 有路径 MTU 发现(PMTUD):发大包被拒, 沿途设备回一个 ICMP "需要分片",发送方据此调小。

现实是大量网络把 ICMP 一刀切地过滤掉了, 这个反馈永远回不来,发送方持续发大包、持续被丢 —— 这就是所谓的 PMTUD 黑洞,表现为连接建立后一传数据就卡死。

MSS clamping 不依赖任何反馈,在建连的那一刻就把上限定死了。 所以它是隧道场景的标准做法。

MSS clamping 只管 TCP

UDP 没有 MSS 协商这回事。大 UDP 包(DNS 响应、QUIC、VoIP、视频) 该分片还是会分片。

那部分只能靠应用自己控制包大小 —— 比如 QUIC 默认就把 数据包控制在 1200 字节左右,正是为了穿过各种隧道和封装。

一个顺序敏感的坑

先删路由,再删隧道接口

拆隧道时如果顺序反了:

bash
vppctl delete gre tunnel src ... dst ...     # 先删接口
vppctl ip route del 10.2.0.0/24 via 10.99.1.2   # 再删路由 → VPP 崩溃

崩溃栈:

text
#10 fib_entry_path_add
#11 fib_table_entry_path_add2
#12 vnet_ip_mroute_cmd        ← ip route 命令的处理函数
#13 vlib_cli_input

这是本教程遇到的第五个"操作已不存在的对象"类崩溃 (前四个见故障速查)。

正确顺序是先删引用它的路由,再删接口。本教程的 reset.sh 就是这么写的,并且在注释里标了原因。

GRE 的定位

text
  ✓ 优点                          ✗ 缺点
  ────────────────────────────    ────────────────────────────────
  极其简单,两端各三条命令          **没有任何加密和认证**
  开销小(24 字节)                任何人都能伪造 GRE 包注入
  协议无关,能封装任何三层协议       不能穿过大多数 NAT
                                  (GRE 是协议 47,不是 TCP/UDP)

GRE 是明文的。 它解决的是"连通性",不是"安全"。 在可信的专线上用 GRE 没问题;跑在互联网上必须叠加加密 —— 要么 GRE over IPsec,要么直接用 WireGuard 这类自带加密的方案。

下一篇讲 VXLAN —— 二层 overlay, 和 GRE 是完全不同的一套东西。再往后是一次 WireGuard 的完整尝试 (排查实录)。

小结

解决什么两个私网穿过只认公网地址的 WAN 互访
配置create gre tunnel src/dst + 配隧道口地址 + 指路由
封装开销24 字节(20 外层 IP + 4 GRE)
关键节点ip4-midchaintunnel-output,查两次路由
最大的坑隧道口 MTU 9000 而物理口 1500 → 分片;DF 时直接丢
改 MTU不支持set interface mtu 对 GRE 返回 not supported)
正解set interface tcp-mss-clamp <口> ip4 rx ip4-mss <值>,必须给方向
MSS clamping 的边界只管 TCP,UDP 无效
拆除顺序先删路由再删接口,反了会崩 VPP
安全性无加密无认证,公网上必须叠加 IPsec 或换 WireGuard

实验环境基于 netlab + containerlab + VPP 26.06