使用 RouterOS,OSPF 和树莓派为国内外 IP 智能分流

这篇问题记录了我在 RouterOS 上通过 OSPF 协议把国外的流量动态拉取到旁路由上的配置过程。文章里面包含了 IPv4 和 IPv6 的配置。

更新历史

2022.05 :修改为 RouterOS 7 配置格式

前言

相信各位使用 RouterOS 做为主路由的同学都遇到过需要为国内和国外 IP 地址分流的需求。国内 IP 直接走运营商出口,而国外 IP 走隧道加速访问。网络上目前能搜到的教程基本上都是使用 RouterOS 的 iptables + ipset 来给链接打标记(比如 autorosvpn,然后通过类似这样的配置来做 PBR (Policy Based Routing)),然后走特殊的路由表来转发到树莓派上。一开始我也尝试了这种做法,但是遇到了以下几个问题:

  1. IP 列表基本上每周都会有变化,如果希望更新,需要全量重新删除创建 address-list,这个操作不是特别的快,而且在更新过程中可能会因为路由震荡导致连接闪断或者丢包。
  2. 没有健康检查,如果树莓派断网或者掉电 RouterOS 还是会傻傻的把流量转发过去,导致所有的国外 IP 都被黑洞路由。
  3. RouterOS 6 不支持 IPv6 的 PBR,看论坛上 8 年前就有帖子问了,但是一直没出。据说 RouterOS 7 发布后会支持。这也可能是为什么我从网上能找到的几乎所有分流教程都只做了 IPv4 的分流的原因。
    注:博主其实一开始试过直接在 RouterOS 里创建 IPv6 的静态路由。10000 多条创建了 10 分钟还没创建完。所以如果想做 IPv6 的分流,动态路由协议几乎不可避免。

    更新:RouterOS 7 已加入 IPv6 PBR 支持,所以此方法理论上可行。

实际上使用 iptables 来做 PBR 本身就有点杀鸡用牛刀,因为其实我们只需要 RouterOS 把所有国外的 IP 段都转发给树莓派,这本身就是路由协议的强项,不需要使用任何的 PBR 也是可以实现的。另外还有以下好处:

  1. OSPF 协议收敛速度非常快,几万条路由也只需要短短几秒钟
  2. OSPF 支持快速增量更新,这样 IP 段变化的时候不会影响已经建立的连接,用户甚至不会察觉
  3. OSPF 是动态路由协议,支持健康监测,一旦树莓派有故障,树莓派宣告过来的国外 IP 路由过个十几秒就会自动清除,这样虽然继续走运营商的线路,但是网络最起码还是通的
  4. 相对来说比较安全,OSPF 协议只可以用来宣告路由,比直接在 RoS 的 cli 上执行命令要安全得多

具体来说,这篇文章通过以下的拓扑结构达到局域网里的终端用户访问国外 IP 自动转发到树莓派上的功能。这里不会赘述树莓派上如何配置隧道,因为网上可以很容易的搜到很多教程。

OSPF 简介

OSPF (Open Shortest Path First) 是一种常用的 IGP 路由协议,相对于 BGP 这种主要是为自治域之间通信设计的协议,OSPF 的配置相对来说要简单易懂的多,而且收敛速度非常的快,适合用来在内网快速发布大量的路由。

OSPF 唯一不足的地方是一开始设计的时候没有考虑到多种协议的支持,导致 IPv6 出来后又设计了 OSPFv3 这个新的协议。因为我们希望有 IPv6 支持,所以后面会同时配置 OSPFv2 和 OSPFv3 的实例。但是相对于 IS-IS 和 iBGP 这些支持原生双栈的协议还是配置起来要简单了不少。

额外需要的信息

为了让树莓派能把国外的 IP 地址路由牵引过来,我们需要国外的 IP 段信息。注意这个跟 chnroutes 不一样,前者是中国的 IP 段,而我们需要取反获得不是中国的 IP 段,因为路由协议只能宣告哪些 IP 段应该被转发到树莓派。

在网上搜了半天无果后我自己写了一个简单的 Python 脚本可以生成符合要求的路由。项目已开源,可以在这里下载:

https://github.com/dndx/nchnroutes

下载了以后 make 即可生成国外 IP 地址的路由表。注意如果树莓派上的隧道连接的是国外的 IP,那么需要从生成的 IP 段里剔除,否则隧道的流量也会被 RouterOS 发给树莓派,形成路由环路。可以参考 README.md 的说明通过 --exclude 选项把这些不应该走隧道的 IP 排除。

另外脚本支持 --next 选项,可以用来指定隧道的下一跳。

运行后会生成两个文件 routes4.confroutes6.conf,这两个文件是 BIRD 的静态路由格式,后面做 OSPF 宣告的时候会用到。

另外要注意,在树莓派上配置隧道的时候,不可以创建去往隧道的默认路由。因为我们后面需要用 BIRD 来动态的插入国外的路由走隧道。对于 WireGuard,可以用 Table = off 这个选项来不安装默认路由。

树莓派的配置

首先,先确保树莓派上的隧道已经正常建立。用默认路由和隧道接口ping 一个国外的 IP 应该通畅。可以这样测试:

ping 1.1.1.1
ping -I <隧道的接口名> 1.1.1.1

树莓派应该开启 IP 转发功能,编辑 /etc/sysctl.confnet.ipv4.ip_forward=1net.ipv6.conf.all.forwarding=1 的注释删掉,然后 sudo sysctl -p /etc/sysctl.conf 来应用设置。

下一步在树莓派上配置一些基本的防火墙规则。 sudo apt install nftables 安装 nftables 配置 NAT 规则。

sudo vim /etc/nftables.conf 编辑规则。其中最重要的部分是 MSS clamping。因为隧道的 MTU 相对于局域网的要小,如果 MSS clamping 没有正确设置会导致 TCP 连接断流或者丢包。另外很重要的是对于隧道的出口要开启 SNAT,因为局域网的客户端没有隧道的 IP 地址。考虑到局域网的客户端 IPv6 获取的也是本地运营商分配的地址,所以对于 IPv6 也要启用 SNAT。

配置完了后如下:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
        }

        chain forward {
                type filter hook forward priority filter; policy drop;
                tcp flags & (syn | rst) == syn tcp option maxseg size set rt mtu
                ct state { established, related } accept
                iif "eth0" oifname "wg0" accept
        }

        chain output {
                type filter hook output priority filter; policy accept;
        }
}
table inet nat {
        chain postrouting {
                type nat hook postrouting priority srcnat; policy accept;
                oifname "wg0" masquerade
        }
}
/etc/nftables.conf

以上使用了 table inet,所以规则对 IPv4/IPv6 流量同时生效。然后可以让规则生效并启用开机自动加载:

sudo systemctl start nftables
sudo systemctl enable nftables

让配置立即生效。

下一步需要在树莓派上安装 BIRD 来与 RouterOS 建立 OSPF 邻居关系和进行路由宣告。在 Raspbian 上可以直接 sudo apt install bird 来安装。

安装好了后,BIRD 默认会自动启动。我们需要把上面生成的 routes4.confroutes6.conf 文件复制到 /etc/bird 目录下。然后编辑 /etc/bird/bird.conf 来导入生成的静态路由和启用 OSPF 支持:

# This is a minimal configuration file, which allows the bird daemon to start
# but will not cause anything else to happen.
#
# Please refer to the documentation in the bird-doc package or BIRD User's
# Guide on http://bird.network.cz/ for more information on configuring BIRD and
# adding routing protocols.

# Change this into your BIRD router ID. It's a world-wide unique identification
# of your router, usually one of router's IPv4 addresses.
router id 192.168.0.2;

# The Kernel protocol is not a real routing protocol. Instead of communicating
# with other routers in the network, it performs synchronization of BIRD's
# routing tables with the OS kernel.
protocol kernel {
	scan time 60;
	import none;
	export all;   # Actually insert routes into the kernel routing table
}

# The Device protocol is not a real routing protocol. It doesn't generate any
# routes and it only serves as a module for getting information about network
# interfaces from the kernel. 
protocol device {
	scan time 60;
}

protocol static {
    include "routes4.conf";
}

protocol ospf {
    export all;

    area 0.0.0.0 {
        interface "eth0" {
            authentication cryptographic;
            password "foobar";
        };
    };
}
/etc/bird/bird.conf

这里需要根据实际情况修改的是:

router id 192.168.0.2; 是 OSPF 实例的唯一标识符,要求在局域网内唯一。惯例使用树莓派的局域网 IP 地址即可。同一台机器上的 OSPFv2 和 v3 实例可以共享 router id

password "foobar"; 这里是跟 RouterOS 握手使用的密码。选一个随机的字符串即可。

对于 IPv6,需要使用 OSPFv3,基本上配置是一样的:

# This is a minimal configuration file, which allows the bird daemon to start
# but will not cause anything else to happen.
#
# Please refer to the documentation in the bird-doc package or BIRD User's
# Guide on http://bird.network.cz/ for more information on configuring BIRD and
# adding routing protocols.

# Change this into your BIRD router ID. It's a world-wide unique identification
# of your router, usually one of router's IPv6 addresses.
router id 192.168.0.2;

# The Kernel protocol is not a real routing protocol. Instead of communicating
# with other routers in the network, it performs synchronization of BIRD's
# routing tables with the OS kernel.
protocol kernel {
	scan time 60;
	import none;
	export all;   # Actually insert routes into the kernel routing table
}

# The Device protocol is not a real routing protocol. It doesn't generate any
# routes and it only serves as a module for getting information about network
# interfaces from the kernel. 
protocol device {
	scan time 60;
}

protocol static {
    include "routes6.conf";
}

protocol ospf {
    export all;

    area 0.0.0.0 {
        interface "eth0" {
        };
    };
}
/etc/bird/bird6.conf

因为 RouterOS 目前好像还不支持 OSPFv3 的认证,所以只能把认证关掉...对于家庭局域网环境基本上没有什么安全隐患。

这个时候只需要 reload BIRD 即可让配置生效:

pi@raspberrypi:~/nchnroutes $ sudo birdc configure
BIRD 1.6.6 ready.
Reading configuration from /etc/bird/bird.conf
Reconfigured
pi@raspberrypi:~/nchnroutes $ sudo birdc6 configure
BIRD 1.6.6 ready.
Reading configuration from /etc/bird/bird6.conf
Reconfigured

配置完了后,来验证树莓派的路由已经正确的按照国内外分流:

pi@raspberrypi:~/nchnroutes $ ip r get 1.1.1.1
1.1.1.1 dev wg0 src 192.168.88.2 uid 1000 
    cache 
pi@raspberrypi:~/nchnroutes $ ip r get 114.114.114.114
114.114.114.114 via 192.168.0.1 dev eth0 src 192.168.0.2 uid 1000 
    cache 

可以看到国内外的 IP 已经返回了不同的路由。也可以 traceroute 国内外的 IP 地址来验证。

至此,树莓派的配置已经完成。接下来需要在 RouterOS 上启用 OSPF 实例来收取树莓派发过来的路由。

RouterOS 7 的配置

启用 OSPF 和 OSPFv3 实例:

/routing ospf instance
add disabled=no name=default-v2 router-id=192.168.0.1
/routing ospf area
add disabled=no instance=default-v2 name=backbone-v2
/routing ospf interface-template
add area=backbone-v2 auth=md5 auth-id=1 auth-key=foobar cost=10 disabled=no interfaces=bridge_lan networks=192.168.0.0/24 priority=1
OSPFv2 配置(IPv4)
/routing ospf instance
add disabled=no name=default-v3 router-id=192.168.0.1 version=3
/routing ospf area
add disabled=no instance=default-v3 name=backbone-v3
/routing ospf interface-template
add area=backbone-v3 cost=10 disabled=no interfaces=bridge_lan priority=1
OSPFv3 配置(IPv6)

这里的 router-id 也应该设为 RouterOS 的局域网 IP 地址。不可以跟树莓派 router-id 的相同。

配置好了后,如果一切正常,树莓派应该会自动与 RouterOS 建立 OSPF 邻居关系:

[user@Router] > /routing/ospf/neighbor/print 
Flags: V - virtual; D - dynamic 
 0  D instance=default-v2 area=backbone-v2 address=192.168.0.2 priority=1 router-id=192.168.0.2 dr=192.168.0.2 bdr=192.168.0.1 state="Full" state-changes=6 adjacency=56s timeout=35s 

 1  D instance=default-v3 area=backbone-v3 address=fe80::xxxx:xxxx:xxxx:xxxx%bridge_lan priority=1 router-id=192.168.0.2 dr=192.168.0.2 bdr=192.168.0.1 state="Full" state-changes=8 
      adjacency=56s timeout=33s 

同时,可以看到 RouterOS 已经收到了树莓派发过来的路由:

[user@Router] > /ip/route/print 
Flags: D - dynamic; X - disabled, I - inactive, A - active; c - connect, s - static, r - rip, b - bgp, o - ospf, d - dhcp, v - vpn, m - modem, y - copy; H - hw-offloaded; + - ecmp 
 #       DST-ADDRESS        GATEWAY            DISTANCE
   DAv   0.0.0.0/0          pppoe_wan                 1
   DAo   1.0.0.0/24         192.168.0.2%bri...      110
   DAo   1.0.4.0/22         192.168.0.2%bri...      110
   DAo   1.0.16.0/20        192.168.0.2%bri...      110
 ...
 
[user@Router] > /ipv6/route/print 
Flags: D - dynamic; X - disabled, I - inactive, A - active; c - connect, s - static, r - rip, b - bgp, o - ospf, d - dhcp, v - vpn, m - modem, y - copy; H - hw-offloaded; + - ecmp 
 #       DST-ADDRESS              GATEWAY                  DISTANCE
   DAv   ::/0                     pppoe_wan                       1
   DAo   2000::/16                fe80::xxxx:xxxx:xxxx:x...      110
   DAo   2001::/23                fe80::xxxx:xxxx:xxxx:x...      110
 ...

效果

在局域网里任何一台机器上 traceroute 114.114.114.114traceroute 1.1.1.1 ,可以看到前者走的是运营商的出口,而后者被转发到了树莓派后走隧

重启树莓派,模拟树莓派宕机,同时立刻在局域网里任何一台机器上 ping 一个国外的地址,可以看到一开始走的是运营商链路,树莓派重启完毕以后会自动把路由拉回到隧道:

PING 1.1.1.1 (1.1.1.1): 56 data bytes
...
64 bytes from 1.1.1.1: icmp_seq=38 ttl=54 time=520.732 ms
64 bytes from 1.1.1.1: icmp_seq=39 ttl=54 time=167.810 ms
64 bytes from 1.1.1.1: icmp_seq=40 ttl=54 time=168.368 ms
64 bytes from 1.1.1.1: icmp_seq=41 ttl=54 time=933.065 ms
Request timeout for icmp_seq 42
64 bytes from 1.1.1.1: icmp_seq=43 ttl=54 time=583.132 ms
64 bytes from 1.1.1.1: icmp_seq=44 ttl=54 time=167.838 ms
64 bytes from 1.1.1.1: icmp_seq=45 ttl=54 time=168.192 ms
64 bytes from 1.1.1.1: icmp_seq=46 ttl=54 time=167.819 ms
64 bytes from 1.1.1.1: icmp_seq=47 ttl=54 time=168.071 ms
64 bytes from 1.1.1.1: icmp_seq=48 ttl=54 time=167.742 ms
Request timeout for icmp_seq 49
64 bytes from 1.1.1.1: icmp_seq=50 ttl=54 time=161.493 ms
64 bytes from 1.1.1.1: icmp_seq=51 ttl=54 time=161.952 ms
64 bytes from 1.1.1.1: icmp_seq=52 ttl=58 time=51.335 ms
64 bytes from 1.1.1.1: icmp_seq=53 ttl=58 time=53.994 ms
64 bytes from 1.1.1.1: icmp_seq=54 ttl=58 time=54.008 ms
64 bytes from 1.1.1.1: icmp_seq=55 ttl=58 time=50.271 ms
64 bytes from 1.1.1.1: icmp_seq=56 ttl=58 time=56.944 ms
64 bytes from 1.1.1.1: icmp_seq=57 ttl=58 time=63.712 ms
64 bytes from 1.1.1.1: icmp_seq=58 ttl=58 time=56.840 ms
64 bytes from 1.1.1.1: icmp_seq=59 ttl=58 time=51.738 ms
64 bytes from 1.1.1.1: icmp_seq=60 ttl=58 time=49.738 ms
64 bytes from 1.1.1.1: icmp_seq=61 ttl=58 time=49.552 ms
...

对于 IPv6 也是一样的效果:

$ traceroute6 www.aliyun.com
traceroute6: Warning: aliyun-adns.aliyun.com.gds.alibabadns.com has multiple addresses; using 2401:b180:1:60::6
traceroute6 to aliyun-adns.aliyun.com.gds.alibabadns.com (2401:b180:1:60::6) from 240e:xxx:xxxx:xxxx:xxx:xxxx:xxxx:xxxx, 64 hops max, 12 byte packets
 1  240e:xxx:xxxx:xxxx::1  1.713 ms  1.952 ms  1.547 ms <= RouterOS
 2  240e:xxx:xxxx::  4.502 ms  5.586 ms  5.146 ms <= 电信 BRAS
 3  240e:6:xxxx:xxxx::4  27.280 ms  7.208 ms
    240e:6:xxxx:xxxx::4  6.626 ms
 4  240e:6:0:xxx::2  12.909 ms  9.909 ms
    240e:6:0:xxx::2  9.815 ms
 5  240e::1:63:71:8302  16.249 ms  13.914 ms
    240e::1:63:71:5803  18.359 ms
 6  240e:1c:1111:1fff::1027  20.819 ms
    240e:1c:1112:1fff::1027  19.767 ms
    240e:1c:1111:1fff::1033  19.771 ms
...

$ traceroute6 2001:4860:4860::8888
traceroute6 to 2001:4860:4860::8888 (2001:4860:4860::8888) from 240e:xxx:xxxx:xxxx:xxx:xxxx:xxxx:xxxx, 64 hops max, 12 byte packets
 1  240e:xxx:xxxx:xxxx::1  7.704 ms  1.639 ms  1.396 ms <= RouterOS
 2  240e:xxx:xxxx:xxxx:xxxx:xxx:xxxx:xxxx  2.113 ms <= 树莓派
    2400:8902:xxxx:xxx::1  50.812 ms  51.607 ms <= 隧道出口
 3  2400:8902::4255:39ff:fe08:e9c1  51.707 ms
    2400:8902:1:0:fa66:f2ff:fe00:841  51.836 ms  52.814 ms
 4  2400:8902:b::1  49.900 ms  50.037 ms
    2400:8902:7::1  52.797 ms
...

至此配置全部完成。

热更新路由

当路由有更新的时候,只需要重新生成 /etc/bird/routes4.conf/etc/bird/routes6.conf 文件,然后执行:

pi@raspberrypi:~/nchnroutes $ sudo birdc configure
BIRD 1.6.6 ready.
Reading configuration from /etc/bird/bird.conf
Reconfigured
pi@raspberrypi:~/nchnroutes $ sudo birdc6 configure
BIRD 1.6.6 ready.
Reading configuration from /etc/bird/bird6.conf
Reconfigured

让 BIRD 重新加载配置文件,然后路由更新会立刻被同步到 RouterOS。在我的 4 代树莓派上测试生成路由表文件 + 加载 + OSPF 更新一共只要几秒钟就可以完成,而且更新过程中路由不会震荡,也不会有任何的丢包和延迟抖动。这个操作可以安全的在树莓派上用一个 cronjob 完成。一般一周更新一次即可。

注:nchnroutes 的 README 里也包含了一个使用 Makefile 更新路由和重载的例子。重载默认禁用,需要编辑 Makefile 删除掉相应的注释。

人工禁用路由广播

有的时候需要人工禁用 OSPF,又不希望树莓派关机,可以通过在 BIRD 里禁用 OSPF 实例来达到:

pi@raspberrypi:~ $ sudo birdc disable ospf1
ospf1: disabled

# 在另一台机器上测试
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=54 time=168.153 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=54 time=158.208 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=54 time=167.272 ms
64 bytes from 1.1.1.1: icmp_seq=3 ttl=54 time=151.313 ms
...
cc

重新启用:

pi@raspberrypi:~ $ sudo birdc enable ospf1
BIRD 1.6.6 ready.
ospf1: enabled

# 在另一台机器上测试
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=59 time=49.699 ms
92 bytes from 192.168.0.1: Redirect Host(New addr: 192.168.0.2)
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 0054 f8a4   0 0000  3f  01 bf0f 192.168.0.75  1.1.1.1 

64 bytes from 1.1.1.1: icmp_seq=1 ttl=59 time=51.306 ms
92 bytes from 192.168.0.1: Redirect Host(New addr: 192.168.0.2)
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 0054 0f69   0 0000  3f  01 a84b 192.168.0.75  1.1.1.1 

64 bytes from 1.1.1.1: icmp_seq=2 ttl=59 time=56.848 ms
92 bytes from 192.168.0.1: Redirect Host(New addr: 192.168.0.2)
Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
 4  5  00 0054 4ef6   0 0000  3f  01 68be 192.168.0.75  1.1.1.1 

64 bytes from 1.1.1.1: icmp_seq=3 ttl=59 time=48.333 ms
^C

这里的 ICMP Redirect 消息是 RouterOS 告诉这台机器:对于 1.1.1.1 这个 IP,以后直接找 192.168.0.2(树莓派)即可,不需要把包发给我了。是一种网络性能优化,对实际使用没有影响。

查看 OSPF 状态:

pi@raspberrypi:~ $ sudo birdc show protocols all ospf1
BIRD 1.6.6 ready.
name     proto    table    state  since       info
ospf1    OSPF     master   up     18:01:11    Running
  Preference:     150
  Input filter:   ACCEPT
  Output filter:  ACCEPT
  Routes:         1 imported, 12128 exported, 1 preferred
  Route change stats:     received   rejected   filtered    ignored   accepted
    Import updates:              2          0          0          0          2
    Import withdraws:            0          0        ---          0          0
    Export updates:          12130          2          0        ---      12128
    Export withdraws:            0        ---        ---        ---          0

可以看到向 RouterOS 成功导出了 12000 多条路由。

对于 OSPFv3,只需要把上面命令里的 bird 替换成 bird6 即可。

下一步

《RouterOS 国内外分流环境下的网络高可用性设计》(可选)