feat(panel, netmgr §11): MeowPanel 网络管理页—netmgrd 期2 面板侧

后端(patch_v2/meowpanel/meowpanel.c,/api/audio 同款透传模式):
- GET /api/net/status    → 0xE0 概览 JSON(接口表 + WiFi/BT 摘要 + 速率)
- GET /api/net/conns?entity= → 0xE8 连接表(解析 entity|本端|远端|状态|寿命|域名 → JSON,transit 截断标记)
- GET /api/net/dns       → 0xEC status(mode/enabled/upstream/缓存命中/查询/deny/resolv_modified)
- POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 身份由 netmgrd 判定)
- GET /api/net/log       → TF MeowOS/logs/ 最新 net_*.log tail(256KB 上限,1000 行)
- netmgrd 不在/超时/应答异常 → 503 降级 JSON(NET_UNAVAILABLE),面板其它页不受影响
- 新增 --sock-net / env MEOW_NET_SOCK(宿主/qemu 测试用)

前端(web/,MD3/MIUIx 双主题沿用 270 落地件):
- 侧栏新增「网络」入口(#/net);接口卡 + WiFi 摘要 + 蓝牙摘要 + 每实体连接表
  (entity 前缀筛选 + 5s 轮询)+ DNS 代理卡(统计 + 清缓存按钮)+ 日志查看器
- netmgrd 不在 → 显示「网络管理服务不可用」占位 + 重试,不白屏不报错栈

验证:宿主 cc -Wall -Wextra 0 warning;mipsel 交叉编译 0 warning;假 netmgrd IPC 桩
(Python 帧级桩)联调五端点含 entity 过滤/flush 成功与 403/503 降级全通过;headless
Chromium 渲染 #/net(服务在线/离线两态)与 #/perf #/audio 无回归。
后端起二进制 md5: 21c70046476d3bf63e76f3d63e9eca2c(mipsel 静态)。

遗留(主会话部署时决策):netmgrd flush 写级依 SO_PEERCRED 判系统身份,面板 exe 位于
/usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/(非 /usr/bin/meow_* 亦非 os.meow.* 包)→ 真机
上 flush 会被 netmgrd 拒(403)。需在 netmgrd classify_exe 增认面板路径,或接受 403 只读。
不部署到设备(留主会话热更)。
This commit is contained in:
MeowOS committed 2026-09-01 21:01:00 +08:00
1 parent 52edf0c9e4
commit d8eeef5c57
6 files changed
+484 -2

No files matched your search

+251 -1
View File
@@ -93,6 +93,7 @@ static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notificat
#define POWER_SOCK "/usr/data/meow_power.sock"
#define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
#define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
@@ -125,6 +126,7 @@ static const char *g_sock_perf = PERF_SOCK;
static const char *g_sock_power = POWER_SOCK;
static const char *g_sock_launcher = LAUNCHER_SOCK;
static const char *g_sock_audio = AUDIO_SOCK;
static const char *g_sock_net = NET_SOCK; /* meow_netmgrd(--sock-net / env MEOW_NET_SOCK 覆盖) */
/* ---------------- 增量帧缓存(GET /api/screen.jpeg?delta=1,阶段 C 增强) ----------------
* 服务端缓存上一帧紧凑 RGB565,按 8×8 块比对,只编码变化块(外接 bbox 子图)并以响应头回传偏移。
@@ -234,6 +236,8 @@ static int send_err(int sock, int status, const char *code, const char *msg);
static int send_json(int sock, int status, const char *data);
static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
static void panel_log(const char *fmt, ...);
static int qparam(const char *query, const char *key, char *out, int outcap); /* 定义在文件后部;网络连接表 query 取值 */
static int tf_present(void); /* 定义在文件后部;net 日志卡根判定 */
static const char *web_root(void); /* 定义在文件后部;此处前向声明供鉴权会话路径使用 */
/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */
@@ -976,6 +980,242 @@ static int api_audio(int sock)
return rc;
}
/* ---------------- 网络管理:meow_netmgrd IPC(MSG_NET_*,期2 观测层/DNS 代理) ----------------
* 收口于独立 socket /usr/data/meow_net.sock(--sock-net / env MEOW_NET_SOCK 覆盖,宿主测试用)。
* 读级(STATUS/CONNS/DNS status|stats)netmgrd 放行任意调用者;flush 写级——HTTP 层沿用面板
* 鉴权门禁(require_auth),IPC 层身份由 netmgrd 依 SO_PEERCRED 判定(面板路径非系统身份时
* netmgrd 会拒 flush,面板如实回传其错误码/文本,不掩盖)。
* netmgrd 不在/超时/应答异常 → 503 降级 JSON({"ok":false,...}),面板其它页不受影响。 */
static int net_ipc(uint16_t req_type, const void *req, uint32_t req_len,
uint16_t *ret_type, uint32_t *code, uint16_t *meta,
char *out, size_t cap)
{
int fd = ipc_connect_tmo(g_sock_net, IPC_TIMEOUT_MS);
if (fd < 0) return -1;
uint8_t buf[MEOW_IPC_MAX];
uint16_t type = 0; uint32_t rid = 0;
int rlen = -1;
if (send_frame(fd, req_type, 0x1020, req, req_len) == 0)
rlen = recv_frame(fd, &type, &rid, buf, sizeof buf);
close(fd);
if (rlen <= 0) return -1;
if (ret_type) *ret_type = type;
/* m_net_*_result 统一布局:code(u32)|len(u16)|meta/pad(u16)|content[len],content 偏移 8 */
uint32_t c = 0; uint16_t ln = 0, mv = 0;
memcpy(&c, buf, 4); memcpy(&ln, buf + 4, 2); memcpy(&mv, buf + 6, 2);
if (code) *code = c;
if (meta) *meta = mv;
if ((size_t)ln >= cap) ln = (uint16_t)(cap - 1);
if ((size_t)(8 + ln) < sizeof buf) buf[8 + ln] = 0;
memcpy(out, buf + 8, ln);
out[ln] = 0;
return (int)ln;
}
/* GET /api/net/status → 透传 0xE0 概览 JSON(接口表 + WiFi/BT 摘要 + 接口速率)。 */
static int api_net_status(int sock)
{
uint16_t t; uint32_t code; char json[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json);
if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0])
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
return send_json(sock, 200, json);
}
/* 连接表一行:entity|lip:lport|rip:rport|state|age_s|domain(netmgrd build_conns_text)。
* 把每条构造成 `{...}` 对象、逗号分隔(不含外层 [])写入 sbuf,返回条数。 */
static int net_conns_parse_body(sbuf_t *s, const char *txt)
{
const char *p = txt;
int first = 1, n = 0;
while (p && *p) {
const char *nl = strchr(p, '\n');
size_t ll = nl ? (size_t)(nl - p) : strlen(p);
if (ll == 0) { p = nl ? nl + 1 : p + 1; continue; }
char line[384];
if (ll >= sizeof line) ll = sizeof line - 1;
memcpy(line, p, ll); line[ll] = 0;
char *f[6]; int fn = 0; char *cur = line;
while (fn < 6) {
f[fn++] = cur;
char *bar = strchr(cur, '|');
if (!bar) break;
*bar = 0; cur = bar + 1;
}
if (fn >= 6) {
if (!first) sA(s, ",");
first = 0;
sA(s, "{\"entity\":"); sA_str(s, f[0]);
sA(s, ",\"local\":"); sA_str(s, f[1]);
sA(s, ",\"remote\":"); sA_str(s, f[2]);
sA(s, ",\"state\":"); sA_str(s, f[3]);
sA(s, ",\"age\":%lld,\"domain\":", atoll(f[4]));
sA_str(s, strcmp(f[5], "-") == 0 ? "" : f[5]);
sA(s, "}");
n++;
}
p = nl ? nl + 1 : p + ll;
}
return n;
}
/* GET /api/net/conns?entity=<可选> → 0xE8 → 连接表 JSON(可实体前缀过滤)。 */
static int api_net_conns(int sock, const char *query)
{
m_net_conns_req_t q; memset(&q, 0, sizeof q);
char ent[128];
if (qparam(query, "entity", ent, sizeof ent))
strncpy(q.entity, ent, sizeof q.entity - 1);
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_CONNS_REQ, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_CONNS_RESULT || code != MEOW_OK)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
sbuf_t a = {0};
int count = net_conns_parse_body(&a, txt);
sbuf_t s = {0};
sA(&s, "{\"truncated\":%s,\"count\":%d,\"conns\":[", meta ? "true" : "false", count);
if (a.n) sA_append(&s, a.b, a.n);
sA(&s, "]}");
sbuf_free(&a);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* GET /api/net/dns → 0xEC status(netmgrd 对 status/stats 同输出)→ DNS 代理统计 JSON。 */
static int api_net_dns(int sock)
{
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
strcpy(q.cmd, "status");
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_DNS_RESULT)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (code != MEOW_OK)
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS 代理不可用");
/* txt: `dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d` */
char mode[32] = "", upstream[64] = "";
int enabled = 0, ch = 0, ct = 0, rmod = 0;
unsigned long long qq = 0, deny = 0, first = 0;
(void)sscanf(txt, "dns mode=%31s enabled=%d upstream=%63s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d",
mode, &enabled, upstream, &ch, &ct, &qq, &deny, &first, &rmod);
sbuf_t s = {0};
sA(&s, "{\"mode\":");
sA_str(&s, mode);
sA(&s, ",\"enabled\":%s,\"upstream\":", enabled ? "true" : "false");
sA_str(&s, upstream);
sA(&s, ",\"cache_hits\":%d,\"cache_total\":%d,\"queries\":%llu,\"denies\":%llu,\"first_seen\":%llu,\"resolv_modified\":%s}",
ch, ct, qq, deny, first, rmod ? "true" : "false");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 层由 netmgrd 依身份判定)。 */
static int api_net_dns_flush(int sock)
{
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
strcpy(q.cmd, "flush");
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
if (n <= 0 || t != MSG_NET_DNS_RESULT)
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
if (code == (uint32_t)MEOW_ERR_PERM)
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "DNS flush 需系统身份");
if (code != MEOW_OK)
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS flush 失败");
/* txt: `dns cache flushed (N entries)` */
int entries = 0;
(void)sscanf(txt, "dns cache flushed (%d entries)", &entries);
sbuf_t s = {0};
sA(&s, "{\"flushed\":true,\"entries\":%d}", entries);
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
#define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限(内存约束:不整读 64MB 滚动文件) */
/* GET /api/net/log → 读 TF MeowOS/logs/ 最新的 net_<bootts>.log 尾部(同日志页模式)。 */
static int api_net_log(int sock)
{
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
DIR *d = opendir(LOGS_DIR);
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
char best[512] = ""; long best_mt = -1;
struct dirent *e;
while ((e = readdir(d))) {
if (strncmp(e->d_name, "net_", 4) != 0) continue;
if (e->d_name[0] == '.') continue;
char full[1024];
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
struct stat st;
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
}
closedir(d);
if (!best[0])
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
char full[1024];
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
int fd = open(full, O_RDONLY);
struct stat st;
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
if (fd >= 0) close(fd);
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
}
off_t off = st.st_size > NET_LOG_TAIL_MAX ? st.st_size - NET_LOG_TAIL_MAX : 0;
size_t buflen = (size_t)(st.st_size - off);
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
if (off) (void)lseek(fd, off, SEEK_SET);
char *buf = malloc(buflen + 1);
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
ssize_t n = read(fd, buf, buflen);
close(fd);
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
sbuf_t s = {0};
sA(&s, "{\"file\":");
sA_str(&s, best);
sA(&s, ",\"lines\":[");
const char *p = buf, *end = buf + n;
int linecnt = 0;
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
if (linecnt > 0 && end[-1] != '\n') linecnt++;
int maxlines = LOG_MAX_LINES;
int startrow = linecnt - maxlines;
if (startrow < 0) startrow = 0;
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
int rowidx = 0, gotlines = 0;
p = buf;
while (p < end) {
const char *nl = memchr(p, '\n', (size_t)(end - p));
const char *line_end = nl ? nl : end;
if (rowidx >= startrow) {
if (gotlines) sA(&s, ",");
sbuf_t ln = {0};
for (const char *c = p; c < line_end; c++) {
unsigned char cc = (unsigned char)*c;
if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc);
else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc);
else sA(&ln, "%c", (int)cc);
}
sA(&s, "\"");
for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c);
sA(&s, "\"");
sbuf_free(&ln);
gotlines++;
}
rowidx++;
if (!nl) break;
p = nl + 1;
}
free(buf);
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
int rc = send_json(sock, 200, s.b ? s.b : "");
sbuf_free(&s);
return rc;
}
/* ---------------- 路径安全 ---------------- */
static int tf_present(void)
{
@@ -4456,6 +4696,12 @@ static int handle(int sock, req_t *r)
/* ---- 音频状态(meow_audiod 快照,只读) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
/* ---- 网络管理(meow_netmgrd 期2 透传:读级任意调用者;flush 写级走 require_auth) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
/* ---- 实时画面(阶段 C) ---- */
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
return api_screen_jpeg(sock, r);
@@ -4473,6 +4719,7 @@ static int handle(int sock, req_t *r)
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock);
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
char seg[256];
@@ -4585,10 +4832,11 @@ int main(int argc, char **argv)
else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i];
else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i];
else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i];
else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i];
else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; }
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
printf("MeowPanel backend %s\n", PANEL_VERSION);
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P]\n");
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P]\n");
return 0;
}
}
@@ -4601,6 +4849,8 @@ int main(int argc, char **argv)
if (env && env[0]) g_sock_launcher = env;
env = getenv("MEOW_AUDIO_SOCK");
if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */
env = getenv("MEOW_NET_SOCK");
if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */
env = getenv("MEOW_SETTINGS_FILE");
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */