如果你看过我的前两篇文章,大概已经知道我和小米这台路由器的爱恨情仇了,在这里说一声抱歉,前面两文的方法或多或少都显得很臃肿,接下来的文章,我将讲述我最后的解决方案。
前言
给没看过前两篇文章的读者介绍下我的网络拓扑:
运营商
│
│ PPPoE + DHCPv6-PD /60
▼
小米 AX6000 原厂固件
│
│ DHCPv6-PD
▼
二级 OpenWrt
│
▼
下级 LAN
运营商实际上会给 AX6000 下发一个 /60 IPv6 Prefix,例如:
240e:391:1aa2:1630::/60
理论上,这个 /60 完全可以继续切出多个 /64,分配给下一级 OpenWrt。
但小米原厂固件存在一个问题:
即使手工将
network.lan.ip6assign改为60,在 PPPoE 重拨或 IPv6 Native 模式重新初始化后,它仍会自动变回64。
结果就是一级路由自己占用一个 /64 后,odhcpd 无法正常向二级 OpenWrt 委派新的 /64,二级 OpenWrt 的:
ifstatus wan6
会出现:
"ipv6-prefix": []
最初可以通过改 UCI、重启 network、重启 odhcpd 等方式临时恢复,但这些都是事后补救,而且容易和小米自己的网络状态机打架。
最后分析原厂脚本后发现,这个问题其实可以从源头修掉。
一、根因:小米自己把 Native IPv6 的 ip6assign 默认写成了 64
AX6000 的 IPv6 管理主要涉及:
/etc/init.d/ipv6
/usr/sbin/enable_ipv6
这两个文件并不是二进制,而是普通 POSIX Shell 脚本。实机检查可以确认这一点。
问题首先出在 /etc/init.d/ipv6。
Native IPv6 模式启动时,它执行的是:
$CMD wan $1
也就是类似:
/usr/sbin/enable_ipv6 wan native
这里没有传入 ip6assign 参数;相比之下,Static 模式会明确取得 ipv6.settings.ip6assign 并传给 enable_ipv6。
再看 /usr/sbin/enable_ipv6:
ip6assign=$6
Native6 分支中存在:
"native")
[ -z "$ip6assign" ] && {
logerr "WARN: [Native6-mode] ip6assign is NULL, set 64 as default."
ip6assign="64"
}
随后 PPPoE 分支直接执行:
set network.${iface}.ipv6='auto'
set network.lan.ip6assign=${ip6assign}
commit network
也就是说调用链实际上是:
PPPoE / IPv6 Native 重新初始化
↓
/etc/init.d/ipv6
↓
enable_ipv6 wan native
↓
第 6 个参数为空
↓
ip6assign="64"
↓
uci 写入 network.lan.ip6assign=64
这就是为什么手工设置的 60 会在重拨以后会恢复成 64。
有意思的是,OpenWrt 底层的 /bin/config_generate 本身却是:
uci set network.$1.ip6assign='60'
所以真正把值覆盖回 /64 的,是小米后来增加的 Native IPv6 管理逻辑。
二、为什么不用直接改 /usr/sbin/enable_ipv6
这台 AX6000 的根文件系统是只读 SquashFS:
mtd:ubi_rootfs on / type squashfs (ro,noatime)
并且:
df -h /
得到:
Filesystem Size Used Available Use%
mtd:ubi_rootfs 21.8M 21.8M 0 100%
因此在不做刷机之类的操作下,应该不能像标准带 overlay 的 OpenWrt 一样直接修改 /usr/sbin/enable_ipv6。
但 /data 是持久、可写的 UBIFS。
所以最终方案是:
只读 SquashFS
/usr/sbin/enable_ipv6
▲
│ bind mount
│
/data/enable_ipv6.patched
可写、持久
Linux 的 bind mount 不会修改原厂 SquashFS 文件,只是运行时让:
/usr/sbin/enable_ipv6
指向 /data 中的修改版。
三、制作修改版 enable_ipv6
以下操作均需要 AX6000 已取得 SSH/root 权限。
首先保存原厂版本,并创建修改版:
cp /usr/sbin/enable_ipv6 /data/enable_ipv6.original
cp /usr/sbin/enable_ipv6 /data/enable_ipv6.patched
chmod 755 /data/enable_ipv6.original
chmod 755 /data/enable_ipv6.patched
只修改 Native6 分支:
awk '
BEGIN { in_native=0 }
$0 ~ /^"native"\)/ {
in_native=1
}
in_native {
if ($0 ~ /WARN: \[Native6-mode\] ip6assign is NULL, set 64 as default\./)
sub(/set 64 as default\./, "set 60 as default.")
if ($0 ~ /ip6assign="64"/)
sub(/ip6assign="64"/, "ip6assign=\"60\"")
}
{
print
}
in_native && $0 ~ /^[[:space:]]*;;[[:space:]]*$/ {
in_native=0
}
' /usr/sbin/enable_ipv6 > /data/enable_ipv6.patched.tmp
mv /data/enable_ipv6.patched.tmp /data/enable_ipv6.patched
chmod 755 /data/enable_ipv6.patched
检查:
grep -n 'ip6assign="6[04]"' /data/enable_ipv6.patched
我的 AX6000 上结果为:
82: ip6assign="64"
119: ip6assign="60"
158: ip6assign="64"
再做一次 Shell 语法检查:
sh -n /data/enable_ipv6.patched
没有输出即正常。
查看 Native6:
grep -n -A 5 -B 1 '^"native")' /data/enable_ipv6.patched
应当看到:
"native")
[ -z "$ip6assign" ] && {
logerr "WARN: [Native6-mode] ip6assign is NULL, set 60 as default."
ip6assign="60"
}
四、临时 bind 测试
执行:
mount -o bind /data/enable_ipv6.patched /usr/sbin/enable_ipv6
确认:
grep -n -A 5 -B 1 '^"native")' /usr/sbin/enable_ipv6
此时应该已经看到:
ip6assign="60"
然后正常重拨 PPPoE / 重新初始化 Native IPv6,验证小米自己的流程能否自动产生正确配置。
重拨完成后:
uci get network.lan.ip6assign
应为:
60
日志也能形成很直观的前后对照:
原厂:
WARN: [Native6-mode] ip6assign is NULL, set 64 as default.
修改后:
WARN: [Native6-mode] ip6assign is NULL, set 60 as default.
五、验证上游 /60 和下游 /64
一级 AX6000:
ubus call network.interface.wan_6 status
我的实测结果包含:
"ipv6-prefix": [
{
"address": "240e:391:1aa2:1630::",
"mask": 60,
"class": "wan_6",
"assigned": {
"lan": {
"address": "240e:391:1aa2:1630::",
"mask": 60
}
}
}
]
这说明:
ISP → AX6000:/60
AX6000 LAN assignment:/60
再看二级 OpenWrt:
ifstatus wan6
最终实测成功取得:
"ipv6-prefix": [
{
"address": "240e:391:1aa2:1631::",
"mask": 64,
"class": "wan6",
"assigned": {
"lan": {
"address": "240e:391:1aa2:1631::",
"mask": 64
}
}
}
]
最终拓扑变成:
ISP
│
└─ 240e:391:1aa2:1630::/60
│
▼
Xiaomi AX6000
│
├─ 上级 LAN:1630::/64
│
└─ DHCPv6-PD
│
▼
二级 OpenWrt
│
└─ 1631::/64
至此无需手工重启 odhcpd,PD 本身即可正常建立。
六、持久化 bind mount
临时测试成功以后,只需要开机执行一次 bind mount。
创建:
cat > /data/apply-enable-ipv6-bind.sh <<'EOF'
#!/bin/sh
SRC="/data/enable_ipv6.patched"
DST="/usr/sbin/enable_ipv6"
TAG="enable-ipv6-bind"
fail() {
logger -t "$TAG" "ERROR: $*"
exit 1
}
[ -f "$SRC" ] || fail "$SRC not found"
sh -n "$SRC" || fail "syntax check failed"
grep -q 'WARN: \[Native6-mode\] ip6assign is NULL, set 60 as default\.' "$SRC" \
|| fail "Native6 /60 patch marker not found"
COUNT="$(grep -c 'ip6assign="60"' "$SRC")"
[ "$COUNT" = "1" ] || fail "unexpected ip6assign=60 count: $COUNT"
if grep -qs " $DST " /proc/mounts; then
logger -t "$TAG" "already mounted"
exit 0
fi
mount -o bind "$SRC" "$DST" || fail "bind mount failed"
if ! grep -q 'WARN: \[Native6-mode\] ip6assign is NULL, set 60 as default\.' "$DST"; then
umount "$DST" 2>/dev/null
fail "post-mount verification failed; rolled back"
fi
logger -t "$TAG" "patch active: $SRC -> $DST"
exit 0
EOF
chmod 700 /data/apply-enable-ipv6-bind.sh
语法检查:
sh -n /data/apply-enable-ipv6-bind.sh
然后利用firewall 的 script include 机制启动:
uci -q delete firewall.enable_ipv6_bind
uci set firewall.enable_ipv6_bind='include'
uci set firewall.enable_ipv6_bind.type='script'
uci set firewall.enable_ipv6_bind.path='/data/apply-enable-ipv6-bind.sh'
uci set firewall.enable_ipv6_bind.reload='1'
uci set firewall.enable_ipv6_bind.enabled='1'
uci commit firewall
检查:
uci show firewall.enable_ipv6_bind
应该类似:
firewall.enable_ipv6_bind=include
firewall.enable_ipv6_bind.type='script'
firewall.enable_ipv6_bind.path='/data/apply-enable-ipv6-bind.sh'
firewall.enable_ipv6_bind.reload='1'
firewall.enable_ipv6_bind.enabled='1'
七、验证持久化机制
不必立即重启路由器,可以先卸载 bind:
umount /usr/sbin/enable_ipv6
检查:
grep -n -A 4 '^"native")' /usr/sbin/enable_ipv6
应该恢复原厂:
ip6assign="64"
然后:
/etc/init.d/firewall reload
输出中应该出现:
* Running script '/data/apply-enable-ipv6-bind.sh'
日志:
grep 'enable-ipv6-bind' /tmp/messages | tail
应出现:
enable-ipv6-bind: patch active: /data/enable_ipv6.patched -> /usr/sbin/enable_ipv6
重新检查:
grep -n -A 4 '^"native")' /usr/sbin/enable_ipv6
应再次变成:
ip6assign="60"
这证明持久化调用链本身已经成立。
八、启动顺序
还需要确认一件事:bind 必须在 Xiaomi 初始化 IPv6 之前完成。
AX6000 1.0.122 实测:
grep -H '^START=' /etc/init.d/firewall /etc/init.d/ipv6
结果:
/etc/init.d/firewall:START=19
/etc/init.d/ipv6:START=58
对应:
/etc/rc.d/S19firewall
/etc/rc.d/S58ipv6
因此启动流程正好是:
系统启动
↓
S19 firewall
↓
运行 /data/apply-enable-ipv6-bind.sh
↓
bind patched enable_ipv6
↓
S58 ipv6
↓
Xiaomi Native IPv6 初始化
↓
ip6assign=60
时序正确。
九、碎碎念。。
折腾到最后,真正长期工作的东西实际上只有两个文件:
/data/enable_ipv6.patched
/data/apply-enable-ipv6-bind.sh
另外建议保留一份以便回滚吧:
/data/enable_ipv6.original
没有后台 daemon,没有 cron,没有 hotplug,没有不断检测 WAN6 状态,也没有自动执行 network restart 或 odhcpd restart,鬼知道我之前到底在干什么。。。不过也确实学到了很多新东西吧。(汗
写在最后
有时候,少即是多。其实什么都不用运行——只是在系统启动时,把一个修正过的 8 KB Shell 脚本盖到原厂脚本上,然后让小米自己继续正常工作,仅此而已。
留言 / CONVERSATION
欢迎交流,评论将在审核后展示,你的邮箱不会被公开。
正在准备评论区…