diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index 9f673c6..6e7a1c6 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -564,4 +564,41 @@ typedef struct m_net_cmd_result { char text[MEOW_IPC_MAX - 8]; } m_net_cmd_result_t; +/* ---- 期 2:观测层(连接普查)+ DNS 代理(NETMGR_DESIGN §4/§5)---- + * 0x00E8/0x00EC 实现取代上方「0x00E8-0x00EF 保留」注释中的连接表/DNS 控制两项; + * 0x00EA(策略读写,期3)/0x00EE(事件推送,期2 订阅制预留)仍保留未实装。 */ +enum { /* 与主 enum 分列,追加式不改既有行 */ + MSG_NET_CONNS_REQ = 0x00E8, /* ↑ 连接表查询(entity 空=全部) */ + MSG_NET_CONNS_RESULT = 0x00E9, /* ↓ 连接表文本(定长行;超帧截断标记) */ + MSG_NET_DNS_CTL = 0x00EC, /* ↑ dns status|stats|flush */ + MSG_NET_DNS_RESULT = 0x00ED /* ↓ 结果/统计文本 */ + /* 0x00EA 策略读写(期3)/ 0x00EE 事件推送(预留,期2 订阅制)/ 0xEB、0xEF 余量 */ +}; + +/* MSG_NET_CONNS_REQ ↑:entity 前缀过滤(空=全部)。entity 形如 `app:`/`svc:`/`proc:`。 */ +typedef struct m_net_conns_req { + char entity[48]; +} m_net_conns_req_t; /* 48 B */ + +/* MSG_NET_CONNS_RESULT ↓:连接表文本(每行 `entity|lip:lport|rip:rport|state|age_s|domain`)。 + * 超单帧上限时截断并置 truncated=1(app/top 由客户端按行聚合,容纳全部)。 */ +typedef struct m_net_conns_result { + uint32_t code; /* MEOW_OK / MEOW_ERR_* */ + uint16_t text_len; + uint16_t truncated; /* 1=被截断 */ + char text[MEOW_IPC_MAX - 8]; +} m_net_conns_result_t; + +/* MSG_NET_DNS_CTL ↑:cmd=status|stats|flush。status/stats 读级任意;flush(清缓存)为动作级系统身份。 */ +typedef struct m_net_dns_ctl { + char cmd[16]; +} m_net_dns_ctl_t; /* 16 B */ + +typedef struct m_net_dns_result { + uint32_t code; + uint16_t text_len; + uint16_t pad; + char text[MEOW_IPC_MAX - 8]; +} m_net_dns_result_t; + #endif /* MEOW_IPC_H */ diff --git a/patch_v2/meow_netmgrd/meow_netmgrd b/patch_v2/meow_netmgrd/meow_netmgrd index 4fa5774..adf6174 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 9011264..5e894e5 100644 --- a/patch_v2/meow_netmgrd/meow_netmgrd.c +++ b/patch_v2/meow_netmgrd/meow_netmgrd.c @@ -44,8 +44,10 @@ #include #include #include +#include +#include -#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00E7) + m_net_*(号段 0x00E0-0x00EF) */ +#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00ED) + m_net_*(号段 0x00E0-0x00EF) */ /* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */ static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */ @@ -67,13 +69,38 @@ static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUET static const char *g_ip = "ip"; /* MEOW_IP */ static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */ +/* 期 2:观测层 + DNS 代理 相关路径(可经 env 覆盖,宿主/qemu 桩测试用) */ +static char g_proc_tcp[160] = "/proc/net/tcp"; /* MEOW_TCP */ +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 只读静态域名规则) */ + #define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */ #define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */ #define IFACE_MAX 16 #define RFKILL_MAX 8 #define DNS_MAX 8 +/* ---- 期 2 常量(NETMGR_DESIGN §4/§5/§8;全部静态表,禁 malloc) ---- */ +#define CONN_MAX 512 /* 连接表上限 */ +#define DNS_CACHE_MAX 256 /* DNS 应答缓存上限 */ +#define DNS_UPSTREAM_TMO_MS 800 /* DNS 上游转发超时 */ +#define DNS_QNAME_MAX 128 /* query 域名最大字节 */ +#define ENTITY_MAX 128 /* pid→entity 缓存条数 */ +#define PORT_ENT_MAX 128 /* udp 本地端口→entity 缓存(DNS 归因用) */ +#define RULE_ENT_MAX 16 /* 域名规则实体上限 */ +#define RULE_HOST_MAX 24 /* 单实体 allow/deny 主机规则上限 */ +#define EVT_DUP_MAX 64 /* 事件日志 1 分钟去重表 */ +#define DNS_RESOLV_CHECK_SEC 5 /* resolv.conf 自检周期 */ + +#define DNS_PORT_DEF 53 /* 监听端口(MEOW_DNS_PORT 覆盖;宿主测试用高位规避 <1024 权限) */ +#define DNS_SELF_IP "127.0.0.1" + static int g_poll_sec = NET_POLL_SEC_DEF; +static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */ +static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */ static long mono_now(void) { @@ -902,6 +929,801 @@ static int build_iface_json(const char *name, char *out, size_t cap) return MEOW_ERR_NOENT; } +/* ================= 期 2:观测层(连接普查)+ DNS 代理(NETMGR_DESIGN §4/§5/§8) ================= */ +/* ---- 连接/实体/DNS/规则/事件去重 静态表(禁 malloc) ---- */ +typedef struct { + uint32_t local_ip; /* dotted-printable uint32(b0<<24|b1<<16|b2<<8|b3) */ + uint32_t remote_ip; + uint16_t local_port; + uint16_t remote_port; + uint8_t state_code; /* tcp state hex;udp 用 0 */ + uint8_t type; /* 0=tcp 1=udp */ + uint32_t inode; /* socket inode(归因键;0=无法归因) */ + int pid; /* 持有 socket 的 pid(0=未知) */ + char entity[48]; /* app: | svc: | proc: | proc:unknown */ + char domain[48]; /* 远端 IP 反查域名(DNS 缓存;空=未命中) */ + long mono_open; /* 首次见此连接(mono 秒) */ + long last_seen; /* 最近出现 */ + int seen_gen; /* 本普查代际标记(防跨拍误判) */ +} conn_t; +static conn_t g_conn[CONN_MAX]; +static int g_conn_n = 0; +static int g_gen = 0; + +typedef struct { char name[64]; uint32_t ip; uint16_t ttl; long exp; } dns_cache_t; +static dns_cache_t g_dns[DNS_CACHE_MAX]; +static int g_dns_n = 0; + +typedef struct { int pid; char entity[48]; } pid_ent_t; +static pid_ent_t g_pid_ent[ENTITY_MAX]; +static int g_pid_ent_n = 0; + +typedef struct { uint16_t port; char entity[48]; } port_ent_t; +static port_ent_t g_port_ent[PORT_ENT_MAX]; +static int g_port_ent_n = 0; + +typedef struct { + char entity[48]; + char allow[RULE_HOST_MAX][48]; int allow_n; + char deny[RULE_HOST_MAX][48]; int deny_n; +} rule_t; +static rule_t g_rules[RULE_ENT_MAX]; +static int g_rule_n = 0; + +typedef struct { char key[96]; long mono; } evt_dup_t; +static evt_dup_t g_evt_dup[EVT_DUP_MAX]; +static int g_evt_dup_n = 0; + +/* ---- DNS 代理状态 ---- */ +static int g_dns_enabled = 0; +static int g_dns_modified_resolv = 0; /* resolv.conf 已被改写(需恢复) */ +static int g_dns_fd = -1; /* 127.0.0.1:port UDP 监听 */ +static int g_dns_up_fd = -1; /* 上游转发 socket */ +static char g_dns_upstream[40] = ""; /* 原上游 nameserver IP */ +static int g_dns_upstream_port = 53; /* 上游端口(MEOW_DNS_UPSTREAM_PORT,宿主测试用) */ +static char g_resolv_backup[384] = ""; /* 原 resolv.conf 内容 */ +static uint64_t g_dns_q = 0, g_dns_deny = 0, g_dns_first = 0; +static long g_last_resolv_check = 0; +static volatile sig_atomic_t g_hup = 0; + +/* ---- 通用工具 ---- */ +static void ip_fmt(uint32_t ip, char *out, size_t cap) +{ + snprintf(out, cap, "%u.%u.%u.%u", (ip >> 24) & 0xff, (ip >> 16) & 0xff, + (ip >> 8) & 0xff, ip & 0xff); +} +/* /proc/net/{tcp,udp} 的十六进制地址 → dotted-printable uint32(LE 字节序还原)。 */ +static uint32_t hex_ip(const char *hex) +{ + uint32_t v = (uint32_t)strtoul(hex, NULL, 16); + uint32_t b0 = v & 0xff, b1 = (v >> 8) & 0xff, b2 = (v >> 16) & 0xff, b3 = (v >> 24) & 0xff; + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; +} +static const char *tcp_state_str(uint8_t s) +{ + switch (s) { + case 0x01: return "ESTAB"; + case 0x02: return "SYN_SENT"; + case 0x03: return "SYN_RECV"; + case 0x04: return "FIN_WAIT1"; + case 0x05: return "FIN_WAIT2"; + case 0x06: return "TIME_WAIT"; + case 0x07: return "CLOSE"; + case 0x08: return "CLOSE_WAIT"; + case 0x09: return "LAST_ACK"; + case 0x0A: return "LISTEN"; + case 0x0B: return "CLOSING"; + default: return "??"; + } +} + +/* AF_UNIX 文本请求/响应(svcd/amsd 用;send 后 shutdown(SHUT_WR) 触发服务端 EOF 处理,读至对端关闭)。 */ +static int sock_xchg_text(const char *sockpath, const char *send, char *resp, int cap) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct timeval tv = { 1, 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, sockpath, sizeof a.sun_path - 1); + if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } + if (send && write(fd, send, strlen(send)) < 0) { close(fd); return -1; } + shutdown(fd, SHUT_WR); /* 通知服务端 EOF(amsd conn_finish 才响应) */ + int n = 0; + while (n < cap - 1) { + ssize_t r = read(fd, resp + n, (size_t)(cap - 1 - n)); + if (r <= 0) break; + n += (int)r; + } + close(fd); + if (n > 0) resp[n] = 0; else resp[0] = 0; + return n > 0 ? 0 : -1; +} + +/* ---- DNS 缓存(≤256,TTL 尊重) ---- */ +static int dns_cache_insert(const char *name, uint32_t ip, uint16_t ttl) +{ + long now = mono_now(); + long exp = now + (ttl ? ttl : 60); + if (exp - now > 86400) exp = now + 86400; + for (int i = 0; i < g_dns_n; i++) { + if (strcmp(g_dns[i].name, name) == 0) { g_dns[i].ip = ip; g_dns[i].exp = exp; return 0; } + } + /* 找空槽或最旧 */ + int slot = -1; long oldest = 0; + for (int i = 0; i < DNS_CACHE_MAX; i++) { + if (g_dns[i].name[0] == 0) { slot = i; break; } + if (g_dns[i].exp < oldest) { oldest = g_dns[i].exp; slot = i; } + } + if (slot < 0) slot = g_dns_n < DNS_CACHE_MAX ? g_dns_n++ : 0; + if (g_dns_n < DNS_CACHE_MAX && slot == g_dns_n) g_dns_n++; + snprintf(g_dns[slot].name, sizeof g_dns[slot].name, "%s", name); + g_dns[slot].ip = ip; g_dns[slot].ttl = ttl; g_dns[slot].exp = exp; + return 0; +} +static int dns_cache_lookup_ip(uint32_t ip, char *out, size_t cap) +{ + long now = mono_now(); + for (int i = 0; i < g_dns_n; i++) { + if (g_dns[i].name[0] && g_dns[i].ip == ip && g_dns[i].exp > now) { + if (out && cap) snprintf(out, cap, "%s", g_dns[i].name); + return 0; + } + } + return -1; +} +static int dns_cache_lookup_name(const char *name) +{ + long now = mono_now(); + for (int i = 0; i < g_dns_n; i++) + if (strcmp(g_dns[i].name, name) == 0 && g_dns[i].exp > now) return 0; + return -1; +} +static void dns_cache_flush(void) +{ + for (int i = 0; i < g_dns_n; i++) g_dns[i].name[0] = 0; + g_dns_n = 0; +} + +/* ---- 域名规则(期2 静态 conf 只读;期3 策略引擎再接管) ---- */ +static int suffix_match(const char *host, const char *rule) +{ + if (!host[0] || !rule[0]) return 0; + size_t hl = strlen(host), rl = strlen(rule); + if (hl < rl) return 0; + 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) +{ + return r->entity[0] && strcmp(r->entity, entity) == 0; +} +/* deny 判定:命中最具体的实体规则(entity+global),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 (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; + } + } + return 0; +} +static void rules_load(void) +{ + g_rule_n = 0; + FILE *f = fopen(g_net_policy, "r"); + if (!f) return; + char line[256]; + while (fgets(line, sizeof line, f) && g_rule_n < RULE_ENT_MAX) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p == '#' || *p == '\n' || !*p) continue; + char *nl = strchr(p, '\n'); if (nl) *nl = 0; + 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 解析 */ + 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 (r->deny_n || r->allow_n) g_rule_n++; + } + fclose(f); +} + +/* ---- 实体归因:pid→(app: / svc: / proc:) ---- */ +static void pid_ent_put(int pid, const char *entity) +{ + if (pid <= 0) return; + for (int i = 0; i < g_pid_ent_n; i++) + if (g_pid_ent[i].pid == pid) { snprintf(g_pid_ent[i].entity, sizeof g_pid_ent[i].entity, "%s", entity); return; } + if (g_pid_ent_n < ENTITY_MAX) { + g_pid_ent[g_pid_ent_n].pid = pid; + snprintf(g_pid_ent[g_pid_ent_n].entity, sizeof g_pid_ent[g_pid_ent_n].entity, "%s", entity); + g_pid_ent_n++; + } +} +static void refresh_entity_maps(void) +{ + g_pid_ent_n = 0; + char resp[2048]; + /* amsd:`status` → "RUN " */ + if (sock_xchg_text(g_ams_sock, "status\n", resp, sizeof resp) == 0) { + char *ln = resp; + while (ln && *ln) { + char *nl = strchr(ln, '\n'); if (nl) *nl = 0; + char *tok[8]; int nt = 0; + char *save = NULL; + for (char *t = strtok_r(ln, " \t", &save); t && nt < 8; t = strtok_r(NULL, " \t", &save)) tok[nt++] = t; + if (nt >= 4 && strcmp(tok[0], "RUN") == 0) { + int pid = atoi(tok[3]); + if (pid > 0) { char ent[48]; snprintf(ent, sizeof ent, "app:%s", tok[1]); pid_ent_put(pid, ent); } + } + if (!nl) break; + ln = nl + 1; + } + } + /* svcd:优先读落盘状态文件(机器可读 `svc= state=... pid=...`)——socket `status` 是 ANSI 表。 + * 文件缺失回退 socket 查询(解析 `svc=` 行)。 */ + { + FILE *f = fopen(g_svcd_status, "r"); + if (f) { + char line[512]; + while (fgets(line, sizeof line, f)) { + if (strncmp(line, "svc=", 4) != 0) continue; + char name[64] = ""; int pid = 0; + char *q = line + 4; + char *sp = strchr(q, ' '); if (sp) *sp = 0; + snprintf(name, sizeof name, "%s", q); + char *ps = strstr(sp ? sp + 1 : line, "pid="); + if (ps) pid = atoi(ps + 4); + if (pid > 0) { char ent[48]; snprintf(ent, sizeof ent, "svc:%s", name); pid_ent_put(pid, ent); } + } + fclose(f); + } else if (sock_xchg_text(g_svcd_sock, "status\n", resp, sizeof resp) == 0) { + char *ln = resp; + while (ln && *ln) { + char *nl = strchr(ln, '\n'); if (nl) *nl = 0; + if (strncmp(ln, "svc=", 4) == 0) { + char name[64] = ""; int pid = 0; + char *q = ln + 4; + char *sp = strchr(q, ' '); if (sp) *sp = 0; + snprintf(name, sizeof name, "%s", q); + char *ps = strstr(sp ? sp + 1 : ln, "pid="); + if (ps) pid = atoi(ps + 4); + if (pid > 0) { char ent[48]; snprintf(ent, sizeof ent, "svc:%s", name); pid_ent_put(pid, ent); } + } + if (!nl) break; + ln = nl + 1; + } + } + } + /* 其余 pid(本表不含)→ 归 proc:,由 entity_for_pid 补 */ +} +static const char *entity_for_pid(int pid, char *out, size_t cap) +{ + for (int i = 0; i < g_pid_ent_n; i++) + if (g_pid_ent[i].pid == pid) { snprintf(out, cap, "%s", g_pid_ent[i].entity); return out; } + char p[64], comm[64] = ""; + snprintf(p, sizeof p, "/proc/%d/comm", pid); + read_file_txt(p, comm, sizeof comm); + if (comm[0]) snprintf(out, cap, "proc:%s", comm); + else snprintf(out, cap, "proc:unknown"); + return out; +} + +/* 由 /proc/net/{tcp,udp} 的 inode 集驱动扫 /proc//fd(不逐连接盲扫)。 */ +static void attribute_inodes(conn_t *cur, int n) +{ + DIR *pd = opendir("/proc"); + if (!pd) return; + struct dirent *de; + while ((de = readdir(pd))) { + if (!isdigit((unsigned char)de->d_name[0])) continue; + int pid = atoi(de->d_name); + if (pid <= 0) continue; + char fddir[64]; + snprintf(fddir, sizeof fddir, "/proc/%d/fd", pid); + DIR *fd = opendir(fddir); + if (!fd) continue; + struct dirent *fe; + while ((fe = readdir(fd))) { + if (fe->d_name[0] == '.') continue; + char lnk[72], target[64]; + snprintf(lnk, sizeof lnk, "/proc/%d/fd/%s", pid, fe->d_name); + ssize_t r = readlink(lnk, target, sizeof target - 1); + if (r <= 0) continue; + target[r] = 0; + if (strncmp(target, "socket:[", 8) != 0) continue; + uint32_t ino = (uint32_t)strtoul(target + 8, NULL, 10); + for (int i = 0; i < n; i++) { + if (cur[i].inode == ino && cur[i].pid == 0) { + cur[i].pid = pid; + char ent[48]; + entity_for_pid(pid, ent, sizeof ent); + snprintf(cur[i].entity, sizeof cur[i].entity, "%s", ent); + break; + } + } + } + closedir(fd); + } + closedir(pd); +} + +/* UDP 本地端口→entity(DNS 归因:源端口反查,查询发起瞬时,miss → proc:unknown)。 */ +static void build_port_entity_map(const conn_t *cur, int n) +{ + g_port_ent_n = 0; + for (int i = 0; i < n && g_port_ent_n < PORT_ENT_MAX; i++) { + if (cur[i].type != 1 || cur[i].local_port == 0) continue; + if (cur[i].local_ip != 0x7f000001 && cur[i].local_ip != 0) continue; /* loopback/通配即可 */ + g_port_ent[g_port_ent_n].port = cur[i].local_port; + snprintf(g_port_ent[g_port_ent_n].entity, sizeof g_port_ent[g_port_ent_n].entity, + "%s", cur[i].entity); + g_port_ent_n++; + } +} +static const char *dns_entity_for_port(uint16_t port) +{ + for (int i = 0; i < g_port_ent_n; i++) + if (g_port_ent[i].port == port) return g_port_ent[i].entity; + return "proc:unknown"; +} + +/* ---- /proc/net/{tcp,udp} 解析(IPv4) ---- */ +static int parse_proc_net(const char *path, int is_udp, conn_t *cur, int *n) +{ + FILE *f = fopen(path, "r"); + if (!f) return 0; + char line[512]; + if (fgets(line, sizeof line, f)) { /* skip header */ } + int cnt = 0; + while (fgets(line, sizeof line, f) && *n < CONN_MAX) { + char *tok[16]; int nt = 0; + char *save = NULL; + for (char *t = strtok_r(line, " \t", &save); t && nt < 16; t = strtok_r(NULL, " \t", &save)) + tok[nt++] = t; + if (nt < 10) continue; + char *colon = strchr(tok[1], ':'); + if (!colon) continue; + *colon = 0; + conn_t *c = &cur[*n]; + memset(c, 0, sizeof *c); + c->local_ip = hex_ip(tok[1]); + c->local_port = (uint16_t)strtoul(colon + 1, NULL, 16); + colon = strchr(tok[2], ':'); + if (!colon) continue; + *colon = 0; + c->remote_ip = hex_ip(tok[2]); + c->remote_port = (uint16_t)strtoul(colon + 1, NULL, 16); + c->state_code = (uint8_t)strtoul(tok[3], NULL, 16); + c->inode = (uint32_t)strtoul(tok[9], NULL, 10); + c->type = (uint8_t)(is_udp ? 1 : 0); + (*n)++; cnt++; + } + fclose(f); + return cnt; +} + +/* ---- 事件日志(同 entity 同 dst 1 分钟去重) ---- */ +static void evt_conn_log(const char *entity, const char *dst, const char *what) +{ + char key[96]; + snprintf(key, sizeof key, "%s|%s|%s", what, entity, dst); + long now = mono_now(); + for (int i = 0; i < g_evt_dup_n; i++) { + if (strcmp(g_evt_dup[i].key, key) == 0) { + if (now - g_evt_dup[i].mono >= 60) { g_evt_dup[i].mono = now; net_log("info", "conn", "%s entity=%s dst=%s", what, entity, dst); } + return; /* 1 分钟内只记首条 */ + } + } + if (g_evt_dup_n < EVT_DUP_MAX) { + snprintf(g_evt_dup[g_evt_dup_n].key, sizeof g_evt_dup[g_evt_dup_n].key, "%s", key); + g_evt_dup[g_evt_dup_n].mono = now; + g_evt_dup_n++; + } + net_log("info", "conn", "%s entity=%s dst=%s", what, entity, dst); +} + +/* ---- 连接普查(每 2s):/proc/net/{tcp,udp} → inode→pid→实体 → 连接表 + 事件 ---- */ +static void census_run(void) +{ + refresh_entity_maps(); + static conn_t cur[CONN_MAX]; + int cn = 0; + parse_proc_net(g_proc_tcp, 0, cur, &cn); + parse_proc_net(g_proc_udp, 1, cur, &cn); + 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"); + if (cur[i].inode && cur[i].remote_ip != 0) + dns_cache_lookup_ip(cur[i].remote_ip, cur[i].domain, sizeof cur[i].domain); + } + build_port_entity_map(cur, cn); + + int prev_n = g_conn_n; + g_gen++; + g_conn_n = 0; + long now = mono_now(); + for (int i = 0; i < cn; i++) { + conn_t *c = &cur[i]; + /* 是否延续既有连接(按 inode)?→ 保 open 时间;否则新连接 → 日志。 */ + int pi = -1; + for (int j = 0; j < prev_n; j++) if (g_conn[j].inode == c->inode && g_conn[j].pid) { pi = j; break; } + if (pi >= 0) { + c->mono_open = g_conn[pi].mono_open; + } else { + c->mono_open = now; + if (c->inode && c->remote_port != 0) { /* 服务端监听口(remote=0/0)不记 open */ + char dst[64], ip[32]; + ip_fmt(c->remote_ip, ip, sizeof ip); + snprintf(dst, sizeof dst, "%s:%u", ip, c->remote_port); + evt_conn_log(c->entity[0] ? c->entity : "proc:unknown", dst, "open"); + } + } + c->seen_gen = g_gen; + c->last_seen = now; + if (g_conn_n < CONN_MAX) g_conn[g_conn_n++] = *c; + } + /* 检测关闭:prev 有、本次无 → 日志 close + 移除。 */ + for (int j = 0; j < prev_n; j++) { + int found = 0; + for (int i = 0; i < g_conn_n; i++) if (g_conn[i].inode == g_conn[j].inode) { found = 1; break; } + if (!found && g_conn[j].inode && g_conn[j].remote_port != 0) { + char dst[64], ip[32]; + ip_fmt(g_conn[j].remote_ip, ip, sizeof ip); + snprintf(dst, sizeof dst, "%s:%u", ip, g_conn[j].remote_port); + evt_conn_log(g_conn[j].entity[0] ? g_conn[j].entity : "proc:unknown", dst, "close"); + } + } +} + +/* ---- DNS 代理(§5;三条兜底:双行 resolv + atexit 恢复 + 5s 自检) ---- */ +static int resolv_read_file(char *buf, int cap) +{ + int fd = open(g_resolv, O_RDONLY); + if (fd < 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; +} +static int resolv_write_file(const char *buf) +{ + int fd = open(g_resolv, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + ssize_t w = write(fd, buf, strlen(buf)); + close(fd); + return w > 0 ? 0 : -1; +} +static void resolv_backup_apply(void) +{ + /* 读当前 resolv.conf,取首个 nameserver 为上游。若已是我们双行形式则跳过。 */ + char buf[384]; + if (resolv_read_file(buf, sizeof buf) <= 0) return; + if (strstr(buf, "nameserver 127.0.0.1") != NULL) { + /* 已在代理指向;补上游(若无双行则显式加) */ + char up[40] = ""; + char *p = buf; + while ((p = strstr(p, "nameserver"))) { + p += 10; while (*p == ' ' || *p == '\t') p++; + char ip[40]; int i = 0; + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && i < 39) ip[i++] = *p++; + ip[i] = 0; + if (strcmp(ip, "127.0.0.1") != 0 && ip[0]) { snprintf(up, sizeof up, "%s", ip); break; } + } + if (up[0]) snprintf(g_dns_upstream, sizeof g_dns_upstream, "%s", up); + return; + } + /* 备份原内容 */ + snprintf(g_resolv_backup, sizeof g_resolv_backup, "%s", buf); + /* 取上游(首个 nameserver) */ + char up[40] = ""; + char *p = buf; + while ((p = strstr(p, "nameserver"))) { + p += 10; while (*p == ' ' || *p == '\t') p++; + char ip[40]; int i = 0; + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && i < 39) ip[i++] = *p++; + ip[i] = 0; + if (ip[0] && strcmp(ip, "127.0.0.1") != 0) { snprintf(up, sizeof up, "%s", ip); break; } + } + snprintf(g_dns_upstream, sizeof g_dns_upstream, "%s", up); + /* 双行:nameserver 127.0.0.1 + nameserver (崩溃零断流) */ + char two[128]; + if (up[0]) snprintf(two, sizeof two, "nameserver 127.0.0.1\nnameserver %s\n", up); + else snprintf(two, sizeof two, "nameserver 127.0.0.1\n"); + if (resolv_write_file(two) == 0) { + g_dns_modified_resolv = 1; + svc_log("dns: resolv.conf -> %s", up[0] ? "two-line (127.0.0.1 + upstream)" : "127.0.0.1 only"); + } +} +static void resolv_restore(void) +{ + if (g_dns_modified_resolv && g_resolv_backup[0]) { + resolv_write_file(g_resolv_backup); + svc_log("dns: resolv.conf restored"); + } + g_dns_modified_resolv = 0; +} +static void dns_atexit_restore(void) { resolv_restore(); } + +static int dns_proxy_start(void) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { svc_log("dns: socket fail"); return -1; } + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + struct sockaddr_in a; + memset(&a, 0, sizeof a); + a.sin_family = AF_INET; + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + a.sin_port = htons((uint16_t)g_dns_port); + if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { + svc_log("dns: bind 127.0.0.1:%d fail (%s) — proxy off", g_dns_port, strerror(errno)); + close(fd); + return -1; + } + g_dns_fd = fd; + g_dns_up_fd = socket(AF_INET, SOCK_DGRAM, 0); + resolv_backup_apply(); /* 仅在 bind 成功后改写 resolv.conf(否则断网) */ + g_dns_enabled = 1; + svc_log("dns proxy on 127.0.0.1:%d upstream=%s cache=%d", g_dns_port, + g_dns_upstream[0] ? g_dns_upstream : "(none)", DNS_CACHE_MAX); + return 0; +} +static void dns_proxy_stop(void) +{ + if (g_dns_fd >= 0) { close(g_dns_fd); g_dns_fd = -1; } + if (g_dns_up_fd >= 0) { close(g_dns_up_fd); g_dns_up_fd = -1; } + resolv_restore(); + g_dns_enabled = 0; + svc_log("dns proxy off"); +} + +static int dns_parse_query(const uint8_t *q, int qlen, char *domain, int domcap, int *qend) +{ + if (qlen < 12) return -1; + if (q[2] & 0x80) return -1; /* 不是 query 位 */ + uint16_t qd = (uint16_t)((q[4] << 8) | q[5]); + if (qd < 1) return -1; + int off = 12, di = 0, first = 1, len = 0; + while (off < qlen) { + uint8_t l = q[off++]; + if (l == 0) break; + if (l & 0xC0) return -1; /* question 名压缩不常见 */ + if (!first && di < domcap - 2) domain[di++] = '.'; + first = 0; + for (int i = 0; i < l && off < qlen && di < domcap - 1; i++) domain[di++] = (char)q[off++]; + len += l; + if (len > 253) return -1; + } + if (off + 4 > qlen) return -1; + domain[di] = 0; + off += 4; /* qtype+qclass */ + *qend = off; + return di > 0 ? 0 : -1; +} +static int dns_build_nxdomain(const uint8_t *q, int qlen, uint8_t *out, int outcap) +{ + int qend = 0; + char dom[DNS_QNAME_MAX]; + if (dns_parse_query(q, qlen, dom, sizeof dom, &qend) != 0) return -1; + int qsec = qend - 12; + if (12 + qsec > outcap) return -1; + memcpy(out, q, 12); + out[2] = 0x81; out[3] = 0x83; /* QR|RD|RA + RCODE=3 (NXDOMAIN) */ + out[4] = 0; out[5] = 1; /* qdcount=1 */ + out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 0; out[11] = 0; + memcpy(out + 12, q + 12, (size_t)qsec); + return 12 + qsec; +} +/* 读 RR owner 域名(支持压缩指针,最多 64 跳);返回字段结束偏移(type 起始处),失败 -1。 */ +static int dns_read_name(const uint8_t *r, int rlen, int start, char *out, int outcap) +{ + int off = start, after = -1, di = 0, first = 1, len = 0, guard = 0; + while (off < rlen && guard++ < 64) { + uint8_t l = r[off]; + if (l == 0) { if (after < 0) after = off + 1; break; } + if (l & 0xC0) { + if (off + 1 >= rlen) return -1; + if (after < 0) after = off + 2; /* 名字字段到此为止 */ + off = (int)(((l & 0x3F) << 8) | r[off + 1]); /* 跟随指针 */ + continue; + } + off++; + if (!first && di < outcap - 2) out[di++] = '.'; + first = 0; + for (int i = 0; i < l && off < rlen && di < outcap - 1; i++) out[di++] = (char)r[off++]; + len += l; + if (len > 253) return -1; + } + out[di] = 0; + return after; +} +static void dns_cache_from_response(const uint8_t *r, int rlen) +{ + if (rlen < 12) return; + if (r[3] & 0x0f) return; /* rcode 非0 不缓存 */ + uint16_t an = (uint16_t)((r[6] << 8) | r[7]); + int off = 12; + while (off < rlen) { /* 跳过 question */ + uint8_t l = r[off++]; + if (l == 0) break; + if (l & 0xC0) { off += 1; break; } + off += l; + } + off += 4; + for (int a = 0; a < an && off + 10 <= rlen; a++) { + char name[64]; + int at = dns_read_name(r, rlen, off, name, sizeof name); + if (at < 0 || at + 10 > rlen) break; + uint16_t type = (uint16_t)((r[at] << 8) | r[at + 1]); + uint32_t ttl = ((uint32_t)r[at + 4] << 24) | (r[at + 5] << 16) | ((uint32_t)r[at + 6] << 8) | r[at + 7]; + uint16_t rdlen = (uint16_t)((r[at + 8] << 8) | r[at + 9]); + int data = at + 10; + if (data + rdlen > rlen) break; + if (type == 1 && rdlen >= 4 && name[0]) { /* A 记录 */ + uint32_t ip = ((uint32_t)r[data] << 24) | (r[data + 1] << 16) | ((uint32_t)r[data + 2] << 8) | r[data + 3]; + dns_cache_insert(name, ip, (uint16_t)(ttl > 65535 ? 65535 : ttl)); + } + off = data + rdlen; + } +} + +static void dns_handle_packet(void) +{ + uint8_t q[1536], rbuf[1536]; + struct sockaddr_in from; + socklen_t fl = sizeof from; + ssize_t n = recvfrom(g_dns_fd, q, sizeof q, 0, (struct sockaddr *)&from, &fl); + 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; + const char *entity = dns_entity_for_port((uint16_t)ntohs(from.sin_port)); + /* 域名规则:deny → NXDOMAIN + 日志 */ + if (dns_query_domain_denied(entity, dom)) { + g_dns_deny++; + net_log("warn", "dns", "deny entity=%s domain=%s", entity, dom); + uint8_t resp[1536]; + int rl = dns_build_nxdomain(q, (int)n, resp, sizeof resp); + if (rl > 0) sendto(g_dns_fd, resp, (size_t)rl, 0, (struct sockaddr *)&from, fl); + 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); } + /* 上游透传转发 */ + if (!g_dns_upstream[0]) { + net_log("warn", "dns", "no upstream, drop domain=%s", dom); + return; + } + struct sockaddr_in up; + memset(&up, 0, sizeof up); + up.sin_family = AF_INET; + up.sin_port = htons((uint16_t)g_dns_upstream_port); + if (inet_pton(AF_INET, g_dns_upstream, &up.sin_addr) != 1) return; + sendto(g_dns_up_fd, q, (size_t)n, 0, (struct sockaddr *)&up, sizeof up); + struct pollfd pf = { g_dns_up_fd, POLLIN, 0 }; + int pr = poll(&pf, 1, DNS_UPSTREAM_TMO_MS); + if (pr > 0) { + ssize_t rn = recv(g_dns_up_fd, rbuf, sizeof rbuf, 0); + if (rn > 12) { + dns_cache_from_response(rbuf, (int)rn); + sendto(g_dns_fd, rbuf, (size_t)rn, 0, (struct sockaddr *)&from, fl); + } + } else { + net_log("warn", "dns", "upstream timeout domain=%s", dom); + } +} + +static void dns_self_check(void) +{ + char buf[384]; + if (resolv_read_file(buf, sizeof buf) <= 0) return; + /* 若我们的双行头丢失(第三方/udhcpc 改回)→ 重写并记日志。 */ + if (strstr(buf, "nameserver 127.0.0.1") == NULL) { + net_log("warn", "dns", "resolv.conf reverted by third-party — re-apply"); + resolv_backup_apply(); + } +} + +/* ---- 期 2 IPC 文本构建 ---- */ +static int build_conns_text(const char *filter, char *out, size_t cap, int *truncated) +{ + if (truncated) *truncated = 0; + out[0] = 0; + size_t off = 0; + long now = mono_now(); + for (int i = 0; i < g_conn_n; i++) { + conn_t *c = &g_conn[i]; + if (filter && filter[0] && strncmp(c->entity, filter, strlen(filter)) != 0) continue; + char lip[32], rip[32]; + ip_fmt(c->local_ip, lip, sizeof lip); + ip_fmt(c->remote_ip, rip, sizeof rip); + const char *dom = c->domain[0] ? c->domain : "-"; /* 空域名占位,保字段数固定 6 */ + char line[192]; + int n; + if (c->type) { + n = snprintf(line, sizeof line, "%s|%s:%u|%s:%u|UDP|%ld|%s\n", + c->entity, lip, c->local_port, rip, c->remote_port, + now - c->mono_open, dom); + } else { + n = snprintf(line, sizeof line, "%s|%s:%u|%s:%u|%s|%ld|%s\n", + c->entity, lip, c->local_port, rip, c->remote_port, + tcp_state_str(c->state_code), now - c->mono_open, dom); + } + if (n <= 0) continue; + if (off + (size_t)n >= cap - 1) { if (truncated) *truncated = 1; break; } + memcpy(out + off, line, (size_t)n); + off += (size_t)n; + } + out[off] = 0; + return (int)off; +} + +static int build_dns_ctl_text(const char *cmd, char *out, size_t cap) +{ + out[0] = 0; + if (!strcmp(cmd, "stats") || !strcmp(cmd, "status")) { + long now = mono_now(); + int hits = 0; + for (int i = 0; i < g_dns_n; i++) if (g_dns[i].name[0] && g_dns[i].exp > now) hits++; + snprintf(out, cap, + "dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d\n", + g_mode_meow ? "meow" : "readonly", g_dns_enabled, + g_dns_upstream[0] ? g_dns_upstream : "(none)", hits, g_dns_n, + (unsigned long long)g_dns_q, (unsigned long long)g_dns_deny, + (unsigned long long)g_dns_first, g_dns_modified_resolv); + } else if (!strcmp(cmd, "flush")) { + dns_cache_flush(); + snprintf(out, cap, "dns cache flushed (%d entries)\n", g_dns_n); + } else { + snprintf(out, cap, "dns: cmd 须 status|stats|flush\n"); + return MEOW_ERR_INVAL; + } + return MEOW_OK; +} + /* ---------------- 节拍:模式热切换 + 接口变化检测(日志)+ 全局速率差分 ---------------- */ static void daemon_tick(void) { @@ -909,6 +1731,17 @@ static void daemon_tick(void) if (fm != g_mode_meow) { svc_log("mode -> %s", fm ? "full(meow)" : "readonly"); g_mode_meow = fm; + /* 模式热切换 → DNS 代理开关(HibyOS 绝不启用/绝不改 resolv.conf)。 */ + if (fm && !g_dns_disabled) dns_proxy_start(); + else if (!fm && g_dns_enabled) dns_proxy_stop(); + } + /* DNS:MeowOS 下 5s 自检 resolv.conf 未被第三方改回。 */ + if (g_dns_enabled) { + long now1 = mono_now(); + if (now1 - g_last_resolv_check >= DNS_RESOLV_CHECK_SEC) { + g_last_resolv_check = now1; + dns_self_check(); + } } /* 接口热插拔/状态变化:节拍检测 → 日志(读侧无 popen)。 */ enum_ifaces(0); @@ -950,6 +1783,9 @@ static void daemon_tick(void) } g_net_last_rx = rx; g_net_last_tx = tx; g_net_last_mono = now; } + + /* 期2 连接普查(读侧:观察层,HibyOS 只读模式同样运行;不含写操作)。 */ + census_run(); } /* ---------------- IPC 鉴权(连接级,MPK_SIGNATURE_DESIGN §6.2;同 svc_power) ---------------- */ @@ -1378,20 +2214,71 @@ static void handle_conn(int c) send_net_bt_result(c, req_id, &r); continue; } - send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1仅 STATUS/IFACE/WIFI_CTL/BT_CTL)"); + if (type == MSG_NET_CONNS_REQ) { + /* 期2 观测层:读级任意调用者。返回连接表文本(可实体过滤)。 */ + m_net_conns_req_t q; + memset(&q, 0, sizeof q); + if (rlen > sizeof q) rlen = sizeof q; + memcpy(&q, payload, rlen); + m_net_conns_result_t res; + memset(&res, 0, sizeof res); + int tr = 0; + int n = build_conns_text(q.entity, res.text, sizeof res.text, &tr); + res.code = MEOW_OK; + res.truncated = (uint16_t)(tr ? 1 : 0); + size_t tl = n > 0 ? (size_t)n : 0; + if (tl > sizeof res.text - 1) tl = sizeof res.text - 1; + res.text_len = (uint16_t)tl; + res.text[tl] = 0; + send_frame(c, MSG_NET_CONNS_RESULT, req_id, &res, (uint32_t)(8 + tl + 1)); + continue; + } + if (type == MSG_NET_DNS_CTL) { + m_net_dns_ctl_t q; + memset(&q, 0, sizeof q); + if (rlen > sizeof q) rlen = sizeof q; + memcpy(&q, payload, rlen); + m_net_dns_result_t res; + memset(&res, 0, sizeof res); + int write = strcmp(q.cmd, "flush") == 0; /* flush=清缓存(动作级) */ + int rc = MEOW_OK; + if (write && !is_sys_identity(&caller)) { + rc = MEOW_ERR_PERM; + snprintf(res.text, sizeof res.text, "permission denied (dns flush needs system identity)"); + net_log("warn", "write", "denied dns flush caller_kind=%d uid=%d", (int)caller.kind, (int)caller.uid); + } else if (write && !g_mode_meow) { + rc = MEOW_ERR_PERM; + snprintf(res.text, sizeof res.text, "readonly (HibyOS mode): dns flush denied"); + net_log("warn", "write", "denied dns flush (HibyOS readonly)"); + } else if (g_dns_disabled) { + snprintf(res.text, sizeof res.text, "dns: proxy disabled (MEOW_DNS_DISABLE)"); + rc = MEOW_ERR_NODEV; + } else { + rc = build_dns_ctl_text(q.cmd, 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_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)"); break; } } -/* ---------------- 守护主循环(单线程 poll;节拍 2s) ---------------- */ +/* ---------------- 守护主循环(单线程 poll;节拍 2s + DNS UDP) ---------------- */ static volatile sig_atomic_t g_stop = 0; static void on_sig(int sig) { (void)sig; g_stop = 1; } +static void on_hup(int sig) { (void)sig; g_hup = 1; } static int run_daemon(const char *sock) { signal(SIGPIPE, SIG_IGN); signal(SIGTERM, on_sig); signal(SIGINT, on_sig); + signal(SIGHUP, on_hup); int lfd = socket(AF_UNIX, SOCK_STREAM, 0); if (lfd < 0) { svc_log("socket fail: %s", strerror(errno)); return 1; } struct sockaddr_un a; @@ -1409,11 +2296,23 @@ static int run_daemon(const char *sock) g_mode_meow = boot_mode_is_meow(); log_open(); - svc_log("meow_netmgrd listening on %s (mode=%s, poll=%ds, log=%s)", - sock, g_mode_meow ? "full" : "readonly", g_poll_sec, g_logfile); + rules_load(); + atexit(dns_atexit_restore); /* 崩溃/退出兜底:恢复原 resolv.conf */ + /* MeowOS 启动即启用 DNS 代理(节点拍重判只在模式切换时触发,此处显式首启)。 */ + if (g_mode_meow && !g_dns_disabled) { + dns_proxy_start(); + g_last_resolv_check = mono_now(); + } + svc_log("meow_netmgrd listening on %s (mode=%s, poll=%ds, log=%s, dns=%s)", + sock, g_mode_meow ? "full" : "readonly", g_poll_sec, g_logfile, + g_dns_enabled ? "on" : "off"); long next_tick = 0; while (!g_stop) { + if (g_hup) { /* SIGHUP:恢复并重挂 DNS 代理(若仍 MeowOS) */ + g_hup = 0; + if (g_dns_enabled) { dns_proxy_stop(); if (g_mode_meow && !g_dns_disabled) dns_proxy_start(); } + } long now = mono_now(); if (now >= next_tick) { daemon_tick(); @@ -1428,6 +2327,11 @@ static int run_daemon(const char *sock) int c = accept(lfd, NULL, NULL); if (c >= 0) { handle_conn(c); close(c); } } + /* DNS 代理(UDP 127.0.0.1:port):非阻塞单包处理。 */ + if (g_dns_fd >= 0) { + struct pollfd dan = { g_dns_fd, POLLIN, 0 }; + if (poll(&dan, 1, 0) > 0 && (dan.revents & POLLIN)) dns_handle_packet(); + } } svc_log("meow_netmgrd exit"); close(lfd); @@ -1660,17 +2564,134 @@ static int cli_iface(const char *name) return 0; } +/* ---- 期2 CLI:conns/apps/top/dns(CONNS_RESULT 逐行 `entity|lip:lport|rip:rport|state|age|domain`) ---- */ +static int cli_conns(const char *filter) +{ + char js[MEOW_IPC_MAX]; + m_net_conns_req_t q; + memset(&q, 0, sizeof q); + if (filter) snprintf(q.entity, sizeof q.entity, "%s", filter); + if (cli_ipc_json(MSG_NET_CONNS_REQ, &q, sizeof q, js, sizeof js) != 0) { + fprintf(stderr, "meow_net conns: 守护不可达(%s)\n", g_sock); + return 1; + } + int n = 0; + char *ln = js; + while (ln && *ln) { + char *nl = strchr(ln, '\n'); if (nl) *nl = 0; + char *f[7]; int nf = 0; + char *save = NULL; + for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t; + if (nf >= 6) { + printf("%-26s %-22s -> %-22s %-9s %5lds %s\n", f[0], f[1], f[2], f[3], + atol(f[4]), f[5]); + n++; + } + if (!nl) break; + ln = nl + 1; + } + printf("(%d connection%s%s)\n", n, n == 1 ? "" : "s", filter && filter[0] ? " filtered" : ""); + return 0; +} + +typedef struct { char ent[48]; int cnt; long maxage; } ent_grp_t; +/* 从 conns 文本聚合实体统计(apps/top 共用)。 */ +static int group_conns(const char *js, ent_grp_t *grp, int max) +{ + int gn = 0; + char *ln = (char *)js; + while (ln && *ln && gn < max) { + char *nl = strchr(ln, '\n'); if (nl) *nl = 0; + char *f[7]; int nf = 0; + char *save = NULL; + for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t; + if (nf >= 5 && f[0][0]) { + int gi = -1; + for (int i = 0; i < gn; i++) if (strcmp(grp[i].ent, f[0]) == 0) { gi = i; break; } + if (gi < 0) { + if (gn >= max) continue; + gi = gn++; + snprintf(grp[gi].ent, sizeof grp[gi].ent, "%s", f[0]); + grp[gi].cnt = 0; grp[gi].maxage = 0; + } + grp[gi].cnt++; + long a = atol(f[4]); + if (a > grp[gi].maxage) grp[gi].maxage = a; + } + if (!nl) break; + ln = nl + 1; + } + /* 按 cnt 降序 */ + for (int i = 0; i < gn; i++) + for (int j = i + 1; j < gn; j++) + if (grp[j].cnt > grp[i].cnt) { + ent_grp_t t = grp[i]; grp[i] = grp[j]; grp[j] = t; + } + return gn; +} + +static int cli_apps(void) +{ + char js[MEOW_IPC_MAX]; + if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) { + fprintf(stderr, "meow_net apps: 守护不可达(%s)\n", g_sock); + return 1; + } + ent_grp_t grp[64]; + int gn = group_conns(js, grp, 64); + printf("%-26s %6s %8s\n", "entity", "conns", "max_age"); + for (int i = 0; i < gn; i++) + printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage); + return 0; +} + +static volatile sig_atomic_t g_cli_stop = 0; +static void cli_sigint(int sig) { (void)sig; g_cli_stop = 1; } + +static int cli_top(void) +{ + g_cli_stop = 0; + signal(SIGINT, cli_sigint); + while (!g_cli_stop) { + char js[MEOW_IPC_MAX]; + if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) { + fprintf(stderr, "meow_net top: 守护不可达(%s)\n", g_sock); + return 1; + } + ent_grp_t grp[64]; + int gn = group_conns(js, grp, 64); + printf("\033[2J\033[H top 连接实体(2s 刷新,Ctrl-C 退)\n"); + printf("%-26s %6s %8s\n", "entity", "conns", "max_age"); + for (int i = 0; i < gn && i < 10; i++) + printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage); + for (int i = 0; i < 10 - gn && i < 10; i++) printf("\n"); + fflush(stdout); + sleep(2); + } + printf("\n"); + return 0; +} + +static int cli_dns(const char *cmd) +{ + char out[MEOW_IPC_MAX - 8]; + int code = cli_ipc_cmd(MSG_NET_DNS_CTL, cmd ? cmd : "status", NULL, out, sizeof out); + printf("%s\n", 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] | help"; + " | bt status|on|off|paired | rfkill [ ] | udhcpc renew|release [if]" + " | apps | conns [entity] | top | dns status|stats|flush | help"; } static int cli_help(void) { printf("meow_net — MeowOS 网络管理 CLI(meow_netmgrd 的 IPC 薄壳)\n"); printf("usage: meow_net <%s>\n", usage()); - printf("\n读级任意调用者;写级(wifi/bt on|off、connect、rfkill set、udhcpc)仅系统身份,且 HibyOS 模式只读。\n"); + printf("\n读级任意调用者;写级(wifi/bt on|off、connect、rfkill set、udhcpc、dns flush)仅系统身份,且 HibyOS 模式只读。\n"); return 0; } @@ -1682,6 +2703,13 @@ static int cli_run(int n, char **v) 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(); + if (!strcmp(cmd, "apps")) return cli_apps(); + if (!strcmp(cmd, "conns")) return cli_conns(arg); + if (!strcmp(cmd, "top")) return cli_top(); + if (!strcmp(cmd, "dns")) { + if (!arg) { fprintf(stderr, "dns 子命令缺失(status|stats|flush)\n"); return 1; } + return cli_dns(arg); + } 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); @@ -1733,6 +2761,15 @@ int main(int argc, char **argv) if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e; if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e; if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e); + if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e); + if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e); + if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e); + if ((e = getenv("MEOW_SVCD_SOCK")) && e[0]) snprintf(g_svcd_sock, sizeof g_svcd_sock, "%s", e); + if ((e = getenv("MEOW_SVCD_STATUS")) && e[0]) snprintf(g_svcd_status, sizeof g_svcd_status, "%s", e); + if ((e = getenv("MEOW_NET_POLICY")) && e[0]) snprintf(g_net_policy, sizeof g_net_policy, "%s", e); + if ((e = getenv("MEOW_DNS_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_port = atoi(e); + if ((e = getenv("MEOW_DNS_UPSTREAM_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_upstream_port = atoi(e); + if ((e = getenv("MEOW_DNS_DISABLE")) && atoi(e) > 0) g_dns_disabled = 1; int i = 1; for (; i < argc; i++) { diff --git a/patch_v2/meow_netmgrd/meow_netmgrd_host b/patch_v2/meow_netmgrd/meow_netmgrd_host index 51522a4..2bc8807 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 0d0cb99..9bb731d 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/test_netmgrd.sh b/patch_v2/meow_netmgrd/test_netmgrd.sh index 0ab5784..cbf65a5 100755 --- a/patch_v2/meow_netmgrd/test_netmgrd.sh +++ b/patch_v2/meow_netmgrd/test_netmgrd.sh @@ -37,6 +37,19 @@ EOF printf 'nameserver 223.5.5.5\nnameserver 10.78.193.1\n' > "$T/resolv" echo meow > "$T/boot_mode" +# ---- 期2 观测层打桩:/proc/net/{tcp,udp}(IPv4 LE hex)+ 域名规则 conf + amsd/svcd 缺省 ---- +cat > "$T/tcp" <<'EOF' + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 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 +EOF +cat > "$T/udp" <<'EOF' + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 0100007F:D301 0100007F:0035 07 00000000:00000000 00:00000000 00000000 0 0 99999 1 0 0 10 0 +EOF +printf 'app:dev.player.ncm domain=-ads.example.com\nproc:dropbear domain=+x\n' > "$T/policy" + # 桩后端命令(popen stubbing) cat > "$T/wpa_cli" <<'EOF' #!/bin/sh @@ -75,10 +88,13 @@ EOF for f in wpa_cli hciconfig bluetoothctl ip; do chmod +x "$T/$f"; done mkdir -p "$T/logs" "$T/pid" +# 期2:DNS 禁用(宿主无 root 不绑 53),观测层经 MEOW_TCP/UDP 打桩;amsd/svcd 缺省(实体归因走 proc:comm→unknown)。 ENVS="MEOW_NET_DIR=$NET MEOW_NETDEV=$T/netdev MEOW_NET_ROUTE=$T/route MEOW_RESOLV=$T/resolv MEOW_RFKILL_DIR=$RF MEOW_BT_DIR=$BT MEOW_BOOT_MODE=$T/boot_mode MEOW_BOOTTS_CACHE=$T/bootts MEOW_IP=$T/ip MEOW_WPA_CLI=$T/wpa_cli MEOW_HCICONFIG=$T/hciconfig MEOW_BLUETOOTHCTL=$T/bluetoothctl - MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs" + MEOW_NET_SOCK=$T/net.sock MEOW_NET_POLL_SEC=1 MEOW_LOG_DIR=$T/logs + MEOW_TCP=$T/tcp MEOW_UDP=$T/udp MEOW_NET_POLICY=$T/policy + MEOW_AMS_SOCK=$T/no_ams MEOW_SVCD_SOCK=$T/no_svcd MEOW_DNS_DISABLE=1" ln -sf "$(readlink -f "$BIN")" "$T/meow_net" # argv[0] 双形态 → CLI # ---- T0: 守护起 ---- @@ -159,6 +175,95 @@ ck T9a "usage: meow_net" "$OUT" OUT=$(env $ENVS $Q "$T/meow_net" bogus 2>&1); RC=$? ck T9b "usage: meow_net" "$OUT" +# ---- 重启 daemon A(T8 已停)供期2 观测/连接表/日志测试 ---- +env $ENVS $Q "$BIN" --sock "$T/net.sock" >"$T/daemon2.log" 2>&1 & +DPID=$! +for i in $(seq 1 30); do [ -S "$T/net.sock" ] && break; sleep 0.2; done +[ -S "$T/net.sock" ] || { echo "FAIL T10-pre: daemon 未重启"; cat "$T/daemon2.log"; exit 1; } + +# ---- T10: 期2 conns(读级;观测层经 MEOW_TCP/UDP 桩 + amsd/svcd 缺省 → proc:unknown 归因)---- +sleep 1 # 等守护普查一拍 +OUT=$(env $ENVS $Q "$T/meow_net" conns 2>&1) +ck T10a "proc:unknown" "$OUT" +ck T10b "10.78.193.50:20791" "$OUT" +ck T10b2 "140.205.5.9:443" "$OUT" +ck T10c "ESTAB" "$OUT" +ck T10d "UDP" "$OUT" +ck T10e "connection" "$OUT" +# ---- T11: apps(实体聚合)---- +OUT=$(env $ENVS $Q "$T/meow_net" apps 2>&1) +ck T11a "entity" "$OUT" +ck T11b "proc:unknown" "$OUT" +# ---- T12: dns status/stats(DNS 禁用 → 显示 disabled)---- +OUT=$(env $ENVS $Q "$T/meow_net" dns status 2>&1) +ck T12a "proxy disabled" "$OUT" +OUT=$(env $ENVS $Q "$T/meow_net" dns stats 2>&1) +ck T12b "proxy disabled" "$OUT" +# ---- T13: dns flush(写级;宿主 CLI=CALL_OTHER → 拒)---- +OUT=$(env $ENVS $Q "$T/meow_net" dns flush 2>&1) +ck T13a "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" +cat > "$T/policy_dns" <<'EOF' +global domain=-ads.example.com +EOF +printf 'nameserver %s\n' "$HOSTIP" > "$T/resolv_dns" +cat > "$T/dns_test.py" <<'PYEOF' +import socket, struct, threading, sys, os +UP_IP = os.environ.get("T_UP_IP", "0.0.0.0") +up = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +up.bind((UP_IP, 5311)) # 0.0.0.0 通配,绑到宿主自身 IP +up.settimeout(6) +def serve(): + try: + while True: + d,a = up.recvfrom(1536) + off=12 + while d[off]!=0: off += 1+d[off] + off += 1+4 + qsec = d[12:off] + resp = d[:2] + b'\x81\x80' + b'\x00\x01\x00\x01\x00\x00\x00\x00' + qsec + resp += b'\xc0\x0c' + b'\x00\x01\x00\x01' + struct.pack('>I',60) + b'\x00\x04' + socket.inet_aton('11.22.33.44') + up.sendto(resp, a) + except Exception: + pass +threading.Thread(target=serve, daemon=True).start() +def query(name): + s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(3) + hdr=b'\x12\x34'+b'\x01\x00'+b'\x00\x01\x00\x00\x00\x00\x00\x00' + q=b''.join(bytes([len(l)])+l.encode() for l in name.split('.'))+b'\x00'+b'\x00\x01\x00\x01' + s.sendto(hdr+q, ("127.0.0.1", 5300)) + r,_=s.recvfrom(1536); rco=r[3]&0xf; an=struct.unpack('>H',r[6:8])[0] + return rco,an +try: + d1,a1 = query("ads.example.com") # deny → NXDOMAIN (rcode 3) + d2,a2 = query("music.163.com") # allow → forward → A 11.22.33.44 + sys.stdout.write("DENY_RCODE=%d\nALLOW_AN=%d\n" % (d1, a2)) +except Exception as e: + sys.stdout.write("ERR=%s\n" % e) +PYEOF +# 追加式覆写(env 左→右,后者胜出):DNS 启用、换独立 socket/resolv/policy、上游端口。 +DNSENV="$ENVS MEOW_DNS_DISABLE=0 MEOW_NET_SOCK=$T/dns.sock MEOW_RESOLV=$T/resolv_dns + MEOW_NET_POLICY=$T/policy_dns MEOW_DNS_PORT=5300 MEOW_DNS_UPSTREAM_PORT=5311" +ln -sf "$(readlink -f "$BIN")" "$T/dns_netmgrd" +env $DNSENV $Q "$BIN" --sock "$T/dns.sock" >"$T/dnsd.log" 2>&1 & +D2PID=$! +for i in $(seq 1 30); do [ -S "$T/dns.sock" ] && break; sleep 0.2; done +sleep 1 +OUT=$(env $DNSENV $Q "$T/meow_net" dns status 2>&1) +ck T14a "enabled=1 upstream=$HOSTIP" "$OUT" +WIRE=$(python3 "$T/dns_test.py" 2>&1) +ck T14b "DENY_RCODE=3" "$WIRE" +ck T14c "ALLOW_AN=1" "$WIRE" +OUT=$(env $DNSENV $Q "$T/meow_net" dns stats 2>&1) +ck T14d "q=2" "$OUT" +ck T14e "deny=1" "$OUT" +kill $D2PID 2>/dev/null; wait $D2PID 2>/dev/null +# 恢复守护 A 的 MEOW_RESOLV(其未被 DNS 触碰,此处确认) +grep -q "nameserver 223.5.5.5" "$T/resolv" && { PASS=$((PASS+1)); echo "PASS T14f daemonA resolv untouched"; } || { FAIL=$((FAIL+1)); echo "FAIL T14f daemonA resolv touched"; } + echo "==============================" echo "PASS=$PASS FAIL=$FAIL" [ $FAIL -eq 0 ] diff --git a/patch_v2/meow_netmgrd/test_netmgrd_p2 b/patch_v2/meow_netmgrd/test_netmgrd_p2 new file mode 100755 index 0000000..10e59f0 Binary files /dev/null and b/patch_v2/meow_netmgrd/test_netmgrd_p2 differ diff --git a/patch_v2/meow_netmgrd/test_netmgrd_p2.c b/patch_v2/meow_netmgrd/test_netmgrd_p2.c new file mode 100644 index 0000000..d96ed23 --- /dev/null +++ b/patch_v2/meow_netmgrd/test_netmgrd_p2.c @@ -0,0 +1,213 @@ +/* test_netmgrd_p2.c — meow_netmgrd 期2 观测层/DNS 代理 纯逻辑宿主自测(本程序非生产代码)。 + * + * 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问 + * FILE-STATIC 函数/全局:dns_parse_query / dns_build_nxdomain / dns_cache_* / suffix_match / + * rules_load / dns_query_domain_denied / pid_ent_put / entity_for_pid / parse_proc_net / + * build_conns_text / build_dns_ctl_text / dns_cache_from_response / hex_ip / ip_fmt。 + * 覆盖 DNS wire 编解码、缓存 TTL、域名规则判定、实体归因(app/svc/proc:comm)、/proc/net 解析、 + * 连接表文本构建。真实 amsd/svcd socket + /proc//fd 归因留真机验证。 + * + * 用法:cc -Wall -Wextra -I../meow_launcherd test_netmgrd_p2.c -o test_netmgrd_p2 && ./test_netmgrd_p2 + */ +#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 void wf(const char *p, const char *s) +{ + FILE *f = fopen(p, "w"); + if (f) { fputs(s, f); fclose(f); } +} + +static int make_query(const char *name, uint8_t *out) +{ + memset(out, 0, 16); + out[0] = 0x12; out[1] = 0x34; /* id */ + out[2] = 0x01; out[3] = 0x00; /* RD */ + out[4] = 0; out[5] = 1; /* qdcount=1 */ + int off = 12; + const char *p = name; + while (*p) { + const char *dot = strchr(p, '.'); + size_t l = dot ? (size_t)(dot - p) : strlen(p); + out[off++] = (uint8_t)l; + memcpy(out + off, p, l); off += (int)l; + p = dot ? dot + 1 : p + l; + } + out[off++] = 0; + out[off++] = 0; out[off++] = 1; /* qtype=A */ + out[off++] = 0; out[off++] = 1; /* qclass=IN */ + return off; +} + +static int make_a_response(const char *name, const char *ip, uint8_t *out) +{ + int ql = make_query(name, out); + out[0] = 0x12; out[1] = 0x34; + out[2] = 0x81; out[3] = 0x80; /* QR|RD|RA */ + out[6] = 0; out[7] = 1; /* ancount=1 */ + int off = ql; + out[off++] = 0xC0; out[off++] = 0x0C; /* answer name pointer → offset 12 (question) */ + out[off++] = 0; out[off++] = 1; /* type A */ + out[off++] = 0; out[off++] = 1; /* class IN */ + out[off++] = 0; out[off++] = 0; out[off++] = 0; out[off++] = 0x3C; /* ttl=60 */ + out[off++] = 0; out[off++] = 4; /* rdlen=4 */ + int b0 = 0, b1 = 0, b2 = 0, b3 = 0; + sscanf(ip, "%d.%d.%d.%d", &b0, &b1, &b2, &b3); + out[off++] = (uint8_t)b0; out[off++] = (uint8_t)b1; + out[off++] = (uint8_t)b2; out[off++] = (uint8_t)b3; + return off; +} + +int main(void) +{ + static const char *T = "/tmp/nmg_p2"; + char path[192]; + mkdir(T, 0755); + uint8_t q[256], r[512]; + + printf("== dns_parse_query ==\n"); + { + int ql = make_query("music.163.com", q); + char dom[DNS_QNAME_MAX]; int qend = 0; + CHECK(dns_parse_query(q, ql, dom, sizeof dom, &qend) == 0); + CHECK(strcmp(dom, "music.163.com") == 0); + CHECK(qend == ql); + } + printf("== dns_build_nxdomain ==\n"); + { + int ql = make_query("ads.example.com", q); + uint8_t resp[300]; + int rl = dns_build_nxdomain(q, ql, resp, sizeof resp); + CHECK(rl > 12); + CHECK((resp[2] & 0x80) != 0); /* QR */ + CHECK((resp[3] & 0x0f) == 3); /* NXDOMAIN */ + CHECK(resp[0] == 0x12 && resp[1] == 0x34); /* id echoed */ + } + printf("== dns_cache insert/lookup/flush (TTL 尊重) ==\n"); + { + dns_cache_flush(); + CHECK(dns_cache_insert("music.163.com", 0x0A4EC132, 60) == 0); + char out[64]; + CHECK(dns_cache_lookup_ip(0x0A4EC132, out, sizeof out) == 0); + CHECK(strcmp(out, "music.163.com") == 0); + CHECK(dns_cache_lookup_name("music.163.com") == 0); + CHECK(dns_cache_lookup_name("absent.com") != 0); + dns_cache_flush(); + CHECK(dns_cache_lookup_name("music.163.com") != 0); + } + printf("== dns_cache_from_response(A 记录抓取)==\n"); + { + dns_cache_flush(); + int rl = make_a_response("music.163.com", "39.164.0.12", r); + dns_cache_from_response(r, rl); + char out[64]; + CHECK(dns_cache_lookup_ip(0x27A4000C, out, sizeof out) == 0); + CHECK(strcmp(out, "music.163.com") == 0); + } + printf("== suffix_match / rules_load / dns_query_domain_denied ==\n"); + { + CHECK(suffix_match("ads.example.com", "example.com") == 1); + CHECK(suffix_match("sub.ads.example.com", "ads.example.com") == 1); + CHECK(suffix_match("example.com", "ads.example.com") == 0); + snprintf(path, sizeof path, "%s/policy", T); + wf(path, "# test\napp:dev.player.ncm domain=-ads.example.com,-tracker.net\nproc:dropbear domain=+k,l\n\n"); + g_net_policy[0] = 0; snprintf(g_net_policy, sizeof g_net_policy, "%s", path); + rules_load(); + CHECK(g_rule_n >= 2); + CHECK(dns_query_domain_denied("app:dev.player.ncm", "ads.example.com") == 1); + CHECK(dns_query_domain_denied("app:dev.player.ncm", "sub.ads.example.com") == 1); + CHECK(dns_query_domain_denied("app:dev.player.ncm", "music.163.com") == 0); + CHECK(dns_query_domain_denied("proc:dropbear", "k") == 0); /* allow 覆盖 */ + CHECK(dns_query_domain_denied("proc:dropbear", "l") == 1); /* deny */ + CHECK(dns_query_domain_denied("proc:other", "ads.example.com") == 0); /* 无实体规则 → 默认 allow */ + } + printf("== refresh_entity_maps(svcd 落盘状态文件 → svc: 实体)==\n"); + { + snprintf(path, sizeof path, "%s/svcd.status", T); + wf(path, "# meow_svcd status\ncard_present=1 wifi_up=1 services=2\n" + "svc=meowpanel state=running pid=1038 restart=0 exit=0 trigger=boot backoff_ms=2000\n" + "svc=meow_mdns state=running pid=1006 restart=0 exit=0 trigger=boot backoff_ms=2000\n"); + g_svcd_status[0] = 0; snprintf(g_svcd_status, sizeof g_svcd_status, "%s", path); + g_svcd_sock[0] = 0; snprintf(g_svcd_sock, sizeof g_svcd_sock, "%s/none.sock", T); + g_ams_sock[0] = 0; snprintf(g_ams_sock, sizeof g_ams_sock, "%s/none.sock", T); + g_pid_ent_n = 0; + refresh_entity_maps(); + char ent[48]; + entity_for_pid(1038, ent, sizeof ent); + CHECK(strcmp(ent, "svc:meowpanel") == 0); + entity_for_pid(1006, ent, sizeof ent); + CHECK(strcmp(ent, "svc:meow_mdns") == 0); + } + printf("== pid_ent_put / entity_for_pid(app/svc/proc:comm)==\n"); + { + g_pid_ent_n = 0; + pid_ent_put(123, "app:dev.player.ncm"); + pid_ent_put(456, "svc:meowpanel"); + char ent[48]; + entity_for_pid(123, ent, sizeof ent); + CHECK(strcmp(ent, "app:dev.player.ncm") == 0); + entity_for_pid(456, ent, sizeof ent); + CHECK(strcmp(ent, "svc:meowpanel") == 0); + /* proc::造一个 /proc-format comm 不可能,直接读当前 pid comm(非空即入 proc:) */ + int self = (int)getpid(); + snprintf(path, sizeof path, "/proc/%d/comm", self); + if (access(path, F_OK) == 0) { + entity_for_pid(self, ent, sizeof ent); + CHECK(strncmp(ent, "proc:", 5) == 0); + } + } + printf("== parse_proc_net(IPv4 地址/端口/inode)==\n"); + { + snprintf(path, sizeof path, "%s/tcp", T); + wf(path, " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n" + " 0: 32C14E0A:5137 0905CD8C:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 12345 1\n" + " 1: 32C14E0A:0A00 00000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 67890 1\n"); + g_proc_tcp[0] = 0; snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", path); + static conn_t cur[CONN_MAX]; + int n = 0; + CHECK(parse_proc_net(g_proc_tcp, 0, cur, &n) == 2); + CHECK(cur[0].local_port == 0x5137 && cur[0].remote_port == 0x01BB); + CHECK(cur[0].inode == 12345 && cur[0].state_code == 1); + char ip[32]; + ip_fmt(cur[0].local_ip, ip, sizeof ip); + CHECK(strcmp(ip, "10.78.193.50") == 0); + ip_fmt(cur[0].remote_ip, ip, sizeof ip); + CHECK(strcmp(ip, "140.205.5.9") == 0); + } + printf("== build_conns_text / build_dns_ctl_text ==\n"); + { + g_conn_n = 0; + conn_t c; + memset(&c, 0, sizeof c); + c.local_ip = hex_ip("32C14E0A"); c.local_port = 443; /* 10.78.193.50 */ + c.remote_ip = hex_ip("32C34E0A"); c.remote_port = 25565; /* 10.78.195.50 */ + c.state_code = 1; c.type = 0; c.inode = 555; c.pid = 12345; + snprintf(c.entity, sizeof c.entity, "app:dev.player.ncm"); + snprintf(c.domain, sizeof c.domain, "music.163.com"); + c.mono_open = mono_now(); c.last_seen = mono_now(); + g_conn[g_conn_n++] = c; + char out[512]; int tr = 0; + int len = build_conns_text(NULL, out, sizeof out, &tr); + CHECK(len > 0 && tr == 0); + CHECK(strstr(out, "app:dev.player.ncm|10.78.193.50:443|10.78.195.50:25565|ESTAB|0|music.163.com") != NULL); + char dout[256]; + CHECK(build_dns_ctl_text("status", dout, sizeof dout) == MEOW_OK); + CHECK(strstr(dout, "dns mode=") != NULL); + CHECK(build_dns_ctl_text("flush", dout, sizeof dout) == MEOW_OK); + } + + if (g_fail) { printf("\nNETMGR P2 TEST: %d FAIL\n", g_fail); return 1; } + printf("\nNETMGR P2 TEST: ALL PASS\n"); + return 0; +}