Linux 软路由单线多拨

Linux 软路由宽带多拨(单线多拨),测试机为树莓派 3B,系统为 ArchLinux for ARM,ISP 为电信,实测只能稳定双拨,但带宽没变。

多拨介绍

所谓多拨就是同一主机同时拨通多条 PPPoE 线路,每个 PPPoE 线路都有独立的公网 IP。这些 PPPoE 线路的 ISP 可以相同也可以不同,如果 ISP 不同,只能使用多线多拨(这里的“线”是指一个独立的物理网口,而“拨”是指一条独立的 PPPoE 线路),如果 ISP 相同(大部分家庭应该都是这种),那么既可以多线多拨,也可以单线多拨(只使用一个物理网卡,但虚拟出多张逻辑网卡,它们有不同的 MAC 地址,在外部看来和实际的物理网卡没有区别)。多线多拨没什么难度,但是单线多拨就比较有吸引力了,毕竟省事啊,网线接法都不用动,只要配置系统就好了。本文的重点也是单线多拨。

那么多拨有什么用途呢?当然是带宽倍增大法了。怎么实现的呢?其实原理还是负载均衡。假设我现在是单线双拨,也就是有两条线路可以走,因为每条线路都有自己的默认网关,但是一台主机同时只能设置一个默认网关,也就是说,如果不进行负载均衡,那么即使多拨成功,你实际上也只能同时使用其中一条线路,当然带宽也不会有任何变化(我要这多拨有何用~)。但是负载均衡也有两大类:主备模式、轮询模式。所谓主备模式也就是选定一条线路为主线路,另一条线路则为备用线路,默认情况下流量还是走主线路,只有当主线路 down 掉时,流量才会走备用线路;显然主备模式也不是我们想要的。那就剩下轮询模式了,既然一个主机同一时间只能设置一个默认网关,那我就通过某些手段来实现默认网关的切换,线路一和线路二不断的进行切换,总可以了吧。那又要怎么实现呢?总不可能使用脚本来不断的切换默认网关吧,太屌丝了。这时候就需要请出 Linux 中的两个强大的工具:iptables、iproute2。总体思路:在 iptables 的 mangle 表中给 1/2 的包打上 0x1 标记,给另外 1/2 的包打上 0x2 标记,然后再使用 ip route 添加两个路由表,假设为 ppp0 和 ppp1,分别添加一条默认网关,指向线路一和线路二,然后再使用 ip rule 设置策略路由,打了 0x1 标记的包走 ppp0 表(线路一),打了 0x2 标记的包走 ppp1 表(线路二)。注意,实际上并没有这么简单,具体的细节后面会详细说明。

轮询模式下,要想充分利用两个线路来到达双倍带宽是有条件的,那就是在多线程下载/上传中才有效果。因为如果是单线程(网页浏览、在线视频等)实际上还是只会走其中一条线路,另一条线路依旧是空闲的,没有利用起来。而多线程就不一样了,假设单线路的速率是 100M,且单线程的下载速率也是 100M(跑满),那么只需两个线程就可以达到 100M + 100M = 200M 下载速率,从而实现带宽倍增。当然也不是必须多线程才能体现,在多个设备、进程同时访问外网时,多拨轮询模式还是能够体现出它的优势的(其实和多线程下载的情形差不多)。

但是,默认的轮询模式可能会导致部分网银、论坛、网站提示帐号异常、网络环境异常、网络环境不安全等等,为什么呢?原因还是因为公网 IP 不固定导致的,因为是轮询模式,前一个连接可能走线路一,后一个连接就可能走线路二了,每个线路的 IP 都是不同的,所以会导致部分注重安全的网站判定你处于不安全的网络环境中,或者是认为你的帐号被盗用,甚至临时封禁你的帐号都有可能。这个问题我目前也没有很好的解决方法,有人说让 80/443 端口固定走一条线路,可是这样多拨的优势就不复存在了,因为多线程下载大多数也是使用 80/443 端口啊;有人说将这些服务器的域名/IP拉黑,专门配置规则让它们固定走一条线路,然而这么多网站怎么收集?!目前我的方法是临时将多拨切换回单拨,等网银什么的弄完了再切换回去。如果是访问特别频繁的网站,那就直接添加规则让它只走一条线路。

温馨提示:多拨不一定能够实现带宽倍增,很多地方的 ISP 都限制了单账号的总速率,你就是 10 拨,速度也不会有任何变化,也就是所谓的端口限速。

单线多拨

单线多拨的第一步是创建多张虚拟网卡,这些虚拟网卡应该拥有不同的 MAC 地址,并且要让外部感觉不到这是虚拟的网卡。
在 Linux 中,借助 macvlan 内核模块就可以轻松做到(一条命令就可以创建一张虚拟网卡)。检查内核是否支持 macvlan:

如果第一条命令报错,或者第二条命令没有输出,则说明当前内核不支持 macvlan,需要升级内核的版本(v3.9-3.19、4.0+)。
如果都没有问题,那就直接使用 ip link add link 物理网卡 name 虚拟网卡 type macvlan 来创建虚拟网卡,这里创建两张:

其中 eth0 是物理 WAN 口的网卡名称,创建的两个虚拟网卡为:wan0wan1。注意,使用 ip link 命令查看时显示的网卡名为 wan0@eth0wan1@eth0@ 前面的是虚拟网卡名,后面的是物理网卡名),但实际上你不能使用这个名称,会提示设备不存在,你必须使用 wan0wan1

我们先断开 eth0 上的拨号网络,然后分别为 wan0、wan1 网卡创建 PPPoE 拨号配置文件,在 ArchLinux 中的步骤为:

根据我的经验,启用 wan0 和 wan1 的时间最好错开,即等 wan0 拨号成功后再执行 netctl start wan1,验证一下:

负载均衡

单线双拨成功,现在我们来看下路由表是什么样的,使用 ip route 命令查看 main 主路由表的条目:

可以发现只有 ppp0 的默认网关生效了(先拨号的那个),如果你尝试添加 ppp1 的默认网关则直接报错:

不过你可以将 ppp0 替换为 ppp1,这样流量就会走 ppp1 线路出去了,步骤是先删掉默认网关,然后添加:

现在进入正题,教你如何充分利用两条线路,让流量平摊在 ppp0 和 ppp1 上。首先创建两个路由表:ppp0ppp1

其中 10 ppp020 ppp1 两行是我们自己加上去的,前面的数字表示路由表的表号(ID),后面的则为路由表的名称。
然后在 ppp0 表中添加一条默认路由指向 ppp0 网卡,同样的,在 ppp1 表中添加一条默认路由指向 ppp1 网卡,具体的:

不过,现在这两个路由表还没有被应用,我们要使用 ip rule 命令添加对应的策略路由才行,假设 fwmark 为 0x100x20

fwmark 是 firewall mark 的简称,也就是防火墙标记,这个 mark 是在 iptables/netfilter 中打上的(mangle 表中的各种 MARK)。
上面的策略路由表示:打了 0x10 标记的包走 ppp0 出去,打了 0x20 标记的包走 ppp1 出去,其它包走 main 表出去(main 表不改动)。

但实际上仅这两条 rule 是有问题的,本机访问外网正常,但外网访问本机却只能访问其中一张网卡(main 表中的那个)。假设 main 表中的默认路由指向 ppp0,那么从外部只能访问 ppp0,不能访问 ppp1。本机可以正常接收外面发来的请求包,但是在进行响应的时候会出现问题,具体的:当响应包发出后,首先经过 iptables 的 mangle 表处理(规则在后头),因为这个响应包属于一个 ESTABLISHED 连接,而这个连接在当初 NEW 的时候(也就是从外面收到请求包的时候)并未被 PREROUTING 链的 mangle 表打上分流标记(因为这是从外部进来的,没进行处理),所以在经过 ip rule 时,不会匹配 0x10/ppp0、0x20/ppp1 规则,所以只能到 main 表,被默认路由 ppp0 命中,所以最终这个响应包会走 ppp0 出去,显然请求端那边不会理会这个响应包,因为不属于同一个连接。解决办法也很简单,只要在 0x10/ppp0、0x20/ppp1 规则后加两条 from 规则就可以了(注意:只能加在 fwmark 规则后面,如果放在它们前面会导致从本机发出的包无法正常负载均衡,因为发出的所有包默认的 from 地址都是 main 表中默认路由指定的那个外网 IP,所以会始终走某个接口出去)。

策略路由配好后,就剩下 iptables 规则了,假设内网网段为 192.168.0.0/16:

现在,我们来测试一下负载均衡是否配置成功,最直观的方法是访问 ip.cn,它会直接显示出你当前的 IP 地址:

看样子是成功了,那么我们来测速试试。Linux 命令行中比较方便的测速工具是 speedtest-cli,使用 pip 安装即可:

speedtest-cli 的用法很简单,直接运行即可,它会自动选择离你最近的测速服务器,然后进行 ping、上传、下载测试:

我连续测了几次,也用了多线程下载工具测试(迅雷、IDM),满速只能平均在 10~11 MB/s 左右,和原来的单拨一模一样。起初怀疑是负载均衡没配好,所以我在多线程下载时查看了 ppp0、ppp1 网卡的实时速度,两个接口的流量都是平均的,都是 5~6 MB/s 左右,所以负载均衡应该是正常的,而光猫、网线都是千兆的,因为树莓派 3B 自带网卡是 100M 的,所以我用的 USB3.0-RJ45 千兆网卡转接器来连接的光猫,不过又因为树莓派 3B 的 USB 接口是 2.0 的,理论最大速度为 480 Mbps,实际测试时可以达到 32MB/s 的内网传输速度,所以应付 200M 的网络应该不是问题,因此断定是电信限制了帐号的总速度,无论你几拨,总速度都不会改变,而且拨数越多每个连接分到的带宽越小,所以我就放弃了。