精简版 OpenWrt/LEDE uhttpd/rpc/mod-rpc/ Ubus Json-RPC 从0修复直到可用

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

升级后 uhttpduhttpd-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.wannetwork.devicenetwork.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 对象 luciluci-rpc 不存在

现象

ACL 修复后,登录成功,但 App 调用 luci.getCPUUsageluci.getOnlineUsersluci-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 对象:提供 getCPUUsagegetOnlineUsersgetTempInfogetMountPointsgetRealtimeStatsgetProcessList 方法
  • luci-rpc 对象:提供 getNetworkDevicesgetWirelessDevicesgetDHCPLeases 方法

这两个对象在标准 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 问题导致 getMountPointsgetProcessList 返回空数据

现象

luci.getMountPointsluci.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.txtwhile 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-filerpcd-mod-iwinfo 的 .so 模块与 rpcd 不兼容

现象

登录成功后,调用 file.read 读取 /proc/sys/net/netfilter/nf_conntrack_count

json 复制代码
{"jsonrpc":"2.0","id":1,"result":[6]}

错误码 6 = 权限拒绝。所有 file.* 方法(file.readfile.statfile.listfile.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: ...
  • 空行分隔下一个客户端

解析时需要注意:

  1. MAC 地址以行首匹配为准:^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}
  2. 信号强度是第一个 dBm 前的数字:sed 's/.* \(-\{0,1\}[0-9]*\) dBm.*/\1/'
  3. 不活跃时间在 ms ago 前:sed 's/.* \([0-9]*\) ms ago.*/\1/'
  4. RX/TX 速率在 MBit/s 前,注意是 MBit/s 不是 Mbit/s
  5. 频宽在 MHz 前:sed 's/.* \([0-9]*\)MHz.*/\1/'
  6. 同样要避免子 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.sofile.so.disabled 禁用 版本不兼容,重命名禁用
/usr/lib/rpcd/iwinfo.soiwinfo.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 个独立问题叠加

  1. uhttpd 版本不匹配 → 400 错误
  2. ACL 权限配置遗漏子对象 → Access denied
  3. App 使用自定义 ubus 对象 → Object not found
  4. Shell 脚本子 shell 陷阱 → 空数据
  5. ps 命令列格式假设错误 → 字段错位
  6. rpcd .so 模块版本不兼容 → 权限拒绝/对象不注册

关键教训

  1. 先看 App 源码,确认它调用的具体接口格式,不要猜测。
  2. curl -v 是排查 HTTP 问题的第一工具,能看到状态码、请求头、响应头。
  3. ubus ACL 中 * 不是文件系统通配符,只表示"该对象的所有方法"。子对象必须显式列出。
  4. cmd | while read 创建子 shell ,在需要修改父 shell 变量(包括 jshn JSON 状态)时,必须用 while read ... done < filehere-string
  5. 不要假设 ps 的列结构,先在目标设备上验证。
  6. .so 模块加载失败是静默的 ,没有错误日志,必须用 ubus list 确认对象是否注册。
  7. 包管理器升级单个组件可能导致版本不匹配 ,尤其是 uhttpduhttpd-mod-ubusrpcdrpcd-mod-* 这种强耦合的组合。

更多

待完善 1. OpenWRT/LEDE Snapshot 更换软件源

待完善 2. OpenWRT/LEDE CPP17 以上运行库修复

待完善 3. OpenWRT/LEDE 交叉编译器配置

相关推荐
小胖xiaopangss10 天前
BRpc使用
c++·rpc
zhuzicc11 天前
Dubbo @Autowired 注入同模块接口,到底走的是本地调用还是 RPC?源码给你答案(Dubbo @Service注解的双重注册机制)
rpc·autowired·dubbo·依赖注入·java面试·spring ioc·dubbo源码分析
leo_yu_yty11 天前
Go语言分布式计算(RPC入门)
网络·网络协议·rpc
酣大智11 天前
RIP路由协议
网络·路由器·路由·rip
衣乌安、12 天前
JSON-RPC协议
网络协议·rpc·json
276695829212 天前
泡泡玛特app 腾讯企业加固/支付宝加固脱修frida rpc调用
网络·网络协议·rpc·frida·泡泡玛特·ppmt·泡泡玛特app-rpc调用
InHand云飞小白15 天前
连锁门店网络困境?5G Wi-Fi 6边缘路由器赋能分布式企业
网络·5g·路由器·网络运维·5g路由器·5gcpe·连锁联网
白露与泡影15 天前
为什么 RPC 要比 HTTP 更快?我:之前项目只用过 HTTP...
网络协议·http·rpc
skywalker_1115 天前
SpringBoot速通(实战教学)
java·spring boot·redis·rpc·ssm·mybatis-plus