← 全部文章

我的拓扑骗了我的那一天

一份针对我自己产品的工程事后分析,以及抓住它的那个实验室

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-gw2metric 20031, type inter areadfw1-cr1
dfw1-cr1metric 30001, type intra areaord1-cr1不是 dfw2-cr1
ord1-cr1metric 20001, type intra areajfk1-cr1
jfk1-cr1metric 10001, type intra arealhr1-cr1
lhr1-cr1metric 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,1930
区域间2,839360(12.68%)
全部4,032360(8.93%)

修法,以及我怎么知道它奏效了

引擎现在会走完整条链。在每一跳,它都用那台路由器自己的 LSDB、区域归属、ABR 身份和管理距离 组合,去评估那台路由器自己安装的、指向该目的地址的路由,然后沿着它真正选出的下一跳走下去, 如此往复。

然后我拿同样这 4,032 对、对照同一份基准真相,又跑了一遍:

检查项结果
路由器自己的度量值被精确复现4,032 / 4,032
路径类型(区域内 / 区域间 / 外部)完全一致4,032 / 4,032
已安装的完整 ECMP 下一跳集合完全一致4,032 / 4,032
链在请求的目的地以 delivered 终止4,032 / 4,032

不是”有所改善”。是每一对都完全一致,而且包含完整的等价下一跳集合,而不是随便挑出来的一个 成员。

修正后的路径:一张 10 跳的表格,带每台路由器自己的区域、端口和开销
修正后的路径:一张 10 跳的表格,带每台路由器自己的区域、端口和开销

这就是当初会凭空多出一跳的那条查询,如今显示的是每一台真正做出决定的路由器、它做决定所在的 区域,以及两侧的物理端口。总计 30061,是源自身安装的度量值:就是 show ip routelax1-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 台真实的路由器,可以问它们自己到底怎么想,而我终于一次性问了全 部,而不是只信任我抽查过的那两三台。

这就引出了那个显而易见的问题,也正是下一篇的主题:如果网络是唯一能告诉你模型对不对的东西,那 你要怎么把它变成一套测试?


下一篇:一名网络工程师的单元测试