Staging in Policy Based Routing

一些 Rule based Tunnel 可以允许用户编写不同的规则,决定哪些流量走哪个 Tunnel,例如下面一段路由起手式:

GEOIP,CN,Direct
MATCH,Proxy

此外,还有很多用于策略路由的规则,例如:

DOMAIN-SUFFIX,github.com,Proxy
DOMAIN-KEYWORD,github,Proxy
DOMAIN,githubusercontent.com,Proxy

甚至你可以用不同的流量出口,而不是一股脑地写 Proxy

DOMAIN-SUFFIX,netflix.com,HK
DOMAIN-SUFFIX,openai.com,US

诸如此类。

# 为什么需要 Staging

Staging 是编程语言领域的一个概念,本质上是一种运行时优化策略。一种比较浅显但不太准确的解释是《分层 JIT》。 JIT 是什么就不解释了。

在这种策略路由中,Staging 的思想能够带来很大的性能提升。尤其是对于使用了 Fake DNS 的配置尤其明显。 如果你熟悉这些 Rule based Tunnel 的实现,就会知道,在启用 Fake DNS 的模式下,会有许多的 DNS 请求被转发到这些 Tunnel 内置的 DNS 服务器, 但是这个 DNS 并不会返回一个真实的 IP 地址,而是返回一个特殊的 IP 地址,例如 198.18.0.0/16 中的任意一个地址。 当客户端使用这个虚假的地址请求数据时,Tunnel 会查找该虚假地址对应的域名,然后将域名和请求头一起转发到远端服务器, 由远端服务器进行 DNS 解析并负责发起请求。

在这个过程中,客户端完全不需要知道被代理访问的域名的真实 IP 地址,这完美地解决了 DNS 污染的问题。

听起来很美好,对吗?但是这里有一个不可忽视的性能影响。即,每当客户端的流量被路由到 Tunnel 时, 如果 Tunnel 中使用的路由策略非常多,那么通常需要对每个入站的流量都进行一次路由规则匹配, 最糟糕的是,就算 Tunnel 之前对这个域名已经匹配过一次了,在下一次遇到这个域名时,还是需要再次匹配。 因为用户可能在两次请求之间修改了配置文件,修改了路由规则,改变了该域名的流量出口,例如,从 HK 切换为 US

路由结果缓存是一种有效的解决办法,但这种方法仍然不完美,因为当流量到来时,依然要从缓存中查找,尽管开销相比于重新匹配要小很多。

# Staging 实现策略路由的动态编译

因此,我们可以使用 Staging 策略,动态地将路由规则分为两个阶段:

  1. 在 DNS 阶段,当域名 A 被分配到 Fake DNS 时,直接将该 Fake DNS 的结果返回给客户端。客户端可以直接用这个结果进行请求。这和上文所述的 Fake DNS 的工作方式一致。 因此被称为 Stage 1
  2. 在后台运行一个线程/协程,将 A 域名通过远端服务器进行 DNS 解析,得到该域名的真实解析地址。 同时,由于该过程在后台运行,因为我们可以在这里进行任意的优化。例如,由于此刻已经拥有域名和其真实的解析地址, 因此可以直接进行路由决策,即,直接运行一次路由规则匹配,得到该域名的流量出口,假设记为 HK。 接下来,可以对这样的结果进行缓存,即可实现上一节中提到的路由结果缓存。并且这个过程是无感的。 这个阶段被称为 Stage 2
  3. 这是最激动人心的一步。如果 Tunnel 支持 TUN 模式,那么我们可以直接为这个出口 HK 创建一个单独的 TUN 设备, 直接将所有该 TUN 的入站流量通过出口 HK 转发出去。 接着,使用 netlink 添加路由表,让 A 的解析地址走这个 TUN 设备。通常这样的操作是一个类似 ip route add <IP-of-A>/32 dev tunHK 的命令。 这便是 Stage 3

可以发现,在 Stage 3 之后,已经不再需要任何路由规则匹配,甚至连缓存查找都不需要! 我称之为《Rule Once, Route Everywhere》。

这也可以被视为:将 Tunnel 的全部路由规则,动态地编译到系统的路由表上,最终路由表收敛,得到一个类似 nchnroutes 的路由表。 但和 nchnroutes 不同,这个路由表是动态的,而且支持动态切换策略路由并“重新编译”,而 chnroutes 要实现这样的效果需要付出很大的努力。

从实现的效果上来看,这个系统十分类似 JIT 编译器。同时,这套系统可以和许多动态路由协议互相配合工作,例如 BGP、OSPF 等等。

如果你的路由器设备支持 IPv6 的策略路由,例如 RouterOS v7,那么这套系统也可以很好地工作在 IPv6 上。 并不需要像一些其他方案为了追求稳定性而牺牲了 IPv6 的支持。当然,这也要求你了解 IPv6 在路由上的一些基本知识, 例如 PMTU 等等。

# 细节问题

# 如果某个域名的流量出口是 Direct,即直连,那么 Stage 3 该如何处理?

什么也不需要做。因为 Direct 意味着不需要任何代理,因此不需要任何将其导向 Tunnel 的路由条目,因此也不需要创建 TUN 设备。 该域名的路由决策通常会被一条是 0.0.0.0/0 dev WAN 的路由条目决定。这很显然就是直连。

# 当用户切换某个域名的流量出口时,例如从 HK 切换到 US,那么需要做什么?

  1. 类似地,创建一个 US 的专属 TUN 设备。
  2. ip route replace <IP-of-A>/32 dev tunUS

第二步中的 <IP-of-A> 可以直接从 Stage 2 中得到的缓存中获取。 如果切换的出口涉及到 Direct,也只是要么删除路由条目,要么增加路由条目,而不是上面的 ip route replace。 这完全就是免费的。

# 可能的不足

系统收敛的时间也许很长,但目前缺乏数据,不好判断。

# Reference