meow_netmgrd 期4:SDK 自愿通道 + wifi forget + RSSI signal_poll + AAAA 过滤 + iptables 评估

NETMGR_DESIGN.md §7/§13 期4 落地(只动 patch_v2/meow_netmgrd/ + patch_v2/sdk/ 追加式):
1. SDK 自愿通道(§6.3/§4):meow_ui.h 新增 meow_net_policy_check(host)(0=放行 1=block;
   netmgrd 不在/超时→0 乐观放行)+ meow_net_report(rx,tx)(自愿上报,§4 字节级记账自报通道)。
   meow_ipc.h 追加 0xEE MSG_NET_SDK(op=CHECK/REPORT)/ 0xEF RESULT。netmgrd 侧:CHECK 以
   app_id→实体 app:<pkg> 评估 policy_net_blocked||dns_query_domain_denied;REPORT 记入 per-实体
   g_ent_bytes(status JSON 新增 entity_bytes)。SDK 实现 meow_net_ipc.c(一次 connect+send+read,
   降级不崩);meow_ui.a 重编。
2. wifi forget <id>:wpa_cli remove_network+save_config(MeowOS;HibyOS 只读门控),CLI+IPC 接通
   (286 launcher 录入 UI 缺口)。
3. RSSI:wifi_status 在 status 无 signal= 时回退 wpa_cli signal_poll(真机 rssi=-38,修面板 rssi=null 缺口)。
4. DNS AAAA 过滤:global dns.filter_aaaa=0|1(默认 0 透传;纯 IPv4 设备可置 1 让 AAAA 查询回空答案回落 A)。
5. 静态 iptables 评估(devdocs/plan/NETMGR_IPTABLES_EVAL.md,本机不入库):iptables-legacy 1.8.9 交叉
   编译成功(~578KB,含 owner/cgroup/REDIRECT 等全部 extension),但内核实测 matches 仅
   quota/icmp/udp/tcp、targets 仅 NFQUEUE/NFLOG/ERROR、无 nat 表、全部进程 UID 0 → **per-app 管控
   与 REDIRECT 透明代理判不可行**(与 NETWORK_HW_FACTS 一致);正路为 per-app UID 框架改造(单独立项)。

验证:宿主 test_netmgrd.sh 70/70 + qemu 70/70;test_netmgrd_p2/p3/p5 宿主全过 + 交叉 0 warning;
netmgrd_authtest 全过;sdk meow_net_ipc 交叉 0 warning。真机(固件 netmgrd 共存,独立 socket+DNS off):
wifi status rssi=-38(signal_poll)、wifi forget 0(DISABLED 安全移除,id1 CURRENT 保留、wifi 未断)、
SDK policy check(CHECK_GOOD=0/CHECK_BLOCK=1/REPORT ok)、policy show 正常;resolv.conf 未触碰、
固件 meow_netmgrd(pid) running 未动;测试产物全清。

新 md5:meow_netmgrd 20243ea1a800b29badefebb395603aaf;meow_ui.a be24214e24faadba63577f3a98c2c9d1
(主会话更 fw_content.conf / 热更设备 / 记录新 meow_ui.a)。
This commit is contained in:
MeowOS committed 2026-09-02 16:19:40 +08:00
1 parent 4e0e7441fd
commit 8a43f17653
17 files changed
+563 -16

No files matched your search

+122 -9
View File
@@ -327,6 +327,9 @@ typedef struct {
long rx, tx; /* 累计字节(/proc/net/dev) */
} iface_t;
static iface_t g_ifaces[IFACE_MAX];
typedef struct { char entity[48]; uint32_t rx, tx; } ent_bytes_t;
static ent_bytes_t g_ent_bytes[ENTITY_MAX]; /* SDK 自愿上报(§4 字节级记账自报通道;声明须在 build_status_json 前) */
static int g_ent_bytes_n = 0;
static int g_iface_n = 0;
typedef struct {
@@ -592,6 +595,15 @@ static void wifi_status(wifi_t *w)
if (!nl) break;
ln = nl + 1;
}
/* 期4:status 无 signal=(本机 wpa_cli status 不输出)→ 回退 wpa_cli signal_poll(RSSI=<val>)。 */
if (!w->has_rssi) {
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s signal_poll 2>/dev/null", g_wpa_cli);
char sbuf[256];
if (run_cmd(sbuf, sizeof sbuf, RUNCMD_TMO_MS, cmd) > 0) {
char *s = strstr(sbuf, "RSSI=");
if (s) { w->rssi = atoi(s + 5); w->has_rssi = 1; }
}
}
}
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
@@ -896,8 +908,17 @@ static void build_status_json(char *out, size_t cap)
if (g_net_rate_rx < 0) jadd(&w, "null,");
else jadd(&w, "%ld,", g_net_rate_rx);
jadd(&w, "\"tx_bps\":");
if (g_net_rate_tx < 0) jadd(&w, "null}}");
else jadd(&w, "%ld}}", g_net_rate_tx);
if (g_net_rate_tx < 0) jadd(&w, "null},"); /* rate object 闭合 + 逗号(tx_bps 为 rate 末字段) */
else jadd(&w, "%ld},", g_net_rate_tx);
/* 期4:SDK 自愿上报的 per-实体字节统计(§4 字节级记账自报通道);空则 []。 */
jadd(&w, "\"entity_bytes\":[");
for (int i = 0; i < g_ent_bytes_n; i++) {
if (i) jadd(&w, ",");
jadd(&w, "{\"entity\":\"%s\",\"rx\":%u,\"tx\":%u}", g_ent_bytes[i].entity,
g_ent_bytes[i].rx, g_ent_bytes[i].tx);
}
jadd(&w, "]}");
}
/* 单/全接口详情 JSON。name 空=全部(数组);否则单对象。返回 MEOW_OK / MEOW_ERR_NOENT。 */
@@ -975,6 +996,7 @@ typedef struct {
static rule_t g_rules[RULE_ENT_MAX];
static int g_rule_n = 0;
static char g_policy_log_level[8] = "info"; /* global log_level= */
static int g_dns_filter_aaaa = 0; /* global dns.filter_aaaa=:0=透传 AAAA(默认),1=过滤(纯 IPv4 设备弱化 IPv6 优先) */
/* ---- 期3 防自锁清单(NETMGR_DESIGN §14 红线):这些实体拒绝/强制置 allow,防断网变砖。 ---- */
static int _is_selflock_by_name(const char *name)
@@ -1185,7 +1207,11 @@ static void rules_load(void)
*sp = 0;
char *entity = p, *rest = sp + 1;
if (strcmp(entity, "global") == 0) {
/* global log_level=<lvl> */
/* global log_level=<lvl> dns.filter_aaaa=0|1
* 注意:log_level 解析会在 rest 中部写 NUL 截断,故 dns.filter_aaaa 必须先解析。 */
char *fa = strstr(rest, "dns.filter_aaaa=");
if (fa) { char *v = fa + 16; while (*v==' '||*v=='\t') v++;
g_dns_filter_aaaa = atoi(v) ? 1 : 0; }
char *lv = strstr(rest, "log_level=");
if (lv) { char *v = lv + 10; while (*v==' '||*v=='\t'||*v=='"') v++;
char *e2 = v; while (*e2 && *e2!=' ' && *e2!='\t' && *e2!='\n') e2++; *e2=0;
@@ -1294,7 +1320,8 @@ static void policy_persist_entity(const char *entity)
}
/* 追加本实体当前态 */
if (strcmp(entity, "global") == 0) {
int l = snprintf(out + o, sizeof out - o, "global log_level=%s\n", g_policy_log_level);
int l = snprintf(out + o, sizeof out - o, "global log_level=%s dns.filter_aaaa=%d\n",
g_policy_log_level, g_dns_filter_aaaa);
if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l;
} else {
rule_t *r = policy_find(entity);
@@ -1684,7 +1711,7 @@ static void dns_proxy_stop(void)
svc_log("dns proxy off");
}
static int dns_parse_query(const uint8_t *q, int qlen, char *domain, int domcap, int *qend)
static int dns_parse_query(const uint8_t *q, int qlen, char *domain, int domcap, int *qend, int *qtype)
{
if (qlen < 12) return -1;
if (q[2] & 0x80) return -1; /* 不是 query 位 */
@@ -1703,6 +1730,7 @@ static int dns_parse_query(const uint8_t *q, int qlen, char *domain, int domcap,
}
if (off + 4 > qlen) return -1;
domain[di] = 0;
if (qtype) *qtype = (q[off] << 8) | q[off + 1]; /* QTYPE:1=A 28=AAAA */
off += 4; /* qtype+qclass */
*qend = off;
return di > 0 ? 0 : -1;
@@ -1711,7 +1739,7 @@ static int dns_build_nxdomain(const uint8_t *q, int qlen, uint8_t *out, int outc
{
int qend = 0;
char dom[DNS_QNAME_MAX];
if (dns_parse_query(q, qlen, dom, sizeof dom, &qend) != 0) return -1;
if (dns_parse_query(q, qlen, dom, sizeof dom, &qend, NULL) != 0) return -1;
int qsec = qend - 12;
if (12 + qsec > outcap) return -1;
memcpy(out, q, 12);
@@ -1783,8 +1811,8 @@ static void dns_handle_packet(void)
if (n < 12) return;
g_dns_q++;
char dom[DNS_QNAME_MAX];
int qend = 0;
if (dns_parse_query(q, (int)n, dom, sizeof dom, &qend) != 0 || !dom[0]) return;
int qend = 0, qtype = 0;
if (dns_parse_query(q, (int)n, dom, sizeof dom, &qend, &qtype) != 0 || !dom[0]) return;
const char *entity = dns_entity_for_port((uint16_t)ntohs(from.sin_port));
/* 域名规则:deny → NXDOMAIN + 日志 */
if (dns_query_domain_denied(entity, dom)) {
@@ -1795,6 +1823,21 @@ static void dns_handle_packet(void)
if (rl > 0) sendto(g_dns_fd, resp, (size_t)rl, 0, (struct sockaddr *)&from, fl);
return;
}
/* 期4 AAAA 过滤(dns.filter_aaaa=1,纯 IPv4 设备弱化 IPv6 优先):回「无 AAAA」空应答 → 客户端回落 A。 */
if (g_dns_filter_aaaa && qtype == 28) {
int qsec = qend - 12;
if (qsec > 0 && 12 + qsec <= (int)sizeof q) {
uint8_t resp[1536];
memcpy(resp, q, 12);
resp[2] = 0x81; resp[3] = 0x80; /* QR|RD|RA, rcode=0 */
resp[4] = 0; resp[5] = 1; /* qdcount=1 */
resp[6] = 0; resp[7] = 0; resp[8] = 0; resp[9] = 0; resp[10] = 0; resp[11] = 0;
memcpy(resp + 12, q + 12, (size_t)qsec);
sendto(g_dns_fd, resp, (size_t)(12 + qsec), 0, (struct sockaddr *)&from, fl);
net_log("info", "dns", "aaaa filtered entity=%s domain=%s", entity, dom);
}
return;
}
/* 只记新域名首见(全量 debug 级,防日志爆炸) */
int cached = dns_cache_lookup_name(dom);
if (cached != 0) { g_dns_first++; net_log("info", "dns", "first entity=%s domain=%s", entity, dom); }
@@ -2257,6 +2300,22 @@ static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, "wifi disconnect: 已下发");
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
wifi_onoff(cmd, r);
} else if (!strcmp(cmd, "forget")) {
int nid = arg ? atoi(arg) : -1;
if (nid < 0) { ctl_sets(r, MEOW_ERR_INVAL, "wifi forget 需 <network_id>"); return; }
char cmd2[192];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s remove_network %d 2>/dev/null",
g_wpa_cli, nid);
char buf[128];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
/* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。 */
if (g_mode_meow) {
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
}
char msg[64];
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? "+save_config" : "");
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
} else if (!strcmp(cmd, "rfkill")) {
rfkill_ctl(arg, r);
} else if (!strcmp(cmd, "udhcpc")) {
@@ -2576,7 +2635,57 @@ static void handle_conn(int c)
send_frame(c, MSG_NET_POLICY_RESULT, req_id, &res, (uint32_t)(8 + tl + 1));
continue;
}
send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1 STATUS/IFACE/WIFI_CTL/BT_CTL; 期2 CONNS/DNS_CTL; 期3 POLICY)");
if (type == MSG_NET_SDK) {
m_net_sdk_t q;
memset(&q, 0, sizeof q);
if (rlen > sizeof q) rlen = sizeof q;
memcpy(&q, payload, rlen);
m_net_sdk_result_t res;
memset(&res, 0, sizeof res);
char ent[48];
snprintf(ent, sizeof ent, "app:%s", q.app_id[0] ? q.app_id : "unknown");
if (q.op == NET_SDK_CHECK) {
int blocked = policy_net_blocked(ent) || dns_query_domain_denied(ent, q.host);
res.allow = blocked ? 0 : 1;
net_log("info", "sdk", "check app=%s host=%s -> %s", q.app_id, q.host, blocked ? "block" : "allow");
snprintf(res.text, sizeof res.text, "sdk check app=%s host=%s %s\n", q.app_id, q.host,
blocked ? "block" : "allow");
res.code = MEOW_OK;
} else if (q.op == NET_SDK_REPORT) {
/* 自愿字节上报 → per-实体统计(§4 字节级记账自报)。 */
for (int i = 0; i < g_ent_bytes_n; i++)
if (strcmp(g_ent_bytes[i].entity, ent) == 0) {
g_ent_bytes[i].rx += q.rx; g_ent_bytes[i].tx += q.tx;
res.code = MEOW_OK;
snprintf(res.text, sizeof res.text, "sdk report app=%s rx=%u tx=%u (累计 %u/%u)\n",
q.app_id, q.rx, q.tx, g_ent_bytes[i].rx, g_ent_bytes[i].tx);
goto sdk_send;
}
if (g_ent_bytes_n < ENTITY_MAX) {
snprintf(g_ent_bytes[g_ent_bytes_n].entity, sizeof g_ent_bytes[g_ent_bytes_n].entity, "%s", ent);
g_ent_bytes[g_ent_bytes_n].rx = q.rx;
g_ent_bytes[g_ent_bytes_n].tx = q.tx;
g_ent_bytes_n++;
res.code = MEOW_OK;
snprintf(res.text, sizeof res.text, "sdk report app=%s rx=%u tx=%u\n", q.app_id, q.rx, q.tx);
} else {
res.code = MEOW_ERR_NOMEM;
snprintf(res.text, sizeof res.text, "sdk report: entity 槽满\n");
}
} else {
res.code = MEOW_ERR_INVAL;
snprintf(res.text, sizeof res.text, "sdk: 未知 op\n");
}
sdk_send:
{
size_t tl = strlen(res.text);
if (tl > sizeof res.text - 1) tl = sizeof res.text - 1;
res.text_len = (uint16_t)tl;
send_frame(c, MSG_NET_SDK_RESULT, req_id, &res, (uint32_t)(12 + tl + 1));
}
continue;
}
send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1-4: STATUS/IFACE/WIFI_CTL/BT_CTL/CONNS/DNS/POLICY/SDK)");
break;
}
}
@@ -3070,6 +3179,10 @@ static int cli_run(int n, char **v)
if (!arg2) { fprintf(stderr, "connect 需 <ssid>[,psk]\n"); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", arg2);
}
if (!strcmp(arg, "forget")) {
if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "forget", arg2);
}
return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
}
if (!strcmp(cmd, "bt")) {