使用 RouterOS,OSPF 和树莓派为国内外 IP 智能分流
这篇问题记录了我在 RouterOS 上通过 OSPF 协议把国外的流量动态拉取到旁路由上的配置过程。文章里面包含了 IPv4 和 IPv6 的配置。
更新历史
2022.05 :修改为 RouterOS 7 配置格式
前言
相信各位使用 RouterOS 做为主路由的同学都遇到过需要为国内和国外 IP 地址分流的需求。国内 IP 直接走运营商出口,而国外 IP 走隧道加速访问。网络上目前能搜到的教程基本上都是使用 RouterOS 的 iptables + ipset 来给链接打标记(比如 autorosvpn,然后通过类似这样的配置来做 PBR (Policy Based Routing)),然后走特殊的路由表来转发到树莓派上。一开始我也尝试了这种做法,但是遇到了以下几个问题:
- IP 列表基本上每周都会有变化,如果希望更新,需要全量重新删除创建
address-list
,这个操作不是特别的快,而且在更新过程中可能会因为路由震荡导致连接闪断或者丢包。 - 没有健康检查,如果树莓派断网或者掉电 RouterOS 还是会傻傻的把流量转发过去,导致所有的国外 IP 都被黑洞路由。
RouterOS 6 不支持 IPv6 的 PBR,看论坛上 8 年前就有帖子问了,但是一直没出。据说 RouterOS 7 发布后会支持。这也可能是为什么我从网上能找到的几乎所有分流教程都只做了 IPv4 的分流的原因。
注:博主其实一开始试过直接在 RouterOS 里创建 IPv6 的静态路由。10000 多条创建了 10 分钟还没创建完。所以如果想做 IPv6 的分流,动态路由协议几乎不可避免。
更新:RouterOS 7 已加入 IPv6 PBR 支持,所以此方法理论上可行。
实际上使用 iptables 来做 PBR 本身就有点杀鸡用牛刀,因为其实我们只需要 RouterOS 把所有国外的 IP 段都转发给树莓派,这本身就是路由协议的强项,不需要使用任何的 PBR 也是可以实现的。另外还有以下好处:
- OSPF 协议收敛速度非常快,几万条路由也只需要短短几秒钟
- OSPF 支持快速增量更新,这样 IP 段变化的时候不会影响已经建立的连接,用户甚至不会察觉
- OSPF 是动态路由协议,支持健康监测,一旦树莓派有故障,树莓派宣告过来的国外 IP 路由过个十几秒就会自动清除,这样虽然继续走运营商的线路,但是网络最起码还是通的
- 相对来说比较安全,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.conf
和 routes6.conf
,这两个文件是 BIRD 的静态路由格式,后面做 OSPF 宣告的时候会用到。
另外要注意,在树莓派上配置隧道的时候,不可以创建去往隧道的默认路由。因为我们后面需要用 BIRD 来动态的插入国外的路由走隧道。对于 WireGuard,可以用 Table = off
这个选项来不安装默认路由。
树莓派的配置
首先,先确保树莓派上的隧道已经正常建立。用默认路由和隧道接口ping 一个国外的 IP 应该通畅。可以这样测试:
ping 1.1.1.1
ping -I <隧道的接口名> 1.1.1.1
树莓派应该开启 IP 转发功能,编辑 /etc/sysctl.conf
把 net.ipv4.ip_forward=1
和 net.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。
配置完了后如下:
以上使用了 table inet
,所以规则对 IPv4/IPv6 流量同时生效。然后可以让规则生效并启用开机自动加载:
sudo systemctl start nftables
sudo systemctl enable nftables
让配置立即生效。
下一步需要在树莓派上安装 BIRD 来与 RouterOS 建立 OSPF 邻居关系和进行路由宣告。在 Raspbian 上可以直接 sudo apt install bird
来安装。
安装好了后,BIRD 默认会自动启动。我们需要把上面生成的 routes4.conf
和 routes6.conf
文件复制到 /etc/bird
目录下。然后编辑 /etc/bird/bird.conf
来导入生成的静态路由和启用 OSPF 支持:
这里需要根据实际情况修改的是:
router id 192.168.0.2;
是 OSPF 实例的唯一标识符,要求在局域网内唯一。惯例使用树莓派的局域网 IP 地址即可。同一台机器上的 OSPFv2 和 v3 实例可以共享 router id
。
password "foobar";
这里是跟 RouterOS 握手使用的密码。选一个随机的字符串即可。
对于 IPv6,需要使用 OSPFv3,基本上配置是一样的:
因为 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 实例:
这里的 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.114
和 traceroute 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 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
即可。