Files
openwrt-build-helper/reports/2026-10-28-linksys-mx8500-wwan-recovery.md

473 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Linksys MX8500:Wi-Fi 上联 wwan2 / wwan5 断流后无法自动恢复
- 日期:2026-10-28
- 设备:Linksys MX8500(`linksys-mx8500.tail694d4.ts.net`)
- 系统:OpenWrt 25.12.5 r33051-f5dae5ece4,qualcommax/ipq807x
- 无线固件:ath11k WLAN.HK.2.9.0.1-02146
## 背景
路由器没有有线 WAN,用两个 STA 接口中继上游 Wi-Fi,mwan3 做负载均衡:
| 接口 | 设备 | 射频 | 上游 SSID | MAC | 说明 |
|---|---|---|---|---|---|
| wwan5 | `phy0-sta0` | radio0(5G) | Urban Residence | 固定 | 上游要求登录,按 MAC 记录 |
| wwan2 | `phy1-sta0` | radio1(2.4G) | Urban Resident | random | 开放网络,无需登录 |
两个射频上同时跑着本地热点(AP + STA 共用一个射频)。两个上游 SSID 后面是同一个 Nomadix 网关(`00:50:e8:04:90:8c`)。
现象:wwan2 / wwan5 断流后会一直不通,要手动重启 Wi-Fi 才恢复。其他设备连同样的 SSID 没有问题。
## 结论
| 接口 | 状态 | 处理 |
|---|---|---|
| wwan5(5G) | 已解决 | radio0 HE160 → HE40,观察 63 分钟无故障 |
| wwan2(2.4G) | 初步解决,看门狗兜底 | radio1 HE40 → HE20,再去掉 `txpower='13'` 限制(现 29 dBm)后观察 41 分钟无故障、tx failed 约 0.1%,需更长时间确认。如果仍复发,下次编译固件加入 `CONFIG_HE_OVERRIDES`,对 wwan2 设置 `disable_he=1`(见“未处理 / 后续”) |
| 两者 | 自动恢复 | 新增 `wwan-watchdog`,断流约 2 分钟内自动重连 |
## 排查结果
### 故障时的状态(wwan2)
| 项目 | 结果 |
|---|---|
| `iw link` | 关联正常,信号 -53 dBm,beacon 正常,无 beacon loss |
| 下行速率 | 162 Mbit/s,正常 |
| 上行 | **tx failed 5775 / 6263(约 92%)**,速率掉到 HE-MCS0 NSS1(8.6 Mbit/s) |
| ping 网关 172.20.8.1 | 100% 丢包(ARP 表项仍为 REACHABLE) |
| ping 外网 | 100% 丢包 |
| 路由 | 正确:`223.5.5.5 from 172.20.8.60 via 172.20.8.1 dev phy1-sta0 table 1` |
路由器能收到上游 AP,但发出去的帧绝大多数得不到 ACK,问题在无线链路层,不在路由或防火墙。
对 wpa_supplicant 发一次 `REASSOCIATE`(不换 MAC、不改配置)后,tx failed 立刻归零,速率恢复到 100+ Mbit/s,网关和外网都通。约 5 分钟后再次进入同样的状态,说明这是可复现的问题,不是偶发。
wwan5 在日志中也有连续丢失 224 个 ping、手动重载 Wi-Fi 后才恢复的记录,但更早的日志已被覆盖(日志缓冲只有 128 KB),无法确认原因是否相同。
### 为什么不会自动恢复
1. STA 仍处于关联状态,beacon 正常,wpa_supplicant / netifd 收不到断开事件,不会重连。
2. mwan3 检测到 ping 失败后只把接口标记为 offline、撤掉路由,不会去修复无线链路。
3. 于是链路一直“半死”,直到有人手动重启 Wi-Fi。
### 根因
主要原因:同一射频上,本地热点的频宽比 STA 宽。
| 射频 | 热点(修改前) | STA(上游给的) |
|---|---|---|
| radio1(2.4G) | HE40,中心 2447 MHz | 20 MHz,中心 2437 MHz |
| radio0(5G) | HE160,中心 5815 MHz | 40 MHz,中心 5795 MHz |
AP 与 STA 频宽不一致时,ath11k 的 STA 上行会进入大量 tx failed 的状态。把热点频宽改成和 STA 一致后:
- wwan5(5G,HE40):观察 66 分钟以上未再出现故障。
- wwan2(2.4G,HE20):故障从“每次重连后约 5 分钟”降到“几十分钟一次”,但没有完全消失。
2.4G 剩下最可疑的是 HE(Wi-Fi 6)本身:ath11k 作为客户端连 HE AP 时有已知的兼容问题。但无法在现有固件上验证,原因见“未处理 / 后续”。
以上是根据对比测试得出的结论,没有在驱动层面确认具体机制。
## 修改内容
所有修改都在路由器上完成,原文件已备份。
### 1. 热点频宽与 STA 对齐(根因修复)
```sh
uci set wireless.radio1.htmode='HE20' # 原 HE40
uci set wireless.radio0.htmode='HE40' # 原 HE160
uci commit wireless
wifi up radio1
wifi up radio0
```
代价:2.4G 热点从 40 MHz 降到 20 MHz,5G 热点从 160 MHz 降到 40 MHz。高带宽需求由 6 GHz 热点(radio2,HE160)承担,未改动。
radio1 原有 `txpower='13'`,后来去掉了(见“验证”中的 txpower 一节)。
注意:`wifi up radioX` 会断开该射频上的 STA。如果 SSH 正好走这条上联,命令可能执行到一半就断,应在路由器上放到后台执行(BusyBox 没有 `nohup`):
```sh
( trap '' HUP; uci set ...; uci commit wireless; wifi up radio0 ) </dev/null >/tmp/change.log 2>&1 &
```
### 2. 新增无线看门狗 `/usr/sbin/wwan-watchdog`(兜底)
每分钟由 cron 运行一次,mwan3 判定接口 offline 时也会立即触发。
“不通”指网关和两个外网探测地址(223.5.5.5、1.1.1.1)全部 ping 不通,任一能通就算正常。
| 检测结果 | 处理 |
|---|---|
| 正常 | 清零计数 |
| 网关通,但 mwan3 显示 offline | 只记日志(wwan5 可能是登录过期,重连 Wi-Fi 无用) |
| 不通,第 1 次 | 只记日志,等下一次确认 |
| 不通,第 2、3 次 | `REASSOCIATE`(不换 MAC,wwan5 登录状态保留) |
| 不通,第 4 次 | `wifi up radioX` |
| 之后仍不通 | 每 10 次检测重启一次射频,避免频繁打断热点 |
判定故障时,日志会附上当时的 `tx packets / tx failed / tx bitrate`,便于区分是无线卡死(tx failed 暴涨、速率掉到 MCS0)还是上游问题。
第一版只 ping 网关,且第一次失败就重连。上游 Nomadix 网关偶尔不回 ping,23:11 因此误触发了一次:wwan2 原本正常,被重连后反而断了约 1 分钟。改成上面的逻辑后没有再误触发。第一版备份在 `/root/wwan-watchdog.v1.bak`。
相关改动:
- `crontab`:新增 `* * * * * /usr/sbin/wwan-watchdog`
- `/etc/mwan3.user`:追加 `disconnected` 事件时调用看门狗(备份:`/root/mwan3.user.bak`)
- `/etc/sysupgrade.conf`:加入 `/usr/sbin/wwan-watchdog` 和 `/etc/mwan3.user`
日志标签为 `wwan-watchdog`,用 `logread -e wwan-watchdog` 查看。
### 3. 修复 `/etc/hotplug.d/iface/99-fix-same-subnet-routing`
原脚本每次 ifup 只删除当前 IP 的旧规则,旧 IP 的规则会一直残留。wwan2 使用随机 MAC,IP 可能变化,残留越积越多。修改前存在两条残留规则:
```
100: from 172.20.8.214 lookup 2 # 指向 wwan5 的表,错误
100: from 172.20.8.55 lookup 1
```
新逻辑:ifup 时先删除所有满足以下任一条件的 prio 100 规则,再添加当前规则:
- 源 IP 与当前 IP 相同
- 指向同一张路由表
- 源 IP 已不在本机任何接口上
已手动运行一次,残留规则已清除。备份:`/root/99-fix-same-subnet-routing.bak`。
这些规则也是看门狗能正确判断的前提。看门狗用 `ping -I <设备>` 检测,本机发出的流量会被 mwan3 按 `balanced` 策略打 mark,可能被分到另一条上联的路由表,绑定的设备与路由出口不一致时,包就发不出去,造成误判。prio 100 的源地址规则排在 mwan3 的 fwmark 规则之前,把流量固定到正确的表。这一点是在 Cudy TR3000 上启用第二条上联时实测确认的,见 [`2026-10-28-cudy-tr3000-wwan-watchdog.md`](2026-10-28-cudy-tr3000-wwan-watchdog.md)。
### 4. 日志缓冲调大
`system.@system[0].log_size` 从 128 改为 1024(KB)。原来 mwan3track 的日志约 6 分钟就会把缓冲写满,无法回溯故障。
## 验证
### wwan2:HE20 前后对比
| | HE40(修改前) | HE20(修改后) |
|---|---|---|
| 正常维持时间 | 每次重连后约 5 分钟即失效 | 观察 33 分钟无失效 |
| tx failed | 5775 / 6263(约 92%) | 24 / 31142(< 0.1%) |
| tx 速率 | HE-MCS0 NSS1,8.6 Mbit/s | HE-MCS4–6 NSS2,68–172 Mbit/s |
| 网关 ping | 不通 | 每分钟检测全部通过 |
之后继续观察到 23:43:
```
23:22 tx failed 2 / 7500
23:33 tx failed 165 / 20807 # 10 分钟内上涨,前兆
23:36:40 mwan3track 开始连续失败(共丢 22 个)
23:37:09 wwan-watchdog: 网关和探测地址都不通,等待确认
23:38:09 wwan-watchdog: 仍不通,reassociating
23:38:15 wwan-watchdog: link passing traffic again
```
之后 00:37 又发生一次(这次连接持续了 52 分钟),看门狗第二版的日志记录了故障发展过程:
```
00:37:09 unreachable (tx=57049 failed=182 rate=1.0 MBit/s), confirming next check
00:37:50 (手动查看)tx failed 628,tx 速率 HE-MCS0 8.6 Mbit/s,网关 ping 不通
00:38:09 still unreachable (check 2, tx=57748 failed=853 rate=8.6 MBit/s HE-MCS), reassociating
00:38:16 link passing traffic again(tx failed 归零,HE-MCS3 68.8 Mbit/s)
```
一分钟内 tx failed 从 182 涨到 853、速率掉到 MCS0,与最初的故障特征一致。两次都是断流约 1.5 分钟,看门狗自动恢复。同一时间 wwan5 正常(两条 SSID 走同一个 Nomadix 网关),说明是 2.4G 本机无线问题,不是上游故障。HE20 大幅降低了频率,但没有根治 wwan2。
### wwan2:去掉 radio1 发射功率限制后
00:41 删除了 radio1 的 `txpower='13'`,实际发射功率从 13 dBm 变为 29 dBm(radio0 原本就是 28 dBm)。
观察期间发生了一次意外重启(见下方“事故”),所以观察从开机算起:00:44 – 01:25,共 41 分钟。
| 项目 | HE20、13 dBm | HE20、29 dBm |
|---|---|---|
| 故障间隔 | 25 – 52 分钟一次 | 41 分钟内 0 次 |
| tx failed / tx packets | 故障前一分钟内 182 → 853 | 56 / 42954(约 0.13%) |
| tx 速率 | 故障时 HE-MCS0 8.6 Mbit/s | HE-MCS8 NSS2 206.5 Mbit/s |
| 看门狗动作 | 每次故障都触发 | 0 次 |
| mwan3 wwan2 状态变化 | offline / online | 只有开机时一次 online |
结果很好,但 41 分钟比之前最长的 52 分钟间隔还短,不能确定已根治,需要观察一天以上。查看方法:
```sh
logread -e wwan-watchdog | grep -v crond
logread | grep -E 'mwan3track.*wwan2 .*(offline|online)'
```
推测:发射功率低时,上行(STA → AP)的帧更容易失败,ath11k 的速率控制退到 MCS0 后没能恢复,进入“半死”状态。
### 事故:误杀 procd 导致路由器重启(00:44)
停止上一轮监控循环时,在路由器上执行了:
```sh
for p in $(pgrep -f 'sleep 60'); do pp=$(awk '{print $4}' /proc/$p/stat); kill $pp $p; done
```
本意是杀掉监控循环的 shell。但路由器上还有其他已被 init 收养的 `sleep 60` 进程,它们的父进程是 PID 1(procd),所以实际执行了 `kill 1`。procd 收到 SIGTERM 会重启系统。
影响:路由器重启一次,两条上联断线约 1 分钟。重启后检查,所有设置都在(HE40 / HE20、去掉的 txpower、crontab、看门狗、路由规则脚本、mwan3.user),wwan2 / wwan5 自动上线,prio 100 规则自动加回。
教训:不要按进程名批量 kill,更不要 kill 查到的父进程。停止远程监控只需结束本机的 ssh 进程。
另外试过把 radio1 改成 `HT20`:只有热点变成 HT,wwan2 仍然以 HE 连接上游(`tx bitrate ... HE-MCS 6`),没有意义,已改回 HE20。
### wwan5:HE160 时看门狗触发一次,HE40 后稳定
radio0 仍为 HE160 时,wwan5 出现了同样的故障,看门狗自动恢复:
```
22:00:23 mwan3track 开始连续 ping 失败(共丢 16 个)
22:01:05 wwan-watchdog: wwan5: gateway 10.33.13.1 unreachable ... reassociating
22:01:13 重新关联成功,DHCP 拿回同一个 IP 10.33.13.15(MAC 不变,登录状态保留)
22:01:16 mwan3track: Interface wwan5 is online
```
从断流到恢复约 50 秒,修改前需要手动处理。
22:40 把 radio0 改为 HE40 后(热点和 STA 都是 40 MHz,中心 5795 MHz):
| 时间 | 连接时长 | tx failed / tx packets |
|---|---|---|
| 22:41 | 52 s | 8 / 14358 |
| 23:02 | 21 min | 49 / 83677 |
| 23:22 | 42 min | 98 / 154419 |
| 23:43 | 63 min | 126 / 228395 |
全程网关 ping 正常,看门狗未触发,mwan3:`wwan5 is online (online 01h:02m:50s)`。修改前大约 17 分钟就会出故障。
### 路由规则
每次 `wifi up radio1` 后 wwan2 都会换随机 MAC 和 IP(172.20.8.60 → 172.20.9.230 → 172.20.11.106),新脚本每次都自动删除了旧 IP 的规则,始终只保留两条:
```
100: from 10.33.13.15 lookup 2
100: from 172.20.11.106 lookup 1
```
## 未处理 / 后续
### wwan2:下次编译固件时加入 HE overrides,对 wwan2 关闭 HE
去掉 txpower 限制后 wwan2 暂未复发。如果长时间观察确认不再复发,这一步可以不做;如果仍复发,再按下面的方案处理。
目前 wwan2 靠看门狗兜底:wwan2 只是副线路(mwan3 权重 3),断流期间流量会自动走 wwan5,约 2 分钟内恢复。
要根治,需要让 wwan2 不用 HE(Wi-Fi 6)连上游,只用 HT(Wi-Fi 4)。wpa_supplicant 有网络级参数 `disable_he=1`,但现有固件不支持:
| 检查项 | 结果 |
|---|---|
| 路由器上的 wpad | `wpad-basic-mbedtls` |
| `GET_NETWORK <id> disable_vht` | `0`(支持) |
| `GET_NETWORK <id> disable_he` | `FAIL`(不支持) |
| OpenWrt 25.12 `wpa_supplicant-basic.config` / `-full.config` | 只有 `CONFIG_HT_OVERRIDES=y`、`CONFIG_VHT_OVERRIDES=y`,**没有** `CONFIG_HE_OVERRIDES` |
| hostap `wpa_supplicant/Makefile` | 支持 `CONFIG_HE_OVERRIDES` → `-DCONFIG_HE_OVERRIDES` |
所以单纯换成完整版 `wpad-mbedtls`(本仓库 `e749002` 已改)也不够,下次编译需要改三处(路径基于 OpenWrt 25.12 源码树):
1. **wpa_supplicant 打开 HE overrides。** 在 `package/network/services/hostapd/files/wpa_supplicant-full.config` 中,`CONFIG_VHT_OVERRIDES=y` 下面加:
```
# Support HE overrides (disable HE)
CONFIG_HE_OVERRIDES=y
```
本仓库的包列表用的是完整版 `wpad-mbedtls`,改 `-full.config` 即可;如果改用 basic 版,要改 `wpa_supplicant-basic.config`。
2. **wifi-scripts 把 `disable_he` 传给 wpa_supplicant。** 在 `package/network/config/wifi-scripts/files-ucode/usr/share/ucode/wifi/supplicant.uc` 的 `setup_sta()` 里,`network_append_vars` 列表中 `'disable_vht'` 旁边加上 `'disable_he'`:
```
'disable_ht', 'disable_ht40', 'disable_vht', 'disable_he', 'vht', 'max_oper_chwidth',
```
3. **schema 登记 `disable_he`。** 在 `package/network/config/wifi-scripts/files-ucode/usr/share/schema/wireless.wifi-iface.json` 的 `properties` 中加入:
```json
"disable_he": {
"description": "Disable HE (802.11ax) for this station",
"type": "boolean"
},
```
不加的话 `validate.uc` 只会打印 `disable_he is not present in the schema`,值仍会传下去,但不会被转换成布尔值。加上更干净。
刷机后在路由器上:
```sh
uci set wireless.wifinet_radio1.disable_he='1'
uci commit wireless
wifi up radio1
```
验证方法:
```sh
# 应返回 1,而不是 FAIL
ubus call wpa_supplicant.phy1-sta0 control '{"command":"LIST_NETWORKS"}'
ubus call wpa_supplicant.phy1-sta0 control '{"command":"GET_NETWORK <id> disable_he"}'
# tx bitrate 应显示 MCS x(HT),不再是 HE-MCS
iw dev phy1-sta0 station dump | grep bitrate
# 观察数小时,看门狗不应再触发 wwan2
logread -e wwan-watchdog | grep wwan2
```
注意:sysupgrade 只保留 `/etc/config/` 和 `/etc/sysupgrade.conf` 里列出的文件,`/usr/share/` 下的文件会被新固件替换。所以第 2、3 步必须进固件,在路由器上手改会在下次升级时丢失;而且第 1 步不进固件的话,手改 2、3 也没用。`disable_he` 只影响 wwan2 这个 STA,不影响 2.4G 热点。wwan5 已经稳定,不需要关 HE。
### 其他
- **wwan5 登录过期**:看门狗只记录日志,不会自动登录。如果上游登录页能用简单的 HTTP 表单提交,可以再加自动登录。
- 每天 07:00 的定时重启(`0 7 * * * reboot`)保持不变。
## 回滚
```sh
uci set wireless.radio1.htmode='HE40' && uci set wireless.radio0.htmode='HE160' && uci commit wireless
wifi up radio1 && wifi up radio0
cp /root/99-fix-same-subnet-routing.bak /etc/hotplug.d/iface/99-fix-same-subnet-routing
cp /root/mwan3.user.bak /etc/mwan3.user
sed -i '\#/usr/sbin/wwan-watchdog#d' /etc/crontabs/root && /etc/init.d/cron restart
rm /usr/sbin/wwan-watchdog
sed -i '\#^/usr/sbin/wwan-watchdog$#d; \#^/etc/mwan3.user$#d' /etc/sysupgrade.conf
```
## 附录:脚本全文
### `/usr/sbin/wwan-watchdog`
```sh
#!/bin/sh
# Recover Wi-Fi STA uplinks that stay associated but stop passing traffic.
# Called every minute by cron and on mwan3 "disconnected" events.
PATH=/usr/sbin:/usr/bin:/sbin:/bin
STATE_DIR=/tmp/wwan-watchdog
TAG=wwan-watchdog
PROBE_IPS="223.5.5.5 1.1.1.1"
exec 9>/var/lock/wwan-watchdog.lock
flock -n 9 || exit 0
mkdir -p "$STATE_DIR"
log() { logger -t "$TAG" "$*"; }
tx_stats() {
iw dev "$1" station dump 2>/dev/null | awk '
/tx packets/ { p = $3 }
/tx failed/ { f = $3 }
/tx bitrate/ { r = $3 " " $4 " " $5 }
END { print "tx=" p " failed=" f " rate=" r }'
}
# Healthy if the gateway or any probe address answers through this device
link_ok() {
local dev="$1" gw="$2" ip
for ip in "$gw" $PROBE_IPS; do
ping -c 2 -W 2 -I "$dev" "$ip" >/dev/null 2>&1 && return 0
done
return 1
}
check() {
local iface="$1" dev="$2" radio="$3"
local count_file="$STATE_DIR/$iface"
local up gw count
up=$(ifstatus "$iface" 2>/dev/null | jsonfilter -e '@.up')
[ "$up" = "true" ] || { rm -f "$count_file"; return; }
gw=$(ifstatus "$iface" | jsonfilter -e '@.route[@.target="0.0.0.0"].nexthop' | head -n 1)
[ -n "$gw" ] || return
if link_ok "$dev" "$gw"; then
[ -f "$count_file" ] && log "$iface: link passing traffic again"
rm -f "$count_file"
if ping -c 2 -W 2 -I "$dev" "$gw" >/dev/null 2>&1 && \
mwan3 interfaces 2>/dev/null | grep -q "interface $iface is offline"; then
log "$iface: gateway reachable but mwan3 offline (login expired or upstream down?)"
fi
return
fi
count=$(( $(cat "$count_file" 2>/dev/null || echo 0) + 1 ))
echo "$count" > "$count_file"
case "$count" in
1)
log "$iface: gateway $gw and probes unreachable via $dev ($(tx_stats "$dev")), confirming next check"
;;
2|3)
log "$iface: still unreachable (check $count, $(tx_stats "$dev")), reassociating"
ubus call "wpa_supplicant.$dev" control '{"command":"REASSOCIATE"}' >/dev/null 2>&1
;;
4)
log "$iface: still unreachable after reassociate ($(tx_stats "$dev")), restarting $radio"
wifi up "$radio"
;;
*)
# Back off: retry the radio restart every 10 failed checks
if [ $(( count % 10 )) -eq 4 ]; then
log "$iface: unreachable for $count checks, restarting $radio again"
wifi up "$radio"
fi
;;
esac
}
check wwan2 phy1-sta0 radio1
check wwan5 phy0-sta0 radio0
```
### `/etc/hotplug.d/iface/99-fix-same-subnet-routing`
```sh
#!/bin/sh
[ "$ACTION" = "ifup" ] || exit 0
case "$INTERFACE" in
wwan2|wwan5)
;;
*)
exit 0
;;
esac
# Get the IP address of this interface
IP=$(ifstatus "$INTERFACE" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
[ -z "$IP" ] && exit 0
# Get the L3 device name of this interface
DEV=$(ifstatus "$INTERFACE" | jsonfilter -e '@.l3_device' 2>/dev/null)
[ -z "$DEV" ] && exit 0
# Find the routing table number assigned by MWAN3 for this device
TABLE=$(ip rule show | grep "iif $DEV lookup" | grep -o 'lookup [0-9]*' | awk '{print $2}' | head -1)
[ -z "$TABLE" ] && exit 0
# Remove stale prio 100 rules: same IP, same table, or source IP no longer on this router
ip rule show | awk '$1 == "100:" && $2 == "from" { print $3, $5 }' | while read -r OLD_IP OLD_TABLE; do
if [ "$OLD_IP" = "$IP" ] || [ "$OLD_TABLE" = "$TABLE" ] || \
! ip -4 addr show | grep -q "inet $OLD_IP/"; then
ip rule del from "$OLD_IP" lookup "$OLD_TABLE" prio 100 2>/dev/null && \
[ "$OLD_IP" != "$IP" ] && \
logger -t fix-routing "Removed stale rule: from $OLD_IP table $OLD_TABLE"
fi
done
# Add source-based policy route
ip rule add from "$IP" table $TABLE prio 100
logger -t fix-routing "Added rule: from $IP table $TABLE (dev $DEV) for $INTERFACE"
```
### `/etc/mwan3.user`(追加部分)
```sh
[ "$ACTION" = "disconnected" ] && case "$INTERFACE" in
wwan2|wwan5) /usr/sbin/wwan-watchdog & ;;
esac
```