mwan3多拨ipv6一对一NAT负载均衡

2024-08-29
#openwrt #mwan3 #ipv6

背景

wan多拨可以拿到多个ipv6的前缀,使用mwan3对ipv6进行负载均衡,发现能够正常将连接分流到各个接口上,但是不会进行地址转化。因此需要自行进行地址转化。本文对“关于多线IPv6与mwan3均衡负载,正确进行源地址转换”进行改进,能够实现入站和出站一对一NAT。 NAT方式为将内网的前缀直接转化为上游下发的前缀,后64位保持不变。这样保证了内网机器的公网连通性和可访问性。

配置

有4个多拨接口,网卡名为eth1mac0eth1mac1eth1mac2eth1mac3。Openwrt接口名为wanv6_0wanv6_1wanv6_2wanv6_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
3lan_prefix=fd00::/64
4
5pd0=$(ifstatus wanv6_0 | jsonfilter -e '@["route"][0].source')
6pd1=$(ifstatus wanv6_1 | jsonfilter -e '@["route"][0].source')
7pd2=$(ifstatus wanv6_2 | jsonfilter -e '@["route"][0].source')
8pd3=$(ifstatus wanv6_3 | jsonfilter -e '@["route"][0].source')
9
10ip0=$(ifstatus wanv6_0 | jsonfilter -e '@["ipv6-address"][0]["address"]')
11ip1=$(ifstatus wanv6_1 | jsonfilter -e '@["ipv6-address"][0]["address"]')
12ip2=$(ifstatus wanv6_2 | jsonfilter -e '@["ipv6-address"][0]["address"]')
13ip3=$(ifstatus wanv6_3 | jsonfilter -e '@["ipv6-address"][0]["address"]')
14
15nft add table ip6 balance_netmap
16nft add set ip6 balance_netmap pds { type ipv6_addr\; flags interval\; comment \"all ipv6 pd of interfaces\" \; }
17nft flush set ip6 balance_netmap pds
18nft 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
24nft add chain ip6 balance_netmap srcnat { type nat hook postrouting priority srcnat \; }
25nft 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
32nft 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
35nft add chain ip6 balance_netmap dctnat { type nat hook prerouting priority dstnat \; }
36nft flush chain ip6 balance_netmap dctnat
37nft add rule ip6 balance_netmap dctnat ip6 daddr @pds dnat ip6 daddr "&" ::ffff:ffff:ffff:ffff "|" ${lan_prefix%%/64}

下面看看上面脚本都干了什么。

5pd0=$(ifstatus wanv6_0 | jsonfilter -e '@["route"][0].source')
6pd1=$(ifstatus wanv6_1 | jsonfilter -e '@["route"][0].source')
7pd2=$(ifstatus wanv6_2 | jsonfilter -e '@["route"][0].source')
8pd3=$(ifstatus wanv6_3 | jsonfilter -e '@["route"][0].source')
9
10ip0=$(ifstatus wanv6_0 | jsonfilter -e '@["ipv6-address"][0]["address"]')
11ip1=$(ifstatus wanv6_1 | jsonfilter -e '@["ipv6-address"][0]["address"]')
12ip2=$(ifstatus wanv6_2 | jsonfilter -e '@["ipv6-address"][0]["address"]')
13ip3=$(ifstatus wanv6_3 | jsonfilter -e '@["ipv6-address"][0]["address"]')

这段代码拿到了每个接口的ip和网段,这里获取网段的方式是看路由表的第一条的来源地址。这会导致如果上游没有下发网段,拿到的网段就是它自己的ip。因此下面的脚本中需要判断网段是否以/64结尾。

15nft add table ip6 balance_netmap

上面这一行创建了一个名字叫balance_netmap类型为ipv6的表。

16nft add set ip6 balance_netmap pds { type ipv6_addr\; flags interval\; comment \"all ipv6 pd of interfaces\" \; }
17nft flush set ip6 balance_netmap pds
18nft 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

24nft add chain ip6 balance_netmap srcnat { type nat hook postrouting priority srcnat \; }
25nft flush chain ip6 balance_netmap srcnat

创建一条链名为srcnat,hook位置为postrouting,即路由后出网卡前,在这个阶段需要修改数据包的来源地址。分为以下三种情况:

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,不便于入站。

32nft 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。注释中是仅转化本机出站流量,内网其他网段地址不进行转化,可以根据自己情况选择用哪一种。

35nft add chain ip6 balance_netmap dctnat { type nat hook prerouting priority dstnat \; }
36nft flush chain ip6 balance_netmap dctnat

创建一条链名为dstnat,hook位置为prerouting,在这个阶段需要修改入站数据包的目的地址。以支持外网主动访问内网服务。如果不需要支持入站可以删去。

37nft 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}