早期折腾网络的时候,家里用 OpenWrt 做核心路由,拨号、上网、代理全都在它身上。这样的稳定性很有问题,只要我折腾点什么,全家的网络就会一起暴毙。就算我什么都没动,代理线路出了问题,也会影响到很多原本可以正常访问的网站。而且家里有很多设备根本不需要代理,比如一些游戏的服务,所以我甚至还要为它们单独设置分流排除,让它们不走代理。后来 Surge 推出了 Apple TV 版,我就打算改用 Surge 做旁路由,把代理从主路由身上拆出去。早期的方案旁路由的做法一般有几种,比如把需要代理的设备的网关和 DNS 手动改成旁路由的地址,或者划 VLAN,还有就是让主路由通过 DHCP 把旁路由的地址下发下去。手动改的话,家里的其他人和设备都得自己去管理,出了问题还得来找我,所以我最早用的是 DHCP 下发这个办法。DHCP 在下发的时候,可以带上几个特定编号的选项,用来指定不同的内容。其中就有网关(option 3)和 DNS(option 6),相当于把原本下发的内容覆盖掉。只要给需要代理的设备下发这两个选项,它们的流量就会走到旁路由上。但是一段时间用下来,还是有之前那个问题,就是家人没有办法主动去切换。因为设备拿到什么地址是路由器说了算,用的人在设备上没有任何办法,所以想退回正常的网络,也只能来找我。我也想过在上面做自动检测,如果旁路由断网了,就不再下发这个内容。但是 DHCP 每次下发都有租约时长,要等到下一次租约,设备才会重新去拿,所以这样做也有时效性的问题,恢复得很慢,效果还是不好。两个 WiFi当时我家里正好有两台小米的路由器做 AP,可以设置成不同的名字。所以我就想,能不能设置两个 WiFi,一个不经过代理,另一个经过代理。这样家里人想用哪个就连哪个,想退回正常的网络,换个 WiFi 就行,不用再等什么租约。最直接的方案是一个 WiFi 接到旁路由上,走旁路由的网络,另一个 WiFi 接到主网关。但是我希望家里都在同一个网段里,因为这样不管是管理,还是智能家居,还是局域网里的各种服务,都可以正常使用。我不想分成多个互不相连,或者需要做 NAT 转换的网络。好在我家的主路由已经换成了 MikroTik 的 RB5009U,它有一个正常路由器都有的功能,路由表。那我就可以把折腾 BGP 和 ASN 的经验挪过来了。思路我新增了一台 Apple TV Surge 旁路由,相当于给网络多开了一个出口,但保持所有设备的默认出口依然指向主路由。想要让旁路由起作用,我只需要在防火墙里写规则,让特定流量(比如从某个 AP 网口进来的流量,或者来自指定 IP、MAC 的流量)改走旁路由。让流量走另一个网关,本质上就是让它使用另一套路由。所以我需要新建一张路由表,并给需要走旁路由的数据包打上标记。带标记的包不再查默认的 main 表,而是查这张新表。新表里只有一条默认路由,下一跳指向 Surge。这其实和 BGP 中按策略选择下一跳的思路相同,唯一的区别在于这里选择的依据是数据包的来源。RouterOS 对数据包的处理是分阶段的,每条规则只在特定阶段生效。所以我们必须先弄清数据包会依次经过哪些阶段,以及这些阶段的先后顺序。数据包在 RouterOS 里的流向RouterOS 的防火墙分成几条链,一个 IP 数据包进来之后,大致是这样走的。这是我按官方文档的 Packet Flow in RouterOS 整理的简化版。文档里有完整的图,包括网桥、硬件卸载、IPsec 这些情况,其中转发的这条路径在官方的图里长这样。Routed forward 的链顺序,来自 MikroTik 文档这里面有几个位置的顺序对这个需求很关键。路由决策排在 prerouting 和 dstnat 之后,所以数据包在被决定走哪条路之前,还有两次被改动的机会,一次是 mangle prerouting 里给它打标签,一次是 dstnat 里改它的目标地址。而且路由器查路由表的时候,用的已经是改过之后的目标地址。srcnat 排在路由决策之后,已经是出口前的最后一步。因为路由已经选完了,所以它只能改数据包的源地址。也就是说,给数据包打路由标签,只能放在路由决策之前的 mangle prerouting 里。如果放到 forward 里,那时候路由已经选完了,标签就来不及起作用。路由器自己发出的流量走 output 链,它会先做一次路由决策,再经过 mangle output。文档里在这后面还画了一个 routing adjustment,说它是一个 workaround,让 output 链里的 mangle 也能做策略路由。不过这篇里的流量都是从设备发过来,经过路由器的,所以用不上这一段。Note下文的地址和规则只处理 IPv4,不会自动接管 IPv6。若客户端同时有 IPv6 出口,需要另行设计对应的策略,不能把这些规则当成全栈代理配置。路由表首先新建一个路由表。BASH复制/routing/table/add name=rsurge fib建表的时候带上 fib,因为官方的策略路由示例里创建自定义表就是这么写的,这样表里的路由会进入转发时使用的那一份 FIB。另外在 RouterOS v7 里,路由标签必须对应一张事先建好的路由表,如果没有这张表,mangle 里写的标签就没有用。然后在这张表里加一条默认路由,指向 Surge 的地址。BASH复制/ip/route/add dst-address=0.0.0.0/0 gateway=10.10.77.4 routing-table=rsurge distance=1我的主路由是 10.10.77.1,Surge 是 10.10.77.4,整个网段是 10.10.77.0/24。现在家里有了两张表,main 里的默认路由还是拨号接口,rsurge 里的默认路由指向 Surge。后面的规则会引用 lan-ip 地址列表,先把本地网段加进去。如果还有其他需要直接互通的内网段,也要追加到这个列表。BASH复制/ip/firewall/address-list/add list=lan-ip address=10.10.77.0/24打标签接下来写 mangle 规则,决定谁的流量去查 rsurge。Important下面按物理桥端口匹配的 in-bridge-port,需要先开启网桥的 use-ip-firewall,默认值是 no。这个选项会让桥接流量经过 IP 防火墙,并影响 Bridge FastPath 和 CPU 开销,启用前要评估设备性能。若改用后面的源 IP 地址列表方案,则不依赖这个桥端口匹配条件。详细规则见 MikroTik 的 Common Firewall Matchers and Actions 和 Bridging and Switching。BASH复制/interface/bridge/settings/set use-ip-firewall=yesBASH复制/ip/firewall/mangle/add chain=prerouting action=mark-routing \ new-routing-mark=rsurge passthrough=yes \ src-address=!10.10.77.4 dst-address-list=!lan-ip \ in-bridge-port=ether8这条规则里有三个条件。in-bridge-port=ether8 决定了谁走代理。我后来接了一个二级交换机,需要代理的 AP 和设备都接在这个口下面,所以从这个口进来的流量就都会被标记,不用给每个设备都写规则。mangle 可以直接匹配 bridge 里具体的物理口,用起来很省事。dst-address-list=!lan-ip 把内网的流量排除。lan-ip 是一个地址列表,里面是内网的地址段。因为目标是内网的流量不应该去找 Surge,应该直接走正常的路由,这样智能家居和局域网里的各种服务才能继续使用。src-address=!10.10.77.4 把 Surge 自己排除。Surge 处理完请求之后,会把要出去的流量发给主路由,如果不排除,这些流量也会被打上标签,又送回给 Surge,就形成了环路。如果设备没有集中在一个口上,也可以改用地址列表,把需要代理的设备的 IP 都放进去,再用 src-address-list 去匹配。BASH复制/ip/firewall/address-list/add list=surge-devices address=10.10.77.50 /ip/firewall/mangle/add chain=prerouting action=mark-routing \ new-routing-mark=rsurge passthrough=yes \ src-address-list=surge-devices dst-address-list=!lan-ipFastTrackRouterOS 默认配置里有一条 FastTrack 的规则,它会让已经建立的连接绕过后面大部分的处理,以提高转发速度。官方文档在 FastTrack 那一节里提到,FastTrack 查路由是在设置路由标签之前,所以它只对 main 路由表有效。也就是说,用了 mark-routing 的连接如果被 FastTrack 了,数据包就会按 main 表去走,不会去 Surge。所以如果有这条规则,就需要把走 Surge 的连接排除在 FastTrack 之外。做法是在 mangle 里多加一条规则,用同样的条件给这些连接打一个连接标记,然后在 FastTrack 的规则里加上 connection-mark=no-mark,这样就只有没有标记的连接才会走 FastTrack。BASH复制/ip/firewall/mangle/add chain=prerouting action=mark-connection \ new-connection-mark=surge passthrough=yes \ src-address=!10.10.77.4 dst-address-list=!lan-ip \ in-bridge-port=ether8 /ip/firewall/filter/set [find action=fasttrack-connection] connection-mark=no-mark这条 mark-connection 放在前面那条 mark-routing 的前面或者后面都行,因为两条的 passthrough 都是 yes,数据包会继续往下匹配。DNS到这里,流量已经能去 Surge 了,但是还剩下 DNS。Surge 在增强模式下使用的是 Fake IP。客户端查询域名的时候,Surge 会返回一个 198.18.0.0/15 里的虚拟地址,客户端再用这个地址去连,Surge 看到之后就知道对应的是哪个域名,然后按规则处理。所以客户端的 DNS 请求必须到 Surge 手上,不然拿到的就是真实的地址,Fake IP 也就没有意义了。我在 dstnat 里把这些设备的 DNS 请求改发到 Surge 的 Fake IP DNS 地址 198.18.0.2。BASH复制/ip/firewall/nat/add chain=dstnat action=dst-nat \ protocol=udp dst-port=53 \ src-address=!10.10.77.4 in-bridge-port=ether8 \ to-addresses=198.18.0.2两条 DNS 规则也用 src-address=!10.10.77.4 排除 Surge 自身,避免它接在同一个桥口时,上游 DNS 又被重定向回自己。DNS 也会使用 TCP,所以再加一条相同条件的 TCP 53 规则。DoH、DoT 等加密 DNS 不在这两条规则的处理范围内,需要在客户端或网络策略里另行管理。如果前面的设备选择条件改成了源地址列表,这里的两条规则也要同步调整。BASH复制/ip/firewall/nat/add chain=dstnat action=dst-nat \ protocol=tcp dst-port=53 \ src-address=!10.10.77.4 in-bridge-port=ether8 \ to-addresses=198.18.0.2这里顺序又起作用了,因为 DNS 请求会先经过 mangle prerouting,再到 dstnat,最后才是路由决策。如果客户端的 DNS 指向的是外部地址,那它不在 lan-ip 里,在 mangle 里就已经被打上了标签。dstnat 只是把目标地址改成 198.18.0.2,之后去查 rsurge,走默认路由到 Surge。如果客户端的 DNS 指向路由器自己,目标就是内网地址,所以在 mangle 里会被 lan-ip 排除,没有标签。到了 dstnat,目标地址变成 198.18.0.2,之后进入路由决策,但是这时候没有标签,会去查 main。因此 main 里也要有一条路由,把 198.18.0.0/16 指向 Surge。BASH复制/ip/route/add dst-address=198.18.0.0/16 gateway=10.10.77.4最后还需要一条 srcnat。BASH复制/ip/firewall/nat/add chain=srcnat action=src-nat \ dst-address=198.18.0.2 \ to-addresses=10.10.77.1客户端发 DNS 请求的时候,目标是路由器的 10.10.77.1,所以它期待收到的回复也来自这个地址。如果请求被改发给 Surge,Surge 就会直接把回复发给客户端,源地址是 198.18.0.2,客户端不认,就会直接丢掉。加上 srcnat 之后,请求的源地址被换成路由器自己,Surge 的回复就会先回到路由器,路由器再通过连接跟踪还原成原来的样子交给客户端。效果现在家里有两个 WiFi,一个正常上网,一个经过 Surge,两个在同一个网段里,所以设备之间可以互相访问。家里人遇到网络问题,换一个 WiFi 就行,不需要再来找我。而且 Surge 就算出了问题,也不会影响到主路由和不需要代理的设备。以后如果有特殊的服务需要特殊的路由,只要往 rsurge 里加一条路由就行。比如某个网段需要绕过 Surge,改走实际的 WAN 下一跳。这里不能把网关填成主路由自己的 LAN 地址 10.10.77.1,否则并不是一个有效的上游出口。静态 WAN 网关可以按下面的形式写,@main 表示在 main 表中解析这个下一跳;如果是 PPPoE 拨号,则按实际配置使用拨号接口。BASH复制/ip/route/add dst-address=<网段> gateway=<实际WAN网关>@main routing-table=rsurge这里的 <网段> 和 <实际WAN网关> 都是占位值,需替换成自己网络里的值。OpenWrt 上也能做同样的事,用 ip rule 和 ip route table,思路是一样的,数据包先被标记,再去查不同的路由表。
早期折腾网络的时候,家里用 OpenWrt 做核心路由,拨号、上网、代理全都在它身上。这样的稳定性很有问题,只要我折腾点什么,全家的网络就会一起暴毙。就算我什么都没动,代理线路出了问题,也会影响到很多原本可以正常访问的网站。而且家里有很多设备根本不需要代理,比如一些游戏的服务,所以我甚至还要为它们单独设置分流排除,让它们不走代理。
后来 Surge 推出了 Apple TV 版,我就打算改用 Surge 做旁路由,把代理从主路由身上拆出去。
早期的方案
旁路由的做法一般有几种,比如把需要代理的设备的网关和 DNS 手动改成旁路由的地址,或者划 VLAN,还有就是让主路由通过 DHCP 把旁路由的地址下发下去。手动改的话,家里的其他人和设备都得自己去管理,出了问题还得来找我,所以我最早用的是 DHCP 下发这个办法。
DHCP 在下发的时候,可以带上几个特定编号的选项,用来指定不同的内容。其中就有网关(option 3)和 DNS(option 6),相当于把原本下发的内容覆盖掉。只要给需要代理的设备下发这两个选项,它们的流量就会走到旁路由上。
但是一段时间用下来,还是有之前那个问题,就是家人没有办法主动去切换。因为设备拿到什么地址是路由器说了算,用的人在设备上没有任何办法,所以想退回正常的网络,也只能来找我。
我也想过在上面做自动检测,如果旁路由断网了,就不再下发这个内容。但是 DHCP 每次下发都有租约时长,要等到下一次租约,设备才会重新去拿,所以这样做也有时效性的问题,恢复得很慢,效果还是不好。
两个 WiFi
当时我家里正好有两台小米的路由器做 AP,可以设置成不同的名字。所以我就想,能不能设置两个 WiFi,一个不经过代理,另一个经过代理。这样家里人想用哪个就连哪个,想退回正常的网络,换个 WiFi 就行,不用再等什么租约。
最直接的方案是一个 WiFi 接到旁路由上,走旁路由的网络,另一个 WiFi 接到主网关。但是我希望家里都在同一个网段里,因为这样不管是管理,还是智能家居,还是局域网里的各种服务,都可以正常使用。我不想分成多个互不相连,或者需要做 NAT 转换的网络。
好在我家的主路由已经换成了 MikroTik 的 RB5009U,它有一个正常路由器都有的功能,路由表。那我就可以把折腾 BGP 和 ASN 的经验挪过来了。
思路
我新增了一台 Apple TV Surge 旁路由,相当于给网络多开了一个出口,但保持所有设备的默认出口依然指向主路由。想要让旁路由起作用,我只需要在防火墙里写规则,让特定流量(比如从某个 AP 网口进来的流量,或者来自指定 IP、MAC 的流量)改走旁路由。
让流量走另一个网关,本质上就是让它使用另一套路由。所以我需要新建一张路由表,并给需要走旁路由的数据包打上标记。带标记的包不再查默认的 main 表,而是查这张新表。新表里只有一条默认路由,下一跳指向 Surge。
这其实和 BGP 中按策略选择下一跳的思路相同,唯一的区别在于这里选择的依据是数据包的来源。
RouterOS 对数据包的处理是分阶段的,每条规则只在特定阶段生效。所以我们必须先弄清数据包会依次经过哪些阶段,以及这些阶段的先后顺序。
数据包在 RouterOS 里的流向
RouterOS 的防火墙分成几条链,一个 IP 数据包进来之后,大致是这样走的。
这是我按官方文档的 Packet Flow in RouterOS 整理的简化版。文档里有完整的图,包括网桥、硬件卸载、IPsec 这些情况,其中转发的这条路径在官方的图里长这样。
这里面有几个位置的顺序对这个需求很关键。
路由决策排在 prerouting 和 dstnat 之后,所以数据包在被决定走哪条路之前,还有两次被改动的机会,一次是 mangle prerouting 里给它打标签,一次是 dstnat 里改它的目标地址。而且路由器查路由表的时候,用的已经是改过之后的目标地址。
srcnat 排在路由决策之后,已经是出口前的最后一步。因为路由已经选完了,所以它只能改数据包的源地址。
也就是说,给数据包打路由标签,只能放在路由决策之前的 mangle prerouting 里。如果放到 forward 里,那时候路由已经选完了,标签就来不及起作用。
路由器自己发出的流量走 output 链,它会先做一次路由决策,再经过 mangle output。文档里在这后面还画了一个 routing adjustment,说它是一个 workaround,让 output 链里的 mangle 也能做策略路由。不过这篇里的流量都是从设备发过来,经过路由器的,所以用不上这一段。
下文的地址和规则只处理 IPv4,不会自动接管 IPv6。若客户端同时有 IPv6 出口,需要另行设计对应的策略,不能把这些规则当成全栈代理配置。
路由表
首先新建一个路由表。
建表的时候带上 fib,因为官方的策略路由示例里创建自定义表就是这么写的,这样表里的路由会进入转发时使用的那一份 FIB。另外在 RouterOS v7 里,路由标签必须对应一张事先建好的路由表,如果没有这张表,mangle 里写的标签就没有用。
然后在这张表里加一条默认路由,指向 Surge 的地址。
我的主路由是 10.10.77.1,Surge 是 10.10.77.4,整个网段是 10.10.77.0/24。
现在家里有了两张表,main 里的默认路由还是拨号接口,rsurge 里的默认路由指向 Surge。
后面的规则会引用 lan-ip 地址列表,先把本地网段加进去。如果还有其他需要直接互通的内网段,也要追加到这个列表。
打标签
接下来写 mangle 规则,决定谁的流量去查 rsurge。
下面按物理桥端口匹配的 in-bridge-port,需要先开启网桥的 use-ip-firewall,默认值是 no。这个选项会让桥接流量经过 IP 防火墙,并影响 Bridge FastPath 和 CPU 开销,启用前要评估设备性能。若改用后面的源 IP 地址列表方案,则不依赖这个桥端口匹配条件。详细规则见 MikroTik 的 Common Firewall Matchers and Actions 和 Bridging and Switching。
这条规则里有三个条件。
in-bridge-port=ether8 决定了谁走代理。我后来接了一个二级交换机,需要代理的 AP 和设备都接在这个口下面,所以从这个口进来的流量就都会被标记,不用给每个设备都写规则。mangle 可以直接匹配 bridge 里具体的物理口,用起来很省事。
dst-address-list=!lan-ip 把内网的流量排除。lan-ip 是一个地址列表,里面是内网的地址段。因为目标是内网的流量不应该去找 Surge,应该直接走正常的路由,这样智能家居和局域网里的各种服务才能继续使用。
src-address=!10.10.77.4 把 Surge 自己排除。Surge 处理完请求之后,会把要出去的流量发给主路由,如果不排除,这些流量也会被打上标签,又送回给 Surge,就形成了环路。
如果设备没有集中在一个口上,也可以改用地址列表,把需要代理的设备的 IP 都放进去,再用 src-address-list 去匹配。
FastTrack
RouterOS 默认配置里有一条 FastTrack 的规则,它会让已经建立的连接绕过后面大部分的处理,以提高转发速度。官方文档在 FastTrack 那一节里提到,FastTrack 查路由是在设置路由标签之前,所以它只对 main 路由表有效。也就是说,用了 mark-routing 的连接如果被 FastTrack 了,数据包就会按 main 表去走,不会去 Surge。
所以如果有这条规则,就需要把走 Surge 的连接排除在 FastTrack 之外。做法是在 mangle 里多加一条规则,用同样的条件给这些连接打一个连接标记,然后在 FastTrack 的规则里加上 connection-mark=no-mark,这样就只有没有标记的连接才会走 FastTrack。
这条 mark-connection 放在前面那条 mark-routing 的前面或者后面都行,因为两条的 passthrough 都是 yes,数据包会继续往下匹配。
DNS
到这里,流量已经能去 Surge 了,但是还剩下 DNS。
Surge 在增强模式下使用的是 Fake IP。客户端查询域名的时候,Surge 会返回一个 198.18.0.0/15 里的虚拟地址,客户端再用这个地址去连,Surge 看到之后就知道对应的是哪个域名,然后按规则处理。所以客户端的 DNS 请求必须到 Surge 手上,不然拿到的就是真实的地址,Fake IP 也就没有意义了。
我在 dstnat 里把这些设备的 DNS 请求改发到 Surge 的 Fake IP DNS 地址 198.18.0.2。
两条 DNS 规则也用 src-address=!10.10.77.4 排除 Surge 自身,避免它接在同一个桥口时,上游 DNS 又被重定向回自己。DNS 也会使用 TCP,所以再加一条相同条件的 TCP 53 规则。DoH、DoT 等加密 DNS 不在这两条规则的处理范围内,需要在客户端或网络策略里另行管理。如果前面的设备选择条件改成了源地址列表,这里的两条规则也要同步调整。
这里顺序又起作用了,因为 DNS 请求会先经过 mangle prerouting,再到 dstnat,最后才是路由决策。
如果客户端的 DNS 指向的是外部地址,那它不在 lan-ip 里,在 mangle 里就已经被打上了标签。dstnat 只是把目标地址改成 198.18.0.2,之后去查 rsurge,走默认路由到 Surge。
如果客户端的 DNS 指向路由器自己,目标就是内网地址,所以在 mangle 里会被 lan-ip 排除,没有标签。到了 dstnat,目标地址变成 198.18.0.2,之后进入路由决策,但是这时候没有标签,会去查 main。因此 main 里也要有一条路由,把 198.18.0.0/16 指向 Surge。
最后还需要一条 srcnat。
客户端发 DNS 请求的时候,目标是路由器的 10.10.77.1,所以它期待收到的回复也来自这个地址。如果请求被改发给 Surge,Surge 就会直接把回复发给客户端,源地址是 198.18.0.2,客户端不认,就会直接丢掉。加上 srcnat 之后,请求的源地址被换成路由器自己,Surge 的回复就会先回到路由器,路由器再通过连接跟踪还原成原来的样子交给客户端。
效果
现在家里有两个 WiFi,一个正常上网,一个经过 Surge,两个在同一个网段里,所以设备之间可以互相访问。家里人遇到网络问题,换一个 WiFi 就行,不需要再来找我。而且 Surge 就算出了问题,也不会影响到主路由和不需要代理的设备。
以后如果有特殊的服务需要特殊的路由,只要往 rsurge 里加一条路由就行。比如某个网段需要绕过 Surge,改走实际的 WAN 下一跳。这里不能把网关填成主路由自己的 LAN 地址 10.10.77.1,否则并不是一个有效的上游出口。静态 WAN 网关可以按下面的形式写,@main 表示在 main 表中解析这个下一跳;如果是 PPPoE 拨号,则按实际配置使用拨号接口。
这里的 <网段> 和 <实际WAN网关> 都是占位值,需替换成自己网络里的值。
OpenWrt 上也能做同样的事,用 ip rule 和 ip route table,思路是一样的,数据包先被标记,再去查不同的路由表。