mwan3多拨ipv6一对一NAT负载均衡
背景
wan多拨可以拿到多个ipv6的前缀,使用mwan3对ipv6进行负载均衡,发现能够正常将连接分流到各个接口上,但是不会进行地址转化。因此需要自行进行地址转化。本文对“关于多线IPv6与mwan3均衡负载,正确进行源地址转换”进行改进,能够实现入站和出站一对一NAT。 NAT方式为将内网的前缀直接转化为上游下发的前缀,后64位保持不变。这样保证了内网机器的公网连通性和可访问性。
配置
有4个多拨接口,网卡名为eth1mac0
,eth1mac1
,eth1mac2
,eth1mac3
。Openwrt接口名为wanv6_0
,wanv6_1
,wanv6_2
,wanv6_3
。每个接口都能拿到一个长度为128的ipv6地址和一个长度为64的前缀。 内网ULA前缀为fd00::/64
。
在/etc/config/firewall
添加以下内容,每当网络接口发生变化时都会自动运行脚本。
config include
option path '/usr/nft-nat6.sh'
编辑防火墙脚本/usr/nft-nat6.sh
1 #!/bin/sh
2
3 lan_prefix=fd00::/64
4
5 pd0=$(ifstatus wanv6_0 | jsonfilter -e '@["route"][0].source')
6 pd1=$(ifstatus wanv6_1 | jsonfilter -e '@["route"][0].source')
7 pd2=$(ifstatus wanv6_2 | jsonfilter -e '@["route"][0].source')
8 pd3=$(ifstatus wanv6_3 | jsonfilter -e '@["route"][0].source')
9
10 ip0=$(ifstatus wanv6_0 | jsonfilter -e '@["ipv6-address"][0]["address"]')
11 ip1=$(ifstatus wanv6_1 | jsonfilter -e '@["ipv6-address"][0]["address"]')
12 ip2=$(ifstatus wanv6_2 | jsonfilter -e '@["ipv6-address"][0]["address"]')
13 ip3=$(ifstatus wanv6_3 | jsonfilter -e '@["ipv6-address"][0]["address"]')
14
15 nft add table ip6 balance_netmap
16 nft add set ip6 balance_netmap pds { type ipv6_addr\; flags interval\; comment \"all ipv6 pd of interfaces\" \; }
17 nft flush set ip6 balance_netmap pds
18 nft add element ip6 balance_netmap pds { $lan_prefix }
19 [[ $pd0 == */64 ]] && nft add element ip6 balance_netmap pds { $pd0 }
20 [[ $pd1 == */64 ]] && nft add element ip6 balance_netmap pds { $pd1 }
21 [[ $pd2 == */64 ]] && nft add element ip6 balance_netmap pds { $pd2 }
22 [[ $pd3 == */64 ]] && nft add element ip6 balance_netmap pds { $pd3 }
23
24 nft add chain ip6 balance_netmap srcnat { type nat hook postrouting priority srcnat \; }
25 nft flush chain ip6 balance_netmap srcnat
26
27 [[ $pd0 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac0 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd0%%/64}
28 [[ $pd1 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac1 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd1%%/64}
29 [[ $pd2 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac2 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd2%%/64}
30 [[ $pd3 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac3 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd3%%/64}
31
32 nft add rule ip6 balance_netmap srcnat oifname {eth1mac0, eth1mac1, eth1mac2, eth1mac3} masquerade
33 # nft add rule ip6 balance_netmap srcnat oifname {eth1mac0, eth1mac1, eth1mac2, eth1mac3} saddr {$ip0, $ip1, $ip2, $ip3} masquerade
34
35 nft add chain ip6 balance_netmap dctnat { type nat hook prerouting priority dstnat \; }
36 nft flush chain ip6 balance_netmap dctnat
37 nft add rule ip6 balance_netmap dctnat ip6 daddr @pds dnat ip6 daddr "&" ::ffff:ffff:ffff:ffff "|" ${lan_prefix%%/64}
下面看看上面脚本都干了什么。
5 pd0=$(ifstatus wanv6_0 | jsonfilter -e '@["route"][0].source')
6 pd1=$(ifstatus wanv6_1 | jsonfilter -e '@["route"][0].source')
7 pd2=$(ifstatus wanv6_2 | jsonfilter -e '@["route"][0].source')
8 pd3=$(ifstatus wanv6_3 | jsonfilter -e '@["route"][0].source')
9
10 ip0=$(ifstatus wanv6_0 | jsonfilter -e '@["ipv6-address"][0]["address"]')
11 ip1=$(ifstatus wanv6_1 | jsonfilter -e '@["ipv6-address"][0]["address"]')
12 ip2=$(ifstatus wanv6_2 | jsonfilter -e '@["ipv6-address"][0]["address"]')
13 ip3=$(ifstatus wanv6_3 | jsonfilter -e '@["ipv6-address"][0]["address"]')
这段代码拿到了每个接口的ip和网段,这里获取网段的方式是看路由表的第一条的来源地址。这会导致如果上游没有下发网段,拿到的网段就是它自己的ip。因此下面的脚本中需要判断网段是否以/64
结尾。
15 nft add table ip6 balance_netmap
上面这一行创建了一个名字叫balance_netmap
类型为ipv6的表。
16 nft add set ip6 balance_netmap pds { type ipv6_addr\; flags interval\; comment \"all ipv6 pd of interfaces\" \; }
17 nft flush set ip6 balance_netmap pds
18 nft add element ip6 balance_netmap pds { $lan_prefix }
19 [[ $pd0 == */64 ]] && nft add element ip6 balance_netmap pds { $pd0 }
20 [[ $pd1 == */64 ]] && nft add element ip6 balance_netmap pds { $pd1 }
21 [[ $pd2 == */64 ]] && nft add element ip6 balance_netmap pds { $pd2 }
22 [[ $pd3 == */64 ]] && nft add element ip6 balance_netmap pds { $pd3 }
创建了一个包含内网网段和所有上游分发下来的前缀的集合,名为pds
。
24 nft add chain ip6 balance_netmap srcnat { type nat hook postrouting priority srcnat \; }
25 nft flush chain ip6 balance_netmap srcnat
创建一条链名为srcnat
,hook位置为postrouting
,即路由后出网卡前,在这个阶段需要修改数据包的来源地址。分为以下三种情况:
- 来源数据包是内网网段(内网机器出站流量):需要将内网网段前缀改成上游下发给该出口网卡的前缀,实现1对1 NAT。
- 来源数据包是上游下发的长度为128的ip(本机出站流量):需要将来源ip改成出口网卡的ip。
- 来源数据包是其他ip(内网还有其他网段):可以选择不进行地址转化,也可以选择强制转化为出口网卡的ip。
27 [[ $pd0 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac0 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd0%%/64}
28 [[ $pd1 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac1 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd1%%/64}
29 [[ $pd2 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac2 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd2%%/64}
30 [[ $pd3 == */64 ]] && nft add rule ip6 balance_netmap srcnat oifname eth1mac3 ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd3%%/64}
匹配出口网卡是需要负载均衡的网卡,并且来源ip是内网网段或者是其他网卡获得的网段(比如内网机器手动添加了一个公网ip),则将来源地址的前缀改成上游下发的前缀。这里使用位运算进行修改,::ffff:ffff:ffff:ffff
是前缀的反掩码。不用map
进行转化的原因是,map
转化不一定是一一对应的,有时候后缀也会修改分配到一个未使用的ip,不便于入站。
32 nft add rule ip6 balance_netmap srcnat oifname {eth1mac0, eth1mac1, eth1mac2, eth1mac3} masquerade
33 # nft add rule ip6 balance_netmap srcnat oifname {eth1mac0, eth1mac1, eth1mac2, eth1mac3} saddr {$ip0, $ip1, $ip2, $ip3} masquerade
对于其它流量,全部转化为出口网卡的ip,即masquerade
。注释中是仅转化本机出站流量,内网其他网段地址不进行转化,可以根据自己情况选择用哪一种。
35 nft add chain ip6 balance_netmap dctnat { type nat hook prerouting priority dstnat \; }
36 nft flush chain ip6 balance_netmap dctnat
创建一条链名为dstnat
,hook位置为prerouting
,在这个阶段需要修改入站数据包的目的地址。以支持外网主动访问内网服务。如果不需要支持入站可以删去。
37 nft add rule ip6 balance_netmap dctnat ip6 daddr @pds dnat ip6 daddr "&" ::ffff:ffff:ffff:ffff "|" ${lan_prefix%%/64}
匹配目标是上游下发的前缀地址的连接,将其目标改为内网前缀。
Openwrt其他设置
在Network -> Interfaces -> Global network options -> IPv6 ULA-Prefix
中指定内网地址。
在lan
接口的Advanced Settings -> IPv6 prefix filter
选择local
,即仅下发上述内网地址。
附录
网卡太多可以使用下面这个使用循环的脚本,修改iface_count
即可。
#!/bin/sh
iface_count=10
lan_prefix=fd00::/64
for i in $(seq 0 $(( $iface_count-1 )) ); do
eval "pd$i=\$(ifstatus wanv6_$i | jsonfilter -e '@[\"route\"][0].source')"
eval "ip$i=\$(ifstatus wanv6_$i | jsonfilter -e '@[\"ipv6-address\"][0][\"address\"]')"
done
nft add table ip6 balance_netmap
nft add set ip6 balance_netmap pds { type ipv6_addr\; flags interval\; \; }
nft flush set ip6 balance_netmap pds
nft add element ip6 balance_netmap pds { $lan_prefix }
for i in $(seq 0 $(( $iface_count-1 )) ); do
pd=$(eval echo -n \$pd$i)
if [[ $pd == */64 ]]; then
nft add element ip6 balance_netmap pds { $pd }
fi
done
nft add chain ip6 balance_netmap srcnat { type nat hook postrouting priority srcnat \; }
nft flush chain ip6 balance_netmap srcnat
for i in $(seq 0 $(( $iface_count-1 )) ); do
pd=$(eval echo -n \$pd$i)
if [[ $pd != */64 ]]; then
continue
fi
nft add rule ip6 balance_netmap srcnat oifname eth1mac$i ip6 saddr @pds snat ip6 saddr "&" ::ffff:ffff:ffff:ffff "|" ${pd%%/64}
done
set="{"
for i in $(seq 0 $(( $iface_count-1 )) ); do
set="${set}eth1mac$i,"
done
set="$set}"
# choose one
if true; then
nft add rule ip6 balance_netmap srcnat oifname $set masquerade
else
ip_set="{"
for i in $(seq 0 $(( $iface_count-1 )) ); do
ip=$(eval echo -n \$ip$i)
ip_set="$ip_set$ip,"
done
ip_set="$ip_set}"
nft add rule ip6 balance_netmap srcnat oifname "$set" saddr "$ip_set" masquerade
fi
nft add chain ip6 balance_netmap dctnat { type nat hook prerouting priority dstnat \; }
nft flush chain ip6 balance_netmap dctnat
nft add rule ip6 balance_netmap dctnat ip6 daddr @pds dnat ip6 daddr "&" ::ffff:ffff:ffff:ffff "|" ${lan_prefix%%/64}