OpenWrt LEDE 路由器 OP Mobile App RPC 接口修复全记录
主播买了一个CR6609刷入了一个第三方精简的 LEDE
_________
/ /\ _ ___ ___ ___
/ LE / \ | | | __| \| __|
/ DE / \ | |__| _|| |) | _|
/________/ LE \ |____|___|___/|___|
\ \ DE /
\ LE \ / -------------------------------------------
\ DE \ / OpenWrt SNAPSHOT, r6520-3f061d422
\________\/ -------------------------------------------
因为这一款路由器有闭源的网驱,可以完全发挥CR6609的性能 (测试了其他的比如ImmortalWRT,峰值性能只能跑到200Mbps,主播排查了很久物理原因,甚至打电话给运营商安排测速了...,最终也是突然想起来使用一下运营商提供的千兆光猫跑了一下速度,可以达到1000Mbps), but RPC/ubus 接口被精简了( 导致无法用手机进行管理 ),最近有时间正好捣鼓一下能否恢复于是诞生了这篇文章,希望帮助到下一个有一样需求的同好
一、问题现象
路由器型号:MT7621 平台,运行 OpenWrt LEDE(基于 R24.3.30 快照,内核 5.4.272)。
手机上安装了 OP_Mobile(一款基于 uni-app 的 OpenWrt 管理 App),无法获取任何数据------首页 CPU 使用率、内存、连接数、挂载点全部为空;统计页面的实时流量和负载图表无数据;客户端页面看不到无线客户端和 DHCP 租约;网络页面看不到接口和设备信息。
更离谱的是,App 甚至无法正常登录,尽管网页 LuCI 和 SSH 都能正常访问路由器。
二、环境信息
系统: OpenWrt SNAPSHOT R24.3.30 (ramips/mt7621, mipsel_24kc)
内核: Linux 5.4.272
Web服务器: uhttpd 2025.07.06 (含 mod-ubus)
RPC框架: rpcd 2025.09.01
App: OP_Mobile (GitHub: FoxiDaily/OP_Mobile)
通信协议: /ubus JSON-RPC
关键已安装包:
rpcd - 2025.09.01~bba95191-r1
rpcd-mod-file - 2025.09.01~bba95191-r1
rpcd-mod-iwinfo - 2025.09.01~bba95191-r1
uhttpd - 2025.07.06~7e64e8ba-r4
uhttpd-mod-ubus - 2025.07.06~7e64e8ba-r4
iwinfo - 2024-03-08-8ffb8bfd-1
三、排查过程与修复
整个问题涉及 6 个独立的故障点,层层叠加。下面按排查顺序逐一说明。
故障 1:uhttpd 版本不匹配,/ubus 端点返回 400 Bad Request
现象
bash
curl -X POST http://192.168.5.1/ubus -d '{"jsonrpc":"2.0","id":1,"method":"call","params":["00000000000000000000000000000000","session","login",{"username":"root","password":"xxx"}]}'
返回 400 Bad Request,没有任何 JSON 响应。
原因
uhttpd 主程序版本是 2022-10-31(路由器出厂固件自带),但 uhttpd-mod-ubus 被 opkg 单独升级到了 2025.07.06。两个版本的 ubus 通信协议不兼容,导致 mod-ubus 无法正确处理请求。
修复
bash
opkg update
opkg upgrade uhttpd
升级后 uhttpd 和 uhttpd-mod-ubus 都是 2025.07.06,/ubus 端点恢复正常。
踩坑记录
一开始没注意到版本不匹配,以为是 ACL 或 ubus 对象的问题,花了大量时间检查 ACL 配置和 rpcd 状态,直到用 curl -v 看到 HTTP 400 才意识到是 Web 服务器层面的问题。建议排查时第一步永远是 curl -v 看 HTTP 状态码。
故障 2:ACL 权限配置错误,网络接口数据返回 "Access denied"
现象
登录成功后,调用 network.interface.wan.status 返回:
json
{"jsonrpc":"2.0","id":1,"result":[6]}
错误码 6 = UBUS_STATUS_PERMISSION_DENIED。
原因
原始 ACL 文件 /usr/share/rpcd/acl.d/luci-mod-rpc.json 中的 ubus 权限配置过于笼统:
json
"ubus": {
"system": ["*"],
"network": ["*"],
"wireless": ["*"],
"uci": ["*"],
"hostapd.*": ["*"]
}
这里 "network": ["*"] 只匹配名为 network 的 ubus 对象 ,不会匹配 network.interface.wan、network.device、network.wireless 这些子对象。在 ubus 的 ACL 系统中,network.interface.wan 是一个独立的对象名,需要显式授权。
同样,"wireless": ["*"] 写错了------OpenWrt 中无线状态的 ubus 对象名是 network.wireless,不是 wireless。
修复
重写 ACL 文件,添加所有需要的子对象:
json
{
"luci-mod-rpc": {
"description": "LuCI JSON-RPC access rules",
"read": {
"ubus": {
"luci": ["*"],
"luci-rpc": ["*"],
"system": ["*"],
"network": ["*"],
"network.device": ["*"],
"network.interface": ["*"],
"network.interface.*": ["*"],
"network.rrdns": ["*"],
"network.wireless": ["*"],
"uci": ["*"],
"hostapd": ["*"],
"hostapd.*": ["*"],
"dhcp": ["*"],
"dnsmasq": ["*"],
"dnsmasq.dns": ["*"],
"file": ["read", "list", "stat", "md5", "exec"],
"rc": ["list", "run"],
"service": ["list"],
"mwan3": ["*"],
"iwinfo": ["*"],
"session": ["login", "list"]
},
"uci": ["*"]
},
"write": {
"ubus": {},
"uci": []
}
}
}
修改后重启 rpcd:
bash
/etc/init.d/rpcd restart
踩坑记录
这个坑非常隐蔽。一开始以为 "network": ["*"] 中的 * 是通配符,能匹配 network.* 的所有子对象。实际上 ubus ACL 中的 * 只表示"该对象的所有方法",不表示"所有子对象" 。network.interface.wan 在 ubus 看来是一个完整的对象名,和 network 没有父子关系。
验证方法:
bash
ubus list # 列出所有 ubus 对象
ubus -v list file # 查看某个对象的方法签名
必须把 App 需要调用的每一个 ubus 对象名都在 ACL 中显式列出。
故障 3:App 使用的自定义 ubus 对象 luci 和 luci-rpc 不存在
现象
ACL 修复后,登录成功,但 App 调用 luci.getCPUUsage、luci.getOnlineUsers、luci-rpc.getDHCPLeases 等方法时,返回:
json
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Object not found"}}
原因
这是整个修复中最关键的发现。拉取 OP_Mobile 源码分析后发现,App 不使用标准 LuCI RPC 端点 (/cgi-bin/luci/rpc/auth、/cgi-bin/luci/rpc/sys 等),而是完全通过 /ubus JSON-RPC 端点通信,调用的是两个自定义的 ubus 对象:
luci对象:提供getCPUUsage、getOnlineUsers、getTempInfo、getMountPoints、getRealtimeStats、getProcessList方法luci-rpc对象:提供getNetworkDevices、getWirelessDevices、getDHCPLeases方法
这两个对象在标准 OpenWrt 中根本不存在。它们是 OP_Mobile 自己定义的接口规范,需要在路由器上自行实现。
修复
创建两个 rpcd handler 脚本,放在 /usr/libexec/rpcd/ 目录下。rpcd 启动时会自动扫描该目录下的可执行脚本,将其注册为 ubus 对象。
/usr/libexec/rpcd/luci(完整代码)
sh
#!/bin/sh
. /usr/share/libubox/jshn.sh
case "$1" in
list)
cat <<EOF
{
"getCPUUsage": {},
"getOnlineUsers": {},
"getTempInfo": {},
"getMountPoints": {},
"getRealtimeStats": {"mode": "String", "device": "String"},
"getProcessList": {}
}
EOF
;;
call)
case "$2" in
getCPUUsage)
read cpu user nice system idle irq softirq steal rest < /proc/stat
total=$((user + nice + system + idle + irq + softirq + steal))
usage=$((100 * (total - idle) / total))
json_init
json_add_string "cpuusage" "CPU: ${usage}%"
json_dump
;;
getOnlineUsers)
count=$(ip neigh show 2>/dev/null | grep -cE "REACHABLE|STALE|DELAY|PERMANENT")
json_init
json_add_string "onlineusers" "$count"
json_dump
;;
getTempInfo)
temp=""
for zone in /sys/class/thermal/thermal_zone*/temp; do
[ -r "$zone" ] || continue
t=$(cat "$zone" 2>/dev/null)
if [ -n "$t" ] && [ "$t" -gt 0 ] 2>/dev/null; then
temp=$(awk "BEGIN{printf \"%.1f\", $t/1000}")
break
fi
done
[ -z "$temp" ] && temp="N/A"
json_init
json_add_string "tempinfo" "${temp} C"
json_dump
;;
getMountPoints)
df -k 2>/dev/null > /tmp/_df_out.txt
json_init
json_add_array "result"
while read fs blocks used avail pct mount; do
[ -z "$fs" ] && continue
[ "$fs" = "Filesystem" ] && continue
json_add_object ""
json_add_string "device" "$fs"
json_add_string "mount" "$mount"
json_add_int "size" $((blocks * 1024))
json_add_int "free" $((avail * 1024))
json_close_object
done < /tmp/_df_out.txt
rm -f /tmp/_df_out.txt
json_close_array
json_dump
;;
getRealtimeStats)
read input
json_load "$input"
json_get_var mode mode
json_get_var device device
case "$mode" in
interface)
[ -z "$device" ] && exit 1
line=$(grep "^[[:space:]]*${device}:" /proc/net/dev 2>/dev/null)
[ -z "$line" ] && exit 1
rx_bytes=$(echo "$line" | awk -F: '{print $2}' | awk '{print $1}')
rx_packets=$(echo "$line" | awk -F: '{print $2}' | awk '{print $2}')
tx_bytes=$(echo "$line" | awk -F: '{print $2}' | awk '{print $9}')
tx_packets=$(echo "$line" | awk -F: '{print $2}' | awk '{print $10}')
ts=$(date +%s)
json_init
json_add_array "result"
json_add_array ""
json_add_int "" $ts
json_add_int "" $rx_bytes
json_add_int "" $rx_packets
json_add_int "" $tx_bytes
json_add_int "" $tx_packets
json_close_array
json_close_array
json_dump
;;
load)
read l1 l5 l15 _ < /proc/loadavg
l1i=$(awk "BEGIN{printf \"%d\", $l1 * 100}")
l5i=$(awk "BEGIN{printf \"%d\", $l5 * 100}")
l15i=$(awk "BEGIN{printf \"%d\", $l15 * 100}")
ts=$(date +%s)
json_init
json_add_array "result"
json_add_array ""
json_add_int "" $ts
json_add_int "" $l1i
json_add_int "" $l5i
json_add_int "" $l15i
json_close_array
json_close_array
json_dump
;;
esac
;;
getProcessList)
ps w 2>/dev/null > /tmp/_ps_out.txt
json_init
json_add_array "result"
while read pid user vsz stat cmd; do
[ -z "$pid" ] && continue
[ "$pid" = "PID" ] && continue
json_add_object ""
json_add_string "PID" "$pid"
json_add_string "USER" "$user"
json_add_string "VSZ" "$vsz"
json_add_string "STAT" "$stat"
json_add_string "COMMAND" "$cmd"
json_close_object
done < /tmp/_ps_out.txt
rm -f /tmp/_ps_out.txt
json_close_array
json_dump
;;
esac
;;
esac
/usr/libexec/rpcd/luci-rpc(完整代码)
sh
#!/bin/sh
. /usr/share/libubox/jshn.sh
case "$1" in
list)
cat <<EOF
{
"getNetworkDevices": {},
"getWirelessDevices": {},
"getDHCPLeases": {}
}
EOF
;;
call)
case "$2" in
getNetworkDevices)
json_init
for dev in $(ls /sys/class/net/ 2>/dev/null); do
[ "$dev" = "lo" ] && continue
json_add_object "$dev"
[ -r "/sys/class/net/$dev/address" ] && json_add_string "macaddr" "$(cat /sys/class/net/$dev/address)"
if [ -r "/sys/class/net/$dev/statistics/rx_bytes" ]; then
json_add_object "stats"
json_add_int "rx_bytes" "$(cat /sys/class/net/$dev/statistics/rx_bytes)"
json_add_int "rx_packets" "$(cat /sys/class/net/$dev/statistics/rx_packets)"
json_add_int "rx_errors" "$(cat /sys/class/net/$dev/statistics/rx_errors)"
json_add_int "rx_dropped" "$(cat /sys/class/net/$dev/statistics/rx_dropped)"
json_add_int "tx_bytes" "$(cat /sys/class/net/$dev/statistics/tx_bytes)"
json_add_int "tx_packets" "$(cat /sys/class/net/$dev/statistics/tx_packets)"
json_add_int "tx_errors" "$(cat /sys/class/net/$dev/statistics/tx_errors)"
json_add_int "tx_dropped" "$(cat /sys/class/net/$dev/statistics/tx_dropped)"
json_close_object
fi
json_close_object
done
json_dump
;;
getWirelessDevices)
wifi_status=$(ubus call network.wireless status 2>/dev/null)
if [ -n "$wifi_status" ]; then
echo "$wifi_status"
else
json_init
json_dump
fi
;;
getDHCPLeases)
json_init
json_add_array "dhcp_leases"
if [ -f /tmp/dhcp.leases ]; then
while read ts mac ip name rest; do
[ -z "$mac" ] || [ -z "$ip" ] && continue
json_add_object ""
json_add_string "macaddr" "$mac"
json_add_string "ipaddr" "$ip"
[ "$name" != "*" ] && json_add_string "hostname" "$name"
json_add_int "expires" "$ts"
json_close_object
done < /tmp/dhcp.leases
fi
json_close_array
json_add_array "dhcp6_leases"
json_close_array
json_dump
;;
esac
;;
esac
设置执行权限并重启 rpcd:
bash
chmod +x /usr/libexec/rpcd/luci
chmod +x /usr/libexec/rpcd/luci-rpc
/etc/init.d/rpcd restart
踩坑记录
这是整个修复过程中最大的坑 。一开始以为 App 使用的是标准 LuCI RPC 接口(/cgi-bin/luci/rpc/auth、/cgi-bin/luci/rpc/sys 等),花了大量时间在 Lua 层面添加 sysinfo()、net.device_stats()、net.dhcp_leases() 等函数到 /usr/lib/lua/luci/sys.lua 中。结果 App 根本不调用这些端点。
教训:遇到 App 无法获取数据时,第一件事应该是拉取 App 源码看它到底调用了哪些接口,而不是猜测。
另一个坑是 sed -i 配合 heredoc 写入文件时,所有内容被压成一行。原因是 heredoc 中的换行符在 sed -i 的替换模式中被吞掉了。最终改用 base64 编码 + SSH exec 的方式上传文件才解决。
故障 4:Shell 脚本子 shell 问题导致 getMountPoints 和 getProcessList 返回空数据
现象
luci.getMountPoints 和 luci.getProcessList 返回 {"result": []},数组为空。
原因
经典的 shell 子进程陷阱。原始写法:
sh
# 错误写法
df -k | tail -n +2 | while read fs blocks used avail pct mount; do
json_add_object ""
# ...
json_close_object
done
cmd | while read ... 这种写法中,while 循环运行在管道创建的子 shell 中 。json_add_object 等 jshn 函数修改的是子 shell 中的 JSON 状态,父 shell 的 json_add_array / json_close_array / json_dump 看不到这些修改。结果就是输出一个空数组。
ps w | while read ... 同理。
修复
用临时文件替代管道,让 while 循环在主 shell 中执行:
sh
# 正确写法
df -k 2>/dev/null > /tmp/_df_out.txt
json_init
json_add_array "result"
while read fs blocks used avail pct mount; do
[ -z "$fs" ] && continue
[ "$fs" = "Filesystem" ] && continue
json_add_object ""
json_add_string "device" "$fs"
json_add_string "mount" "$mount"
json_add_int "size" $((blocks * 1024))
json_add_int "free" $((avail * 1024))
json_close_object
done < /tmp/_df_out.txt
rm -f /tmp/_df_out.txt
json_close_array
json_dump
getProcessList 同理,用 ps w > /tmp/_ps_out.txt 再 while read ... done < /tmp/_ps_out.txt。
踩坑记录
这个坑在 shell 编程中非常经典,但在配合 jshn.sh 的 JSON 构建函数时特别容易中招,因为 json_add_* 函数的输出不会报错,只是静默地写到了子 shell 的内存中,最终 json_dump 输出空对象。
诊断方法:在 while 循环内部加 echo "debug: $fs" >&2,如果 stderr 有输出但 stdout 的 JSON 为空,就确认是子 shell 问题。
故障 5:ps w 列格式与脚本解析不匹配
现象
getProcessList 修复子 shell 问题后,返回的数据字段错位:PPID 字段显示的是 root(用户名),USER 字段显示的是 2056(VSZ),STAT 字段显示的是 /sbin/procd(命令名)。
原因
脚本按 8 列解析 ps w 输出:
sh
read pid ppid user stat vsz mem cpu rest
但这个 OpenWrt 的 BusyBox ps w 实际输出只有 5 列:
PID USER VSZ STAT COMMAND
1 root 2056 S /sbin/procd
2 root 0 SW [kthreadd]
没有 PPID、%MEM、%CPU 列。8 个变量读 5 列数据,导致字段全部错位。
修复
改为 5 列解析:
sh
while read pid user vsz stat cmd; do
# ...
done
踩坑记录
不同 OpenWrt 版本和 BusyBox 配置下,ps w 的输出格式可能不同。有些版本有 PPID 列,有些没有。最可靠的方法是在目标设备上先执行 ps w | head -3 看一下实际列结构,而不是凭经验假设。
故障 6:rpcd-mod-file 和 rpcd-mod-iwinfo 的 .so 模块与 rpcd 不兼容
现象
登录成功后,调用 file.read 读取 /proc/sys/net/netfilter/nf_conntrack_count:
json
{"jsonrpc":"2.0","id":1,"result":[6]}
错误码 6 = 权限拒绝。所有 file.* 方法(file.read、file.stat、file.list、file.exec)全部返回同样的错误。
同时,iwinfo 对象在 ubus list 中完全不出现,尽管 rpcd-mod-iwinfo 包已安装。
原因
这是最诡异的一个问题。ACL 配置完全正确:
json
"file": ["read", "list", "stat", "md5", "exec"]
登录返回的 session ACL 也确认包含这些权限。但 rpcd-mod-file 的 .so 模块(版本 2025.09.01)内部有额外的访问控制检查逻辑,与这个路由器的基础系统(2017-2018 年的 LEDE 快照)不兼容。即使 ACL 授权正确,.so 模块内部的检查仍然失败。
rpcd-mod-iwinfo 同理,.so 模块加载失败(可能因为符号链接或依赖库版本不匹配),导致 iwinfo 对象从未注册到 ubus。
验证过程:
bash
# file.so 存在但所有方法返回权限拒绝
ubus call file read '{"path": "/tmp/test"}' # 返回 "Access denied"
# iwinfo.so 存在但对象不注册
ubus list | grep iwinfo # 无输出
# 检查 .so 文件
ls -la /usr/lib/rpcd/
# -rwxr-xr-x 1 root root 65919 Sep 2 2025 file.so
# -rwxr-xr-x 1 root root 65847 Sep 2 2025 iwinfo.so
修复
禁用不兼容的 .so 模块,用 shell 脚本处理器替代。
步骤 1:禁用 .so 模块
bash
mv /usr/lib/rpcd/file.so /usr/lib/rpcd/file.so.disabled
mv /usr/lib/rpcd/iwinfo.so /usr/lib/rpcd/iwinfo.so.disabled
步骤 2:创建 /usr/libexec/rpcd/file(完整代码)
sh
#!/bin/sh
. /usr/share/libubox/jshn.sh
case "$1" in
list)
cat <<EOF
{
"read":{"path":"String","base64":"Boolean"},
"write":{"path":"String","data":"String","append":"Boolean","mode":"Integer","base64":"Boolean"},
"list":{"path":"String"},
"stat":{"path":"String"},
"md5":{"path":"String"},
"exec":{"command":"String","params":"Array","env":"Table"}
}
EOF
;;
call)
case "$2" in
read)
read input
json_load "$input"
json_get_var path path
[ -z "$path" ] && exit 1
if [ -r "$path" ]; then
content=$(cat "$path" 2>/dev/null)
json_init
json_add_string "data" "$content"
json_dump
else
json_init
json_add_int "code" 6
json_dump
fi
;;
write)
read input
json_load "$input"
json_get_var path path
json_get_var data data
json_get_var append append
json_get_var mode mode
[ -z "$path" ] && exit 1
if [ "$append" = "true" ]; then
printf '%s' "$data" >> "$path"
else
printf '%s' "$data" > "$path"
fi
[ -n "$mode" ] && chmod "$mode" "$path" 2>/dev/null
json_init
json_add_boolean "" true
json_dump
;;
list)
read input
json_load "$input"
json_get_var path path
[ -z "$path" ] && path="/"
json_init
if [ -d "$path" ]; then
for entry in "$path"/*; do
[ -e "$entry" ] || continue
name=$(basename "$entry")
json_add_object "$name"
if [ -d "$entry" ]; then
json_add_int "TYPE" 1
else
json_add_int "TYPE" 0
[ -r "$entry" ] && json_add_int "SIZE" "$(wc -c < "$entry" 2>/dev/null)"
fi
json_close_object
done
fi
json_dump
;;
stat)
read input
json_load "$input"
json_get_var path path
[ -z "$path" ] && exit 1
if [ -e "$path" ]; then
json_init
json_add_string "path" "$path"
if [ -d "$path" ]; then
json_add_int "type" 1
else
json_add_int "type" 0
[ -r "$path" ] && json_add_int "size" "$(wc -c < "$path" 2>/dev/null)"
fi
json_add_int "mtime" "$(stat -c %Y "$path" 2>/dev/null || echo 0)"
json_dump
else
json_init
json_add_int "code" 6
json_dump
fi
;;
md5)
read input
json_load "$input"
json_get_var path path
[ -z "$path" ] && exit 1
if [ -r "$path" ]; then
md5=$(md5sum "$path" 2>/dev/null | awk '{print $1}')
json_init
json_add_string "md5" "$md5"
json_dump
else
json_init
json_add_int "code" 6
json_dump
fi
;;
exec)
read input
json_load "$input"
json_get_var command command
[ -z "$command" ] && exit 1
params=""
if json_get_type ptype params && [ "$ptype" = "array" ]; then
json_select params
idx=1
while json_get_type etype $idx && [ "$etype" = "string" ]; do
json_get_var val $idx
params="$params $val"
idx=$((idx + 1))
done
json_select ..
fi
json_get_type etype env 2>/dev/null
if [ "$etype" = "table" ]; then
json_select env
json_get_keys env_keys
for k in $env_keys; do
json_get_var v "$k"
export "$k=$v"
done
json_select ..
fi
output=$($command $params 2>&1)
rc=$?
json_init
json_add_int "code" $rc
json_add_string "stdout" "$output"
json_dump
;;
esac
;;
esac
步骤 3:创建 /usr/libexec/rpcd/iwinfo(完整代码)
sh
#!/bin/sh
. /usr/share/libubox/jshn.sh
case "$1" in
list)
cat <<EOF
{
"assoclist":{"device":"String"},
"freqlist":{"device":"String"},
"txpowerlist":{"device":"String"},
"scan":{"device":"String"},
"countrylist":{"device":"String"}
}
EOF
;;
call)
case "$2" in
assoclist)
read input
json_load "$input"
json_get_var device device
[ -z "$device" ] && exit 1
iwinfo "$device" assoclist 2>/dev/null > /tmp/_iwinfo_out.txt
json_init
json_add_array "results"
mac=""
signal=""
inactive=""
rx_rate=""
tx_rate=""
mhz=""
flush_client() {
[ -z "$mac" ] && return
json_add_object ""
json_add_string "mac" "$mac"
[ -n "$signal" ] && json_add_int "signal" "$signal"
[ -n "$inactive" ] && json_add_int "inactive" "$inactive"
json_add_object "rx"
if [ -n "$rx_rate" ]; then
rkbps=$(awk "BEGIN{printf \"%d\", $rx_rate * 1000}")
json_add_int "rate" "$rkbps"
else
json_add_int "rate" 0
fi
[ -n "$mhz" ] && json_add_int "mhz" "$mhz"
json_close_object
json_add_object "tx"
if [ -n "$tx_rate" ]; then
tkbps=$(awk "BEGIN{printf \"%d\", $tx_rate * 1000}")
json_add_int "rate" "$tkbps"
else
json_add_int "rate" 0
fi
[ -n "$mhz" ] && json_add_int "mhz" "$mhz"
json_close_object
json_close_object
mac=""
signal=""
inactive=""
rx_rate=""
tx_rate=""
mhz=""
}
while IFS= read -r line; do
nm=$(echo "$line" | grep -oE '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}')
if [ -n "$nm" ]; then
flush_client
mac="$nm"
signal=$(echo "$line" | sed 's/.* \(-\{0,1\}[0-9]*\) dBm.*/\1/')
[ "$signal" = "$line" ] && signal=""
inactive=$(echo "$line" | sed 's/.* \([0-9]*\) ms ago.*/\1/')
[ "$inactive" = "$line" ] && inactive=""
continue
fi
case "$line" in
*RX:*MBit/s*)
rx_rate=$(echo "$line" | sed 's/.*RX: \([0-9.]*\) MBit.*/\1/')
[ "$rx_rate" = "$line" ] && rx_rate=""
;;
esac
case "$line" in
*TX:*MBit/s*)
tx_rate=$(echo "$line" | sed 's/.*TX: \([0-9.]*\) MBit.*/\1/')
[ "$tx_rate" = "$line" ] && tx_rate=""
mhz=$(echo "$line" | sed 's/.* \([0-9]*\)MHz.*/\1/')
[ "$mhz" = "$line" ] && mhz=""
;;
esac
done < /tmp/_iwinfo_out.txt
flush_client
rm -f /tmp/_iwinfo_out.txt
json_close_array
json_dump
;;
*)
json_init
json_dump
;;
esac
;;
esac
步骤 4:设置权限并重启
bash
chmod +x /usr/libexec/rpcd/file
chmod +x /usr/libexec/rpcd/iwinfo
/etc/init.d/rpcd restart
踩坑记录
这个坑分两层。
第一层 :一开始以为是 ACL 配置问题,反复修改 ACL 文件,给 file 加了 "exec" 权限、加了 "*" 通配符,甚至尝试了 "file.read": ["*"] 这种写法,全部无效。直到用 ubus -v list file 确认 ACL 权限确实已授予,但调用仍然返回 6,才排除 ACL 问题。
第二层 :怀疑是 .so 模块内部问题后,尝试降级 rpcd-mod-file 包,但 opkg 源里只有 2025.09.01 一个版本,无法降级。最终方案是禁用 .so 模块,用 shell 脚本替代。
关键诊断命令:
bash
# 确认 ACL 是否生效
ubus -v list file # 查看对象的方法签名
# 确认 .so 模块是否加载
ubus list | grep file # 如果出现 "file" 说明对象已注册
# 确认是否是 .so 模块内部问题
# 如果 ubus list 有 "file" 但所有方法返回 6,且 ACL 正确,就是 .so 模块的问题
iwinfo 的特殊情况 :rpcd-mod-iwinfo 安装的是 /usr/lib/rpcd/iwinfo.so(共享库),而不是 /usr/libexec/rpcd/iwinfo(脚本)。rpcd 会同时扫描两个目录,但 .so 模块加载失败时不会有任何错误日志,静默失败。需要 ubus list | grep iwinfo 确认对象是否注册。
四、iwinfo 输出格式解析
iwinfo <device> assoclist 的原始输出格式如下:
10:91:A8:4D:8B:C8 -39 dBm / unknown (SNR -39) 680 ms ago
RX: 6.0 MBit/s 3760 Pkts.
TX: 72.2 MBit/s, MCS 7, 20MHz 549 Pkts.
expected throughput: unknown
CC:4D:75:2E:76:9A -30 dBm / unknown (SNR -30) 3750 ms ago
RX: 6.0 MBit/s 2302 Pkts.
TX: 72.2 MBit/s, MCS 7, 20MHz 391 Pkts.
expected throughput: unknown
每个客户端占 4-5 行:
- 第 1 行 :
MAC地址 信号强度 dBm / 噪声 (SNR 值) 不活跃时间 ms ago - 第 2 行 :
\tRX: 速率 MBit/s 数据包数 Pkts. - 第 3 行 :
\tTX: 速率 MBit/s, MCS X, YMHz 数据包数 Pkts. - 第 4 行 :
\texpected throughput: ... - 空行分隔下一个客户端
解析时需要注意:
- MAC 地址以行首匹配为准:
^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2} - 信号强度是第一个
dBm前的数字:sed 's/.* \(-\{0,1\}[0-9]*\) dBm.*/\1/' - 不活跃时间在
ms ago前:sed 's/.* \([0-9]*\) ms ago.*/\1/' - RX/TX 速率在
MBit/s前,注意是 MBit/s 不是 Mbit/s - 频宽在
MHz前:sed 's/.* \([0-9]*\)MHz.*/\1/' - 同样要避免子 shell 问题,先输出到临时文件再逐行读取
五、最终验证结果
所有 17 个 App 依赖的 ubus 端点全部通过:
=== HOME PAGE ===
[OK] system.board
[OK] system.info
[OK] luci.getCPUUsage
[OK] luci.getOnlineUsers
[OK] file.read(conntrack_count) → {"data": "542"}
[OK] file.read(conntrack_max) → {"data": "65535"}
[OK] network.interface.dump
[OK] luci.getTempInfo → {"tempinfo": "N/A C"}
[OK] luci.getMountPoints → 5 个挂载点
=== STATISTICS PAGE ===
[OK] getRealtimeStats(interface) → eth1 实时流量
[OK] getRealtimeStats(load) → 负载数据
=== CLIENT PAGE ===
[OK] luci-rpc.getDHCPLeases → 10 条 DHCP 租约
[OK] iwinfo.assoclist(wlan0) → 5 个无线客户端
[OK] iwinfo.assoclist(wlan1) → 3 个无线客户端
=== NETWORK PAGE ===
[OK] luci-rpc.getNetworkDevices
[OK] luci-rpc.getWirelessDevices
=== PROCESS LIST ===
[OK] luci.getProcessList → 110 个进程
六、最终文件清单
所有修改/新增的文件:
| 文件路径 | 操作 | 说明 |
|---|---|---|
/usr/libexec/rpcd/luci |
新增 | rpcd handler,提供 CPU/内存/温度/挂载点/流量/进程列表 |
/usr/libexec/rpcd/luci-rpc |
新增 | rpcd handler,提供网络设备/无线状态/DHCP 租约 |
/usr/libexec/rpcd/file |
新增 | 替代 rpcd-mod-file.so,提供文件读写/列表/stat/md5/exec |
/usr/libexec/rpcd/iwinfo |
新增 | 替代 rpcd-mod-iwinfo.so,提供无线客户端关联列表 |
/usr/share/rpcd/acl.d/luci-mod-rpc.json |
修改 | 添加所有 App 需要的 ubus 对象权限 |
/usr/lib/rpcd/file.so → file.so.disabled |
禁用 | 版本不兼容,重命名禁用 |
/usr/lib/rpcd/iwinfo.so → iwinfo.so.disabled |
禁用 | 版本不兼容,重命名禁用 |
无需修改的配置:
/etc/config/uhttpd---option ubus_prefix '/ubus'已正确配置/usr/lib/lua/luci/sys.lua--- 之前的修改(sysinfo/net.device_stats/net.dhcp_leases)现在不再需要,因为 rpcd handler 直接提供数据
七、总结与教训
核心问题
这不是一个单一故障,而是 6 个独立问题叠加:
- uhttpd 版本不匹配 → 400 错误
- ACL 权限配置遗漏子对象 → Access denied
- App 使用自定义 ubus 对象 → Object not found
- Shell 脚本子 shell 陷阱 → 空数据
- ps 命令列格式假设错误 → 字段错位
- rpcd .so 模块版本不兼容 → 权限拒绝/对象不注册
关键教训
- 先看 App 源码,确认它调用的具体接口格式,不要猜测。
curl -v是排查 HTTP 问题的第一工具,能看到状态码、请求头、响应头。- ubus ACL 中
*不是文件系统通配符,只表示"该对象的所有方法"。子对象必须显式列出。 cmd | while read创建子 shell ,在需要修改父 shell 变量(包括 jshn JSON 状态)时,必须用while read ... done < file或here-string。- 不要假设
ps的列结构,先在目标设备上验证。 - .so 模块加载失败是静默的 ,没有错误日志,必须用
ubus list确认对象是否注册。 - 包管理器升级单个组件可能导致版本不匹配 ,尤其是
uhttpd和uhttpd-mod-ubus、rpcd和rpcd-mod-*这种强耦合的组合。
更多
待完善 1. OpenWRT/LEDE Snapshot 更换软件源