在 AX88U + Merlin 384.16 上实现 LAN 端口隔离(静态 VLAN)

最近把手头用了四年多的 AC68P 升级成了支持 802.11ax (WiFi 6) 的 AX88U。不用多说,到手第一天就刷了 Merlin 梅林固件。

不过无论是原厂固件还是梅林固件,它们都没有提供原生的 GUI 划分 VLAN 的功能(相比之下,在 OpenWrt 上配置 VLAN 就很方便)。但是由于博通搞的闭源驱动1,OpenWrt 在有生之年大概都不会支持 AX88U。所以要在 AX88U 上配置 VLAN,只能通过命令行的方式。

需求

AX88U 上一共有 8 个 LAN 口,我想把 LAN 2 隔离出来,用做访客网络。换句话说,LAN 1, 3 - 8 划入 VLAN 1, LAN 2 单独划入 VLAN 2

理论上,真正的隔离应该是禁止两个 VLAN 互相访问。但是考虑到万一需要在我的电脑(VLAN 1)上重新配置二级路由器(VLAN 2),还是得允许 VLAN 1VLAN 2 的单向通信。

此外,上述提到的隔离对客户端应当是完全透明的:VLAN 2 里的客户端不需要额外的配置,只需要插上网线即可上网。

有关 AX88U

没有 robocfg

很不幸的是,AX88U(博通 HND 平台)移除了 robocfg 命令。搜索了一番之后,发现似乎 vlanctl2 可以用来配置 VLAN。但是玩了一阵子,目测 vlanctl 只能搞 Tagged VLAN,没法实现静态 VLAN。

不过以防有人有这方面的需求,推荐阅读这篇文章:上海电信 TL-EP110 + RT-AC86U 实现观看 4K IPTV 无卡顿 (2019-10)。这篇文章介绍了如何在 AC86U(也是 HND 平台的路由器)上配置 Tagged VLAN。

以太网接口到物理端口的映射

在网上我没搜到相关的信息,不过经过若干次网线插拔之后,总结出来这张表:

以太网接口 物理端口
eth0 WAN
eth1 LAN 4
eth2 LAN 3
eth3 LAN 2
eth4 LAN 1
eth5 LAN 5 - 8 的网桥
eth6 2.4 GHz 无线
eth7 5 GHz 无线

这里注意 eth5 应该是通过 BCM53134 组的硬件网桥(LAN 5 - 8)。使用 etchtlethswctl 或许能解散这个网桥,不过我没试验。所以,下文提到的解决方案只能用于隔离 LAN 1 - 4。

此外,AX88U 默认把 eth1 ~ eth7 组成了网桥 br0

注意:我的 WAN 联机类型动态 IP。如果你用的是 PPPoE 的话,可能需要看情况把下文提到的 eth0 换成 ppp0

一条命令(ebtables)的解决方案

如果不需要给两个 VLAN 划分独立子网的话,只需要一条 ebtables 命令就可以了:

# 在 AX88U 上,eth3 是物理端口 LAN 2 
ebtables -A FORWARD -i eth3 -o br0 -j DROP

这里的 eth3 是物理端口 LAN 2。这条命令使用了二层防火墙 ebtables,用于阻隔 eth3br0 之间的通信。由于 eth3 还在 br0 里,所以不需要额外配置 iptables 或者 DHCP。

基于网桥的解决方案

如果需要给两个 VLAN 划分独立的子网的话,就需要创建新的网桥了。下文以 VLAN 1 使用 192.168.50.0/24VLAN 2 使用 192.168.150.0/24 为例。

新建网桥 br1

首先,从 br0 里移除 eth3,创建 br1 并把 eth3 加入 br1

# 从 br0 中删除需要隔离的端口
brctl delif br0 eth3

# 把需要隔离的端口加入 br1
brctl addbr br1
brctl stp br1 on # 启用 STP 协议防止环路,避免网络风暴
brctl addif br1 eth3

这里新建网桥 br1 而不是直接使用 eth3 是为了方便管理:如果以后想把其他端口也隔离进 VLAN 2,只需要把对应的端口移出 br0 并加入 br1 即可。由于下文提到的所有规则都是针对 br1 的,所以新加入的端口不需要额外的设置。

执行 brctl show 验证 br1 已正确创建:

[email protected]:/# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.a85e45fakeid       yes             eth1
                                                        eth2
                                                        eth4
                                                        eth5
                                                        eth6
                                                        eth7
br1             8000.a85e45fakeid       yes             eth3

然后,给 br1 设置 IPv4 地址 192.168.150.1/24,并启用 br1

# 设置 br1 的 IPv4 地址并启用 br1
# 这里使用的 IPv4 子网是 192.168.150.0/24
# 系统会自动设置 IPv6 链路本地地址
ifconfig br1 192.168.150.1 netmask 255.255.255.0
ifconfig br1 allmulti up

由于 AX88U 默认给 br0 分配了 192.168.50.1/24,所以不需要设置 br0

最后,执行 ifconfig br 验证 br1 的 IPv4 地址:

[email protected]:/# ifconfig br1
br1       Link encap:Ethernet  HWaddr A8:5E:45:00:FA:KE
          inet addr:192.168.150.1  Bcast:192.168.150.255  Mask:255.255.255.0
          inet6 addr: fe80::aa5e:45ff:fe00:fake/64 Scope:Link
          UP BROADCAST RUNNING ALLMULTI MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

系统会使用 EUI-64 算法自动为 br1 生成 IPv6 链路本地地址。

提示:完整的脚本可以在 脚本与配置文件:/jffs/scripts/services-start 中找到。

添加 iptables 规则

首先,允许来自 br1 的传入连接:

# 允许来自 br1 的传入连接
iptables -I INPUT -i br1 -m state --state NEW -j ACCEPT

但是禁止 br1 访问路由器 Web UI 和 SSH:

# 禁止 br1 访问路由器 Web UI 和 SSH
iptables -I INPUT -i br1 -p tcp --dport 80 -j DROP
iptables -I INPUT -i br1 -p tcp --dport 22 -j DROP
ip6tables -I INPUT -i br1 -p tcp --dport 80 -j DROP
ip6tables -I INPUT -i br1 -p tcp --dport 22 -j DROP

然后,不转发来自 br1 的数据包:

# 不转发来自 br1 的数据包
iptables -I FORWARD -i br1 -j DROP

但是允许 br1 内部的转发:

# 但是允许 br1 内部的转发
iptables -I FORWARD -i br1 -o br1 -j ACCEPT

也允许 br1 的数据包转发至 eth0 (WAN):

# 允许 br1 的数据包转发至 eth0 (WAN)
iptables -I FORWARD -i br1 -o eth0 -j ACCEPT

也允许 br0br1 的单向通信:

# 允许 br0 到 br1 的单向通信
iptables -I FORWARD -i br0 -o br1 -j ACCEPT
iptables -I FORWARD -i br1 -o br0 -m state \
  --state RELATED,ESTABLISHED -j ACCEPT

最后,配置 br1 所属子网 192.168.150.0/24 的 NAT:

iptables -t nat -A POSTROUTING -s 192.168.150.0/24 -d 192.168.150.0/24 \
  -o br1 -j MASQUERADE

分别执行 iptables -S INPUTiptables -S FORWARDiptables -t nat -S POSTROUTING 验证相应的规则已添加:

[email protected]:/# iptables -S INPUT
-P INPUT ACCEPT
-A INPUT -i br1 -p tcp -m tcp --dport 22 -j DROP
-A INPUT -i br1 -p tcp -m tcp --dport 80 -j DROP
-A INPUT -i br1 -m state --state NEW -j ACCEPT
# ... 省略多余输出 ...
-A INPUT -j DROP

[email protected]:/# iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -i br1 -o br0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i br0 -o br1 -j ACCEPT
-A FORWARD -i br1 -o eth0 -j ACCEPT
-A FORWARD -i br1 -o br1 -j ACCEPT
-A FORWARD -i br1 -j DROP
# ... 省略多余输出 ...

[email protected]:/# iptables -t nat -S POSTROUTING
-P POSTROUTING ACCEPT
# ... 省略多余输出 ...
-A POSTROUTING -s 192.168.50.0/24 -d 192.168.50.0/24 -o br0 -j MASQUERADE
-A POSTROUTING -s 192.168.150.0/24 -d 192.168.150.0/24 -o br1 -j MASQUERADE

提示:

配置 DHCPv4 (dnsmasq)

/jffs/configs/ 中创建配置文件 dnsmasq.conf.add

touch /jffs/configs/dnsmasq.conf.add

比照 br0,为 br1 设置 DHCPv4 的地址池和选项:

cat <<EOF >> /jffs/configs/dnsmasq.conf.add
interface=br1
# DHCPv4 地址池: 192.168.150.2 - 192.168.150.254, 子网掩码: 255.255.255.0
# DHCPv4 租约时间: 86400 秒 (1 天)
dhcp-range=br1,192.168.150.2,192.168.150.254,255.255.255.0,86400s
# DHCPv4 路由器 (option 3): 192.168.150.1
dhcp-option=br1,3,192.168.150.1
EOF

重启 dnsmasq 以应用设置:

service restart_dnsmasq

执行 tail /tmp/syslog.log -n 50 验证 dnsmasq.conf.add 已被正确加载:

[email protected]:/# tail /tmp/syslog.log -n 50
# ... 省略多余输出 ...
Apr 14 20:49:22 rc_service: service 15995:notify_rc restart_dnsmasq
Apr 14 20:49:22 dnsmasq[1149]: exiting on receipt of SIGTERM
Apr 14 20:49:22 custom_config: Appending content of /jffs/configs/dnsmasq.conf.add.
Apr 14 20:49:22 dnsmasq[15998]: started, version 2.81rc4-33-g7558f2b cachesize 1500
Apr 14 20:49:22 dnsmasq[15998]: asynchronous logging enabled, queue limit is 5 messages
Apr 14 20:49:22 dnsmasq-dhcp[15998]: DHCP, IP range 192.168.150.2 -- 192.168.150.254, lease time 1d
Apr 14 20:49:22 dnsmasq-dhcp[15998]: DHCP, IP range 192.168.50.2 -- 192.168.50.254, lease time 1d
# ... 省略多余输出 ...

配置 IPv6

本文使用的是无状态 DHCPv6 (DHCPv6 stateless),主机的 IPv6 地址是通过 SLAAC3 自动生成的。

由于 SLAAC 使用了 EUI-64 算法,这要求 IPv6 前缀长度必须小于 /64。所以要使用下文提到的解决方案,ISP 分配的 IPv6 LAN 前缀长度必须小于等于 /63

通过前缀提示 (Prefix Hint) 获取更短的前缀

某些 ISP 允许通过发送前缀长度提示获取小于 /64 的前缀。例如,我这里的 ISP Charter Spectrum 就允许通过前缀提示获取 /56 的前缀。

我的 AX88U 上 IPv6 的设置是:

名称
基本设置  
联机类型 Native
DHCP-PD 启用
IPv6 内部网络设置  
自动配置设置 Stateless
IPv6 DNS 设置  
自动接上 DNS 服务器 关闭
IPv6 DNS 服务器 1 2606:4700:4700::1111
IPv6 DNS 服务器 2 2606:4700:4700::1001
IPv6 DNS 服务器 3 留空
自动配置设置  
是否启动路由广播 启用

AX88U 上的 DHCPv6 客户端 odhcp6c 默认不会发送前缀提示。要强制 odhcp6c 发送前缀提示4,首先执行:

ps | sed -e 's/^.*\odhcp6c \(.*\)$/\1/;t;d' | head -n 1

获取系统运行 odhcp6c 的命令:

-df -R -s /tmp/dhcp6c -N try -c <duid> -FP 0:<iaid> eth0

<duid><iaid> 与路由器的 MAC 地址有关。取决于 IPv6 设置,可能还有类似 -r23-r24 的参数,在下面的命令里也要加上这些参数。

对应替换 <duid><iaid>,执行下面的两条命令:

# 结束现有的 odhcp6c
killall odhcp6c
# 重新运行 odhcp6c,前缀提示长度为 56
# 对应替换 -c 和 -FP 的参数 <duid> 和 <iaid>
odhcp6c -df -P56 -R -s /tmp/dhcp6c -N try -c <duid> -FP 0:<iaid> eth0

这里 -P56 中的 56 即是要发送的前缀提示长度。

执行 ifconfig br0 验证 br0 已被分配到 /56 的 IPv6 全局地址:

[email protected]:/# ifconfig br0
br0       Link encap:Ethernet  HWaddr A8:5E:45:00:FA:KE
          inet addr:192.168.50.1  Bcast:192.168.50.255  Mask:255.255.255.0
          inet6 addr: 2600:6c51:fake:n00b::1/56 Scope:Global
          inet6 addr: fe80::aa5e:45ff:fe00:fake/64 Scope:Link
          UP BROADCAST RUNNING ALLMULTI MULTICAST  MTU:1500  Metric:1
          RX packets:4241 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3714 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:829133 (809.7 KiB)  TX bytes:1787334 (1.7 MiB)

如果没看到类似的输出,那下文的解决方案并不适合你,但你可以在 br1 上设置有状态 DHCPv6 (DHCPv6 stateful)。

提示:完整的脚本可以在 脚本与配置文件:/jffs/scripts/wan-event 中找到。

添加 ip6tables 规则

ip6tables 的规则类似上文 iptables 的规则,但是不需要设置 NAT。

INPUT 链:

# 允许来自 br1 的传入连接
ip6tables -I INPUT -i br1 -j ACCEPT # br0 的默认规则
ip6tables -I INPUT -i br1 -m state --state NEW -j ACCEPT

# 禁止 br1 访问路由器 Web UI 和 SSH
ip6tables -I INPUT -i br1 -p tcp --dport 80 -j DROP
ip6tables -I INPUT -i br1 -p tcp --dport 22 -j DROP

FORWARD 链:

# 不转发来自 br1 的数据包
ip6tables -I FORWARD -i br1 -j DROP

# 但是允许 br1 内部的转发
ip6tables -I FORWARD -i br1 -o br1 -j ACCEPT

# 也允许 `br1` 的数据包转发至 `eth0` (WAN)
ip6tables -I FORWARD -i br1 -o eth0 -j ACCEPT

# 也允许 `br0` 到 `br1` 的单向通信
ip6tables -I FORWARD -i br0 -o br1 -j ACCEPT
ip6tables -I FORWARD -i br1 -o br0 -m state \
  --state RELATED,ESTABLISHED -j ACCEPT

提示:完整的脚本可以在 脚本与配置文件:/jffs/scripts/firewall-start 中找到。

配置 DHCPv6

默认情况下,br0 会使用整个 /56 的子网。如果要为 br1 划分子网,则需要改变 br0 的前缀长度。

首先,执行 ip -6 addr show br0 scope global 获取 ISP 分配的 IPv6 LAN 前缀:

[email protected]:/# ip -6 addr show br0 scope global
22: br0: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500
    inet6 2600:6c51:fake:n00b::1/56 scope global
       valid_lft forever preferred_lft forever

你拿到的前缀肯定跟这里的是不一样的,所以执行下面的命令时别忘了对应替换。

然后,为 br0 重新设置 /64 的前缀长度:

# 删除 /56 前缀,把 2600:6c51:fake:n00b::1 换成你的
ip -6 addr del 2600:6c51:fake:n00b::1/56 dev br0
# 设置 /64 前缀,把 2600:6c51:fake:n00b::1 换成你的
ip -6 addr add 2600:6c51:fake:n00b::1/64 dev br0

当然这里可以为 br0 设置更短的前缀长度,但是我觉得 /64 已经够用了。

再次执行 ip -6 addr show br0 scope global,检查 br0 的新前缀:

[email protected]:/# ip -6 addr show br0 scope global
22: br0: <BROADCAST,MULTICAST,ALLMULTI,UP,LOWER_UP> mtu 1500
    inet6 2600:6c51:fake:n00b::1/64 scope global
       valid_lft forever preferred_lft forever

由于 dnsmasq 并不支持 DHCPv6 前缀代理(DHCPv6 Prefix Delegation, DHCPv6-PD)5 6 所需的 option 25 和 26 7,下面分两种情况讨论。

不使用 DHCPv6-PD

如果不接二级路由器的话,倒是可以用 dnsmasq 当 DHCPv6 服务器。

首先,为 br1 设置 br0 的邻接 /64 前缀:

# 为 br1 设置 br0 的邻接 /64 前缀
# 比如 br0 的前缀为 aaaa:bbbb:cccc:dddd::1/64,
# 则 br1 的前缀为 aaaa:bbbb:cccc:ddde::1/64.
# 把 2600:6c51:fake:n00c::1 换成你的
ip -6 addr add 2600:6c51:fake:n00c::1/64 dev br1

然后,为 br1 设置 DHCPv6 地址池和选项:

cat <<EOF >> /jffs/configs/dnsmasq.conf.add
# DHCPv6 路由广播间隔:10 秒, 路由器生命周期:600 秒
ra-param=br1,10,600
# DHCPv6 地址池:通过 br1 的前缀构造
# DHCPv6 前缀长度:64, 模式:无状态 DHCPv6
# DHCPv6 租约时间:600 秒 (10 分钟)
dhcp-range=br1,::,constructor:br1,ra-stateless,64,600
# DHCPv6 DNS (option 23):从路由器继承
dhcp-option=br1,option6:23,[::]
EOF

重启 dnsmasq 以应用设置:

service restart_dnsmasq

执行 tail /tmp/syslog.log -n 50 验证 dnsmasq.conf.add 已被正确加载:

[email protected]:/# tail /tmp/syslog.log -n 50
# ... 省略多余输出 ...
Apr 14 20:49:22 rc_service: service 15995:notify_rc restart_dnsmasq
Apr 14 20:49:22 dnsmasq[1149]: exiting on receipt of SIGTERM
Apr 14 20:49:22 custom_config: Appending content of /jffs/configs/dnsmasq.conf.add.
Apr 14 20:49:22 dnsmasq[15998]: started, version 2.81rc4-33-g7558f2b cachesize 1500
Apr 14 20:49:22 dnsmasq[15998]: asynchronous logging enabled, queue limit is 5 messages
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCP, IP range 192.168.150.2 -- 192.168.150.254, lease time 1d
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCP, IP range 192.168.50.2 -- 192.168.50.254, lease time 1d
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCPv6 stateless on br0
Apr 15 18:09:01 dnsmasq-dhcp[18800]: router advertisement on br0
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCPv6 stateless on 2600:6c51:fake:n00b::, constructed for br0
Apr 15 18:09:01 dnsmasq-dhcp[18800]: router advertisement on 2600:6c51:fake:n00b::, constructed for br0
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCPv6 stateless on br1
Apr 15 18:09:01 dnsmasq-dhcp[18800]: router advertisement on br1
Apr 15 18:09:01 dnsmasq-dhcp[18800]: DHCPv6 stateless on 2600:6c51:fake:n00b::, constructed for br1
Apr 15 18:09:01 dnsmasq-dhcp[18800]: router advertisement on 2600:6c51:fake:n00c::, constructed for br1
Apr 15 18:09:01 dnsmasq-dhcp[18800]: IPv6 router advertisement enabled
# ... 省略多余输出 ...

使用 DHCPv6-PD

如果需要在二级路由器上启用 DHCPv6-PD,则应该使用 6relayd 作为 DHCPv6-PD 服务器。

注意:如果使用 6relayd,不要在 /jffs/configs/dnsmasq.conf.add 中添加 DHCPv6 相关的设置。

6relayd8 的服务器模式下,它会通过 DHCPv6-PD 把第二个 /64 前缀分配给二级路由器。所以,只需要为 br1 分配一个 /63 的前缀。

这里使用的是 br0 的邻接 /63 前缀:

# 为 br1 设置 br0 的邻接 /63 前缀
# 把 2600:6c51:fake:n00c::1 换成你的
ip -6 addr add 2600:6c51:fake:n00c::1/63 dev br1

然后,执行 6relayd -v -d -S . br1 启动 DHCPv6-PD 服务器:

# 监听于 br1 上的 DHCPv6-PD 服务器
6relayd -v -d -S . br1

如果收到 DHCPv6 请求的话,系统日志 (cat /tmp/syslog.log | grep 6relayd) 应该显示:

# [email protected]:/# cat /tmp/syslog.log | grep 6relayd
Apr 15 18:20:00 6relayd[1765]: Got DHCPv6 request
Apr 15 18:20:01 6relayd[1765]: Got DHCPv6 request

最后,在二级路由器上检查 IPv6 设置(系统记录 -> IPv6):

IPv6 Connection Type Native with DHCP-PD
WAN IPv6 Address 2600:6c51:fake:n00c::fake
WAN IPv6 Gateway fe80::aa5e:45ff:fe00:fake
LAN IPv6 Address 2600:6c51:fake:n00d::1/64
LAN IPv6 Link-Local Address fe80::a62:66ff:fe01:fake/64
DHCP-PD Enabled
LAN IPv6 Prefix 2600:6c51:fake:n00d::/64
DNS Address 2600:6c51:fake:n00c::1

如果一切正常,LAN IPv6 Prefix 应该显示 2600:6c51:fake:n00d::/64

提示: 自动配置 6relayd 的脚本可以在 脚本与配置文件:/jffs/scripts/dhcpc-event 中找到。

脚本与配置文件

把下文中的脚本与配置文件上传到路由器对应的目录里,即可在路由器每次重启时自动配置 VLAN。

注意:系统管理 -> 系统设置 中的 Enable JFFS custom scripts and configs 需要设为

/jffs/scripts/services-start

#!/bin/sh

# Make sure the script is indeed invoked
touch /tmp/000-services-start

# Physical port to interface map:
# eth0   WAN
# eth1   LAN 4
# eth2   LAN 3
# eth3   LAN 2
# eth4   LAN 1
# eth5   Bridge of LAN 5, LAN 6, LAN 7, LAN 8
# eth6   2.4 GHz Radio
# eth7   5 GHz Radio

# Delete those interfaces that we want to isolate from br0
logger -t "isolate_port" "services-start: deleting LAN 2 (eth3) from br0"
brctl delif br0 eth3

# Create a new bridge br1 for isolated interfaces
logger -t "isolate_port" "services-start: creating br1 with LAN 2 (eth3)"
brctl addbr br1
brctl stp br1 on # STP to prevent bridge loops
brctl addif br1 eth3

# Set up the IPv4 address for br1
# Here we set the subnet to be 192.168.150.0/24
# IPv6 link local address will be assigned automatically
logger -t "isolate_port" "services-start: setting up IPv4 address for br1"
ifconfig br1 192.168.150.1 netmask 255.255.255.0
ifconfig br1 allmulti up

logger -t "isolate_port" "services-start: all done"
date >> /tmp/000-services-start

/jffs/scripts/firewall-start

#!/bin/sh

# Make sure the script is indeed invoked
touch /tmp/000-firewall-start
logger -t "isolate_port" "firewall-start: applying INPUT rules for br1"

# Allow new incoming connections from br1
iptables -I INPUT -i br1 -m state --state NEW -j ACCEPT
ip6tables -I INPUT -i br1 -j ACCEPT # Same rule as br0 by default
ip6tables -I INPUT -i br1 -m state --state NEW -j ACCEPT

# Only forbid br1 access the web UI and SSH of the main router
iptables -I INPUT -i br1 -p tcp --dport 80 -j DROP
iptables -I INPUT -i br1 -p tcp --dport 22 -j DROP
ip6tables -I INPUT -i br1 -p tcp --dport 80 -j DROP
ip6tables -I INPUT -i br1 -p tcp --dport 22 -j DROP

logger -t "isolate_port" "firewall-start: applying FORWARD rules for br1"

# Forbid packets from br1 to be forwarded to other interfaces
iptables -I FORWARD -i br1 -j DROP
ip6tables -I FORWARD -i br1 -j DROP

# But allow packet forwarding inside br1
iptables -I FORWARD -i br1 -o br1 -j ACCEPT
ip6tables -I FORWARD -i br1 -o br1 -j ACCEPT

# Allow packet forwarding between br1 and eth0 (WAN)
iptables -I FORWARD -i br1 -o eth0 -j ACCEPT
ip6tables -I FORWARD -i br1 -o eth0 -j ACCEPT

# Allow one-way traffic from br0 to br1
iptables -I FORWARD -i br0 -o br1 -j ACCEPT
iptables -I FORWARD -i br1 -o br0 -m state \
  --state RELATED,ESTABLISHED -j ACCEPT
ip6tables -I FORWARD -i br0 -o br1 -j ACCEPT
ip6tables -I FORWARD -i br1 -o br0 -m state \
  --state RELATED,ESTABLISHED -j ACCEPT

logger -t "isolate_port" "firewall-start: all done"
date >> /tmp/000-firewall-start

/jffs/scripts/nat-start

#!/bin/sh

# Make sure the script is indeed invoked
touch /tmp/000-nat-start
logger -t "isolate_port" "nat-start: applying POSTROUTING rules for br1"

# NAT inside 192.168.150.0/24 on br1
iptables -t nat -A POSTROUTING -s 192.168.150.0/24 -d 192.168.150.0/24 \
  -o br1 -j MASQUERADE

logger -t "isolate_port" "nat-start: all done"
date >> /tmp/000-nat-start

/jffs/scripts/wan-event

该脚本用于 IPv6。

#!/bin/sh

PD_PREFIX=56 # Change to which works for your ISP
RETRY=10

# Only run for connected event (the old wan-start)
if [ ! "$2" = "connected" ]; then
  exit 0
fi

# Make sure the script is indeed invoked
touch /tmp/000-wan-event-connected

# ipv6_service = dhcp6 means Connection Type: Native, 
# also we require DHCP PD to be enabled
if [ "$(nvram get ipv6_service)" = "dhcp6" ] && \
   [ "$(nvram get ipv6_dhcp_pd)" = "1" ]; then

  # Try to find odhcp6c
  i=0;
  while [ $i -lt $RETRY ]; do
    # Get odhcp6c's pid. If there are multiple instances (unlikely), 
    # we use the smallest one.
    PID=$(pidof odhcp6c | tr ' ' '\n' | head -n 1)
    CMDLINE=/proc/$PID/cmdline

    # Found odhcp6c?
    if [ ! -z "$PID" ] && [ -f "$CMDLINE" ]; then
      COMMAND="$(tr '\0' ' ' < /proc/$PID/cmdline)"
      logger -t "isolate_port" "wan-event[connected]:" \
        "found odhcp6c, PID: $PID, command: $COMMAND"

      # The first 11 chars should be "odhcp6c -df"
      PREFIX=$(echo $COMMAND | cut -c1-11)

      if [ "$PREFIX" = "odhcp6c -df" ]; then
        # There is a space between "odhcp6c -df" and remaining arguments.
        # So we start from the 13rd char.
        ARGS=$(echo $COMMAND | cut -c13-)

        # Check if arguments start with -P$PD_PREFIX
        ARG1=$(echo $ARGS | cut -c1-$(expr length "$PD_PREFIX" + 2))
        if [ ! "$ARG1" = "-P$PD_PREFIX" ]; then
          # Prefix length (-P$PD_PREFIX)
          COMMAND="odhcp6c -df -P$PD_PREFIX $ARGS"
          logger -t "isolate_port" "wan-event[connected]:" \
            "re-run odhcp6c with prefix hint $PD_PREFIX: $COMMAND"

          killall odhcp6c
          eval $COMMAND
        else
          logger -t "isolate_port" "wan-event[connected]:" \
            "odhcp6c already started with prefix hint $PD_PREFIX"
        fi
      else
        logger -t "isolate_port" "wan-event[connected]:" \
          "odhcp6c command prefix mismatch!" \
          "found '$PREFIX', expects 'odhcp6c -df'"
      fi

      # We break from here once found the `odhcp6c` process
      break
    else
      i=$(($i+1))
      logger -t "isolate_port" "wan-event[connected]:" \
        "odhcp6c not found ($i/$RETRY)"

      # Hope we're lucky next time
      sleep 1
    fi
  done
else
  logger -t "isolate_port" "wan-event[connected]: DHCPv6 PD not enabled"
fi

logger -t "isolate_port" "wan-event[connected]: all done"
date >> /tmp/000-wan-event-connected

/jffs/scripts/dhcpc-event

该脚本用于 IPv6。

#!/bin/sh

link_local_ipv6() {
  # Get IPv6 link local address from br0 (with the prefix length part)
  echo $(ip -6 addr show br0 scope link |\
    sed -e's/^.*inet6 \([^ ]*\).*$/\1/;t;d')
}

setup_br1() {
  # ipv6_service = dhcp6 means Connection Type: Native, 
  # also we require DHCP PD to be enabled
  if [ "$(nvram get ipv6_service)" = "dhcp6" ] && \
     [ "$(nvram get ipv6_dhcp_pd)" = "1" ]; then
    
    # Wait for udhcpc (udhcpc.c: bound6 -> add_ip6_lanaddr). 
    # We need to the updated ipv6 prefix from nvram.
    sleep 2

    # Allocated IPv6 prefix from ISP
    PREFIX=$(nvram get ipv6_prefix)

    if [ -z "$PREFIX" ]; then
      logger -t "isolate_port" "dhcpc-event: empty IPv6 prefix"
    else
      PD_PREFIX=$(nvram get ipv6_prefix_length)
      if [ "$PD_PREFIX" -ge 63 ]; then
        # We need at least 3 /64 subnets, /63 only gives 2 /64 subnet
        logger -t "isolate_port" "dhcpc-event: IPv6 prefix" \
          "(/$PD_PREFIX) is too long, at least /62 requried"
      else
        if [ "$PD_PREFIX" -le 48 ]; then
          # Shorter than /49? 
          # br1 will on a:b:c:2::/63 (br0 is on a:b:c:0::/64)
          BR1_PREFIX="$(echo $PREFIX | cut -d':' -f1-3):2::"
        else
          # /49 to /62? 
          # br1 will on a:b:c:dddf::/63 (br0 is on a:b:c:dddd::/64)
          BR0_NET_ID=$(echo $PREFIX | cut -d':' -f4)
          BR1_NET_ID=$(printf "%x" $((0x$BR0_NET_ID + 0x2)))
          BR1_PREFIX="$(echo $PREFIX | cut -d':' -f1-3):${BR1_NET_ID}::"
        fi

        # Clean up br0
        ip -6 route flush dev br0
        ip -6 addr flush dev br0 scope global

        # Assign br0 with the first /64 subnet (instead the /56 one)
        # ipv6_rtr_addr is the default router's IPv6 address 
        logger -t "isolate_port" "dhcpc-event: set prefix" \
          "$(nvram get ipv6_rtr_addr)/64 to br0"
        ip -6 addr add "$(nvram get ipv6_rtr_addr)/64" dev br0

        # Clean up br1 (note we also remove link local address)
        ip -6 route flush dev br1
        ip -6 addr flush dev br1

        # Re-add link local address
        # So route to fe80::/64 will be added back
        # Note this is VERY important for 6relayd
        # Otherwise 6relayd will throw network unreachable error
        # Because route to fe80::/64 doesn't exist
        ip -6 addr add $(link_local_ipv6) dev br1

        # Assign br1 with a /63 subnet, so the cascading router 
        # will be on the first /64 subnet and it will also get
        # the second /64 prefix via DHCPv6-PD by 6relayd
        logger -t "isolate_port" "dhcpc-event: set prefix" \
          "${BR1_PREFIX}1/63 to br1"
        ip -6 addr add "${BR1_PREFIX}1/63" dev br1

        # Re-run 6relayd
        logger -t "isolate_port" "dhcpc-event: re-run 6relayd on br1"
        killall 6relayd

        # Automatic DHCPv6 server to delegate prefix on br1 in daemon
        6relayd -v -d -S . br1
      fi
    fi
  fi
}

teardown_br1() {
  # ipv6_service = dhcp6 means Connection Type: Native,
  # also we require DHCP PD to be enabled
  if [ "$(nvram get ipv6_service)" = "dhcp6" ] && \
     [ "$(nvram get ipv6_dhcp_pd)" = "1" ]; then
     
    logger -t "isolate_port" "dhcpc-event:" \
      "flush IPv6 route and IPv6 address on br1"

    # br0 will be handled by udhcpc, 
    # we only need to care about br1
    # Note we also remove link local address
    ip -6 route flush dev br1
    ip -6 addr flush dev br1

    # Re-add link local address
    # So route to fe80::/64 will be added back
    # Note this is VERY important for 6relayd
    # Otherwise 6relayd will throw network unreachable error
    # Because route to fe80::/64 doesn't exist
    ip -6 addr add $(link_local_ipv6) dev br1
  fi
}

# Make sure the script is indeed invoked
touch /tmp/000-dhcpc-event

# Adapted from odhcp6c-example-script.sh
(
  flock 9
  case "$1" in
    bound)
      teardown_br1
      setup_br1
    ;;
    informed|updated|rebound|ra-updated)
      setup_br1
    ;;
    stopped|unbound)
      teardown_br1
    ;;
    started)
      teardown_br1
    ;;
  esac
) 9>/tmp/odhcp6c.lock.br1
rm -f /tmp/odhcp6c.lock.br1

date >> /tmp/000-dhcpc-event

/jffs/configs/dnsmasq.conf.add

不需要 DHCPv6:

interface=br1
# DHCPv4 range: 192.168.150.2 - 192.168.150.254, netmask: 255.255.255.0
# DHCPv4 lease time: 86400s (1 day)
dhcp-range=br1,192.168.150.2,192.168.150.254,255.255.255.0,86400s
# DHCPv4 router (option 3): 192.168.150.1
dhcp-option=br1,3,192.168.150.1

需要 DHCPv6,并且需要为二级路由器启用 DHCPv6-PD:

interface=br1
# DHCPv4 range: 192.168.150.2 - 192.168.150.254, netmask: 255.255.255.0
# DHCPv4 lease time: 86400s (1 day)
dhcp-range=br1,192.168.150.2,192.168.150.254,255.255.255.0,86400s
# DHCPv4 router (option 3): 192.168.150.1
dhcp-option=br1,3,192.168.150.1

(你没看错,和不用 DHCPv6 是一样的)

需要 DHCPv6,但是不需要 DHCPv6-PD:

interface=br1
# DHCPv4 range: 192.168.150.2 - 192.168.150.254, netmask: 255.255.255.0
# DHCPv4 lease time: 86400s (1 day)
dhcp-range=br1,192.168.150.2,192.168.150.254,255.255.255.0,86400s
# DHCPv4 router (option 3): 192.168.150.1
dhcp-option=br1,3,192.168.150.1
# DHCPv6 RA interval: 10s, router lifetime: 600s
ra-param=br1,10,600
# DHCPv6 range: whole subnet, constructing from br1's prefix
# DHCPv6 prefix length: 64, mode: Stateless DHCPv6
# DHCPv6 lease time: 600s (10 minutes)
dhcp-range=br1,::,constructor:br1,ra-stateless,64,600
# DHCPv6 DNS (option 23): inherit from the router
dhcp-option=br1,option6:23,[::]

参考文献