Files
openwrt-build-helper/reports/2026-10-28-cudy-tr3000-wwan-watchdog.md

356 lines
15 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.

# Cudy TR3000:部署 Wi-Fi 上联看门狗、双上联策略路由与日志优化
- 日期:2026-10-28
- 设备:Cudy TR3000 256MB v1(`cudy-tr3000.tail694d4.ts.net`,远程)
- 系统:OpenWrt 25.12.6 r33311-981c978eff,mediatek/filogic
- 无线:MT7981,mt76 驱动
- 相关报告:[`2026-10-28-linksys-mx8500-wwan-recovery.md`](2026-10-28-linksys-mx8500-wwan-recovery.md)
## 背景
Linksys MX8500 上发现 Wi-Fi 上联(STA)会卡在“已关联但发不出包”的状态,且不会自动恢复(见相关报告)。本次把同一套看门狗和日志设置部署到 Cudy TR3000,并检查它是否存在同样的问题。
这台路由器是远程设备,上联只有 Wi-Fi。改 5G 设置会断开 wwan5、导致远程断线,所以 5G 只做了不影响 Wi-Fi 的改动。
部署完成后,wwan2 被启用,变成两条上联同时在线。随即发现 mwan3 的路由问题(见“启用 wwan2 后的问题”),并做了相应修改。
## 现状
| 接口 | 设备 | 射频 | 上游 | 状态 |
|---|---|---|---|---|
| wwan5 | `phy1-sta0` | radio1(5G) | eduroam(WPA2-Enterprise,PEAP/MSCHAPv2) | 在用,mwan3 online,权重 80 |
| wwan2 | `phy0-sta0` | radio0(2.4G) | eduroam | 00:10 启用,mwan3 online,权重 50 |
| wan | `eth0` | — | 有线 | 无载波,未使用 |
两条 eduroam 上联拿到的是不同网段:wwan5 `172.27.64.0/20`(网关 172.27.64.1),wwan2 `172.27.80.0/20`(网关 172.27.80.1)。mwan3 策略 `balanced` 按权重在两条之间分配连接。
注意射频编号和 MX8500 相反:这台的 5G 是 radio1 / phy1,2.4G 是 radio0 / phy0。
5G 射频上同样存在热点与 STA 频宽不一致:
| | 频宽 | 中心频率 |
|---|---|---|
| 热点 `phy1-ap0` | HE80 | 5290 MHz |
| STA `phy1-sta0` | 20 MHz(eduroam 只给 20 MHz) | 5260 MHz |
信道 52 是 DFS 信道。
## 检查结果:没有发现 MX8500 的故障
| 项目 | MX8500 故障时 | Cudy wwan5 |
|---|---|---|
| 连接时长 | 约 5 分钟后失效 | 30728 s(约 8.5 小时,开机至今未断) |
| tx failed / tx packets | 5775 / 6263(约 92%) | 67345 / 2242239(约 3%) |
| tx 速率 | HE-MCS0 NSS1,8.6 Mbit/s | HE-MCS11 NSS2,286.7 Mbit/s |
| 信号 | -53 dBm | -51 dBm |
| mwan3 离线记录 | 有 | 日志覆盖 17:43 起,无任何 offline 事件 |
结论:mt76 驱动目前没有表现出 ath11k 的“半死”问题。频宽不一致本身不一定会触发故障。
## 启用 wwan2 后的问题:绑定接口的流量被 mwan3 分到另一条上联
wwan2 启用后,看门狗使用的检测方式(`ping -I <设备>`)出现大量失败:
| 测试 | 结果 |
|---|---|
| `ping -I phy0-sta0`(wwan2)1.1.1.1 | 3/3 失败 |
| `ping -I phy0-sta0`(wwan2)8.8.8.8 | 时通时不通 |
| `ping -I phy1-sta0`(wwan5) | 正常(启用 wwan2 的瞬间因防火墙重载短暂失败) |
| 不绑定接口的普通 ping、按源 IP 绑定的 ping | 正常 |
| `iw` 链路状态 | 正常,HE-MCS 11,不是无线卡死 |
证据:
- wwan2 这次 ping 的 conntrack 条目带的是 `mark=16128`(0x3f00),不是 wwan2 的 0x500,且 `[UNREPLIED]`。
- `phy0-sta0` 的邻居表里出现 `1.1.1.1 FAILED`、`8.8.8.8 FAILED`:路由器把公网地址当作直连地址,直接在 Wi-Fi 上发 ARP。
原因:本机发出、绑定到某个设备的流量,经过 mwan3 的 OUTPUT 钩子时会被打上 mark,mark 由 `balanced` 策略决定,可能指向另一条上联的路由表。查到的路由出口和绑定的设备不一致时,内核找不到可用路由,就退化成直连 ARP,包发不出去。时通时不通,是因为不同连接被分到了不同的上联。
后果:
1. **看门狗误判**:wwan2 实际可用,却会被判定为“不通”而强制重连。发现问题时,看门狗的下一次运行在 00:12,已在 00:11:43 先暂停了它的 cron 任务,修好后才恢复,未发生误触发。
2. 本机其他绑定接口的程序(mwan3track 自己有处理,不受影响)也可能受影响。
验证:手动添加源地址规则后,两条上联、两个探测地址各测 3 轮,12/12 全部通过:
```sh
ip rule add from 172.27.85.164 table 5 prio 100 # wwan2
ip rule add from 172.27.76.50 table 3 prio 100 # wwan5
```
MX8500 上的 `99-fix-same-subnet-routing` 做的就是这件事:按源地址把流量固定到对应上联的路由表。prio 100 排在 mwan3 的 fwmark 规则(2003、2005)之前,所以优先生效。
## 修改内容
### 1. 部署看门狗 `/usr/sbin/wwan-watchdog`
逻辑与 MX8500 的第二版相同(判定方式、确认次数、恢复步骤、退避),只有两处按本机调整:
| 项目 | MX8500 | Cudy TR3000 | 原因 |
|---|---|---|---|
| 探测地址 `PROBE_IPS` | `223.5.5.5 1.1.1.1` | `1.1.1.1 8.8.8.8` | 设备在美国 |
| 接口映射 | wwan2→phy1/radio1,wwan5→phy0/radio0 | wwan2→phy0/radio0,wwan5→phy1/radio1 | 射频编号相反 |
**eduroam 网关 172.27.64.1 不回 ping**(实测 100% 丢包,但外网正常)。MX8500 的第一版看门狗只 ping 网关、且第一次失败就重连,放在这里会每分钟强制重连一次。现在的版本是“网关或任一探测地址能通就算正常”,并且要连续两次失败才动手,所以不受影响。代价是“网关通但 mwan3 离线”这条提示在这台上不会出现。
恢复步骤:
| 检测结果 | 处理 |
|---|---|
| 正常 | 清零计数 |
| 不通,第 1 次 | 只记日志,等下一次确认 |
| 不通,第 2、3 次 | `REASSOCIATE`(eduroam 会重新走一次 EAP 认证) |
| 不通,第 4 次 | `wifi up radio1` |
| 之后仍不通 | 每 10 次检测重启一次射频 |
接口未 up 时看门狗会跳过它,所以 wwan2 启用前后都不需要改脚本。但两条上联同时在线时,必须配合第 3 项的源地址规则,否则会误判(见上一节)。
相关改动:
- `/etc/crontabs/root`:新增 `* * * * * /usr/sbin/wwan-watchdog`(原来没有任何定时任务),并 `cron enable`
- `/etc/mwan3.user`:追加 `disconnected` 事件时调用看门狗(备份:`/root/mwan3.user.bak`)
- `/etc/sysupgrade.conf`:加入 `/usr/sbin/wwan-watchdog` 和 `/etc/mwan3.user`
部署过程中发现:BusyBox 的 `crontab -` 从管道读入时会写出空文件。所以这里直接写 `/etc/crontabs/root`,然后重启 cron。
### 2. 日志缓冲调大
`system.@system[0].log_size` 从 128 改为 1024(KB),出故障时能回溯更久的记录。
### 3. 部署 `/etc/hotplug.d/iface/99-fix-same-subnet-routing`
直接从 MX8500 复制,内容完全相同(md5 `79f134c39e38d67c1ab43cfe201236a7`)。wwan2 / wwan5 每次 ifup 时:
- 从 mwan3 的 `iif <设备> lookup <表>` 规则里找到该接口的路由表(本机 wwan5 = 表 3,wwan2 = 表 5)
- 删除指向同一张表、同一 IP、或源 IP 已不在本机的旧 prio 100 规则
- 添加 `from <当前 IP> lookup <表> prio 100`
已手动对两个接口各运行一次,当前规则:
```
100: from 172.27.76.50 lookup 3
100: from 172.27.85.164 lookup 5
```
已加入 `/etc/sysupgrade.conf`。
### 4. radio0(2.4G)HE40 → HE20
2.4G 热点 HE40,wwan2 只有 20 MHz,和 MX8500 的 2.4G 改动前情况相同。改 radio0 只会让 wwan2 和 2.4G 热点短暂断开,wwan5 在 radio1 上不受影响,不会丢失远程连接,所以直接改了:
```sh
( trap '' HUP; uci set wireless.radio0.htmode='HE20'; uci commit wireless; wifi up radio0 ) </dev/null >/tmp/radio0-change.log 2>&1 &
```
改完后热点与 STA 都是 20 MHz。wwan2 漫游到了信道 6 的另一台 eduroam AP,IP 不变,源地址规则仍然有效。
### 未改动
- **5G 频宽**:保持 HE80,没有和 STA 对齐。对齐只能降到 HE20,5G 热点带宽会降到约 1/4,而 TR3000 没有 6 GHz 可以补充;且目前没有任何故障证据;改的时候还会远程断线。如果以后看门狗日志里出现 wwan5 卡死(判定故障时记录的 tx failed 暴涨、速率掉到 MCS0),再考虑改。
- **radio0 `txpower='10'`**:wwan2 的 tx failed 约 10%,高于 wwan5 的约 3%,可能与 2.4G 只有 10 dBm 发射功率有关。速率仍是 HE-MCS 11,链路可用,没有改。
- **HE overrides**:这台已经是完整版 `wpad-mbedtls`,但和 MX8500 一样不支持 `disable_he`(OpenWrt 25.12 没有打开 `CONFIG_HE_OVERRIDES`)。目前不需要。
## 验证
```
$ crontab -l
* * * * * /usr/sbin/wwan-watchdog
$ logread | grep wwan-watchdog
... cron.err crond[...]: USER root pid ... cmd /usr/sbin/wwan-watchdog # 每分钟运行
(无 wwan-watchdog: 开头的动作日志,即没有误触发)
$ mwan3 interfaces | grep wwan5
interface wwan5 is online and tracking is active (online 08h:45m:53s, ...)
```
部署后观察期间 wwan5 未断线,看门狗未触发。
### 启用 wwan2、加源地址规则、radio0 改 HE20 之后(00:14:48 – 00:35:29)
每分钟用看门狗同样的方式(`ping -I <设备>`,1.1.1.1 或 8.8.8.8 任一通即可)检测两条上联:
| 时间 | wwan5 tx failed / tx packets | wwan5 连接时长 | wwan2 tx failed / tx packets | wwan2 连接时长 |
|---|---|---|---|---|
| 00:14:50 | 67739 / 2343668 | 31951 s | 81 / 742 | 57 s |
| 00:20:00 | 67821 / 2365324 | 32261 s | 380 / 3322 | 367 s |
| 00:25:10 | 67939 / 2385476 | 32571 s | 725 / 6079 | 677 s |
| 00:30:20 | 68021 / 2404660 | 32881 s | 1067 / 8696 | 987 s |
- 20 分钟内两条上联 0 次检测失败。
- 看门狗 0 次动作(无误触发)。
- 结束时 mwan3:`wwan5 online 09h:13m:05s`,`wwan2 online 00h:21m:29s`。
- 观察期间 wwan5 新增 tx failed 约 0.5%;wwan2 约 12%,偏高但稳定,速率保持 HE-MCS 11(见“未改动”中的 txpower)。
hotplug 自动生效的证据:radio0 重启后 wwan2 重新 ifup,脚本自动重新添加了规则,无需手动干预:
```
00:13:32 fix-routing: Added rule: from 172.27.76.50 table 3 (dev phy1-sta0) for wwan5 # 手动运行
00:13:32 fix-routing: Added rule: from 172.27.85.164 table 5 (dev phy0-sta0) for wwan2 # 手动运行
00:14:00 fix-routing: Added rule: from 172.27.85.164 table 5 (dev phy0-sta0) for wwan2 # radio0 重启后自动
```
查看看门狗动作:
```sh
logread -e wwan-watchdog | grep -v crond
```
## 回滚
```sh
sed -i '\#/usr/sbin/wwan-watchdog#d' /etc/crontabs/root && /etc/init.d/cron restart
cp /root/mwan3.user.bak /etc/mwan3.user
rm /usr/sbin/wwan-watchdog
sed -i '\#^/usr/sbin/wwan-watchdog$#d; \#^/etc/mwan3.user$#d' /etc/sysupgrade.conf
uci set system.@system[0].log_size='128' && uci commit system && /etc/init.d/log restart
# 源地址规则(两条上联同时在线时不建议回滚这一项)
rm /etc/hotplug.d/iface/99-fix-same-subnet-routing
sed -i '\#^/etc/hotplug.d/iface/99-fix-same-subnet-routing$#d' /etc/sysupgrade.conf
ip rule show prio 100 | awk '{print $3, $5}' | while read ip t; do ip rule del from $ip lookup $t prio 100; done
# 2.4G 频宽(会短暂断开 wwan2)
uci set wireless.radio0.htmode='HE40' && uci commit wireless && wifi up radio0
```
不要用 `crontab -l | grep -v ... | crontab -`:在 BusyBox 上会清空整个 crontab。
## 附录:脚本全文
### `/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="1.1.1.1 8.8.8.8"
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 phy0-sta0 radio0
check wwan5 phy1-sta0 radio1
```
### `/etc/mwan3.user`(追加部分)
```sh
[ "$ACTION" = "disconnected" ] && case "$INTERFACE" in
wwan2|wwan5) /usr/sbin/wwan-watchdog & ;;
esac
```
### `/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"
```