惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
博客园 - 【当耐特】
Cloudbric
Cloudbric
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Attack and Defense Labs
Attack and Defense Labs
爱范儿
爱范儿
The Cloudflare Blog
腾讯CDC
Security Archives - TechRepublic
Security Archives - TechRepublic
TaoSecurity Blog
TaoSecurity Blog
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
C
Check Point Blog
Schneier on Security
Schneier on Security
S
Schneier on Security
J
Java Code Geeks
B
Blog RSS Feed
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
Stack Overflow Blog
Stack Overflow Blog
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
A
About on SuperTechFans
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
罗磊的独立博客
A
Arctic Wolf
S
Secure Thoughts
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security
量子位
U
Unit 42
I
InfoQ
D
DataBreaches.Net
P
Privacy International News Feed
T
Troy Hunt's Blog
博客园 - 叶小钗
T
Threatpost
博客园 - Franky
K
Kaspersky official blog
Hugging Face - Blog
Hugging Face - Blog
IT之家
IT之家
www.infosecurity-magazine.com
www.infosecurity-magazine.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cisco Blogs

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
精简版 OpenWrt/LEDE uhttpd/rpc/mod-rpc/ Ubus Json-RPC 从0修复直到可用
AsulTop · 2026-06-27 · via 博客园_首页

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

现象

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 无法正确处理请求。

修复

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 返回:

{"jsonrpc":"2.0","id":1,"result":[6]}

错误码 6 = UBUS_STATUS_PERMISSION_DENIED

原因

原始 ACL 文件 /usr/share/rpcd/acl.d/luci-mod-rpc.json 中的 ubus 权限配置过于笼统:

"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 文件,添加所有需要的子对象:

{
    "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:

/etc/init.d/rpcd restart

踩坑记录

这个坑非常隐蔽。一开始以为 "network": ["*"] 中的 * 是通配符,能匹配 network.* 的所有子对象。实际上 ubus ACL 中的 * 只表示"该对象的所有方法",不表示"所有子对象"network.interface.wan 在 ubus 看来是一个完整的对象名,和 network 没有父子关系。

验证方法:

ubus list          # 列出所有 ubus 对象
ubus -v list file  # 查看某个对象的方法签名

必须把 App 需要调用的每一个 ubus 对象名都在 ACL 中显式列出。


故障 3:App 使用的自定义 ubus 对象 luciluci-rpc 不存在

现象

ACL 修复后,登录成功,但 App 调用 luci.getCPUUsageluci.getOnlineUsersluci-rpc.getDHCPLeases 等方法时,返回:

{"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(完整代码)
#!/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(完整代码)
#!/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:

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 子进程陷阱。原始写法:

# 错误写法
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 中执行:

# 正确写法
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 输出:

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 列解析:

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

{"jsonrpc":"2.0","id":1,"result":[6]}

错误码 6 = 权限拒绝。所有 file.* 方法(file.readfile.statfile.listfile.exec)全部返回同样的错误。

同时,iwinfo 对象在 ubus list 中完全不出现,尽管 rpcd-mod-iwinfo 包已安装。

原因

这是最诡异的一个问题。ACL 配置完全正确:

"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。

验证过程:

# 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 模块
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(完整代码)
#!/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(完整代码)
#!/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:设置权限并重启
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 脚本替代。

关键诊断命令

# 确认 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/uhttpdoption 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 交叉编译器配置