diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 6e7a1c6..1003901 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -601,4 +601,36 @@ typedef struct m_net_dns_result { char text[MEOW_IPC_MAX - 8]; } m_net_dns_result_t; +/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)---- + * 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份 + * (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/ + * udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。 + * 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */ +enum { /* 与主 enum 分列,追加式不改既有行 */ + MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */ + MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */ + /* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */ +}; + +/* MSG_NET_POLICY 操作码 */ +enum { + NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */ + NET_POLICY_SET_NET = 1, /* set allow|block */ + NET_POLICY_SET_DOMAIN = 2, /* domain +host|-host */ + NET_POLICY_DEL = 3 /* del */ +}; + +typedef struct m_net_policy { + uint32_t op; /* NET_POLICY_* */ + char entity[48]; /* app: | svc: | proc: | global;空=适用 SHOW 的全量 */ + char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */ +} m_net_policy_t; /* 120 B */ + +typedef struct m_net_policy_result { + uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */ + uint16_t text_len; + uint16_t pad; + char text[MEOW_IPC_MAX - 8]; +} m_net_policy_result_t; + #endif /* MEOW_IPC_H */ diff --git a/patch_v2/meow_netmgrd/meow_netmgrd b/patch_v2/meow_netmgrd/meow_netmgrd index 08fe893..08fd43a 100755 Binary files a/patch_v2/meow_netmgrd/meow_netmgrd and b/patch_v2/meow_netmgrd/meow_netmgrd differ diff --git a/patch_v2/meow_netmgrd/meow_netmgrd.c b/patch_v2/meow_netmgrd/meow_netmgrd.c index 15b7467..445b2ba 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -46,8 +46,11 @@ #include #include #include +#include +#include +#include -#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00ED) + m_net_*(号段 0x00E0-0x00EF) */ +#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00EF) + m_net_*(号段 0x00E0-0x00EF) */ /* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */ static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */ @@ -75,7 +78,7 @@ static char g_proc_udp[160] = "/proc/net/udp"; /* MEOW_UDP * static char g_ams_sock[128] = "/usr/data/meow_ams.sock"; /* MEOW_AMS_SOCK(amsd 注册表 pid→pkg) */ static char g_svcd_sock[128] = "/tmp/meow_svcd.sock"; /* MEOW_SVCD_SOCK(svcd 服务表 pid→svc,socket 查询兜底) */ static char g_svcd_status[160]= "/tmp/meow_svcd.status"; /* MEOW_SVCD_STATUS(svcd 落盘状态,机器可读 `svc= ... pid=`) */ -static char g_net_policy[160]= "/usr/data/meow/net_policy.conf"; /* MEOW_NET_POLICY(期2 只读静态域名规则) */ +static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET_POLICY(期3 读改写;/data=/usr/data 符号链接) */ #define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */ #define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */ @@ -966,9 +969,29 @@ typedef struct { char entity[48]; char allow[RULE_HOST_MAX][48]; int allow_n; char deny[RULE_HOST_MAX][48]; int deny_n; + int net_block; /* net=block(默认 allow;防自锁实体强制 0) */ + int has_net; /* 显式 net= 出现过 */ } rule_t; 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= */ + +/* ---- 期3 防自锁清单(NETMGR_DESIGN §14 红线):这些实体拒绝/强制置 allow,防断网变砖。 ---- */ +static int _is_selflock_by_name(const char *name) +{ + static const char *const k[] = { + "dropbear", "wpa_supplicant", "udhcpc", + "meow_netmgrd", "meow_svcd", "meow_sysui", "init", NULL + }; + for (int i = 0; k[i]; i++) if (strcmp(name, k[i]) == 0) return 1; + return 0; +} +static int is_self_lock(const char *entity) +{ + const char *ns = strchr(entity, ':'); + const char *name = ns ? ns + 1 : entity; + return _is_selflock_by_name(name); +} typedef struct { char key[96]; long mono; } evt_dup_t; static evt_dup_t g_evt_dup[EVT_DUP_MAX]; @@ -1088,7 +1111,7 @@ static void dns_cache_flush(void) g_dns_n = 0; } -/* ---- 域名规则(期2 静态 conf 只读;期3 策略引擎再接管) ---- */ +/* ---- 策略引擎(§6:per-entity net=allow|block + domain 白/黑 + global log_level;默认 allow) ---- */ static int suffix_match(const char *host, const char *rule) { if (!host[0] || !rule[0]) return 0; @@ -1097,23 +1120,43 @@ static int suffix_match(const char *host, const char *rule) if (strcmp(host + hl - rl, rule) != 0) return 0; return hl == rl || host[hl - rl - 1] == '.'; } -static int entity_rules_match(const rule_t *r, const char *entity) +static rule_t *entity_rules_match(rule_t *r, const char *entity) { - return r->entity[0] && strcmp(r->entity, entity) == 0; + if (!r || !r->entity[0] || strcmp(r->entity, entity) != 0) return NULL; + if (strcmp(r->entity, "global") == 0) return r; + return r; } -/* deny 判定:命中最具体的实体规则(entity+global),allow 覆盖 deny;默认 allow。 */ +static rule_t *policy_find(const char *entity) +{ + for (int i = 0; i < g_rule_n; i++) + if (entity_rules_match(&g_rules[i], entity)) return &g_rules[i]; + return NULL; +} +static rule_t *policy_find_global(void) +{ + for (int i = 0; i < g_rule_n; i++) + if (strcmp(g_rules[i].entity, "global") == 0) return &g_rules[i]; + return NULL; +} +/* net 层 block 判定:防自锁实体恒 allow;实体 net=block → block;global net=block → block;默认 allow。 */ +static int policy_net_blocked(const char *entity) +{ + if (is_self_lock(entity)) return 0; + rule_t *e = policy_find(entity); + if (e && e->has_net) return e->net_block; + rule_t *g = policy_find_global(); + return (g && g->has_net) ? g->net_block : 0; +} +/* domain 层 deny 判定:①网被 block 的实体 → 全量 deny(DNS 层是落地保障,无 SOCK_DESTROY 也生效) + * ②实体规则中最具体(entity → global)的 domain= 白/黑;allow 覆盖 deny;默认 allow。 */ static int dns_query_domain_denied(const char *entity, const char *domain) { - const rule_t *g = NULL, *e = NULL; - for (int i = 0; i < g_rule_n; i++) { - if (strcmp(g_rules[i].entity, "global") == 0) g = &g_rules[i]; - else if (!e && entity_rules_match(&g_rules[i], entity)) e = &g_rules[i]; - } - const rule_t *r = e ? e : g; + if (policy_net_blocked(entity)) return 1; + rule_t *e = policy_find(entity); + const rule_t *r = e ? e : policy_find_global(); if (r && r->deny_n) { for (int i = 0; i < r->deny_n; i++) if (suffix_match(domain, r->deny[i])) { - /* allow 白名单可覆盖 */ for (int j = 0; j < r->allow_n; j++) if (suffix_match(domain, r->allow[j])) return 0; return 1; @@ -1121,9 +1164,12 @@ static int dns_query_domain_denied(const char *entity, const char *domain) } return 0; } + +/* 规则加载(启动 + 每次写后重载以自愈)。防自锁:文件被手工改坏(net=block on 命根实体)→ 加载校验强制 allow。 */ static void rules_load(void) { g_rule_n = 0; + snprintf(g_policy_log_level, sizeof g_policy_log_level, "%s", "info"); FILE *f = fopen(g_net_policy, "r"); if (!f) return; char line[256]; @@ -1135,33 +1181,152 @@ static void rules_load(void) char *sp = strchr(p, ' '); char *tab = strchr(p, '\t'); if (!sp || (tab && tab < sp)) sp = tab; - char *entity; char *rest; - if (sp) { *sp = 0; entity = p; rest = sp + 1; } - else continue; - rule_t *r = &g_rules[g_rule_n]; - memset(r, 0, sizeof *r); - snprintf(r->entity, sizeof r->entity, "%s", entity); - /* rest 形如 "domain=+a,-b,c" 或 "net=block";逐 token 解析 */ + if (!sp) continue; + *sp = 0; + char *entity = p, *rest = sp + 1; + if (strcmp(entity, "global") == 0) { + /* global log_level= */ + 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; + snprintf(g_policy_log_level, sizeof g_policy_log_level, "%s", v); } + } + rule_t *r = policy_find(entity); + if (!r) { + if (g_rule_n >= RULE_ENT_MAX) continue; + r = &g_rules[g_rule_n++]; + memset(r, 0, sizeof *r); + snprintf(r->entity, sizeof r->entity, "%s", entity); + } char *save = NULL; for (char *tok = strtok_r(rest, " \t", &save); tok; tok = strtok_r(NULL, " \t", &save)) { - if (strncmp(tok, "domain=", 7) != 0) continue; - char *sub = tok + 7; - char *ssave = NULL; - for (char *h = strtok_r(sub, ",", &ssave); h; h = strtok_r(NULL, ",", &ssave)) { - if (h[0] == '+') { - if (r->allow_n < RULE_HOST_MAX) snprintf(r->allow[r->allow_n++], 48, "%s", h + 1); - } else if (h[0] == '-') { - if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h + 1); - } else if (h[0]) { - if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h); + if (strncmp(tok, "domain=", 7) == 0) { + char *sub = tok + 7, *ssave = NULL; + for (char *h = strtok_r(sub, ",", &ssave); h; h = strtok_r(NULL, ",", &ssave)) { + if (h[0] == '+') { if (r->allow_n < RULE_HOST_MAX) snprintf(r->allow[r->allow_n++], 48, "%s", h+1); } + else if (h[0] == '-') { if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h+1); } + else if (h[0]) { if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h); } } + } else if (strncmp(tok, "net=", 4) == 0) { + int blocked = (strcmp(tok + 4, "block") == 0); + /* 防自锁:加载校验把命根实体的 net=block 强制复位为 allow(§14 红线)。 */ + if (blocked && is_self_lock(entity)) { + blocked = 0; + net_log("warn", "policy", "load: self-lock entity %s forced allow (net=block 被剔除)", entity); + } + r->net_block = blocked; + r->has_net = 1; } } - if (r->deny_n || r->allow_n) g_rule_n++; + /* 空规则(无 net/domain)且为新建末槽 → 移除(防占槽)。 */ + if (r->deny_n == 0 && r->allow_n == 0 && !r->has_net && + r == &g_rules[g_rule_n - 1]) { + memset(r, 0, sizeof *r); + g_rule_n--; + } } fclose(f); } +/* ---- 策略持久化:读改写单键(只替换目标实体行,其余逐字保留)+ 原子写(watchdog 毁设置教训) ---- */ +static int policy_read_text(char *buf, int cap) +{ + int fd = open(g_net_policy, O_RDONLY); + if (fd < 0) { buf[0] = 0; return -1; } + int n = 0; + while (n < cap - 1) { + ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n)); + if (r <= 0) break; + n += (int)r; + } + close(fd); + buf[n] = 0; + return n; +} +/* 原子写:写临时文件 + rename(避免半截文件)。 */ +static int policy_write_atomic(const char *text) +{ + char tmp[192]; + snprintf(tmp, sizeof tmp, "%s.tmp", g_net_policy); + int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + /* 尝试建目录(/data/meow/netmgrd/) */ + char dir[160]; snprintf(dir, sizeof dir, "%s", g_net_policy); + char *s = strrchr(dir, '/'); if (s) *s = 0; + mkdir(dir, 0755); + fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + } + if (fd < 0) return -1; + size_t tl = strlen(text); + ssize_t w = write(fd, text, tl); + close(fd); + if (w < 0 || (size_t)w != tl) { unlink(tmp); return -1; } /* 空文本 write 返回 0 == tl,视为成功 */ + if (rename(tmp, g_net_policy) < 0) { unlink(tmp); return -1; } + return 0; +} +/* 只重写 entity 的行(删旧 + 按 g_rules 的当前态重发),其余行逐字保留。 */ +static void policy_persist_entity(const char *entity) +{ + char text[2048]; + int n = policy_read_text(text, sizeof text); + char out[2048]; size_t o = 0; + char line[256]; + int start = 0; + int elen = (int)strlen(entity); + /* 复制非本实体的行 */ + while (start < n) { + int i = 0; + while (start + i < n && text[start + i] != '\n') i++; + if (i >= (int)sizeof line - 1) i = (int)sizeof line - 2; + memcpy(line, text + start, (size_t)i); line[i] = 0; + start += i; + if (start < n && text[start] == '\n') { start++; } + /* 去掉行首空白判断 */ + char *q = line; while (*q == ' ' || *q == '\t') q++; + int is_target = (strncmp(q, entity, (size_t)elen) == 0 && + (q[elen] == ' ' || q[elen] == '\t' || q[elen] == 0)); + int is_blank = (q[0] == 0); + if (is_target) continue; /* 本实体的旧行将被替换/删除 */ + if (is_blank) continue; /* 空行丢弃(避免双空行) */ + /* 注释与其它实体行逐字保留(读改写单键:只动目标实体)。 */ + int l = snprintf(out + o, sizeof out - o, "%s\n", line); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + /* 追加本实体当前态 */ + if (strcmp(entity, "global") == 0) { + int l = snprintf(out + o, sizeof out - o, "global log_level=%s\n", g_policy_log_level); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } else { + rule_t *r = policy_find(entity); + if (r) { + if (r->has_net) { + int l = snprintf(out + o, sizeof out - o, "%s net=%s\n", entity, r->net_block ? "block" : "allow"); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + } + if (r->deny_n || r->allow_n) { + int l = snprintf(out + o, sizeof out - o, "%s domain=", entity); + if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; + int first = 1; + for (int i = 0; i < r->allow_n; i++) { + int la = snprintf(out + o, sizeof out - o, "%s+%s", first ? "" : ",", r->allow[i]); + if (la > 0 && o + (size_t)la < sizeof out) o += (size_t)la; + first = 0; + } + for (int i = 0; i < r->deny_n; i++) { + int ld = snprintf(out + o, sizeof out - o, "%s-%s", first ? "" : ",", r->deny[i]); + if (ld > 0 && o + (size_t)ld < sizeof out) o += (size_t)ld; + first = 0; + } + int ln = snprintf(out + o, sizeof out - o, "\n"); + if (ln > 0 && o + (size_t)ln < sizeof out) o += (size_t)ln; + } + } + } + out[o] = 0; + policy_write_atomic(out); + rules_load(); /* 写后重载(自愈:也剔除任何被手工改回的自锁 block) */ +} + /* ---- 实体归因:pid→(app: / svc: / proc:) ---- */ static void pid_ent_put(int pid, const char *entity) { @@ -1368,7 +1533,8 @@ static void census_run(void) for (int i = 0; i < cn; i++) { cur[i].pid = 0; cur[i].entity[0] = 0; } attribute_inodes(cur, cn); for (int i = 0; i < cn; i++) { - if (cur[i].inode && !cur[i].entity[0]) snprintf(cur[i].entity, sizeof cur[i].entity, "proc:unknown"); + /* 任何未归因的连接一律落 proc:unknown(防 apps 表出现空 entity→被 strtok 当端点显示成 IP:port)。 */ + if (!cur[i].entity[0]) snprintf(cur[i].entity, sizeof cur[i].entity, "proc:unknown"); if (cur[i].inode && cur[i].remote_ip != 0) dns_cache_lookup_ip(cur[i].remote_ip, cur[i].domain, sizeof cur[i].domain); } @@ -1820,6 +1986,8 @@ static void classify_exe(const char *exe, caller_t *out) char tmp[MEOW_APP_ID_MAX]; if (strcmp(exe, "/usr/bin/meow_launcherd") == 0) { snprintf(tmp, sizeof tmp, "%s", "os.meow.launcher"); + } else if (strcmp(exe, "/usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/meowpanel") == 0) { + snprintf(tmp, sizeof tmp, "%s", "os.meow.panel"); /* MeowPanel 精确登记(面板 PIN 已有门禁;dns flush 等动作需系统身份) */ } else if (strncmp(exe, "/usr/bin/meow_", 14) == 0) { size_t l = strlen(exe + 9); /* "/usr/bin/" 长 9 → 取 "meow_xyz" */ if (l >= sizeof tmp) l = sizeof tmp - 1; @@ -1994,9 +2162,16 @@ static void wifi_connect(const char *arg, ctl_result_t *r) snprintf(sel, sizeof sel, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s select_network %d 2>/dev/null", g_wpa_cli, id); run_cmd(out, sizeof out, RUNCMD_TMO_MS, sel); + /* 期3:MeowOS 模式 connect 后持久化 wpa_supplicant.conf(HibyOS 只读由服务端门控,这里仅 MeowOS 生效)。 */ + if (g_mode_meow) { + char sc[160]; + snprintf(sc, sizeof sc, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli); + run_cmd(out, sizeof out, RUNCMD_TMO_MS, sc); + } } char msg[140]; - snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select)", tmp_ssid, id); + snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select%s)", tmp_ssid, id, + g_mode_meow ? "+save_config" : ""); ctl_sets(r, MEOW_OK, msg); } @@ -2129,6 +2304,116 @@ static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) } } +/* ---------------- 期3 策略命令(IPC 0xEA / CLI policy 共用;写级已在 handle_conn 鉴权) ---------------- */ +/* 从 g_rules 移除某实体(shift 数组),返回 0=存在已删。 */ +static int policy_remove(const char *entity) +{ + for (int i = 0; i < g_rule_n; i++) { + if (strcmp(g_rules[i].entity, entity) == 0) { + for (int j = i; j < g_rule_n - 1; j++) g_rules[j] = g_rules[j + 1]; + memset(&g_rules[g_rule_n - 1], 0, sizeof(rule_t)); + g_rule_n--; + return 0; + } + } + return -1; +} +/* 输出某实体当前行;无规则输出 (none)。 */ +static void policy_print_entity(const rule_t *r, char *out, size_t cap) +{ + out[0] = 0; + if (!r) { snprintf(out, cap, "(none)\n"); return; } + size_t o = 0; + int l = snprintf(out + o, cap - o, "%s ", r->entity); + if (l > 0 && o + (size_t)l < cap) o += (size_t)l; + int any = 0; + if (r->has_net) { l = snprintf(out + o, cap - o, "net=%s ", r->net_block ? "block" : "allow"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; any = 1; } + if (r->deny_n || r->allow_n) { + l = snprintf(out + o, cap - o, "domain="); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; + int first = 1; + for (int i = 0; i < r->allow_n; i++) { int la = snprintf(out + o, cap - o, "%s+%s", first ? "" : ",", r->allow[i]); if (la > 0 && o + (size_t)la < cap) o += (size_t)la; first = 0; } + for (int i = 0; i < r->deny_n; i++) { int ld = snprintf(out + o, cap - o, "%s-%s", first ? "" : ",", r->deny[i]); if (ld > 0 && o + (size_t)ld < cap) o += (size_t)ld; first = 0; } + any = 1; + } + if (!any) { l = snprintf(out + o, cap - o, "(allow)"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; } + l = snprintf(out + o, cap - o, "\n"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; +} + +static int policy_cmd(int op, const char *entity, const char *arg, char *out, size_t cap) +{ + out[0] = 0; + if (op == NET_POLICY_SHOW) { + snprintf(out, cap, "# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n", + g_policy_log_level); + size_t o = strlen(out); + if (entity && entity[0]) { + rule_t *r = policy_find(entity); + policy_print_entity(r, out + o, cap - o); + } else { + for (int i = 0; i < g_rule_n; i++) { + char l[160]; policy_print_entity(&g_rules[i], l, sizeof l); + int n = snprintf(out + o, cap - o, "%s", l); + if (n > 0 && o + (size_t)n < cap) o += (size_t)n; + } + } + return MEOW_OK; + } + if (op == NET_POLICY_SET_NET) { + if (!entity || !entity[0] || !arg || (strcmp(arg, "allow") != 0 && strcmp(arg, "block") != 0)) + { snprintf(out, cap, "policy set 需 allow|block\n"); return MEOW_ERR_INVAL; } + if (strcmp(arg, "block") == 0 && is_self_lock(entity)) { + snprintf(out, cap, "policy: %s 是防自锁实体(SSH/WiFi/自身命根),拒绝置 block(NETMGR_DESIGN §14 红线)\n", entity); + net_log("warn", "policy", "self-lock entity %s tried block — 拒", entity); + return MEOW_ERR_PERM; + } + rule_t *r = policy_find(entity); + if (!r) { + if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; } + r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r); + snprintf(r->entity, sizeof r->entity, "%s", entity); + } + r->net_block = strcmp(arg, "block") == 0; + r->has_net = 1; + policy_persist_entity(entity); + net_log("info", "policy", "set %s net=%s", entity, arg); + snprintf(out, cap, "policy set %s net=%s ok\n", entity, arg); + return MEOW_OK; + } + if (op == NET_POLICY_SET_DOMAIN) { + if (!entity || !entity[0] || !arg || (arg[0] != '+' && arg[0] != '-') || arg[1] == 0) + { snprintf(out, cap, "policy domain 需 +host|-host\n"); return MEOW_ERR_INVAL; } + rule_t *r = policy_find(entity); + if (!r) { + if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; } + r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r); + snprintf(r->entity, sizeof r->entity, "%s", entity); + } + if (arg[0] == '+') { + if (r->allow_n < RULE_HOST_MAX) { snprintf(r->allow[r->allow_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s +%s ok\n", entity, arg + 1); } + else { snprintf(out, cap, "policy: allow 槽满\n"); return MEOW_ERR_NOMEM; } + } else { + if (r->deny_n < RULE_HOST_MAX) { snprintf(r->deny[r->deny_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s -%s ok\n", entity, arg + 1); } + else { snprintf(out, cap, "policy: deny 槽满\n"); return MEOW_ERR_NOMEM; } + } + policy_persist_entity(entity); + net_log("info", "policy", "domain %s %s", entity, arg); + return MEOW_OK; + } + if (op == NET_POLICY_DEL) { + if (!entity || !entity[0]) { snprintf(out, cap, "policy del 需 \n"); return MEOW_ERR_INVAL; } + if (policy_remove(entity) == 0) { + policy_persist_entity(entity); + net_log("info", "policy", "del %s", entity); + snprintf(out, cap, "policy del %s ok\n", entity); + } else { + snprintf(out, cap, "policy del %s: 无此实体\n", entity); + } + return MEOW_OK; + } + snprintf(out, cap, "policy: 未知 op\n"); + return MEOW_ERR_INVAL; +} + /* ---------------- 处理单条连接 ---------------- */ static void handle_conn(int c) { @@ -2263,7 +2548,35 @@ static void handle_conn(int c) send_frame(c, MSG_NET_DNS_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)"); + if (type == MSG_NET_POLICY) { + m_net_policy_t q; + memset(&q, 0, sizeof q); + if (rlen > sizeof q) rlen = sizeof q; + memcpy(&q, payload, rlen); + int is_write = (q.op != NET_POLICY_SHOW); /* SHOW=读级;SET_NET/SET_DOMAIN/DEL=写级 */ + m_net_policy_result_t res; + memset(&res, 0, sizeof res); + int rc = MEOW_OK; + if (is_write && !is_sys_identity(&caller)) { + rc = MEOW_ERR_PERM; + snprintf(res.text, sizeof res.text, "permission denied (policy write needs system identity)"); + net_log("warn", "write", "denied policy op=%d entity=%s caller_kind=%d uid=%d", + (int)q.op, q.entity, (int)caller.kind, (int)caller.uid); + } else if (is_write && !g_mode_meow) { + rc = MEOW_ERR_PERM; + snprintf(res.text, sizeof res.text, "readonly (HibyOS mode): policy write denied"); + net_log("warn", "write", "denied policy op=%d (HibyOS readonly)", (int)q.op); + } else { + rc = policy_cmd((int)q.op, q.entity, q.arg, res.text, sizeof res.text); + } + res.code = (uint32_t)rc; + 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_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)"); break; } } @@ -2680,11 +2993,32 @@ static int cli_dns(const char *cmd) return code == MEOW_OK ? 0 : 1; } +/* 期3 policy:show/set net/domain/del(IPC 0xEA;写级在服务端鉴权系统身份)。 */ +static int cli_policy(int op, const char *entity, const char *arg) +{ + m_net_policy_t q; + memset(&q, 0, sizeof q); + q.op = (uint32_t)op; + if (entity) snprintf(q.entity, sizeof q.entity, "%s", entity); + if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg); + char out[MEOW_IPC_MAX - 8]; + int fd = ipc_connect(); + if (fd < 0) { fprintf(stderr, "meow_net 守护不可达(%s)\n", g_sock); return 1; } + uint16_t rt; uint32_t code; + int rc = ipc_xchg(fd, MSG_NET_POLICY, &q, sizeof q, &rt, &code, out, sizeof out); + close(fd); + if (rc != 0) { fprintf(stderr, "meow_net policy: IPC 往返失败\n"); return 1; } + printf("%s", out); + return code == MEOW_OK ? 0 : 1; +} + static const char *usage(void) { return "status | iface [name] | wifi scan|networks|connect [,psk]|disconnect|on|off" " | bt status|on|off|paired | rfkill [ ] | udhcpc renew|release [if]" - " | apps | conns [entity] | top | dns status|stats|flush | help"; + " | apps | conns [entity] | top | dns status|stats|flush" + " | policy show [entity] | policy set allow|block | policy domain +host|-host | policy del " + " | help"; } static int cli_help(void) @@ -2700,6 +3034,7 @@ static int cli_run(int n, char **v) const char *cmd = v[0]; const char *arg = (n > 1) ? v[1] : NULL; const char *arg2 = (n > 2) ? v[2] : NULL; + const char *arg3 = (n > 3) ? v[3] : NULL; if (!strcmp(cmd, "status")) return cli_status(); if (!strcmp(cmd, "iface")) return cli_iface(arg); if (!strcmp(cmd, "help") || !strcmp(cmd, "-h") || !strcmp(cmd, "--help")) return cli_help(); @@ -2710,6 +3045,24 @@ static int cli_run(int n, char **v) if (!arg) { fprintf(stderr, "dns 子命令缺失(status|stats|flush)\n"); return 1; } return cli_dns(arg); } + if (!strcmp(cmd, "policy")) { + if (!arg) { fprintf(stderr, "policy 子命令缺失(show [entity] | set allow|block | domain +host|-host | del )\n"); return 1; } + if (!strcmp(arg, "show")) return cli_policy(NET_POLICY_SHOW, arg2, NULL); + if (!strcmp(arg, "set")) { + if (!arg2 || !arg3) { fprintf(stderr, "policy set 需 allow|block\n"); return 1; } + return cli_policy(NET_POLICY_SET_NET, arg2, arg3); + } + if (!strcmp(arg, "domain")) { + if (!arg2 || !arg3) { fprintf(stderr, "policy domain 需 +host|-host\n"); return 1; } + return cli_policy(NET_POLICY_SET_DOMAIN, arg2, arg3); + } + if (!strcmp(arg, "del")) { + if (!arg2) { fprintf(stderr, "policy del 需 \n"); return 1; } + return cli_policy(NET_POLICY_DEL, arg2, NULL); + } + fprintf(stderr, "policy 子命令未知(%s)\n", arg); + return 1; + } if (!strcmp(cmd, "wifi")) { if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; } if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL); diff --git a/patch_v2/meow_netmgrd/meow_netmgrd_host b/patch_v2/meow_netmgrd/meow_netmgrd_host index f4cf43c..d0e4d39 100755 Binary files a/patch_v2/meow_netmgrd/meow_netmgrd_host and b/patch_v2/meow_netmgrd/meow_netmgrd_host differ diff --git a/patch_v2/meow_netmgrd/netmgrd_authtest b/patch_v2/meow_netmgrd/netmgrd_authtest index 52216cb..7e0530c 100755 Binary files a/patch_v2/meow_netmgrd/netmgrd_authtest and b/patch_v2/meow_netmgrd/netmgrd_authtest differ diff --git a/patch_v2/meow_netmgrd/sockdestroy_probe.c b/patch_v2/meow_netmgrd/sockdestroy_probe.c new file mode 100644 index 0000000..8ec7cef --- /dev/null +++ b/patch_v2/meow_netmgrd/sockdestroy_probe.c @@ -0,0 +1,178 @@ +/* sockdestroy_probe.c — inet_diag SOCK_DESTROY 内核可用性探针(dev 工具,不入固件/不部署)。 + * + * 目的:内核 4.4(R3 Pro II)是否支持 netlink SOCK_DESTROY 连接级杀 socket。NETWORK_HW_FACTS §5 待验证项。 + * 安全:只对「本进程自建」的 127.0.0.1 临时 TCP 连接发 SOCK_DESTROY,绝不动设备现有连接。 + * + * 方法:建 loopback TCP 服务端+客户端,accept 得已连接 socket。用 inet_diag DUMP(SOCK_DIAG_BY_FAMILY) + * 列出全部 TCP,按 tuple(sport/dport/src/dst)匹配出自建 socket 的 idiag_cookie(与 ss -K 同法—— + * 无需 SO_COOKIE);再发 SOCK_DESTROY(tuple + dump cookie),对 accept 出的 socket recv 判是否已杀。 + * 输出:SD_DUMP_DUMPED= SD_MATCH=<0|1> SD_DESTROY_RC= SD_DESTROYED=<0|1> + * 结论:SD_DESTROY_RC==0 && SD_DESTROYED==1 → 内核支持;-EPERM → 需 root/CAP_NET_ADMIN;-EOPNOTSUPP/ENOENT → 不支持。 + * + * 用法:交叉编译后 scp 到设备 /usr/data 前台跑(root):`./sockdestroy_probe`(无副作用残留)。 + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int nl = -1; + +static void open_nl(void) +{ + nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG); + if (nl < 0) { perror("netlink socket"); exit(2); } + struct sockaddr_nl sa; memset(&sa, 0, sizeof sa); sa.nl_family = AF_NETLINK; + if (bind(nl, (struct sockaddr *)&sa, sizeof sa) < 0) { perror("bind"); exit(2); } +} + +/* dump 全部 TCP,找匹配 tuple 的卡片,取回 cookie。返回 0=找到。 */ +static int diag_find_cookie(uint16_t sport, uint16_t dport, uint32_t sip, uint32_t dip, + uint32_t *cookie_c0, uint32_t *cookie_c1) +{ + struct { + struct nlmsghdr nlh; + struct inet_diag_req_v2 req; + } r; + memset(&r, 0, sizeof r); + r.nlh.nlmsg_len = sizeof r; + r.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; + r.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + r.req.sdiag_family = AF_INET; + r.req.sdiag_protocol = IPPROTO_TCP; + r.req.idiag_states = 0xFFFFFFFF; + struct sockaddr_nl sa; memset(&sa, 0, sizeof sa); sa.nl_family = AF_NETLINK; + if (sendto(nl, &r, sizeof r, 0, (struct sockaddr *)&sa, sizeof sa) < 0) return -1; + + struct timeval tv = { 2, 0 }; + setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + int dumped = 0; + for (;;) { + char buf[8192]; + ssize_t n = recv(nl, buf, sizeof buf, 0); + if (n <= 0) break; + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, (unsigned)n); h = NLMSG_NEXT(h, n)) { + if (h->nlmsg_type == NLMSG_DONE) goto done; + if (h->nlmsg_type == NLMSG_ERROR) { int e = ((struct nlmsgerr *)NLMSG_DATA(h))->error; if (e) return e; continue; } + if (h->nlmsg_type == SOCK_DIAG_BY_FAMILY) { + struct inet_diag_msg *m = (struct inet_diag_msg *)NLMSG_DATA(h); + dumped++; + if (ntohs(m->id.idiag_sport) == sport && ntohs(m->id.idiag_dport) == dport && + (ntohl(m->id.idiag_src[0]) == sip) && (ntohl(m->id.idiag_dst[0]) == dip)) { + *cookie_c0 = m->id.idiag_cookie[0]; + *cookie_c1 = m->id.idiag_cookie[1]; + printf("SD_DUMPED=%d SD_MATCH=1\n", dumped); + return 0; + } + } + } + } +done: + printf("SD_DUMPED=%d SD_MATCH=0\n", dumped); + return -1; +} + +static int sd_try(uint16_t sport, uint16_t dport, uint32_t sip, uint32_t dip, + uint32_t c0, uint32_t c1, int with_cookie) +{ + struct { + struct nlmsghdr nlh; + struct inet_diag_req_v2 req; + } r; + memset(&r, 0, sizeof r); + r.nlh.nlmsg_len = sizeof r; + r.nlh.nlmsg_type = SOCK_DESTROY; + r.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + r.req.sdiag_family = AF_INET; + r.req.sdiag_protocol = IPPROTO_TCP; + r.req.idiag_states = 0xFFFFFFFF; + r.req.id.idiag_sport = htons(sport); + r.req.id.idiag_dport = htons(dport); + r.req.id.idiag_src[0] = htonl(sip); + r.req.id.idiag_dst[0] = htonl(dip); + r.req.id.idiag_cookie[0] = with_cookie ? c0 : 0; + r.req.id.idiag_cookie[1] = with_cookie ? c1 : 0; + struct sockaddr_nl sa; memset(&sa, 0, sizeof sa); sa.nl_family = AF_NETLINK; + if (sendto(nl, &r, sizeof r, 0, (struct sockaddr *)&sa, sizeof sa) < 0) return -errno; + char buf[512]; + struct timeval tv = { 0, 300000 }; + setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + ssize_t rr = recv(nl, buf, sizeof buf, 0); + if (rr < 0) return -errno; + struct nlmsghdr *h = (struct nlmsghdr *)buf; + if (h->nlmsg_type == NLMSG_ERROR) return ((struct nlmsgerr *)NLMSG_DATA(h))->error; + return 0; +} + +static int check_destroyed(int fd) +{ + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + char b[16]; + ssize_t r = recv(fd, b, sizeof b, 0); + if (r == 0) return 1; + if (r < 0) return (errno == ECONNRESET || errno == ECONNABORTED) ? 1 : 0; + return 0; +} + +int main(void) +{ + int lfd = socket(AF_INET, SOCK_STREAM, 0); + int cfd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(0); + int one = 1; + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + bind(lfd, (struct sockaddr *)&addr, sizeof addr); + listen(lfd, 8); + socklen_t alen = sizeof addr; + getsockname(lfd, (struct sockaddr *)&addr, &alen); + connect(cfd, (struct sockaddr *)&addr, sizeof addr); + int sfd = accept(lfd, NULL, NULL); + struct sockaddr_in sa, sc; + socklen_t sla = sizeof sa, slc = sizeof sc; + getsockname(sfd, (struct sockaddr *)&sa, &sla); + getpeername(sfd, (struct sockaddr *)&sc, &slc); + uint16_t sport = ntohs(sa.sin_port); + uint16_t dport = ntohs(sc.sin_port); + uint32_t sip = ntohl(sa.sin_addr.s_addr); + uint32_t dip = ntohl(sc.sin_addr.s_addr); + open_nl(); + + uint32_t c0 = 0, c1 = 0; + int found = (diag_find_cookie(sport, dport, sip, dip, &c0, &c1) == 0); + if (found) { + /* 主测靠谱路径:cookie 版(与 ss -K 同法;无需 SO_COOKIE)。 */ + int rc_c = sd_try(sport, dport, sip, dip, c0, c1, 1); + int dead_c = check_destroyed(sfd); + printf("SD_DESTROY(cookie) rc=%d destroyed=%d\n", rc_c, dead_c); + if (dead_c) { + printf("CONCL=SOCK_DESTROY_SUPPORTED\n"); + } else if (rc_c == -EPERM) { + printf("CONCL=SOCK_DESTROY_NEED_CAP(rc=-EPERM; 需 root/CAP_NET_ADMIN)\n"); + } else if (rc_c == 0) { + printf("CONCL=SOCK_DESTROY_NOOP(rc=0 但未杀; 内核未真正销毁——视为不可用)\n"); + } else { + printf("CONCL=SOCK_DESTROY_UNSUPPORTED(rc=%d)\n", rc_c); + } + /* 附带:tuple+cookie=0 结果(诊断参考) */ + int rc_t = sd_try(sport, dport, sip, dip, c0, c1, 0); + printf("SD_DESTROY(tuple,cookie=0) rc=%d destroyed=%d\n", rc_t, check_destroyed(sfd)); + } else { + printf("CONCL=SOCK_DESTROY_UNKNOWN(dump no match)\n"); + } + close(cfd); close(sfd); close(lfd); + return 0; +} diff --git a/patch_v2/meow_netmgrd/test_netmgrd.sh b/patch_v2/meow_netmgrd/test_netmgrd.sh index 0cd2bd6..3e6ba5d 100755 --- a/patch_v2/meow_netmgrd/test_netmgrd.sh +++ b/patch_v2/meow_netmgrd/test_netmgrd.sh @@ -43,6 +43,7 @@ cat > "$T/tcp" <<'EOF' 0: 32C14E0A:5137 0905CD8C:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0 0 10 0 1: 32C14E0A:4F32 32C34E0A:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 12346 1 0 0 10 0 2: 32C14E0A:0A00 00000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 67890 1 0 0 10 0 + 3: 32C14E0A:4F33 32C34E0A:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 0 1 0 0 10 0 EOF cat > "$T/udp" <<'EOF' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode @@ -232,6 +233,25 @@ ck T13c "IFACE_ALL VALID" "$JOUT" ck T13d "IFACE_WLAN VALID" "$JOUT" echo "$JOUT" | grep -q "INVALID" && { FAIL=$((FAIL+1)); echo "FAIL T13e 存在无效 JSON"; } || { PASS=$((PASS+1)); echo "PASS T13e no INVALID"; } +# ---- T15: apps 无原始端点(tcp 桩含 inode=0 未归因 → proc:unknown,绝不落成 IP:port entity)---- +OUT=$(env $ENVS $Q "$T/meow_net" conns 2>&1) +ck T15a "10.78.195.50:443" "$OUT" # inode=0 的连接仍在表中 +OUT=$(env $ENVS $Q "$T/meow_net" apps 2>&1) +ck T15b "proc:unknown" "$OUT" +echo "$OUT" | grep -qE '^[0-9][0-9.]*:' && { FAIL=$((FAIL+1)); echo "FAIL T15c apps 出现原始端点行"; } || { PASS=$((PASS+1)); echo "PASS T15c no raw endpoint row"; } + +# ---- T16: policy(读级 show 任意调用者;写级仅系统身份 → 宿主 CLI=CALL_OTHER 拒)---- +OUT=$(env $ENVS $Q "$T/meow_net" policy show 2>&1) +ck T16a "app:dev.player.ncm" "$OUT" +ck T16b "domain=" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_net" policy set app:dev.player.ncm allow 2>&1); RC=$? +ck T16c "permission denied" "$OUT" +[ $RC -eq 1 ] && { PASS=$((PASS+1)); echo "PASS T16d rc=1"; } || { FAIL=$((FAIL+1)); echo "FAIL T16d rc=$RC"; } +OUT=$(env $ENVS $Q "$T/meow_net" policy domain app:dev.player.ncm +x 2>&1) +ck T16e "permission denied" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_net" policy del app:dev.player.ncm 2>&1) +ck T16f "permission denied" "$OUT" + # ---- T14: DNS 代理 wire 实测(守护 B:高端口 + 桩上游 + 全局 deny;测 deny/forward/cache)---- HOSTIP=$(hostname -I 2>/dev/null | awk '{print $1}') [ -n "$HOSTIP" ] || HOSTIP="127.0.0.1" diff --git a/patch_v2/meow_netmgrd/test_netmgrd_p2 b/patch_v2/meow_netmgrd/test_netmgrd_p2 index b0e1c32..0a9ce02 100755 Binary files a/patch_v2/meow_netmgrd/test_netmgrd_p2 and b/patch_v2/meow_netmgrd/test_netmgrd_p2 differ diff --git a/patch_v2/meow_netmgrd/test_netmgrd_p3 b/patch_v2/meow_netmgrd/test_netmgrd_p3 new file mode 100755 index 0000000..2a9f51e Binary files /dev/null and b/patch_v2/meow_netmgrd/test_netmgrd_p3 differ diff --git a/patch_v2/meow_netmgrd/test_netmgrd_p3.c b/patch_v2/meow_netmgrd/test_netmgrd_p3.c new file mode 100644 index 0000000..c6b8258 --- /dev/null +++ b/patch_v2/meow_netmgrd/test_netmgrd_p3.c @@ -0,0 +1,119 @@ +/* test_netmgrd_p3.c — meow_netmgrd 期3 管控层 纯逻辑宿主自测(本程序非生产代码)。 + * + * 通过 #include 生产源,同一 TU 直接访问 FILE-STATIC:policy_cmd / rules_load / policy_persist_entity / + * policy_net_blocked / dns_query_domain_denied / is_self_lock / policy_find / g_policy_log_level。 + * 覆盖:policy set net(per-entity 持久化读改写)、防自锁拒绝 + 加载校验剔除、domain 白/黑、 + * net=block → DNS 层全量 deny、del、global log_level、RMW 保留他行。真实 IPC 鉴权(CALL_OTHER 拒) + * 由 test_netmgrd.sh 覆盖。 + * + * 用法:cc -Wall -Wextra -I../meow_launcherd test_netmgrd_p3.c -o test_netmgrd_p3 && ./test_netmgrd_p3 + */ +#define main meow_netmgrd_prod_main_unused +#include "meow_netmgrd.c" +#undef main + +#include +#include +#include +#include +#include + +static int g_fail = 0; +#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \ + else { printf(" ok : %s\n", #cond); } } while (0) +static char rd[512]; +static const char *readfile(const char *p) +{ + FILE *f = fopen(p, "r"); + if (!f) { rd[0] = 0; return rd; } + size_t n = fread(rd, 1, sizeof rd - 1, f); rd[n] = 0; fclose(f); + return rd; +} + +int main(void) +{ + static const char *T = "/tmp/nmg_p3"; + char policy[192]; + mkdir(T, 0755); + snprintf(policy, sizeof policy, "%s/policy", T); + g_net_policy[0] = 0; snprintf(g_net_policy, sizeof g_net_policy, "%s", policy); + FILE *f = fopen(policy, "w"); /* 先建空 policy 文件 */ + if (f) { fputs("# boot\n", f); fclose(f); } + + char out[512]; + int rc; + + printf("== policy_cmd SET_NET(per-entity 持久化)==\n"); + rules_load(); + CHECK(g_rule_n == 0); + rc = policy_cmd(NET_POLICY_SET_NET, "proc:thttpd", "block", out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(strstr(readfile(policy), "proc:thttpd net=block") != NULL); + CHECK(policy_net_blocked("proc:thttpd") == 1); + CHECK(policy_net_blocked("proc:unknown") == 0); /* 未显式规则 → 默认 allow */ + + printf("== 防自锁:拒绝 block + 加载校验剔除 ==\n"); + rc = policy_cmd(NET_POLICY_SET_NET, "proc:dropbear", "block", out, sizeof out); + CHECK(rc == MEOW_ERR_PERM); + CHECK(strstr(out, "防自锁") != NULL); + CHECK(policy_net_blocked("proc:dropbear") == 0); + CHECK(policy_net_blocked("svc:meow_netmgrd") == 0); + /* 手工改坏文件塞入 dropbear net=block → 加载校验剔除 */ + f = fopen(policy, "w"); + if (f) { fputs("proc:dropbear net=block\nproc:thttpd net=block\n", f); fclose(f); } + rules_load(); + CHECK(policy_net_blocked("proc:dropbear") == 0); + CHECK(policy_net_blocked("proc:thttpd") == 1); + + printf("== policy SET_DOMAIN(白/黑;allow 覆盖)+ net=block → DNS 全量 deny ==\n"); + rc = policy_cmd(NET_POLICY_SET_DOMAIN, "proc:web", "+music.163.com", out, sizeof out); + CHECK(rc == MEOW_OK); + rc = policy_cmd(NET_POLICY_SET_DOMAIN, "proc:web", "-ads.example.com", out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(strstr(readfile(policy), "proc:web domain=+music.163.com,-ads.example.com") != NULL); + CHECK(dns_query_domain_denied("proc:web", "music.163.com") == 0); /* 白名单允许 */ + CHECK(dns_query_domain_denied("proc:web", "p.music.163.com") == 0); /* 子域也允许 */ + CHECK(dns_query_domain_denied("proc:web", "ads.example.com") == 1); /* 黑名单拒绝 */ + CHECK(dns_query_domain_denied("proc:web", "other.com") == 0); + /* net=block 的实体 → DNS 全量 deny(即使白名单);且已有 domain 规则的实体被 block 后同样全 deny */ + rc = policy_cmd(NET_POLICY_SET_NET, "proc:web", "block", out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(dns_query_domain_denied("proc:web", "music.163.com") == 1); /* block 覆盖白名单 */ + rc = policy_cmd(NET_POLICY_SET_NET, "app:dev.player.ncm", "block", out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(dns_query_domain_denied("app:dev.player.ncm", "music.163.com") == 1); + + printf("== del 移除实体(RMW 保留其它行)==\n"); + rc = policy_cmd(NET_POLICY_DEL, "proc:thttpd", NULL, out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(strstr(readfile(policy), "proc:thttpd") == NULL); + CHECK(strstr(readfile(policy), "app:dev.player.ncm net=block") != NULL); /* 其它实体行保留 */ + /* RMW 保留注释/无关行(写回前 seed 应有 # 头) */ + f = fopen(policy, "w"); + if (f) { fputs("# commentary\nproc:thttpd net=block\napp:dev.player.ncm domain=-tracker.net\n", f); fclose(f); } + rules_load(); + rc = policy_cmd(NET_POLICY_SET_NET, "app:dev.player.ncm", "block", out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(strstr(readfile(policy), "# commentary") != NULL); + CHECK(strstr(readfile(policy), "app:dev.player.ncm domain=-tracker.net") != NULL); /* domain 行保留 */ + CHECK(strstr(readfile(policy), "app:dev.player.ncm net=block") != NULL); + + printf("== global log_level ==\n"); + f = fopen(policy, "w"); + if (f) { fputs("global log_level=debug\n", f); fclose(f); } + rules_load(); + CHECK(strcmp(g_policy_log_level, "debug") == 0); + + printf("== del 到空文件(write 0 字节场景)==\n"); + f = fopen(policy, "w"); + if (f) { fputs("proc:thttpd net=block\n", f); fclose(f); } + rules_load(); + rc = policy_cmd(NET_POLICY_DEL, "proc:thttpd", NULL, out, sizeof out); + CHECK(rc == MEOW_OK); + CHECK(strstr(readfile(policy), "proc:thttpd") == NULL); /* 文件已无该实体(可为空) */ + CHECK(g_rule_n == 0); + + if (g_fail) { printf("\nNETMGR P3 TEST: %d FAIL\n", g_fail); return 1; } + printf("\nNETMGR P3 TEST: ALL PASS\n"); + return 0; +}