又好久没有写文章了,想起来写一篇。
最近组网,发现有些场景下,这个地方有多条宽带,然后需要充分利用这么多宽带,用来优化上网体验或是提供更多的对外服务。于是就需要一个可以分流的方案,把入站流量以及出站流量分到不同的宽带里面,充分利用这些宽带的上行或者下行优势。
经过这一段时间的研究,算是研究出来了几种可行的方案,这里列举一下。本文默认内网 IP 段为 10.0.0.0/24。
PPPoE 多拨
这里顺便提供一下 PPPoE 多拨在 Linux 下的正确姿势。PPPoE 多拨需要有不同的 MAC 地址才能拨号。因此我们这里用 macvlan 模拟多个 MAC 地址进行拨号。这种方法不需要添加物理交换机,从墙上或者光猫直接插网线就行。
这里以 Debian 为例,在 /etc/network/interfaces 这样写即可。这里假设 PPPoE 所在的物理网卡为 ens3,只拨 2 个号。如果有 VLAN 的话,可以写成 ens3.10 这样的形式。
auto ens3
iface ens3 inet manual
auto ppp0
iface ppp0 inet ppp
pre-up /bin/ip link add link ens3 dev pppm0 type macvlan && /bin/ip link set pppm0 up
post-down /bin/ip link del dev pppm0
provider ppp0
auto ppp1
iface ppp1 inet ppp
pre-up /bin/ip link add link ens3 dev pppm1 type macvlan && /bin/ip link set pppm1 up
post-down /bin/ip link del dev pppm1
provider ppp1
然后创建 /etc/ppp/peers/pppX 文件(X为ppp编号,从0开始排多个),如下。记得自行更换 ppp0 以及 pppm0 为其他数字。
noipdefault
hide-password
noauth
persist
plugin rp-pppoe.so pppm0
ifname ppp0
user "宽带帐号"
然后在 /etc/ppp/pap-secrets 和 /etc/ppp/chap-secrets 各添加一行宽带帐号信息。
"宽带帐号" * "宽带密码"
需要注意的是,Ubuntu 的 netplan 是不可以拨 PPPoE 的,不过可以把 ifupdown 装回来,就可以如同 Debian 那样配置了。在这种情况下,只需要在/etc/network/interfaces 写和 PPPoE 的虚拟网卡相关的配置就行,物理网卡的启动可以继续放在 netplan 里面。
源进源出
还是从这个古老的话题说起。源进源出之前也研究的很成熟了,尤其是折腾各种 VDIP 和隧道的时候研究的最多的。
先做一个小小的科普,VDIP 这个名词出自 Sakura Frp 之前的附属服务(现在消亡了),全称是 Virtual Dedicated IP Address,给没有公网 IP 的用户提供一条虚拟的公网 IP,并且可以保证用户源 IP。然而这个服务的主人 Akkariin Meiko 也没能悟出这个服务的精髓,即源进源出,即正常的入站流量正常走,而从隧道进来的流量从隧道回去。这里说一个常见的用于 wireguard 的源进源出的命令配置。这里 10.0.1.0/24 为隧道网段。
ipset create mycard hash:net family inet || true
ipset add mycard 10.0.0.0/24 || true
ip rule add pref 80 to 10.0.0.0/24 lookup main || true
ipset add mycard 10.0.1.0/24 || true
ip rule add pref 80 to 10.0.1.0/24 lookup main || true
ip -4 route add 10.0.1.0/24 dev wgmc || true
ip rule add pref 90 fwmark 777 lookup 777 || true
iptables -t mangle -A PREROUTING -i wgmc -m set ! --match-set mycard src -j CONNMARK --set-xmark 777/0xffffffff
iptables -t mangle -A PREROUTING -m connmark --mark 777 -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff
iptables -t mangle -A OUTPUT -m connmark --mark 777 -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff
上面的 mycard 这个 ipset ,代表全部内网的地址段的 ipset,如果有多个子网或者和隧道连接的其他地方,则都需要写上,ip rule pref 80 lookup main 也是如此。本文后面出现的 mycard ipset 也是这个意思,将不再列举。
这里稍微变通一下,在多宽带的场景下,每条宽带都需要做一个源进源出的规则,然后在 DNS 方面设置一条解析记录,均匀返回两个宽带的 IP,即可完成基本的入站带宽叠加,宽带就有了两倍的对外服务能力。
本人比较习惯于,在 PPPoE 或者其他宽带形式的 PostUp 脚本内,设置 ip rule 和 ip route 规则,而在 wireguard 的 PostUp 脚本内,设置 iptables 规则。这是因为 iptables 会涉及 ipset,而这个 ipset 由于涉及众多节点,只会在wireguard 的脚本内进行添加。
本人使用的 PPPoE 的 PostUp脚本如下,可以参考。
#!/bin/bash
INIT_ID=$[400 + $(echo "$PPP_IFACE" | sed "s/ppp//g")]
INIT_ID_2=$[510 + $(echo "$PPP_IFACE" | sed "s/ppp//g")]
iptables -t mangle -A FORWARD -o "$PPP_IFACE" -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1452:1460 -j TCPMSS --set-mss 1452
iptables -t mangle -A FORWARD -i "$PPP_IFACE" -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1452:1460 -j TCPMSS --set-mss 1452
ip6tables -t mangle -A FORWARD -o "$PPP_IFACE" -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1432:1460 -j TCPMSS --set-mss 1432
ip6tables -t mangle -A FORWARD -i "$PPP_IFACE" -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1432:1460 -j TCPMSS --set-mss 1432
iptables -t nat -o "$PPP_IFACE" -A POSTROUTING -j MASQUERADE
ip route add default dev "$PPP_IFACE" table $INIT_ID
ip route add default dev "$PPP_IFACE" metric $INIT_ID
ip rule add pref 300 fwmark $INIT_ID lookup $INIT_ID
ip rule add pref 400 fwmark $INIT_ID_2 lookup $INIT_ID
iptables -t mangle -A POSTROUTING -o "$PPP_IFACE" -j TTL --ttl-set 64
iptables -t mangle -A OUTPUT -o "$PPP_IFACE" -j TTL --ttl-set 64
iptables -t mangle -A FORWARD -o "$PPP_IFACE" -j TTL --ttl-set 64
iptables -t mangle -A FORWARD -i "$PPP_IFACE" -j TTL --ttl-set 64
iptables -t mangle -A PREROUTING -i "$PPP_IFACE" -j TTL --ttl-set 64
iptables -t mangle -A INPUT -i "$PPP_IFACE" -j TTL --ttl-set 64
这里简要说明一下,INIT_ID 取值和 pppX 的数值 X 相关,用于批量配置多个 ppp 方便。INIT_ID_2 是另一个辅助 ID。这两个 ID 用于两个不同的 mark。第一个 mark 是用于源进源出的,第二个 mark 是用于分流的。
后 4 行与 mss 相关的规则,是用来协调 PPPoE 的 MTU 的。之后一行是 NAT 。而后面的 ip 开头的命令是用来添加默认路由和策略路由的。这里后面的 iptables 会用到。最后 6 行是用来规范 PPPoE 网卡的数据包的 TTL 的,防止部分运营商通过这个来查到共享上网。
最后是 iptables 部分,放置于 WireGuard 的 PostUp 脚本内,设置源进源出使用的。
ipset create mycard hash:net family inet || true
ipset add mycard 10.0.0.0/24 || true
ip rule add pref 80 to 10.0.0.0/24 lookup main || true
restore_mark() {
OPTION=$1
MARK=$2
iptables -t mangle "$OPTION" PREROUTING -m connmark --mark "$MARK" -j CONNMARK --restore-mark
iptables -t mangle "$OPTION" OUTPUT -m connmark --mark "$MARK" -j CONNMARK --restore-mark
}
ppp_origin() {
OPTION=$1
INTERFACE=$2
INIT_ID=$[400 + $(echo "$INTERFACE" | sed "s/ppp//g")]
INIT_ID_2=$[$INIT_ID + 110]
iptables -t mangle "$OPTION" PREROUTING -i "$INTERFACE" -m set ! --match-set mycard src -j CONNMARK --set-xmark "$INIT_ID"
restore_mark "$OPTION" "$INIT_ID"
}
ppp_origin -A ppp0
ppp_origin -A ppp1
,18002这里的 INIT_ID 与 PPPoE 的脚本吻合,对应两个 ppp 网卡。这里主要使用与 INIT_ID 相关的规则。INIT_ID_2 相关的规则将在下一节使用。
这样,每个宽带的入站请求都会打上 mark,通过 mark 把流量送回,而实现源进源出。
出站分流
说到这个,就是一个痛苦的话题了。各种出站的需求都不一样。很难有一个统一的出站分流方案。比如有些人想要最快的下载速度,用两份带宽同时下载。有些人需要银行这种场景的 IP 稳定性,不能让 IP 左右跳来影响 IP 触发网银风控。而有些情况下需要某些重要的人使用一份独立的宽带,其他人用其他宽带。
这里我们就分别说几种方案,分开来讨论。不过不管使用哪种方案,上面的源进源出的规则必须带着走,否则对外服务可能会出现问题。
Plan A: 均衡
均衡是最通用的一种方法。就是按均分所有的连接到每一个出口,进行带宽叠加。这种方法可以直接叠加带宽上去。但是呢对于网银等有些 IP 敏感的业务,可能会被风控。
下面上规则代码。注意需要与上面的 PPPoE 启动脚本一起使用,用于添加配合的 ip rule 策略路由。
ipset create mycard hash:net family inet || true
ipset add mycard 10.0.0.0/24 || true
ip rule add pref 80 to 10.0.0.0/24 lookup main || true
restore_mark() {
OPTION=$1
MARK=$2
iptables -t mangle "$OPTION" PREROUTING -m connmark --mark "$MARK" -j CONNMARK --restore-mark
iptables -t mangle "$OPTION" OUTPUT -m connmark --mark "$MARK" -j CONNMARK --restore-mark
}
ppp_origin() {
OPTION=$1
INTERFACE=$2
INIT_ID=$[400 + $(echo "$INTERFACE" | sed "s/ppp//g")]
INIT_ID_2=$[$INIT_ID + 110]
iptables -t mangle "$OPTION" PREROUTING -i "$INTERFACE" -m set ! --match-set mycard src -j CONNMARK --set-xmark "$INIT_ID"
restore_mark "$OPTION" "$INIT_ID"
}
ppp_origin -A ppp0
ppp_origin -A ppp1
iptables -t mangle -A PREROUTING -s 10.0.0.0/24 -m set ! --match-set mycard dst -m statistic --mode nth --every 2 --packet 0 -j CONNMARK --set-xmark 510
iptables -t mangle -A PREROUTING -s 10.0.0.0/24 -m set ! --match-set mycard dst -m statistic --mode nth --every 2 --packet 1 -j CONNMARK --set-xmark 511
restore_mark -A 510
restore_mark -A 511
注意这里的 iptables 规则顺序。源进源出规则必须在分流规则之前,避免内网对外服务失效。
Plan B: 奇偶 IP
Plan A 比较适合需要大量下载的场景。但是在需要访问外网源 IP 稳定的场景,这里奇偶 IP 的方法是比较好的,可以做到相同内网源 IP 和目标 IP 访问的出口 IP 一致。
首先,科普一下非标掩码的概念。在通常的情况下,子网掩码是从左往右叠加的。如 /24 的完整掩码是 255.255.255.0。在特殊的子网的情况下,不一定每一位是255。如 /22 是 255.255.252.0,是 /24 的 IP 数量的 4 倍。然而还有一种更特殊的,也就是我们这里需要用到的。形如 255.255.255.1这样的掩码,二进制分解之后就是 11111111.11111111.11111111.00000001,也就是判断前三端以及最后一段的最后一位,表示的意思是 “头三段完全一致以及最后1位一致” 的地址。
这里的用途在于,可以构造一个非标掩码子网段,用来把一个网段分割成两部分,按奇偶分开,实现负载均衡。如 10.0.0.0/255.255.255.1 为内网内 IP 末位是偶数的地址,而 10.0.0.1/255.255.255.1 则为内网内 IP 末位是奇数的地址。类似的,0.0.0.0/0.0.0.1则为全部 IP 末位是偶数的地址,0.0.0.1/0.0.0.1 同理。
非标掩码支持的地方有限。目前只知道 iptables 支持,但是 ip rule 不支持。因此规则如下。
ipset create mycard hash:net family inet || true
ipset add mycard 10.0.0.0/24 || true
ip rule add pref 80 to 10.0.0.0/24 lookup main || true
restore_mark() {
OPTION=$1
MARK=$2
iptables -t mangle "$OPTION" PREROUTING -m connmark --mark "$MARK" -j CONNMARK --restore-mark
iptables -t mangle "$OPTION" OUTPUT -m connmark --mark "$MARK" -j CONNMARK --restore-mark
}
ppp_origin() {
OPTION=$1
INTERFACE=$2
INIT_ID=$[400 + $(echo "$INTERFACE" | sed "s/ppp//g")]
INIT_ID_2=$[$INIT_ID + 110]
iptables -t mangle "$OPTION" PREROUTING -i "$INTERFACE" -m set ! --match-set mycard src -j CONNMARK --set-xmark "$INIT_ID"
restore_mark "$OPTION" "$INIT_ID"
}
ppp_origin -A ppp0
ppp_origin -A ppp1
iptables -t mangle -A PREROUTING -m mark --mark 0 -s 10.0.0.0/255.255.255.1 -d 0.0.0.0/0.0.0.1 -m set ! --match-set mycard dst -j CONNMARK --set-xmark 510
iptables -t mangle -A PREROUTING -m mark --mark 0 -s 10.0.0.1/255.255.255.1 -d 0.0.0.0/0.0.0.1 -m set ! --match-set mycard dst -j CONNMARK --set-xmark 511
iptables -t mangle -A PREROUTING -m mark --mark 0 -s 10.0.0.0/255.255.255.1 -d 0.0.0.1/0.0.0.1 -m set ! --match-set mycard dst -j CONNMARK --set-xmark 511
iptables -t mangle -A PREROUTING -m mark --mark 0 -s 10.0.0.1/255.255.255.1 -d 0.0.0.1/0.0.0.1 -m set ! --match-set mycard dst -j CONNMARK --set-xmark 510
restore_mark -A 510
restore_mark -A 511
这里奇偶性错位稍微有一点小技巧。这里把源和目标 IP 奇偶性相同的放在一个宽带里面,把不同的放在另一个宽带里面。至于为什么这么错位,是为了保证,在不同内网机器访问同一外网网站,以及同一内网机器访问不同外网网站的情况下,都能把流量分开。
题外话
本文的场景都是 PPPoE 或者其他三层隧道的情况下的分流。如果是 IDC 网络,涉及三层寻址找网关的,则需要这么写源进源出规则。
ppp_origin() {
OPTION=$1
ADDRESS=$2
NEIGH_LINE=$(ip neigh show $ADDRESS)
INTERFACE=$(echo $NEIGH_LINE | awk '{print $3}')
MAC=$(echo $NEIGH_LINE | awk '{print $5}')
INIT_ID=$3
INIT_ID_2=$[$INIT_ID + 110]
iptables -t mangle "$OPTION" PREROUTING -i "$INTERFACE" -m mac --mac-source $MAC -m set ! --match-set mycard src -j CONNMARK --set-xmark "$INIT_ID"
restore_mark "$OPTION" "$INIT_ID"
}
这里第二个参数是机房提供的网关的 IP 地址,第三个参数是 mark 以及 table 的取值。