OpenWrt配置IPv6 NAT

 

介绍了OpenWrt使用NAT配置IPv6转发,之前的一文中已经阐述了IPv6的基本情况,在大多数情况下IPv6 NAT还是适用的,尤其是对教育网;相比于网络上一般的配置文章,本文要详细得多:不仅给出了方便配置的一键脚本,还梳理设置步骤及原理,方便分析具体场景下的问题

介绍了OpenWrt使用NAT配置IPv6转发,之前的一文中已经阐述了IPv6的基本情况,在大多数情况下IPv6 NAT还是适用的,尤其是对教育网;相比于网络上一般的配置文章,本文要详细得多:不仅给出了方便配置的一键脚本,还梳理设置步骤及原理,方便分析具体场景下的问题

回想起当年的配置经历,只能说有些幸运吧,有一台对OpenWrt支持的非常好的Newifi mini,还得到了学长的指点

在官网的中的方法:NAT6: IPv6 Masquerading Router被描述为:The solution provided here can be considered more robust and portable,只是粗浅的看了下,大概是通过添加解析配置文件来实现的,真厉害,不过个人还是偏向于本文的方法,简单而模块化的设计方便做进一步的设置

实际使用过程中遇到什么情况都有可能,所以也就没有什么最好的方法了,只有原理依旧适用,而通过一些分析又可以发现问题,下文给出的方法实际用了很长一段时间被证明是稳定可行的

:部分内容可能会随着系统版本和软件版本的更迭而不同,个人也来不及分析和测试,所以下文给出了一些分析问题的方法

准备工作

  • 一台刷了OpenWrt或者LEDE的路由器(OpenWrt至少14.07以上,建议官网最新的稳定版,网络上的个人编译版可能会遇到一些解释不清的问题)
  • 安装了LuCI界面(比如说每日构建版本就没有自带,就需要部分shell操作了,或者用WinSCP)
  • 支持IPv6的网络,能够分配得到全局IPv6地址
  • SSH客户端(ex:Putty,Linux终端,Finalshell或者WSL,Win10 Powershell自带的SSH)

我的设备

路由器:Newifi mini

系统:LEDE 17.01.6

网络:校园网,需要PPPoE认证的IPv4/IPv6双栈

快速配置方法

只要接入是正常的,对官方的OpenWrt的适用性是没问题的,如果用的修改版的系统或者安装过较多的软件的系统,建议按照后面的分步配置来(不然kmo-ipt-nat6安装不上都找不出问题)

确认IPv6接入

在路由器的管理页面中(地址就是LAN IP不同路由器可能不一样,如果是官方系统默认是192.168.1.1)设置WAN口为具体的上网方式,常见的有:

  1. PPPoE认证之后获得双栈网络
  2. 插上网线就可以获得IPv6地址(如系统默认设置的wan6) 这种直接在wan口上建立一个DHCPv6 Client就好,连接之后就可以获取IPv6地址(可能要等一段时间),如果是如下这样的本地链路地址

IPv6: fe80::dcb8:cd0d:422:c123/128

可以用以下方法确认是否获得了全局的IPv6地址

  1. 在SSH连接后输入命令ifconfig就会得到形似下面的输出
...
inet6 addr: fe80::dcb8:cd0d:422:c123/10 Scope:Link
inet6 addr: 2001:xxxx:xxxx:xxxx:dcb8:cd0d:422:c123/64 Scope:Global
...

可以看到两个地址的后64位dcb8:cd0d:422:c123是一样的,第二个地址的末尾也表明这是一个全局IPv6地址

  1. 其实在Status->Overview 的IPv6 WAN Status中也可以看到详细的IPv6连接信息

  2. Network->Interface在较新的版本可能会出现对应于PPPoE接口的DHCPv6 Client,如果是2001开头就是全局的

那么接下就是测试下IPv6的连接性,只需要运行ping6 ipv6.mirrors.ustc.edu.cn

如果得到了类似的输出就说明IPv6网络是可用的

root@LEDE:~# ping6 ipv6.mirrors.ustc.edu.cn
PING ipv6.mirrors.ustc.edu.cn (2001:da8:d800:95::110): 56 data bytes
64 bytes from 2001:da8:d800:95::110: seq=0 ttl=52 time=124.260 ms
64 bytes from 2001:da8:d800:95::110: seq=1 ttl=52 time=43.021 ms
64 bytes from 2001:da8:d800:95::110: seq=2 ttl=52 time=43.080 ms
64 bytes from 2001:da8:d800:95::110: seq=3 ttl=52 time=43.020 ms
64 bytes from 2001:da8:d800:95::110: seq=4 ttl=52 time=42.940 ms
...

某些安装了mwan3(一个多拨软件,LuCI APP为Loadbalance或者负载均衡)的路由器由于mwan3会修改路由表和防火墙设置,进而导致IPv6功能无法使用,可以尝试在启动项完全关闭mwan3再重启试下

一键配置脚本

这种东西最暴力了,一方面也是个人比较懒,有段时间经常刷机折腾,所以这个就应运而生了,直接复制粘贴到SSH窗口等待连接断开重启就好,省时省力。

:此脚本适用于较新版本的OpenWrt(大概17.01以上),如果配置失败,建议重置设置,之后参考下面的原理部分自行修改测试

opkg update && opkg install kmod-ipt-nat6

echo "net.ipv6.conf.default.accept_ra=2" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.accept_ra=2" >> /etc/sysctl.conf

uci set network.globals.ula_prefix="$(uci get network.globals.ula_prefix | sed 's/^./d/')"
uci commit network
uci set dhcp.lan.ra_default='1'
uci commit dhcp

touch /etc/hotplug.d/iface/99-ipv6

cat > /etc/hotplug.d/iface/99-ipv6 << EOF
#!/bin/sh
[ "\$ACTION" = ifup ] || exit 0
iface_dhcp=wan_6
iface_route=pppoe-wan
[ -z "\$iface_dhcp" -o "\$INTERFACE" = "\$iface_dhcp" ] || exit 0

ip6tables -t nat -I POSTROUTING -s \`uci get network.globals.ula_prefix\` -j MASQUERADE
gw=\$(ip -6 route show default | grep \$iface_route | sed 's/from [^ ]* //' | head -n1)
status=\$(ip -6 route add \$gw 2>&1)
logger -t IPv6 "Done: \$status"
EOF
/etc/init.d/network restart

停用

如果不需要再做NAT6了,可以将/etc/hotplug.d/iface/99-ipv6文件移动下位置,之后再重启下设备,如用以下的命令停用和再次启用

#disable
mv /etc/hotplug.d/iface/99-ipv6 /root/
#enable
mv  /root/ /etc/hotplug.d/iface/99-ipv6

当然也可以把之前的设置全部都逆向做一遍,这里就不细说了

分步设置

如果使用上面的配置脚本已经成功的话,那么就已经可以用了,下面就是对上面脚本的步骤的解析了

要使网络能够贯通,需要解决的问题:

  1. 路由器能够给下级设备分配IPv6地址(DHCPv6+SLAAC)
  2. 路由器能够完成数据包的路由和转发
  3. 路由器能够应对掉线等会导致路由表和防火墙部分设置重置的情况

网络基础配置

以下均在SSH中输入运行,主要是RA的配置,第三行是修改内网IPv6地址的前缀,否则一些软件会默认使用IPv4(他以为你的 IPv6 地址不通外网)

echo "net.ipv6.conf.default.accept_ra=2" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.accept_ra=2" >> /etc/sysctl.conf

uci set network.globals.ula_prefix="$(uci get network.globals.ula_prefix | sed 's/^./d/')"
uci commit network
uci set dhcp.lan.ra_default='1'
uci commit dhcp

:这里还是详细的说明一下内网地址前缀的问题

内网地址前缀的修改说明

IPv6地址空间分配表 Internet Protocol Version 6 Address Space 这里截取了需要关注的部分

IPv6 Prefix Allocation Reference Notes
c000::/3 Reserved by IETF [RFC4291]  
fc00::/7 Unique Local Unicast [RFC4193] For complete registration details, see [IANA registry iana-ipv6-special-registry].
fe80::/10 Link-Scoped Unicast [RFC4291] Reserved by protocol. For authoritative registration, see [IANA registry iana-ipv6-special-registry].
2000::/3 Global Unicast [RFC4291] The IPv6 Unicast space encompasses the entire IPv6 address range with the exception of ff00::/8, per [RFC4291]. IANA unicast address assignments are currently limited to the IPv6 unicast address range of 2000::/3…

而fc00::/7中的fd00::/8是被指定为IPv6的私网地址段,也就是无法在外网路由

然后就是OpenWrt官网上的NAT6的文档 NAT6: IPv6 Masquerading Router

If you are handing out only local addresses (ie not part of delegated prefix from your upstream): Change the first letter of the “IPv6 ULA Prefix” from to (see the FAQ section below, for an explanation why this is needed)

uci set network.globals.ula_prefix="$(uci get network.globals.ula_prefix | sed 's/^./d/')"
uci commit network

Why should the ULA prefix be changed?

The default ULA prefix starting with represents an address that is not globally routed on the internet. Some (or most) clients will prefer the IPv4 route, if they don’t have a global IPv6 address assigned, so you need to change the prefix to indicate a global address. It doesn’t necessarily have to start with , but to avoid conflicts, you should use a prefix that is not being used yet. The letters are unassigned and therefore safe choices.

Using your ISP assigned prefix as ULA works, too. However, unless you have a static IPv6 prefix assigned by your ISP, this is not recommended, since it can cause address conflicts once the prefix changes.

其中提到了需要修改ULA前缀第一个字母为d(也就是变成了表格第一行的保留地址段),不然客户端根据之前的fd开头的地址可能会 优先采用 IPv4,为了避免这种情况(主要是 校园网IPv4流量有限 ),,只好修改一下ULA前缀

正常情况下经过了NAT,最终发出的数据包的源地址都是路由器的地址,应该是不会有冲突的,如果还是担心冲突问题的话就采用fe80::/8内的ULA就好了

添加ip6tables的NAT支持

在Linux网络中,NAT与iptables中的nat表有关,而IPv6设计的初衷之一就是消除NAT,所以直到比较晚的时候才在添加了IPv6 NAT的RFC,并且也不建议使用,相比于IPv4 NAT的约定俗成,IPv6的NAT还是需要手动配置ip6tables的

ip6tables的IPv6 NAT支持需要Linux Kernel version大于3.7,当然也不绝对,因为Linux内核是可以修改的:北邮学长的国创项目就实现了在OpenWrt 10.03(Kernel Version 2.6)的Linux内核中加入了NAT66的支持,而OpenWrt自14.07(Kernel Version 3.10)才加入原生IPv6以及NAT6的支持,而后LEDE把内核版本更新到4.X,LEDE与OpenWrt合并后也一直有对NAT6的改进

OpenWrt已经有NAT6模块可以直接安装了,首先是更新软件源,偶尔连接性不太好可能速度比较慢,可选替换为国内的软件源,原生的OpnWrt直接在shell中运行

sed -i 's_downloads\.openwrt\.org_mirrors.ustc.edu.cn/lede_' /etc/opkg/distfeeds.conf

这里使用的是中科大的软件源,LEDE曾经从OpenWrt项目分离出来,后来又合并回去了,然而科大的源还是在lede目录下,里面有OpenWrt的源就是了,USTC Mirrors Help

之后就是更新软件列表,安装NAT6模块,安装完成之后会有正常的输出

opkg update && opkg install kmod-ipt-nat6

添加hotplug脚本

因为需要设置防火墙规则和修改IPv6路由表,而在接口断开之后就会失效,所以需要添加热插拔脚本中;下面依次是创建文件(不需要赋予执行的权限),使用vi编辑器进行编辑,输入完第二行命令之后就进入了vi编辑器的编辑

  1. 一开始进入是vi编辑器的命令行模式(Command Mode)
  2. 需要按下I键进入插入模式(Insert Mode)
  3. 之后复制下面的脚本粘贴到SSH窗口中
  4. 按下Esc键退出Insert Mode,输入:wq!便保存了编辑的内容并退出
touch /etc/hotplug.d/iface/99-ipv6
vi /etc/hotplug.d/iface/99-ipv6

:Hotplug功能是相当的实用的,OpenWrt Hotplug原理分析 这篇文章做了详细的剖析

脚本内容如下

#!/bin/sh
[ "$ACTION" = ifup ] || exit 0
iface_dhcp=wan_6
iface_route=pppoe-wan
[ -z "$iface_dhcp" -o "$INTERFACE" = "$iface_dhcp" ] || exit 0

ip6tables -t nat -I POSTROUTING -s `uci get network.globals.ula_prefix` -j MASQUERADE
gw=$(ip -6 route show default | grep $iface_route | sed 's/from [^ ]* //' | head -n1)
status=$(ip -6 route add $gw 2>&1)
logger -t IPv6 "Done: $status"

关于添加脚本的位置: 本文添加的是Hotplug脚本,因为在连接(拨号)变动(断开或连接)时,路由表和防火墙会变化,网络上的方法也有添加init.d脚本的(我也用了很长的时间),优点是遇到路由变动的时候手动restart service,如果不需要NAT6的话disable就好了(其实是没有添加到ip6tables的nat表),缺点也就是偶尔需要手动…

防火墙规则也可以添加到/etc/firewall.user文件中作为用户的自定义规则,而在IPv6路由表添加条目也可以在LuCI界面中通过直接添加静态路由来实现

OpenWrt的接口

iface_dhcp=wan_6
iface_route=pppoe-wan

这是针对默认情况写的,也就是名为wan的口以PPPoE连接网络,然后在系统中显示的接口名就变成了pppoe-wan,一般默认设置OpenWrt对PPPoE使用了内建的IPv6管理,所以会虚拟出一个IPv6接口(DHCPv6客户端,不可编辑,旧版本的OpenWrt只能通过命令查到),其接口名是原名称后面接上_6,也就是wan_6,如果是eth0.2直接获取的IPv6地址,那么这个接口就是wan6了

:这里是以iface_dhcp的插拔来侦测IPv6连接,通过iface_route来获取路由表中的网关,这里对接口单独设置了变量,可以按照实际情况修改,比如说,如果是手动设定DHCPv6客户端模式提供IPv6接入,把第一个接口名改为获取IPv6网络的接口,第二个改为路由表中指向上级网关的接口

OpenWrt在LuCI里面的接口(interface)很多,部分也被称为设备(device),从硬件接口如有线网卡eth0,无线网卡wlan0,到VLAN的虚拟网卡eth0.1,eth0.2,再到由macvlan在VLAN上虚拟的网卡,以及建立在网卡eth0.2上的虚拟设备pppoe-wan,建立在eth0.1与wlan0上的网桥br-lan,具体可以参考Linux Network Interfaces

向路由表添加默认网关

WiKi/Iptables可以了解到Linux路由器是如何处理数据包的,主要流程可见下图

iptables

路由器对收到的、目的是其他主机的包:PRETOUTING -> Routing Decision -> FORWARD -> POSTROUTING

因为路由器本身可以获得IPv6地址,查看路由表就有

root@OpenWrt:~# ip -6 route | grep pppoe
default from 2001:xxx:xxxx:xxxx::/64 via fe80::96db:daff:fe3e:8fcf dev pppoe-wan proto static metric 512 pref medium
...

这一条仅仅是把来自路由器本机的数据包通过pppoe-wan发送到上级交换机的网卡,而来自下级设备的IPv6数据包被发送到路由器,路由器查询路由表,由于没有获得与路由器相同前缀的地址,故找不到合适的路由条目,路由器无法转发数据包

在IPv6的NAT中需要关注的部分:

原数据包(源地址fd00:……) -> 查询路由表(添加使内网地址可路由的条目)-> SNAT(修改源地址为2001:……) -> 发送到上级路由

那么下面对此进行处理

gw=$(ip -6 route show default | grep $iface_route | sed 's/from [^ ]* //' | head -n1)
status=$(ip -6 route add $gw 2>&1)

在IPv6路由表中添加条目,去掉原路由表中的from 2001:xxx:xxx:xxx::/64字段,以上级交换机端口为默认网关,这个时候再看下路由表

root@OpenWrt:~# ip -6 route | grep pppoe
default from 2001:xxx:xxxx:xxxx::/64 via fe80::96db:daff:fe3e:8fcf dev pppoe-wan proto static metric 512 pref medium
default via fe80::96db:daff:fe3e:8fcf dev pppoe-wan
...

第二条也就让所有的数据包都可以在这台路由器上路由了,但是由于内网分配的是非2000::/3的全局地址,所以在外网是无法被路由的,因此还需要做SNAT

status=$(ip -6 route add $gw 2>&1)
logger -t IPv6 "Done: $status"

这一段就是将添加网关的错误输出重定向到标准输出中,在通过命令替换把输出结果赋予变量,输出到system log中方便查看

防火墙设置

ip6tables -t nat -I POSTROUTING -s `uci get network.globals.ula_prefix` -j MASQUERADE

这是nat的一般写法,不需要写出SNAT替换的出口IP,但是SNAT的写法看得更加清楚:

ip6tables -t nat -A POSTROUTING -s `uci get network.globals.ula_prefix` -j SNAT --to 2001:xxxx:xxxx:xxxx:dcb8:cd0d:422:c123/64

这里采用的是在数据包路由之后对来自ULA地址前缀的包做SNAT——替换源地址为路由器本身的全局IPv6地址

网络重启

/etc/init.d/network restart

可能会掉一会线,不出问题就可以用IPv6了

后续

随着IPv6的普及,这种方法已经不太好用了

可以在通过添加路由器的ipv6-hosts文件的方法让访问更多的网络资源

命名为hosts6,上传到路由器的/etc目录中,再使用额外添加的方法就好

uci set dhcp.@dnsmasq[0].addnhosts=hosts6

不用写脚本的方法

纯LuCI界面下做设置,当然能够应对的情况也就相对有限了,原理上和上面是差不多的,参考自:

在LuCI界面下选择 系统(System)» 软件(Software) 更新软件源,安装 kmod-ipt-nat6

“网络”»“接口”;

  1. 将下面的IPv6 ULA-Prefix的第一个字母f改为d
  2. 点击修改LAN口的配置,在IPv6 Settings选项卡那里,执行:

(1)Router Advertisement-Service选为“服务器模式”;

(2)禁用DHCPv6-Service和NDP-Proxy;

(3)勾上Always announce default router。

下面这段加到系统(System)» 启动项(Startup)底部的Local Startup中去就行(exit 0 之前):

#/bin/ash
line=0
while [ $line -eq 0 ]
do
        sleep 10
        line=`route -A inet6 | grep ::/0 | awk 'END{print NR}'`
done
ip6tables -t nat -I POSTROUTING -s `uci get network.globals.ula_prefix` -j MASQUERADE
route -A inet6 add 2000::/3 `route -A inet6 | grep ::/0 | awk 'NR==1{print "gw "$2" dev "$7}'`

while循环的目的在于直到获取IPv6的路由表信息之后,才开始配置防火墙和路由表

Q&A

拨号IPv6地址变动

如果留心的话会发现OpenWrt每次PPPoE拨号得到的IPv6地址是不一样的,似乎与SLAAC相悖,这个问题涉及到拨号时的路由器提供的网卡MAC是随机的,OpenWrt PPPoE拨号问题之mac地址克隆

而Windows同一张网卡拨号在更换账号之后,地址也会变,也是某种随机,随机可以保护隐私,但是会失去一个静态的IP地址,考虑到校园网防火墙会阻止IPv6的传入连接也就没什么了

PPPoE的连接建立过程中,认证是L2链路层的以太网完成的,在OpenWrt的device中,pppoe-wan作为L3的设备是没有mac的,其SLAAC地址就依赖于上面的随机产生的MAC

IPv6 NAT下的端口转发

NAT下内网设备就没有公网IP,对挂PT来说,可连接性可能会显示为“否”,在做种时会导致没有传入连接,所以我在网上搜索了下,找到了篇博客文章: IPv6 NAT后配置端口转发

主要的代码如下:

ip6tables -t nat -I PREROUTING -p udp --dport 49461 -j DNAT --to [2fff::17c]:49461
ip6tables -t nat -I PREROUTING -p tcp --dport 49461 -j DNAT --to [2fff::17c]:49461
ip6tables -I INPUT -p udp --dport 49461 -j ACCEPT
ip6tables -I INPUT -p tcp --dport 49461 -j ACCEPT

其中49461需要替换为BT的监听端口,最后的IP替换为BT客户端所在设备的的IP

网络环境改变导致脚本失效

在上文中提的添加路由表的一步,第二个参考链接中的状况问题也就是出在这里,这里解决的就是热插拔的情况下,可能会遇到的添加路由表时接口不对的问题,这个脚本在之后也有其他的应用,但是需要注意的一点就是$iface在高版本的OpenWrt 18.06的LuCI界面中显示为wan_6,这里还是用的原版

第一种是由于网页登录的存在(比如教学区),结果导致 IPv6 数据包被发送到 eth0.2 而不是 pppoe-wan ,当然是发不出去的,我们需要把它删掉。

第二种是不明原因导致只有本机的路由表项,结果路由器可以ping6,而连在路由器上的设备不能,我们需要把 from 字段删掉。可以新建一个 /etc/hotplug.d/iface/99-ipv6

#!/bin/sh
[ "$ACTION" = ifup ] || exit 0
iface=wan6
[ -z "$iface" -o "$INTERFACE" = "$iface" ] || exit 0

# Bad route 1
bad=$(ip -6 route show default | grep -v "pppoe-wan" | sed 's/expire.*//')
logger -t IPv6 "Old IPv6 route w/o PPPoE: $bad"
if [ "x$bad" != "x" ]; then
  logger -t IPv6 "Remove old IPv6 route..."
	status=$(ip -6 route delete $bad 2>&1)
	logger -t IPv6 "Done: $status"
fi

# Bad route 2
good=$(ip -6 route show default | grep "pppoe-wan" | sed 's/from [^ ]* //' | head -n1)
logger -t IPv6 "Good route is: $good"
logger -t IPv6 "Add good IPv6 route..."
status=$(ip -6 route add $good 2>&1)
logger -t IPv6 "Done: $status"

参考

IPv6 NAT方面看的方法比较多,列的不全,以上其实也没什么原创,按照自己的情况拼凑了些代码,写了点注释

TUNA

XDOSC

对于Linux Netfilter和iptables有两篇很好的翻译可以帮助理解

[译] 深入理解 iptables 和 netfilter 架构

[译] NAT - 网络地址转换(2016)