因为入手了一台蜗牛星际的小机器,想着把宿舍里的设备连起来,因此笔者决定购置一台路由器。 考虑到我需要用 OpenWrt 等非官方系统,在群友的推荐下,笔者购入了一台红米 AC2100,花费了 199 元巨款——一年前此设备还只需要 ¥199,让我不禁感叹不愧是理财产品。 系统方面笔者使用了自己编译的 OpenWrt 22.03.0-rc4 ——比新更新!!此版本在 2022 年 6 月才发布,属于 release candidate 版本,类似于某种“预览版”。需要指出的是,此决定带来了诸多的问题,但本着版本号党的精神和折腾不休的精神,笔者认为并不是太大的障碍。这个相当新的版本带来了大量的新东西,如以 nftables 代替了 iptables 系列、引入了全新的防火墙版本 firewall4 等。同时笔者也没有使用国内热门的 lean 版本 OpenWrt,因为笔者不太喜欢使用这种“懒人包”一样的东西,同时也对其中各种魔法上网工具等没有需求。跟随本文,我们可以看到,我还要很多次地重新编译系统…… 最后这玩意花了几天的十几个小时,再加上大量的抓包等操作,只能说纯纯的精神折磨了……看来 ITSC 的人并不打算为师生行个方便啊(

给懒得看的人的快速方案

首先,我采用了 NAT6 的方法。如果你用的旧系统,或新系统上装了 ebtables,则你也可以用网桥+过滤非 IPv6 数据包的方法,但笔者认为不太优雅。后者请自行在网上找方案。中继的方法在南大校园网内不可用,具体原因见下。其次,这里假定用的是最新系统,不保证旧的系统能用。特别注意这里是更新成 DSA 模式后的版本,如果你的系统还在 18.x 时代,那可能并不适用。 然而,如果你用了基于 nftables 和 firewall4 的系统,务必确保你的系统里没有传统的 ip6tables 等和相对应的 kmod-ipt-xxxx!(或者你也可以装上相关的 *-legacy 工具,手动排查冲突——祝你好运!)这些包经常会被作为依赖装进来,比如 MWAN3 就会把传统 netfilter 全家桶装进来,因此务必小心排查(显然,MWAN3 还没有适配好最新的 nftables,因此追新是有代价的)。 这里的配置文件只会保证你的 WAN 口可以获取到能用的 IPv6 地址,在此之上的防火墙配置等请按照自己的系统情况配置。具体的配置(firewall4 的配置相当简单)见后面的节。 配置文件需要这样写:

/etc/config/network

# 此文件主要定义接口啥的
# 省略上面的部分

config globals 'globals'
        option packet_steering '1'
        option ula_prefix 'fd76:d462:c4e3::/48' # 这里可以随便写一个fd00::/8 下的 /48 前缀,用于分配给下层设备

# 中间省略

config interface 'wan6' # 这节应该是已经存在的,那只需要做修改
        option device 'wan' # wan 口接口名
        option proto 'dhcpv6'
        option reqaddress 'try' # 重要
        option vendorclass '0000013700084D5346542035102E30'
        option reqprefix 'no'   # 重要!!
/etc/config/dhcp 里不需要有 wan6 相关的配置。重启 WAN 口,即可获取到 IPv6 地址。如果你的系统是旧版,再参照网上的其它教程就可以了。如果是新版,请继续往下看。

环境

南大的 IPv6 环境不可谓不恶劣。其地址分配采用了有状态 DHCPv6,且每个客户端只能拿到单个 IPv6 地址。这和大部分家用宽带可获得 /56 或 /64 前缀的情况完全不同,更遑论使用 DHCP-PD 方式下发前缀或利用 SLAAC 自动管理了。同时,单个 IP 也让通过 ndppd 等 ND 代理方式强行划分子网段的手法变得不可行。 更离谱的是,学校的 IPv4 与 IPv6 均仅允许单 MAC 地址使用单个 IPv4/6 地址。因此,如果单一 MAC 地址两次获取地址,前面获取的地址是不可用的,发的包会被丢掉!这造成普通的 ND/RA/DHCPv6 中继模式也不可用。 另外,南大 IPv6 不需要过 Portal 验证,即不登录也能用。但只有有线网络可以用 IPv6。

详细过程和配置

在网上可以了解到,目前有三种在教育网这样的特殊环境配置 OpenWrt 的方法,按资料的量排,分别是 NAT6、ND/RA/DHCPv6 中继与桥接。我三种都折腾了,最后还是用了 NAT6。

ND/RA/DHCPv6 中继

这种方式本来应该是最优雅的方式,但在南大是几乎不可用的(原因前面说了),除非伪造 MAC(实际上似乎也不是那么难)。要看懂这种方式,需要了解一下 IPv6 中主机之间、主机与路由器之间是怎么互相发现的。 与 IPv4 的 ARP 协议不同,IPv6 使用基于 ICMPv6 的 Neighbor Solicitation/Advertise (NS/NA) 方式寻找某个 IPv6 地址对应的 MAC 地址。同时路由器的寻找也并非像 IPv4 那样使用 DHCP 上的某个选项实现,而是使用 Router Solication/Advertise (RS/RA) 方式自动寻找。 这样,为了让下层的设备能获取 IPv6 地址,我们只需要把客户端的 DHCPv6 Solicitaion/Request 改写成路由器的 MAC 通过 WAN 转发出来,再将 DHCPv6 Advertise/Reply 转发回去即可。为了让上层设备能看到下层,只需要把上层发到 WAN 口的 Neighbor Solicitation 转发下去,再把下层回复的 Neighbor Advertisement 包的 MAC 改写、转发到 WAN。最后为了让下层设备能往外发包,将 LAN 收到的 Router Solicitation 的 MAC 改写发 WAN 再将上层设备的 Router Advertisement MAC 改写成路由器 LAN 口的 MAC 发给客户端即可。 因此理论上,我们只需要让路由器转发 IPv6 流量,再配置 odhcpd 的 ND/RA/DHCPv6 中继即可实现我们要的功能:
# (/etc/config/dhcp)
config dhcp lan
    (各种 DHCPv4 设置)
    option master '1'
    option dhcpv6 'relay'
    option ra 'relay'
    option ndp 'relay'
    option ndproxy_slave '1'

config dhcp wan6
    option ignore '1'
    option master '1'
    option dhcpv6 'relay'
    option ra 'relay'
    option ndp 'relay'
这样在南大确实所有设备都能获取到 IPv6 地址,但并不能上网。刚开始因为不知道南大有单 MAC 单 IP 限制,因此进行了大量的抓包检测,最后得到单 IP 限制的结论(其实 25 号晚 IPv4 相关的一件事才让此事变得确凿)。这里也分享我抓包的命令(必须在 Windows CMD 里运行,假定你已经装了 Wireshark 在下面说的路径里):
ssh root@<路由器的IP> tcpdump -i <设备名 一般抓wan> -U -s0 -w - '<筛选器,可以省>' | "C:\Program Files\Wireshark\Wireshark.exe" -k -i -

其中设备名可以用 ip addr 看,注意不要 @ 后面的部分
例子:ssh root@192.168.1.1 tcpdump -i wan -U -s0 -w - 'ip6' | "C:\Program Files\Wireshark\Wireshark.exe" -k -i -
就假定 OpenWrt 在 192.168.1.1,抓取 WAN 口上的 IPv6 数据包
顺道一提,odhcpd 这个工具的风评一般。旧的 odhcpd 版本有诸多问题,而且网上的配置教程少得可怜。因此自己摸索一下是必须的。 和本方案相关的资料会放在文末的延伸阅读里(包括 ND/SA 等知识)。

网桥

这是我个人认为最不优雅的方案,然而也是一个能让所有下层设备都得到 GUA 地址的手段。其基本原理就是把 WAN 也加入原来只有 LAN 口的 br-lan 网桥,相当于把 WAN 口和 LAN 口用交换机连接在一起,再利用 ebtables 过滤掉非 IPv6 的流量(否则局域网 DHCP 会污染外面,且也可能造成路由器挂掉)。这样就实现了类似于“v4 路由,v6 交换”的效果。 然而,根据一位 19 级同学(YDJSIR)的测试结果,这样做造成了巨大的性能损失。同时笔者也认为这种先把所有包串起来,再用防火墙过滤掉的方法很“妖”。无论如何,还是进行了尝试。 这里就是 nftables 第一次背刺我的地方了。这里过滤非 IPv6 使用了 ebtables 里 BROUTE 表的 BROUTING 链具有的特殊行为——在该链被 accept 的包会被转发,被 drop 的包会进入 input 被路由、NAT。因此我们只需要两条命令就可以实现全部操作:
ebtables -t broute -A BROUTING -p ! ipv6 -j DROP
brctl addif br-lan wan
当然,每次启动系统都需要这两条,因此需要放启动脚本里。也可以通过配置 LuCI 里网桥的配置和防火墙配置(不知道 firewall3 是怎么配置的)来达到目的。 问题在于——nftables 并没有 BROUTING 链的特殊功能!ebtables-nft 命令的 man page 明确指出了该功能没有实现,同时 netfilter 的 mailing list 上关于此问题的 E-Mail 也最终没有得到任何有效回答。我在 Super User SE 上问了这个问题,也没有啥回答。 因此我最后的方法是——同时安装传统的 ebtables-legacy 包和 nftables 包,用 ebtables-legacy 实现此功能。然而不知道是不是配置错误或内核模块没有正确启动,在这样做后,IPv6 是通了,既可拿 IP 也可访问外网,但整个 IPv4 崩了,显然 ebtables 抽风了。 不过,根据社团网络配置来看,这种配置应当是可以实现的,可能只是我配置的问题,或 ebtables 与 nftables 冲突了。

NAT6

因此 NAT6 成了我最后的手段。NAT 为何物想必不用赘述。然而,如果看网上所有有关 NAT6 的教程,会注意到他们全部假定你的路由器 WAN 口有可上网的 IPv6 地址,而这正是南大宿舍楼里直接接 OpenWrt 获取不到的。 调试这个需要了解 OpenWrt 的 DHCP 客户端。其用的是 odhcp6c,OpenWrt 项目自己搓的客户端。在我一波胡乱操作 LuCI 里的 DHCPv6 设置后,我注意到系统日志里打了大量来自 odhcp6c 的“Server returned IA_NA status: No prefix available (NoPrefixAvail)” 错误。再通过抓包发现,路由器同时请求了 IPv6 地址和前缀,而服务器的响应中,地址给了,前缀没给(NoPrefixAvail)。再排查 OpenWrt 如何应用地址,通过 ps | grep odhcp6c 发现了:
 2895 root      1132 S    odhcp6c -s /lib/netifd/dhcpv6.script -Nforce -P0 -t120 wan
(具体的参数记不清了)
这暗示着 odhcp6c 可以直接调用,且问题可能出在 /lib/netifd/dhcpv6.script 上,它很可能有 bug。但我懒得去改这套极端复杂的 Shell 脚本。有没有办法让 odhcp6c 不获取前缀呢?通过手动调用 odhcp6c,我发现不加 -P 参数就行了,再通过 -V 拟态一下 Windows 客户端,就可以获取到 IPv6 地址。 如何在 LuCI 或 UCI(即 /etc/config/network)中体现这一点呢?答案是:
config interface 'wan6'
        option device 'wan'
        option proto 'dhcpv6'
        option reqaddress 'try'
        option vendorclass '0000013700084D5346542035102E30'
        option reqprefix 'no' # 重要
如果原来没有 wan6 接口,则应直接把这段写进去。重启路由器即可。可以通过 curl 等检查下路由器是否已经可以正常用 IPv6 上外网。 接下来是 NAT6 的配置。首先我们需要配置 DHCPv6 让客户端可以获取到内网 IP 地址。同样在 /etc/config/network 中,我们在 global 段如下配置:
config globals 'globals'
        option packet_steering '1'
        option ula_prefix 'fd76:d462:c4e3::/48'

config interface 'lan'
        (省略 v4 部分)
        option delegate '0'
        option ip6assign '64'
其中的 ula_prefix 可以是 fd00::/8 下的任何一个 /48 前缀,不用和我一样。 再看 /etc/config/dhcp,需要做如下的配置(如果对注释报错,手动删一下注释,UCI 不一定能正确处理):
config dhcp 'lan'
        (省略 v4 部分)
        option ra 'server'
        option dhcpv6 'server'
        option ra_management '1'
        option ra_default '1' # 重要

config dhcp 'wan'
        option interface 'wan'
        option ignore '1'

# 不需要 wan6 的部分

config odhcpd 'odhcpd' # 这里就是默认设置
        option maindhcp '0'
        option leasefile '/tmp/hosts/odhcpd'
        option leasetrigger '/usr/sbin/odhcpd-update'
        option loglevel '4'
再重启 odhcpd(service odhcpd restart),即可看看效果。注意上述 ra_default 是必须的,否则 odhcpd 会不向下层设备发送 Router Advertisement,然后往日志里打一大堆“A default route is present but there is no public prefix on br-lan thus we don't announce a default route!”。因此需要用这个开关强制让它发 RA。 最后我们只需要配置防火墙——得益于 firewall4,这个过程在新版 OpenWrt 相当轻松。只需要在 /etc/config/firewall 里原来的 WAN Zone 加一行即可:
config zone
        option name 'wan'
        option input 'REJECT'
        option output 'ACCEPT'
        option forward 'REJECT'
        option masq '1'
        option mtu_fix '1'
        list network 'wan'
        list network 'wan6'
        option masq6 '1' # 加了这一行!!!
重启防火墙,即可看看是不是客户端也可以正常访问 IPv6 外网了。如果你用的 firewall3,可以去网上找找相应的文章。 2022/06/26 upd: 再次出现了路由器能上 v6 客户端上不了的情况……感觉 nftables 又出锅了,我先去 OP 的社区问问。如果你用 firewall3,则没有这种问题。 解决了。为了让 LAN 里的机器也能上网,你还需要配置 /etc/sysctl.conf,在其中加上:
net.ipv6.conf.wan.accept_ra = 2
重启路由器即可。

一些小插曲

在配置好 NAT6 后,我遇到了路由器能正常用 IPv6,但下层的客户端不能的情况——NAT 没有正常运行。经过几小时痛苦的 nft monitor trace 后(具体食用方法见 nftables wiki),我最终定位到:包能到达 nat prerouting 链,但既不可进入 mangle input 链,也无法进入 mangle forward 链(见下图)。
在此之上,我一筹莫展了。我一度怀疑我忘了装 NAT6 所必须的内核扩展,因此去编译时的环境看了眼,结果我大呼好家伙——里面不仅没有少扩展(kmod-nft-nat6),还多了一堆传统的扩展(kmod-ipt-xxxx)。然而,我并没有装 ip6tables-legacy 之类的旧工具,因此我甚至无法管理这些扩展的规则。这些传统扩展是跟着 ip6tables-nft 等 nftables 系管理工具装进来的,感觉是某种 OpenWrt 上游的 bug。 把这些扩展从编译的包里去掉后,NAT 就恢复正常了。并没有!重启后问题重新出现。各种查资料后,最终在 OpenWrt 论坛上的一个帖子里找到了他的配置和我不同的地方:他的 /etc/sysctl.conf 多了一行:
net.ipv6.conf.wan.accept_ra = 2
这样似乎才可以让内核里也收到路由器发来的 RA 报文。我这样配置后不知道为什么,还是不工作。因此又在 Super UserOpenWrt 论坛里发了两个帖子。SU 上一个评论也提到要用上面的这个开关,因此我晚上回去后再次尝试,居然就成功了。 另一个小插曲是那天我回宿舍发现 IPv4 又挂了。各种 ping 不通,traceroute 也没啥有效结果。最戏剧性的是我跑了一下 ip addr,发现路由器的 WAN 口有两个 IPv4 地址。结合前面对学校做了 MAC-IP 绑定限制的猜想,用 Wireshark 确定当前发包用的 IP 后删掉这个发包的 IP(也就是切换到 secondary IP),结果居然短暂地恢复的网络。几秒后问题再次出现,再次观测到两个 IP。 显然,这说明:
  1. 实锤了学校限制了一个 MAC 只能用一个 IP,而且是最后一个从 DHCP/DHCPv6 获取的 IP;
  2. 证明我的 OpenWrt 里有两个 DHCP 客户端,这造成了冲突。
最后发现是 dhcpcd 和自带的默认客户端 udhcpc 冲突。dhcpcd 是前面 odhcp6c 出问题的时候,我想装为替代的 DHCP 全栈客户端,结果忘了删造成冲突。重新编译去掉后正常。

延伸阅读

实际上还参考了其它大量的文章,有的没记起来……也一并致谢!

不想被自己的惰性打败。