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

15 KiB
Raw Blame History

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

背景

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 全部通过:

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 上不受影响,不会丢失远程连接,所以直接改了:

( 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 重启后自动

查看看门狗动作:

logread -e wwan-watchdog | grep -v crond

回滚

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

#!/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(追加部分)


[ "$ACTION" = "disconnected" ] && case "$INTERFACE" in
	wwan2|wwan5) /usr/sbin/wwan-watchdog & ;;
esac

/etc/hotplug.d/iface/99-fix-same-subnet-routing

#!/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"