#define _GNU_SOURCE /* struct ucred / SO_PEERCRED、asprintf 等 GNU/musl 扩展 */ /* meow_netmgrd.c — MeowOS 网络管理专项服务(devdocs/plan/NETMGR_DESIGN.md,期 1 骨架)。 * * 定位(与 powermgr/audiod 同级的系统服务):接口统一层——WiFi/BT/USB-eth/rfkill/IP/路由/DNS * 的查询与控制收口为一个系统权威;系统设置、MeowPanel、未来系统组件一律走它的 IPC,不再各自直调 * wpa_cli/hciconfig(设计 §1/§3)。期 1 范围(设计 §13 表,→ M6.0):骨架 + 接口统一层 + * CLI(status/iface/wifi/bt/rfkill) + IPC(E0-E7) + 日志框架 + svcd conf + HibyOS 只读降级。 * * 单一二进制双形态(同 powermgr/svc_power): * - svcd 空参拉起(板载 services.d/meow_netmgrd.conf,trigger=boot/restart=on-fail)→ * IPC 服务常驻:bind /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖),收 * MSG_HELLO→MSG_ACK、MSG_NET_STATUS_REQ/IFACE_REQ/WIFI_CTL/BT_CTL。 * - `meow_net `(argv[0]=meow_net 双形态)或 `meow_netmgrd ` → 一次 CLI(IPC 薄壳)。 * CLI 是 IPC 客户端:查询经守护,写操作经守护鉴权;守护不可达 → 报错(help 除外,help 本地必可用)。 * * HibyOS 只读降级(用户定案 2026-09-01):进程内读 /tmp/meow_boot_mode,"meow"=全功能; * 否则(hiby/缺失)=只读——读级查询放行,一切写操作(wifi/bt on-off 等)拒 + 记日志。 * (与 audiod 的 exit(0) 门控不同:本服务读侧无害故常驻只读,每采样拍重判可热切换,同 powermgr。) * * 内核事实(NETWORK_HW_FACTS.md,期 0 实测):无 iptables/cgroup/conntrack;内存 available≈27MB → * 本服务 RSS 预算 ≤1MB(单线程 poll、静态表、热路径禁 malloc;popen 有界收割防 wedge,257 教训)。 * * 日志(§8):TF `MeowOS/logs/net_.log`(无卡降级 /tmp),每次开机新文件(复用 /tmp/meow_bootts * 共享 bootts,一次开机一份);接口状态变化/写操作必记。行格式 `bootts lvl tag kv...`。 * * 编译(mipsel 静态):build_netmgrd.sh;宿主:build_netmgrd.sh host。 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 */ static char g_netdev[160] = "/proc/net/dev"; /* MEOW_NETDEV */ static char g_net_route[160] = "/proc/net/route"; /* MEOW_NET_ROUTE */ static char g_fibtrie[160] = "/proc/net/fib_trie"; /* MEOW_FIBTRIE(无 ip 工具兜底) */ static char g_resolv[160] = "/etc/resolv.conf"; /* MEOW_RESOLV */ static char g_rfkill_dir[128] = "/sys/class/rfkill"; /* MEOW_RFKILL_DIR */ static char g_bt_dir[128] = "/sys/class/bluetooth"; /* MEOW_BT_DIR */ static char g_boot_mode[160] = "/tmp/meow_boot_mode"; /* MEOW_BOOT_MODE */ static char g_log_dir[192] = "/usr/data/mnt/sd_0/MeowOS/logs"; /* MEOW_LOG_DIR(TF 卡日志根) */ static char g_bootts_cache[160]= "/tmp/meow_bootts"; /* MEOW_BOOTTS_CACHE */ static const char *g_sock = "/usr/data/meow_net.sock"; /* MEOW_NET_SOCK/--sock */ /* 后端命令路径(宿主测试用桩脚本覆盖;默认走设备 PATH 的真工具) */ static const char *g_wpa_cli = "wpa_cli"; /* MEOW_WPA_CLI */ static const char *g_hciconfig = "hciconfig"; /* MEOW_HCICONFIG */ static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUETOOTHCTL */ 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]= "/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) */ #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) { struct timespec tp; clock_gettime(CLOCK_MONOTONIC, &tp); return tp.tv_sec; } /* ---------------- 极简日志(stderr;svcd supervisor 捕获) ---------------- */ static void svc_log(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "[meow_netmgrd] "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } /* ---------------- 文件小工具(同 svc_power) ---------------- */ static int read_file_txt(const char *path, char *buf, int cap) { int fd = open(path, 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; while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0; return n; } static int read_file_int_def(const char *path, int def) { char buf[32]; if (read_file_txt(path, buf, sizeof buf) <= 0) return def; return atoi(buf); } /* ---------------- 日志框架(§8:每次开机新文件,TF 优先降级 /tmp) ---------------- */ static int g_logfd = -1; static char g_logfile[192]; /* bootts 复用(与 launcher/audiod 共用 /tmp/meow_bootts 缓存,一次开机一份日志) */ static void get_bootts(char *out, size_t n) { int fd = open(g_bootts_cache, O_RDONLY); if (fd >= 0) { int r = read(fd, out, (int)n - 1); close(fd); if (r > 0) { out[r] = 0; return; } } time_t now = time(NULL), up = 0; fd = open("/proc/uptime", O_RDONLY); if (fd >= 0) { char buf[64]; int r = read(fd, buf, sizeof buf - 1); close(fd); if (r > 0) { buf[r] = 0; char *p = strchr(buf, ' '); if (p) *p = 0; up = (time_t)atof(buf); } } struct tm tm; localtime_r(&now - up, &tm); strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm); fd = open(g_bootts_cache, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd >= 0) { (void)write(fd, out, strlen(out)); close(fd); } } static void log_open(void) { char ts[64]; get_bootts(ts, sizeof ts); char path[260]; int use_tf = 0; if (access(g_log_dir, W_OK) == 0) { snprintf(path, sizeof path, "%s/net_%s.log", g_log_dir, ts); use_tf = 1; } else { snprintf(path, sizeof path, "/tmp/net_%s.log", ts); } snprintf(g_logfile, sizeof g_logfile, "%s", path); g_logfd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644); svc_log("log: %s (%s)", g_logfile, use_tf ? "TF" : "/tmp fallback"); } /* 日志行:`bootts lvl tag kv...`;stderr 与文件双写(svcd 捕获 stderr + 文件归档)。 */ static void net_log(const char *lvl, const char *tag, const char *fmt, ...) { char msg[512]; va_list ap; va_start(ap, fmt); vsnprintf(msg, sizeof msg, fmt, ap); va_end(ap); char bootts[64]; get_bootts(bootts, sizeof bootts); if (g_logfd >= 0) { char line[600]; int n = snprintf(line, sizeof line, "%s %s %s %s\n", bootts, lvl, tag, msg); if (n > 0 && n < (int)sizeof line) (void)write(g_logfd, line, (size_t)n); } fprintf(stderr, "[net] %s %s %s\n", lvl, tag, msg); } /* ---------------- 有界 popen 收割(257 教训:命令 wedge 时无条件 waitpid 会拖死调用线程) ---------------- * 非阻塞读子进程 stdout(poll 限时);读完后 WNOHANG 优雅等 300ms,不死 SIGKILL 再等 300ms; * 仍不死(D 态)放弃收割(泄漏一子进程换线程不卡死,已记日志)。返回读到的字节数(-1=起不来)。 * cmd 为完整 shell 行;buf 由调用方提供(静态/栈),不 malloc——热路径禁 malloc。 */ static void reap_bounded(pid_t pid, const char *cmd) { for (int i = 0; i < 30; i++) { if (waitpid(pid, NULL, WNOHANG) == pid) return; struct timespec ts = { 0, 10 * 1000000L }; nanosleep(&ts, NULL); } svc_log("run_cmd: child %ld not exit in 300ms, SIGKILL (%s)", (long)pid, cmd); kill(pid, SIGKILL); for (int i = 0; i < 30; i++) { if (waitpid(pid, NULL, WNOHANG) == pid) return; struct timespec ts = { 0, 10 * 1000000L }; nanosleep(&ts, NULL); } svc_log("run_cmd: child %ld unkillable (kernel D-state?) — abandon reap (%s)", (long)pid, cmd); } static int run_cmd(char *buf, int cap, int tmo_ms, const char *cmd) { int p[2]; if (pipe(p) < 0) return -1; pid_t pid = fork(); if (pid < 0) { close(p[0]); close(p[1]); return -1; } if (pid == 0) { dup2(p[1], 1); int dn = open("/dev/null", O_WRONLY); if (dn >= 0) { dup2(dn, 2); close(dn); } close(p[0]); close(p[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); } close(p[1]); fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK); int off = 0; int remain = tmo_ms; while (remain > 0 && off < cap - 1) { struct pollfd pf = { p[0], POLLIN, 0 }; int r = poll(&pf, 1, remain); if (r < 0) { if (errno == EINTR) continue; break; } if (r == 0) break; ssize_t rd = read(p[0], buf + off, (size_t)(cap - 1 - off)); if (rd == 0) break; if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; } off += (int)rd; } close(p[0]); buf[off] = 0; reap_bounded(pid, cmd); return off; } /* ---------------- JSON 构建(vsnprintf 助手法,截断分析免疫;同 powermgr) ---------------- */ typedef struct { char *buf; size_t cap, off; } jw_t; static void jadd(jw_t *w, const char *fmt, ...) { if (w->off >= w->cap - 1) return; va_list ap; va_start(ap, fmt); int n = vsnprintf(w->buf + w->off, w->cap - w->off, fmt, ap); va_end(ap); if (n > 0) { w->off += (size_t)n; if (w->off >= w->cap) w->off = w->cap - 1; } } /* 带引号的字符串值(转义 `"`/`\`/换行)。空串 → ""(不当 null,调用方自决)。 */ static void jadd_q(jw_t *w, const char *key, const char *val) { jadd(w, "\"%s\":\"", key); for (const char *p = val; *p && w->off < w->cap - 3; p++) { if (*p == '"' || *p == '\\') jadd(w, "\\%c", *p); else if (*p == '\n') jadd(w, "\\n"); else if (*p == '\t') jadd(w, "\\t"); else w->buf[w->off++] = *p; } jadd(w, "\","); } static void jadd_int_or_null(jw_t *w, const char *key, int v) { if (v < 0) jadd(w, "\"%s\":null,", key); else jadd(w, "\"%s\":%d,", key, v); } static void jadd_str_or_null(jw_t *w, const char *key, const char *s) { if (!s || !s[0]) jadd(w, "\"%s\":null,", key); else jadd_q(w, key, s); } /* ---------------- 动作执行结果容器 ---------------- */ typedef struct { int code; char text[MEOW_IPC_MAX - 8]; } ctl_result_t; static void ctl_reset(ctl_result_t *r, int code) { r->code = code; r->text[0] = 0; } static void ctl_sets(ctl_result_t *r, int code, const char *s) { r->code = code; snprintf(r->text, sizeof r->text, "%s", s); } /* ---------------- 接口统一层:模型(设计 §3 统一模型) ---------------- */ typedef struct { char name[32]; char kind[8]; /* "lo" | "wifi" | "usb-eth" */ int up; /* 1=up 0=down */ char ip[40]; /* ""=null */ char gw[40]; /* ""=null */ long rx, tx; /* 累计字节(/proc/net/dev) */ } iface_t; static iface_t g_ifaces[IFACE_MAX]; typedef struct { char entity[48]; uint32_t rx, tx; } ent_bytes_t; static ent_bytes_t g_ent_bytes[ENTITY_MAX]; /* SDK 自愿上报(§4 字节级记账自报通道;声明须在 build_status_json 前) */ static int g_ent_bytes_n = 0; static int g_iface_n = 0; typedef struct { int connected; /* 0/1 */ char state[16]; /* wpa_state 值;""=unknown */ char ssid[64]; char ip[40]; int rssi; /* 信号强度 dBm(可为负);has_rssi=0 时无效 */ int has_rssi; /* wpa_cli 是否给出 signal= */ } wifi_t; typedef struct { int available; int powered; char address[32]; int paired_n; /* -1=null */ } bt_t; typedef struct { char type[16]; /* wlan/bluetooth/... */ int soft; /* 0/1/-1 */ int hard; /* 0/1/-1 */ } rfkill_t; static rfkill_t g_rfkill[RFKILL_MAX]; static int boot_mode_is_meow(void) { char buf[16] = ""; if (read_file_txt(g_boot_mode, buf, sizeof buf) <= 0) return 0; /* 缺失=只读等落地 */ return strcmp(buf, "meow") == 0; } /* kind 判定:0=lo 1=wifi 2=usb-eth(热插拔出现即记录)。 */ static int iface_parse_kind(const char *name) { if (strcmp(name, "lo") == 0) return 0; if (strcmp(name, g_wifi_if) == 0) return 1; return 2; } static const char *kind_str(int k) { return k == 0 ? "lo" : (k == 1 ? "wifi" : "usb-eth"); } /* /proc/net/dev 单接口行 → rx(列1)/tx(列9) 累计字节;返回 0=找到。 */ static int netdev_iface(const char *ifname, long *rx, long *tx) { FILE *f = fopen(g_netdev, "r"); if (!f) return -1; char line[256]; int found = -1; while (fgets(line, sizeof line, f)) { char *c = strchr(line, ':'); if (!c) continue; *c = 0; char *n = line; while (*n == ' ' || *n == '\t') n++; if (strcmp(n, ifname) != 0) continue; char *save = NULL; char *tk = strtok_r(c + 1, " \t", &save); /* 列1 = rx bytes */ long vr = tk ? strtol(tk, NULL, 10) : 0; for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); /* 推进到列9 */ long vt = tk ? strtol(tk, NULL, 10) : 0; if (rx) *rx = vr; if (tx) *tx = vt; found = 0; break; } fclose(f); return found; } /* 全局 /proc/net/dev RX/TX 累计(所有接口求和)。 */ static void netdev_totals(long *rx, long *tx) { long trx = 0, ttx = 0; FILE *f = fopen(g_netdev, "r"); if (f) { char line[256]; while (fgets(line, sizeof line, f)) { char *c = strchr(line, ':'); if (!c) continue; char *save = NULL; char *tk = strtok_r(c + 1, " \t", &save); long vr = 0; if (tk) { vr = strtol(tk, NULL, 10); for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); if (tk) ttx += strtol(tk, NULL, 10); } trx += vr; } fclose(f); } if (rx) *rx = trx; if (tx) *tx = ttx; } /* iface IP:优先 `ip -o -4 addr show dev `;ip 缺失/无输出 → fib_trie 兜底(作用于主口)。 * 内核不提供 per-iface 主机 IP 的 /proc 文件,fib_trie 兜底无法映射接口——仅作用于 wifi 主口。 */ static int ip_parse_addr(char *out, size_t cap, const char *buf) { const char *p = buf; while (*p) { while (*p == ' ' || *p == '\t' || *p == '\n') p++; if (!*p) break; const char *tk = p; while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++; size_t tl = (size_t)(p - tk); const char *dot = memchr(tk, '.', tl); if (!dot) continue; const char *sl = memchr(tk, '/', tl); if (!sl) continue; size_t len = (size_t)(sl - tk); if (len >= cap) len = cap - 1; memcpy(out, tk, len); out[len] = 0; return 0; } return -1; } static int fib_trie_first_ip(char *out, size_t cap) { FILE *f = fopen(g_fibtrie, "r"); if (!f) return -1; char line[160]; char last_local[48] = ""; while (fgets(line, sizeof line, f)) { char *p = line; while (*p == ' ' || *p == '\t') p++; if (!isdigit((unsigned char)*p)) continue; char ip[48]; int i = 0; while (p[i] && p[i] != ' ' && p[i] != '\n' && i < 47) { ip[i] = p[i]; i++; } ip[i] = 0; if (strchr(ip, '.')) snprintf(last_local, sizeof last_local, "%s", ip); } fclose(f); if (last_local[0]) { snprintf(out, cap, "%s", last_local); return 0; } return -1; } static void iface_ip(const char *ifname, int want_fallback, char *out, size_t cap) { out[0] = 0; char cmd[192]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s -o -4 addr show dev %s 2>/dev/null", g_ip, ifname); char buf[256]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0 && ip_parse_addr(out, cap, buf) == 0) return; if (want_fallback) (void)fib_trie_first_ip(out, cap); } /* /proc/net/route → 该接口网关(RTF_GATEWAY,优取默认路由)。 */ static void iface_gw(const char *ifname, char *out, size_t cap) { out[0] = 0; FILE *f = fopen(g_net_route, "r"); if (!f) return; char line[256]; char best[40] = ""; while (fgets(line, sizeof line, f)) { char ifc[32]; unsigned long dest = 0, gw = 0, flags = 0; int n = sscanf(line, "%31s %lx %lx %lx", ifc, &dest, &gw, &flags); if (n < 4) continue; if (strcmp(ifc, ifname) != 0) continue; if (!(flags & 0x2)) continue; /* RTF_GATEWAY */ unsigned char b0 = (unsigned char)(gw & 0xff), b1 = (unsigned char)((gw >> 8) & 0xff); unsigned char b2 = (unsigned char)((gw >> 16) & 0xff), b3 = (unsigned char)((gw >> 24) & 0xff); snprintf(best, sizeof best, "%u.%u.%u.%u", b0, b1, b2, b3); if (dest == 0) break; /* 默认路由:最佳 */ } fclose(f); if (best[0]) snprintf(out, cap, "%s", best); } /* resolv.conf → nameserver 列表(≤DNS_MAX)。 */ static int dns_read(char out[DNS_MAX][40], int *n) { FILE *f = fopen(g_resolv, "r"); *n = 0; if (!f) return -1; char line[160]; while (fgets(line, sizeof line, f) && *n < DNS_MAX) { char *p = line; while (*p == ' ' || *p == '\t') p++; if (strncmp(p, "nameserver", 10) != 0) continue; 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 (strchr(ip, '.')) { snprintf(out[*n], 40, "%s", ip); (*n)++; } } fclose(f); return *n > 0 ? 0 : -1; } /* 扫描 /sys/class/net。detail=1 补 ip/gw(触发 ip popen);detail=0 仅 name/kind/up/rx/tx(节拍用,无 popen)。 */ static void enum_ifaces(int detail) { DIR *d = opendir(g_net_dir); g_iface_n = 0; if (d) { struct dirent *de; while ((de = readdir(d)) && g_iface_n < IFACE_MAX) { if (de->d_name[0] == '.') continue; iface_t *f = &g_ifaces[g_iface_n]; memset(f, 0, sizeof *f); snprintf(f->name, sizeof f->name, "%s", de->d_name); int ki = iface_parse_kind(de->d_name); snprintf(f->kind, sizeof f->kind, "%s", kind_str(ki)); char p[192]; snprintf(p, sizeof p, "%s/%s/operstate", g_net_dir, de->d_name); char os[16] = ""; read_file_txt(p, os, sizeof os); /* lo 的 operstate 常为 "unknown"(真机实测),但回环恒 up。 */ f->up = (ki == 0 || strcmp(os, "up") == 0) ? 1 : 0; long rx = -1, tx = -1; netdev_iface(de->d_name, &rx, &tx); f->rx = rx >= 0 ? rx : 0; f->tx = tx >= 0 ? tx : 0; if (detail) { iface_ip(f->name, ki == 1, f->ip, sizeof f->ip); /* fib_trie 兜底仅 wifi 主口 */ iface_gw(f->name, f->gw, sizeof f->gw); } g_iface_n++; } closedir(d); } /* 排序稳定:lo 首、wifi 次、usb-eth 末(冒泡,量小)。 */ for (int i = 0; i < g_iface_n; i++) for (int j = i + 1; j < g_iface_n; j++) { if (iface_parse_kind(g_ifaces[i].name) > iface_parse_kind(g_ifaces[j].name)) { iface_t t = g_ifaces[i]; g_ifaces[i] = g_ifaces[j]; g_ifaces[j] = t; } } } /* ---------------- WiFi:wpa_cli 包装(popen 有界) ---------------- */ static void wifi_status(wifi_t *w) { memset(w, 0, sizeof *w); w->rssi = -1; char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s status 2>/dev/null", g_wpa_cli); char buf[512]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) return; char *ln = buf; while (ln && *ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; if (!strncmp(ln, "wpa_state=", 10)) { snprintf(w->state, sizeof w->state, "%s", ln + 10); if (!strcmp(ln + 10, "COMPLETED")) w->connected = 1; } else if (!strncmp(ln, "ssid=", 5)) { snprintf(w->ssid, sizeof w->ssid, "%s", ln + 5); } else if (!strncmp(ln, "ip_address=", 11)) { snprintf(w->ip, sizeof w->ip, "%s", ln + 11); } else if (!strncmp(ln, "signal=", 7)) { w->rssi = atoi(ln + 7); w->has_rssi = 1; } if (!nl) break; ln = nl + 1; } /* 期4:status 无 signal=(本机 wpa_cli status 不输出)→ 回退 wpa_cli signal_poll(RSSI=)。 */ if (!w->has_rssi) { snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s signal_poll 2>/dev/null", g_wpa_cli); char sbuf[256]; if (run_cmd(sbuf, sizeof sbuf, RUNCMD_TMO_MS, cmd) > 0) { char *s = strstr(sbuf, "RSSI="); if (s) { w->rssi = atoi(s + 5); w->has_rssi = 1; } } } } /* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */ static void bt_status(bt_t *b) { memset(b, 0, sizeof *b); b->paired_n = -1; char p[192]; snprintf(p, sizeof p, "%s/hci0", g_bt_dir); if (access(p, F_OK) == 0) b->available = 1; if (!b->available) return; /* powered:/sys/.../power(on/off)优先,回退 hciconfig 行首 UP/DOWN */ snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir); char pw[8] = ""; if (read_file_txt(p, pw, sizeof pw) > 0 && pw[0]) { b->powered = !strcmp(pw, "on"); } else { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig); char buf[256]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) { char *ln = buf; while (ln && *ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; char *q = ln; while (*q == ' ' || *q == '\t') q++; if (!strncmp(q, "UP", 2)) { b->powered = 1; break; } if (!strncmp(q, "DOWN", 4)) { b->powered = 0; break; } if (!nl) break; ln = nl + 1; } } } /* address:/sys/.../address 优先,回退 hciconfig "BD Address:" */ snprintf(p, sizeof p, "%s/hci0/address", g_bt_dir); read_file_txt(p, b->address, sizeof b->address); if (!b->address[0]) { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig); char buf[256]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) { char *q = strstr(buf, "BD Address:"); if (q) { q += 11; while (*q == ' ') q++; int i = 0; while (*q && *q != ' ' && *q != '\n' && i < (int)sizeof b->address - 1) b->address[i++] = *q++; b->address[i] = 0; } } } /* paired 数:bluetoothctl paired(BlueZ 5.54);失败/空 → -1。 */ char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired 2>/dev/null", g_bluetoothctl); char buf[512]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) { int n = 0; char *ln = buf; while (ln && *ln) { char *nl = strchr(ln, '\n'); if (nl) *nl = 0; char *q = ln; while (*q == ' ' || *q == '\t') q++; if (!strncmp(q, "Device", 6)) n++; if (!nl) break; ln = nl + 1; } b->paired_n = n; } } /* ---------------- rfkill:统一软开关 ---------------- */ static void rfkill_list(void) { int n = 0; DIR *d = opendir(g_rfkill_dir); if (d) { struct dirent *de; while ((de = readdir(d)) && n < RFKILL_MAX) { if (strncmp(de->d_name, "rfkill", 6) != 0) continue; char p[448]; rfkill_t *r = &g_rfkill[n]; memset(r, 0, sizeof *r); snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name); read_file_txt(p, r->type, sizeof r->type); snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name); r->soft = read_file_int_def(p, -1); snprintf(p, sizeof p, "%s/%s/hard", g_rfkill_dir, de->d_name); r->hard = read_file_int_def(p, -1); n++; } closedir(d); } for (int i = n; i < RFKILL_MAX; i++) memset(&g_rfkill[i], 0, sizeof(rfkill_t)); } /* 找某类型 rfkill 设备并写 soft=block(1)/unblock(0)。返回 0=ok;-1=无该类型/写失败。 */ static int rfkill_set_block(const char *type, int block) { char norm[16] = ""; if (!strcmp(type, "wifi")) snprintf(norm, sizeof norm, "%s", "wlan"); else if (!strcmp(type, "bt")) snprintf(norm, sizeof norm, "%s", "bluetooth"); else snprintf(norm, sizeof norm, "%s", type); DIR *d = opendir(g_rfkill_dir); int rc = -1; if (d) { struct dirent *de; while ((de = readdir(d))) { if (strncmp(de->d_name, "rfkill", 6) != 0) continue; char p[448], t[16] = ""; snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name); if (read_file_txt(p, t, sizeof t) <= 0 || strcmp(t, norm) != 0) continue; snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name); int fd = open(p, O_WRONLY); if (fd < 0) continue; char one = block ? '1' : '0'; if (write(fd, &one, 1) == 1) rc = 0; close(fd); break; } closedir(d); } return rc; } /* ---------------- udhcpc:续租/释放(SIGUSR1/SIGUSR2,不改启动路径) ---------------- */ static int udhcpc_ctl(const char *op, const char *ifname) { char pidfile[160]; const char *iff = (ifname && ifname[0]) ? ifname : g_wifi_if; snprintf(pidfile, sizeof pidfile, "/var/run/udhcpc.%s.pid", iff); char buf[32]; if (read_file_txt(pidfile, buf, sizeof buf) <= 0) return -1; int pid = atoi(buf); if (pid <= 0) return -1; int sig = (strcmp(op, "release") == 0) ? SIGUSR2 : SIGUSR1; return kill(pid, sig) == 0 ? 0 : -1; } /* ---------------- IPC 帧读写(同 svc_power 约定,全小端) ---------------- */ static int x_read_full(int fd, uint8_t *buf, size_t n) { size_t off = 0; while (off < n) { ssize_t r = read(fd, buf + off, n - off); if (r <= 0) return -1; off += (size_t)r; } return 0; } static int x_write_full(int fd, const uint8_t *buf, size_t n) { size_t off = 0; while (off < n) { ssize_t w = write(fd, buf + off, n - off); if (w <= 0) return -1; off += (size_t)w; } return 0; } static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len) { uint8_t hdr[MEOW_IPC_HDR_LEN]; uint32_t magic = MEOW_IPC_MAGIC, rlen = len; uint16_t ver = MEOW_IPC_VERSION, t = type; memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2); memcpy(hdr + 8, &req_id, 4); memcpy(hdr + 12, &rlen, 4); if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1; if (len && payload && x_write_full(fd, payload, len) < 0) return -1; return 0; } static int send_ack(int fd, uint32_t req_id, uint32_t code, uint32_t status) { m_ack_t a; memset(&a, 0, sizeof a); a.code = code; a.status = status; return send_frame(fd, MSG_ACK, req_id, &a, sizeof a); } static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg) { m_err_t e; memset(&e, 0, sizeof e); e.code = code; uint16_t ml = msg ? (uint16_t)strlen(msg) : 0; if (ml > 63) ml = 63; e.msg_len = ml; if (ml) memcpy(e.msg, msg, ml); return send_frame(fd, MSG_ERR, req_id, &e, sizeof e); } /* json/text 结果通发(m_net_status_result_t / m_net_iface_result_t / m_net_cmd_result_t 布局同构 * code(4)+len(2)+pad(2)+payload,payload ≤ MEOW_IPC_MAX-8)。 */ static int send_payload_result(int fd, uint16_t type, uint32_t req_id, uint32_t code, const char *payload, size_t pl) { m_net_cmd_result_t r; /* 复用布局最大的那个(payload 区 4088B)。 */ memset(&r, 0, sizeof r); r.code = code; if (pl > sizeof r.text - 1) pl = sizeof r.text - 1; r.text_len = (uint16_t)pl; memcpy(r.text, payload, pl); r.text[pl] = 0; return send_frame(fd, type, req_id, &r, (uint32_t)(8 + pl + 1)); } static int send_net_status_result(int fd, uint32_t req_id, const char *json) { return send_payload_result(fd, MSG_NET_STATUS_RESULT, req_id, MEOW_OK, json, strlen(json)); } static int send_net_iface_result(int fd, uint32_t req_id, uint32_t code, const char *json) { return send_payload_result(fd, MSG_NET_IFACE_RESULT, req_id, code, json, strlen(json)); } static int send_net_wifi_result(int fd, uint32_t req_id, const ctl_result_t *r) { return send_payload_result(fd, MSG_NET_WIFI_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text)); } static int send_net_bt_result(int fd, uint32_t req_id, const ctl_result_t *r) { return send_payload_result(fd, MSG_NET_BT_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text)); } /* ---------------- 守护进程态 ---------------- */ static int g_mode_meow = 0; /* meow=1 全功能;0=只读降级 */ static long g_net_last_rx = -1, g_net_last_tx = 0, g_net_last_mono = 0; static long g_net_rate_rx = -1, g_net_rate_tx = -1; /* B/s;-1=未知 */ static char g_prev_if[IFACE_MAX][32]; static int g_prev_if_up[IFACE_MAX]; static int g_prev_if_n = 0; /* ---------------- JSON 构建:status/iface ---------------- */ static void iface_json_obj(jw_t *w, const iface_t *f) { jadd(w, "{\"name\":\"%s\",\"kind\":\"%s\",\"up\":%s,", f->name, f->kind, f->up ? "true" : "false"); jadd_str_or_null(w, "ip", f->ip); jadd_str_or_null(w, "gw", f->gw); jadd(w, "\"rx_bytes\":%ld,\"tx_bytes\":%ld}", f->rx, f->tx); } static void build_status_json(char *out, size_t cap) { jw_t w = { out, cap, 0 }; out[0] = 0; jadd(&w, "{\"mode\":\"%s\",\"boot_ts\":\"", g_mode_meow ? "meow" : "readonly"); { char ts[64]; get_bootts(ts, sizeof ts); for (const char *p = ts; *p; p++) w.buf[w.off++] = *p; } jadd(&w, "\",\"ts\":%ld,", (long)time(NULL)); enum_ifaces(1); jadd(&w, "\"ifaces\":["); for (int i = 0; i < g_iface_n; i++) { if (i) jadd(&w, ","); iface_json_obj(&w, &g_ifaces[i]); } jadd(&w, "],"); wifi_t wi; wifi_status(&wi); jadd(&w, "\"wifi\":{"); jadd_q(&w, "state", wi.state); jadd(&w, "\"connected\":%s,", wi.connected ? "true" : "false"); jadd_str_or_null(&w, "ssid", wi.ssid); jadd_str_or_null(&w, "ip", wi.ip); if (wi.has_rssi) jadd(&w, "\"rssi\":%d,", wi.rssi); else jadd(&w, "\"rssi\":null,"); jadd(&w, "\"networks\":null},"); bt_t bt; bt_status(&bt); jadd(&w, "\"bt\":{"); jadd(&w, "\"available\":%s,\"powered\":%s,", bt.available ? "true" : "false", bt.powered ? "true" : "false"); jadd_str_or_null(&w, "address", bt.address); jadd_int_or_null(&w, "paired", bt.paired_n); jadd(&w, "\"dns\":["); { char ns[DNS_MAX][40]; int n = 0; dns_read(ns, &n); for (int i = 0; i < n; i++) { if (i) jadd(&w, ","); jadd(&w, "\"%s\"", ns[i]); } } jadd(&w, "]},"); /* 关闭 dns[] + bt 对象 + 逗号(dns 是 bt 末字段,不能留尾逗号;此前漏收 bt 致 rfkill/rate 被吞、root 不闭合) */ rfkill_list(); jadd(&w, "\"rfkill\":["); for (int i = 0; i < RFKILL_MAX && g_rfkill[i].type[0]; i++) { if (i) jadd(&w, ","); jadd(&w, "{\"type\":\"%s\",\"soft\":%d,\"hard\":%d}", g_rfkill[i].type, g_rfkill[i].soft, g_rfkill[i].hard); } jadd(&w, "],"); jadd(&w, "\"rate\":{\"rx_bps\":"); if (g_net_rate_rx < 0) jadd(&w, "null,"); else jadd(&w, "%ld,", g_net_rate_rx); jadd(&w, "\"tx_bps\":"); if (g_net_rate_tx < 0) jadd(&w, "null},"); /* rate object 闭合 + 逗号(tx_bps 为 rate 末字段) */ else jadd(&w, "%ld},", g_net_rate_tx); /* 期4:SDK 自愿上报的 per-实体字节统计(§4 字节级记账自报通道);空则 []。 */ jadd(&w, "\"entity_bytes\":["); for (int i = 0; i < g_ent_bytes_n; i++) { if (i) jadd(&w, ","); jadd(&w, "{\"entity\":\"%s\",\"rx\":%u,\"tx\":%u}", g_ent_bytes[i].entity, g_ent_bytes[i].rx, g_ent_bytes[i].tx); } jadd(&w, "]}"); } /* 单/全接口详情 JSON。name 空=全部(数组);否则单对象。返回 MEOW_OK / MEOW_ERR_NOENT。 */ static int build_iface_json(const char *name, char *out, size_t cap) { enum_ifaces(1); jw_t w = { out, cap, 0 }; out[0] = 0; if (!name || !name[0]) { jadd(&w, "["); for (int i = 0; i < g_iface_n; i++) { if (i) jadd(&w, ","); iface_json_obj(&w, &g_ifaces[i]); } jadd(&w, "]"); return MEOW_OK; } for (int i = 0; i < g_iface_n; i++) { if (strcmp(g_ifaces[i].name, name) == 0) { iface_json_obj(&w, &g_ifaces[i]); /* 去掉尾 } 补 dns(期 1 不分接口,统一读 resolv.conf)。 */ if (w.off >= 2 && w.buf[w.off - 1] == '}') w.buf[--w.off] = 0; jadd(&w, ",\"dns\":["); char ns[DNS_MAX][40]; int n = 0; dns_read(ns, &n); for (int i2 = 0; i2 < n; i2++) { if (i2) jadd(&w, ","); jadd(&w, "\"%s\"", ns[i2]); } jadd(&w, "]}"); return MEOW_OK; } } 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; 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= */ static int g_dns_filter_aaaa = 0; /* global dns.filter_aaaa=:0=透传 AAAA(默认),1=过滤(纯 IPv4 设备弱化 IPv6 优先) */ /* ---- 期3 防自锁清单(NETMGR_DESIGN §14 红线):这些实体拒绝/强制置 allow,防断网变砖。 ---- */ static int _is_selflock_by_name(const char *name) { 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]; 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; } /* ---- 策略引擎(§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; 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 rule_t *entity_rules_match(rule_t *r, const char *entity) { if (!r || !r->entity[0] || strcmp(r->entity, entity) != 0) return NULL; if (strcmp(r->entity, "global") == 0) return r; return r; } 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) { 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])) { for (int j = 0; j < r->allow_n; j++) if (suffix_match(domain, r->allow[j])) return 0; return 1; } } 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]; 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; if (!sp) continue; *sp = 0; char *entity = p, *rest = sp + 1; if (strcmp(entity, "global") == 0) { /* global log_level= dns.filter_aaaa=0|1 * 注意:log_level 解析会在 rest 中部写 NUL 截断,故 dns.filter_aaaa 必须先解析。 */ char *fa = strstr(rest, "dns.filter_aaaa="); if (fa) { char *v = fa + 16; while (*v==' '||*v=='\t') v++; g_dns_filter_aaaa = atoi(v) ? 1 : 0; } char *lv = strstr(rest, "log_level="); if (lv) { char *v = lv + 10; while (*v==' '||*v=='\t'||*v=='"') v++; char *e2 = v; while (*e2 && *e2!=' ' && *e2!='\t' && *e2!='\n') e2++; *e2=0; 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) { 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; } } /* 空规则(无 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 dns.filter_aaaa=%d\n", g_policy_log_level, g_dns_filter_aaaa); if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; } else { rule_t *r = policy_find(entity); if (r) { if (r->has_net) { int l = snprintf(out + o, sizeof out - o, "%s net=%s\n", entity, r->net_block ? "block" : "allow"); if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; } if (r->deny_n || r->allow_n) { int l = snprintf(out + o, sizeof out - o, "%s domain=", entity); if (l > 0 && o + (size_t)l < sizeof out) o += (size_t)l; int first = 1; for (int i = 0; i < r->allow_n; i++) { int la = snprintf(out + o, sizeof out - o, "%s+%s", first ? "" : ",", r->allow[i]); if (la > 0 && o + (size_t)la < sizeof out) o += (size_t)la; first = 0; } for (int i = 0; i < r->deny_n; i++) { int ld = snprintf(out + o, sizeof out - o, "%s-%s", first ? "" : ",", r->deny[i]); if (ld > 0 && o + (size_t)ld < sizeof out) o += (size_t)ld; first = 0; } int ln = snprintf(out + o, sizeof out - o, "\n"); if (ln > 0 && o + (size_t)ln < sizeof out) o += (size_t)ln; } } } out[o] = 0; policy_write_atomic(out); rules_load(); /* 写后重载(自愈:也剔除任何被手工改回的自锁 block) */ } /* ---- 实体归因:pid→(app: / svc: / proc:) ---- */ static void pid_ent_put(int pid, const char *entity) { 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++) { /* 任何未归因的连接一律落 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); } 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, int *qtype) { 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; if (qtype) *qtype = (q[off] << 8) | q[off + 1]; /* QTYPE:1=A 28=AAAA */ off += 4; /* qtype+qclass */ *qend = off; return di > 0 ? 0 : -1; } 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, NULL) != 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, qtype = 0; if (dns_parse_query(q, (int)n, dom, sizeof dom, &qend, &qtype) != 0 || !dom[0]) return; const char *entity = dns_entity_for_port((uint16_t)ntohs(from.sin_port)); /* 域名规则:deny → NXDOMAIN + 日志 */ if (dns_query_domain_denied(entity, dom)) { 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; } /* 期4 AAAA 过滤(dns.filter_aaaa=1,纯 IPv4 设备弱化 IPv6 优先):回「无 AAAA」空应答 → 客户端回落 A。 */ if (g_dns_filter_aaaa && qtype == 28) { int qsec = qend - 12; if (qsec > 0 && 12 + qsec <= (int)sizeof q) { uint8_t resp[1536]; memcpy(resp, q, 12); resp[2] = 0x81; resp[3] = 0x80; /* QR|RD|RA, rcode=0 */ resp[4] = 0; resp[5] = 1; /* qdcount=1 */ resp[6] = 0; resp[7] = 0; resp[8] = 0; resp[9] = 0; resp[10] = 0; resp[11] = 0; memcpy(resp + 12, q + 12, (size_t)qsec); sendto(g_dns_fd, resp, (size_t)(12 + qsec), 0, (struct sockaddr *)&from, fl); net_log("info", "dns", "aaaa filtered entity=%s domain=%s", entity, dom); } return; } /* 只记新域名首见(全量 debug 级,防日志爆炸) */ int cached = dns_cache_lookup_name(dom); if (cached != 0) { g_dns_first++; net_log("info", "dns", "first entity=%s domain=%s", entity, dom); } /* 上游透传转发 */ 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) { int fm = boot_mode_is_meow(); 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); int cur = g_iface_n; for (int i = 0; i < cur; i++) { int found = -1; for (int j = 0; j < g_prev_if_n; j++) if (strcmp(g_prev_if[j], g_ifaces[i].name) == 0) { found = j; break; } if (found < 0) net_log("info", "iface", "add kind=%s name=%s", g_ifaces[i].kind, g_ifaces[i].name); else if (g_prev_if_up[found] != g_ifaces[i].up) net_log("info", "iface", "state name=%s up=%d", g_ifaces[i].name, g_ifaces[i].up); if (found >= 0) g_prev_if_up[found] = g_ifaces[i].up; } for (int j = 0; j < g_prev_if_n; j++) { int gone = 1; for (int i = 0; i < cur; i++) if (strcmp(g_prev_if[j], g_ifaces[i].name) == 0) { gone = 0; break; } if (gone) net_log("info", "iface", "del name=%s", g_prev_if[j]); } for (int i = 0; i < cur; i++) { snprintf(g_prev_if[i], sizeof g_prev_if[i], "%s", g_ifaces[i].name); g_prev_if_up[i] = g_ifaces[i].up; } g_prev_if_n = cur; /* 全局速率差分(2s 拍):/proc/net/dev 累计。 */ long rx = -1, tx = 0; netdev_totals(&rx, &tx); if (rx >= 0) { long now = mono_now(); if (g_net_last_rx >= 0 && now > g_net_last_mono) { long dt = now - g_net_last_mono; if (dt > 0) { g_net_rate_rx = (rx - g_net_last_rx) / dt; g_net_rate_tx = (tx - g_net_last_tx) / dt; if (g_net_rate_rx < 0) g_net_rate_rx = 0; if (g_net_rate_tx < 0) g_net_rate_tx = 0; } } 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) ---------------- */ typedef enum { CALL_OTHER, CALL_PKG, CALL_SYS } caller_kind_t; typedef struct { caller_kind_t kind; char pkg[MEOW_APP_ID_MAX]; uid_t uid; } caller_t; static int is_sys_identity(const caller_t *c) { if (c->kind == CALL_SYS) return 1; if (c->kind == CALL_PKG && !strncmp(c->pkg, "os.meow.", 8)) return 1; return 0; } static void classify_exe(const char *exe, caller_t *out) { static const char *mp = "/usr/data/mnt/sd_0/MeowAPPs/"; if (strncmp(exe, mp, strlen(mp)) == 0) { const char *p = exe + strlen(mp); if (*p == '/') p++; const char *end = strchr(p, '/'); int plen = end ? (int)(end - p) : (int)strlen(p); if (plen > 0 && plen < MEOW_APP_ID_MAX) { memcpy(out->pkg, p, (size_t)plen); out->pkg[plen] = 0; out->kind = CALL_PKG; return; } } 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; memcpy(tmp, exe + 9, l); tmp[l] = 0; } else if (strncmp(exe, "/usr/data/meow/", 15) == 0) { snprintf(tmp, sizeof tmp, "%s", "os.meow.core"); } else { return; /* CALL_OTHER */ } snprintf(out->pkg, sizeof out->pkg, "%s", tmp); out->kind = CALL_SYS; } static void classify_caller(int fd, caller_t *out) { memset(out, 0, sizeof *out); out->uid = (uid_t)-1; struct ucred cred; socklen_t cl = sizeof cred; if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cl) != 0) return; out->uid = cred.uid; char procpath[64], exe[256]; snprintf(procpath, sizeof procpath, "/proc/%d/exe", (int)cred.pid); ssize_t n = readlink(procpath, exe, sizeof exe - 1); if (n <= 0) return; exe[n] = 0; classify_exe(exe, out); } /* CTL 写级分类(设计 §10/HibyOS:写级仅系统身份 + MeowOS 模式才允许)。 */ static int wifi_cmd_is_write(const char *cmd, const char *arg) { if (!strcmp(cmd, "status") || !strcmp(cmd, "networks")) return 0; /* 读级 */ if (!strcmp(cmd, "rfkill")) return arg && strcmp(arg, "list") == 0 ? 0 : 1; return 1; /* scan/connect/disconnect/on/off/udhcpc */ } static int bt_cmd_is_write(const char *cmd) { if (!strcmp(cmd, "status") || !strcmp(cmd, "paired")) return 0; return 1; /* on/off */ } /* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。 * 返回 MEOW_OK(放行)或 MEOW_ERR_PERM(拒绝)。 */ static int wifi_write_authorize(const caller_t *c, int mode_meow, const char *cmd, const char *arg) { if (!wifi_cmd_is_write(cmd, arg)) return MEOW_OK; if (!is_sys_identity(c)) return MEOW_ERR_PERM; if (!mode_meow) return MEOW_ERR_PERM; return MEOW_OK; } static int bt_write_authorize(const caller_t *c, int mode_meow, const char *cmd) { if (!bt_cmd_is_write(cmd)) return MEOW_OK; if (!is_sys_identity(c)) return MEOW_ERR_PERM; if (!mode_meow) return MEOW_ERR_PERM; return MEOW_OK; } /* ---------------- WiFi/BT/rfkill/udhcpc 动作执行(dæmon 侧;写级已在上游鉴权) ---------------- */ static const char *str_capsafe(const char *s, char *tmp, size_t cap) { size_t n = strlen(s); while (n > 0 && (s[0] == ' ' || s[0] == '\t')) { s++; n--; } while (n > 0 && (s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\n')) n--; if (n >= cap) n = cap - 1; memcpy(tmp, s, n); tmp[n] = 0; return tmp; } static void wifi_scan(ctl_result_t *r) { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli); char buf[128]; run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd); struct timespec ts = { 0, 300 * 1000000L }; /* 触发后稍候再读结果(有界)。 */ nanosleep(&ts, NULL); char bufout[MEOW_IPC_MAX - 16]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli); int n = run_cmd(bufout, sizeof bufout, RUNCMD_TMO_MS, cmd); if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "scan: no results (wpa_cli 空/失败)"); return; } ctl_sets(r, MEOW_OK, bufout); } static void wifi_networks(ctl_result_t *r) { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli); char buf[MEOW_IPC_MAX - 16]; int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd); if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "networks: no output (wpa_cli 空/失败)"); return; } ctl_sets(r, MEOW_OK, buf); } static void wifi_connect(const char *arg, ctl_result_t *r) { char ssid[96] = "", psk[96] = ""; if (arg && arg[0]) { const char *comma = strchr(arg, ','); if (comma) { size_t sl = (size_t)(comma - arg); if (sl >= sizeof ssid) sl = sizeof ssid - 1; memcpy(ssid, arg, sl); ssid[sl] = 0; snprintf(psk, sizeof psk, "%s", comma + 1); } else { snprintf(ssid, sizeof ssid, "%s", arg); } } char tmp_ssid[96], tmp_psk[96]; str_capsafe(ssid, tmp_ssid, sizeof tmp_ssid); str_capsafe(psk, tmp_psk, sizeof tmp_psk); if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; } /* 找既有网络:list_networks → 匹配 ssid(剥引号比较);命中则复用其 id。 */ char cmd[200]; char list[1024]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli); int n = run_cmd(list, sizeof list, RUNCMD_TMO_MS, cmd); int id = -1; if (n > 0) { char *ln = strtok(list, "\n"); while (ln) { char *tab0 = strchr(ln, '\t'); if (tab0) { int this_id = atoi(ln); char *ss = tab0 + 1; char *tab1 = strchr(ss, '\t'); if (tab1) *tab1 = 0; char clean[128], norm_clean[128]; size_t cl = strlen(ss); if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') { memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0; } else { snprintf(clean, sizeof clean, "%s", ss); } str_capsafe(clean, norm_clean, sizeof norm_clean); if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id; } ln = strtok(NULL, "\n"); } } char out[128]; if (id < 0) { snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s add_network 2>/dev/null", g_wpa_cli); if (run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd) <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)"); return; } id = atoi(out); if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; } snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d ssid '\"%s\"' 2>/dev/null", g_wpa_cli, id, tmp_ssid); run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd); if (tmp_psk[0]) { snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d psk '\"%s\"' 2>/dev/null", g_wpa_cli, id, tmp_psk); } else { snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d key_mgmt NONE 2>/dev/null", g_wpa_cli, id); } run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd); } { /* enable + select 该网络(复用/新建皆用)。 */ char en[256], sel[256]; snprintf(en, sizeof en, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s enable_network %d 2>/dev/null", g_wpa_cli, id); run_cmd(out, sizeof out, RUNCMD_TMO_MS, en); 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%s)", tmp_ssid, id, g_mode_meow ? "+save_config" : ""); ctl_sets(r, MEOW_OK, msg); } static void wifi_onoff(const char *cmd, ctl_result_t *r) { int block = strcmp(cmd, "off") == 0; int rc = rfkill_set_block("wlan", block); if (rc == 0) { char msg[64]; snprintf(msg, sizeof msg, "wifi %s: rfkill wlan %s", cmd, block ? "blocked" : "unblocked"); ctl_sets(r, MEOW_OK, msg); } else { ctl_sets(r, MEOW_ERR_NODEV, "wifi on/off: 无 wlan rfkill 设备或写失败"); } } static void rfkill_ctl(const char *arg, ctl_result_t *r) { char tmp[64]; str_capsafe(arg ? arg : "", tmp, sizeof tmp); char *sp = strchr(tmp, ' '); if (!sp) { ctl_sets(r, MEOW_ERR_INVAL, "rfkill: 用法 (如 `rfkill wlan on`)"); return; } *sp = 0; char *type = tmp; char *state = sp + 1; int block = strcmp(state, "off") == 0 ? 1 : (strcmp(state, "on") == 0 ? 0 : -1); if (block < 0) { ctl_sets(r, MEOW_ERR_INVAL, "rfkill: state 须 on/off"); return; } int rc = rfkill_set_block(type, block); if (rc == 0) { char msg[64]; snprintf(msg, sizeof msg, "rfkill %s %s: done", type, block ? "blocked" : "unblocked"); ctl_sets(r, MEOW_OK, msg); } else { ctl_sets(r, MEOW_ERR_NODEV, "rfkill: 无该类型设备或写失败"); } } static void udhcpc_ctl_svc(const char *arg, ctl_result_t *r) { char tmp[96]; str_capsafe(arg ? arg : "", tmp, sizeof tmp); char *sp = strchr(tmp, ' '); char op[16], iff[32] = ""; if (sp) { *sp = 0; snprintf(op, sizeof op, "%s", tmp); snprintf(iff, sizeof iff, "%s", sp + 1); } else snprintf(op, sizeof op, "%s", tmp); if (strcmp(op, "renew") != 0 && strcmp(op, "release") != 0) { ctl_sets(r, MEOW_ERR_INVAL, "udhcpc: op 须 renew/release"); return; } int rc = udhcpc_ctl(op, iff[0] ? iff : NULL); if (rc == 0) { char msg[80]; snprintf(msg, sizeof msg, "udhcpc %s %s: sent SIGUSR%s", op, iff[0] ? iff : g_wifi_if, strcmp(op, "release") == 0 ? "2" : "1"); ctl_sets(r, MEOW_OK, msg); } else { ctl_sets(r, MEOW_ERR_NOENT, "udhcpc: 无运行进程 (pid 文件缺失)"); } } static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) { ctl_reset(r, MEOW_OK); if (!strcmp(cmd, "status")) { wifi_t w; wifi_status(&w); char msg[256]; snprintf(msg, sizeof msg, "wifi state=%s connected=%d ssid=%s ip=%s rssi=%d", w.state[0] ? w.state : "unknown", w.connected, w.ssid[0] ? w.ssid : "-", w.ip[0] ? w.ip : "-", w.rssi); ctl_sets(r, MEOW_OK, msg); } else if (!strcmp(cmd, "scan")) { wifi_scan(r); } else if (!strcmp(cmd, "networks")) { wifi_networks(r); } else if (!strcmp(cmd, "connect")) { wifi_connect(arg, r); } else if (!strcmp(cmd, "disconnect")) { char cmd2[160]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli); char buf[128]; int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, "wifi disconnect: 已下发"); } else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) { wifi_onoff(cmd, r); } else if (!strcmp(cmd, "forget")) { int nid = arg ? atoi(arg) : -1; if (nid < 0) { ctl_sets(r, MEOW_ERR_INVAL, "wifi forget 需 "); return; } char cmd2[192]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s remove_network %d 2>/dev/null", g_wpa_cli, nid); char buf[128]; int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); /* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。 */ if (g_mode_meow) { snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli); run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); } char msg[64]; snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? "+save_config" : ""); ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg); } else if (!strcmp(cmd, "rfkill")) { rfkill_ctl(arg, r); } else if (!strcmp(cmd, "udhcpc")) { udhcpc_ctl_svc(arg, r); } else { ctl_sets(r, MEOW_ERR_INVAL, "wifi: 未知子命令"); } } static void bt_onoff(const char *cmd, ctl_result_t *r) { int up = strcmp(cmd, "on") == 0; char cmd2[160]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 %s 2>/dev/null", g_hciconfig, up ? "up" : "down"); char buf[128]; int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); rfkill_set_block("bluetooth", up ? 0 : 1); ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, up ? "bt on: hci0 up + rfkill unblocked" : "bt off: hci0 down + rfkill blocked"); } static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) { (void)arg; ctl_reset(r, MEOW_OK); if (!strcmp(cmd, "status")) { bt_t b; bt_status(&b); char msg[192]; snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d", b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n); ctl_sets(r, MEOW_OK, msg); } else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) { bt_onoff(cmd, r); } else if (!strcmp(cmd, "paired")) { char cmd2[160]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired 2>/dev/null", g_bluetoothctl); char buf[MEOW_IPC_MAX - 16]; int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2); if (n <= 0) ctl_sets(r, MEOW_ERR_NODEV, "bt paired: 无输出 (bluetoothctl 不可用/未开)"); else ctl_sets(r, MEOW_OK, buf); } else { ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令"); } } /* ---------------- 期3 策略命令(IPC 0xEA / CLI policy 共用;写级已在 handle_conn 鉴权) ---------------- */ /* 从 g_rules 移除某实体(shift 数组),返回 0=存在已删。 */ static int policy_remove(const char *entity) { for (int i = 0; i < g_rule_n; i++) { if (strcmp(g_rules[i].entity, entity) == 0) { for (int j = i; j < g_rule_n - 1; j++) g_rules[j] = g_rules[j + 1]; memset(&g_rules[g_rule_n - 1], 0, sizeof(rule_t)); g_rule_n--; return 0; } } return -1; } /* 输出某实体当前行;无规则输出 (none)。 */ static void policy_print_entity(const rule_t *r, char *out, size_t cap) { out[0] = 0; if (!r) { snprintf(out, cap, "(none)\n"); return; } size_t o = 0; int l = snprintf(out + o, cap - o, "%s ", r->entity); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; int any = 0; if (r->has_net) { l = snprintf(out + o, cap - o, "net=%s ", r->net_block ? "block" : "allow"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; any = 1; } if (r->deny_n || r->allow_n) { l = snprintf(out + o, cap - o, "domain="); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; int first = 1; for (int i = 0; i < r->allow_n; i++) { int la = snprintf(out + o, cap - o, "%s+%s", first ? "" : ",", r->allow[i]); if (la > 0 && o + (size_t)la < cap) o += (size_t)la; first = 0; } for (int i = 0; i < r->deny_n; i++) { int ld = snprintf(out + o, cap - o, "%s-%s", first ? "" : ",", r->deny[i]); if (ld > 0 && o + (size_t)ld < cap) o += (size_t)ld; first = 0; } any = 1; } if (!any) { l = snprintf(out + o, cap - o, "(allow)"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; } l = snprintf(out + o, cap - o, "\n"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; } static int policy_cmd(int op, const char *entity, const char *arg, char *out, size_t cap) { out[0] = 0; if (op == NET_POLICY_SHOW) { snprintf(out, cap, "# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n", g_policy_log_level); size_t o = strlen(out); if (entity && entity[0]) { rule_t *r = policy_find(entity); policy_print_entity(r, out + o, cap - o); } else { for (int i = 0; i < g_rule_n; i++) { char l[160]; policy_print_entity(&g_rules[i], l, sizeof l); int n = snprintf(out + o, cap - o, "%s", l); if (n > 0 && o + (size_t)n < cap) o += (size_t)n; } } return MEOW_OK; } if (op == NET_POLICY_SET_NET) { if (!entity || !entity[0] || !arg || (strcmp(arg, "allow") != 0 && strcmp(arg, "block") != 0)) { snprintf(out, cap, "policy set 需 allow|block\n"); return MEOW_ERR_INVAL; } if (strcmp(arg, "block") == 0 && is_self_lock(entity)) { snprintf(out, cap, "policy: %s 是防自锁实体(SSH/WiFi/自身命根),拒绝置 block(NETMGR_DESIGN §14 红线)\n", entity); net_log("warn", "policy", "self-lock entity %s tried block — 拒", entity); return MEOW_ERR_PERM; } rule_t *r = policy_find(entity); if (!r) { if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; } r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r); snprintf(r->entity, sizeof r->entity, "%s", entity); } r->net_block = strcmp(arg, "block") == 0; r->has_net = 1; policy_persist_entity(entity); net_log("info", "policy", "set %s net=%s", entity, arg); snprintf(out, cap, "policy set %s net=%s ok\n", entity, arg); return MEOW_OK; } if (op == NET_POLICY_SET_DOMAIN) { if (!entity || !entity[0] || !arg || (arg[0] != '+' && arg[0] != '-') || arg[1] == 0) { snprintf(out, cap, "policy domain 需 +host|-host\n"); return MEOW_ERR_INVAL; } rule_t *r = policy_find(entity); if (!r) { if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; } r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r); snprintf(r->entity, sizeof r->entity, "%s", entity); } if (arg[0] == '+') { if (r->allow_n < RULE_HOST_MAX) { snprintf(r->allow[r->allow_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s +%s ok\n", entity, arg + 1); } else { snprintf(out, cap, "policy: allow 槽满\n"); return MEOW_ERR_NOMEM; } } else { if (r->deny_n < RULE_HOST_MAX) { snprintf(r->deny[r->deny_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s -%s ok\n", entity, arg + 1); } else { snprintf(out, cap, "policy: deny 槽满\n"); return MEOW_ERR_NOMEM; } } policy_persist_entity(entity); net_log("info", "policy", "domain %s %s", entity, arg); return MEOW_OK; } if (op == NET_POLICY_DEL) { if (!entity || !entity[0]) { snprintf(out, cap, "policy del 需 \n"); return MEOW_ERR_INVAL; } if (policy_remove(entity) == 0) { policy_persist_entity(entity); net_log("info", "policy", "del %s", entity); snprintf(out, cap, "policy del %s ok\n", entity); } else { snprintf(out, cap, "policy del %s: 无此实体\n", entity); } return MEOW_OK; } snprintf(out, cap, "policy: 未知 op\n"); return MEOW_ERR_INVAL; } /* ---------------- 处理单条连接 ---------------- */ static void handle_conn(int c) { caller_t caller; classify_caller(c, &caller); for (;;) { uint8_t hdr[MEOW_IPC_HDR_LEN]; if (x_read_full(c, hdr, sizeof hdr) < 0) break; uint32_t magic, rlen, req_id; uint16_t ver, type; memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(&type, hdr + 6, 2); memcpy(&req_id, hdr + 8, 4); memcpy(&rlen, hdr + 12, 4); if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) { send_err(c, req_id, MEOW_ERR_INVAL, "bad magic/ver"); break; } if (rlen > MEOW_IPC_MAX) { send_err(c, req_id, MEOW_ERR_INVAL, "payload too big"); break; } uint8_t payload[MEOW_IPC_MAX]; if (rlen && x_read_full(c, payload, rlen) < 0) break; if (type == MSG_HELLO) { send_ack(c, req_id, MEOW_OK, 0); continue; } if (type == MSG_NET_STATUS_REQ) { char json[MEOW_IPC_MAX]; build_status_json(json, sizeof json); send_net_status_result(c, req_id, json); continue; } if (type == MSG_NET_IFACE_REQ) { m_net_iface_req_t q; memset(&q, 0, sizeof q); if (rlen > sizeof q) rlen = sizeof q; memcpy(&q, payload, rlen); char json[MEOW_IPC_MAX]; int rc = build_iface_json(q.name, json, sizeof json); send_net_iface_result(c, req_id, rc == MEOW_OK ? MEOW_OK : MEOW_ERR_NOENT, json); continue; } if (type == MSG_NET_WIFI_CTL) { m_net_cmd_t q; memset(&q, 0, sizeof q); if (rlen > sizeof q) rlen = sizeof q; memcpy(&q, payload, rlen); ctl_result_t r; int wr = wifi_write_authorize(&caller, g_mode_meow, q.cmd, q.arg); if (wr != MEOW_OK) { ctl_sets(&r, MEOW_ERR_PERM, !is_sys_identity(&caller) ? "permission denied (wifi ctl write needs system identity)" : "readonly (HibyOS mode): wifi write denied"); net_log("warn", "write", "denied wifi cmd=%s arg=%s caller_kind=%d uid=%d", q.cmd, q.arg, (int)caller.kind, (int)caller.uid); send_net_wifi_result(c, req_id, &r); continue; } wifi_ctl_execute(q.cmd, q.arg, &r); if (wifi_cmd_is_write(q.cmd, q.arg)) net_log("info", "write", "wifi cmd=%s arg=%s rc=%d", q.cmd, q.arg, r.code); send_net_wifi_result(c, req_id, &r); continue; } if (type == MSG_NET_BT_CTL) { m_net_cmd_t q; memset(&q, 0, sizeof q); if (rlen > sizeof q) rlen = sizeof q; memcpy(&q, payload, rlen); ctl_result_t r; int wr = bt_write_authorize(&caller, g_mode_meow, q.cmd); if (wr != MEOW_OK) { ctl_sets(&r, MEOW_ERR_PERM, !is_sys_identity(&caller) ? "permission denied (bt ctl write needs system identity)" : "readonly (HibyOS mode): bt write denied"); net_log("warn", "write", "denied bt cmd=%s caller_kind=%d uid=%d", q.cmd, (int)caller.kind, (int)caller.uid); send_net_bt_result(c, req_id, &r); continue; } bt_ctl_execute(q.cmd, q.arg, &r); if (bt_cmd_is_write(q.cmd)) net_log("info", "write", "bt cmd=%s rc=%d", q.cmd, r.code); send_net_bt_result(c, req_id, &r); continue; } 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; } 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; } if (type == MSG_NET_SDK) { m_net_sdk_t q; memset(&q, 0, sizeof q); if (rlen > sizeof q) rlen = sizeof q; memcpy(&q, payload, rlen); m_net_sdk_result_t res; memset(&res, 0, sizeof res); char ent[48]; snprintf(ent, sizeof ent, "app:%s", q.app_id[0] ? q.app_id : "unknown"); if (q.op == NET_SDK_CHECK) { int blocked = policy_net_blocked(ent) || dns_query_domain_denied(ent, q.host); res.allow = blocked ? 0 : 1; net_log("info", "sdk", "check app=%s host=%s -> %s", q.app_id, q.host, blocked ? "block" : "allow"); snprintf(res.text, sizeof res.text, "sdk check app=%s host=%s %s\n", q.app_id, q.host, blocked ? "block" : "allow"); res.code = MEOW_OK; } else if (q.op == NET_SDK_REPORT) { /* 自愿字节上报 → per-实体统计(§4 字节级记账自报)。 */ for (int i = 0; i < g_ent_bytes_n; i++) if (strcmp(g_ent_bytes[i].entity, ent) == 0) { g_ent_bytes[i].rx += q.rx; g_ent_bytes[i].tx += q.tx; res.code = MEOW_OK; snprintf(res.text, sizeof res.text, "sdk report app=%s rx=%u tx=%u (累计 %u/%u)\n", q.app_id, q.rx, q.tx, g_ent_bytes[i].rx, g_ent_bytes[i].tx); goto sdk_send; } if (g_ent_bytes_n < ENTITY_MAX) { snprintf(g_ent_bytes[g_ent_bytes_n].entity, sizeof g_ent_bytes[g_ent_bytes_n].entity, "%s", ent); g_ent_bytes[g_ent_bytes_n].rx = q.rx; g_ent_bytes[g_ent_bytes_n].tx = q.tx; g_ent_bytes_n++; res.code = MEOW_OK; snprintf(res.text, sizeof res.text, "sdk report app=%s rx=%u tx=%u\n", q.app_id, q.rx, q.tx); } else { res.code = MEOW_ERR_NOMEM; snprintf(res.text, sizeof res.text, "sdk report: entity 槽满\n"); } } else { res.code = MEOW_ERR_INVAL; snprintf(res.text, sizeof res.text, "sdk: 未知 op\n"); } sdk_send: { size_t tl = strlen(res.text); if (tl > sizeof res.text - 1) tl = sizeof res.text - 1; res.text_len = (uint16_t)tl; send_frame(c, MSG_NET_SDK_RESULT, req_id, &res, (uint32_t)(12 + tl + 1)); } continue; } send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1-4: STATUS/IFACE/WIFI_CTL/BT_CTL/CONNS/DNS/POLICY/SDK)"); break; } } /* ---------------- 守护主循环(单线程 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; memset(&a, 0, sizeof a); a.sun_family = AF_UNIX; strncpy(a.sun_path, sock, sizeof a.sun_path - 1); unlink(sock); if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) { svc_log("bind %s fail: %s", sock, strerror(errno)); close(lfd); return 1; } chmod(sock, 0666); if (listen(lfd, 8) < 0) { svc_log("listen fail: %s", strerror(errno)); close(lfd); return 1; } g_mode_meow = boot_mode_is_meow(); log_open(); 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(); next_tick = now + g_poll_sec; } int wait_ms = (int)((next_tick - mono_now()) * 1000); if (wait_ms < 50) wait_ms = 50; if (wait_ms > g_poll_sec * 1000) wait_ms = g_poll_sec * 1000; struct pollfd pfd = { lfd, POLLIN, 0 }; int pr = poll(&pfd, 1, wait_ms); if (pr > 0 && (pfd.revents & POLLIN)) { 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); unlink(sock); return 0; } /* ---------------- CLI(IPC 薄壳) ---------------- */ static int ipc_connect(void) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return -1; struct timeval tv = { 2, 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, g_sock, sizeof a.sun_path - 1); if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; } return fd; } /* 收发一轮:req(可有可无)+ resp type/code + payload 文本(json/text)。返回 0=OK。 */ static int ipc_xchg(int fd, uint16_t req_type, const void *req, uint32_t req_len, uint16_t *resp_type, uint32_t *resp_code, char *out, size_t outcap) { if (send_frame(fd, req_type, 1, req, req_len) < 0) return -1; uint8_t hdr[MEOW_IPC_HDR_LEN]; if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1; uint32_t rl; uint16_t rt; memcpy(&rt, hdr + 6, 2); memcpy(&rl, hdr + 12, 4); if (resp_type) *resp_type = rt; if (rl > MEOW_IPC_MAX) return -1; uint8_t pl[MEOW_IPC_MAX]; if (rl && x_read_full(fd, pl, rl) < 0) return -1; if (rl >= 8) { uint32_t code; uint16_t jl; memcpy(&code, pl, 4); memcpy(&jl, pl + 4, 2); if (resp_code) *resp_code = code; if (out && outcap) { if (jl >= outcap) jl = (uint16_t)(outcap - 1); memcpy(out, pl + 8, jl); out[jl] = 0; } return 0; } return -1; } static int cli_ipc_json(uint16_t req_type, const void *req, uint32_t req_len, char *json, size_t cap) { int fd = ipc_connect(); if (fd < 0) return -1; uint16_t rt; uint32_t code; int rc = ipc_xchg(fd, req_type, req, req_len, &rt, &code, json, cap); close(fd); return rc == 0 ? 0 : -1; } static int cli_ipc_cmd(uint16_t req_type, const char *cmd, const char *arg, char *out, size_t cap) { m_net_cmd_t q; memset(&q, 0, sizeof q); snprintf(q.cmd, sizeof q.cmd, "%s", cmd); if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg); int fd = ipc_connect(); if (fd < 0) { snprintf(out, cap, "meow_netmgrd 守护不可达 (%s)", g_sock); return -1; } uint16_t rt; uint32_t code; int rc = ipc_xchg(fd, req_type, &q, sizeof q, &rt, &code, out, cap); close(fd); if (rc != 0) { snprintf(out, cap, "IPC 往返失败"); return -1; } return (int)code; } static int cli_cmd_result(uint16_t req_type, const char *cmd, const char *arg) { char out[MEOW_IPC_MAX - 8]; int code = cli_ipc_cmd(req_type, cmd, arg, out, sizeof out); printf("%s\n", out); return code == MEOW_OK ? 0 : 1; } /* ---- 简版 JSON 字段提取(自产 JSON 固定形状,不做通用解析;同 powermgr) ---- */ static const char *jfind(const char *js, const char *key) { char pat[48]; snprintf(pat, sizeof pat, "\"%s\":", key); const char *p = strstr(js, pat); return p ? p + strlen(pat) : NULL; } static int jint(const char *js, const char *key, int *out) { const char *p = jfind(js, key); if (!p || strncmp(p, "null", 4) == 0) { *out = -1; return 1; } *out = atoi(p); return 0; } static void jstr(const char *js, const char *key, char *out, size_t cap) { const char *p = jfind(js, key); if (!p || strncmp(p, "null", 4) == 0 || *p != '"') { out[0] = 0; return; } p++; size_t n = 0; while (p[n] && p[n] != '"' && n < cap - 1) { out[n] = p[n]; n++; } out[n] = 0; } /* 定位 `"key":{` 的子对象并切到其闭合 `}`(扁平对象)。返回切片进 out。 */ static int slice_obj(const char *js, const char *key, char *out, size_t cap) { char pat[64]; snprintf(pat, sizeof pat, "\"%s\":", key); const char *p = strstr(js, pat); if (!p) { out[0] = 0; return -1; } const char *q = strchr(p, '}'); if (!q) { out[0] = 0; return -1; } size_t n = (size_t)(q - p + 1); if (n >= cap) n = cap - 1; memcpy(out, p, n); out[n] = 0; return 0; } /* 在 json 中迭代 `"":"` 开头的扁平条目,取每个对象切片(到 `}`)。首调 *ctx=NULL。 */ static int next_obj(const char *json, const char *key, const char **ctx, char *out, size_t cap) { char pat[64]; snprintf(pat, sizeof pat, "\"%s\":\"", key); const char *p = *ctx ? *ctx : json; const char *q = strstr(p, pat); if (!q) { *ctx = NULL; return -1; } q += strlen(pat); const char *e = strchr(q, '}'); if (!e) { *ctx = NULL; return -1; } size_t n = (size_t)(e - q + 1); if (n >= cap) n = cap - 1; memcpy(out, q, n); out[n] = 0; *ctx = e + 1; return 0; } static int cli_status(void) { char js[MEOW_IPC_MAX]; if (cli_ipc_json(MSG_NET_STATUS_REQ, NULL, 0, js, sizeof js) != 0) { fprintf(stderr, "meow_net status: 守护不可达(%s)\n", g_sock); return 1; } char mode[16]; jstr(js, "mode", mode, sizeof mode); printf("mode=%s src=daemon\n", mode[0] ? mode : "?"); /* ifaces 数组 */ const char *ctx = NULL; for (;;) { char tmp[256]; if (next_obj(js, "name", &ctx, tmp, sizeof tmp) != 0) break; char name[32]; int i = 0; while (tmp[i] && tmp[i] != '"' && i < 31) { name[i] = tmp[i]; i++; } name[i] = 0; char kind[12] = "", ip[40] = "", gw[40] = ""; int up = strstr(tmp, "\"up\":true") != NULL; jstr(tmp, "kind", kind, sizeof kind); jstr(tmp, "ip", ip, sizeof ip); jstr(tmp, "gw", gw, sizeof gw); printf("iface:%s kind=%s up=%s ip=%s gw=%s\n", name, kind[0] ? kind : "?", up ? "true" : "false", ip[0] ? ip : "null", gw[0] ? gw : "null"); } /* wifi */ { char obj[256]; slice_obj(js, "wifi", obj, sizeof obj); char ssid[64], wstate[16], wip[40]; int wc = strstr(obj, "\"connected\":true") != NULL; jstr(obj, "state", wstate, sizeof wstate); jstr(obj, "ssid", ssid, sizeof ssid); jstr(obj, "ip", wip, sizeof wip); int rssi = -1; jint(obj, "rssi", &rssi); printf("wifi connected=%d state=%s ssid=%s ip=%s rssi=%d\n", wc, wstate[0] ? wstate : "unknown", ssid[0] ? ssid : "-", wip[0] ? wip : "-", rssi); } /* bt */ { char obj[192]; slice_obj(js, "bt", obj, sizeof obj); char baddr[32]; int bavail = strstr(obj, "\"available\":true") != NULL; int bpow = strstr(obj, "\"powered\":true") != NULL; jstr(obj, "address", baddr, sizeof baddr); int bpaired = -1; jint(obj, "paired", &bpaired); printf("bt available=%d powered=%d address=%s paired=%d\n", bavail, bpow, baddr[0] ? baddr : "-", bpaired); } /* rfkill 数组 */ { const char *rf = strstr(js, "\"rfkill\":["); if (rf) { const char *ctx2 = NULL; for (;;) { char tmp[96]; if (next_obj(rf, "type", &ctx2, tmp, sizeof tmp) != 0) break; char type[16]; int i = 0; while (tmp[i] && tmp[i] != '"' && i < 15) { type[i] = tmp[i]; i++; } type[i] = 0; int soft = -1, hard = -1; jint(tmp, "soft", &soft); jint(tmp, "hard", &hard); printf("rfkill:%s soft=%d hard=%d\n", type, soft, hard); } } } return 0; } static int cli_iface(const char *name) { char js[MEOW_IPC_MAX]; m_net_iface_req_t q; memset(&q, 0, sizeof q); if (name) snprintf(q.name, sizeof q.name, "%s", name); if (cli_ipc_json(MSG_NET_IFACE_REQ, &q, sizeof q, js, sizeof js) != 0) { fprintf(stderr, "meow_net iface: 守护不可达(%s)\n", g_sock); return 1; } printf("%s\n", js); 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; } /* 期3 policy:show/set net/domain/del(IPC 0xEA;写级在服务端鉴权系统身份)。 */ static int cli_policy(int op, const char *entity, const char *arg) { m_net_policy_t q; memset(&q, 0, sizeof q); q.op = (uint32_t)op; if (entity) snprintf(q.entity, sizeof q.entity, "%s", entity); if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg); char out[MEOW_IPC_MAX - 8]; int fd = ipc_connect(); if (fd < 0) { fprintf(stderr, "meow_net 守护不可达(%s)\n", g_sock); return 1; } uint16_t rt; uint32_t code; int rc = ipc_xchg(fd, MSG_NET_POLICY, &q, sizeof q, &rt, &code, out, sizeof out); close(fd); if (rc != 0) { fprintf(stderr, "meow_net policy: IPC 往返失败\n"); return 1; } printf("%s", out); return code == MEOW_OK ? 0 : 1; } static const char *usage(void) { return "status | iface [name] | wifi scan|networks|connect [,psk]|disconnect|on|off" " | bt status|on|off|paired | rfkill [ ] | udhcpc renew|release [if]" " | apps | conns [entity] | top | dns status|stats|flush" " | policy show [entity] | policy set allow|block | policy domain +host|-host | policy del " " | 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、dns flush)仅系统身份,且 HibyOS 模式只读。\n"); return 0; } 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(); 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, "policy")) { if (!arg) { fprintf(stderr, "policy 子命令缺失(show [entity] | set allow|block | domain +host|-host | del )\n"); return 1; } if (!strcmp(arg, "show")) return cli_policy(NET_POLICY_SHOW, arg2, NULL); if (!strcmp(arg, "set")) { if (!arg2 || !arg3) { fprintf(stderr, "policy set 需 allow|block\n"); return 1; } return cli_policy(NET_POLICY_SET_NET, arg2, arg3); } if (!strcmp(arg, "domain")) { if (!arg2 || !arg3) { fprintf(stderr, "policy domain 需 +host|-host\n"); return 1; } return cli_policy(NET_POLICY_SET_DOMAIN, arg2, arg3); } if (!strcmp(arg, "del")) { if (!arg2) { fprintf(stderr, "policy del 需 \n"); return 1; } return cli_policy(NET_POLICY_DEL, arg2, NULL); } fprintf(stderr, "policy 子命令未知(%s)\n", arg); return 1; } if (!strcmp(cmd, "wifi")) { if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; } if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL); if (!strcmp(arg, "connect")) { if (!arg2) { fprintf(stderr, "connect 需 [,psk]\n"); return 1; } return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", arg2); } if (!strcmp(arg, "forget")) { if (!arg2) { fprintf(stderr, "forget 需 \n"); return 1; } return cli_cmd_result(MSG_NET_WIFI_CTL, "forget", arg2); } return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL); } if (!strcmp(cmd, "bt")) { if (!arg) { fprintf(stderr, "bt 子命令缺失(%s)\n", usage()); return 1; } return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL); } if (!strcmp(cmd, "rfkill")) { if (!arg) return cli_status(); /* 读 rfkill 表 → status JSON 的 rfkill[] */ if (!arg2) { fprintf(stderr, "rfkill 需 \n"); return 1; } char combined[64]; snprintf(combined, sizeof combined, "%s %s", arg, arg2); return cli_cmd_result(MSG_NET_WIFI_CTL, "rfkill", combined); } if (!strcmp(cmd, "udhcpc")) { if (!arg) { fprintf(stderr, "udhcpc 需 renew|release [if]\n"); return 1; } char combined[64]; snprintf(combined, sizeof combined, "%s %s", arg, arg2 ? arg2 : ""); return cli_cmd_result(MSG_NET_WIFI_CTL, "udhcpc", combined); } fprintf(stderr, "usage: meow_net <%s>\n", usage()); return 1; } /* ---------------- main:单一二进制双形态 ---------------- */ int main(int argc, char **argv) { const char *e; if ((e = getenv("MEOW_NET_DIR")) && e[0]) snprintf(g_net_dir, sizeof g_net_dir, "%s", e); if ((e = getenv("MEOW_NETDEV")) && e[0]) snprintf(g_netdev, sizeof g_netdev, "%s", e); if ((e = getenv("MEOW_NET_ROUTE")) && e[0]) snprintf(g_net_route, sizeof g_net_route, "%s", e); if ((e = getenv("MEOW_FIBTRIE")) && e[0]) snprintf(g_fibtrie, sizeof g_fibtrie, "%s", e); if ((e = getenv("MEOW_RESOLV")) && e[0]) snprintf(g_resolv, sizeof g_resolv, "%s", e); if ((e = getenv("MEOW_RFKILL_DIR")) && e[0]) snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s", e); if ((e = getenv("MEOW_BT_DIR")) && e[0]) snprintf(g_bt_dir, sizeof g_bt_dir, "%s", e); if ((e = getenv("MEOW_BOOT_MODE")) && e[0]) snprintf(g_boot_mode, sizeof g_boot_mode, "%s", e); if ((e = getenv("MEOW_LOG_DIR")) && e[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", e); if ((e = getenv("MEOW_BOOTTS_CACHE")) && e[0]) snprintf(g_bootts_cache, sizeof g_bootts_cache, "%s", e); if ((e = getenv("MEOW_WPA_CLI")) && e[0]) g_wpa_cli = e; if ((e = getenv("MEOW_HCICONFIG")) && e[0]) g_hciconfig = e; if ((e = getenv("MEOW_BLUETOOTHCTL")) && e[0]) g_bluetoothctl = e; if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e; 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++) { if (!strcmp(argv[i], "--sock") && i + 1 < argc) g_sock = argv[++i]; else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { printf("meow_netmgrd / meow_net (%s)\n", usage()); return 0; } else break; } /* argv[0] basename=meow_net → 强制 CLI(双形态);meow_netmgrd 空参 → 守护常驻。 */ const char *base = strrchr(argv[0], '/'); base = base ? base + 1 : argv[0]; if (!strcmp(base, "meow_net")) { if (i >= argc) return cli_help(); return cli_run(argc - i, argv + i); } if (i >= argc) return run_daemon(g_sock); /* svcd 空参 → IPC 常驻 */ return cli_run(argc - i, argv + i); }