我的拓扑骗了我的那一天
一份针对我自己产品的工程事后分析,以及抓住它的那个实验室
Michel Wijnberg
我做 Osprey,是为了让它告诉我关于网络的真相。后来我发现,它一直在骗我。自信地骗,白纸黑 字地骗,就在我一直拿给别人演示的那张画布上。
下面是事情的经过、我是怎么抓到它的,以及我改了什么。这是这个系列里最有用的一篇,因为我写 过的所有关于诚实的话都很廉价,直到被审计的对象是你自己的作品。
我原本相信的
Osprey 的路径引擎做的事,几乎所有路径工具都在做。给定一个源和一个目的,它在 OSPF 拓扑上 从源跑一次区域感知的 Dijkstra,然后把最便宜的那条路画出来。
我有充分的理由信任它。它遵守了我在意的那些 RFC 2328 规则:区域内优先于区域间、区域间要经 骨干中转、E1 先于 E2、ECMP 被枚举而不是被折叠。它有测试。它和我对这个实验室的心智模型对 得上。我点两台路由器,就会亮起一条像模像样的路径。
它同时也在每八次跨区域查询里有一次,描述了一条任何报文都不会走的路由。
我是怎么发现的
不是靠缺陷报告,是靠一件杂活。
我当时在给完全不相干的另一件事搭验证工装,需要一份基准真相数据集来对照。实验室在 AS 200
里有 64 台路由器,全都能 SSH 上去,全都愿意打印自己的路由表。于是我干了件又笨又显然的事:
我登进了全部 64 台,把每一台的 show ip route ospf 都导了出来。
然后,我没有拿眼睛去扫,而是把每台路由器的真实路由表,对照 Osprey 的路径引擎所声称的路 径,逐一比对了全部 4,032 个有序的源/目的对。
其中 360 个对不上。
不是”开销略有出入”。是画出来的路径里含着一台报文可被证明从未经过的路由器。
那个典型案例
这是其中一例,每一行都是在设备上实测的,不是建模算出来的:
| 路由器 | 对该目的地的说法 | 转发给 |
|---|---|---|
dfw1-gw2 | metric 20031, type inter area | dfw1-cr1 |
dfw1-cr1 | metric 30001, type intra area | ord1-cr1 ← 不是 dfw2-cr1 |
ord1-cr1 | metric 20001, type intra area | jfk1-cr1 |
jfk1-cr1 | metric 10001, type intra area | lhr1-cr1 |
lhr1-cr1 | metric 1, connected | 送达 |
Osprey 画的是:
dfw1-gw2 → dfw1-cr1 → dfw2-cr1 → iad2-cr1 → ams1-cr1 → lhr1-cr1
报文根本到不了 dfw2-cr1。Osprey 那条路径更便宜。它只是没有任何一台路由器会真的构建
出来。
有两条互相独立的 RFC 2328 规则让 dfw1-cr1 拒绝了这条便宜走廊,而这两条我在”单台中间路由
器”这个层面上一条都没实现:
§16 步骤 (3) / §16.2 第 1 段:骨干区闸门。连接到多个区域的路由器只检查来自骨干区
的 summary-LSA。dfw2-cr1 那条开销为 20011 的更便宜的 summary 确实存在于 dfw1-cr1 的
4.6.23.0 区域数据库里(我用 show ip ospf database summary 确认过,因为我自己也不信),
但它不是候选项,因为 dfw1-cr1 是 ABR,而那条 summary 不是从区域 0 进来的。RFC 3509
§2.1/§2.2,也就是这些设备上实际运行的 Cisco ABR 行为,把这一条件挂在”有一条活跃的骨干连
接”之上,而 dfw1-cr1 正好有。
§16.2 步骤 (6):区域内胜过区域间,没有例外。“If the paths present in the table are
intra-area paths, do nothing with the LSA.” dfw1-cr1 手里已经有一条度量值为 30001 的区
域内路径。那条 20011 的 summary 输给了它。**度量值根本不会进入比较。**这是一条关于路径
类型的规则。
这两条规则中的任意一条单独就足以杀死那条走廊。我的引擎两条都看不见,因为它从来没问过
dfw1-cr1 任何事。它问的是源路由器眼中的世界,然后就穿过另外八台路由器画了一条线,好
像它们也是这么看的一样。
这个错误的形状
这一点值得精确地命名,因为具体的 OSPF 细节远不如它所属的类别有意思。
我建模的是一条路径,而网络实现的是一串互相独立的决策。这两样东西完全一致的前提, 是沿途每一台路由器都在同一个输入集合上使用同一条决策规则。在单个 OSPF 区域内部它们确实如 此(同一份 LSDB、同一个算法、同一个结果),这也正是区域内对的缺陷数为:
1,193 中的 0。
一旦牵扯到 ABR,输入集合就不再共享了:ABR 会刻意去看一组和它周围路由器不同的 LSA。我 的模型没有地方安放这个事实,因为我的模型只有一张图和一次 Dijkstra,而不是六十四台各有主张 的路由器。
| 对数 | 含有没有任何路由器会走的跳 | |
|---|---|---|
| 区域内 | 1,193 | 0 |
| 区域间 | 2,839 | 360(12.68%) |
| 全部 | 4,032 | 360(8.93%) |
修法,以及我怎么知道它奏效了
引擎现在会走完整条链。在每一跳,它都用那台路由器自己的 LSDB、区域归属、ABR 身份和管理距离 组合,去评估那台路由器自己安装的、指向该目的地址的路由,然后沿着它真正选出的下一跳走下去, 如此往复。
然后我拿同样这 4,032 对、对照同一份基准真相,又跑了一遍:
| 检查项 | 结果 |
|---|---|
| 路由器自己的度量值被精确复现 | 4,032 / 4,032 |
| 路径类型(区域内 / 区域间 / 外部)完全一致 | 4,032 / 4,032 |
| 已安装的完整 ECMP 下一跳集合完全一致 | 4,032 / 4,032 |
链在请求的目的地以 delivered 终止 | 4,032 / 4,032 |
不是”有所改善”。是每一对都完全一致,而且包含完整的等价下一跳集合,而不是随便挑出来的一个 成员。
这就是当初会凭空多出一跳的那条查询,如今显示的是每一台真正做出决定的路由器、它做决定所在的
区域,以及两侧的物理端口。总计 30061,是源自身安装的度量值:就是 show ip route 在
lax1-gw1 上打印出来的那个数字。
而在链不可计算的地方(OSPFv3、IS-IS、EIGRP、部分作用域、带虚链路的区域),引擎不会悄悄
退回旧画法。它返回以源为根的视图,附上一个点名原因的 model_note,并作为一个独立的解释步骤
呈现出来。那个拒绝就是第四篇文章的主题,而它之所以存
在,正是因为这个缺陷。
再说个小的,好平衡一下
不是每一处自伤都是算法层面的。下面这个只有两行,花了我一个下午。
Osprey 的 GRE 记录器会建立真实的 OSPF 邻接,所以它必须产生一条 Router-LSA。早期,它把隧道的 内层子网作为 stub 网络通告了出去:看起来正确、符合 RFC、也完全合情合理。
于是区域内的每一台路由器都装上了一条指向隧道前缀的路由。那条路由盖过了 GRE 隧道本身所依赖的 承载路径。外层传输断了。邻接掉了。记录器重连、重新产生 LSA,然后又来一遍。
修法现在写在代码注释里,免得有人不小心把它删掉:
// Only the P2P link is advertised. NO stub network for the tunnel
// inner subnet. Advertising the tunnel subnet (even at max cost) causes SPF
// on other routers to install a route for the tunnel prefix, which can
// disrupt the GRE outer path and kill the adjacency.
一个把自己所监控的东西弄挂的监控工具,是一种特别难堪的存在,而这种事我在图纸上是发现不了的。
实验室到底是干什么用的
我以前把实验室描述成”我演示 Osprey 的地方”。那是个错误的岗位说明,而把它改对之后,我的开发 方式也变了。
实验室不是为了让 Osprey 好看。它是为了证明 Osprey 是错的。
这意味着实验室的价值,取决于它有多大本事反驳我:所以它是刻意做得不好对付的。三家不共享任何协 议的运营商。十五个 OSPF 区域,尽管三个演示起来更好看。一个有十七个 level-1 区域的 IS-IS 域。 EIGRP,它根本没有链路状态数据库,能推翻 OSPF 代码所做的每一条假设。还有双栈链路,全部 124 条 的 v4 与 v6 开销都差了 10 倍。
这些没有一样能让截图更好看。它们每一样都抓出过东西。
那 360 条虚构走廊,不是靠仔细阅读发现的,也不是靠一个我聪明到能提前写出来的测试发现的。它们 之所以被发现,是因为我有 64 台真实的路由器,可以问它们自己到底怎么想,而我终于一次性问了全 部,而不是只信任我抽查过的那两三台。
这就引出了那个显而易见的问题,也正是下一篇的主题:如果网络是唯一能告诉你模型对不对的东西,那 你要怎么把它变成一套测试?
下一篇:一名网络工程师的单元测试。