/* mp_net.c — 网络管理域:meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)。 */ #include "mp.h" #define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限 */ 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; } 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", "网络管理服务不可用"); if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */ return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节"); return send_json(sock, 200, json); } 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; } 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; } int net_dns_obj_fill(sbuf_t *s) { 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 -1; if (code != MEOW_OK) return -2; /* 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); 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"); return 0; } int api_net_dns(int sock) { sbuf_t s = {0}; int rc = net_dns_obj_fill(&s); if (rc == -1) { sbuf_free(&s); return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); } if (rc == -2) { sbuf_free(&s); return send_err(sock, 503, "NET_NODEV", "DNS 代理不可用"); } int out = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return out; } int net_block_fill(sbuf_t *s) { uint16_t nt; uint32_t ncode; char njson[MEOW_IPC_MAX]; int nn = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &nt, &ncode, NULL, njson, sizeof njson); if (nn <= 0 || nt != MSG_NET_STATUS_RESULT || ncode != MEOW_OK || !njson[0]) return -1; /* 状态 JSON 为对象:剥外层大括号,拼入 DNS 子键(DNS 不可用则省略该键,仍回 net 状态,不谎报)。 */ size_t l = strlen(njson); while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0; if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1; if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */ njson[l - 1] = 0; /* 去尾部 } */ sbuf_t dns = {0}; int drc = net_dns_obj_fill(&dns); sA(s, ",\"net\":{"); sA(s, "%s", njson + 1); /* 去首 { 的状态内层 */ if (drc == 0 && dns.b) sA(s, ",\"dns\":%s", dns.b); sA(s, "}"); /* net 块只开了一个 {,一个 } 收尾(批2 曾写 }} 多一括号,浏览器 Extra data 破页 2026-09-02) */ sbuf_free(&dns); return 0; } 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; } /* ---- F1-③ 蓝牙只读详情(FUTURE_MEOWPANEL §2.6):netmgrd MSG_NET_BT_CTL(0xE6) 读级五命令 * (status/paired/connected/a2dp/devices)聚合为一份 JSON。读级任意调用者(bt_cmd_is_write=读), * 无阻塞(net_ipc 2s 超时兜底);子命令失败/坏字节 → 该子块空数组降级,绝不拖垮整卡、绝不发非法 JSON。 ---- */ /* 单发 0xE6 读命令;成功返回文本长度(>0),IPC/服务端失败 -1,坏 UTF-8 -2(该子块降级)。 */ static int bt_ctl_read(const char *cmd, char *out, size_t cap) { m_net_cmd_t q; memset(&q, 0, sizeof q); snprintf(q.cmd, sizeof q.cmd, "%s", cmd); uint16_t t; uint32_t code; int n = net_ipc(MSG_NET_BT_CTL, &q, sizeof q, &t, &code, NULL, out, cap); if (n <= 0 || t != MSG_NET_BT_RESULT || code != MEOW_OK) return -1; if (!utf8_valid_n(out, (size_t)n)) return -2; return n; } static int bt_mac_ok(const char *m) { for (int i = 0; i < 17; i++) { if (i % 3 == 2) { if (m[i] != ':') return 0; } else if (!is_hex(m[i])) return 0; } return 1; } /* "Device " 行集 → JSON 数组项(不合法行跳过;名经 sA_str_n 消毒)。返回条数。 */ static int bt_devlines_json(sbuf_t *s, const char *txt, int max) { int n = 0; const char *ln = txt; while (ln && *ln && n < max) { const char *nl = strchr(ln, '\n'); size_t ll = nl ? (size_t)(nl - ln) : strlen(ln); const char *p = ln; while ((size_t)(p - ln) < ll && (*p == ' ' || *p == '\t')) p++; if ((size_t)(ln + ll - p) >= 7 + 17 && !strncmp(p, "Device ", 7) && bt_mac_ok(p + 7)) { const char *mac = p + 7; const char *nm = mac + 17; while ((size_t)(nm - ln) < ll && (*nm == ' ' || *nm == '\t')) nm++; size_t nlen = ll - (size_t)(nm - ln); while (nlen && (nm[nlen - 1] == ' ' || nm[nlen - 1] == '\t' || nm[nlen - 1] == '\r')) nlen--; if (n) sA(s, ","); sA(s, "{\"mac\":"); sA_str_n(s, mac, 17); sA(s, ",\"name\":"); sA_str_n(s, nm, nlen); sA(s, "}"); n++; } ln = nl ? nl + 1 : NULL; } return n; } /* 行内 " key=value"(空白/行尾收尾)取值。 */ static void bt_line_kv(const char *line, const char *key, char *out, size_t cap) { out[0] = 0; const char *v = strstr(line, key); if (!v) return; v += strlen(key); size_t i = 0; while (v[i] && v[i] != ' ' && v[i] != '\t' && v[i] != '\n' && v[i] != '\r' && i + 1 < cap) { out[i] = v[i]; i++; } out[i] = 0; } /* "A2DP codec=.. rate=.. channels=.. bits=.. acquired=<0|1> role=<..>" 行集 → JSON 数组项。 */ static int bt_a2dp_json(sbuf_t *s, const char *txt, int max) { int n = 0; const char *ln = txt; while (ln && *ln && n < max) { const char *nl = strchr(ln, '\n'); size_t ll = nl ? (size_t)(nl - ln) : strlen(ln); char line[320]; if (ll >= sizeof line) ll = sizeof line - 1; memcpy(line, ln, ll); line[ll] = 0; if (!strncmp(line, "A2DP ", 5) && strncmp(line, "A2DP none", 9) && bt_mac_ok(line + 5)) { char codec[24], rate[16], chn[8], bits[8], acq[8], role[8]; bt_line_kv(line, " codec=", codec, sizeof codec); bt_line_kv(line, " rate=", rate, sizeof rate); bt_line_kv(line, " channels=", chn, sizeof chn); bt_line_kv(line, " bits=", bits, sizeof bits); bt_line_kv(line, " acquired=", acq, sizeof acq); bt_line_kv(line, " role=", role, sizeof role); if (n) sA(s, ","); sA(s, "{\"mac\":"); sA_str_n(s, line + 5, 17); sA(s, ",\"codec\":"); sA_str(s, codec); sA(s, ",\"rate\":"); sA_str(s, rate); sA(s, ",\"channels\":"); sA_str(s, chn); sA(s, ",\"bits\":"); sA_str(s, bits); sA(s, ",\"acquired\":%s", !strcmp(acq, "1") ? "true" : "false"); sA(s, ",\"role\":"); sA_str(s, role); sA(s, "}"); n++; } ln = nl ? nl + 1 : NULL; } return n; } /* 五读文本聚合(任一 NULL=该子块空数组;st=NULL → {"available":false})。 */ int net_bt_aggregate(const char *st, const char *paired, const char *connected, const char *a2dp, const char *devices, sbuf_t *s) { if (!st) { sA(s, "{\"available\":false}"); return -1; } int avail = 0, powered = 0; long long paired_n = -1, connected_n = -1; char addr[24] = ""; (void)sscanf(st, "bt available=%d powered=%d address=%23s paired=%lld connected=%lld", &avail, &powered, addr, &paired_n, &connected_n); if (!strcmp(addr, "-")) addr[0] = 0; sA(s, "{\"available\":%s", avail ? "true" : "false"); sA(s, ",\"powered\":%s", powered ? "true" : "false"); sA(s, ",\"address\":"); sA_str(s, addr); if (paired_n >= 0) sA(s, ",\"paired_n\":%lld", paired_n); else sA(s, ",\"paired_n\":null"); if (connected_n >= 0) sA(s, ",\"connected_n\":%lld", connected_n); else sA(s, ",\"connected_n\":null"); sA(s, ",\"paired\":["); if (paired) bt_devlines_json(s, paired, 32); sA(s, "]"); sA(s, ",\"connected\":["); if (connected) bt_devlines_json(s, connected, 16); sA(s, "]"); sA(s, ",\"a2dp\":["); if (a2dp) bt_a2dp_json(s, a2dp, 8); sA(s, "]"); sA(s, ",\"nearby\":["); if (devices) bt_devlines_json(s, devices, 16); sA(s, "]"); sA(s, "}"); return 0; } int api_net_bt(int sock) { char st[MEOW_IPC_MAX], pa[MEOW_IPC_MAX], co[MEOW_IPC_MAX], a2[MEOW_IPC_MAX], dv[MEOW_IPC_MAX]; int ns = bt_ctl_read("status", st, sizeof st); if (ns <= 0) /* netmgrd 不可达/status 坏字节 → 整体降级(net 域惯例:200 available:false) */ return send_json(sock, 200, "{\"available\":false}"); int np = bt_ctl_read("paired", pa, sizeof pa); int nc = bt_ctl_read("connected", co, sizeof co); int na = bt_ctl_read("a2dp", a2, sizeof a2); int nd = bt_ctl_read("devices", dv, sizeof dv); sbuf_t s = {0}; net_bt_aggregate(st, np > 0 ? pa : NULL, nc > 0 ? co : NULL, na > 0 ? a2 : NULL, nd > 0 ? dv : NULL, &s); /* 放行前自校验(与 overview 同口径:浏览器 JSON.parse 不容忍尾随垃圾/括号错位/内嵌 NUL/坏字节) */ jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2)); int bad = !s.b || !chk || (strlen(s.b) != s.n) || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n); if (chk) jv_free(chk); if (bad) { FILE *df = fopen("/tmp/net_bt_bad.json", "w"); if (df && s.b) { fwrite(s.b, 1, s.n, df); fclose(df); } panel_log("net/bt: SELF-CHECK fail, raw dumped /tmp/net_bt_bad.json"); sbuf_free(&s); return send_err(sock, 500, "INTERNAL", "蓝牙聚合输出未通过 JSON 自校验(原始数据已留 /tmp/net_bt_bad.json)"); } int rc = send_json(sock, 200, s.b); sbuf_free(&s); return rc; } 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, ","); /* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */ sA_str_n(&s, p, (size_t)(line_end - p)); 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; } int api_net_ifaces(int sock) { m_net_iface_req_t q; memset(&q, 0, sizeof q); uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr); if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0]) return send_json(sock, 200, "{\"available\":false}"); /* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */ if (!utf8_valid_n(arr, strlen(arr))) return send_json(sock, 200, "{\"available\":false}"); /* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */ uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX]; int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj); long long rssi = -9999; int got_rssi = 0; sbuf_t rfkill = {0}, dns = {0}; if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) { jv_t *jr = jv_parse(sj, (int)strlen(sj)); if (jr) { jv_t *w = jv_obj_get(jr, "wifi"); jv_t *r = w ? jv_obj_get(w, "rssi") : NULL; if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; } jv_t *rf = jv_obj_get(jr, "rfkill"); if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf); jv_t *bt = jv_obj_get(jr, "bt"); jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL; if (dn && dn->type == J_ARR) jv_to_json(&dns, dn); jv_free(jr); } } sbuf_t s = {0}; sA(&s, "{\"available\":true,\"rssi\":"); if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null"); sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr); sbuf_free(&rfkill); sbuf_free(&dns); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; } /* 防自锁实体:对本机关键网络/服务组件禁设「阻断」(net=block / set_domain 域名限制), * 否则会把能连上设备/面板/系统核心的网络能力一并锁死(SSH / WiFi / DHCP / DNS / 面板 / init)。 * netmgrd 加载时也会剔除自锁项,面板侧抢先硬拒(guard①),保证 0 warning + 如实回错误码。 * 匹配口径:取 `svc:|app:|proc:` 之后的裸名精确比对(`svc:netmgrd`/`proc:sysui`/裸 `udhcpc` 均命中)。 */ int net_locked_entity(const char *entity) { if (!entity || !entity[0]) return 0; const char *base = entity; const char *colon = strrchr(entity, ':'); if (colon) base = colon + 1; static const char *locked[] = { "dropbear", "wpa_supplicant", "udhcpc", "netmgrd", "svcd", "sysui", "init", NULL }; for (int i = 0; locked[i]; i++) if (!strcmp(base, locked[i])) return 1; return 0; } void net_policy_parse(const char *txt, sbuf_t *s) { char lvl[24] = "", enf[24] = "dns_only"; const char *p = txt; sA(s, "{\"available\":true,\"policy_write_enabled\":%s,\"enforce\":", panel_policy_write_enabled() ? "true" : "false"); /* 首行找 enforce=... */ /* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */ const char *en = strstr(p, "enforce="); if (en) { en += 8; int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; } enf[i] = 0; } const char *lg = strstr(p, "log_level="); if (lg) { lg += 10; int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; } lvl[i] = 0; } sA_str(s, enf); sA(s, ",\"log_level\":"); sA_str(s, lvl); sA(s, ",\"rules\":["); int first = 1; const char *ln = txt; while (ln && *ln) { const char *nl = strchr(ln, '\n'); size_t ll = nl ? (size_t)(nl - ln) : strlen(ln); if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; } /* 跳注释行(以 # 开头)与空;数据行为 ` net=... domain=...`(entity 含冒号无空格) */ const char *c = ln; while (c < ln + ll && (*c == ' ' || *c == '\t')) c++; int is_comment = (c < ln + ll && *c == '#'); int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0); if (!is_comment && !is_none && c < ln + ll) { /* entity = 首个空白符之前 */ char entity[64] = ""; size_t ei = 0; const char *e = c; while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; } entity[ei] = 0; /* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */ char net[8] = "allow"; int has_net = 0; char allow_h[512] = "", deny_h[512] = ""; const char *k = e; if (k < ln + ll) { const char *seg = k; while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++; const char *nh = strstr(seg, "net="); if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; } const char *dh = strstr(seg, "domain="); if (dh) { dh += 7; /* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */ size_t ao = 0, do2 = 0; const char *h = dh; while (*h && *h != ' ' && *h != '\n' && *h != '\r') { if (*h == '+') { h++; if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ','; while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++; } else if (*h == '-') { h++; if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ','; while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++; } else h++; if (*h == ',') h++; } allow_h[ao] = 0; deny_h[do2] = 0; } if (!first) sA(s, ","); first = 0; sA(s, "{\"entity\":"); sA_str(s, entity); sA(s, ",\"net\":"); sA_str(s, has_net ? net : ""); sA(s, ",\"has_net\":%s", has_net ? "true" : "false"); sA(s, ",\"allow_hosts\":["); { const char *hp = allow_h; int hf = 1; while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp); if (!hf) { sA(s, ","); } hf = 0; sA_str_n(s, hp, hl); hp = comma ? comma + 1 : hp + hl; } } sA(s, "],\"deny_hosts\":["); { const char *hp = deny_h; int hf = 1; while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp); if (!hf) { sA(s, ","); } hf = 0; sA_str_n(s, hp, hl); hp = comma ? comma + 1 : hp + hl; } } sA(s, "]}"); } } ln = nl ? nl + 1 : ln + ll; } sA(s, "]}"); } int api_net_policy_get(int sock) { m_net_policy_t q; memset(&q, 0, sizeof q); q.op = NET_POLICY_SHOW; uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt); if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK) return send_json(sock, 200, "{\"available\":false}"); if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */ return send_json(sock, 200, "{\"available\":false}"); sbuf_t s = {0}; net_policy_parse(txt, &s); int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}"); sbuf_free(&s); return rc; } int api_net_policy_set(int sock, const char *body, size_t body_len) { if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); /* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。 */ if (!panel_policy_write_enabled()) return send_err(sock, 403, "POLICY_DISABLED", "网络策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)"); jv_t *root = jv_parse(body, (int)body_len); if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg"); char op[16] = "set_net", entity[64] = "", arg[64] = ""; if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str); if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str); if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str); jv_free(root); if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:/svc:/proc:)"); if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法"); int nop = 0; if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET; else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN; else if (!strcmp(op, "del")) nop = NET_POLICY_DEL; else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del"); if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg"); /* guard① 防自锁:对系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)禁设阻断。 * 「放行 / 删除规则」不阻断(那是恢复连通),仍放行;仅 set_net=block 与 set_domain(域名限制)命中即拒。 */ { int blocking = ((nop == NET_POLICY_SET_NET) && !strcmp(arg, "block")) || (nop == NET_POLICY_SET_DOMAIN); if (blocking && net_locked_entity(entity)) return send_err(sock, 422, "SELF_LOCK", "防自锁:禁止阻断系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)的网络"); } m_net_policy_t q; memset(&q, 0, sizeof q); q.op = (uint32_t)nop; snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */ snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */ uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX]; int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt); if (n <= 0 || t != MSG_NET_POLICY_RESULT) return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); if (code == (uint32_t)MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?"); if (code == (uint32_t)MEOW_ERR_INVAL) return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法"); if (code != MEOW_OK) return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败"); sbuf_t s = {0}; sA(&s, "{\"accepted\":true,\"message\":"); sA_str(&s, txt); sA(&s, "}"); int rc = send_json(sock, 200, s.b ? s.b : ""); sbuf_free(&s); return rc; }