Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/meow_netmgrd.c
T
MeowOS 30e2ec901f meow_netmgrd 期2.1 修复:status JSON 漏闭 bt 对象(/api/net/status res.json() 必炸)
根因(patch_v2/meow_netmgrd.c build_status_json):bt 对象开 '{' 后,dns 数组之后漏收 '}',
导致 rfkill/rate 被吞进 bt、bt 与 root 均不闭合 → 外层 data 对象收尾只剩 3 个 '}',JSON 非法,
面板 /api/net/status 纯透传 MSG_NET_STATUS_RESULT(0xE0) 后 res.json() 失败。

修法:bt 的 dns 是末字段,收尾改为 ']},'(数组闭 + bt 闭 + 逗号)——不应含尾逗号
(首版 '],},' 产生 "dns":[...],} 尾逗号 JSON 非法,一并修正)。

顺手核查:build_iface_json(0xE2 单/全接口)括号配对正确、JSON 合法;conns/dns 为文本表非 JSON,不受影响。

加固:test_netmgrd.sh 增 T13b-e 端到端断言 STATUS(0xE0)/IFACE_ALL(0xE2)/IFACE_WLAN(0xE2) 的原始 JSON
可被 json.loads 完整解析(括号配对/无尾逗号/唯一有效对象)。

验证:宿主 test_netmgrd.sh 52/52、qemu-mipsel 52/52、test_netmgrd_p2(宿主+qemu) 全过、netmgrd_authtest 全过;
交叉 -Wall -Wextra 0 warning。新 meow_netmgrd md5 abfb97680b4f576f3669a3e4b3b29134(主会话热更设备)。
2026-09-01 21:24:35 +08:00

2794 lines
108 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 "meow_ipc.h" /* 帧格式/消息号 + MSG_NET_*(0x00E0-0x00ED) + m_net_*(号段 0x00E0-0x00EF) */
/* ---------------- 路径(全部可经 env 覆盖,宿主/qemu 桩测试用) ---------------- */
static char g_net_dir[128] = "/sys/class/net"; /* MEOW_NET_DIR */
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]= "/usr/data/meow/net_policy.conf"; /* MEOW_NET_POLICY(期2 只读静态域名规则) */
#define NET_POLL_SEC_DEF 2 /* 接口轮询/状态节拍(秒;MEOW_NET_POLL_SEC 覆盖,测试用) */
#define RUNCMD_TMO_MS 1500 /* 后端命令(wpa_cli/hciconfig/etc)有界收割限时(ms) */
#define IFACE_MAX 16
#define RFKILL_MAX 8
#define DNS_MAX 8
/* ---- 期 2 常量(NETMGR_DESIGN §4/§5/§8;全部静态表,禁 malloc) ---- */
#define CONN_MAX 512 /* 连接表上限 */
#define DNS_CACHE_MAX 256 /* DNS 应答缓存上限 */
#define DNS_UPSTREAM_TMO_MS 800 /* DNS 上游转发超时 */
#define DNS_QNAME_MAX 128 /* query 域名最大字节 */
#define ENTITY_MAX 128 /* pid→entity 缓存条数 */
#define PORT_ENT_MAX 128 /* udp 本地端口→entity 缓存(DNS 归因用) */
#define RULE_ENT_MAX 16 /* 域名规则实体上限 */
#define RULE_HOST_MAX 24 /* 单实体 allow/deny 主机规则上限 */
#define EVT_DUP_MAX 64 /* 事件日志 1 分钟去重表 */
#define DNS_RESOLV_CHECK_SEC 5 /* resolv.conf 自检周期 */
#define DNS_PORT_DEF 53 /* 监听端口(MEOW_DNS_PORT 覆盖;宿主测试用高位规避 <1024 权限) */
#define DNS_SELF_IP "127.0.0.1"
static int g_poll_sec = NET_POLL_SEC_DEF;
static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
static long mono_now(void)
{
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];
static int g_iface_n = 0;
typedef struct {
int connected; /* 0/1 */
char state[16]; /* wpa_state 值;""=unknown */
char ssid[64];
char ip[40];
int rssi; /* 信号强度 dBm(可为负);has_rssi=0 时无效 */
int has_rssi; /* wpa_cli 是否给出 signal= */
} wifi_t;
typedef struct {
int available;
int powered;
char address[32];
int paired_n; /* -1=null */
} bt_t;
typedef struct {
char type[16]; /* wlan/bluetooth/... */
int soft; /* 0/1/-1 */
int hard; /* 0/1/-1 */
} rfkill_t;
static rfkill_t g_rfkill[RFKILL_MAX];
static int boot_mode_is_meow(void)
{
char buf[16] = "";
if (read_file_txt(g_boot_mode, buf, sizeof buf) <= 0) return 0; /* 缺失=只读等落地 */
return strcmp(buf, "meow") == 0;
}
/* kind 判定:0=lo 1=wifi 2=usb-eth(热插拔出现即记录)。 */
static int iface_parse_kind(const char *name)
{
if (strcmp(name, "lo") == 0) return 0;
if (strcmp(name, g_wifi_if) == 0) return 1;
return 2;
}
static const char *kind_str(int k)
{
return k == 0 ? "lo" : (k == 1 ? "wifi" : "usb-eth");
}
/* /proc/net/dev 单接口行 → rx(列1)/tx(列9) 累计字节;返回 0=找到。 */
static int netdev_iface(const char *ifname, long *rx, long *tx)
{
FILE *f = fopen(g_netdev, "r");
if (!f) return -1;
char line[256];
int found = -1;
while (fgets(line, sizeof line, f)) {
char *c = strchr(line, ':');
if (!c) continue;
*c = 0;
char *n = line;
while (*n == ' ' || *n == '\t') n++;
if (strcmp(n, ifname) != 0) continue;
char *save = NULL;
char *tk = strtok_r(c + 1, " \t", &save); /* 列1 = rx bytes */
long vr = tk ? strtol(tk, NULL, 10) : 0;
for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); /* 推进到列9 */
long vt = tk ? strtol(tk, NULL, 10) : 0;
if (rx) *rx = vr;
if (tx) *tx = vt;
found = 0;
break;
}
fclose(f);
return found;
}
/* 全局 /proc/net/dev RX/TX 累计(所有接口求和)。 */
static void netdev_totals(long *rx, long *tx)
{
long trx = 0, ttx = 0;
FILE *f = fopen(g_netdev, "r");
if (f) {
char line[256];
while (fgets(line, sizeof line, f)) {
char *c = strchr(line, ':');
if (!c) continue;
char *save = NULL;
char *tk = strtok_r(c + 1, " \t", &save);
long vr = 0;
if (tk) {
vr = strtol(tk, NULL, 10);
for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save);
if (tk) ttx += strtol(tk, NULL, 10);
}
trx += vr;
}
fclose(f);
}
if (rx) *rx = trx;
if (tx) *tx = ttx;
}
/* iface IP:优先 `ip -o -4 addr show dev <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;
}
}
/* ---------------- 蓝牙:hciconfig/bluetoothctl 包装 ---------------- */
static void bt_status(bt_t *b)
{
memset(b, 0, sizeof *b);
b->paired_n = -1;
char p[192];
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
if (access(p, F_OK) == 0) b->available = 1;
if (!b->available) return;
/* powered:/sys/.../power(on/off)优先,回退 hciconfig 行首 UP/DOWN */
snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir);
char pw[8] = "";
if (read_file_txt(p, pw, sizeof pw) > 0 && pw[0]) {
b->powered = !strcmp(pw, "on");
} else {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
char buf[256];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
char *ln = buf;
while (ln && *ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
char *q = ln;
while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "UP", 2)) { b->powered = 1; break; }
if (!strncmp(q, "DOWN", 4)) { b->powered = 0; break; }
if (!nl) break;
ln = nl + 1;
}
}
}
/* address:/sys/.../address 优先,回退 hciconfig "BD Address:" */
snprintf(p, sizeof p, "%s/hci0/address", g_bt_dir);
read_file_txt(p, b->address, sizeof b->address);
if (!b->address[0]) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
char buf[256];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
char *q = strstr(buf, "BD Address:");
if (q) {
q += 11;
while (*q == ' ') q++;
int i = 0;
while (*q && *q != ' ' && *q != '\n' && i < (int)sizeof b->address - 1) b->address[i++] = *q++;
b->address[i] = 0;
}
}
}
/* paired 数:bluetoothctl paired(BlueZ 5.54);失败/空 → -1。 */
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired 2>/dev/null", g_bluetoothctl);
char buf[512];
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
int n = 0;
char *ln = buf;
while (ln && *ln) {
char *nl = strchr(ln, '\n');
if (nl) *nl = 0;
char *q = ln;
while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "Device", 6)) n++;
if (!nl) break;
ln = nl + 1;
}
b->paired_n = n;
}
}
/* ---------------- rfkill:统一软开关 ---------------- */
static void rfkill_list(void)
{
int n = 0;
DIR *d = opendir(g_rfkill_dir);
if (d) {
struct dirent *de;
while ((de = readdir(d)) && n < RFKILL_MAX) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char p[448];
rfkill_t *r = &g_rfkill[n];
memset(r, 0, sizeof *r);
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
read_file_txt(p, r->type, sizeof r->type);
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
r->soft = read_file_int_def(p, -1);
snprintf(p, sizeof p, "%s/%s/hard", g_rfkill_dir, de->d_name);
r->hard = read_file_int_def(p, -1);
n++;
}
closedir(d);
}
for (int i = n; i < RFKILL_MAX; i++) memset(&g_rfkill[i], 0, sizeof(rfkill_t));
}
/* 找某类型 rfkill 设备并写 soft=block(1)/unblock(0)。返回 0=ok;-1=无该类型/写失败。 */
static int rfkill_set_block(const char *type, int block)
{
char norm[16] = "";
if (!strcmp(type, "wifi")) snprintf(norm, sizeof norm, "%s", "wlan");
else if (!strcmp(type, "bt")) snprintf(norm, sizeof norm, "%s", "bluetooth");
else snprintf(norm, sizeof norm, "%s", type);
DIR *d = opendir(g_rfkill_dir);
int rc = -1;
if (d) {
struct dirent *de;
while ((de = readdir(d))) {
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
char p[448], t[16] = "";
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
if (read_file_txt(p, t, sizeof t) <= 0 || strcmp(t, norm) != 0) continue;
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
int fd = open(p, O_WRONLY);
if (fd < 0) continue;
char one = block ? '1' : '0';
if (write(fd, &one, 1) == 1) rc = 0;
close(fd);
break;
}
closedir(d);
}
return rc;
}
/* ---------------- udhcpc:续租/释放(SIGUSR1/SIGUSR2,不改启动路径) ---------------- */
static int udhcpc_ctl(const char *op, const char *ifname)
{
char pidfile[160];
const char *iff = (ifname && ifname[0]) ? ifname : g_wifi_if;
snprintf(pidfile, sizeof pidfile, "/var/run/udhcpc.%s.pid", iff);
char buf[32];
if (read_file_txt(pidfile, buf, sizeof buf) <= 0) return -1;
int pid = atoi(buf);
if (pid <= 0) return -1;
int sig = (strcmp(op, "release") == 0) ? SIGUSR2 : SIGUSR1;
return kill(pid, sig) == 0 ? 0 : -1;
}
/* ---------------- IPC 帧读写(同 svc_power 约定,全小端) ---------------- */
static int x_read_full(int fd, uint8_t *buf, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, buf + off, n - off);
if (r <= 0) return -1;
off += (size_t)r;
}
return 0;
}
static int x_write_full(int fd, const uint8_t *buf, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, buf + off, n - off);
if (w <= 0) return -1;
off += (size_t)w;
}
return 0;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &rlen, 4);
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
return 0;
}
static int send_ack(int fd, uint32_t req_id, uint32_t code, uint32_t status)
{
m_ack_t a;
memset(&a, 0, sizeof a);
a.code = code;
a.status = status;
return send_frame(fd, MSG_ACK, req_id, &a, sizeof a);
}
static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg)
{
m_err_t e;
memset(&e, 0, sizeof e);
e.code = code;
uint16_t ml = msg ? (uint16_t)strlen(msg) : 0;
if (ml > 63) ml = 63;
e.msg_len = ml;
if (ml) memcpy(e.msg, msg, ml);
return send_frame(fd, MSG_ERR, req_id, &e, sizeof e);
}
/* json/text 结果通发(m_net_status_result_t / m_net_iface_result_t / m_net_cmd_result_t 布局同构
* code(4)+len(2)+pad(2)+payload,payload ≤ MEOW_IPC_MAX-8)。 */
static int send_payload_result(int fd, uint16_t type, uint32_t req_id, uint32_t code,
const char *payload, size_t pl)
{
m_net_cmd_result_t r; /* 复用布局最大的那个(payload 区 4088B)。 */
memset(&r, 0, sizeof r);
r.code = code;
if (pl > sizeof r.text - 1) pl = sizeof r.text - 1;
r.text_len = (uint16_t)pl;
memcpy(r.text, payload, pl);
r.text[pl] = 0;
return send_frame(fd, type, req_id, &r, (uint32_t)(8 + pl + 1));
}
static int send_net_status_result(int fd, uint32_t req_id, const char *json)
{
return send_payload_result(fd, MSG_NET_STATUS_RESULT, req_id, MEOW_OK, json, strlen(json));
}
static int send_net_iface_result(int fd, uint32_t req_id, uint32_t code, const char *json)
{
return send_payload_result(fd, MSG_NET_IFACE_RESULT, req_id, code, json, strlen(json));
}
static int send_net_wifi_result(int fd, uint32_t req_id, const ctl_result_t *r)
{
return send_payload_result(fd, MSG_NET_WIFI_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
}
static int send_net_bt_result(int fd, uint32_t req_id, const ctl_result_t *r)
{
return send_payload_result(fd, MSG_NET_BT_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
}
/* ---------------- 守护进程态 ---------------- */
static int g_mode_meow = 0; /* meow=1 全功能;0=只读降级 */
static long g_net_last_rx = -1, g_net_last_tx = 0, g_net_last_mono = 0;
static long g_net_rate_rx = -1, g_net_rate_tx = -1; /* B/s;-1=未知 */
static char g_prev_if[IFACE_MAX][32];
static int g_prev_if_up[IFACE_MAX];
static int g_prev_if_n = 0;
/* ---------------- JSON 构建:status/iface ---------------- */
static void iface_json_obj(jw_t *w, const iface_t *f)
{
jadd(w, "{\"name\":\"%s\",\"kind\":\"%s\",\"up\":%s,", f->name, f->kind,
f->up ? "true" : "false");
jadd_str_or_null(w, "ip", f->ip);
jadd_str_or_null(w, "gw", f->gw);
jadd(w, "\"rx_bytes\":%ld,\"tx_bytes\":%ld}", f->rx, f->tx);
}
static void build_status_json(char *out, size_t cap)
{
jw_t w = { out, cap, 0 };
out[0] = 0;
jadd(&w, "{\"mode\":\"%s\",\"boot_ts\":\"", g_mode_meow ? "meow" : "readonly");
{
char ts[64]; get_bootts(ts, sizeof ts);
for (const char *p = ts; *p; p++) w.buf[w.off++] = *p;
}
jadd(&w, "\",\"ts\":%ld,", (long)time(NULL));
enum_ifaces(1);
jadd(&w, "\"ifaces\":[");
for (int i = 0; i < g_iface_n; i++) { if (i) jadd(&w, ","); iface_json_obj(&w, &g_ifaces[i]); }
jadd(&w, "],");
wifi_t wi;
wifi_status(&wi);
jadd(&w, "\"wifi\":{");
jadd_q(&w, "state", wi.state);
jadd(&w, "\"connected\":%s,", wi.connected ? "true" : "false");
jadd_str_or_null(&w, "ssid", wi.ssid);
jadd_str_or_null(&w, "ip", wi.ip);
if (wi.has_rssi) jadd(&w, "\"rssi\":%d,", wi.rssi);
else jadd(&w, "\"rssi\":null,");
jadd(&w, "\"networks\":null},");
bt_t bt;
bt_status(&bt);
jadd(&w, "\"bt\":{");
jadd(&w, "\"available\":%s,\"powered\":%s,", bt.available ? "true" : "false",
bt.powered ? "true" : "false");
jadd_str_or_null(&w, "address", bt.address);
jadd_int_or_null(&w, "paired", bt.paired_n);
jadd(&w, "\"dns\":[");
{
char ns[DNS_MAX][40];
int n = 0;
dns_read(ns, &n);
for (int i = 0; i < n; i++) { if (i) jadd(&w, ","); jadd(&w, "\"%s\"", ns[i]); }
}
jadd(&w, "]},"); /* 关闭 dns[] + bt 对象 + 逗号(dns 是 bt 末字段,不能留尾逗号;此前漏收 bt 致 rfkill/rate 被吞、root 不闭合) */
rfkill_list();
jadd(&w, "\"rfkill\":[");
for (int i = 0; i < RFKILL_MAX && g_rfkill[i].type[0]; i++) {
if (i) jadd(&w, ",");
jadd(&w, "{\"type\":\"%s\",\"soft\":%d,\"hard\":%d}", g_rfkill[i].type,
g_rfkill[i].soft, g_rfkill[i].hard);
}
jadd(&w, "],");
jadd(&w, "\"rate\":{\"rx_bps\":");
if (g_net_rate_rx < 0) jadd(&w, "null,");
else jadd(&w, "%ld,", g_net_rate_rx);
jadd(&w, "\"tx_bps\":");
if (g_net_rate_tx < 0) jadd(&w, "null}}");
else jadd(&w, "%ld}}", g_net_rate_tx);
}
/* 单/全接口详情 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;
} rule_t;
static rule_t g_rules[RULE_ENT_MAX];
static int g_rule_n = 0;
typedef struct { char key[96]; long mono; } evt_dup_t;
static evt_dup_t g_evt_dup[EVT_DUP_MAX];
static int g_evt_dup_n = 0;
/* ---- DNS 代理状态 ---- */
static int g_dns_enabled = 0;
static int g_dns_modified_resolv = 0; /* resolv.conf 已被改写(需恢复) */
static int g_dns_fd = -1; /* 127.0.0.1:port UDP 监听 */
static int g_dns_up_fd = -1; /* 上游转发 socket */
static char g_dns_upstream[40] = ""; /* 原上游 nameserver IP */
static int g_dns_upstream_port = 53; /* 上游端口(MEOW_DNS_UPSTREAM_PORT,宿主测试用) */
static char g_resolv_backup[384] = ""; /* 原 resolv.conf 内容 */
static uint64_t g_dns_q = 0, g_dns_deny = 0, g_dns_first = 0;
static long g_last_resolv_check = 0;
static volatile sig_atomic_t g_hup = 0;
/* ---- 通用工具 ---- */
static void ip_fmt(uint32_t ip, char *out, size_t cap)
{
snprintf(out, cap, "%u.%u.%u.%u", (ip >> 24) & 0xff, (ip >> 16) & 0xff,
(ip >> 8) & 0xff, ip & 0xff);
}
/* /proc/net/{tcp,udp} 的十六进制地址 → dotted-printable uint32(LE 字节序还原)。 */
static uint32_t hex_ip(const char *hex)
{
uint32_t v = (uint32_t)strtoul(hex, NULL, 16);
uint32_t b0 = v & 0xff, b1 = (v >> 8) & 0xff, b2 = (v >> 16) & 0xff, b3 = (v >> 24) & 0xff;
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
}
static const char *tcp_state_str(uint8_t s)
{
switch (s) {
case 0x01: return "ESTAB";
case 0x02: return "SYN_SENT";
case 0x03: return "SYN_RECV";
case 0x04: return "FIN_WAIT1";
case 0x05: return "FIN_WAIT2";
case 0x06: return "TIME_WAIT";
case 0x07: return "CLOSE";
case 0x08: return "CLOSE_WAIT";
case 0x09: return "LAST_ACK";
case 0x0A: return "LISTEN";
case 0x0B: return "CLOSING";
default: return "??";
}
}
/* AF_UNIX 文本请求/响应(svcd/amsd 用;send 后 shutdown(SHUT_WR) 触发服务端 EOF 处理,读至对端关闭)。 */
static int sock_xchg_text(const char *sockpath, const char *send, char *resp, int cap)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct timeval tv = { 1, 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, sockpath, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
if (send && write(fd, send, strlen(send)) < 0) { close(fd); return -1; }
shutdown(fd, SHUT_WR); /* 通知服务端 EOF(amsd conn_finish 才响应) */
int n = 0;
while (n < cap - 1) {
ssize_t r = read(fd, resp + n, (size_t)(cap - 1 - n));
if (r <= 0) break;
n += (int)r;
}
close(fd);
if (n > 0) resp[n] = 0; else resp[0] = 0;
return n > 0 ? 0 : -1;
}
/* ---- DNS 缓存(≤256,TTL 尊重) ---- */
static int dns_cache_insert(const char *name, uint32_t ip, uint16_t ttl)
{
long now = mono_now();
long exp = now + (ttl ? ttl : 60);
if (exp - now > 86400) exp = now + 86400;
for (int i = 0; i < g_dns_n; i++) {
if (strcmp(g_dns[i].name, name) == 0) { g_dns[i].ip = ip; g_dns[i].exp = exp; return 0; }
}
/* 找空槽或最旧 */
int slot = -1; long oldest = 0;
for (int i = 0; i < DNS_CACHE_MAX; i++) {
if (g_dns[i].name[0] == 0) { slot = i; break; }
if (g_dns[i].exp < oldest) { oldest = g_dns[i].exp; slot = i; }
}
if (slot < 0) slot = g_dns_n < DNS_CACHE_MAX ? g_dns_n++ : 0;
if (g_dns_n < DNS_CACHE_MAX && slot == g_dns_n) g_dns_n++;
snprintf(g_dns[slot].name, sizeof g_dns[slot].name, "%s", name);
g_dns[slot].ip = ip; g_dns[slot].ttl = ttl; g_dns[slot].exp = exp;
return 0;
}
static int dns_cache_lookup_ip(uint32_t ip, char *out, size_t cap)
{
long now = mono_now();
for (int i = 0; i < g_dns_n; i++) {
if (g_dns[i].name[0] && g_dns[i].ip == ip && g_dns[i].exp > now) {
if (out && cap) snprintf(out, cap, "%s", g_dns[i].name);
return 0;
}
}
return -1;
}
static int dns_cache_lookup_name(const char *name)
{
long now = mono_now();
for (int i = 0; i < g_dns_n; i++)
if (strcmp(g_dns[i].name, name) == 0 && g_dns[i].exp > now) return 0;
return -1;
}
static void dns_cache_flush(void)
{
for (int i = 0; i < g_dns_n; i++) g_dns[i].name[0] = 0;
g_dns_n = 0;
}
/* ---- 域名规则(期2 静态 conf 只读;期3 策略引擎再接管) ---- */
static int suffix_match(const char *host, const char *rule)
{
if (!host[0] || !rule[0]) return 0;
size_t hl = strlen(host), rl = strlen(rule);
if (hl < rl) return 0;
if (strcmp(host + hl - rl, rule) != 0) return 0;
return hl == rl || host[hl - rl - 1] == '.';
}
static int entity_rules_match(const rule_t *r, const char *entity)
{
return r->entity[0] && strcmp(r->entity, entity) == 0;
}
/* deny 判定:命中最具体的实体规则(entity+global),allow 覆盖 deny;默认 allow。 */
static int dns_query_domain_denied(const char *entity, const char *domain)
{
const rule_t *g = NULL, *e = NULL;
for (int i = 0; i < g_rule_n; i++) {
if (strcmp(g_rules[i].entity, "global") == 0) g = &g_rules[i];
else if (!e && entity_rules_match(&g_rules[i], entity)) e = &g_rules[i];
}
const rule_t *r = e ? e : g;
if (r && r->deny_n) {
for (int i = 0; i < r->deny_n; i++)
if (suffix_match(domain, r->deny[i])) {
/* allow 白名单可覆盖 */
for (int j = 0; j < r->allow_n; j++)
if (suffix_match(domain, r->allow[j])) return 0;
return 1;
}
}
return 0;
}
static void rules_load(void)
{
g_rule_n = 0;
FILE *f = fopen(g_net_policy, "r");
if (!f) return;
char line[256];
while (fgets(line, sizeof line, f) && g_rule_n < RULE_ENT_MAX) {
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\n' || !*p) continue;
char *nl = strchr(p, '\n'); if (nl) *nl = 0;
char *sp = strchr(p, ' ');
char *tab = strchr(p, '\t');
if (!sp || (tab && tab < sp)) sp = tab;
char *entity; char *rest;
if (sp) { *sp = 0; entity = p; rest = sp + 1; }
else continue;
rule_t *r = &g_rules[g_rule_n];
memset(r, 0, sizeof *r);
snprintf(r->entity, sizeof r->entity, "%s", entity);
/* rest 形如 "domain=+a,-b,c" 或 "net=block";逐 token 解析 */
char *save = NULL;
for (char *tok = strtok_r(rest, " \t", &save); tok; tok = strtok_r(NULL, " \t", &save)) {
if (strncmp(tok, "domain=", 7) != 0) continue;
char *sub = tok + 7;
char *ssave = NULL;
for (char *h = strtok_r(sub, ",", &ssave); h; h = strtok_r(NULL, ",", &ssave)) {
if (h[0] == '+') {
if (r->allow_n < RULE_HOST_MAX) snprintf(r->allow[r->allow_n++], 48, "%s", h + 1);
} else if (h[0] == '-') {
if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h + 1);
} else if (h[0]) {
if (r->deny_n < RULE_HOST_MAX) snprintf(r->deny[r->deny_n++], 48, "%s", h);
}
}
}
if (r->deny_n || r->allow_n) g_rule_n++;
}
fclose(f);
}
/* ---- 实体归因:pid→(app:<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);
}
/* ---- 连接普查(每 2s):/proc/net/{tcp,udp} → inode→pid→实体 → 连接表 + 事件 ---- */
static void census_run(void)
{
refresh_entity_maps();
static conn_t cur[CONN_MAX];
int cn = 0;
parse_proc_net(g_proc_tcp, 0, cur, &cn);
parse_proc_net(g_proc_udp, 1, cur, &cn);
for (int i = 0; i < cn; i++) { cur[i].pid = 0; cur[i].entity[0] = 0; }
attribute_inodes(cur, cn);
for (int i = 0; i < cn; i++) {
if (cur[i].inode && !cur[i].entity[0]) snprintf(cur[i].entity, sizeof cur[i].entity, "proc:unknown");
if (cur[i].inode && cur[i].remote_ip != 0)
dns_cache_lookup_ip(cur[i].remote_ip, cur[i].domain, sizeof cur[i].domain);
}
build_port_entity_map(cur, cn);
int prev_n = g_conn_n;
g_gen++;
g_conn_n = 0;
long now = mono_now();
for (int i = 0; i < cn; i++) {
conn_t *c = &cur[i];
/* 是否延续既有连接(按 inode)?→ 保 open 时间;否则新连接 → 日志。 */
int pi = -1;
for (int j = 0; j < prev_n; j++) if (g_conn[j].inode == c->inode && g_conn[j].pid) { pi = j; break; }
if (pi >= 0) {
c->mono_open = g_conn[pi].mono_open;
} else {
c->mono_open = now;
if (c->inode && c->remote_port != 0) { /* 服务端监听口(remote=0/0)不记 open */
char dst[64], ip[32];
ip_fmt(c->remote_ip, ip, sizeof ip);
snprintf(dst, sizeof dst, "%s:%u", ip, c->remote_port);
evt_conn_log(c->entity[0] ? c->entity : "proc:unknown", dst, "open");
}
}
c->seen_gen = g_gen;
c->last_seen = now;
if (g_conn_n < CONN_MAX) g_conn[g_conn_n++] = *c;
}
/* 检测关闭:prev 有、本次无 → 日志 close + 移除。 */
for (int j = 0; j < prev_n; j++) {
int found = 0;
for (int i = 0; i < g_conn_n; i++) if (g_conn[i].inode == g_conn[j].inode) { found = 1; break; }
if (!found && g_conn[j].inode && g_conn[j].remote_port != 0) {
char dst[64], ip[32];
ip_fmt(g_conn[j].remote_ip, ip, sizeof ip);
snprintf(dst, sizeof dst, "%s:%u", ip, g_conn[j].remote_port);
evt_conn_log(g_conn[j].entity[0] ? g_conn[j].entity : "proc:unknown", dst, "close");
}
}
}
/* ---- DNS 代理(§5;三条兜底:双行 resolv + atexit 恢复 + 5s 自检) ---- */
static int resolv_read_file(char *buf, int cap)
{
int fd = open(g_resolv, O_RDONLY);
if (fd < 0) return -1;
int n = 0;
while (n < cap - 1) {
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
if (r <= 0) break;
n += (int)r;
}
close(fd);
buf[n] = 0;
return n;
}
static int resolv_write_file(const char *buf)
{
int fd = open(g_resolv, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return -1;
ssize_t w = write(fd, buf, strlen(buf));
close(fd);
return w > 0 ? 0 : -1;
}
static void resolv_backup_apply(void)
{
/* 读当前 resolv.conf,取首个 nameserver 为上游。若已是我们双行形式则跳过。 */
char buf[384];
if (resolv_read_file(buf, sizeof buf) <= 0) return;
if (strstr(buf, "nameserver 127.0.0.1") != NULL) {
/* 已在代理指向;补上游(若无双行则显式加) */
char up[40] = "";
char *p = buf;
while ((p = strstr(p, "nameserver"))) {
p += 10; while (*p == ' ' || *p == '\t') p++;
char ip[40]; int i = 0;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && i < 39) ip[i++] = *p++;
ip[i] = 0;
if (strcmp(ip, "127.0.0.1") != 0 && ip[0]) { snprintf(up, sizeof up, "%s", ip); break; }
}
if (up[0]) snprintf(g_dns_upstream, sizeof g_dns_upstream, "%s", up);
return;
}
/* 备份原内容 */
snprintf(g_resolv_backup, sizeof g_resolv_backup, "%s", buf);
/* 取上游(首个 nameserver) */
char up[40] = "";
char *p = buf;
while ((p = strstr(p, "nameserver"))) {
p += 10; while (*p == ' ' || *p == '\t') p++;
char ip[40]; int i = 0;
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && i < 39) ip[i++] = *p++;
ip[i] = 0;
if (ip[0] && strcmp(ip, "127.0.0.1") != 0) { snprintf(up, sizeof up, "%s", ip); break; }
}
snprintf(g_dns_upstream, sizeof g_dns_upstream, "%s", up);
/* 双行:nameserver 127.0.0.1 + nameserver <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)
{
if (qlen < 12) return -1;
if (q[2] & 0x80) return -1; /* 不是 query 位 */
uint16_t qd = (uint16_t)((q[4] << 8) | q[5]);
if (qd < 1) return -1;
int off = 12, di = 0, first = 1, len = 0;
while (off < qlen) {
uint8_t l = q[off++];
if (l == 0) break;
if (l & 0xC0) return -1; /* question 名压缩不常见 */
if (!first && di < domcap - 2) domain[di++] = '.';
first = 0;
for (int i = 0; i < l && off < qlen && di < domcap - 1; i++) domain[di++] = (char)q[off++];
len += l;
if (len > 253) return -1;
}
if (off + 4 > qlen) return -1;
domain[di] = 0;
off += 4; /* qtype+qclass */
*qend = off;
return di > 0 ? 0 : -1;
}
static int dns_build_nxdomain(const uint8_t *q, int qlen, uint8_t *out, int outcap)
{
int qend = 0;
char dom[DNS_QNAME_MAX];
if (dns_parse_query(q, qlen, dom, sizeof dom, &qend) != 0) return -1;
int qsec = qend - 12;
if (12 + qsec > outcap) return -1;
memcpy(out, q, 12);
out[2] = 0x81; out[3] = 0x83; /* QR|RD|RA + RCODE=3 (NXDOMAIN) */
out[4] = 0; out[5] = 1; /* qdcount=1 */
out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 0; out[11] = 0;
memcpy(out + 12, q + 12, (size_t)qsec);
return 12 + qsec;
}
/* 读 RR owner 域名(支持压缩指针,最多 64 跳);返回字段结束偏移(type 起始处),失败 -1。 */
static int dns_read_name(const uint8_t *r, int rlen, int start, char *out, int outcap)
{
int off = start, after = -1, di = 0, first = 1, len = 0, guard = 0;
while (off < rlen && guard++ < 64) {
uint8_t l = r[off];
if (l == 0) { if (after < 0) after = off + 1; break; }
if (l & 0xC0) {
if (off + 1 >= rlen) return -1;
if (after < 0) after = off + 2; /* 名字字段到此为止 */
off = (int)(((l & 0x3F) << 8) | r[off + 1]); /* 跟随指针 */
continue;
}
off++;
if (!first && di < outcap - 2) out[di++] = '.';
first = 0;
for (int i = 0; i < l && off < rlen && di < outcap - 1; i++) out[di++] = (char)r[off++];
len += l;
if (len > 253) return -1;
}
out[di] = 0;
return after;
}
static void dns_cache_from_response(const uint8_t *r, int rlen)
{
if (rlen < 12) return;
if (r[3] & 0x0f) return; /* rcode 非0 不缓存 */
uint16_t an = (uint16_t)((r[6] << 8) | r[7]);
int off = 12;
while (off < rlen) { /* 跳过 question */
uint8_t l = r[off++];
if (l == 0) break;
if (l & 0xC0) { off += 1; break; }
off += l;
}
off += 4;
for (int a = 0; a < an && off + 10 <= rlen; a++) {
char name[64];
int at = dns_read_name(r, rlen, off, name, sizeof name);
if (at < 0 || at + 10 > rlen) break;
uint16_t type = (uint16_t)((r[at] << 8) | r[at + 1]);
uint32_t ttl = ((uint32_t)r[at + 4] << 24) | (r[at + 5] << 16) | ((uint32_t)r[at + 6] << 8) | r[at + 7];
uint16_t rdlen = (uint16_t)((r[at + 8] << 8) | r[at + 9]);
int data = at + 10;
if (data + rdlen > rlen) break;
if (type == 1 && rdlen >= 4 && name[0]) { /* A 记录 */
uint32_t ip = ((uint32_t)r[data] << 24) | (r[data + 1] << 16) | ((uint32_t)r[data + 2] << 8) | r[data + 3];
dns_cache_insert(name, ip, (uint16_t)(ttl > 65535 ? 65535 : ttl));
}
off = data + rdlen;
}
}
static void dns_handle_packet(void)
{
uint8_t q[1536], rbuf[1536];
struct sockaddr_in from;
socklen_t fl = sizeof from;
ssize_t n = recvfrom(g_dns_fd, q, sizeof q, 0, (struct sockaddr *)&from, &fl);
if (n < 12) return;
g_dns_q++;
char dom[DNS_QNAME_MAX];
int qend = 0;
if (dns_parse_query(q, (int)n, dom, sizeof dom, &qend) != 0 || !dom[0]) return;
const char *entity = dns_entity_for_port((uint16_t)ntohs(from.sin_port));
/* 域名规则:deny → NXDOMAIN + 日志 */
if (dns_query_domain_denied(entity, dom)) {
g_dns_deny++;
net_log("warn", "dns", "deny entity=%s domain=%s", entity, dom);
uint8_t resp[1536];
int rl = dns_build_nxdomain(q, (int)n, resp, sizeof resp);
if (rl > 0) sendto(g_dns_fd, resp, (size_t)rl, 0, (struct sockaddr *)&from, fl);
return;
}
/* 只记新域名首见(全量 debug 级,防日志爆炸) */
int cached = dns_cache_lookup_name(dom);
if (cached != 0) { g_dns_first++; net_log("info", "dns", "first entity=%s domain=%s", entity, dom); }
/* 上游透传转发 */
if (!g_dns_upstream[0]) {
net_log("warn", "dns", "no upstream, drop domain=%s", dom);
return;
}
struct sockaddr_in up;
memset(&up, 0, sizeof up);
up.sin_family = AF_INET;
up.sin_port = htons((uint16_t)g_dns_upstream_port);
if (inet_pton(AF_INET, g_dns_upstream, &up.sin_addr) != 1) return;
sendto(g_dns_up_fd, q, (size_t)n, 0, (struct sockaddr *)&up, sizeof up);
struct pollfd pf = { g_dns_up_fd, POLLIN, 0 };
int pr = poll(&pf, 1, DNS_UPSTREAM_TMO_MS);
if (pr > 0) {
ssize_t rn = recv(g_dns_up_fd, rbuf, sizeof rbuf, 0);
if (rn > 12) {
dns_cache_from_response(rbuf, (int)rn);
sendto(g_dns_fd, rbuf, (size_t)rn, 0, (struct sockaddr *)&from, fl);
}
} else {
net_log("warn", "dns", "upstream timeout domain=%s", dom);
}
}
static void dns_self_check(void)
{
char buf[384];
if (resolv_read_file(buf, sizeof buf) <= 0) return;
/* 若我们的双行头丢失(第三方/udhcpc 改回)→ 重写并记日志。 */
if (strstr(buf, "nameserver 127.0.0.1") == NULL) {
net_log("warn", "dns", "resolv.conf reverted by third-party — re-apply");
resolv_backup_apply();
}
}
/* ---- 期 2 IPC 文本构建 ---- */
static int build_conns_text(const char *filter, char *out, size_t cap, int *truncated)
{
if (truncated) *truncated = 0;
out[0] = 0;
size_t off = 0;
long now = mono_now();
for (int i = 0; i < g_conn_n; i++) {
conn_t *c = &g_conn[i];
if (filter && filter[0] && strncmp(c->entity, filter, strlen(filter)) != 0) continue;
char lip[32], rip[32];
ip_fmt(c->local_ip, lip, sizeof lip);
ip_fmt(c->remote_ip, rip, sizeof rip);
const char *dom = c->domain[0] ? c->domain : "-"; /* 空域名占位,保字段数固定 6 */
char line[192];
int n;
if (c->type) {
n = snprintf(line, sizeof line, "%s|%s:%u|%s:%u|UDP|%ld|%s\n",
c->entity, lip, c->local_port, rip, c->remote_port,
now - c->mono_open, dom);
} else {
n = snprintf(line, sizeof line, "%s|%s:%u|%s:%u|%s|%ld|%s\n",
c->entity, lip, c->local_port, rip, c->remote_port,
tcp_state_str(c->state_code), now - c->mono_open, dom);
}
if (n <= 0) continue;
if (off + (size_t)n >= cap - 1) { if (truncated) *truncated = 1; break; }
memcpy(out + off, line, (size_t)n);
off += (size_t)n;
}
out[off] = 0;
return (int)off;
}
static int build_dns_ctl_text(const char *cmd, char *out, size_t cap)
{
out[0] = 0;
if (!strcmp(cmd, "stats") || !strcmp(cmd, "status")) {
long now = mono_now();
int hits = 0;
for (int i = 0; i < g_dns_n; i++) if (g_dns[i].name[0] && g_dns[i].exp > now) hits++;
snprintf(out, cap,
"dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d\n",
g_mode_meow ? "meow" : "readonly", g_dns_enabled,
g_dns_upstream[0] ? g_dns_upstream : "(none)", hits, g_dns_n,
(unsigned long long)g_dns_q, (unsigned long long)g_dns_deny,
(unsigned long long)g_dns_first, g_dns_modified_resolv);
} else if (!strcmp(cmd, "flush")) {
dns_cache_flush();
snprintf(out, cap, "dns cache flushed (%d entries)\n", g_dns_n);
} else {
snprintf(out, cap, "dns: cmd 须 status|stats|flush\n");
return MEOW_ERR_INVAL;
}
return MEOW_OK;
}
/* ---------------- 节拍:模式热切换 + 接口变化检测(日志)+ 全局速率差分 ---------------- */
static void daemon_tick(void)
{
int fm = boot_mode_is_meow();
if (fm != g_mode_meow) {
svc_log("mode -> %s", fm ? "full(meow)" : "readonly");
g_mode_meow = fm;
/* 模式热切换 → DNS 代理开关(HibyOS 绝不启用/绝不改 resolv.conf)。 */
if (fm && !g_dns_disabled) dns_proxy_start();
else if (!fm && g_dns_enabled) dns_proxy_stop();
}
/* DNS:MeowOS 下 5s 自检 resolv.conf 未被第三方改回。 */
if (g_dns_enabled) {
long now1 = mono_now();
if (now1 - g_last_resolv_check >= DNS_RESOLV_CHECK_SEC) {
g_last_resolv_check = now1;
dns_self_check();
}
}
/* 接口热插拔/状态变化:节拍检测 → 日志(读侧无 popen)。 */
enum_ifaces(0);
int cur = g_iface_n;
for (int i = 0; i < cur; i++) {
int found = -1;
for (int j = 0; j < g_prev_if_n; j++)
if (strcmp(g_prev_if[j], g_ifaces[i].name) == 0) { found = j; break; }
if (found < 0)
net_log("info", "iface", "add kind=%s name=%s", g_ifaces[i].kind, g_ifaces[i].name);
else if (g_prev_if_up[found] != g_ifaces[i].up)
net_log("info", "iface", "state name=%s up=%d", g_ifaces[i].name, g_ifaces[i].up);
if (found >= 0) g_prev_if_up[found] = g_ifaces[i].up;
}
for (int j = 0; j < g_prev_if_n; j++) {
int gone = 1;
for (int i = 0; i < cur; i++) if (strcmp(g_prev_if[j], g_ifaces[i].name) == 0) { gone = 0; break; }
if (gone) net_log("info", "iface", "del name=%s", g_prev_if[j]);
}
for (int i = 0; i < cur; i++) {
snprintf(g_prev_if[i], sizeof g_prev_if[i], "%s", g_ifaces[i].name);
g_prev_if_up[i] = g_ifaces[i].up;
}
g_prev_if_n = cur;
/* 全局速率差分(2s 拍):/proc/net/dev 累计。 */
long rx = -1, tx = 0;
netdev_totals(&rx, &tx);
if (rx >= 0) {
long now = mono_now();
if (g_net_last_rx >= 0 && now > g_net_last_mono) {
long dt = now - g_net_last_mono;
if (dt > 0) {
g_net_rate_rx = (rx - g_net_last_rx) / dt;
g_net_rate_tx = (tx - g_net_last_tx) / dt;
if (g_net_rate_rx < 0) g_net_rate_rx = 0;
if (g_net_rate_tx < 0) g_net_rate_tx = 0;
}
}
g_net_last_rx = rx; g_net_last_tx = tx; g_net_last_mono = now;
}
/* 期2 连接普查(读侧:观察层,HibyOS 只读模式同样运行;不含写操作)。 */
census_run();
}
/* ---------------- IPC 鉴权(连接级,MPK_SIGNATURE_DESIGN §6.2;同 svc_power) ---------------- */
typedef enum { CALL_OTHER, CALL_PKG, CALL_SYS } caller_kind_t;
typedef struct {
caller_kind_t kind;
char pkg[MEOW_APP_ID_MAX];
uid_t uid;
} caller_t;
static int is_sys_identity(const caller_t *c)
{
if (c->kind == CALL_SYS) return 1;
if (c->kind == CALL_PKG && !strncmp(c->pkg, "os.meow.", 8)) return 1;
return 0;
}
static void classify_exe(const char *exe, caller_t *out)
{
static const char *mp = "/usr/data/mnt/sd_0/MeowAPPs/";
if (strncmp(exe, mp, strlen(mp)) == 0) {
const char *p = exe + strlen(mp);
if (*p == '/') p++;
const char *end = strchr(p, '/');
int plen = end ? (int)(end - p) : (int)strlen(p);
if (plen > 0 && plen < MEOW_APP_ID_MAX) {
memcpy(out->pkg, p, (size_t)plen); out->pkg[plen] = 0;
out->kind = CALL_PKG;
return;
}
}
char tmp[MEOW_APP_ID_MAX];
if (strcmp(exe, "/usr/bin/meow_launcherd") == 0) {
snprintf(tmp, sizeof tmp, "%s", "os.meow.launcher");
} else if (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);
}
char msg[140];
snprintf(msg, sizeof msg, "connect: ssid=%s network_id=%d (已 enable+select)", tmp_ssid, id);
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, "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: 未知子命令");
}
}
/* ---------------- 处理单条连接 ---------------- */
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;
}
send_err(c, req_id, MEOW_ERR_INVAL, "unknown type (期1 STATUS/IFACE/WIFI_CTL/BT_CTL; 期2 CONNS/DNS_CTL)");
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;
}
static const char *usage(void)
{
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|disconnect|on|off"
" | bt status|on|off|paired | rfkill [<type> <on|off>] | udhcpc renew|release [if]"
" | apps | conns [entity] | top | dns status|stats|flush | help";
}
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;
if (!strcmp(cmd, "status")) return cli_status();
if (!strcmp(cmd, "iface")) return cli_iface(arg);
if (!strcmp(cmd, "help") || !strcmp(cmd, "-h") || !strcmp(cmd, "--help")) return cli_help();
if (!strcmp(cmd, "apps")) return cli_apps();
if (!strcmp(cmd, "conns")) return cli_conns(arg);
if (!strcmp(cmd, "top")) return cli_top();
if (!strcmp(cmd, "dns")) {
if (!arg) { fprintf(stderr, "dns 子命令缺失(status|stats|flush)\n"); return 1; }
return cli_dns(arg);
}
if (!strcmp(cmd, "wifi")) {
if (!arg) { fprintf(stderr, "wifi 子命令缺失(%s)\n", usage()); return 1; }
if (!strcmp(arg, "on") || !strcmp(arg, "off")) return cli_cmd_result(MSG_NET_WIFI_CTL, arg, NULL);
if (!strcmp(arg, "connect")) {
if (!arg2) { fprintf(stderr, "connect 需 <ssid>[,psk]\n"); return 1; }
return cli_cmd_result(MSG_NET_WIFI_CTL, "connect", 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);
}