RouterOS + OSPF 实现高可用的 IP 分流

未完成,仅仅只搭建了实验环境。所以先把命令一股脑扔在这里,日后有空再完善。

# 0. 背景

  • RouterOS 作为路由器。

    • 共有 ether0 到 ether7 共 8 个网口
    • ether0 接入 ISP 网线,建立 PPPoE Client 接口 pppoe-out1,作为 WAN
    • ether1 到 ether6 设立桥接设备,名为 bridge,作为 LAN 区域
      • IP: 192.168.50.1/24
      • DNS: 223.5.5.5, 119.29.29.29
    • ether7 独立接口,不加入网桥,单独设置该网口作为 OSPF 专用子网:
      • IP: 192.168.255.1/24
  • 任意 Linux 服务器作为分流出口,安装 Bird,作为 OSPF 路由器。

    • 共有 eth0 一个网口
    • eth0 和 RouterOS 的 ether7 相连,设置为:
      • IP: 192.168.255.254/32
      • Gateway: 192.168.255.1
      • DNS: 不设置

# 1. RouterOS:打通 LAN 子网和 OSPF 子网

由于 OSPF 子网中的设备仍然需要能访问 WAN 区域,所以将 OSPF 子网的流量 直接通过 PPPoE Client 接口转发出去,避免网络成环。

/routing table
add fib name=bypass

/ip/route
add distance=1 gateway=pppoe-out1@main dst-address=0.0.0.0/0 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=192.168.0.0/16 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=10.0.0.0/8 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=169.254.0.0/16 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=172.16.0.0/12 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=224.0.0.0/4 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=255.255.255.255/32 routing-table=bypass

/ipv6/route
add distance=1 gateway=pppoe-out1@main  dst-address=::/0 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=::ffff:0:0/96 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=::ffff:0:0:0/96 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=64:ff9b::/96 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=100::/64 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=2001::/32 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=2001:20::/28 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=2001:db8::/32 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=2002::/16 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=fc00::/7 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=fe80::/10 routing-table=bypass
add distance=1 gateway=bridge@main dst-address=ff00::/8 routing-table=bypass

/routing/rule
add interface=bridge table=main
add interface=ether7 table=bypass

如果有某些接口提供了一些私网地址,那么应该修改上面的私网地址,让它们从对应的接口出去, 而不是直接使用 bridge@main。例如,当 WireGuard 接口 wg0 绑定了地址段 10.0.0.0/24 时, 上面命令中的这一条:

add distance=1 gateway=bridge@main dst-address=10.0.0.0/8 routing-table=bypass

应该相应地修改为:

add distance=1 gateway=wg0@main dst-address=10.0.0.0/24 routing-table=bypass

依此类推。反正就是照抄 main 表里的东西。 此外,@main 这个标记是可以省略的,因为这是 ROS 6.x 版本的命令语法。

现在应该可以在 LAN 子网下的设备上 ping 通 OSPF 子网的设备了, 反过来也是一样。同时,OSPF 子网的设备也可以访问 WAN 区域正常上网。

# 2. RouterOS:高可用 DNS

新建两个脚本,分别设置使用默认 DNS 和使用 X DNS。

/system script
add dont-require-permissions=yes name=use-default-dns owner=admin \
  policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
  source="/ip dns set allow-remote-requests=yes servers=119.29.29.29,223.5.5.5 \r\n/ip dns cache flush"

add dont-require-permissions=yes name=use-ospf-dns owner=admin \
  policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon \
  source="/ip dns set allow-remote-requests=yes servers=192.168.255.254\r\n/ip dns cache flush"

然后在 netwatch 中添加任务,每 10 秒通过 ICMP ping 确认 X 服务的运行状态。 当 X 服务未启动时,切换到默认 DNS。 当 X 服务正常相应时,切换回 X DNS。

/tool netwatch
add down-script=use-default-dns host=192.168.255.254 interval=10s up-script=use-ospf-dns

# 3. RouterOS:启用 OSPF

/routing ospf instance
add disabled=no name=default-v2 router-id=192.168.255.1
/routing ospf area
add disabled=no instance=default-v2 name=backbone-v2
/routing ospf interface-template
add area=backbone-v2 cost=10 disabled=no interfaces=ether7 networks=192.168.255.0/24 priority=1

/routing ospf instance
add disabled=no name=default-v3 router-id=192.168.255.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=ether7 priority=1

# 4. OSPF 路由:初始化

本文使用的环境为 Ubuntu Server 22.04

# 开启 IPv4 和 IPv6 转发

编辑 /etc/sysctl.conf 取消注释下列内容

net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv4.icmp_echo_ignore_all=1

其中 net.ipv4.icmp_echo_ignore_all=1 会让本机不响应 ICMP Echo 请求, 但是在上面的 netwatch 中,我们是通过 ICMP Echo 来判断 X 服务是否正常运行的, 因此,在后续的脚本中,会在 X 服务正常运行时,将这个值改为 0。

然后

sudo sysctl -p /etc/sysctl.conf

# 释放本机 53 端口

Ubuntu Server 会默认启用 systemd-resolve,这个会占用本机的 53 端口,可以通过修改 /etc/systemd/resolved.conf 来禁用它:

DNSStubListener=no

然后

systemctl stop systemd-resolved
systemctl disable systemd-resolved
rm /etc/resolv.conf

# netplan 设置静态 IP

network:
  ethernets:
    eth0:
      dhcp4: false
      addresses: [192.168.255.254/24]
      routes:
        - to: default
          via: 192.168.255.1
      nameservers:
        addresses: []

# Crontab

systemctl enable --now cron

# 5. OSPF 路由:X

略。注意:

  • 启用 X DNS,监听 53 端口
  • 启用 TUN 设备,假定为 utun

# 6. OSPF 路由:部署 bird

安装 bird 服务:

sudo apt install bird

来到 /etc/bird 下,编辑 bird.conf,内容如下:

router id 192.168.255.254;

protocol kernel {
	scan time 60;
	import none;
	export all;
}

protocol device {
	scan time 60;
}

protocol static {
  include "routes4.conf";
}

protocol ospf {
  export all;

  area 0.0.0.0 {
    interface "eth0" {
    };
  };
}

编辑 bird6.conf,内容如下:

router id 192.168.255.254;

protocol kernel {
	scan time 60;
	import none;
	export all;
}

protocol device {
	scan time 60;
}

protocol static {
  include "routes6.conf";
}

protocol ospf {
  export all;

  area 0.0.0.0 {
    interface "eth0" {
    };
  };
}

准备好路由分流文件,一般 ISP 会提供,将这些加入到 crontab 中,定时更新:

0 2 */1 * * curl -s <ROUTES4> -o /etc/bird/routes4.conf
0 2 */1 * * curl -s <ROUTES6> -o /etc/bird/routes6.conf
0 3 *   * * birdc configure
0 3 *   * * birdc6 configure

如果不行让路由太复杂,可以只路由部分 IP 段,比如:

route 198.18.0.0/16 via "utun";

route 2001:too:long::/48 via "utun";

然后手动执行一下上面的命令,启动 bird 服务。

# 7. OSPF 路由:网络可用性检测

crontab 如下:

* * * * * /root/check-network.sh

然后编辑 /root/check-network.sh,内容如下:

#!/usr/bin/bash
COUNT=0
MAX_COUNT=3
URL=https://www.google.com/generate_204
while [ $COUNT -lt $MAX_COUNT ]
do
  SER=0
  NET=0
  if [ $(curl --connect-timeout 10 --interface utun -w "%{http_code}" -s $URL) -eq 204 ];then
    NET=1
  fi
  if /etc/init.d/bird status | grep Active | grep -q running; then
    SER=1
  fi
  if [ $NET -eq 1 ] && [ $SER -eq 0 ];then
    echo "Network is ready, starting bird and bird6"
    /etc/init.d/bird start
    /etc/init.d/bird6 start
    echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_all
    exit 0
  fi
  if [ $NET -eq 0 ] && [ $SER -eq 1 ];then
    let COUNT+=1
    if [ $COUNT -eq $MAX_COUNT ];then
      echo "Network is down, stopping bird and bird6"
      echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
      /etc/init.d/bird stop
      /etc/init.d/bird6 stop
      exit 0
    fi
    continue
  fi
  if [ $NET -eq 1 ] && [ $SER -eq 1 ]; then
    echo "Force enabling icmp echo"
    echo 0 > /proc/sys/net/ipv4/icmp_echo_ignore_all
  fi
  if [ $NET -eq 0 ] && [ $SER -eq 0 ]; then
    echo "Force disabling icmp echo"
    echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
  fi
  e
  echo "Everything goes well."
  exit 0
done

# 8. DNS 上直接分流

经过上面的设置之后,如果 X DNS 的运行模式为 Fake IP, 那么在使用 X DNS 时,所有的 DNS 请求都会返回 198.18.0.0/16 中的地址, 而这段地址无疑时需要 RouterOS 交给 X 进行分流的。

这很不好,因为这样依然会导致所有的流量都被分流到 X 上,然后 X 内部再根据 配置文件的规则进行分流。因此,当我们访问 baidu.com 这种有路线优化 的网站时,流量也会被 RouterOS 分流到 X 上,然后 X 内部自己的分流规则再将流量 转发回 RouterOS,这很不好。

因此,我们需要在 DNS 上直接分流,这样就可以避免上面的问题。

# OSPF 路由:部署 SmartDNS

将 X DNS 的监听端口设置为 10053,然后安装 SmartDNS:

apt install smartdns

配置 /etc/smartdns/smartdns.conf,内容如下:

bind :53
cache-size 0
prefetch-domain no
serve-expired no
speed-check-mode none

log-level info
log-file /var/log/smartdns.log
server 127.0.0.1:10053
server 119.29.29.29     -group CN -exclude-default-group
server 223.5.5.5        -group CN -exclude-default-group
conf-file /etc/smartdns/cn-domains.conf

文件 /etc/smartdns/cn-domains.conf 内容如下:

nameserver /.baidu.com/CN

这表明,所有 cn-domains.conf 中的域名都会直接使用 223.5.5.5 和 119.29.29.29 进行解析, 而这个文件之外的所有域名再交给 X DNS 处理。这样可以将大部分常用的国内域名直接解析它们的真实地址。 cache-size 0 表示不缓存任何 DNS 记录,因为 RouterOS 自己会维护 DNS 缓存。而且我发现 RouterOS 的 DNS 缓存会遵守 Fake IP 的 TTL 为 1 的规则,这样会导致 Fake IP 部分的缓存会很快失效,并不会出现 X 和 smartdns 重启后短时间内 RouterOS 缓存失效的问题。

网上也有根据某列表进行的 DNS 分流,但这种处理方式并不能保证所有不可访问的域名都能被 X DNS 处理, 因此我选择了上面的方式,我称呼为“域名白名单”。

在后续的使用中,可以根据 X Dashboard 中的日志,将更多常用的一定有线路优化的域名加入到 cn-domains.conf 中。

启动 SmartDNS:

systemctl enable --now smartdns

# Always Direct Devices

由于已经在 DNS 上进行了策略路由,因此,对于不需要策略路由的设备, 我们可以直接让这些设备发来的 DNS 请求劫持到“不具有分流能力”的 DNS 服务器上。

/ip/firewall/nat
add action=dst-nat chain=dstnat protocol=udp \
  dst-address=192.168.50.1 dst-port=53 \
  src-address=<IP> \
  to-addresses=223.5.5.5 to-ports=53

也可以使用 /ip/firewall/address-list

/ip/firewall/address-list
add list=direct_ips address=<IP1>
add list=direct_ips address=<IP2>

/ip/firewall/nat
add action=dst-nat chain=dstnat protocol=udp \
  dst-address=192.168.50.1 dst-port=53 \
  src-address-list=direct_ips \
  to-addresses=223.5.5.5 to-ports=53

也可以使用 /routing/rules,但这个方法和 DNS 劫持有本质区别, 使用前最好知道加入下面的规则以后,<IP> 这个设备的两类流量是如何路由的:

/routing/rule
add src-address=<IP> action=lookup table=bypass

当然也可以用 mangle 表,即 /ip/firewall/mangle, 但我不喜欢。

# 参考文献