这是《OpenWrt IPv6 中继间歇断流排查》的后续。

上一篇解决的是 IPv6 relay 架构中的一个具体故障:Apple TV 发出的 Router Lifetime=0 RA 经 odhcpd 中继后换成了 OpenWrt 的身份,最终让下游客户端撤销真正的 IPv6 默认路由。精确过滤能够止血,但 relay 仍然是一个不得已的结构。

后来我们发现,运营商其实已经给了主路由一个完整的 /60。真正阻止二级 OpenWrt 获得独立 /64 的,不是运营商,也不是二级 OpenWrt,而是小米 AX6000 原厂配置中的这一项:

network.lan.ip6assign='64'

把它改为 60 后,小米原本已经存在的 odhcpd 就能够继续向下游委派 /64。网络也由“把同一个 /64 勉强中继到二级 LAN”,变成了标准的分层路由式 IPv6。

不过事情没有到此结束:小米会在部分重拨流程里把该值重新写回 64;即使脚本修回 60 并重启网络,odhcpd 的下游 PD 池也可能没有同步重建。最终方案因此包含两个动作:恢复 ip6assign=60,并在网络稳定后重启 odhcpd。

本文只讨论 IPv6 前缀委派、RA 和路由结构,不涉及任何代理软件或分流策略。文中的公网 IPv6 均使用文档地址替换,设备 MAC、链路本地地址和账户信息也已隐去。

省流

完整结论如下:

运营商通过 DHCPv6-PD 给小米 AX6000 一个 /60
                    ↓
小米的 network.lan.ip6assign 却是 64
                    ↓
主 LAN 只获得一个 /64,无法再从该 assignment 中分出下游 PD
                    ↓
二级 OpenWrt 请求 IA_PD /64
                    ↓
小米回复空 IA_PD 或 NoPrefixAvail
                    ↓
二级 OpenWrt 只能退回 RA/DHCPv6/NDP relay

修复后:

运营商 /60
   ↓
小米 LAN assignment /60
   ├── 第一个 /64:小米自己的 LAN
   └── 其余空间:odhcpd 向下游路由器委派 /64
                         ↓
                   二级 OpenWrt
                         ↓
                   独立 LAN /64

一、两种架构:没有 PD 与拿到 PD 以后

1. 没有 PD 时:只能把上游 /64 中继下来

最初的网络是:

光猫桥接
   ↓
小米 AX6000 PPPoE 拨号
   ↓  同一个上游 LAN /64
二级 OpenWrt:RA relay + DHCPv6 relay + NDP relay
   ↓
客户端

这种结构能让二级 LAN 获得公网 IPv6,却没有真正的子网边界。二级 OpenWrt 没有属于自己的 delegated prefix,只能在两个 routed、non-bridged 接口之间转发 RA、DHCPv6 和 NDP(邻居发现协议)。

OpenWrt 的 odhcpd 本来就提供 relay 功能,官方也明确说明它用于“没有 delegated prefix 时”的场景。因此 relay 不是错误配置,但它有天然代价:

  • 上游和下游共享同一个 /64;
  • 二级路由器必须代理邻居发现;
  • 上游 LAN 中的 RA 可能被带到下游;
  • 前缀变化、睡眠唤醒、默认路由和地址生命周期都更依赖控制报文被正确中继;
  • 某些报文经过 relay 后无法保留原始链路身份。

上一篇IPV6故障排查中 Apple TV RA 所带来的故障就是这种架构复杂度的具体表现。

2. 有 PD 后:每层路由器拥有自己的前缀

改造后的结构是:

光猫桥接
   ↓
运营商 DHCPv6-PD /60
   ↓
小米 AX6000
   ├── 主 LAN:2001:db8:1234:abc0::/64
   └── 下发给 OpenWrt:2001:db8:1234:abc1::/64
                              ↓
                       OpenWrt RA Server
                              ↓
                           客户端

小米和二级 OpenWrt 的 LAN 不再共享同一个 /64。二级 OpenWrt 是真正的下游路由器,客户端默认路由只由它自己的 RA Server 宣告,不再需要把主路由 LAN 上其他设备的 RA 中继进来。

这一步从架构上消除了此前 Apple TV RA 经 relay 改换身份的条件,也让 RA/DHCPv6/NDP relay 和为它添加的临时 RA Guard 可以退出运行态。

二、先证明:运营商到底给了 /60 还是只给了 /64

声明:以下所有操作均建立在已获取并固化小米路由器 SSH 权限的前提下。文中涉及的命令、配置修改及系统操作,默认读者已经具备基本的 Linux / OpenWrt / 网络配置知识,并能够自行判断操作风险、排查异常以及完成必要的备份与恢复。需要特别说明的是,小米路由器默认并不面向普通用户开放 SSH。自行获取 SSH 权限、修改系统文件、持久化启动项或更改原厂配置,均可能带来设备异常、无法启动、配置丢失等风险,并且可能影响或失去官方保修、售后支持。因此,如果你准备按照本文进行操作,请务必确认自己已经理解相关原理,并对可能出现的后果具备基本处理能力。本文仅记录个人实践过程,不保证适用于所有机型、固件版本或网络环境;因照搬本文操作导致的设备故障、数据损失、网络中断或其他问题,需要由操作者自行承担。

不要妄图从小米的后台页面上的 LAN 地址推测运营商分配了多大前缀。一个接口显示 /64,并不代表 WAN 没拿到更大的 delegated prefix。

在小米 SSH 后台执行:

ubus call network.interface.wan_6 status

现场输出中最关键的部分经过匿名化后类似:

{
  "delegation": true,
  "ipv6-prefix": [
    {
      "address": "2001:db8:1234:abc0::",
      "mask": 60,
      "assigned": {
        "lan": {
          "address": "2001:db8:1234:abc0::",
          "mask": 64
        }
      }
    }
  ]
}

这段状态同时证明了两件事:

  1. 运营商确实向小米委派了 /60;
  2. 小米只从中给自己的 LAN assignment 取了一个 /64。

一个 /60 可以划分成 16 个 /64:末尾的子网 nibble 可以从 0 到 f。现场并不是“运营商扣死只给一个 /64”,而是主路由没有把已经获得的地址空间继续作为下游 PD 池使用。

检查配置:

uci get network.lan.ip6assign

原始值为:

64

这里的 ip6assign 不是“向运营商申请前缀”的选项,而是 OpenWrt/netifd 给下游接口分配多大前缀的参数。OpenWrt 官方 IPv6 文档明确说明:当 ip6assign 小于 64 时,DHCPv6 Server 才能把除第一个 /64 之外的空间继续通过 IA_PD 委派给下游路由器。

因此 /60 已经到达小米,问题卡在小米的 LAN assignment 层。

注意:uci show network 可能包含 PPPoE 用户名、密码或其他凭据。不要把未经脱敏的完整输出上传到论坛、聊天记录或博客。

三、下游抓包:OpenWrt 请求了,但小米并没有给

在二级 OpenWrt 的上联接口抓 DHCPv6:

tcpdump -ni eth1 -vvv -s0 'udp port 546 or udp port 547'

然后重新拉起 WAN6:

ifdown wan6
sleep 3
ifup wan6

修复前的关键过程是:

OpenWrt Solicit:请求 IA_PD ::/64
Xiaomi Advertise:返回 IA_PD 容器,但没有 IA_PD-prefix
或返回 NoPrefixAvail
后续 Request:无法带上可用的 delegated prefix

这组报文排除了两个常见误判:

  • 不是二级 OpenWrt 没有发起 PD 请求;
  • 不是仅仅少配了客户端侧的 reqprefix。

结合小米 wan_6 中明确存在的 /60,故障层可以定位为:

小米已经从 ISP 获得前缀池,但 LAN assignment 为 /64,odhcpd 没有可用于下游 IA_PD 的剩余空间。

四、先做一次最小化验证

1. 备份

修改前至少备份 UCI 配置。/tmp 只适合短期查看,想跨重启保留应放在持久目录或下载到本地。不同小米固件的持久分区布局可能不同。先确认 /data 在自己的型号和固件上跨重启保留,再照搬本文的持久化部分。

2. 在小米上把 LAN assignment 改为 /60

uci set network.lan.ip6assign='60'
uci commit network
/etc/init.d/network restart

这会中断家庭网络一小段时间。

3. 让二级 OpenWrt 请求 /64

二级 OpenWrt 的 WAN6 至少应满足:

uci set network.wan6.proto='dhcpv6'
uci set network.wan6.reqaddress='try'
uci set network.wan6.reqprefix='64'
uci set network.wan6.delegate='1'
uci commit network

ifdown wan6
sleep 3
ifup wan6

再次查看:

ubus call network.interface.wan6 status

修复后第一次成功状态中出现了:

"ipv6-prefix": [
  {
    "address": "2001:db8:1234:abc1::",
    "mask": 64,
    "preferred": 43196,
    "valid": 43196,
    "class": "wan6"
  }
]

而小米自己的 LAN 使用的是另一个 /64:

Xiaomi LAN: 2001:db8:1234:abc0::/64
OpenWrt PD: 2001:db8:1234:abc1::/64

这就是决定性证据:二级 OpenWrt 得到的是带租期的 DHCPv6-PD。

五、把二级 OpenWrt 从 relay 改成标准 server 架构

确认 WAN6 已经稳定获得 /64 后,再退出 relay。建议在 LuCI 中逐项核对,而不是盲删未知名称的 UCI section。

最终状态应为:

位置 配置 目标状态
OpenWrt WAN6 协议 DHCPv6 Client
OpenWrt WAN6 请求 IPv6 地址 try
OpenWrt WAN6 请求 IPv6 前缀 /64
OpenWrt WAN6 使用内置 IPv6 管理 启用
OpenWrt LAN IPv6 assignment length /64
OpenWrt LAN RA 服务 Server
OpenWrt LAN DHCPv6 服务 Server
OpenWrt LAN NDP Proxy/Relay Disabled
旧 WAN6 relay/master section RA/DHCPv6/NDP relay Disabled

对应 LAN 的核心 UCI 可整理为:

uci set network.lan.ip6assign='64'
uci set dhcp.lan.ra='server'
uci set dhcp.lan.dhcpv6='server'
uci set dhcp.lan.ndp='disabled'
uci -q delete dhcp.lan.master
uci commit network
uci commit dhcp
/etc/init.d/network restart
/etc/init.d/odhcpd restart

如果原来为 WAN6 单独建立过 relay master section,应在 LuCI 中将它停用或改回普通上联配置。section 名称会因固件和历史配置不同而变化,不应直接照抄删除命令。

完成后检查:

ubus call network.interface.wan6 status
ubus call network.interface.lan status
ip -6 addr show br-lan
ip -6 route show

应看到:

  • WAN6 的 ipv6-prefix 中有一个 /64;
  • LAN 的 ipv6-prefix-assignment 使用同一个 /64;
  • br-lan 上有来自当前 PD 的地址;
  • 客户端默认路由指向二级 OpenWrt;
  • RA、DHCPv6 使用 server,NDP relay 已关闭。

六、第一处坑:重拨后小米会把 60 改回 64

单次修改成功后,我们先重启过小米,配置一度仍然存在。但在后续真实 PPPoE/IPv6 重拨中,原厂流程又把:

network.lan.ip6assign='60'

改回了:

network.lan.ip6assign='64'

与此同时,ubus call network.interface.wan_6 status 里运营商下发的 ipv6-prefix 仍是 /60。

因此可以严格区分:

  • /60 租约没有消失;
  • 小米原厂的网络重建流程覆盖了 LAN assignment;
  • 具体是哪一个 vendor 二进制或配置生成器写回 64,本文没有完成逆向,所以不把责任归到某个未经证明的进程。

如果能找到并修改写回源头,那是最优雅的根治。但在“不换主路由、不刷第三方固件、不逆向厂商二进制”的约束下,最现实的方法是在 wan_6 重建后自动校正。

七、第二处坑:只重启 network,PD 池未必恢复

最初的自愈脚本只做了:

发现 ip6assign=64
→ 改回 60
→ network restart

表面状态已经恢复:

ip6assign = 60
wan_6 prefix = /60
assigned.lan.mask = 60

但下游 OpenWrt 再次请求时,抓包仍然看到:

Solicit:IA_PD ::/64
Advertise:只有 IA_PD 容器,没有 IA_PD-prefix

在小米上额外执行:

/etc/init.d/odhcpd restart

随后同一抓包立刻变为:

Solicit
→ Advertise,包含 IA_PD-prefix 2001:db8:1234:abc1::/64
→ Request,携带该 /64
→ Reply,确认该 /64

这证明在本文这台 AX6000 和该版原厂固件中,network restart 后 odhcpd 的下游 PD 池可能没有可靠地同步重建;在 /60 assignment 稳定后重启 odhcpd,是自愈闭环不可缺少的一步。

八、最终使用的重拨自愈方案

1. 主修复脚本

保存在持久分区:

/data/fix-ipv6-pd.sh

整理后的脚本如下。锁文件用于防止 network restart 再次触发 hotplug 后递归执行:

#!/bin/sh

LOCK="/tmp/fix-ipv6-pd.lock"
TAG="fix-ipv6-pd"

[ -e "$LOCK" ] && exit 0

CURRENT="$(uci -q get network.lan.ip6assign)"
[ "$CURRENT" = "60" ] && exit 0

touch "$LOCK" || exit 1
cleanup() {
    rm -f "$LOCK"
}
trap cleanup EXIT INT TERM

logger -t "$TAG" "detected lan.ip6assign=$CURRENT, restoring to 60"

uci set network.lan.ip6assign='60'
uci commit network

logger -t "$TAG" "restarting network"
/etc/init.d/network restart

# 等待 WAN6 与 LAN 的 /60 assignment 稳定
sleep 15

logger -t "$TAG" "restarting odhcpd to rebuild downstream PD pool"
/etc/init.d/odhcpd restart
sleep 3

NEW="$(uci -q get network.lan.ip6assign)"
if [ "$NEW" = "60" ]; then
    logger -t "$TAG" "success: lan.ip6assign=60, odhcpd restarted"
else
    logger -t "$TAG" "failed: lan.ip6assign=$NEW"
fi

exit 0

权限建议限制为 root:

chmod 700 /data/fix-ipv6-pd.sh

2. wan_6 hotplug 触发器

持久模板:

/data/99-fix-ipv6-pd

内容:

#!/bin/sh

[ "$INTERFACE" = "wan_6" ] || exit 0

case "$ACTION" in
    ifup|ifupdate)
        ;;
    *)
        exit 0
        ;;
esac

(
    sleep 10
    /data/fix-ipv6-pd.sh
) &

exit 0
chmod 700 /data/99-fix-ipv6-pd

实际运行位置为:

/etc/hotplug.d/iface/99-fix-ipv6-pd

但这台小米的 /etc/hotplug.d 在重启后可能恢复,因此不能只把脚本放在这里。

3. 开机重新安装 hotplug

创建:

/data/install-ipv6-pd-fix.sh

内容:

#!/bin/sh

mkdir -p /etc/hotplug.d/iface
cp /data/99-fix-ipv6-pd /etc/hotplug.d/iface/99-fix-ipv6-pd
chmod 700 /etc/hotplug.d/iface/99-fix-ipv6-pd
logger -t ipv6-pd-fix "hotplug hook installed"

exit 0
chmod 700 /data/install-ipv6-pd-fix.sh

本文这台 AX6000 使用原厂 UCI firewall include 作为持久化入口:

uci set firewall.ipv6_pd_fix=include
uci set firewall.ipv6_pd_fix.type='script'
uci set firewall.ipv6_pd_fix.path='/data/install-ipv6-pd-fix.sh'
uci set firewall.ipv6_pd_fix.enabled='1'
uci commit firewall

/data/install-ipv6-pd-fix.sh

确认:

uci show firewall.ipv6_pd_fix
ls -l /etc/hotplug.d/iface/99-fix-ipv6-pd

这一做法有明确的设备/固件依赖。firewall include 会以 root 权限执行脚本,因此 /data 中这些文件必须只有 root 可写,SSH 也只能留在可信 LAN 内。其他小米型号或固件应先验证启动机制,不要把本文当成通用官方功能。

最终自愈时间线是:

PPPoE / WAN6 重建
        ↓
小米原厂流程可能先写回 ip6assign=64
        ↓
wan_6 ifup / ifupdate 触发 hotplug
        ↓ 等 10 秒
若当前不是 60,则写回 60 并 network restart
        ↓ 等 15 秒
重启 odhcpd,重建下游 PD 池
        ↓
二级 OpenWrt 再次获得 /64

九、如何做一次真正的最终验收

仅仅看到“现在能上 IPv6”不够。至少要验证一次整机重启和一次真实重拨。

1. 小米侧

uci get network.lan.ip6assign
ubus call network.interface.wan_6 status
ubus call network.interface.lan status
logread | grep -E 'fix-ipv6-pd|ipv6-pd-fix'

应满足:

network.lan.ip6assign = 60
WAN6 ipv6-prefix mask = 60
assigned.lan.mask = 60
日志出现 odhcpd restarted / success

2. 二级 OpenWrt 侧

ubus call network.interface.wan6 status
ubus call network.interface.lan status
ip -6 addr show br-lan
ip -6 route show

应满足:

WAN6 delegation = true
WAN6 ipv6-prefix mask = 64
LAN ipv6-prefix-assignment = 同一个 /64
RA = server
DHCPv6 = server
NDP relay = disabled

若 WAN6 的 ipv6-prefix 为空,重新抓 DHCPv6:

tcpdump -ni eth1 -vvv -s0 'udp port 546 or udp port 547'

正常的服务器回复必须包含 IA_PD-prefix .../64,不能只有一个空的 IA_PD 容器。

3. 客户端侧

Windows:

ipconfig /all
route print -6
curl.exe -6 https://api64.ipify.org

macOS/Linux:

ifconfig
netstat -rn -f inet6
curl -6 https://api64.ipify.org

最终再做一次真实场景:

客户端保持联网或进入睡眠
→ 小米重新拨号,公网前缀变化
→ 自愈脚本完成 64 → 60
→ odhcpd 重建下游 PD
→ OpenWrt 获得新 /64
→ 客户端收到新前缀,旧前缀正常退场
→ IPv6 网站继续可用

反复连续重拨可能让路由器和客户端暂时保留多代前缀。测试完成后应给地址生命周期正常收敛的时间;若实验现场已经严重污染,可重启二级 OpenWrt,并让客户端网卡重新入网一次,再从干净状态验收。

十、为什么旧 Apple TV RA Guard 可以退出

旧架构需要中继:

主路由 LAN 上的 Apple TV RA
→ OpenWrt RA relay
→ 下游客户端

新架构中:

小米 /60
→ DHCPv6-PD 给 OpenWrt /64
→ OpenWrt 自己作为 RA Server
→ 客户端

下游客户端不再依赖主路由 LAN 的 RA,也不再接收经 OpenWrt 改换身份的 Apple TV RA。此前为 relay 添加的精确入口规则因此可以停用;更不能恢复那条“全局丢弃所有 Router Lifetime=0 RA”的旧规则,因为它会连合法的前缀退场 PIO 一起丢掉。

本文现场最终将旧规则文件保留为禁用备份:

/etc/nftables.d/20-ra-relay-zero-lifetime-guard.nft.disabled

检查运行态:

nft -a list ruleset | grep -Ei 'apple|zero-lifetime|router-advert'

正常情况下不应再有那条自定义 RA DROP。

十一、完整回滚:恢复小米原厂行为

如果自愈逻辑导致异常,或者以后不再需要给二级 OpenWrt 委派前缀,可以在小米本地 SSH 中执行:

rm -f /data/fix-ipv6-pd.sh
rm -f /data/99-fix-ipv6-pd
rm -f /data/install-ipv6-pd-fix.sh
rm -f /etc/hotplug.d/iface/99-fix-ipv6-pd

uci -q delete firewall.ipv6_pd_fix
uci commit firewall

uci set network.lan.ip6assign='64'
uci commit network

reboot

这会取消本文添加的自愈脚本和持久化入口,并把小米 LAN assignment 恢复为原来的 /64。随后二级 OpenWrt 若仍需 IPv6,要么重新启用原来的 relay 架构,要么改由其他支持下游 DHCPv6-PD 的主路由负责拨号。

在执行前应确认路径与 UCI section 名称完全一致;这些删除命令只针对本文列出的三个 /data 文件、一个 hotplug 文件和 firewall.ipv6_pd_fix section。

十二、结论

已证明

  • ISP 已向本文的 AX6000 委派 /60;
  • 小米最初只给 LAN 分配 /64;
  • 二级 OpenWrt 确实发出了 IA_PD /64 请求;
  • 修复前,小米返回空 IA_PD 或 NoPrefixAvail;
  • 把 network.lan.ip6assign 改为 60 后,二级 OpenWrt 获得了独立 /64;
  • 某些真实重拨会把 ip6assign 重新写回 64;
  • 在一次已复现现场中,network restart 后 PD 池仍为空,重启 odhcpd 后立即恢复 IA_PD-prefix /64;
  • 重启与再次重拨后,自愈链仍能恢复 /60 → /64 PD。

未经证明

  • 所有运营商都会给家庭宽带 /60,V2EX上的帖子中可以发现,不同地区,不同运营商的下发策略可能不同;
  • 所有小米 AX6000 固件都会以完全相同方式覆盖 ip6assign;
  • 所有小米/Redmi 路由器都能使用同一套 /data + firewall include 持久化;
  • 写回 64 的具体 vendor 进程或二进制已经被定位;
  • 只要强行设置 ip6assign=60,一个只从 ISP 获得 /64 的用户也能凭空得到 16 个 /64。

最后一点尤其重要:本文只是释放并正确使用运营商已经委派给主路由的 /60。如果 wan_6 status 中确实只有 /64,就没有可继续切分的标准下游 /64;修改一个 UCI 数字不会创造地址空间。

十三、经验

1. 不要用 LAN 地址长度代替 WAN-PD 证据

主路由 LAN 显示 /64,只说明该链路需要一个 /64。判断 ISP 到底委派了多大前缀,应看 wan_6 的 ipv6-prefix,而不是 ip addr 中某个单独地址。

2. IA_NA 和 IA_PD 是两件事

路由器获得一个 IPv6 地址,并不等于获得可继续下发的前缀。二级 OpenWrt 能拿到地址却拿不到 ipv6-prefix 时,应抓 DHCPv6,分别检查 IA_NA 与 IA_PD。

3. 空 IA_PD 也是证据

服务器没有显式打印 NoPrefixAvail,但 Advertise 中存在 IA_PD 容器却没有 IA Prefix Option,同样说明客户端没有获得可用的 delegated prefix。

4. 配置值正确,不等于守护进程运行态正确

本案中 UCI、ubus assignment 都已恢复 /60,但 odhcpd 的 PD 池仍为空。只有抓包看到完整的 Solicit → Advertise(IA_PD-prefix) → Request → Reply,才算真正恢复。

5. 能用 PD,就不要把 relay 当长期等价替代品

relay 解决的是“上游不提供前缀委派”时的兼容问题;PD 解决的是分层路由本身。二者都能让客户端出现公网 IPv6 地址,但控制面复杂度、故障边界和重编号行为完全不同。

结语

这次最反直觉的地方是:我们一度以为运营商只给了 /64,甚至准备联系运营商;但当真正ssh到小米后台看 wan_6 后才发现,完整的 /60 一直都在。

问题不是“没有地址”,而是地址池没有被正确分配到能够继续委派的层级:

ISP 已给 /60
≠ 主路由已经把 /60 用作下游 PD 池
≠ 二级路由器已经拿到 /64

最终的解决也不是给 relay 再叠一层补丁,而是让网络回到 IPv6 原本擅长的方式:上级拿较大前缀,下级拿独立 /64,每一层各自路由和宣告。

从此,二级 OpenWrt 不再“借用”主路由 LAN 的 IPv6,而是真正拥有了自己的前缀。

参考资料