meow_netmgrd 期3:管控层(三实体策略引擎+防自锁+SOCK_DESTROY 实测→仅 DNS 层)

NETMGR_DESIGN.md §6/§13 期3 落地(全在 patch_v2/meow_netmgrd/ + meow_ipc.h 追加):
1. 策略引擎全量:/data/meow/netmgrd/net_policy.conf(/data=/usr/data)读改写单键+原子写
   (temp+rename,绝不 O_TRUNC 整文件重写——watchdog 毁设置教训);per-实体(app:/svc:/proc:)
   net=allow|block + domain 白/黑(接通 DNS 层执行)+ global log_level;默认 allow。
2. 防自锁硬编码:dropbear/wpa_supplicant/udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init 拒绝 block
   (CLI/IPC 拒 + 加载校验把手工改坏的文件里这些实体的 net=block 同样强制复位 allow——§14 红线)。
3. SOCK_DESTROY 真机实测(sockdestroy_probe.c,root @内核4.4):**内核接受 SOCK_DESTROY 请求(rc=0)
   但未真正销毁 socket(cookie=0 与 dump-cookie 两法均 dead=0)→ 判定 no-op/不可用**。
   ⇒ 连接级执行降级为「仅 DNS 层生效」(net=block → dns_query_domain_denied 全量 deny,DNS 层落地),
   policy show 头 enfor=dns_only 明示。SDK 自愿通道/设置页迁移期外。
4. IPC 0x00EA MSG_NET_POLICY(op: show/set net/set domain/del)+ RESULT 0x00EB:读级任意、写级仅系统身份。
   CLI:policy show [entity] | set <entity> allow|block | domain <entity> +host|-host | del <entity>。
5. 两小修:①classify_exe 增认 MeowPanel 精确路径(修面板 dns flush 403;面板已有 PIN 门禁)
   ②连接未归因(inode=0/查不到)一律落 proc:unknown——修 apps 表出现「IP:port」原始端点行。
6. WiFi connect 持久化:MeowOS 模式 connect 后 wpa_cli save_config(HibyOS 只读由服务端门控)。

验证:宿主 test_netmgrd.sh 61/61 + qemu 61/61;test_netmgrd_p2/p3 宿主+qemu 全过;netmgrd_authtest 全过;
交叉 -Wall -Wextra 0 warning。真机只读实测(DNS-on 固件共存,用独立 socket+/tmp 临时 policy.json):
policy set/domain/del 全链 + 防自锁拒绝 proc:dropbear(rc=1) + 空文件 del(size=0) + apps 无原始端点 +
resolv.conf 未触碰(测后核验固件两行形式);Firmware meow_netmgrd(pid 8142) running 未动。

新 md5:meow_netmgrd 62369ebe29e7ef4b55067a0f14e240d0(主会话更 fw_content.conf/热更设备)。
This commit is contained in:
MeowOS committed 2026-09-01 22:08:10 +08:00
1 parent 7ba0fde3e6
commit b37dc23cd9
10 files changed
+737 -35

No files matched your search

+32
View File
@@ -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 <entity> allow|block */
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
NET_POLICY_DEL = 3 /* del <entity> */
};
typedef struct m_net_policy {
uint32_t op; /* NET_POLICY_* */
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | 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 */
Binary file not shown.
+388 -35
View File
@@ -46,8 +46,11 @@
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <linux/netlink.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#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=<name> ... 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=<lvl> */
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:<pkg> / svc:<svc> / proc:<comm>) ---- */
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 需 <entity> 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 需 <entity> +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 需 <entity>\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 <ssid>[,psk]|disconnect|on|off"
" | bt status|on|off|paired | rfkill [<type> <on|off>] | 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 <entity> allow|block | policy domain <entity> +host|-host | policy del <entity>"
" | 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 <entity> allow|block | domain <entity> +host|-host | del <entity>)\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 需 <entity> 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 需 <entity> +host|-host\n"); return 1; }
return cli_policy(NET_POLICY_SET_DOMAIN, arg2, arg3);
}
if (!strcmp(arg, "del")) {
if (!arg2) { fprintf(stderr, "policy del 需 <entity>\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);
Binary file not shown.
Binary file not shown.
+178
View File
@@ -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=<n> SD_MATCH=<0|1> SD_DESTROY_RC=<errno|0> 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <linux/netlink.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
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;
}
+20
View File
@@ -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"
Binary file not shown.
Binary file not shown.
+119
View File
@@ -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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
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;
}