Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/meow_netmgrd.c
T

3517 lines
146 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 <cmd>`(argv[0]=meow_net 双形态)或 `meow_netmgrd <cmd>` → 一次 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_<bootts>.log`(无卡降级 /tmp),每次开机新文件(复用 /tmp/meow_bootts
* 共享 bootts,一次开机一份);接口状态变化/写操作必记。行格式 `bootts lvl tag kv...`。
*
* 编译(mipsel 静态):build_netmgrd.sh;宿主:build_netmgrd.sh host。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <poll.h>
#include <time.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <linux/netlink.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#include "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00EF) + m_net_*(号段 0x00E0-0x00EF) */
/* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */
static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */
static char g_netdev[160] = "/proc/net/dev"; /* MEOW_NETDEV */
static char g_net_route[160] = "/proc/net/route"; /* MEOW_NET_ROUTE */
static char g_fibtrie[160] = "/proc/net/fib_trie"; /* MEOW_FIBTRIE(无 ip 工具兜底) */
static char g_resolv[160] = "/etc/resolv.conf"; /* MEOW_RESOLV */
static char g_rfkill_dir[128] = "/sys/class/rfkill"; /* MEOW_RFKILL_DIR */
static char g_bt_dir[128] = "/sys/class/bluetooth"; /* MEOW_BT_DIR */
static char g_boot_mode[160] = "/tmp/meow_boot_mode"; /* MEOW_BOOT_MODE */
static char g_log_dir[192] = "/usr/data/mnt/sd_0/MeowOS/logs"; /* MEOW_LOG_DIR(TF 卡日志根) */
static char g_bootts_cache[160]= "/tmp/meow_bootts"; /* MEOW_BOOTTS_CACHE */
static const char *g_sock = "/usr/data/meow_net.sock"; /* MEOW_NET_SOCK/--sock */
/* 后端命令路径(宿主测试用桩脚本覆盖;默认走设备 PATH 的真工具) */
static const char *g_wpa_cli = "wpa_cli"; /* MEOW_WPA_CLI */
static const char *g_hciconfig = "hciconfig"; /* MEOW_HCICONFIG */
static const char *g_bluetoothctl= "bluetoothctl"; /* MEOW_BLUETOOTHCTL */
static const char *g_ip = "ip"; /* MEOW_IP */
static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */
/* 期 2:观测层 + DNS 代理 相关路径(可经 env 覆盖,宿主/qemu 桩测试用) */
static char g_proc_tcp[160] = "/proc/net/tcp"; /* MEOW_TCP */
static char g_proc_udp[160] = "/proc/net/udp"; /* MEOW_UDP */
static char g_ams_sock[128] = "/usr/data/meow_ams.sock"; /* MEOW_AMS_SOCK(amsd 注册表 pid→pkg) */
static char g_svcd_sock[128] = "/tmp/meow_svcd.sock"; /* MEOW_SVCD_SOCK(svcd 服务表 pid→svc,socket 查询兜底) */
static char g_svcd_status[160]= "/tmp/meow_svcd.status"; /* MEOW_SVCD_STATUS(svcd 落盘状态,机器可读 `svc=<name> ... pid=`) */
static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET_POLICY(期3 读改写;/data=/usr/data 符号链接) */
#define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */
#define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */
#define IFACE_MAX 16
#define RFKILL_MAX 8
#define DNS_MAX 8
/* ---- 期 2 常量(NETMGR_DESIGN §4/§5/§8;全部静态表,禁 malloc) ---- */
#define CONN_MAX 512 /* 连接表上限 */
#define DNS_CACHE_MAX 256 /* DNS 应答缓存上限 */
#define DNS_UPSTREAM_TMO_MS 800 /* DNS 上游转发超时 */
#define DNS_QNAME_MAX 128 /* query 域名最大字节 */
#define ENTITY_MAX 128 /* pid→entity 缓存条数 */
#define PORT_ENT_MAX 128 /* udp 本地端口→entity 缓存(DNS 归因用) */
#define RULE_ENT_MAX 16 /* 域名规则实体上限 */
#define RULE_HOST_MAX 24 /* 单实体 allow/deny 主机规则上限 */
#define EVT_DUP_MAX 64 /* 事件日志 1 分钟去重表 */
#define DNS_RESOLV_CHECK_SEC 5 /* resolv.conf 自检周期 */
/* ---- 期 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_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
static long mono_now(void)
{
struct timespec tp;
clock_gettime(CLOCK_MONOTONIC, &tp);
return tp.tv_sec;
}
/* ---------------- 极简日志(stderr;svcd supervisor 捕获) ---------------- */
static void svc_log(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[meow_netmgrd] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
/* ---------------- 文件小工具(同 svc_power) ---------------- */
static int read_file_txt(const char *path, char *buf, int cap)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
int n = 0;
while (n < cap - 1) {
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
if (r <= 0) break;
n += (int)r;
}
close(fd);
buf[n] = 0;
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0;
return n;
}
static int read_file_int_def(const char *path, int def)
{
char buf[32];
if (read_file_txt(path, buf, sizeof buf) <= 0) return def;
return atoi(buf);
}
/* ---------------- 日志框架(§8:每次开机新文件,TF 优先降级 /tmp) ---------------- */
static int g_logfd = -1;
static char g_logfile[192];
/* bootts 复用(与 launcher/audiod 共用 /tmp/meow_bootts 缓存,一次开机一份日志) */
static void get_bootts(char *out, size_t n)
{
int fd = open(g_bootts_cache, O_RDONLY);
if (fd >= 0) {
int r = read(fd, out, (int)n - 1);
close(fd);
if (r > 0) { out[r] = 0; return; }
}
time_t now = time(NULL), up = 0;
fd = open("/proc/uptime", O_RDONLY);
if (fd >= 0) {
char buf[64];
int r = read(fd, buf, sizeof buf - 1);
close(fd);
if (r > 0) { buf[r] = 0; char *p = strchr(buf, ' '); if (p) *p = 0; up = (time_t)atof(buf); }
}
struct tm tm;
localtime_r(&now - up, &tm);
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
fd = open(g_bootts_cache, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { (void)write(fd, out, strlen(out)); close(fd); }
}
static void log_open(void)
{
char ts[64];
get_bootts(ts, sizeof ts);
char path[260];
int use_tf = 0;
if (access(g_log_dir, W_OK) == 0) {
snprintf(path, sizeof path, "%s/net_%s.log", g_log_dir, ts);
use_tf = 1;
} else {
snprintf(path, sizeof path, "/tmp/net_%s.log", ts);
}
snprintf(g_logfile, sizeof g_logfile, "%s", path);
g_logfd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
svc_log("log: %s (%s)", g_logfile, use_tf ? "TF" : "/tmp fallback");
}
/* 日志行:`bootts lvl tag kv...`;stderr 与文件双写(svcd 捕获 stderr + 文件归档)。 */
static void net_log(const char *lvl, const char *tag, const char *fmt, ...)
{
char msg[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof msg, fmt, ap);
va_end(ap);
char bootts[64];
get_bootts(bootts, sizeof bootts);
if (g_logfd >= 0) {
char line[600];
int n = snprintf(line, sizeof line, "%s %s %s %s\n", bootts, lvl, tag, msg);
if (n > 0 && n < (int)sizeof line) (void)write(g_logfd, line, (size_t)n);
}
fprintf(stderr, "[net] %s %s %s\n", lvl, tag, msg);
}
/* ---------------- 有界 popen 收割(257 教训:命令 wedge 时无条件 waitpid 会拖死调用线程) ----------------
* 非阻塞读子进程 stdout(poll 限时);读完后 WNOHANG 优雅等 300ms,不死 SIGKILL 再等 300ms;
* 仍不死(D 态)放弃收割(泄漏一子进程换线程不卡死,已记日志)。返回读到的字节数(-1=起不来)。
* cmd 为完整 shell 行;buf 由调用方提供(静态/栈),不 malloc——热路径禁 malloc。 */
static void reap_bounded(pid_t pid, const char *cmd)
{
for (int i = 0; i < 30; i++) {
if (waitpid(pid, NULL, WNOHANG) == pid) return;
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
svc_log("run_cmd: child %ld not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
kill(pid, SIGKILL);
for (int i = 0; i < 30; i++) {
if (waitpid(pid, NULL, WNOHANG) == pid) return;
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
svc_log("run_cmd: child %ld unkillable (kernel D-state?) — abandon reap (%s)", (long)pid, cmd);
}
static int run_cmd(char *buf, int cap, int tmo_ms, const char *cmd)
{
int p[2];
if (pipe(p) < 0) return -1;
pid_t pid = fork();
if (pid < 0) { close(p[0]); close(p[1]); return -1; }
if (pid == 0) {
dup2(p[1], 1);
int dn = open("/dev/null", O_WRONLY);
if (dn >= 0) { dup2(dn, 2); close(dn); }
close(p[0]); close(p[1]);
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
_exit(127);
}
close(p[1]);
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
int off = 0;
int remain = tmo_ms;
while (remain > 0 && off < cap - 1) {
struct pollfd pf = { p[0], POLLIN, 0 };
int r = poll(&pf, 1, remain);
if (r < 0) { if (errno == EINTR) continue; break; }
if (r == 0) break;
ssize_t rd = read(p[0], buf + off, (size_t)(cap - 1 - off));
if (rd == 0) break;
if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; }
off += (int)rd;
}
close(p[0]);
buf[off] = 0;
reap_bounded(pid, cmd);
return off;
}
/* ---------------- JSON 构建(vsnprintf 助手法,截断分析免疫;同 powermgr) ---------------- */
typedef struct { char *buf; size_t cap, off; } jw_t;
static void jadd(jw_t *w, const char *fmt, ...)
{
if (w->off >= w->cap - 1) return;
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(w->buf + w->off, w->cap - w->off, fmt, ap);
va_end(ap);
if (n > 0) {
w->off += (size_t)n;
if (w->off >= w->cap) w->off = w->cap - 1;
}
}
/* 带引号的字符串值(转义 `"`/`\`/换行)。空串 → ""(不当 null,调用方自决)。 */
static void jadd_q(jw_t *w, const char *key, const char *val)
{
jadd(w, "\"%s\":\"", key);
for (const char *p = val; *p && w->off < w->cap - 3; p++) {
if (*p == '"' || *p == '\\') jadd(w, "\\%c", *p);
else if (*p == '\n') jadd(w, "\\n");
else if (*p == '\t') jadd(w, "\\t");
else w->buf[w->off++] = *p;
}
jadd(w, "\",");
}
static void jadd_int_or_null(jw_t *w, const char *key, int v)
{
if (v < 0) jadd(w, "\"%s\":null,", key);
else jadd(w, "\"%s\":%d,", key, v);
}
static void jadd_str_or_null(jw_t *w, const char *key, const char *s)
{
if (!s || !s[0]) jadd(w, "\"%s\":null,", key);
else jadd_q(w, key, s);
}
/* ---------------- 动作执行结果容器 ---------------- */
typedef struct {
int code;
char text[MEOW_IPC_MAX - 8];
} ctl_result_t;
static void ctl_reset(ctl_result_t *r, int code) { r->code = code; r->text[0] = 0; }
static void ctl_sets(ctl_result_t *r, int code, const char *s)
{
r->code = code;
snprintf(r->text, sizeof r->text, "%s", s);
}
/* ---------------- 接口统一层:模型(设计 §3 统一模型) ---------------- */
typedef struct {
char name[32];
char kind[8]; /* "lo" | "wifi" | "usb-eth" */
int up; /* 1=up 0=down */
char ip[40]; /* ""=null */
char gw[40]; /* ""=null */
long rx, tx; /* 累计字节(/proc/net/dev) */
} iface_t;
static iface_t g_ifaces[IFACE_MAX];
typedef struct { char entity[48]; uint32_t rx, tx; } ent_bytes_t;
static ent_bytes_t g_ent_bytes[ENTITY_MAX]; /* SDK 自愿上报(§4 字节级记账自报通道;声明须在 build_status_json 前) */
static int g_ent_bytes_n = 0;
static int g_iface_n = 0;
typedef struct {
int connected; /* 0/1 */
char state[16]; /* wpa_state 值;""=unknown */
char ssid[64];
char ip[40];
int rssi; /* 信号强度 dBm(可为负);has_rssi=0 时无效 */
int has_rssi; /* wpa_cli 是否给出 signal= */
} wifi_t;
typedef struct {
int available;
int powered;
char address[32];
int paired_n; /* -1=null */
} bt_t;
typedef struct {
char type[16]; /* wlan/bluetooth/... */
int soft; /* 0/1/-1 */
int hard; /* 0/1/-1 */
} rfkill_t;
static rfkill_t g_rfkill[RFKILL_MAX];
static int boot_mode_is_meow(void)
{
char buf[16] = "";
if (read_file_txt(g_boot_mode, buf, sizeof buf) <= 0) return 0; /* 缺失=只读等落地 */
return strcmp(buf, "meow") == 0;
}
/* kind 判定:0=lo 1=wifi 2=usb-eth(热插拔出现即记录)。 */
static int iface_parse_kind(const char *name)
{
if (strcmp(name, "lo") == 0) return 0;
if (strcmp(name, g_wifi_if) == 0) return 1;
return 2;
}
static const char *kind_str(int k)
{
return k == 0 ? "lo" : (k == 1 ? "wifi" : "usb-eth");
}
/* /proc/net/dev 单接口行 → rx(列1)/tx(列9) 累计字节;返回 0=找到。 */
static int netdev_iface(const char *ifname, long *rx, long *tx)
{
FILE *f = fopen(g_netdev, "r");
if (!f) return -1;
char line[256];
int found = -1;
while (fgets(line, sizeof line, f)) {
char *c = strchr(line, ':');
if (!c) continue;
*c = 0;
char *n = line;
while (*n == ' ' || *n == '\t') n++;
if (strcmp(n, ifname) != 0) continue;
char *save = NULL;
char *tk = strtok_r(c + 1, " \t", &save); /* 列1 = rx bytes */
long vr = tk ? strtol(tk, NULL, 10) : 0;
for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); /* 推进到列9 */
long vt = tk ? strtol(tk, NULL, 10) : 0;
if (rx) *rx = vr;
if (tx) *tx = vt;
found = 0;
break;
}
fclose(f);
return found;
}
/* 上行接口累计字节(/proc/net/dev)。仅取「非回环且 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 <if>`;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=<val>)。 */
if (!w->has_rssi) {
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s signal_poll 2>/dev/null", g_wpa_cli);
char sbuf[256];
if (run_cmd(sbuf, sizeof sbuf, RUNCMD_TMO_MS, cmd) > 0) {
char *s = strstr(sbuf, "RSSI=");
if (s) { w->rssi = atoi(s + 5); w->has_rssi = 1; }
}
}
}
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
static void bt_status(bt_t *b)
{
memset(b, 0, sizeof *b);
b->paired_n = -1;
char p[192];
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
if (access(p, F_OK) == 0) b->available = 1;
if (!b->available) return;
/* powered:/sys/.../power(on/off)优先,回退 hciconfig 行首 UP/DOWN */
snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir);
char pw[8] = "";
if (read_file_txt(p, pw, sizeof pw) > 0 && pw[0]) {
b->powered = !strcmp(pw, "on");
} else {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
char buf[256];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
char *ln = buf;
while (ln && *ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
char *q = ln;
while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "UP", 2)) { b->powered = 1; break; }
if (!strncmp(q, "DOWN", 4)) { b->powered = 0; break; }
if (!nl) break;
ln = nl + 1;
}
}
}
/* address:/sys/.../address 优先,回退 hciconfig "BD Address:" */
snprintf(p, sizeof p, "%s/hci0/address", g_bt_dir);
read_file_txt(p, b->address, sizeof b->address);
if (!b->address[0]) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
char buf[256];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
char *q = strstr(buf, "BD Address:");
if (q) {
q += 11;
while (*q == ' ') q++;
int i = 0;
while (*q && *q != ' ' && *q != '\n' && i < (int)sizeof b->address - 1) b->address[i++] = *q++;
b->address[i] = 0;
}
}
}
/* paired 数:bluetoothctl paired(BlueZ 5.54);失败/空 → -1。 */
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired 2>/dev/null", g_bluetoothctl);
char buf[512];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
int n = 0;
char *ln = buf;
while (ln && *ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
char *q = ln;
while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "Device", 6)) n++;
if (!nl) break;
ln = nl + 1;
}
b->paired_n = n;
}
}
/* ---------------- rfkill:统一软开关 ---------------- */
static void rfkill_list(void)
{
int n = 0;
DIR *d = opendir(g_rfkill_dir);
if (d) {
struct dirent *de;
while ((de = readdir(d)) && n < RFKILL_MAX) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char p[448];
rfkill_t *r = &g_rfkill[n];
memset(r, 0, sizeof *r);
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
read_file_txt(p, r->type, sizeof r->type);
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
r->soft = read_file_int_def(p, -1);
snprintf(p, sizeof p, "%s/%s/hard", g_rfkill_dir, de->d_name);
r->hard = read_file_int_def(p, -1);
n++;
}
closedir(d);
}
for (int i = n; i < RFKILL_MAX; i++) memset(&g_rfkill[i], 0, sizeof(rfkill_t));
}
/* 找某类型 rfkill 设备并写 soft=block(1)/unblock(0)。返回 0=ok;-1=无该类型/写失败。 */
static int rfkill_set_block(const char *type, int block)
{
char norm[16] = "";
if (!strcmp(type, "wifi")) snprintf(norm, sizeof norm, "%s", "wlan");
else if (!strcmp(type, "bt")) snprintf(norm, sizeof norm, "%s", "bluetooth");
else snprintf(norm, sizeof norm, "%s", type);
DIR *d = opendir(g_rfkill_dir);
int rc = -1;
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char p[448], t[16] = "";
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
if (read_file_txt(p, t, sizeof t) <= 0 || strcmp(t, norm) != 0) continue;
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
int fd = open(p, O_WRONLY);
if (fd < 0) continue;
char one = block ? '1' : '0';
if (write(fd, &one, 1) == 1) rc = 0;
close(fd);
break;
}
closedir(d);
}
return rc;
}
/* ---------------- udhcpc:续租/释放(SIGUSR1/SIGUSR2,不改启动路径) ---------------- */
static int udhcpc_ctl(const char *op, const char *ifname)
{
char pidfile[160];
const char *iff = (ifname && ifname[0]) ? ifname : g_wifi_if;
snprintf(pidfile, sizeof pidfile, "/var/run/udhcpc.%s.pid", iff);
char buf[32];
if (read_file_txt(pidfile, buf, sizeof buf) <= 0) return -1;
int pid = atoi(buf);
if (pid <= 0) return -1;
int sig = (strcmp(op, "release") == 0) ? SIGUSR2 : SIGUSR1;
return kill(pid, sig) == 0 ? 0 : -1;
}
/* ---------------- IPC 帧读写(同 svc_power 约定,全小端) ---------------- */
static int x_read_full(int fd, uint8_t *buf, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, buf + off, n - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return 0;
}
static int x_write_full(int fd, const uint8_t *buf, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, buf + off, n - off);
if (w <= 0) return -1;
off += (size_t)w;
}
return 0;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &rlen, 4);
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
return 0;
}
static int send_ack(int fd, uint32_t req_id, uint32_t code, uint32_t status)
{
m_ack_t a;
memset(&a, 0, sizeof a);
a.code = code;
a.status = status;
return send_frame(fd, MSG_ACK, req_id, &a, sizeof a);
}
static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg)
{
m_err_t e;
memset(&e, 0, sizeof e);
e.code = code;
uint16_t ml = msg ? (uint16_t)strlen(msg) : 0;
if (ml > 63) ml = 63;
e.msg_len = ml;
if (ml) memcpy(e.msg, msg, ml);
return send_frame(fd, MSG_ERR, req_id, &e, sizeof e);
}
/* json/text 结果通发(m_net_status_result_t / m_net_iface_result_t / m_net_cmd_result_t 布局同构
* code(4)+len(2)+pad(2)+payload,payload ≤ MEOW_IPC_MAX-8)。 */
static int send_payload_result(int fd, uint16_t type, uint32_t req_id, uint32_t code,
const char *payload, size_t pl)
{
m_net_cmd_result_t r; /* 复用布局最大的那个(payload 区 4088B)。 */
memset(&r, 0, sizeof r);
r.code = code;
if (pl > sizeof r.text - 1) pl = sizeof r.text - 1;
r.text_len = (uint16_t)pl;
memcpy(r.text, payload, pl);
r.text[pl] = 0;
return send_frame(fd, type, req_id, &r, (uint32_t)(8 + pl + 1));
}
static int send_net_status_result(int fd, uint32_t req_id, const char *json)
{
return send_payload_result(fd, MSG_NET_STATUS_RESULT, req_id, MEOW_OK, json, strlen(json));
}
static int send_net_iface_result(int fd, uint32_t req_id, uint32_t code, const char *json)
{
return send_payload_result(fd, MSG_NET_IFACE_RESULT, req_id, code, json, strlen(json));
}
static int send_net_wifi_result(int fd, uint32_t req_id, const ctl_result_t *r)
{
return send_payload_result(fd, MSG_NET_WIFI_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
}
static int send_net_bt_result(int fd, uint32_t req_id, const ctl_result_t *r)
{
return send_payload_result(fd, MSG_NET_BT_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
}
/* ---------------- 守护进程态 ---------------- */
static int g_mode_meow = 0; /* meow=1 全功能;0=只读降级 */
static long g_net_last_rx = -1, g_net_last_tx = 0, g_net_last_mono = 0;
static long g_net_rate_rx = -1, g_net_rate_tx = -1; /* B/s;-1=未知 */
static char g_prev_if[IFACE_MAX][32];
static int g_prev_if_up[IFACE_MAX];
static int g_prev_if_n = 0;
/* ---- 期 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(&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:<pkg> | svc:<svc> | proc:<comm> | 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=<lvl> 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:<pkg> / svc:<svc> / proc:<comm>) ---- */
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 <pkg> <st> <pid> <dur> <act>" */
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=<name> 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:<comm>,由 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/<pid>/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 <upstream>(崩溃零断流) */
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")) return 0;
return 1; /* on/off */
}
/* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。
* 返回 MEOW_OK(放行)或 MEOW_ERR_PERM(拒绝)。 */
static int wifi_write_authorize(const caller_t *c, int mode_meow, const char *cmd, const char *arg)
{
if (!wifi_cmd_is_write(cmd, arg)) return MEOW_OK;
if (!is_sys_identity(c)) return MEOW_ERR_PERM;
if (!mode_meow) return MEOW_ERR_PERM;
return MEOW_OK;
}
static int bt_write_authorize(const caller_t *c, int mode_meow, const char *cmd)
{
if (!bt_cmd_is_write(cmd)) return MEOW_OK;
if (!is_sys_identity(c)) return MEOW_ERR_PERM;
if (!mode_meow) return MEOW_ERR_PERM;
return MEOW_OK;
}
/* ---------------- WiFi/BT/rfkill/udhcpc 动作执行(dæmon 侧;写级已在上游鉴权) ---------------- */
static const char *str_capsafe(const char *s, char *tmp, size_t cap)
{
size_t n = strlen(s);
while (n > 0 && (s[0] == ' ' || s[0] == '\t')) { s++; n--; }
while (n > 0 && (s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\n')) n--;
if (n >= cap) n = cap - 1;
memcpy(tmp, s, n); tmp[n] = 0;
return tmp;
}
static void wifi_scan(ctl_result_t *r)
{
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli);
char buf[128];
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
struct timespec ts = { 0, 300 * 1000000L }; /* 触发后稍候再读结果(有界)。 */
nanosleep(&ts, NULL);
char bufout[MEOW_IPC_MAX - 16];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli);
int n = run_cmd(bufout, sizeof bufout, RUNCMD_TMO_MS, cmd);
if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "scan: no results (wpa_cli 空/失败)"); return; }
ctl_sets(r, MEOW_OK, bufout);
}
static void wifi_networks(ctl_result_t *r)
{
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli);
char buf[MEOW_IPC_MAX - 16];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "networks: no output (wpa_cli 空/失败)"); return; }
ctl_sets(r, MEOW_OK, buf);
}
static void wifi_connect(const char *arg, ctl_result_t *r)
{
char ssid[96] = "", psk[96] = "";
if (arg && arg[0]) {
const char *comma = strchr(arg, ',');
if (comma) {
size_t sl = (size_t)(comma - arg);
if (sl >= sizeof ssid) sl = sizeof ssid - 1;
memcpy(ssid, arg, sl); ssid[sl] = 0;
snprintf(psk, sizeof psk, "%s", comma + 1);
} else {
snprintf(ssid, sizeof ssid, "%s", arg);
}
}
char tmp_ssid[96], tmp_psk[96];
str_capsafe(ssid, tmp_ssid, sizeof tmp_ssid);
str_capsafe(psk, tmp_psk, sizeof tmp_psk);
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
/* 找既有网络:list_networks → 匹配 ssid(剥引号比较);命中则复用其 id。 */
char cmd[200];
char list[1024];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli);
int n = run_cmd(list, sizeof list, RUNCMD_TMO_MS, cmd);
int id = -1;
if (n > 0) {
char *ln = strtok(list, "\n");
while (ln) {
char *tab0 = strchr(ln, '\t');
if (tab0) {
int this_id = atoi(ln);
char *ss = tab0 + 1;
char *tab1 = strchr(ss, '\t');
if (tab1) *tab1 = 0;
char clean[128], norm_clean[128];
size_t cl = strlen(ss);
if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') {
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
} else {
snprintf(clean, sizeof clean, "%s", ss);
}
str_capsafe(clean, norm_clean, sizeof norm_clean);
if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id;
}
ln = strtok(NULL, "\n");
}
}
char out[128];
if (id < 0) {
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s add_network 2>/dev/null", g_wpa_cli);
if (run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd) <= 0) {
ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)");
return;
}
id = atoi(out);
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
snprintf(cmd, sizeof cmd,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d ssid '\"%s\"' 2>/dev/null",
g_wpa_cli, id, tmp_ssid);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd);
if (tmp_psk[0]) {
snprintf(cmd, sizeof cmd,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d psk '\"%s\"' 2>/dev/null",
g_wpa_cli, id, tmp_psk);
} else {
snprintf(cmd, sizeof cmd,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s set_network %d key_mgmt NONE 2>/dev/null",
g_wpa_cli, id);
}
run_cmd(out, sizeof out, RUNCMD_TMO_MS, cmd);
}
{ /* enable + select 该网络(复用/新建皆用)。 */
char en[256], sel[256];
snprintf(en, sizeof en, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s enable_network %d 2>/dev/null",
g_wpa_cli, id);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, en);
snprintf(sel, sizeof sel, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s select_network %d 2>/dev/null",
g_wpa_cli, id);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sel);
/* 期3:MeowOS 模式 connect 后持久化 wpa_supplicant.conf(HibyOS 只读由服务端门控,这里仅 MeowOS 生效)。 */
if (g_mode_meow) {
char sc[160];
snprintf(sc, sizeof sc, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
run_cmd(out, sizeof out, RUNCMD_TMO_MS, sc);
}
}
char msg[140];
snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select%s)", tmp_ssid, id,
g_mode_meow ? "+save_config" : "");
ctl_sets(r, MEOW_OK, msg);
}
static void wifi_onoff(const char *cmd, ctl_result_t *r)
{
int block = strcmp(cmd, "off") == 0;
int rc = rfkill_set_block("wlan", block);
if (rc == 0) {
char msg[64];
snprintf(msg, sizeof msg, "wifi %s: rfkill wlan %s", cmd, block ? "blocked" : "unblocked");
ctl_sets(r, MEOW_OK, msg);
} else {
ctl_sets(r, MEOW_ERR_NODEV, "wifi on/off: 无 wlan rfkill 设备或写失败");
}
}
static void rfkill_ctl(const char *arg, ctl_result_t *r)
{
char tmp[64];
str_capsafe(arg ? arg : "", tmp, sizeof tmp);
char *sp = strchr(tmp, ' ');
if (!sp) { ctl_sets(r, MEOW_ERR_INVAL, "rfkill: 用法 <type> <on|off>(如 `rfkill wlan on`)"); return; }
*sp = 0;
char *type = tmp;
char *state = sp + 1;
int block = strcmp(state, "off") == 0 ? 1 : (strcmp(state, "on") == 0 ? 0 : -1);
if (block < 0) { ctl_sets(r, MEOW_ERR_INVAL, "rfkill: state 须 on/off"); return; }
int rc = rfkill_set_block(type, block);
if (rc == 0) {
char msg[64];
snprintf(msg, sizeof msg, "rfkill %s %s: done", type, block ? "blocked" : "unblocked");
ctl_sets(r, MEOW_OK, msg);
} else {
ctl_sets(r, MEOW_ERR_NODEV, "rfkill: 无该类型设备或写失败");
}
}
static void udhcpc_ctl_svc(const char *arg, ctl_result_t *r)
{
char tmp[96];
str_capsafe(arg ? arg : "", tmp, sizeof tmp);
char *sp = strchr(tmp, ' ');
char op[16], iff[32] = "";
if (sp) { *sp = 0; snprintf(op, sizeof op, "%s", tmp); snprintf(iff, sizeof iff, "%s", sp + 1); }
else snprintf(op, sizeof op, "%s", tmp);
if (strcmp(op, "renew") != 0 && strcmp(op, "release") != 0) {
ctl_sets(r, MEOW_ERR_INVAL, "udhcpc: op 须 renew/release");
return;
}
int rc = udhcpc_ctl(op, iff[0] ? iff : NULL);
if (rc == 0) {
char msg[80];
snprintf(msg, sizeof msg, "udhcpc %s %s: sent SIGUSR%s", op, iff[0] ? iff : g_wifi_if,
strcmp(op, "release") == 0 ? "2" : "1");
ctl_sets(r, MEOW_OK, msg);
} else {
ctl_sets(r, MEOW_ERR_NOENT, "udhcpc: 无运行进程 (pid 文件缺失)");
}
}
static void wifi_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
{
ctl_reset(r, MEOW_OK);
if (!strcmp(cmd, "status")) {
wifi_t w;
wifi_status(&w);
char msg[256];
snprintf(msg, sizeof msg, "wifi state=%s connected=%d ssid=%s ip=%s rssi=%d",
w.state[0] ? w.state : "unknown", w.connected, w.ssid[0] ? w.ssid : "-",
w.ip[0] ? w.ip : "-", w.rssi);
ctl_sets(r, MEOW_OK, msg);
} else if (!strcmp(cmd, "scan")) {
wifi_scan(r);
} else if (!strcmp(cmd, "networks")) {
wifi_networks(r);
} else if (!strcmp(cmd, "connect")) {
wifi_connect(arg, r);
} else if (!strcmp(cmd, "disconnect")) {
char cmd2[160];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli);
char buf[128];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, "wifi disconnect: 已下发");
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
wifi_onoff(cmd, r);
} else if (!strcmp(cmd, "forget")) {
int nid = arg ? atoi(arg) : -1;
if (nid < 0) { ctl_sets(r, MEOW_ERR_INVAL, "wifi forget 需 <network_id>"); return; }
char cmd2[192];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s remove_network %d 2>/dev/null",
g_wpa_cli, nid);
char buf[128];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
/* 期4:移除后持久化(MeowOS 模式;HibyOS 只读由服务端门控)。 */
if (g_mode_meow) {
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s save_config 2>/dev/null", g_wpa_cli);
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
}
char msg[64];
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid, g_mode_meow ? "+save_config" : "");
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
} else if (!strcmp(cmd, "rfkill")) {
rfkill_ctl(arg, r);
} else if (!strcmp(cmd, "udhcpc")) {
udhcpc_ctl_svc(arg, r);
} else {
ctl_sets(r, MEOW_ERR_INVAL, "wifi: 未知子命令");
}
}
static void bt_onoff(const char *cmd, ctl_result_t *r)
{
int up = strcmp(cmd, "on") == 0;
char cmd2[160];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 %s 2>/dev/null",
g_hciconfig, up ? "up" : "down");
char buf[128];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
rfkill_set_block("bluetooth", up ? 0 : 1);
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV,
up ? "bt on: hci0 up + rfkill unblocked" : "bt off: hci0 down + rfkill blocked");
}
static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
{
(void)arg;
ctl_reset(r, MEOW_OK);
if (!strcmp(cmd, "status")) {
bt_t b;
bt_status(&b);
char msg[192];
snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d",
b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n);
ctl_sets(r, MEOW_OK, msg);
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
bt_onoff(cmd, r);
} else if (!strcmp(cmd, "paired")) {
char cmd2[160];
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired 2>/dev/null", g_bluetoothctl);
char buf[MEOW_IPC_MAX - 16];
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
if (n <= 0) ctl_sets(r, MEOW_ERR_NODEV, "bt paired: 无输出 (bluetoothctl 不可用/未开)");
else ctl_sets(r, MEOW_OK, buf);
} else {
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
}
}
/* ---------------- 期3 策略命令(IPC 0xEA / CLI policy 共用;写级已在 handle_conn 鉴权) ---------------- */
/* 从 g_rules 移除某实体(shift 数组),返回 0=存在已删。 */
static int policy_remove(const char *entity)
{
for (int i = 0; i < g_rule_n; i++) {
if (strcmp(g_rules[i].entity, entity) == 0) {
for (int j = i; j < g_rule_n - 1; j++) g_rules[j] = g_rules[j + 1];
memset(&g_rules[g_rule_n - 1], 0, sizeof(rule_t));
g_rule_n--;
return 0;
}
}
return -1;
}
/* 输出某实体当前行;无规则输出 (none)。 */
static void policy_print_entity(const rule_t *r, char *out, size_t cap)
{
out[0] = 0;
if (!r) { snprintf(out, cap, "(none)\n"); return; }
size_t o = 0;
int l = snprintf(out + o, cap - o, "%s ", r->entity);
if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
int any = 0;
if (r->has_net) { l = snprintf(out + o, cap - o, "net=%s ", r->net_block ? "block" : "allow"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; any = 1; }
if (r->deny_n || r->allow_n) {
l = snprintf(out + o, cap - o, "domain="); if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
int first = 1;
for (int i = 0; i < r->allow_n; i++) { int la = snprintf(out + o, cap - o, "%s+%s", first ? "" : ",", r->allow[i]); if (la > 0 && o + (size_t)la < cap) o += (size_t)la; first = 0; }
for (int i = 0; i < r->deny_n; i++) { int ld = snprintf(out + o, cap - o, "%s-%s", first ? "" : ",", r->deny[i]); if (ld > 0 && o + (size_t)ld < cap) o += (size_t)ld; first = 0; }
any = 1;
}
if (!any) { l = snprintf(out + o, cap - o, "(allow)"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; }
l = snprintf(out + o, cap - o, "\n"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
}
static int policy_cmd(int op, const char *entity, const char *arg, char *out, size_t cap)
{
out[0] = 0;
if (op == NET_POLICY_SHOW) {
snprintf(out, cap, "# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n",
g_policy_log_level);
size_t o = strlen(out);
if (entity && entity[0]) {
rule_t *r = policy_find(entity);
policy_print_entity(r, out + o, cap - o);
} else {
for (int i = 0; i < g_rule_n; i++) {
char l[160]; policy_print_entity(&g_rules[i], l, sizeof l);
int n = snprintf(out + o, cap - o, "%s", l);
if (n > 0 && o + (size_t)n < cap) o += (size_t)n;
}
}
return MEOW_OK;
}
if (op == NET_POLICY_SET_NET) {
if (!entity || !entity[0] || !arg || (strcmp(arg, "allow") != 0 && strcmp(arg, "block") != 0))
{ snprintf(out, cap, "policy set 需 <entity> allow|block\n"); return MEOW_ERR_INVAL; }
if (strcmp(arg, "block") == 0 && is_self_lock(entity)) {
snprintf(out, cap, "policy: %s 是防自锁实体(SSH/WiFi/自身命根),拒绝置 block(NETMGR_DESIGN §14 红线)\n", entity);
net_log("warn", "policy", "self-lock entity %s tried block — 拒", entity);
return MEOW_ERR_PERM;
}
rule_t *r = policy_find(entity);
if (!r) {
if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; }
r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r);
snprintf(r->entity, sizeof r->entity, "%s", entity);
}
r->net_block = strcmp(arg, "block") == 0;
r->has_net = 1;
policy_persist_entity(entity);
net_log("info", "policy", "set %s net=%s", entity, arg);
snprintf(out, cap, "policy set %s net=%s ok\n", entity, arg);
return MEOW_OK;
}
if (op == NET_POLICY_SET_DOMAIN) {
if (!entity || !entity[0] || !arg || (arg[0] != '+' && arg[0] != '-') || arg[1] == 0)
{ snprintf(out, cap, "policy domain 需 <entity> +host|-host\n"); return MEOW_ERR_INVAL; }
rule_t *r = policy_find(entity);
if (!r) {
if (g_rule_n >= RULE_ENT_MAX) { snprintf(out, cap, "policy: 规则槽满\n"); return MEOW_ERR_NOMEM; }
r = &g_rules[g_rule_n++]; memset(r, 0, sizeof *r);
snprintf(r->entity, sizeof r->entity, "%s", entity);
}
if (arg[0] == '+') {
if (r->allow_n < RULE_HOST_MAX) { snprintf(r->allow[r->allow_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s +%s ok\n", entity, arg + 1); }
else { snprintf(out, cap, "policy: allow 槽满\n"); return MEOW_ERR_NOMEM; }
} else {
if (r->deny_n < RULE_HOST_MAX) { snprintf(r->deny[r->deny_n++], 48, "%s", arg + 1); snprintf(out, cap, "policy domain %s -%s ok\n", entity, arg + 1); }
else { snprintf(out, cap, "policy: deny 槽满\n"); return MEOW_ERR_NOMEM; }
}
policy_persist_entity(entity);
net_log("info", "policy", "domain %s %s", entity, arg);
return MEOW_OK;
}
if (op == NET_POLICY_DEL) {
if (!entity || !entity[0]) { snprintf(out, cap, "policy del 需 <entity>\n"); return MEOW_ERR_INVAL; }
if (policy_remove(entity) == 0) {
policy_persist_entity(entity);
net_log("info", "policy", "del %s", entity);
snprintf(out, cap, "policy del %s ok\n", entity);
} else {
snprintf(out, cap, "policy del %s: 无此实体\n", entity);
}
return MEOW_OK;
}
snprintf(out, cap, "policy: 未知 op\n");
return MEOW_ERR_INVAL;
}
/* ---------------- 处理单条连接 ---------------- */
static void handle_conn(int c)
{
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 中迭代 `"<key>":"` 开头的扁平条目,取每个对象切片(到 `}`)。首调 *ctx=NULL。 */
static int next_obj(const char *json, const char *key, const char **ctx, char *out, size_t cap)
{
char pat[64];
snprintf(pat, sizeof pat, "\"%s\":\"", key);
const char *p = *ctx ? *ctx : json;
const char *q = strstr(p, pat);
if (!q) { *ctx = NULL; return -1; }
q += strlen(pat);
const char *e = strchr(q, '}');
if (!e) { *ctx = NULL; return -1; }
size_t n = (size_t)(e - q + 1);
if (n >= cap) n = cap - 1;
memcpy(out, q, n); out[n] = 0;
*ctx = e + 1;
return 0;
}
static int cli_status(void)
{
char js[MEOW_IPC_MAX];
if (cli_ipc_json(MSG_NET_STATUS_REQ, NULL, 0, js, sizeof js) != 0) {
fprintf(stderr, "meow_net status: 守护不可达(%s)\n", g_sock);
return 1;
}
char mode[16];
jstr(js, "mode", mode, sizeof mode);
printf("mode=%s src=daemon\n", mode[0] ? mode : "?");
/* ifaces 数组 */
const char *ctx = NULL;
for (;;) {
char tmp[256];
if (next_obj(js, "name", &ctx, tmp, sizeof tmp) != 0) break;
char name[32]; int i = 0;
while (tmp[i] && tmp[i] != '"' && i < 31) { name[i] = tmp[i]; i++; }
name[i] = 0;
char kind[12] = "", ip[40] = "", gw[40] = "";
int up = strstr(tmp, "\"up\":true") != NULL;
jstr(tmp, "kind", kind, sizeof kind);
jstr(tmp, "ip", ip, sizeof ip);
jstr(tmp, "gw", gw, sizeof gw);
printf("iface:%s kind=%s up=%s ip=%s gw=%s\n", name, kind[0] ? kind : "?",
up ? "true" : "false", ip[0] ? ip : "null", gw[0] ? gw : "null");
}
/* wifi */
{
char obj[256];
slice_obj(js, "wifi", obj, sizeof obj);
char ssid[64], wstate[16], wip[40];
int wc = strstr(obj, "\"connected\":true") != NULL;
jstr(obj, "state", wstate, sizeof wstate);
jstr(obj, "ssid", ssid, sizeof ssid);
jstr(obj, "ip", wip, sizeof wip);
int rssi = -1;
jint(obj, "rssi", &rssi);
printf("wifi connected=%d state=%s ssid=%s ip=%s rssi=%d\n",
wc, wstate[0] ? wstate : "unknown", ssid[0] ? ssid : "-",
wip[0] ? wip : "-", rssi);
}
/* bt */
{
char obj[192];
slice_obj(js, "bt", obj, sizeof obj);
char baddr[32];
int bavail = strstr(obj, "\"available\":true") != NULL;
int bpow = strstr(obj, "\"powered\":true") != NULL;
jstr(obj, "address", baddr, sizeof baddr);
int bpaired = -1;
jint(obj, "paired", &bpaired);
printf("bt available=%d powered=%d address=%s paired=%d\n",
bavail, bpow, baddr[0] ? baddr : "-", bpaired);
}
/* rfkill 数组 */
{
const char *rf = strstr(js, "\"rfkill\":[");
if (rf) {
const char *ctx2 = NULL;
for (;;) {
char tmp[96];
if (next_obj(rf, "type", &ctx2, tmp, sizeof tmp) != 0) break;
char type[16]; int i = 0;
while (tmp[i] && tmp[i] != '"' && i < 15) { type[i] = tmp[i]; i++; }
type[i] = 0;
int soft = -1, hard = -1;
jint(tmp, "soft", &soft);
jint(tmp, "hard", &hard);
printf("rfkill:%s soft=%d hard=%d\n", type, soft, hard);
}
}
}
return 0;
}
static int cli_iface(const char *name)
{
char js[MEOW_IPC_MAX];
m_net_iface_req_t q;
memset(&q, 0, sizeof q);
if (name) snprintf(q.name, sizeof q.name, "%s", name);
if (cli_ipc_json(MSG_NET_IFACE_REQ, &q, sizeof q, js, sizeof js) != 0) {
fprintf(stderr, "meow_net iface: 守护不可达(%s)\n", g_sock);
return 1;
}
printf("%s\n", js);
return 0;
}
/* ---- 期2 CLI:conns/apps/top/dns(CONNS_RESULT 逐行 `entity|lip:lport|rip:rport|state|age|domain`) ---- */
static int cli_conns(const char *filter)
{
char js[MEOW_IPC_MAX];
m_net_conns_req_t q;
memset(&q, 0, sizeof q);
if (filter) snprintf(q.entity, sizeof q.entity, "%s", filter);
if (cli_ipc_json(MSG_NET_CONNS_REQ, &q, sizeof q, js, sizeof js) != 0) {
fprintf(stderr, "meow_net conns: 守护不可达(%s)\n", g_sock);
return 1;
}
int n = 0;
char *ln = js;
while (ln && *ln) {
char *nl = strchr(ln, '\n'); if (nl) *nl = 0;
char *f[7]; int nf = 0;
char *save = NULL;
for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t;
if (nf >= 6) {
printf("%-26s %-22s -> %-22s %-9s %5lds %s\n", f[0], f[1], f[2], f[3],
atol(f[4]), f[5]);
n++;
}
if (!nl) break;
ln = nl + 1;
}
printf("(%d connection%s%s)\n", n, n == 1 ? "" : "s", filter && filter[0] ? " filtered" : "");
return 0;
}
typedef struct { char ent[48]; int cnt; long maxage; } ent_grp_t;
/* 从 conns 文本聚合实体统计(apps/top 共用)。 */
static int group_conns(const char *js, ent_grp_t *grp, int max)
{
int gn = 0;
char *ln = (char *)js;
while (ln && *ln && gn < max) {
char *nl = strchr(ln, '\n'); if (nl) *nl = 0;
char *f[7]; int nf = 0;
char *save = NULL;
for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t;
if (nf >= 5 && f[0][0]) {
int gi = -1;
for (int i = 0; i < gn; i++) if (strcmp(grp[i].ent, f[0]) == 0) { gi = i; break; }
if (gi < 0) {
if (gn >= max) continue;
gi = gn++;
snprintf(grp[gi].ent, sizeof grp[gi].ent, "%s", f[0]);
grp[gi].cnt = 0; grp[gi].maxage = 0;
}
grp[gi].cnt++;
long a = atol(f[4]);
if (a > grp[gi].maxage) grp[gi].maxage = a;
}
if (!nl) break;
ln = nl + 1;
}
/* 按 cnt 降序 */
for (int i = 0; i < gn; i++)
for (int j = i + 1; j < gn; j++)
if (grp[j].cnt > grp[i].cnt) {
ent_grp_t t = grp[i]; grp[i] = grp[j]; grp[j] = t;
}
return gn;
}
static int cli_apps(void)
{
char js[MEOW_IPC_MAX];
if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) {
fprintf(stderr, "meow_net apps: 守护不可达(%s)\n", g_sock);
return 1;
}
ent_grp_t grp[64];
int gn = group_conns(js, grp, 64);
printf("%-26s %6s %8s\n", "entity", "conns", "max_age");
for (int i = 0; i < gn; i++)
printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage);
return 0;
}
static volatile sig_atomic_t g_cli_stop = 0;
static void cli_sigint(int sig) { (void)sig; g_cli_stop = 1; }
static int cli_top(void)
{
g_cli_stop = 0;
signal(SIGINT, cli_sigint);
while (!g_cli_stop) {
char js[MEOW_IPC_MAX];
if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) {
fprintf(stderr, "meow_net top: 守护不可达(%s)\n", g_sock);
return 1;
}
ent_grp_t grp[64];
int gn = group_conns(js, grp, 64);
printf("\033[2J\033[H top 连接实体(2s 刷新,Ctrl-C 退)\n");
printf("%-26s %6s %8s\n", "entity", "conns", "max_age");
for (int i = 0; i < gn && i < 10; i++)
printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage);
for (int i = 0; i < 10 - gn && i < 10; i++) printf("\n");
fflush(stdout);
sleep(2);
}
printf("\n");
return 0;
}
static int cli_dns(const char *cmd)
{
char out[MEOW_IPC_MAX - 8];
int code = cli_ipc_cmd(MSG_NET_DNS_CTL, cmd ? cmd : "status", NULL, out, sizeof out);
printf("%s\n", out);
return code == MEOW_OK ? 0 : 1;
}
/* 期3 policy:show/set net/domain/del(IPC 0xEA;写级在服务端鉴权系统身份)。 */
static int cli_policy(int op, const char *entity, const char *arg)
{
m_net_policy_t q;
memset(&q, 0, sizeof q);
q.op = (uint32_t)op;
if (entity) snprintf(q.entity, sizeof q.entity, "%s", entity);
if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg);
char out[MEOW_IPC_MAX - 8];
int fd = ipc_connect();
if (fd < 0) { fprintf(stderr, "meow_net 守护不可达(%s)\n", g_sock); return 1; }
uint16_t rt; uint32_t code;
int rc = ipc_xchg(fd, MSG_NET_POLICY, &q, sizeof q, &rt, &code, out, sizeof out);
close(fd);
if (rc != 0) { fprintf(stderr, "meow_net policy: IPC 往返失败\n"); return 1; }
printf("%s", out);
return code == MEOW_OK ? 0 : 1;
}
static const char *usage(void)
{
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off"
" | bt status|on|off|paired | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
" | apps | conns [entity] | top | dns status|stats|flush"
" | policy show [entity] | policy set <entity> allow|block | policy domain <entity> +host|-host | policy del <entity>"
" | 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 <entity> allow|block | domain <entity> +host|-host | del <entity>)\n"); return 1; }
if (!strcmp(arg, "show")) return cli_policy(NET_POLICY_SHOW, arg2, NULL);
if (!strcmp(arg, "set")) {
if (!arg2 || !arg3) { fprintf(stderr, "policy set 需 <entity> allow|block\n"); return 1; }
return cli_policy(NET_POLICY_SET_NET, arg2, arg3);
}
if (!strcmp(arg, "domain")) {
if (!arg2 || !arg3) { fprintf(stderr, "policy domain 需 <entity> +host|-host\n"); return 1; }
return cli_policy(NET_POLICY_SET_DOMAIN, arg2, arg3);
}
if (!strcmp(arg, "del")) {
if (!arg2) { fprintf(stderr, "policy del 需 <entity>\n"); return 1; }
return cli_policy(NET_POLICY_DEL, arg2, NULL);
}
fprintf(stderr, "policy 子命令未知(%s)\n", arg);
return 1;
}
if (!strcmp(cmd, "wifi")) {
if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; }
if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
if (!strcmp(arg, "connect")) {
if (!arg2) { fprintf(stderr, "connect 需 <ssid>[,psk]\n"); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", arg2);
}
if (!strcmp(arg, "forget")) {
if (!arg2) { fprintf(stderr, "forget 需 <network_id>\n"); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "forget", arg2);
}
return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
}
if (!strcmp(cmd, "bt")) {
if (!arg) { fprintf(stderr, "bt 子命令缺失(%s)\n", usage()); return 1; }
return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL);
}
if (!strcmp(cmd, "rfkill")) {
if (!arg) return cli_status(); /* 读 rfkill 表 → status JSON 的 rfkill[] */
if (!arg2) { fprintf(stderr, "rfkill 需 <type> <on|off>\n"); return 1; }
char combined[64];
snprintf(combined, sizeof combined, "%s %s", arg, arg2);
return cli_cmd_result(MSG_NET_WIFI_CTL, "rfkill", combined);
}
if (!strcmp(cmd, "udhcpc")) {
if (!arg) { fprintf(stderr, "udhcpc 需 renew|release [if]\n"); return 1; }
char combined[64];
snprintf(combined, sizeof combined, "%s %s", arg, arg2 ? arg2 : "");
return cli_cmd_result(MSG_NET_WIFI_CTL, "udhcpc", combined);
}
fprintf(stderr, "usage: meow_net <%s>\n", usage());
return 1;
}
/* ---------------- main:单一二进制双形态 ---------------- */
int main(int argc, char **argv)
{
const char *e;
if ((e = getenv("MEOW_NET_DIR")) && e[0]) snprintf(g_net_dir, sizeof g_net_dir, "%s", e);
if ((e = getenv("MEOW_NETDEV")) && e[0]) snprintf(g_netdev, sizeof g_netdev, "%s", e);
if ((e = getenv("MEOW_NET_ROUTE")) && e[0]) snprintf(g_net_route, sizeof g_net_route, "%s", e);
if ((e = getenv("MEOW_FIBTRIE")) && e[0]) snprintf(g_fibtrie, sizeof g_fibtrie, "%s", e);
if ((e = getenv("MEOW_RESOLV")) && e[0]) snprintf(g_resolv, sizeof g_resolv, "%s", e);
if ((e = getenv("MEOW_RFKILL_DIR")) && e[0]) snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s", e);
if ((e = getenv("MEOW_BT_DIR")) && e[0]) snprintf(g_bt_dir, sizeof g_bt_dir, "%s", e);
if ((e = getenv("MEOW_BOOT_MODE")) && e[0]) snprintf(g_boot_mode, sizeof g_boot_mode, "%s", e);
if ((e = getenv("MEOW_LOG_DIR")) && e[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", e);
if ((e = getenv("MEOW_BOOTTS_CACHE")) && e[0]) snprintf(g_bootts_cache, sizeof g_bootts_cache, "%s", e);
if ((e = getenv("MEOW_WPA_CLI")) && e[0]) g_wpa_cli = e;
if ((e = getenv("MEOW_HCICONFIG")) && e[0]) g_hciconfig = e;
if ((e = getenv("MEOW_BLUETOOTHCTL")) && e[0]) g_bluetoothctl = e;
if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e;
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e);
if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e);
if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e);
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e);
if ((e = getenv("MEOW_SVCD_SOCK")) && e[0]) snprintf(g_svcd_sock, sizeof g_svcd_sock, "%s", e);
if ((e = getenv("MEOW_SVCD_STATUS")) && e[0]) snprintf(g_svcd_status, sizeof g_svcd_status, "%s", e);
if ((e = getenv("MEOW_NET_POLICY")) && e[0]) snprintf(g_net_policy, sizeof g_net_policy, "%s", e);
if ((e = getenv("MEOW_DNS_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_port = atoi(e);
if ((e = getenv("MEOW_DNS_UPSTREAM_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_upstream_port = atoi(e);
if ((e = getenv("MEOW_DNS_DISABLE")) && atoi(e) > 0) g_dns_disabled = 1;
int i = 1;
for (; i < argc; i++) {
if (!strcmp(argv[i], "--sock") && i + 1 < argc) g_sock = argv[++i];
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
printf("meow_netmgrd / meow_net (%s)\n", usage());
return 0;
}
else break;
}
/* argv[0] basename=meow_net → 强制 CLI(双形态);meow_netmgrd 空参 → 守护常驻。 */
const char *base = strrchr(argv[0], '/');
base = base ? base + 1 : argv[0];
if (!strcmp(base, "meow_net")) {
if (i >= argc) return cli_help();
return cli_run(argc - i, argv + i);
}
if (i >= argc) return run_daemon(g_sock); /* svcd 空参 → IPC 常驻 */
return cli_run(argc - i, argv + i);
}