3517 lines
146 KiB
C
3517 lines
146 KiB
C
#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);
|
||
}
|