Hyffer
发布于 2023-02-11 / 607 阅读 / 0 评论 / 0 点赞

OpenWrt 配置IPv6端口转发

何时IPv6将完全取代IPv4仍是未知数,但大局已定,时间终会解决一切。当今网络环境对IPv6的支持已经相当完善,国内三大运营商也都陆续遵循规范(如RFC6177)向终端用户分配IPv6前缀。我所在的地区目前家庭宽带能分配到/56地址块,又恰好运营商不再接受用户申请公网IPv4地址,是时候在家庭网络中折腾一下IPv6了。

为何使用IPv6 NAT

在网上搜索“OpenWrt IPv6 port forwarding”,真正的回答[1][2]寥寥无几,大量的内容都是关于IPv6该不该使用NAT的争论。我个人认为IPv6与NAT本身并不冲突,IPv6提供了端到端的连接,而NAT技术,虽然诞生于IPv4资源紧缺的背景下,但若换一个角度,它实际上起到了对外隐藏内部网络结构的作用。在这个意义上,端口转发所解决的问题与反向代理、负载均衡是相似的,只不过处于不同的层次上(网络层,而非应用层)。

虽然在IPv6环境下任意一台主机都是可寻址的,内网与外网的分界不再像使用IPv4时那样明确,但适当地使用端口转发功能可以极大地简化网络配置,给自己带来便利。我可以将不同主机提供的服务集中于一台机器,并只用在这一台机器上配置DDNS和防火墙。另外,有些服务或软件不提供更改监听端口的功能,但可以使用端口转发变相地修改其端口号。

如果你愿意,完全可以使用一些防火墙规则配合其他工具来实现同样的功能,比如socat(如果你没听说过socat,强烈建议你了解一下,也许它更能满足你的需求)。这些应用层的工具与NAT端口转发各有各的优势,应用层工具部署简单、使用方便,NAT端口转发用法灵活、性能开销更小。后者更适用于我的应用场景,因此我选择使用IPv6 NAT来实现端口转发。

[1]一则相关性比较高的stack overflow上的问答:openwrt - IPv6 NAT6 and port-forwarding?,回答较为可靠,但在我的路由器上并不能解决问题。

[2]两篇内容丰富的博客:ipv6 NAT后配置端口转发 | Shura's自留地OpenWrt配置IPv6 NAT - TimeForget,但他们所采用的方法都是直接操作ip6tables。一方面,直接对底层进行设置通常都是不推荐的做法,我更希望通过配置防火墙规则解决问题,另一方面,OpenWrt 22.03版本已经不再支持iptables配置:[OpenWrt Wiki] Netfilter Management: any custom firewall configuration using iptables will not work on OpenWrt 22.03. It needs to be replaced by an equivalent nft mechanism.

系统版本

配置端口转发所使用的系统版本为OpenWrt 22.03.2

特别强调系统版本是因为目前系统中的防火墙为fw4,原生支持NAT66[3],而早些版本的防火墙fw3并不支持IPv6 DNAT。请检查你的系统中的防火墙,如果是fw3,你可能需要另想办法。

其次,目前防火墙生成nftables规则[4],而早些版本使用的是iptables。nftables不是必须的,iptables也可以正常工作,且本文并不会直接修改nftables,只是简单查看由防火墙规则生成的nftables配置。

由于系统版本、设置等各种因素造成的差异,任何时候,请依据实际情况适当地进行操作

[3][OpenWrt Wiki] IPv6 firewall examples

[4][OpenWrt Wiki] nftables: Since OpenWrt 22.03, fw4 is used by default, and it generates nftables rules.

如何配置

启用NAT6

这里默认你的OpenWrt路由器已经成功获取到IPv6前缀。如果你不清楚如何做,网上有很多详细的教程。

  1. 用包管理器opkg安装kmod-ipt-nat6包。

  2. 在防火墙配置文件/etc/config/firewall中,wan zone配置下添加option masq6 1

    现在,你的防火墙配置文件中应当有类似如下这段配置:

    config zone                            
            option name 'wan'              
            list network 'wan'             
            list network 'wan6'            
            option input 'REJECT'          
            option output 'ACCEPT'        
            option forward 'REJECT'        
            option masq '1'                
            option masq6 '1'               <-- HERE
            option mtu_fix '1'

    执行命令service firewall restart 重启防火墙。

关于masq6的作用是什么,官方文档中的解释十分简略:

Specifies whether outgoing zone IPv6 traffic should be masqueraded. This is typically enabled on the wan zone. Available with fw4.

后文中将通过查看fw4生成的nftables配置来说明添加这一行产生了什么效果。

在防火墙添加端口转发规则

fw4支持IPv6端口转发,但是网页UI并不支持,如果填入IPv6地址,会显示错误信息"Expecting: valid IPv4 network"。因此只能在防火墙配置文件/etc/config/firewall 中手动添加端口转发规则。

OpenWrt-Port_Forwards-UI.png

config redirect                                 
        option dest 'lan'                       
        option target 'DNAT'                    
        option name '<name>'                   
        list proto 'tcp'                    
        option src 'wan'                       
        option src_dport '<src_dport>'                 
        option dest_ip '<Server IPv6 Address>'
        option dest_port '<dest_port>'

请按实际需求填写配置信息,然后执行命令service firewall restart 重启防火墙。如果你不清楚这些配置是什么意思,可以通过网页UI生成IPv4端口转发的配置对照着看。

其中<Server IPv6 Address> 为运行服务的主机的IPv6地址,可以填写GUA (Global Unicast Address),也可以填写ULA (Unique Local Address)或LLA (Link Local Address)。如果你填写的是公网IPv6地址,那么端口转发现在应该已经能够正常工作了,你可以享受一下成果,然后带着愉悦的心情浏览完这篇文章。

但是当运营商分配的IPv6前缀发生变化,公网IPv6地址随之而变,依赖于公网地址的端口转发规则就失效了。我希望能像以前使用IPv4端口转发那样,在内网中使用静态的内部地址,从而不需要在公网地址变化时修改配置,只需更新DNS记录即可。所幸,IPv6中存在这样的地址分配方案[5]。在OpenWrt路由器中,IPv6 ULA前缀是固定的,一般在系统初始化时自动设定,可以在UI页面Network > Interface > Global network options中查看或修改。对于IPv6后缀,使用SLAAC (Stateless Address Autoconfiguration)让内网机器通过MAC地址自动生成64位的接口标识是最简单的方式,只要MAC地址不变,其ULA地址就不会改变,或者,也可以使用和IPv4地址分配一样的方式,开启DHCPv6,并在Network > DHCP and DNS > Static Leases页面手动指定内网机器的IPv6后缀。这样,内网机器就有了固定的ULA地址。当然,固定的LLA也可以。

如果你和我一样,选择使用内网IPv6地址,那很遗憾,你可以测试一下,现在端口转发应该不能使用,还需要做一些设置才能让它正常工作。(测试时记得让你的客户端处于外网环境,从OpenWrt wan侧访问端口转发。如果你没有禁用NAT Loopback,lan侧端口转发现在应是可用的,其规则与wan侧的不同)

[5]如果你对IPv6地址及其分配还很陌生,可以阅读这篇文章:IPv6动态地址分配机制详解 | 网络热度

在继续接下来的配置之前,我们先看一看这项端口转发规则生成了什么nftables配置。执行fw4 print指令,会输出由fw4生成的nftables配置,应当包含类似如下的NAT规则(其中reflection部分为NAT Loopback):

#
# NAT rules
#

chain dstnat {
	type nat hook prerouting priority dstnat; policy accept;
	iifname "br-lan" jump dstnat_lan comment "!fw4: Handle lan IPv4/IPv6 dstnat traffic"
	iifname { "pppoe-wan", "eth0.2" } jump dstnat_wan comment "!fw4: Handle wan IPv4/IPv6 dstnat traffic"
}

chain srcnat {
	type nat hook postrouting priority srcnat; policy accept;
	oifname "br-lan" jump srcnat_lan comment "!fw4: Handle lan IPv4/IPv6 srcnat traffic"
	oifname { "pppoe-wan", "eth0.2" } jump srcnat_wan comment "!fw4: Handle wan IPv4/IPv6 srcnat traffic"
}

chain dstnat_lan {
	ip6 saddr { <IPv6-PD>, <IPv6 ULA-Prefix> } ip6 daddr { <OpenWrt wan LLA>, <OpenWrt wan GUA> } tcp dport <src_dport> dnat [<Server IPv6 Address>]:<dst_port> comment "!fw4: <name> (reflection)"
}

chain srcnat_lan {
	ip6 saddr { <IPv6-PD>, <IPv6 ULA-Prefix> } ip6 daddr <Server IPv6 Address> tcp dport <dst_port> snat <OpenWrt ULA> comment "!fw4: <name> (reflection)"
}

chain dstnat_wan {
	meta nfproto ipv6 tcp dport <src_dport> counter dnat [<Server IPv6 Address>]:<dst_port> comment "!fw4: <name>"
}

chain srcnat_wan {
	meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 wan traffic"
	meta nfproto ipv6 masquerade comment "!fw4: Masquerade IPv6 wan traffic"
}

(如果你对IPv6不熟悉,可以设置一个IPv4端口转发,看看其生成的nftables配置是什么。)

若删除之前在wan zone中添加的option masq6 1 配置,则生成的nftables规则如下。可见启用masq6 对于IPv6端口转发是必要的。

#
# NAT rules
#

chain dstnat {
	type nat hook prerouting priority dstnat; policy accept;
	iifname "br-lan" jump dstnat_lan comment "!fw4: Handle lan IPv4/IPv6 dstnat traffic"
	iifname { "pppoe-wan", "eth0.2" } jump dstnat_wan comment "!fw4: Handle wan IPv4/IPv6 dstnat traffic"
}

chain srcnat {
	type nat hook postrouting priority srcnat; policy accept;
	oifname "br-lan" jump srcnat_lan comment "!fw4: Handle lan IPv4/IPv6 srcnat traffic"
	oifname { "pppoe-wan", "eth0.2" } jump srcnat_wan comment "!fw4: Handle wan IPv4/IPv6 srcnat traffic"
}

chain dstnat_lan {
}

chain srcnat_lan {
}

chain dstnat_wan {
	meta nfproto ipv6 tcp dport <src_dport> counter dnat [<Server IPv6 Address>]:<dst_port> comment "!fw4: <name>"
}

chain srcnat_wan {
	meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 wan traffic"
}

获取IPv6默认路由

在端口转发规则中使用ULA作为目标IP地址,生成的nftables配置并没有问题,为何却不能正常运作呢?于是我在路由器上抓包,试图获取有价值的信息。

客户端在局域网内时,测试端口转发是可以正常使用的(即上文所述NAT Loopback)。通过路由器的数据包如下,这是典型的TCP三次握手过程:

Source

Destination

Info

<Client GUA>(内网)

<OpenWrt wan GUA>

53416 → <src_dport> [SYN] Seq=0

<OpenWrt ULA>

<Server ULA>

53416 → <dst_port> [SYN] Seq=0

<Server ULA>

<OpenWrt ULA>

<dst_port> → 53416 [SYN, ACK] Seq=0 Ack=1

<OpenWrt wan GUA>

<Client GUA>(内网)

<src_dport> → 53416 [SYN, ACK] Seq=0 Ack=1

<Client GUA>(内网)

<OpenWrt wan GUA>

53416 → <src_dport> [ACK] Seq=1 Ack=1

<OpenWrt ULA>

<Server ULA>

53416 → <dst_port> [ACK] Seq=1 Ack=1

而当客户端在不在局域网中时,路由器中抓取的数据包如下:

Source

Destination

Protocol

Info

<Client GUA>(外网)

<OpenWrt wan GUA>

TCP

55184 → <src_dport> [SYN] Seq=0

<Client GUA>(外网)

<Server ULA>

TCP

55184 → <dst_port> [SYN] Seq=0

<Server ULA>

<Client GUA>(外网)

TCP

<dst_port> → 55184 [SYN, ACK] Seq=0 Ack=1

<OpenWrt ULA>

<Server ULA>

ICMPv6

Destination Unreachable (no route to destination)

前三个数据包依旧是TCP握手部分。第三个数据包,由服务器发出的ACK没能送达客户端,此时在客户端使用netstat 命令查看TCP连接状态为SYN_SENT,这就是端口转发不能正常运作的原因。至于为什么该数据包不能送回客户端,第四条由路由器返回的ICMPv6错误信息[6]说明了原因:没有到达目标地址的路由。

[6]关于ICPMv6错误信息,更详细的内容见RFC 4443: Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification, Ch.3 ICMPv6 Error Messages.

使用命令ip route show table all 查看路由表,其中缺省路由如下:

default via <IPv4 Upstream Gateway> dev pppoe-wan 
default from <OpenWrt wan Network> via <IPv6 Upstream Gateway> dev pppoe-wan  metric 512 
default from <IPv6 Prefix Delegated> via <IPv6 Upstream Gateway> dev pppoe-wan  metric 512

从中可以看出,对于IPv6数据包,只有当源地址为子网GUA,或源地址和wan接口处于同一个网络,路由器才会正确转发数据包。这是OpenWrt的默认行为,在官方文档中有提及[7]。而服务器返回客户端的ACK数据包,其源地址是服务器的ULA,不能匹配任何一条路由。

[7][OpenWrt Wiki] Routing basics: Note that by default OpenWrt announces IPv6 default route only for GUA and applies source routing for IPv6 that allows routing only for prefixes delegated from the upstream router.

要添加一条IPv6缺省路由,有很多途径。我采用了nftables: IPv6 NAT packets not going into forward chain but returned destination unreachable - Super User回答中给出的方法,将accept_ra 的值设置为2,让OpenWrt接受来自上游的路由器通告(Router Advertisement),从而自动设置IPv6默认路由。

执行命令sysctl -w net.ipv6.conf.pppoe-wan.accept_ra=2 设置参数,你需要将"pppoe-wan"更改成你的路由器上wan侧接口的名称。如果你不清楚实际接口名称是什么,可以使用命令ls /proc/sys/net/ipv6/conf列出所有的可选项(注意其中的all和default,尽量避免使用它们,根据OpenWrt Forum上的这篇帖子Windows clients cannot access public ipv6 unless a few miniutes later while use fw4 nat,在all和default中配置accept_ra没有任何效果)。耐心等待一段时间,再查看路由表,多了一条IPv6默认路由:

default via <IPv4 Upstream Gateway> dev pppoe-wan 
default from <OpenWrt wan Network> via <IPv6 Upstream Gateway> dev pppoe-wan  metric 512 
default from <IPv6 Prefix Delegated> via <IPv6 Upstream Gateway> dev pppoe-wan  metric 512 
default via <IPv6 Upstream Gateway> dev pppoe-wan  metric 1024  expires 0sec

现在端口转发应当可以正常工作了!

先别急着庆祝,使用sysctl -w 命令设置的参数不是持久性的,如果机器重启,之前的设置都会丢失。这还不简单?你可能会想,将net.ipv6.conf.pppoe-wan.accept_ra=2 添加到/etc/sysctl.conf 配置文件就可以了。但事实恰恰相反,这样做不会有任何作用。因为在系统启动的过程中,按照初始化顺序,载入sysctl配置时,很多内核模块尚未被加载,那些对不存在的条目的配置将被忽略[8][9]。实际测试中发现,即使将指令sysctl -w net.ipv6.conf.pppoe-wan.accept_ra=2添加到/etc/rc.local文件中,使其执行于模块加载之后,也可能因为pppoe-wan接口尚未启动而配置失败。因此,我简单粗暴地将该指令添加到crontab计划任务中,并每分钟执行。至此,IPv6端口转发配置完成。

[8]Bug #50093 "Some sysctls are ignored on boot"

[9]Ubuntu Manpage: sysctl.d - Configure kernel parameters at boot: Many sysctl parameters only become available when certain kernel modules are loaded. Modules are usually loaded on demand, e.g. when certain hardware is plugged in or network brought up. This means that systemd-sysctl.service(8) which runs during early boot will not configure such parameters if they become available after it has run.

一些调试技巧

由于各种因素造成的差异,我的配置过程可能并不适用于你的系统。如果本文没能成功帮助你解决问题,也别灰心,有一些通用的调试技巧,或许可以让你更迅速地定位问题所在。

  • 使用fw4 print 输出由防火墙生成的nftables配置。[OpenWrt Wiki] Netfilter Management: When debugging rules emitted by fw4, this is a good starting point.

  • Wireshark抓包分析工具你大概率用过,或至少听说过。但是如果你不知道Wireshark可以配合tcpdump工具进行远程抓包,这篇文章是一个很好的起点:使用Wireshark完成OpenWrt抓包

  • 当遇到数据包迷路、走丢的情况,检查路由表是个不错的选择。命令ip -6 route 可以打印IPv6路由表,或者使用命令ip route show table all 打印所有表。

感谢

在查资料的过程中,我阅读了很多优秀的文章。本文与之相关处,均附上了指向它们的链接。

特别感谢TUNA技术群中的@Blaok和@ξ,他们引导我通过远程抓包、检查nftables规则等方法探寻问题的根源。如果没有他们,我不可能解决这么多问题,实现所需功能,也就不存在这篇文章。

后续

2025.2.6更新

最近更换了新的路由设备,系统版本也升级到 24.10,新版Web UI中的端口转发设置已经支持填写IPv6地址。

另外,在Network > Interfaces > wan > Advanced Settings页面下有个"IPv6 source routing"选项,可以修改系统的IPv6路由行为,取消勾选该选项,即可解决源地址为ULA的IPv6数据包无法路由的问题。