Skip to content

动态路由:OSPF

前十章的所有实验都用静态路由:

bash
ip route add 10.2.0.0/24 via 10.99.1.2

两个站点、一条链路时这没问题。这一章讲为什么规模一上来就不行, 以及 VPP 上的动态路由是怎么工作的。

VPP 自己不跑路由协议

这是理解这一章的前提。

VPP 是纯转发面。路由协议由外部的控制面进程跑 —— netlab 的 VPP 镜像 用的是 bird(也支持 FRR),它跑在容器的 dataplane netns 里。

三段链条:

text
  1. bird 跑 OSPF,学到路由,写进 netns 的内核路由表
  2. linux-cp 插件的 netlink 监听器捕获这个变化
  3. 路由被同步进 VPP 的 FIB,数据面按它转发

这个架构决定了排错要分两段看

bash
# 控制面:协议层面对不对
docker exec <> ip netns exec dataplane birdc show ospf neighbors
docker exec <> ip netns exec dataplane birdc show route

# 数据面:路由有没有真的进转发表
docker exec <> vppctl show ip fib 10.2.0.0/24

两边都要看。 邻接建起来了但 FIB 里没有,说明 linux-cp 那一环出了问题; FIB 里有但不通,那是转发面的事(邻接解析、feature 拦截等)。

实验拓扑

启动动态路由底座

bash
./labctl up ospf
text
  hA 10.1.0.11 ── r1 ══ WAN-1 198.51.100.0/30 ══ r2 ── hB 10.2.0.11
                    ╚══ WAN-2 198.51.100.4/30 ══╝

两条平行的 WAN 链路 —— 这是演示动态路由价值的最小拓扑:能学路由,也能看多路径。

netlab 的 OSPF 配置在 --no-config 下也生效

本教程所有底座都用 netlab up --no-config(为了抢在 VPP 启动前改配置)。 那 OSPF 配置从哪来?

netlab 把它生成在一个独立文件 node_files/<节点>/ospf, 并在 clab.yml 里挂载到容器的 /etc/bird/ospf.mod.conf

yaml
binds:
- node_files/r1/bird:/etc/bird/bird.conf
- node_files/r1/ospf:/etc/bird/ospf.mod.conf

主配置里有一行 include "/etc/bird/ospf.mod.conf";。 容器启动时 bird 自动加载 —— 完全不依赖 netlab 的配置部署阶段

生成的内容也是对的:

text
protocol ospf v2 ospf_global_v2 {
  area 0.0.0.0 {
    interface "loop0" { stub; };
    interface "eth1"  { stub; type ptp; };     ← LAN 口是 stub
    interface "eth2"  { type ptp; };            ← WAN 口跑协议
    interface "eth3"  { type ptp; };
  };
}

LAN 口设成 stub 是标准做法:那边没有别的路由器, 发 Hello 纯属浪费,但网段本身要被通告出去。

零手工路由即互通

底座起来后直接就通了:

text
── 1) 零手工路由,站点已互通 ──
  3 packets transmitted, 3 packets received, 0% packet loss
✓ hA → hB 全通
  整个过程没有执行过一条 ip route add

对比前十章 —— 每个隧道实验都要在两端手工配 ip route add, 少配一边就单向不通。

自动 ECMP

bash
./labctl vppctl ospf show ip fib 10.2.0.0/24
text
  [@0]: ipv4 via 198.51.100.2 host-eth2
  [@0]: ipv4 via 198.51.100.6 host-eth3

OSPF 算出两条等价路径(两条 WAN 的 cost 相同), linux-cp 把它们都同步进了 FIB,VPP 自动做 ECMP 负载分担。

静态路由要达到同样效果,得手工在每台设备上配两条等价路由, 而且链路变化时要手工调整。

一条路径失效会怎样

跑自检

bash
./labctl apply ospf 01
./labctl verify ospf 01

自检在持续 ping 期间删掉一条路径的邻接:

text
  已删除 198.51.100.2 的邻接(WAN-1 那一跳)
  30 packets transmitted, 29 packets received, 3% packet loss
✓ 丢包 3% —— 一条路径失效,数据流基本没受影响

30 个包只丢了 1 个。 因为两条路径本来就都在转发表里, 一条失效时 VPP 把对应的 bucket 标为未解析,后续包全走剩下那条 —— 不需要等 OSPF 重算,也不需要等任何定时器。

text
  单链路 + OSPF   故障 → 等 dead interval(默认 40 秒)
                  → 重算 SPF → 才切过去。这 40 秒全丢。

  双链路 + ECMP   失效的瞬间就切。

这就是生产网络宁可多花钱建两条链路的原因 —— 不只是为了带宽,更是为了这个"不用等"的性质。

前提是故障能被本地感知

上面的即时切换建立在"本地知道这条路径坏了"之上(接口 down、邻接失效)。

如果是对端静默失效或者中间某一跳黑洞,本地看什么都正常, 那就只能等 OSPF 的 dead interval。要让这种情况也做到亚秒级,需要 BFD (Bidirectional Forwarding Detection)—— VPP 有 bfd 插件, 但本教程没有覆盖它和 OSPF 的联动。

在本环境模拟链路故障有多难

这一节是踩坑记录。想演示"链路断了会怎样",三种直觉的做法全都不行

set interface state down 会永久破坏 af_packet 的 ARP

bash
vppctl set interface state host-eth2 down
vppctl set interface state host-eth2 up      # 恢复

接口是 up 回来了,但邻居再也解析不了

text
vppctl show ip fib 10.2.0.0/24
  [0] [@3]: arp-ipv4: via 198.51.100.2 host-eth2   ← 永远停在这
  [1] [@5]: ipv4 via 198.51.100.6 host-eth3

show ip neighbors 里那个地址干脆消失了,怎么 ping 都解析不回来。 只能重建容器。

这个坑让我误判了两次:ECMP 下一条流会确定性地哈希到某个 bucket, 所以坏掉一个 bucket 的表现是"有些流通、有些流不通" —— 看起来像随机故障,实际是确定的。

容器里 ip link set down,VPP 感知不到

bash
docker exec <> ip link set eth2 down

VPP 通过 af_packet 附着在这个口上,但它不监听 Linux 侧的链路状态。 实测 OSPF 邻居数不变、FIB 不变,什么都没发生。

ACL / policer 拦不住 OSPF

最后试了用 ACL 全 deny 来模拟"链路还在但流量过不去":

bash
vppctl set acl-plugin acl deny src 0.0.0.0/0
vppctl set acl-plugin interface host-eth2 input acl <N>

OSPF 邻居纹丝不动,而且 ACL 计数是 0

原因:OSPF 用组播 224.0.0.5,走的不是 ip4-unicast

bash
vppctl show interface features host-eth2
text
ip4-multicast:
  none configured        ← ACL 挂在 unicast 弧上,管不到这里

policer 同理,计数也是 0。

这是个有普遍意义的教训第 9 章的 feature 顺序 讲的都是 ip4-unicast 弧。组播流量(路由协议、PIM、IGMP)走的是另一条弧, 你在 unicast 弧上挂的所有东西对它们都无效。

想过滤路由协议报文,得用别的手段(比如 OSPF 自己的认证, 或者在 ip4-multicast 弧上挂)。

所以自检最终改成直接删邻接来演示 ECMP 切换 —— 它对应的是 "故障能被本地立即感知"这一类,也是真实网络里最常见的一类。

一个假随机故障:未解析的 ECMP bucket

上面那个坑值得单独提炼,因为它在生产里也会遇到。

text
vppctl show ip fib 10.2.0.0/24
  [0] [@3]: arp-ipv4: via 198.51.100.2 host-eth2   ← 未解析
  [1] [@5]: ipv4 via 198.51.100.6 host-eth3        ← 正常

后果是一部分流不通,另一部分完全正常。因为一条流会按五元组 确定性地哈希到某一个 bucket。

症状表现得像随机故障:

text
  · ping 通了,换个源端口再 ping 又不通
  · 同一个服务,有些客户端好、有些客户端坏
  · 今天正常,明天不正常(源端口变了)
  · 重启应用就好了(换了源端口)

排查方法show ip fib <前缀>,直接看有没有 arp-ipv4

这类"看起来随机、实际确定"的故障,是 ECMP 环境特有的。 本教程的 reset.sh 会主动把两条路径的邻接都解析好,就是为了避免它干扰实验。

动态路由 vs 静态路由

text
  ┌──────────────┬────────────────────────┬──────────────────────────┐
  │              │ 静态路由                 │ OSPF                     │
  ├──────────────┼────────────────────────┼──────────────────────────┤
  │ 加一条链路     │ 改所有相关设备            │ 自动学到                  │
  │ 链路故障      │ 人工切换                 │ 自动收敛                  │
  │ 多路径        │ 手工配等价路由            │ 自动 ECMP                │
  │ 排错         │ 看配置                   │ 看邻接 + LSA + FIB       │
  │ 出错风险      │ 配错一台就黑洞            │ 协议保证一致性             │
  │ 需要理解的东西  │ 几乎没有                 │ 区域、LSA、cost、定时器    │
  └──────────────┴────────────────────────┴──────────────────────────┘

代价是多了一层需要理解和排查的东西。邻接建不起来、区域设计不对、 LSA 泛洪范围过大 —— 这些都是静态路由不会有的问题。

小规模、拓扑稳定的场景,静态路由依然是对的选择。本书前十章 全部用静态路由,不是偷懒,是因为那些实验的拓扑本来就只有一两条路径。

小结

VPP 的角色纯转发面,路由协议由 bird / FRR 跑
同步链条bird → netns 内核路由表 → linux-cp netlink → VPP FIB
排错要分两段birdc show ospf neighbors(控制面)+ show ip fib(数据面)
netlab 集成OSPF 配置独立成文件挂载,--no-config 下也生效
实测结果零手工路由即互通;两条等价路径自动 ECMP
ECMP 切换一条路径失效,30 个包只丢 1 个
模拟链路故障本环境三种做法都不行(见上文),最后用删邻接代替
重要发现OSPF 组播不走 ip4-unicast,ACL / policer 拦不住它
假随机故障未解析的 ECMP bucket → 部分流不通,show ip fibarp-ipv4
未覆盖BFD 联动、多区域设计、BGP

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