背景

我们小区终于有联通线路啦,之前一直用的联通的手机号。它套餐满一定额度以后送一条宽带,本着不用白不用的精神,那必须不能浪费。还好我之前设置软路由得时候就预留了两个网口作wan,所以新增得联通得线路直接插那个口上就行了。(吐槽一下联通给得光猫竟然是8年前生产的老古董)

背景知识简介

这里先普及一下Linux下查找出口路由的方式,首先可以参考这个文档:

nf-packet-flow.svg

当Linux要主动发出一个包时,如果是 本机创建发出 的,会从 OUTPUT PATHxfrm encode 开始,直接进入 OUTPUT 链,第一个Hook点是 raw output 。如果是 消息包转发 (一般是来自子网)的数据包,第一个Hook点是中间 Network Layerraw prerouting 。我家里设置的子网都连接到了一个虚拟网桥,这种 网桥包转发 的情况下,第一个Hook点是 最下面那条链的 broute brouting 。网桥的 broute brouting 是一个比较特殊的filter,在这里 DROP 的包,会进入和 消息包转发 一样的流程。(这里也是 nftables 尚未支持的功能,所以我用了 ebtables

第二个要点是需要关注一下 man 8 ip-rule 里的 SELECTOR 参数。如果这里面任意的参数变化,会导致 OUTPUT 链中 reroute check 检查的时候触发重路由。

第三个要点是上面图里的 routing decision 的部分就是决定消息包要怎么发出去的路由策略的判定点了。

关于路由判定的规则,首先也是会按 man 8 ip-rule 来决定走哪条路由规则,选择规则的策略是按优先级倒叙,选最小匹配的rule。然后这条rule会告诉我们跳转到哪个规则或者lookup哪个路由表。Linux系统最多有255个路由表(ID: 1-255),命名的路由表配置位于 /etc/iproute2/rt_tables ,默认应该是有:

255     local
254     main
253     default

这三项,而默认的路由策略应该是:

$ ip rule 
0:      from all lookup local
32766:  from all lookup main
32767:  from all lookup default

同时我们可以查看默认的路由规则:

$ ip route show table main
default via 10.64.255.254 dev ppp0 proto static metric 102 
61.165.188.1 dev ppp1 proto kernel scope link src 61.165.190.37 metric 103 
172.18.0.0/16 dev br0 proto kernel scope link src 172.18.1.10 metric 425 
172.20.0.0/16 dev br0 proto kernel scope link src 172.20.1.1 metric 425

在只有一个ppp拨号 (ppp0) 的情况下,路由规则如上, 172.18.0.0/16172.20.0.0/16 是我家里网络设置的两个走网桥(br0)的子网,可以先忽略。另外两个路由表。default 我在合理是空的, local 就是所有本地回环的策略,我就不贴了。

多播和策略路由

我的软路由是自己安装的Linux( Manjaro 发行版),拨号直接用的 NetworkManager 的DSL的Interface就可以了。 我家里的工具用得比较激进,目前是 iptablesnftables 共存的。NAT由 nftables 提供,iptables 仅仅提供了一些第三方工具尚未支持和 nftables 不支持的功能。 默认情况下,两个PPP连接成功以后,都会添加到默认路由表中。

$ ip route
default via 61.165.188.1 dev ppp1 proto static metric 100 
default via 10.64.255.254 dev ppp0 proto static metric 102

按这种配置方式,出口路由永远会选中第一个。而我们需要 按两条带宽大小的比例 均分流量(我这里是 3:1)。用 OpenWRT 的同学就比较简单了,直接用里面带的 mwan3 模块即可。但是我是 Manjaro 所以只能自己配置,不过也可以参考一下 mwan3 的配置方法。

简单来说就是由于 ip rule 这一层没有随机或者Hash分发的功能,所以我们只能给出口包打 mark,触发重路由。然后在路由策略里动手脚。最终我是和 mwan3 一样,用了mark里的第二个字节(即第 8-15 位)用于多播的策略路由。

  1. 如果8-15位已经设置,则是已经决定过多播策略路由,不再重设Mark。
  2. 跳过局域网。
  3. 跳过子网。
  4. 按策略打Mark,定向路由。不同的目标ppp出口使用不同的Mark。

还有注意NAT策略中要跳过所有ppp拨号接口的本地IP。这个和多播无关,这里提一下是因为我们这儿电信都会给外网IP,所以我之前的规则里没这条也没事儿。但是联通没给外网ip,给的ip是 10.*.*.* 所以NAT策略里要排除这个源IP。这种情况下对于ppp的对端IP也是局域网IP,可以不用管,NetworkManager 会生成正确的 ip rule 规则能设置正确的路由。

保持连接

对于TCP链接和某些软件的UDP打洞之类的网络策略可能需要固定链路。特别是 TCP,我们不能一会包从A链路走一会从B链路走,上层也是有路由表的。这里可以用 connection track 的MARK,把包的MARK保存进 connection track 的MARK 里。然后策略路由前先尝试从 connection 中恢复 MARK (如果有的话)。mwan3 也是这么做的。

mwan3 里,链路的选择是随机的,但是我觉得可能用ip+端口Hash更好一些,这样对同一个ip+端口链路是固定的,不会跳来跳去。对一些网络可能更友好一些。nftables 也自带随机和Hash功能,这样设置起来就很简单。

和其他服务的tproxy策略路由配合

在这之前,我的软路由里已经有其他服务使用了Mark做tproxy了(使用了0-7位)。但是上面的操作里要操作Mark,那就势必会影响Mark的设置和顺序。

所以写脚本的时候要注意两边的Mark不要互相覆盖了,不然容易出现死循环。iptables 的打Mark和判定Mark都是可以设置 Mask 的。nftables 则支持表达式,更灵活一些。但是 nftables 目前版本对Mark操作时,逻辑运算符的右边的第二个参数必须是常量,不能是变量。这就导致两个使用Mark的模块,即便Mark范围不冲突,操作之间也会受影响。更何况并不是所有的软件都支持仅判定和设置部分Mark段。

我最终的设置脚本位于: https://github.com/owent-utils/docker-setup/blob/main/setup-router/ppp-nat/setup-multi-wan.sh

设置的规则如下:

最终规则

ip rule and ip route

$ ip rule
0:      from all lookup local
1:      from all lookup local
7100:   from all iif ppp0 lookup main
7101:   from all iif ppp1 lookup main
19991:  from all fwmark 0xe/0xf lookup 100
23001:  from all fwmark 0x100/0xff00 lookup main suppress_prefixlength 0
23002:  from all fwmark 0x100/0xff00 lookup 101
23003:  from all fwmark 0x200/0xff00 lookup main suppress_prefixlength 0
23004:  from all fwmark 0x200/0xff00 lookup 107
32766:  from all lookup main
32767:  from all lookup default

$ ip route show table main
default via 114.95.200.1 dev ppp1 proto static metric 101 
default via 10.64.255.254 dev ppp0 proto static metric 104 
10.64.255.254 dev ppp0 proto kernel scope link src 10.64.26.184 metric 113 
114.95.200.1 dev ppp1 proto kernel scope link src 114.95.201.4 metric 112 
172.18.0.0/16 dev br0 proto kernel scope link src 172.18.1.10 metric 425 
172.20.0.0/16 dev br0 proto kernel scope link src 172.20.1.1 metric 425

$ ip route show table 101 
default via 114.95.200.1 dev ppp1 proto static metric 101

$ ip route show table 107
default via 10.64.255.254 dev ppp0 proto static metric 20104

nftables

$ sudo nft list table inet mwan
table inet mwan {
  chain PREROUTING {
    type filter hook prerouting priority mangle; policy accept;
    jump MARK
  }

  chain OUTPUT {
    type route hook output priority mangle; policy accept;
    jump MARK
  }

  chain MARK {
    meta l4proto != { tcp, udp } return
    meta mark & 0x0000ff00 != 0x00000000 return
    meta mark & 0x0000ffff != 0x00000000 ct mark & 0x0000ff00 == 0x00000000 ct mark set meta mark & 0x0000ffff
    meta mark & 0x0000ff00 == 0x00000000 ct mark & 0x0000ff00 != 0x00000000 meta mark set ct mark & 0x0000ffff
    meta mark & 0x0000ff00 != 0x00000000 return
    ip daddr { 127.0.0.1, 224.0.0.0/4, 255.255.255.255 } return
    ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } return
    ip daddr { 119.29.29.29, 180.76.76.76, 223.5.5.5, 223.6.6.6 } return
    ip6 daddr { ::1, fc00::/7, fe80::/10, ff00::-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff } return
    ip6 daddr { 2400:3200::1, 2400:3200:baba::1, 2400:da00::6666 } return
    meta mark & 0x0000ff00 == 0x00000000 ip saddr 114.95.201.4 meta mark set meta mark | 0x0000ff00
    meta mark & 0x0000ff00 == 0x00000000 ip saddr 114.95.200.1 meta mark set meta mark | 0x0000ff00
    meta mark & 0x0000ff00 == 0x00000000 ip saddr 10.64.26.184 meta mark set meta mark | 0x0000ff00
    meta mark & 0x0000ff00 == 0x00000000 ip saddr 10.64.255.254 meta mark set meta mark | 0x0000ff00
    meta mark & 0x0000ff00 == 0x00000000 jump POLICY_MARK
    meta mark & 0x0000ff00 == 0x00000000 meta mark set meta mark & 0xfffffeff | 0x0000fe00
    ct mark set meta mark & 0x0000ffff
  }

  chain POLICY_MARK {
      meta mark & 0x0000ff00 == 0x00000000 symhash mod 4 1 meta mark set meta mark & 0xffff01ff | 0x00000100
      meta mark & 0x0000ff00 == 0x00000000 symhash mod 4 2 meta mark set meta mark & 0xffff02ff | 0x00000200
  }
}

这里并不是所有的 symhash mod 4 [VALUE] 都设置了Mark指定路由。因为不指定的话就会组默认路由,我的默认路由的第一条刚好是电信的,所以这么设置以后,1/4 走联通,其他走电信,最终的结果仍然是 3:1

上面虽然设置了部分ipv6的规则,但是我目前还没有配置ipv6 (电信给了ipv6地址,但联通没给) 。主要是没找到合适的方法获取ipv6中 SLAAC 的子网范围,这个子网要加入到子网排除列表里,ipv4下,子网范围是固定的。 ip.upip.down 传入的参数是不完整的,我又不想用 ip 命令+一顿裁剪输出字符串的骚操作。反正一时半会儿也没需要必须用。

某些系统的安全策略

有些安全性比较高的服务会检测链路一致,比如招商银行。如果同时多条链路访问招商银行app会提示 “当前网络不稳定货才用了动态IP” 。我的想法是Hash策略仅按目标ip Hash,应该能解决这个问题。不过这会影响测速,也会影响并发效果。所以我暂时没这么改。要用这种带链路检查的服务的时候先关闭wifi好了。

写在最后

我这里最后测了一下速,详情不贴了。电信+联通双线路,流量配比 3:1 的情况下,测速结果大致是:

  • 下行: 略低于两者的叠加。有时候下行可以到接近两者带框和,但大多数情况下到不了,只有轻微增加了下行速率。
  • 上行: 基本上等于两条线路的上行叠加,效果明显。

最后,欢迎有兴趣的童鞋互相交流。