#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_hcitool = "hcitool"; /* MEOW_HCITOOL(内核 ACL 连接权威) */ static const char *g_bluealsa_cli= "bluealsa-cli"; /* MEOW_BLUEALSA_CLI(A2DP 传输详情) */ static const char *g_bt_scan_cache = "/tmp/meow_bt_scan.txt"; /* MEOW_BT_SCAN_CACHE(扫描缓存;launcher net.c 同路径) */ 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) */ /* WiFi 设置页 bug1(2026-09-15):connect 有界等待 wpa_state=COMPLETED 的上限(ms)。 * 普通 ~10s;隐藏网络(connecth,主动探测+关联更慢)~15s。env 可调(宿主桩测试缩短)。 */ #define WIFI_CONNECT_WAIT_MS 10000 /* MEOW_WIFI_CONNECT_WAIT_MS */ #define WIFI_CONNECTH_WAIT_MS 15000 /* MEOW_WIFI_CONNECTH_WAIT_MS */ /* 蓝牙写操作的**总预算**必须压在 launcher 侧 `netmgr_bt()` 的 IPC 超时(NM_TMO_DFL=1500ms, * netmgr_ipc.c,非本单 scope)以内:越线即被客户端判「不可达」→ 直连回退再下发一次 → * 刚写好的「未生效」文案被客户端丢掉,净效果退回「恒报成功」。 * 预算模型(每一层都受限,不是只在两次探测之间看 deadline): * 命令 ≤ BT_ACT_TMO_MS / BT_ACT_SHORT_MS * + 复查 ≤ BT_VERIFY_BUDGET_MS,且**每次探测的 run_cmd 也只用剩余预算** * + 收尾 reap(正常/超时路径都先 SIGKILL,不额外等 300ms) * 目标上界 ≈ 1150ms(MIPS 更慢的 fork/exec 留了余量),实测见 test_bt_ctl 的 `budget-*` 用例。 */ #define BT_ACT_TMO_MS 800 /* connect/pair 的 bluetoothctl 上限 */ #define BT_ACT_SHORT_MS 600 /* disconnect/unpair/on/off */ #define BT_VERIFY_BUDGET_MS 220 /* 写后真态复查总预算(含探测内部 run_cmd) */ #define BT_VERIFY_GAP_MS 90 /* 每轮间隔(ms) */ /* A2DP 查询是 1+N 个子进程(list-pcms + 每端点 info),而守护单线程、DNS 代理同一主循环, * 所以**每条命令与整段查询都设上限**;status JSON 另走 TTL 缓存(见 g_bt_a2dp_*)。 */ #define BT_A2DP_TEXT_BUDGET_MS 900 /* `bt a2dp` 命令总预算(客户端 1500ms 窗口内) */ #define BT_A2DP_STATUS_BUDGET_MS 500 /* status JSON 刷新缓存时的总预算 */ #define BT_A2DP_LIST_TMO_MS 400 /* list-pcms 单命令上限 */ #define BT_A2DP_INFO_TMO_MS 300 /* 每个端点 info 单命令上限 */ #define BT_A2DP_CACHE_SEC 10 /* status JSON 的 a2dp 摘要缓存 TTL(秒) */ /* 防堆积熔断(真机 0.8.49 事故:55 个挂死的 bluetoothctl 吃光进程槽/内存,launcherd 与 sshd * 同时失去 fork 能力)。BT 写操作**连续** NN 次超时后,冷却期内不再 fork 新命令,如实回 * 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */ #define BT_TMO_STREAK_MAX 3 #define BT_TMO_COOLDOWN_SEC 30 #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 自检周期 */ /* ---- 期 5 常量(公网流量统计:netlink sock_diag 按 TCP 连接累计字节;静态表,禁 malloc) ---- */ #define FLOW_LAST_MAX 128 /* inode→上拍累计 跟踪表(仅公网/内网 TCP 流) */ #define PUB_FLOW_MAX 16 /* 公网字节 top 流表(内存累加用) */ #define PUB_SERVE_ENT 6 /* status JSON 序列化的 top 实体数(4096 帧预算内) */ #define PUB_SERVE_FLOW 6 /* status JSON 序列化的 top 流数 */ #define TCP_INFO_MIN_LEN 136 /* tcp_info 载荷≥此值才含 tcpi_bytes_received(off128+8) */ #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_connect_wait_ms = WIFI_CONNECT_WAIT_MS; /* MEOW_WIFI_CONNECT_WAIT_MS */ static int g_connecth_wait_ms = WIFI_CONNECTH_WAIT_MS; /* MEOW_WIFI_CONNECTH_WAIT_MS */ 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); } #define RUNCMD_ERR_MAX 256 /* stderr 只留前 256B:失败文案要有料,但不能把 IPC 报文撑爆 */ /* run_cmd 四态(M49)。P1 之前 run_cmd 只返回「读到的字节数」,命令失败也返回 0, * 调用方 `n >= 0 → MEOW_OK`,于是 on/off/connect/disconnect/pair 全部恒报成功。 */ typedef enum { RUNCMD_OK = 0, /* 退出 0,有输出 */ RUNCMD_EMPTY = 1, /* 退出 0,无输出(成功但没内容,≠ 失败) */ RUNCMD_EXIT = 2, /* 非 0 退出码(127 = 命令不存在 / execl 失败) */ RUNCMD_TIMEOUT = 3, /* 限时内没收完或子进程不退出(已 SIGKILL) */ RUNCMD_EXEC = 4, /* 本进程 fork/pipe 失败,或子进程被信号杀死(非正常退出) */ } runcmd_status_t; typedef struct { int status; /* runcmd_status_t */ int bytes; /* stdout 字节数(不含尾部 NUL;截断时 = cap-1) */ int exit_code; /* WEXITSTATUS;未正常退出 = -1 */ int signal; /* 被信号杀死时的信号号,否则 0 */ int truncated; /* stdout 超出 buf 容量:已排空但丢弃尾部 */ char err[RUNCMD_ERR_MAX]; /* stderr 前 256B(失败时进错误文案) */ } runcmd_res_t; static long mono_ms(void) { struct timespec tp; clock_gettime(CLOCK_MONOTONIC, &tp); return tp.tv_sec * 1000L + tp.tv_nsec / 1000000L; } /* ---------------- 有界 popen 收割(257 教训:命令 wedge 时无条件 waitpid 会拖死调用线程) ---------------- * 非阻塞读子进程 stdout/stderr(poll 限时);读完后 WNOHANG 优雅等 300ms,不死 SIGKILL 再等 300ms; * 仍不死(D 态)放弃收割(泄漏一子进程换线程不卡死,已记日志)。 * 返回 1=已收割(*st 有值),0=放弃收割。 * cmd 为完整 shell 行;buf 由调用方提供(静态/栈),不 malloc——热路径禁 malloc。 */ static int reap_bounded(pid_t pid, const char *cmd, int *st) { *st = 0; for (int i = 0; i < 30; i++) { if (waitpid(pid, st, WNOHANG) == pid) return 1; 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, st, WNOHANG) == pid) return 1; 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); return 0; } /* 带四态结果的执行器(res 可为 NULL,等价于旧 run_cmd)。返回 stdout 字节数(-1=起不来)。 * stdout 超容量时继续排空(丢弃),保证子进程不被 EPIPE/SIGPIPE 提前打断、退出码可信。 */ static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_res_t *res) { runcmd_res_t lr; runcmd_res_t *r = res ? res : &lr; memset(r, 0, sizeof *r); r->exit_code = -1; r->status = RUNCMD_EXEC; if (cap > 0) buf[0] = 0; int op[2], ep[2]; if (pipe(op) < 0) return -1; if (pipe(ep) < 0) { close(op[0]); close(op[1]); return -1; } pid_t pid = fork(); if (pid < 0) { close(op[0]); close(op[1]); close(ep[0]); close(ep[1]); return -1; } if (pid == 0) { dup2(op[1], 1); dup2(ep[1], 2); /* M49:stderr 不再丢 /dev/null,失败时留一段进错误文案 */ close(op[0]); close(op[1]); close(ep[0]); close(ep[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); } close(op[1]); close(ep[1]); fcntl(op[0], F_SETFL, fcntl(op[0], F_GETFL, 0) | O_NONBLOCK); fcntl(ep[0], F_SETFL, fcntl(ep[0], F_GETFL, 0) | O_NONBLOCK); int off = 0, eoff = 0, trunc = 0, out_eof = 0, err_eof = 0, timed_out = 0; long deadline = mono_ms() + (tmo_ms > 0 ? tmo_ms : 1); char stray[256]; while (!(out_eof && err_eof)) { long remain = deadline - mono_ms(); if (remain <= 0) { timed_out = 1; break; } struct pollfd pf[2]; int nf = 0; if (!out_eof) { pf[nf].fd = op[0]; pf[nf].events = POLLIN; pf[nf].revents = 0; nf++; } if (!err_eof) { pf[nf].fd = ep[0]; pf[nf].events = POLLIN; pf[nf].revents = 0; nf++; } int pr = poll(pf, (nfds_t)nf, (int)remain); if (pr < 0) { if (errno == EINTR) continue; timed_out = 1; break; } if (pr == 0) { timed_out = 1; break; } for (int i = 0; i < nf; i++) { if (!(pf[i].revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL))) continue; int is_out = (pf[i].fd == op[0]); char *dst = is_out ? buf : r->err; int dcap = is_out ? (cap > 0 ? cap : 1) : RUNCMD_ERR_MAX; int *doff = is_out ? &off : &eoff; int into = 1; char *p = dst + *doff; int room = dcap - 1 - *doff; if (room <= 0) { p = stray; room = (int)sizeof stray; into = 0; } ssize_t rd = read(pf[i].fd, p, (size_t)room); if (rd == 0) { if (is_out) out_eof = 1; else err_eof = 1; continue; } if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; if (is_out) out_eof = 1; else err_eof = 1; continue; } if (into) *doff += (int)rd; else if (is_out) trunc = 1; } } close(op[0]); close(ep[0]); if (cap > 0) buf[off] = 0; r->err[eoff] = 0; if (timed_out) kill(pid, SIGKILL); /* 已经超时:不再多等 300ms */ int st = 0; int reaped = reap_bounded(pid, cmd, &st); r->bytes = off; r->truncated = trunc; if (reaped) { if (WIFEXITED(st)) r->exit_code = WEXITSTATUS(st); else if (WIFSIGNALED(st)) r->signal = WTERMSIG(st); } if (timed_out || !reaped) r->status = RUNCMD_TIMEOUT; else if (r->signal || r->exit_code < 0) r->status = RUNCMD_EXEC; else if (r->exit_code != 0) r->status = RUNCMD_EXIT; else if (off > 0) r->status = RUNCMD_OK; else r->status = RUNCMD_EMPTY; return off; } /* 兼容外壳:返回读到的字节数(-1=起不来),语义与 P1 之前一致(调用点无需改动)。 */ static int run_cmd(char *buf, int cap, int tmo_ms, const char *cmd) { return run_cmd_ex(buf, cap, tmo_ms, cmd, NULL); } /* 失败时的一句话原因(有界;stderr 有料就带上,最多 120B,避免报文过长)。 */ static const char *run_cmd_why(const runcmd_res_t *r, int *is_fail) { static char why[160]; why[0] = 0; if (is_fail) *is_fail = 0; switch (r->status) { case RUNCMD_OK: case RUNCMD_EMPTY: return why; case RUNCMD_TIMEOUT: snprintf(why, sizeof why, "超时"); break; case RUNCMD_EXIT: if (r->exit_code == 127) snprintf(why, sizeof why, "命令不存在(exit 127)"); else snprintf(why, sizeof why, "exit %d", r->exit_code); break; case RUNCMD_EXEC: if (r->signal) snprintf(why, sizeof why, "被信号 %d 终止", r->signal); else snprintf(why, sizeof why, "无法执行"); break; default: snprintf(why, sizeof why, "未知失败"); break; } if (is_fail) *is_fail = 1; if (r->err[0]) { char one[121]; int j = 0; for (int i = 0; r->err[i] && j < (int)sizeof one - 1; i++) { unsigned char c = (unsigned char)r->err[i]; if (c == '\n' || c == '\r' || c == '\t') { if (j && one[j-1] != ' ') one[j++] = ' '; } else if (c >= 0x20) one[j++] = (char)c; } one[j] = 0; if (one[0]) { size_t l = strlen(why); snprintf(why + l, sizeof why - l, ": %s", one); } } return why; } /* ---------------- 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 */ int connected_n; /* 已连接(内核 ACL 权威)计数;-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)。仅取「非回环且 up」接口求和——本设备即 wlan0;去掉 lo 对 * 实时速率的虚高污染。依赖调用方先 enum_ifaces(0) 刷新 g_ifaces[] 再取数(daemon_tick 节拍内如此)。 * 无任何 up 非回环接口 → rx 置 -1(速率未知/无连接,不谎报 0)。 */ static void uplink_totals(long *rx, long *tx) { long trx = 0, ttx = 0, seen = 0; for (int i = 0; i < g_iface_n; i++) { if (iface_parse_kind(g_ifaces[i].name) == 0) continue; /* lo */ if (!g_ifaces[i].up) continue; /* 只取 up */ trx += g_ifaces[i].rx; ttx += g_ifaces[i].tx; seen = 1; } if (rx) *rx = seen ? trx : -1; if (tx) *tx = seen ? ttx : 0; } /* 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 包装 ---------------- */ /* hci0 真实 power 态:1=up 0=down -1=未知。sysfs `/sys/class/bluetooth/hci0/power` 优先, * 回退 `hciconfig hci0` 行首 UP/DOWN(P1 之后从 bt_status 抽出,写操作复查也用它)。 * tmo_ms 只管回退那次 run_cmd:写后复查要能把它压进总预算,读路径传 RUNCMD_TMO_MS。 */ static int bt_powered_now(int tmo_ms) { char p[192]; snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir); char pw[8] = ""; if (read_file_txt(p, pw, sizeof pw) > 0 && pw[0]) { if (!strcmp(pw, "on")) return 1; if (!strcmp(pw, "off")) return 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, tmo_ms, cmd) <= 0) return -1; 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)) return 1; if (!strncmp(q, "DOWN", 4)) return 0; if (!nl) break; ln = nl + 1; } return -1; } static int bt_acl_macs(char macs[][24], int max, int tmo_ms); /* 前向声明(bt_status 早于其定义) */ static void bt_status(bt_t *b) { memset(b, 0, sizeof *b); b->paired_n = -1; b->connected_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; { int pw = bt_powered_now(RUNCMD_TMO_MS); b->powered = pw > 0 ? 1 : 0; } /* 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-devices`(BlueZ 5.54 实测没有 `paired` 子命令, * 旧实现 `paired` → "Invalid command in menu main"。`paired-devices` 才是已配对全量; * `devices` 含未配对的历史缓存,只用于补名,不能当配对权威)。失败/空 → -1。 */ char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired-devices 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; } /* 已连接数:内核 ACL(hcitool con)权威;快速,不受 bluetoothctl 过滤/慢启动影响。 */ { char macs[8][24]; b->connected_n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS); } } /* ================================================================ * P1:蓝牙权威收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八 P1)。 * 权威路径设计(修复「已连接设备显示不全」根因): * - 已连接 = 内核 ACL(hcitool con,快速/权威/不受 bluetoothctl 过滤影响), * 名/配对来自 bluetoothctl `paired-devices`/`devices`(蓝虫子缓存)——不再依赖 * bluetoothctl `devices Connected`(慢 + 只反映 agent 缓存视图,易截断)。 * - A2DP 详情 = bluealsa-cli `list-pcms` → 每端点 `info `(v4.1.1 没有 `a2dp list`)。 * - 扫描 = netmgrd 统一生命周期(scan on 才扫、off/超时收尾;缓存 /tmp/meow_bt_scan.txt)。 * 输出均为文本(m_net_cmd_result_t.text),launcher 只做客户端解析,不再 bluetoothctl popen。 * * M49(2026-11-11)修掉 P1 遗留的三个「部署新件后依然存在」的真 bug: * ① `bluetoothctl paired` 子命令不存在(BlueZ 5.54 → `paired-devices`); * ② `bluealsa-cli a2dp list` 子命令不存在(v4.1.1 → `list-pcms` + `info`); * ③ run_cmd 只回字节数、丢 stderr → 写操作恒报成功(改为四态 + 写后真态复查)。 * 另修:扫描缓存的实时 `[NEW] Device` 行以 ANSI ESC 开头,旧解析器只认行首 → 实时行全丢。 * ================================================================ */ static int bt_hex(int c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static int bt_mac_eq(const char *a, const char *b) { for (int i = 0; i < 17; i++) { if (bt_hex((unsigned char)a[i]) != bt_hex((unsigned char)b[i])) return 0; if (i % 3 == 2 && a[i] != b[i]) return 0; } return 1; } /* ---------------- 纯解析函数(宿主单测直接调用;不做 IO) ---------------- * 真机原始字节(`od -c /tmp/meow_bt_scan.txt`,2026-11-11 15:46): * ESC [ 0 ; 9 4 m [bluetooth] ESC [ 0 m # \r ESC [ K [001 ESC [ 0 ; 9 2 m 002 N E W * 001 ESC [ 0 m 002 ] D e v i c e 7 9 : 8 5 : ... 7 9 - 8 5 - ... * 即:每行前缀是彩色提示符 + `\r\033[K`,事件标签 `[NEW]` 外层还套了 \001/\002 与控制序列, * 而 `Device ` 根本不在行首 —— 只认行首的旧解析器把这些实时行全丢了。 */ /* 剥掉 ANSI 转义序列与终端控制字符,写 out(NUL 结尾;超长截断)。返回 out。 * 处理:CSI(ESC [ ... 终止字节 0x40..0x7E,含 ESC[K / ESC[P / 颜色码)、其余 ESC+x 单字节、 * \r \t 等 C0 控制符、DEL;>=0x80 的字节原样保留(UTF-8 设备名)。 */ static const char *bt_clean_ansi(const char *in, char *out, size_t cap) { size_t o = 0; if (!out || cap == 0) return out; for (const char *p = in; p && *p; p++) { unsigned char c = (unsigned char)*p; if (c == 0x1b) { /* ESC */ if (p[1] == '[') { /* CSI:跳到终止字节 */ p += 2; while (*p && !((unsigned char)*p >= 0x40 && (unsigned char)*p <= 0x7e)) p++; if (!*p) break; continue; } if (p[1]) p++; /* ESC + 单字节(ESC 7 / ESC ( 等) */ continue; } if (c < 0x20 || c == 0x7f) continue; /* \r \t \001 \002 等一律丢弃 */ if (o + 1 < cap) out[o++] = (char)c; } out[o] = 0; return out; } /* 解析 bluetoothctl 输出的一行 → "Device "。 * 输入 raw 可以是任意原始行(含 ANSI/提示符/标签);成功返 1 并写 mac[≥18]/name。 * 只认 `[NEW] Device `(发现事件)与裸 `Device `(`devices` 列表块); * `[CHG]`/`[DEL]` 是属性变更/离开,不是「附近有这台」,返回 0。 * name 允许为空(截断输出),MAC 不完整(行被截断)返回 0。 */ static int bt_scan_line_parse(const char *raw, char *mac, size_t mcap, char *name, size_t ncap) { if (mac && mcap) mac[0] = 0; if (name && ncap) name[0] = 0; if (!raw) return 0; char clean[512]; bt_clean_ansi(raw, clean, sizeof clean); const char *p = clean; /* 1) 提示符 "[bluetooth]# " 前缀(事件在它后面) */ for (;;) { const char *pr = strstr(p, "[bluetooth]"); if (!pr) break; const char *q = pr + 11; while (*q == ' ' || *q == '\t') q++; if (*q != '#') break; q++; while (*q == ' ' || *q == '\t') q++; p = q; } /* 2) 事件标签:取行内最早出现的那个;只有 NEW 算「发现」 */ size_t tag_off = 0; int tag_kind = -1; static const char *const kinds[3] = { "[NEW]", "[CHG]", "[DEL]" }; for (int i = 0; i < 3; i++) { const char *t = strstr(p, kinds[i]); if (t && (tag_kind < 0 || (size_t)(t - p) < tag_off)) { tag_off = (size_t)(t - p); tag_kind = i; } } if (tag_kind > 0) return 0; if (tag_kind == 0) p += tag_off + 5; while (*p == ' ' || *p == '\t') p++; if (strncmp(p, "Device ", 7) != 0) return 0; p += 7; /* 3) MAC:严格 17 字符 XX:XX:XX:XX:XX:XX,后面必须是空白或行尾 */ for (int i = 0; i < 17; i++) { if (i % 3 == 2) { if (p[i] != ':') return 0; } else if (bt_hex((unsigned char)p[i]) < 0) return 0; } char sep = p[17]; if (sep && sep != ' ' && sep != '\t') return 0; if (mac && mcap > 17) { memcpy(mac, p, 17); mac[17] = 0; } p += 17; while (*p == ' ' || *p == '\t') p++; size_t l = strlen(p); while (l > 0 && (p[l - 1] == ' ' || p[l - 1] == '\t' || p[l - 1] == '\n' || p[l - 1] == '\r')) l--; if (l && name && ncap) { if (l > ncap - 1) l = ncap - 1; memcpy(name, p, l); name[l] = 0; } return 1; } /* 解析 `hcitool con` 输出 → ACL MAC 列表(返回数量 ≤max)。 * 行样本:"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER";只认 "< ACL" 行, * MAC 不完整(截断)跳过;表头 "Connections:" 自然不匹配。 */ static int bt_acl_parse(const char *raw, char macs[][24], int max) { int n = 0; if (!raw || max <= 0) return 0; const char *line = raw; while (line && *line && n < max) { const char *nl = strchr(line, '\n'); const char *e = nl ? nl : line + strlen(line); const char *acl = NULL; for (const char *q = line; q + 6 <= e; q++) { if (!strncmp(q, "< ACL ", 6)) { acl = q + 6; break; } } if (acl) { int ok = 1; for (int i = 0; i < 17 && ok; i++) { if (i % 3 == 2) { if (acl[i] != ':') ok = 0; } else if (bt_hex((unsigned char)acl[i]) < 0) ok = 0; } if (ok && acl + 17 <= e) { memcpy(macs[n], acl, 17); macs[n][17] = 0; n++; } } line = nl ? nl + 1 : NULL; } return n; } /* bluealsa D-Bus 路径里的设备 MAC:/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc * → 33:DE:2E:21:AD:30。成功返 1(mac 至少 18B)。 */ static int bt_path_mac(const char *path, char *mac, size_t mcap) { if (!mac || mcap < 18) return 0; mac[0] = 0; const char *d = path ? strstr(path, "dev_") : NULL; if (!d) return 0; d += 4; char out[18]; int j = 0; for (int g = 0; g < 6; g++) { for (int k = 0; k < 2; k++) { if (bt_hex((unsigned char)*d) < 0) return 0; out[j++] = *d++; } if (g < 5) { if (*d != '_') return 0; d++; out[j++] = ':'; } } out[17] = 0; memcpy(mac, out, 18); return 1; } /* 在 [s, e) 内找子串 needle(不跨行匹配;行内 strstr 会越过 '\n' 命中下一行)。 * 返回 1=命中。纯函数。 */ static int bt_mem_has(const char *s, const char *e, const char *needle) { size_t nl = strlen(needle); if (!nl || e <= s) return 0; for (const char *q = s; q + nl <= e; q++) { if (!memcmp(q, needle, nl)) return 1; } return 0; } /* 解析 `bluealsa-cli list-pcms` → A2DP 端点路径(每行一个 /org/bluealsa/... 路径)。 * 只收 a2dpsrc/a2dpsnk(SCO/HFP 端点不算 A2DP);行尾空白/CR 去掉。返回数量(≤max)。 */ static int bt_pcm_paths_parse(const char *raw, char paths[][80], int max) { int n = 0; if (!raw || max <= 0) return 0; const char *line = raw; while (line && *line && n < max) { const char *nl = strchr(line, '\n'); const char *e = nl ? nl : line + strlen(line); const char *p = line; while (p < e && (*p == ' ' || *p == '\t')) p++; while (e > p && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r')) e--; if (e - p > 14 && (bt_mem_has(p, e, "/a2dpsrc") || bt_mem_has(p, e, "/a2dpsnk"))) { size_t l = (size_t)(e - p); if (l > 79) l = 79; memcpy(paths[n], p, l); paths[n][l] = 0; n++; } line = nl ? nl + 1 : NULL; } return n; } typedef struct { char codec[16], rate[16], channels[8], bits[8], state[24]; int running; /* 1=在跑 0=停 -1=未知 */ int has; /* 至少命中一个字段 → 这份 info 输出可用 */ } bt_pcm_info_t; /* 取 "Key: value" 里 Key 之后的值(到行尾,去首尾空白)。键必须整词匹配(避免 * CodecConfiguration 命中 Codec)。value 写 v(可空)。返回 1=命中。 */ static int bt_kv(const char *line, const char *key, char *v, size_t vcap) { size_t kl = strlen(key); if (v && vcap) v[0] = 0; if (strncmp(line, key, kl) != 0) return 0; const char *p = line + kl; /* 整词判定:键后面必须是分隔符或行尾(避免 "Codec" 命中 "CodecConfiguration") */ if (*p && *p != ':' && *p != '=' && *p != ' ' && *p != '\t') return 0; while (*p == ' ' || *p == '\t') p++; if (*p == ':' || *p == '=') { p++; while (*p == ' ' || *p == '\t') p++; } if (v && vcap) { size_t l = strlen(p); while (l > 0 && (p[l - 1] == ' ' || p[l - 1] == '\t' || p[l - 1] == '\r' || p[l - 1] == '\n')) l--; if (l > vcap - 1) l = vcap - 1; memcpy(v, p, l); v[l] = 0; } return 1; } /* 解析 `bluealsa-cli info ` 输出。字段名按 BlueALSA PCM 属性(Codec / Sampling / * Channels / Bits per Sample / State / Running / Transport);同时兼容 `Format:` 一行里 * 打包的 " Hz, channels, bits"。纯函数。 */ static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o) { memset(o, 0, sizeof *o); o->running = -1; if (!raw) return; char fmt[64] = ""; const char *line = raw; while (line && *line) { const char *nl = strchr(line, '\n'); const char *e = nl ? nl : line + strlen(line); char lbuf[256]; size_t l = (size_t)(e - line); if (l > sizeof lbuf - 1) l = sizeof lbuf - 1; memcpy(lbuf, line, l); lbuf[l] = 0; char clean[256]; bt_clean_ansi(lbuf, clean, sizeof clean); char *p = clean; while (*p == ' ' || *p == '\t') p++; char v[64]; /* 真机 v4.1.1 的键名(M50 原文):Selected codec / Available codecs / Format: S16_LE / * Running: true|false / Channels / Sampling / Transport / Mode / Delay / Volume / Muted。 * 注意 Available codecs 是空格分隔的**列表**,绝不能当 codec;Selected codec 才是当前值。 */ if (!o->codec[0] && (bt_kv(p, "Selected codec", v, sizeof v) || bt_kv(p, "Codec", v, sizeof v)) && v[0]) { snprintf(o->codec, sizeof o->codec, "%s", v); o->has = 1; } else if (!o->rate[0] && bt_kv(p, "Sampling", v, sizeof v) && v[0]) { snprintf(o->rate, sizeof o->rate, "%s", v); o->has = 1; } else if (!o->channels[0] && bt_kv(p, "Channels", v, sizeof v) && v[0]) { snprintf(o->channels, sizeof o->channels, "%s", v); o->has = 1; } else if (!o->bits[0] && (bt_kv(p, "Bits per Sample", v, sizeof v) || bt_kv(p, "Bits", v, sizeof v) || bt_kv(p, "Bit depth", v, sizeof v)) && v[0]) { snprintf(o->bits, sizeof o->bits, "%s", v); o->has = 1; } else if (bt_kv(p, "Format", v, sizeof v) && v[0]) { snprintf(fmt, sizeof fmt, "%s", v); o->has = 1; } else if (bt_kv(p, "Mode", v, sizeof v) && v[0]) { o->has = 1; /* sink/source:本机侧 PCM 模式(不是 codec/不是 MAC) */ } else if (bt_kv(p, "State", v, sizeof v) && v[0]) { snprintf(o->state, sizeof o->state, "%s", v); o->has = 1; if (strstr(v, "RUNNING") || strstr(v, "ACTIVE") || strstr(v, "PLAYING")) o->running = 1; else if (strstr(v, "IDLE") || strstr(v, "STOP") || strstr(v, "SETUP")) o->running = 0; } else if (bt_kv(p, "Running", v, sizeof v) && v[0]) { o->has = 1; if (!strcmp(v, "true") || !strcmp(v, "yes")) o->running = 1; else if (!strcmp(v, "false") || !strcmp(v, "no")) o->running = 0; } else if (bt_kv(p, "Transport", v, sizeof v) && v[0]) { o->has = 1; } line = nl ? nl + 1 : NULL; } /* Format 兜底:真机是 ALSA 格式串(S16_LE / S24_3LE),也可能是 * "44100 Hz, 2 channels, 16 bits" 这种打包写法。 */ if (fmt[0]) { char *q; if (!o->bits[0] && (fmt[0] == 'S' || fmt[0] == 'U')) { char *n = fmt + 1; int bw = 0; while (*n >= '0' && *n <= '9') { bw = bw * 10 + (*n - '0'); n++; } if (bw > 0 && bw <= 32) snprintf(o->bits, sizeof o->bits, "%d", bw); } if (!o->rate[0] && (q = strstr(fmt, "Hz")) != NULL) { char *b = q; while (b > fmt && (b[-1] == ' ' || b[-1] == '\t')) b--; /* 跳过 "Hz" 前的空格 */ char *num = b; while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--; if (num < b && b - num <= 6) { size_t l = (size_t)(b - num); if (l > sizeof o->rate - 1) l = sizeof o->rate - 1; memcpy(o->rate, num, l); o->rate[l] = 0; } } if (!o->channels[0] && (q = strstr(fmt, "channels")) != NULL) { char *b = q; while (b > fmt && (b[-1] == ' ')) b--; char *num = b; while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--; if (num < b && b - num <= 3) { size_t l = (size_t)(b - num); memcpy(o->channels, num, l); o->channels[l] = 0; } } if (!o->bits[0] && (q = strstr(fmt, "bits")) != NULL) { char *b = q; while (b > fmt && (b[-1] == ' ')) b--; char *num = b; while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--; if (num < b && b - num <= 3) { size_t l = (size_t)(b - num); memcpy(o->bits, num, l); o->bits[l] = 0; } } } } /* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。 * tmo_ms 由调用方给:读路径用 RUNCMD_TMO_MS,写后复查只给剩余预算。 */ static int bt_acl_macs(char macs[][24], int max, int tmo_ms) { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool); char buf[1024]; if (run_cmd(buf, sizeof buf, tmo_ms, cmd) <= 0) return 0; return bt_acl_parse(buf, macs, max); } /* 写后复查专用三态:1=有 ACL,0=确实没有,-1=**查不到**(命令失败/超时/被 kill)。 * 必须和「没有」分开:探测超时被当成「没有连接」的话,disconnect 就会在查不到的情况下 * 报「已断开」——那正是本单要消灭的「假成功」。 */ static int bt_acl_has_tri(const char *mac, int tmo_ms) { char cmd[160]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool); char buf[1024]; runcmd_res_t rr; int n = run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr); if (n < 0 || rr.status == RUNCMD_TIMEOUT || rr.status == RUNCMD_EXEC || rr.status == RUNCMD_EXIT) return -1; char macs[8][24]; int cnt = bt_acl_parse(buf, macs, 8); for (int i = 0; i < cnt; i++) if (bt_mac_eq(macs[i], mac)) return 1; return 0; } /* `bluetoothctl info ` 的布尔字段真态(Paired / Connected / Trusted): * 1=yes 0=no -1=未知(设备不在 BlueZ 列表里 / 后端失败)。 */ static int bt_info_flag(const char *mac, const char *key, int tmo_ms) { char cmd[224]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s info %s 2>&1", g_bluetoothctl, mac); char buf[1024]; runcmd_res_t rr; if (run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr) <= 0) return -1; char keyp[32]; snprintf(keyp, sizeof keyp, "%s:", key); const char *p = strstr(buf, keyp); if (!p) return -1; p += strlen(keyp); while (*p == ' ' || *p == '\t') p++; if (!strncmp(p, "yes", 3)) return 1; if (!strncmp(p, "no", 2)) return 0; return -1; } /* 设备名解析(bluetoothctl `paired-devices`)+`devices` 两次兜底;命中返 1。 */ static int bt_name_for_mac(const char *mac, char *name, int nlen) { if (name && nlen > 0) name[0] = 0; static const char *subs[] = { "paired-devices", "devices" }; for (int s = 0; s < 2; s++) { char cmd[176]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", g_bluetoothctl, subs[s]); char buf[1024]; if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) continue; char *line = buf; while (line && *line) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; char m[24], nm[64]; if (bt_scan_line_parse(line, m, sizeof m, nm, sizeof nm) && bt_mac_eq(m, mac)) { if (nm[0]) { int l = (int)strlen(nm); if (l > nlen - 1) l = nlen - 1; memcpy(name, nm, (size_t)l); name[l] = 0; } else { memcpy(name, mac, 17); name[17] = 0; } return 1; } line = nl ? nl + 1 : NULL; } } return 0; } /* 已连接设备文本(权威=内核 ACL;名经 paired/devices 兜底)。行格式 "Device "。 */ static void bt_connected_text(char *buf, int cap) { int off = 0; char macs[8][24]; int n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS); for (int i = 0; i < n; i++) { char name[64] = ""; bt_name_for_mac(macs[i], name, sizeof name); int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", macs[i], name); if (l <= 0 || off + l >= cap) break; off += l; } buf[off] = 0; } /* A2DP 传输详情(M49 重写)。 * v4.1.1 的 bluealsa-cli **没有** `a2dp list` 子命令(旧实现恒失败 → 恒 "A2DP none")。 * 正确路径:`list-pcms` 拿端点路径 → 每个 a2dpsrc/a2dpsnk 端点 `info ` 取属性。 * 输出每端点一行: * "A2DP codec=<..> rate=<..> channels= bits= acquired=<0|1> role=" * 前提(真机实测,塔 2026-11-11):端点路径形如 `/org/bluealsa/hci0/dev_XX_../a2dpsrc/sink`, * 末端段是 PCM 模式而非 MAC,且**端点易失**——只有 client 打开 PCM 时 bluealsa 才暴露它, * 断开后 list-pcms 立刻空、info 报 Object path not found。 * 所以:本函数是「此刻快照」;没有端点 → "A2DP none"(= 当前无 A2DP 活动,不是错误)。 * 字段缺失写 "-"(info 输出认得但字段名对不上时也照样出行,至少让 UI 看见端点在);info 查询 * 失败(端点刚好消失)就跳过该端点;一个都没有 → "A2DP none"。 */ static void bt_a2dp_query(char *buf, int cap, int budget_ms) { if (cap <= 0) return; buf[0] = 0; long deadline = mono_ms() + (budget_ms > 0 ? budget_ms : 1); char cmd[176]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list-pcms 2>&1", g_bluealsa_cli); char raw[1024]; runcmd_res_t rr; long remain = deadline - mono_ms(); if (remain <= 0) { snprintf(buf, (size_t)cap, "A2DP none\n"); return; } run_cmd_ex(raw, sizeof raw, (int)(remain > BT_A2DP_LIST_TMO_MS ? BT_A2DP_LIST_TMO_MS : remain), cmd, &rr); char paths[4][80]; int np = bt_pcm_paths_parse(raw, paths, 4); int off = 0; for (int i = 0; i < np; i++) { remain = deadline - mono_ms(); if (remain <= 0) break; /* 总预算用完:剩下的端点不再查(宁缺不卡) */ char mac[24] = "-"; if (!bt_path_mac(paths[i], mac, sizeof mac)) continue; const char *role = strstr(paths[i], "a2dpsnk") ? "snk" : "src"; char cmd2[256]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s info %s 2>&1", g_bluealsa_cli, paths[i]); char ibuf[1024]; runcmd_res_t ir; run_cmd_ex(ibuf, sizeof ibuf, (int)(remain > BT_A2DP_INFO_TMO_MS ? BT_A2DP_INFO_TMO_MS : remain), cmd2, &ir); /* 端点消失/查询失败:跳过,不编造。EMPTY(exit 0 无输出)不算失败——端点确实在。 */ if (ir.status != RUNCMD_OK && ir.status != RUNCMD_EMPTY) continue; bt_pcm_info_t info; bt_pcm_info_parse(ibuf, &info); /* info 成功但字段名一个都没命中(版本差异)→ 仍出行,字段写 "-": * 「端点存在」本身是真信息,报 "A2DP none" 会把在用的蓝牙音频说成「没有」。 */ int l = snprintf(buf + off, (size_t)(cap - off), "A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n", mac, info.codec[0] ? info.codec : "-", info.rate[0] ? info.rate : "-", info.channels[0] ? info.channels : "-", info.bits[0] ? info.bits : "-", info.running > 0 ? 1 : 0, role); if (l <= 0 || off + l >= cap) break; off += l; } if (off == 0) snprintf(buf, (size_t)cap, "A2DP none\n"); else buf[off] = 0; } /* `bt a2dp` 命令入口:总预算 BT_A2DP_TEXT_BUDGET_MS(客户端 1500ms 窗口内)。 */ static void bt_a2dp_text(char *buf, int cap) { bt_a2dp_query(buf, cap, BT_A2DP_TEXT_BUDGET_MS); } /* status JSON 用的 a2dp 摘要缓存:TTL + 总预算,且**无连接时直接不查**(没 ACL 就没有 A2DP * 端点,一次 fork 都不必花)。守护单线程、DNS 代理同一主循环,status 是高频路径。 */ static long g_bt_a2dp_cache_ms = 0; static char g_bt_a2dp_codec[16] = "", g_bt_a2dp_rate[16] = ""; /* A2DP 摘要(首个带 codec/rate 的端点);budget_ms 为总预算,失败置空。纯读,不改缓存。 */ static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen, int budget_ms) { if (codec && clen > 0) codec[0] = 0; if (rate && rlen > 0) rate[0] = 0; char buf[MEOW_IPC_MAX - 16]; bt_a2dp_query(buf, sizeof buf, budget_ms); char *line = buf; while (line && *line) { char *nl = strchr(line, '\n'); if (nl) *nl = 0; if (!strncmp(line, "A2DP ", 5)) { const char *c = strstr(line, " codec="); const char *r = strstr(line, " rate="); if (c && codec) { c += 7; const char *e = c; while (*e && *e != ' ' && *e != '\n') e++; int l = (int)(e - c); if (l > clen - 1) l = clen - 1; memcpy(codec, c, (size_t)l); codec[l] = 0; } if (r && rate) { r += 6; const char *e = r; while (*e && *e != ' ' && *e != '\n') e++; int l = (int)(e - r); if (l > rlen - 1) l = rlen - 1; memcpy(rate, r, (size_t)l); rate[l] = 0; } if ((codec && codec[0]) || (rate && rate[0])) return; } line = nl ? nl + 1 : NULL; } } /* 附近设备文本:netmgrd 权威扫描缓存(/tmp/meow_bt_scan.txt),行 "Device "(去重)。 * M49:实时 `[NEW] Device` 行以 ANSI ESC 开头(见 bt_scan_line_parse 注释里的字节证据), * 旧实现只认行首 "Device "/"[NEW] Device " → 实时行全丢,只能靠末尾 `devices` 块。 * `[DEL]` 的处理是「不新增」而不是「删掉已有」:bt_scan_on 每次扫描都用 `>` 整文件覆盖缓存, * 所以跨会话不会累积;但**同一次扫描窗口内**先出现后离开的设备仍会留在列表里,且末尾 * `devices` 块本身含 BlueZ 历史缓存——所以这份列表是「本轮扫描见到过的设备」,不是「此刻在场」。 */ static void bt_devices_text(char *buf, int cap) { int off = 0; if (cap <= 0) return; buf[0] = 0; FILE *f = fopen(g_bt_scan_cache, "r"); if (!f) return; char seen[16][24]; int n_seen = 0; char line[512]; while (fgets(line, sizeof line, f) && n_seen < 16) { char mac[24], name[64]; if (!bt_scan_line_parse(line, mac, sizeof mac, name, sizeof name)) continue; int dup = 0; for (int i = 0; i < n_seen; i++) if (bt_mac_eq(seen[i], mac)) { dup = 1; break; } if (dup) continue; memcpy(seen[n_seen], mac, 18); seen[n_seen][18] = 0; n_seen++; int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", mac, name); if (l <= 0 || off + l >= cap) break; off += l; } fclose(f); buf[off] = 0; } /* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。 * M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效, * 宿主测试没法覆盖扫描链路)。 */ static long g_bt_scan_ms = 0; static void bt_scan_on(void) { long t = mono_now(); if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; g_bt_scan_ms = t; char cmd[320]; snprintf(cmd, sizeof cmd, "(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | " "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1 &", g_bluetoothctl, g_bt_scan_cache); if (system(cmd) != 0) svc_log("bt scan on: spawn failed (%s)", g_bt_scan_cache); } static void bt_scan_off(void) { char cmd[192]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan off 2>/dev/null", g_bluetoothctl); system(cmd); } /* ---------------- 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=无该类型/写失败。 */ /* rfkill 软件开关。all=0 只写第一个同类型节点(旧行为),all=1 写**全部**同类型节点: * 真机有 rfkill0/rfkill3 两个 bluetooth 节点,只写首个会让另一个仍处于 block。 */ static int rfkill_set_block_ex(const char *type, int block, int all) { 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); if (!all) break; } closedir(d); } return rc; } static int rfkill_set_block(const char *type, int block) { return rfkill_set_block_ex(type, block, 0); } /* 蓝牙用「全节点」版本:任一节点仍 block 就等于没开(M47 finding 的第二个点)。 */ static int rfkill_set_block_all(const char *type, int block) { return rfkill_set_block_ex(type, block, 1); } /* ---------------- 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; /* ---- 期 5:公网/内网流量统计(netlink sock_diag,按 TCP 连接累计字节;UDP 无字节计数,诚实标注)。 * 全部静态表(禁 malloc);每 2s 节拍在 census_run 内刷新,build_status_json 只读序列化。 ---- */ typedef struct { char entity[48]; uint64_t pub_rx, pub_tx; /* 远端为公网的 TCP 累计 */ uint64_t lan_rx, lan_tx; } pub_ent_t; /* 远端为内网/本链路的 TCP 累计 */ static pub_ent_t g_pub_ent[ENTITY_MAX]; static int g_pub_ent_n = 0; typedef struct { uint32_t inode; uint64_t last_rx, last_tx; long seen_gen; } flow_last_t; /* inode→上拍累计(增量用) */ static flow_last_t g_flow_last[FLOW_LAST_MAX]; static int g_flow_last_n = 0; static long g_flow_gen = 0; /* 每拍代际(剔陈旧 inode 用) */ typedef struct { uint32_t remote_ip; uint16_t remote_port; char entity[48]; char domain[48]; uint64_t rx, tx; long last_seen; } pub_flow_t; /* 公网字节 top 流 */ static pub_flow_t g_pub_flow[PUB_FLOW_MAX]; static int g_pub_flow_n = 0; static uint64_t g_pub_rx = 0, g_pub_tx = 0; /* 公网总收发累计(TCP) */ static int g_diag_fd = -1; /* NETLINK_SOCK_DIAG(懒开) */ static int g_diag_warned = 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); } /* status JSON 里的公网/内网流量子块。TOP-K 序列化保住 4096 单帧预算;数据侧仍全量累计。 * 口径:netlink sock_diag 按 TCP 连接(远端分公网/内网)累计字节;UDP/ARP/mDNS 无字节计数,标 tcp_only。 * 注意:本函数只闭合 pub_bytes 对象(}, 不闭合外层 status 对象,由调用方收尾)。 */ static void pub_bytes_json(jw_t *w) { jadd(w, ",\"pub_bytes\":{"); jadd(w, "\"source\":\"sock_diag_tcp\",\"tcp_only\":true,"); jadd(w, "\"total\":{\"rx\":%llu,\"tx\":%llu},", (unsigned long long)g_pub_rx, (unsigned long long)g_pub_tx); /* by_entity:按 pub+lan total 取 TOP-K(量小,线性选择)。 */ jadd(w, "\"by_entity\":["); { int sel[PUB_SERVE_ENT], seln = 0; for (int i = 0; i < g_pub_ent_n; i++) { uint64_t s = g_pub_ent[i].pub_rx + g_pub_ent[i].pub_tx + g_pub_ent[i].lan_rx + g_pub_ent[i].lan_tx; if (seln < PUB_SERVE_ENT) { sel[seln++] = i; continue; } int mi = 0; for (int j = 1; j < seln; j++) { pub_ent_t *a = &g_pub_ent[sel[j]]; pub_ent_t *b = &g_pub_ent[sel[mi]]; if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx < b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) mi = j; } pub_ent_t *m = &g_pub_ent[sel[mi]]; if (m->pub_rx + m->pub_tx + m->lan_rx + m->lan_tx < s) sel[mi] = i; } for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++) { pub_ent_t *a = &g_pub_ent[sel[i]], *b = &g_pub_ent[sel[j]]; if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx < b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; } } for (int k = 0; k < seln; k++) { pub_ent_t *e = &g_pub_ent[sel[k]]; if (k) jadd(w, ","); jadd(w, "{\"entity\":\"%s\",\"pub\":[%llu,%llu],\"lan\":[%llu,%llu]}", e->entity, (unsigned long long)e->pub_rx, (unsigned long long)e->pub_tx, (unsigned long long)e->lan_rx, (unsigned long long)e->lan_tx); } } jadd(w, "],"); /* by_flow:仅公网 top 流。 */ jadd(w, "\"by_flow\":["); { int sel[PUB_SERVE_FLOW], seln = 0; for (int i = 0; i < g_pub_flow_n; i++) { uint64_t s = g_pub_flow[i].rx + g_pub_flow[i].tx; if (seln < PUB_SERVE_FLOW) { sel[seln++] = i; continue; } int mi = 0; for (int j = 1; j < seln; j++) if (g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx < g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx) mi = j; if (g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx < s) sel[mi] = i; } for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++) if (g_pub_flow[sel[i]].rx + g_pub_flow[sel[i]].tx < g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; } for (int k = 0; k < seln; k++) { pub_flow_t *f = &g_pub_flow[sel[k]]; char ip[32]; snprintf(ip, sizeof ip, "%u.%u.%u.%u", (f->remote_ip >> 24) & 0xff, (f->remote_ip >> 16) & 0xff, (f->remote_ip >> 8) & 0xff, f->remote_ip & 0xff); if (k) jadd(w, ","); jadd(w, "{\"ip\":\"%s\",\"port\":%u,\"entity\":\"%s\",\"domain\":\"%s\",\"rx\":%llu,\"tx\":%llu}", ip, (unsigned)f->remote_port, f->entity, f->domain, (unsigned long long)f->rx, (unsigned long long)f->tx); } } jadd(w, "]"); jadd(w, "}"); } 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_int_or_null(&w, "connected", bt.connected_n); /* a2dp_codec/a2dp_rate 是 MeowPanel btCard 的字段(docs/plan/FUTURE_MEOWPANEL.md:48), * 但 status 是高频路径且守护单线程(DNS 代理同环)——所以:无连接直接不查(0 次 fork), * 有连接时走 TTL 缓存,刷新也只给 BT_A2DP_STATUS_BUDGET_MS。`bt a2dp` 命令另有独立调用。 */ if (bt.connected_n > 0) { long now_s = mono_now(); if (!g_bt_a2dp_cache_ms || now_s - g_bt_a2dp_cache_ms >= BT_A2DP_CACHE_SEC) { g_bt_a2dp_cache_ms = now_s; bt_a2dp_summary(g_bt_a2dp_codec, sizeof g_bt_a2dp_codec, g_bt_a2dp_rate, sizeof g_bt_a2dp_rate, BT_A2DP_STATUS_BUDGET_MS); } } else { g_bt_a2dp_cache_ms = 0; g_bt_a2dp_codec[0] = 0; g_bt_a2dp_rate[0] = 0; } jadd_str_or_null(&w, "a2dp_codec", g_bt_a2dp_codec); jadd_str_or_null(&w, "a2dp_rate", g_bt_a2dp_rate); 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, "]"); /* 期5:公网/内网流量(netlink sock_diag)——只闭合 pub_bytes 对象,外层 status 由下方 `}` 收尾。 */ pub_bytes_json(&w); 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); } /* ---- 期5:公网/内网流量计量(netlink sock_diag,per-TCP 连接累计字节) ---- * 口径:每个 TCP 连接 tcp_info 的 tcpi_bytes_received(off128)/tcpi_bytes_acked(off120) 是连接 * 生命周期累计;按拍差分(首见=基线 0,防一次性计入存量),远端分公网/内网(RFC1918/回环/链路本地/ * 组播都归内网)。UDP 无字节计数(DNS/流媒体 UDP 不计量),故整体标 tcp_only。内核 4.4.94 实测返回 * tcp_info plen=144,bytes_* 有效;所有表静态,禁 malloc。 */ static int ip_is_public(uint32_t ip) { uint8_t b0 = (uint8_t)(ip >> 24), b1 = (uint8_t)(ip >> 16); if (b0 == 0 || b0 == 127) return 0; /* 保留/回环 */ if (b0 == 10) return 0; /* RFC1918 10/8 */ if (b0 == 172 && b1 >= 16 && b1 <= 31) return 0; /* RFC1918 172.16/12 */ if (b0 == 192 && b1 == 168) return 0; /* RFC1918 192.168/16 */ if (b0 == 169 && b1 == 254) return 0; /* 链路本地 */ if (b0 >= 224) return 0; /* 组播/保留 224/4+240/4 */ return 1; } static flow_last_t *flow_last_get(uint32_t inode) { for (int i = 0; i < g_flow_last_n; i++) if (g_flow_last[i].inode == inode) return &g_flow_last[i]; if (g_flow_last_n < FLOW_LAST_MAX) return &g_flow_last[g_flow_last_n++]; return &g_flow_last[0]; /* 全满:复用首槽(本设备并发 TCP 极少,防御性兜底) */ } static pub_ent_t *pub_ent_get(const char *entity) { for (int i = 0; i < g_pub_ent_n; i++) if (strcmp(g_pub_ent[i].entity, entity) == 0) return &g_pub_ent[i]; if (g_pub_ent_n < ENTITY_MAX) { pub_ent_t *p = &g_pub_ent[g_pub_ent_n++]; memset(p, 0, sizeof *p); snprintf(p->entity, sizeof p->entity, "%s", entity); return p; } return &g_pub_ent[0]; /* 满:并入首槽(保稳定,不新键) */ } static void pub_flow_add(uint32_t rip, uint16_t rport, const char *entity, const char *domain, uint64_t drx, uint64_t dtx) { if (drx == 0 && dtx == 0) return; pub_flow_t *f = NULL; for (int i = 0; i < g_pub_flow_n; i++) if (g_pub_flow[i].remote_ip == rip && g_pub_flow[i].remote_port == rport && strcmp(g_pub_flow[i].entity, entity) == 0) { f = &g_pub_flow[i]; break; } if (!f) { if (g_pub_flow_n < PUB_FLOW_MAX) f = &g_pub_flow[g_pub_flow_n++]; else { f = &g_pub_flow[0]; for (int i = 1; i < PUB_FLOW_MAX; i++) if (g_pub_flow[i].rx + g_pub_flow[i].tx < f->rx + f->tx) f = &g_pub_flow[i]; } memset(f, 0, sizeof *f); f->remote_ip = rip; f->remote_port = rport; snprintf(f->entity, sizeof f->entity, "%s", entity); snprintf(f->domain, sizeof f->domain, "%s", domain ? domain : ""); } f->rx += drx; f->tx += dtx; f->last_seen = mono_now(); } static void pub_bytes_run(const conn_t *conns, int n) { /* 懒开 NETLINK_SOCK_DIAG。 */ if (g_diag_fd < 0) { g_diag_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_SOCK_DIAG); if (g_diag_fd < 0) { if (!g_diag_warned) { g_diag_warned = 1; net_log("warn", "pub", "sock_diag open fail errno=%d", errno); } return; } struct timeval tv = { 0, 120000 }; /* 120ms 收超时,防单拍卡死(本设备连接极少,秒回) */ setsockopt(g_diag_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); struct sockaddr_nl sa; memset(&sa, 0, sizeof sa); sa.nl_family = AF_NETLINK; bind(g_diag_fd, (struct sockaddr *)&sa, sizeof sa); } struct { struct nlmsghdr nlh; struct inet_diag_req_v2 req; } req; memset(&req, 0, sizeof req); req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.req)); req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY; req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; req.req.sdiag_family = AF_INET; req.req.sdiag_protocol = IPPROTO_TCP; req.req.idiag_ext = (1u << (INET_DIAG_INFO - 1)); /* 请求 tcp_info 扩展 */ req.req.idiag_states = 0xFFFFFFFF; struct sockaddr_nl kern; memset(&kern, 0, sizeof kern); kern.nl_family = AF_NETLINK; if (sendto(g_diag_fd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr *)&kern, sizeof kern) < 0) { if (!g_diag_warned) { g_diag_warned = 1; net_log("warn", "pub", "sock_diag send fail errno=%d", errno); } return; } g_flow_gen++; char buf[16384]; int done = 0; for (int round = 0; round < 8 && !done; round++) { /* 有界收割 */ ssize_t r = recv(g_diag_fd, buf, sizeof buf, 0); if (r < 0) break; /* 超时/错误 → 结束本拍 */ for (struct nlmsghdr *nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned)r); nlh = NLMSG_NEXT(nlh, r)) { if (nlh->nlmsg_type == NLMSG_DONE || nlh->nlmsg_type == NLMSG_ERROR) { done = 1; break; } struct inet_diag_msg *dm = (struct inet_diag_msg *)NLMSG_DATA(nlh); uint32_t inode = dm->idiag_inode; uint32_t rnet = ntohl(dm->id.idiag_dst[0]); /* 远端(host 序) */ uint16_t rport = ntohs(dm->id.idiag_dport); if (rnet == 0 || rport == 0 || inode == 0) continue; /* 监听口/未定 → 跳过 */ int msglen = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(struct inet_diag_msg))); if (msglen <= 0) continue; /* 解析 INET_DIAG_INFO(tcp_info),取 bytes_received(off128)/bytes_acked(off120)。 */ uint64_t brx = 0, btx = 0; int have = 0; struct nlattr *at = (struct nlattr *)((char *)dm + sizeof(struct inet_diag_msg)); int rem = msglen; while (rem >= (int)sizeof(struct nlattr) && at->nla_len >= sizeof(struct nlattr) && at->nla_len <= (unsigned)rem) { int type = at->nla_type & NLA_TYPE_MASK; int plen = (int)at->nla_len - NLA_HDRLEN; if (type == INET_DIAG_INFO && plen >= TCP_INFO_MIN_LEN) { unsigned char *pay = (unsigned char *)at + NLA_HDRLEN; memcpy(&brx, pay + 128, 8); /* tcpi_bytes_received */ memcpy(&btx, pay + 120, 8); /* tcpi_bytes_acked */ have = 1; break; } int alen = NLA_ALIGN((int)at->nla_len); rem -= alen; at = (struct nlattr *)((char *)at + alen); } if (!have) continue; /* 匹配连接表 → 实体 + 域名;未归因 → 跳过(保归因干净)。 */ const conn_t *c = NULL; for (int i = 0; i < n; i++) if (conns[i].inode == inode) { c = &conns[i]; break; } if (!c) continue; flow_last_t *fl = flow_last_get(inode); uint64_t drx, dtx; if (fl->inode != inode) { /* 首见/复槽 → 基线扣 0,防一次性计入存量 */ fl->inode = inode; fl->last_rx = brx; fl->last_tx = btx; fl->seen_gen = g_flow_gen; drx = 0; dtx = 0; } else { drx = brx >= fl->last_rx ? (brx - fl->last_rx) : brx; dtx = btx >= fl->last_tx ? (btx - fl->last_tx) : btx; fl->last_rx = brx; fl->last_tx = btx; fl->seen_gen = g_flow_gen; } if (drx == 0 && dtx == 0) continue; pub_ent_t *pe = pub_ent_get(c->entity); if (ip_is_public(rnet)) { pe->pub_rx += drx; pe->pub_tx += dtx; g_pub_rx += drx; g_pub_tx += dtx; pub_flow_add(rnet, rport, c->entity, c->domain, drx, dtx); } else { pe->lan_rx += drx; pe->lan_tx += dtx; } } } /* 剔掉本拍未出现的 inode(已关闭/变监听口),防表膨胀。 */ if (g_flow_last_n) { int w = 0; for (int i = 0; i < g_flow_last_n; i++) if (g_flow_last[i].seen_gen == g_flow_gen) g_flow_last[w++] = g_flow_last[i]; g_flow_last_n = w; } } /* ---- 连接普查(每 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"); } } /* 期5:公网/内网流量计量(netlink sock_diag,基于本拍连接表归因)。 */ pub_bytes_run(g_conn, g_conn_n); } /* ---- 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 拍):仅上行接口累计(去 lo)。 */ long rx = -1, tx = 0; uplink_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") || !strcmp(cmd, "connected") || !strcmp(cmd, "a2dp") || !strcmp(cmd, "devices")) return 0; return 1; /* on/off/connect/disconnect/pair/unpair/scan */ } /* 写级授权决策(纯函数,宿主自测用;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; } /* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。 */ static int wpa_cmd(char *out, int cap, const char *args) { char cmd[512]; snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", g_wpa_cli, args); return run_cmd(out, cap, RUNCMD_TMO_MS, cmd); } /* WiFi 设置页 bug4-②(2026-09-15):list_networks/scan_results 对非 ASCII 字节输出 \xNN 转义 * (中文 SSID)——还原为原始字节串再与输入 SSID 做同表示比较;不还原则中文 SSID 永不命中 * 既有网络 → 新建同名 open 记录顶影原记录。net.c wpa_decode_ssid / netmgr_ipc.c nm_decode_ssid * 已有两份同功能实现(后续可收拢,本次先在 netmgrd 补齐)。 */ static int wpa_hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } static void wpa_unescape_ssid(const char *src, char *dst, size_t dcap) { size_t o = 0; while (*src && o + 1 < dcap) { if (src[0] == '\\' && src[1] == 'x' && wpa_hexval(src[2]) >= 0 && wpa_hexval(src[3]) >= 0) { dst[o++] = (char)((wpa_hexval(src[2]) << 4) | wpa_hexval(src[3])); src += 4; } else { dst[o++] = *src++; } } dst[o] = 0; } /* WiFi 设置页 bug4-③(2026-09-15):save_config 结果校验。wpa_supplicant 仅在 conf 含 * update_config=1 时允许写盘;固件模板 /etc/wpa_supplicant.conf 无此项 → 首次 FAIL 时先 * `set update_config 1`(运行期开关,wpa_supplicant 标准用法;成功落盘后文件自带 * update_config=1)再重试一次。返回 1=已持久化;0=仍失败(调用方须在报文/日志明示 * 「记录未持久化」)。仅 MeowOS 模式调用(写盘红线不变,NETMGR_DESIGN §14)。 */ static int wifi_save_config(void) { char out[160]; if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) return 1; wpa_cmd(out, sizeof out, "set update_config 1"); if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) { net_log("info", "write", "save_config 初次 FAIL → set update_config 1 重试成功(此后 conf 自带该位)"); return 1; } net_log("warn", "write", "save_config 失败(含 set update_config 1 重试):记录未持久化"); return 0; } /* WiFi 设置页 bug1(2026-09-15):连接状态机——有界等待 wpa_state=COMPLETED(每 200ms 查 * status,上限 timeout_ms,防卡死)。返回 1=已关联(out 带回 ssid/ip 供诚实报文);0=超时。 */ static int wifi_wait_completed(int timeout_ms, wifi_t *out) { int waited = 0; for (;;) { wifi_t w; wifi_status(&w); if (w.connected) { if (out) *out = w; return 1; } if (waited >= timeout_ms) return 0; struct timespec ts = { 0, 200 * 1000000L }; nanosleep(&ts, NULL); waited += 200; } } 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); /* P3(WiFi 设置页附带,2026-09-15):触发后 300ms→1500ms 再读结果——真机扫描常 1-2s, * 300ms 读到的多是上一轮陈旧结果。IPC 客户端 per-cmd 超时相应放宽(netmgr_ipc scan=3.5s)。 */ struct timespec ts = { 1, 500 * 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); } /* WiFi 连接(bug1/bug2/bug4 修复,2026-09-15)。hidden=1 走隐藏网络流程(connecth): * 对所用 id(新建/命中皆然)追加 scan_ssid=1,并把 COMPLETED 等待上限放宽(主动探测+关联更慢)。 * 流程:匹配既有(\xNN 还原同表示比较)→ 新建或(命中且带新凭据时)更新 psk → * enable+select → 有界等待 wpa_state=COMPLETED(不再无条件报成功)→ * 成功:enable_network all(消除 select 的 disabled 副作用)+ save_config(校验+重试); * 失败:本次新建的网络 remove 回收(防坏记录堆积),报文明示失败原因。 */ static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden) { 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(剥引号 + \xNN 还原后同表示比较);命中则复用其 id。 */ char cmd[256]; char list[1024]; int n = wpa_cmd(list, sizeof list, "list_networks"); 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], unesc[160], 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); } wpa_unescape_ssid(clean, unesc, sizeof unesc); /* bug4-②:中文 SSID 转义还原 */ str_capsafe(unesc, norm_clean, sizeof norm_clean); if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id; } ln = strtok(NULL, "\n"); } } char out[128]; int created = 0; if (id < 0) { if (wpa_cmd(out, sizeof out, "add_network") <= 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; } created = 1; snprintf(cmd, sizeof cmd, "set_network %d ssid '\"%s\"'", id, tmp_ssid); wpa_cmd(out, sizeof out, cmd); if (tmp_psk[0]) snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk); else snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id); wpa_cmd(out, sizeof out, cmd); } else if (tmp_psk[0]) { /* bug4-①/bug1-④:命中既有网络且调用方带了新 psk → 先更新凭据再连 * (旧记录密码错误可经扫描路径重输修复;psk 为空则不动既有凭据)。 */ snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk); wpa_cmd(out, sizeof out, cmd); } if (hidden) { /* bug2:隐藏 AP 必须 scan_ssid=1,supplicant 才会主动发 Probe Request(命中既有也要补)。 */ snprintf(cmd, sizeof cmd, "set_network %d scan_ssid 1", id); wpa_cmd(out, sizeof out, cmd); } { /* enable + select 该网络(复用/新建皆用)。 */ snprintf(cmd, sizeof cmd, "enable_network %d", id); wpa_cmd(out, sizeof out, cmd); snprintf(cmd, sizeof cmd, "select_network %d", id); wpa_cmd(out, sizeof out, cmd); } /* bug1:有界等待 wpa_state=COMPLETED——密码错误/AP 不在范围/握手失败不再误报成功。 */ wifi_t w; int wait_ms = hidden ? g_connecth_wait_ms : g_connect_wait_ms; if (!wifi_wait_completed(wait_ms, &w)) { if (created) { /* 本次新建且失败 → 回收防坏记录堆积(bug4 联动) */ snprintf(cmd, sizeof cmd, "remove_network %d", id); wpa_cmd(out, sizeof out, cmd); if (g_mode_meow) wifi_save_config(); } net_log("info", "write", "wifi connect ssid=%s id=%d 超时未关联(%dms)", tmp_ssid, id, wait_ms); char msg[128]; snprintf(msg, sizeof msg, "连接失败:密码错误或信号不佳(超时未关联)"); ctl_sets(r, MEOW_ERR_NODEV, msg); return; } /* bug4-④:验证成功后 enable_network all 复位其它网络 disabled 态(select_network 副作用), * 再 save_config 落盘(bug4-③:结果校验,FAIL 时 set update_config 1 重试)。 * 写盘仍受 MeowOS 模式门控(红线不变,NETMGR_DESIGN §14)。 */ int persisted = 0; if (g_mode_meow) { wpa_cmd(out, sizeof out, "enable_network all"); persisted = wifi_save_config(); } net_log("info", "write", "wifi connect ssid=%s id=%d 已连接 ip=%s persisted=%d", tmp_ssid, id, w.ip[0] ? w.ip : "-", persisted); char msg[160]; snprintf(msg, sizeof msg, "已连接:%s ip=%s%s", w.ssid[0] ? w.ssid : tmp_ssid, w.ip[0] ? w.ip : "-", (g_mode_meow && !persisted) ? "(记录未持久化)" : ""); 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_ex(arg, r, 0); } else if (!strcmp(cmd, "connecth")) { wifi_connect_ex(arg, r, 1); /* bug2:隐藏网络(scan_ssid=1 + 更长 COMPLETED 等待) */ } 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 只读由服务端门控)。 * bug4-③:save_config 结果校验(FAIL 时 set update_config 1 重试),报文不再谎称已落盘。 */ int persisted = 0; if (g_mode_meow) persisted = wifi_save_config(); char msg[96]; snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? (persisted ? "+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: 未知子命令"); } } /* 取命令输出的一段可读文本(去 ANSI/控制符,连续空白折成一个空格;最多 cap-1 字符)。 */ static void bt_snip(const char *s, char *dst, size_t cap) { if (!dst || cap == 0) return; dst[0] = 0; char tmp[512]; bt_clean_ansi(s ? s : "", tmp, sizeof tmp); size_t o = 0; int prev_sp = 0; for (size_t i = 0; tmp[i] && o + 1 < cap; i++) { char c = tmp[i]; if (c == ' ' || c == '\t') { if (prev_sp) continue; c = ' '; prev_sp = 1; } else prev_sp = 0; dst[o++] = c; } while (o && dst[o - 1] == ' ') o--; dst[o] = 0; } /* 探测函数带 tmo:写后复查的**总**预算必须传到每一次 run_cmd 里,否则「两次探测之间看 * deadline」挡不住探测自身那 1500ms(r1 评审 P1:实测 connect 1528ms / bt on 2424ms, * 越过客户端 NM_TMO_DFL=1500 的 IPC 窗口 → 客户端判不可达 → 二次下发 + 假成功)。 */ typedef int (*bt_probe_fn)(const char *mac, int tmo_ms); static int bt_probe_acl(const char *mac, int tmo_ms) { return bt_acl_has_tri(mac, tmo_ms); } static int bt_probe_paired(const char *mac, int tmo_ms) { return bt_info_flag(mac, "Paired", tmo_ms); } /* 熔断器状态(只服务 BT 写操作;守护单线程,无需加锁)。 */ static int g_bt_tmo_streak = 0; static long g_bt_tmo_until = 0; /* 冷却截止(mono 秒),0=未熔断 */ static int bt_circuit_open(void) { return (g_bt_tmo_streak >= BT_TMO_STREAK_MAX && g_bt_tmo_until && mono_now() < g_bt_tmo_until); } static void bt_circuit_note(runcmd_status_t st) { if (st == RUNCMD_TIMEOUT) { if (g_bt_tmo_streak < BT_TMO_STREAK_MAX) g_bt_tmo_streak++; if (g_bt_tmo_streak >= BT_TMO_STREAK_MAX) g_bt_tmo_until = mono_now() + BT_TMO_COOLDOWN_SEC; } else { g_bt_tmo_streak = 0; g_bt_tmo_until = 0; } } /* 写后真态复查:总预算 budget_ms,**每次探测只拿剩余预算**,一旦命中 want 立刻返回。 * 返回最后一次探测值(1/0/-1=未知)。总耗时 ≤ budget_ms + 一次探测的收尾。 */ static int bt_poll_probe(bt_probe_fn fn, const char *mac, int want, int budget_ms) { long deadline = mono_ms() + budget_ms; long remain = budget_ms; int v = fn(mac, (int)(remain > 0 ? remain : 1)); while (v != want) { remain = deadline - mono_ms(); if (remain <= 0) break; struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L }; nanosleep(&ts, NULL); remain = deadline - mono_ms(); if (remain <= 0) break; v = fn(mac, (int)remain); } return v; } /* bt on/off:顺序按 M47 finding 走 —— 开:先解 rfkill(全部节点)再 up(blocked 时 up 必败); * 关:先 down 再 block。up 第一次没生效时在**剩余预算内**重试一次(射频刚放开常差一拍)。 */ static void bt_onoff(const char *cmd, ctl_result_t *r) { int up = strcmp(cmd, "on") == 0; int want = up ? 1 : 0; long deadline = mono_ms() + BT_ACT_SHORT_MS + BT_VERIFY_BUDGET_MS; char cmd2[192]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 %s", g_hciconfig, up ? "up" : "down"); char buf[256]; buf[0] = 0; runcmd_res_t res; memset(&res, 0, sizeof res); res.exit_code = -1; res.status = RUNCMD_EXEC; if (bt_circuit_open()) { snprintf(r->text, sizeof r->text, "bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)", cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now()); r->code = MEOW_ERR_NODEV; return; } if (up) rfkill_set_block_all("bluetooth", 0); /* 先解 block,否则 up 会被拦下 */ run_cmd_ex(buf, sizeof buf, BT_ACT_SHORT_MS, cmd2, &res); bt_circuit_note(res.status); if (!up) rfkill_set_block_all("bluetooth", 1); /* 先 down 再 block */ int pw = -1, retried = 0; for (;;) { long remain = deadline - mono_ms(); if (remain <= 0) break; pw = bt_powered_now((int)remain); if (pw == want) break; if (up && !retried) { /* up 未生效:剩余预算内重试一次 */ retried = 1; remain = deadline - mono_ms(); if (remain > 0) { run_cmd_ex(buf, sizeof buf, (int)(remain > BT_ACT_SHORT_MS ? BT_ACT_SHORT_MS : remain), cmd2, &res); continue; } } remain = deadline - mono_ms(); if (remain <= 0) break; struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L }; nanosleep(&ts, NULL); } char msg[280]; if (pw == want) { snprintf(msg, sizeof msg, "bt %s: hci0 %s + rfkill %s(power 已复查)", cmd, up ? "up" : "down", up ? "unblocked" : "blocked"); ctl_sets(r, MEOW_OK, msg); return; } int fail = 0; const char *why = run_cmd_why(&res, &fail); char snip[121]; bt_snip(buf, snip, sizeof snip); snprintf(msg, sizeof msg, "bt %s: 未生效(powered=%s rfkill=%s%s%s%s%s)", cmd, pw > 0 ? "on" : pw == 0 ? "off" : "unknown", up ? "unblocked" : "blocked", why[0] ? ";" : "", why, (!why[0] && snip[0]) ? ";输出: " : "", (!why[0] && snip[0]) ? snip : ""); ctl_sets(r, MEOW_ERR_NODEV, msg); } /* connect/disconnect/pair/unpair:下发 + 按真态(hcitool con / bluetoothctl info)定稿。 * 旧实现 `n >= 0 → MEOW_OK` 恒报成功(命令失败也返回 0 字节),这是「点配对提示成功但没配上」的根因。 * 成功文案不再补设备名:那次 bluetoothctl 查询是 1-2 个额外子进程,会吃掉 1500ms 窗口; * UI 本来就按 MAC join 名字(nm_parse_devices)。 */ static void bt_write_execute(const char *cmd, const char *arg, ctl_result_t *r) { int is_conn = !strcmp(cmd, "connect"), is_disc = !strcmp(cmd, "disconnect"); int is_pair = !strcmp(cmd, "pair"), is_unpair = !strcmp(cmd, "unpair"); const char *sub = is_unpair ? "remove" : cmd; /* bluetoothctl 取消配对=remove */ int tmo = (is_conn || is_pair) ? BT_ACT_TMO_MS : BT_ACT_SHORT_MS; if (bt_circuit_open()) { /* 连续超时:不再 fork,如实回熔断 */ snprintf(r->text, sizeof r->text, "bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)", cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now()); r->code = MEOW_ERR_NODEV; return; } char cmd2[224]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s %s", g_bluetoothctl, sub, arg); char out[512]; runcmd_res_t res; run_cmd_ex(out, sizeof out, tmo, cmd2, &res); bt_circuit_note(res.status); int fail = 0; const char *why = run_cmd_why(&res, &fail); char snip[121]; bt_snip(out, snip, sizeof snip); char msg[300]; char tail[220]; tail[0] = 0; if (why[0]) snprintf(tail, sizeof tail, ";%s", why); if (snip[0]) { size_t l = strlen(tail); if (l < sizeof tail - 1) snprintf(tail + l, sizeof tail - l, ";输出: %s", snip); } if (is_conn || is_disc) { int want = is_conn ? 1 : 0; int v = bt_poll_probe(bt_probe_acl, arg, want, BT_VERIFY_BUDGET_MS); if (v == want) { snprintf(msg, sizeof msg, "bt %s: %s %s(hcitool con 已复查)", cmd, is_conn ? "已连接" : "已断开", arg); ctl_sets(r, MEOW_OK, msg); return; } snprintf(msg, sizeof msg, "bt %s: 未生效:%s(hcitool con %s%s)", cmd, arg, is_conn ? "无该 ACL" : "仍有该 ACL", tail); ctl_sets(r, MEOW_ERR_NODEV, msg); return; } /* pair / unpair:以 bluetoothctl info 的 Paired: 字段为准 */ int want = is_pair ? 1 : 0; int v = bt_poll_probe(bt_probe_paired, arg, want, BT_VERIFY_BUDGET_MS); int ok = (v == want); if (!ok && is_unpair && v < 0 && !fail) ok = 1; /* remove 后设备整体消失(info 查不到)也算已解除 */ if (ok) { snprintf(msg, sizeof msg, "bt %s: %s %s(bluetoothctl info Paired:%s 已复查)", cmd, is_pair ? "已配对" : "已解除配对", arg, v > 0 ? "yes" : "no"); ctl_sets(r, MEOW_OK, msg); return; } snprintf(msg, sizeof msg, "bt %s: 未生效:%s(Paired=%s%s)", cmd, arg, v > 0 ? "yes" : v == 0 ? "no" : "unknown", tail); ctl_sets(r, MEOW_ERR_NODEV, msg); } static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r) { 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 connected=%d", b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n, b.connected_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[176]; snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired-devices", g_bluetoothctl); char buf[MEOW_IPC_MAX - 16]; runcmd_res_t res; run_cmd_ex(buf, sizeof buf, RUNCMD_TMO_MS, cmd2, &res); if (res.status == RUNCMD_OK) { ctl_sets(r, MEOW_OK, buf); } else if (res.status == RUNCMD_EMPTY) { ctl_sets(r, MEOW_OK, ""); /* 真态:一台都没配对,不是错误 */ } else { int fail = 0; const char *why = run_cmd_why(&res, &fail); char msg[224]; snprintf(msg, sizeof msg, "bt paired: 后端失败(%s)", why); ctl_sets(r, MEOW_ERR_NODEV, msg); } } else if (!strcmp(cmd, "connected")) { char buf[MEOW_IPC_MAX - 16]; bt_connected_text(buf, sizeof buf); ctl_sets(r, MEOW_OK, buf); } else if (!strcmp(cmd, "a2dp")) { char buf[MEOW_IPC_MAX - 16]; bt_a2dp_text(buf, sizeof buf); ctl_sets(r, MEOW_OK, buf); } else if (!strcmp(cmd, "devices")) { char buf[MEOW_IPC_MAX - 16]; bt_devices_text(buf, sizeof buf); ctl_sets(r, MEOW_OK, buf); } else if (!strcmp(cmd, "connect") || !strcmp(cmd, "disconnect") || !strcmp(cmd, "pair") || !strcmp(cmd, "unpair")) { if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 ", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; } bt_write_execute(cmd, arg, r); } else if (!strcmp(cmd, "scan")) { if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); } else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); } else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off"); } 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; int bconn = -1; jint(obj, "paired", &bpaired); jint(obj, "connected", &bconn); char bcodec[24] = "", brate[24] = ""; jstr(obj, "a2dp_codec", bcodec, sizeof bcodec); jstr(obj, "a2dp_rate", brate, sizeof brate); printf("bt available=%d powered=%d address=%s paired=%d connected=%d a2dp=%s@%s\n", bavail, bpow, baddr[0] ? baddr : "-", bpaired, bconn, bcodec[0] ? bcodec : "-", brate[0] ? brate : "-"); } /* 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]|connecth [,psk]|disconnect|on|off" " | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair " " | 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") || !strcmp(arg, "connecth")) { if (!arg2) { fprintf(stderr, "%s 需 [,psk]\n", arg); return 1; } return cli_cmd_result(MSG_NET_WIFI_CTL, arg, 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; } if (!strcmp(arg, "connect") || !strcmp(arg, "disconnect") || !strcmp(arg, "pair") || !strcmp(arg, "unpair")) { if (!arg2) { fprintf(stderr, "bt %s 需 \n", arg); return 1; } return cli_cmd_result(MSG_NET_BT_CTL, arg, arg2); } if (!strcmp(arg, "scan")) { if (arg2 && !strcmp(arg2, "off")) return cli_cmd_result(MSG_NET_BT_CTL, "scan", "off"); return cli_cmd_result(MSG_NET_BT_CTL, "scan", "on"); } 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_HCITOOL")) && e[0]) g_hcitool = e; if ((e = getenv("MEOW_BLUEALSA_CLI")) && e[0]) g_bluealsa_cli = e; if ((e = getenv("MEOW_BT_SCAN_CACHE")) && e[0]) g_bt_scan_cache = 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_WIFI_CONNECT_WAIT_MS")) && atoi(e) >= 200) g_connect_wait_ms = atoi(e); if ((e = getenv("MEOW_WIFI_CONNECTH_WAIT_MS")) && atoi(e) >= 200) g_connecth_wait_ms = 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); }