rebase 到 main(00e7271,M43 time32 已合入) 时解开 build_netmgrd.sh 冲突:**两者都保留**—— M43 的 `../sdk/meow_time32.c` + `-DMEOW_TIME32_LIBC_OVERRIDES`,以及我的 ROOT/ZIGCC/OUT 环境变量化。编译改在 mktemp 目录里做(产物名仍是字面量 `-o meow_netmgrd`,供 sdk/test_time32_abi.sh 的按名定位),编完再落到 OUT —— 设了 OUT 也不会覆盖/删除仓库里 那份被跟踪的二进制(r1 期间 mv 曾把它删掉过)。门禁自证:`--builds` → build meow_netmgrd src=yes macro=yes;`--no-builds` → 4263=3 4403=0 16B-layout=OK ALL PASS。 P1-1(写操作越过客户端 1500ms 窗口):预算模型改成**每一层都受限**—— `BT_ACT_TMO_MS 800 / BT_ACT_SHORT_MS 600 / BT_VERIFY_BUDGET_MS 220 / gap 90`, `bt_poll_probe` 把**剩余预算**传给探测函数(`bt_acl_has_tri`/`bt_info_flag` 新增 tmo 形参), 探测自身那次 run_cmd 也受同一个 deadline 约束;`bt_onoff` 同理(up 失败只在剩余预算内重试 一次);成功文案不再补设备名(省掉 1-2 次 fork,UI 本来就按 MAC join)。 探测改为三态:命令失败/超时 = -1(**未知**),不再被当成「没有 ACL」——否则 disconnect 会在 查不到的情况下报「已断开」,又是一次假成功。 实测(宿主桩驱动 bt_ctl_execute,命令卡死 5s 的形态):connect 1042-1047ms、pair 1042-1045ms、 bt on 831ms,全部 < 1450 且有 400ms+ 余量(r1 实测 1528/2424ms)。设备侧实测 fork 成本 ≈25-30ms/次(bt a2dp 50ms、bt status 100ms),余量足够。 P2-1(A2DP 1+N 子进程塞进 status JSON):`bt_a2dp_query(buf,cap,budget)` 加**总预算**, list-pcms 单命令 ≤400ms、每个 info ≤300ms、剩下端点超预算就不再查;`bt a2dp` 命令用 900ms。 status JSON 不在无连接时查(0 次 fork),有连接时走 10s TTL 缓存 + 500ms 预算; `a2dp_codec`/`a2dp_rate` 字段保留(MeowPanel btCard 依赖,见 docs/plan/FUTURE_MEOWPANEL.md:48)。 实测 4 端点全卡死:911ms(r1 6053ms);status JSON 首次 350-384ms、缓存命中 31-49ms。 P2-2(test_bt_ctl 不可重复):开跑先 `rm -rf` 状态目录、退出再清;连跑 3 次 ALL PASS(贴了输出)。 P2-3(bt_onoff 顺序 + 多节点):开=先 rfkill unblock(**全部**同类型节点)再 hciconfig up, up 未生效时剩余预算内重试一次;关=先 down 再 block。新增 `rfkill_set_block_all()`(旧 `rfkill_set_block` 仍只写首个节点,WiFi 路径行为不变)。桩改成「blocked 时 up 必败」,并加了 反向用例证明桩真的会失败(否则断言空转)。 P3:①info 的 RUNCMD_EMPTY(exit 0 无输出)不再当失败跳过;②`bt_pcm_paths_parse` 的 `/a2dpsrc` 搜索限制在行内 [p,e)(原来会跨 \n 命中下一行);③`bt_devices_text` 注释写明 「本轮扫描见到过的设备」而非「此刻在场」([DEL] 不删除、devices 块含 BlueZ 历史缓存)。 附带(塔 11:06 建议,BT 作用域内先落地):连续 3 次 RUNCMD_TIMEOUT 后熔断 30s,冷却期内 直接回「熔断中」不再 fork(真机 55 个挂死 bluetoothctl 吃光进程槽的事故);任何非超时返回清零。 单测覆盖:第 4 次调用 0ms 返回。范围只限 BT 写操作,WiFi/DNS 调用点未动。 真机(本轮只读,设备已恢复在线):新件 md5 e62e7512 上卡只读验收 status/paired/connected/ devices/a2dp 全通;`ps w | grep -c bluetoothctl` 前后均为 2(≤3);Music du 199164544; 临时目录/独立实例清理干净,/usr/bin 未动。
4629 lines
199 KiB
C
4629 lines
199 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_hcitool = "hcitool"; /* MEOW_HCITOOL(内核 ACL 连接权威) */
|
||
static const char *g_bluealsa_cli= "bluealsa-cli"; /* MEOW_BLUEALSA_CLI(A2DP 传输详情) */
|
||
static const char *g_bt_scan_cache = "/tmp/meow_bt_scan.txt"; /* MEOW_BT_SCAN_CACHE(扫描缓存;launcher net.c 同路径) */
|
||
static const char *g_ip = "ip"; /* MEOW_IP */
|
||
static const char *g_wifi_if = "wlan0"; /* MEOW_WIFI_IF */
|
||
|
||
/* 期 2:观测层 + DNS 代理 相关路径(可经 env 覆盖,宿主/qemu 桩测试用) */
|
||
static char g_proc_tcp[160] = "/proc/net/tcp"; /* MEOW_TCP */
|
||
static char g_proc_udp[160] = "/proc/net/udp"; /* MEOW_UDP */
|
||
static char g_ams_sock[128] = "/usr/data/meow_ams.sock"; /* MEOW_AMS_SOCK(amsd 注册表 pid→pkg) */
|
||
static char g_svcd_sock[128] = "/tmp/meow_svcd.sock"; /* MEOW_SVCD_SOCK(svcd 服务表 pid→svc,socket 查询兜底) */
|
||
static char g_svcd_status[160]= "/tmp/meow_svcd.status"; /* MEOW_SVCD_STATUS(svcd 落盘状态,机器可读 `svc=<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) */
|
||
/* WiFi 设置页 bug1(2026-09-15):connect 有界等待 wpa_state=COMPLETED 的上限(ms)。
|
||
* 普通 ~10s;隐藏网络(connecth,主动探测+关联更慢)~15s。env 可调(宿主桩测试缩短)。 */
|
||
#define WIFI_CONNECT_WAIT_MS 10000 /* MEOW_WIFI_CONNECT_WAIT_MS */
|
||
#define WIFI_CONNECTH_WAIT_MS 15000 /* MEOW_WIFI_CONNECTH_WAIT_MS */
|
||
/* 蓝牙写操作的**总预算**必须压在 launcher 侧 `netmgr_bt()` 的 IPC 超时(NM_TMO_DFL=1500ms,
|
||
* netmgr_ipc.c,非本单 scope)以内:越线即被客户端判「不可达」→ 直连回退再下发一次 →
|
||
* 刚写好的「未生效」文案被客户端丢掉,净效果退回「恒报成功」。
|
||
* 预算模型(每一层都受限,不是只在两次探测之间看 deadline):
|
||
* 命令 ≤ BT_ACT_TMO_MS / BT_ACT_SHORT_MS
|
||
* + 复查 ≤ BT_VERIFY_BUDGET_MS,且**每次探测的 run_cmd 也只用剩余预算**
|
||
* + 收尾 reap(正常/超时路径都先 SIGKILL,不额外等 300ms)
|
||
* 目标上界 ≈ 1150ms(MIPS 更慢的 fork/exec 留了余量),实测见 test_bt_ctl 的 `budget-*` 用例。 */
|
||
#define BT_ACT_TMO_MS 800 /* connect/pair 的 bluetoothctl 上限 */
|
||
#define BT_ACT_SHORT_MS 600 /* disconnect/unpair/on/off */
|
||
#define BT_VERIFY_BUDGET_MS 220 /* 写后真态复查总预算(含探测内部 run_cmd) */
|
||
#define BT_VERIFY_GAP_MS 90 /* 每轮间隔(ms) */
|
||
/* A2DP 查询是 1+N 个子进程(list-pcms + 每端点 info),而守护单线程、DNS 代理同一主循环,
|
||
* 所以**每条命令与整段查询都设上限**;status JSON 另走 TTL 缓存(见 g_bt_a2dp_*)。 */
|
||
#define BT_A2DP_TEXT_BUDGET_MS 900 /* `bt a2dp` 命令总预算(客户端 1500ms 窗口内) */
|
||
#define BT_A2DP_STATUS_BUDGET_MS 500 /* status JSON 刷新缓存时的总预算 */
|
||
#define BT_A2DP_LIST_TMO_MS 400 /* list-pcms 单命令上限 */
|
||
#define BT_A2DP_INFO_TMO_MS 300 /* 每个端点 info 单命令上限 */
|
||
#define BT_A2DP_CACHE_SEC 10 /* status JSON 的 a2dp 摘要缓存 TTL(秒) */
|
||
/* 防堆积熔断(真机 0.8.49 事故:55 个挂死的 bluetoothctl 吃光进程槽/内存,launcherd 与 sshd
|
||
* 同时失去 fork 能力)。BT 写操作**连续** NN 次超时后,冷却期内不再 fork 新命令,如实回
|
||
* 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */
|
||
#define BT_TMO_STREAK_MAX 3
|
||
#define BT_TMO_COOLDOWN_SEC 30
|
||
#define IFACE_MAX 16
|
||
#define RFKILL_MAX 8
|
||
#define DNS_MAX 8
|
||
|
||
/* ---- 期 2 常量(NETMGR_DESIGN §4/§5/§8;全部静态表,禁 malloc) ---- */
|
||
#define CONN_MAX 512 /* 连接表上限 */
|
||
#define DNS_CACHE_MAX 256 /* DNS 应答缓存上限 */
|
||
#define DNS_UPSTREAM_TMO_MS 800 /* DNS 上游转发超时 */
|
||
#define DNS_QNAME_MAX 128 /* query 域名最大字节 */
|
||
#define ENTITY_MAX 128 /* pid→entity 缓存条数 */
|
||
#define PORT_ENT_MAX 128 /* udp 本地端口→entity 缓存(DNS 归因用) */
|
||
#define RULE_ENT_MAX 16 /* 域名规则实体上限 */
|
||
#define RULE_HOST_MAX 24 /* 单实体 allow/deny 主机规则上限 */
|
||
#define EVT_DUP_MAX 64 /* 事件日志 1 分钟去重表 */
|
||
#define DNS_RESOLV_CHECK_SEC 5 /* resolv.conf 自检周期 */
|
||
|
||
/* ---- 期 5 常量(公网流量统计:netlink sock_diag 按 TCP 连接累计字节;静态表,禁 malloc) ---- */
|
||
#define FLOW_LAST_MAX 128 /* inode→上拍累计 跟踪表(仅公网/内网 TCP 流) */
|
||
#define PUB_FLOW_MAX 16 /* 公网字节 top 流表(内存累加用) */
|
||
#define PUB_SERVE_ENT 6 /* status JSON 序列化的 top 实体数(4096 帧预算内) */
|
||
#define PUB_SERVE_FLOW 6 /* status JSON 序列化的 top 流数 */
|
||
#define TCP_INFO_MIN_LEN 136 /* tcp_info 载荷≥此值才含 tcpi_bytes_received(off128+8) */
|
||
|
||
#define DNS_PORT_DEF 53 /* 监听端口(MEOW_DNS_PORT 覆盖;宿主测试用高位规避 <1024 权限) */
|
||
#define DNS_SELF_IP "127.0.0.1"
|
||
|
||
static int g_poll_sec = NET_POLL_SEC_DEF;
|
||
static int g_connect_wait_ms = WIFI_CONNECT_WAIT_MS; /* MEOW_WIFI_CONNECT_WAIT_MS */
|
||
static int g_connecth_wait_ms = WIFI_CONNECTH_WAIT_MS; /* MEOW_WIFI_CONNECTH_WAIT_MS */
|
||
static int g_dns_port = DNS_PORT_DEF; /* MEOW_DNS_PORT */
|
||
static int g_dns_disabled = 0; /* MEOW_DNS_DISABLE=1:禁用 DNS 代理(宿主测试无 root 绑 53) */
|
||
|
||
static long mono_now(void)
|
||
{
|
||
struct timespec tp;
|
||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||
return tp.tv_sec;
|
||
}
|
||
|
||
/* ---------------- 极简日志(stderr;svcd supervisor 捕获) ---------------- */
|
||
static void svc_log(const char *fmt, ...)
|
||
{
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
fprintf(stderr, "[meow_netmgrd] ");
|
||
vfprintf(stderr, fmt, ap);
|
||
fprintf(stderr, "\n");
|
||
va_end(ap);
|
||
}
|
||
|
||
/* ---------------- 文件小工具(同 svc_power) ---------------- */
|
||
static int read_file_txt(const char *path, char *buf, int cap)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
int n = 0;
|
||
while (n < cap - 1) {
|
||
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
|
||
if (r <= 0) break;
|
||
n += (int)r;
|
||
}
|
||
close(fd);
|
||
buf[n] = 0;
|
||
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0;
|
||
return n;
|
||
}
|
||
|
||
static int read_file_int_def(const char *path, int def)
|
||
{
|
||
char buf[32];
|
||
if (read_file_txt(path, buf, sizeof buf) <= 0) return def;
|
||
return atoi(buf);
|
||
}
|
||
|
||
/* ---------------- 日志框架(§8:每次开机新文件,TF 优先降级 /tmp) ---------------- */
|
||
static int g_logfd = -1;
|
||
static char g_logfile[192];
|
||
|
||
/* bootts 复用(与 launcher/audiod 共用 /tmp/meow_bootts 缓存,一次开机一份日志) */
|
||
static void get_bootts(char *out, size_t n)
|
||
{
|
||
int fd = open(g_bootts_cache, O_RDONLY);
|
||
if (fd >= 0) {
|
||
int r = read(fd, out, (int)n - 1);
|
||
close(fd);
|
||
if (r > 0) { out[r] = 0; return; }
|
||
}
|
||
time_t now = time(NULL), up = 0;
|
||
fd = open("/proc/uptime", O_RDONLY);
|
||
if (fd >= 0) {
|
||
char buf[64];
|
||
int r = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (r > 0) { buf[r] = 0; char *p = strchr(buf, ' '); if (p) *p = 0; up = (time_t)atof(buf); }
|
||
}
|
||
struct tm tm;
|
||
localtime_r(&now - up, &tm);
|
||
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
|
||
fd = open(g_bootts_cache, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { (void)write(fd, out, strlen(out)); close(fd); }
|
||
}
|
||
|
||
static void log_open(void)
|
||
{
|
||
char ts[64];
|
||
get_bootts(ts, sizeof ts);
|
||
char path[260];
|
||
int use_tf = 0;
|
||
if (access(g_log_dir, W_OK) == 0) {
|
||
snprintf(path, sizeof path, "%s/net_%s.log", g_log_dir, ts);
|
||
use_tf = 1;
|
||
} else {
|
||
snprintf(path, sizeof path, "/tmp/net_%s.log", ts);
|
||
}
|
||
snprintf(g_logfile, sizeof g_logfile, "%s", path);
|
||
g_logfd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||
svc_log("log: %s (%s)", g_logfile, use_tf ? "TF" : "/tmp fallback");
|
||
}
|
||
|
||
/* 日志行:`bootts lvl tag kv...`;stderr 与文件双写(svcd 捕获 stderr + 文件归档)。 */
|
||
static void net_log(const char *lvl, const char *tag, const char *fmt, ...)
|
||
{
|
||
char msg[512];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
vsnprintf(msg, sizeof msg, fmt, ap);
|
||
va_end(ap);
|
||
char bootts[64];
|
||
get_bootts(bootts, sizeof bootts);
|
||
if (g_logfd >= 0) {
|
||
char line[600];
|
||
int n = snprintf(line, sizeof line, "%s %s %s %s\n", bootts, lvl, tag, msg);
|
||
if (n > 0 && n < (int)sizeof line) (void)write(g_logfd, line, (size_t)n);
|
||
}
|
||
fprintf(stderr, "[net] %s %s %s\n", lvl, tag, msg);
|
||
}
|
||
|
||
#define RUNCMD_ERR_MAX 256 /* stderr 只留前 256B:失败文案要有料,但不能把 IPC 报文撑爆 */
|
||
|
||
/* run_cmd 四态(M49)。P1 之前 run_cmd 只返回「读到的字节数」,命令失败也返回 0,
|
||
* 调用方 `n >= 0 → MEOW_OK`,于是 on/off/connect/disconnect/pair 全部恒报成功。 */
|
||
typedef enum {
|
||
RUNCMD_OK = 0, /* 退出 0,有输出 */
|
||
RUNCMD_EMPTY = 1, /* 退出 0,无输出(成功但没内容,≠ 失败) */
|
||
RUNCMD_EXIT = 2, /* 非 0 退出码(127 = 命令不存在 / execl 失败) */
|
||
RUNCMD_TIMEOUT = 3, /* 限时内没收完或子进程不退出(已 SIGKILL) */
|
||
RUNCMD_EXEC = 4, /* 本进程 fork/pipe 失败,或子进程被信号杀死(非正常退出) */
|
||
} runcmd_status_t;
|
||
|
||
typedef struct {
|
||
int status; /* runcmd_status_t */
|
||
int bytes; /* stdout 字节数(不含尾部 NUL;截断时 = cap-1) */
|
||
int exit_code; /* WEXITSTATUS;未正常退出 = -1 */
|
||
int signal; /* 被信号杀死时的信号号,否则 0 */
|
||
int truncated; /* stdout 超出 buf 容量:已排空但丢弃尾部 */
|
||
char err[RUNCMD_ERR_MAX]; /* stderr 前 256B(失败时进错误文案) */
|
||
} runcmd_res_t;
|
||
|
||
static long mono_ms(void)
|
||
{
|
||
struct timespec tp;
|
||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||
return tp.tv_sec * 1000L + tp.tv_nsec / 1000000L;
|
||
}
|
||
|
||
/* ---------------- 有界 popen 收割(257 教训:命令 wedge 时无条件 waitpid 会拖死调用线程) ----------------
|
||
* 非阻塞读子进程 stdout/stderr(poll 限时);读完后 WNOHANG 优雅等 300ms,不死 SIGKILL 再等 300ms;
|
||
* 仍不死(D 态)放弃收割(泄漏一子进程换线程不卡死,已记日志)。
|
||
* 返回 1=已收割(*st 有值),0=放弃收割。
|
||
* cmd 为完整 shell 行;buf 由调用方提供(静态/栈),不 malloc——热路径禁 malloc。 */
|
||
static int reap_bounded(pid_t pid, const char *cmd, int *st)
|
||
{
|
||
*st = 0;
|
||
for (int i = 0; i < 30; i++) {
|
||
if (waitpid(pid, st, WNOHANG) == pid) return 1;
|
||
struct timespec ts = { 0, 10 * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
}
|
||
svc_log("run_cmd: child %ld not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
|
||
kill(pid, SIGKILL);
|
||
for (int i = 0; i < 30; i++) {
|
||
if (waitpid(pid, st, WNOHANG) == pid) return 1;
|
||
struct timespec ts = { 0, 10 * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
}
|
||
svc_log("run_cmd: child %ld unkillable (kernel D-state?) — abandon reap (%s)", (long)pid, cmd);
|
||
return 0;
|
||
}
|
||
|
||
/* 带四态结果的执行器(res 可为 NULL,等价于旧 run_cmd)。返回 stdout 字节数(-1=起不来)。
|
||
* stdout 超容量时继续排空(丢弃),保证子进程不被 EPIPE/SIGPIPE 提前打断、退出码可信。 */
|
||
static int run_cmd_ex(char *buf, int cap, int tmo_ms, const char *cmd, runcmd_res_t *res)
|
||
{
|
||
runcmd_res_t lr;
|
||
runcmd_res_t *r = res ? res : &lr;
|
||
memset(r, 0, sizeof *r);
|
||
r->exit_code = -1;
|
||
r->status = RUNCMD_EXEC;
|
||
if (cap > 0) buf[0] = 0;
|
||
|
||
int op[2], ep[2];
|
||
if (pipe(op) < 0) return -1;
|
||
if (pipe(ep) < 0) { close(op[0]); close(op[1]); return -1; }
|
||
pid_t pid = fork();
|
||
if (pid < 0) { close(op[0]); close(op[1]); close(ep[0]); close(ep[1]); return -1; }
|
||
if (pid == 0) {
|
||
dup2(op[1], 1);
|
||
dup2(ep[1], 2); /* M49:stderr 不再丢 /dev/null,失败时留一段进错误文案 */
|
||
close(op[0]); close(op[1]); close(ep[0]); close(ep[1]);
|
||
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
|
||
_exit(127);
|
||
}
|
||
close(op[1]); close(ep[1]);
|
||
fcntl(op[0], F_SETFL, fcntl(op[0], F_GETFL, 0) | O_NONBLOCK);
|
||
fcntl(ep[0], F_SETFL, fcntl(ep[0], F_GETFL, 0) | O_NONBLOCK);
|
||
|
||
int off = 0, eoff = 0, trunc = 0, out_eof = 0, err_eof = 0, timed_out = 0;
|
||
long deadline = mono_ms() + (tmo_ms > 0 ? tmo_ms : 1);
|
||
char stray[256];
|
||
while (!(out_eof && err_eof)) {
|
||
long remain = deadline - mono_ms();
|
||
if (remain <= 0) { timed_out = 1; break; }
|
||
struct pollfd pf[2];
|
||
int nf = 0;
|
||
if (!out_eof) { pf[nf].fd = op[0]; pf[nf].events = POLLIN; pf[nf].revents = 0; nf++; }
|
||
if (!err_eof) { pf[nf].fd = ep[0]; pf[nf].events = POLLIN; pf[nf].revents = 0; nf++; }
|
||
int pr = poll(pf, (nfds_t)nf, (int)remain);
|
||
if (pr < 0) { if (errno == EINTR) continue; timed_out = 1; break; }
|
||
if (pr == 0) { timed_out = 1; break; }
|
||
for (int i = 0; i < nf; i++) {
|
||
if (!(pf[i].revents & (POLLIN | POLLHUP | POLLERR | POLLNVAL))) continue;
|
||
int is_out = (pf[i].fd == op[0]);
|
||
char *dst = is_out ? buf : r->err;
|
||
int dcap = is_out ? (cap > 0 ? cap : 1) : RUNCMD_ERR_MAX;
|
||
int *doff = is_out ? &off : &eoff;
|
||
int into = 1;
|
||
char *p = dst + *doff;
|
||
int room = dcap - 1 - *doff;
|
||
if (room <= 0) { p = stray; room = (int)sizeof stray; into = 0; }
|
||
ssize_t rd = read(pf[i].fd, p, (size_t)room);
|
||
if (rd == 0) { if (is_out) out_eof = 1; else err_eof = 1; continue; }
|
||
if (rd < 0) {
|
||
if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||
if (is_out) out_eof = 1; else err_eof = 1;
|
||
continue;
|
||
}
|
||
if (into) *doff += (int)rd;
|
||
else if (is_out) trunc = 1;
|
||
}
|
||
}
|
||
close(op[0]);
|
||
close(ep[0]);
|
||
if (cap > 0) buf[off] = 0;
|
||
r->err[eoff] = 0;
|
||
|
||
if (timed_out) kill(pid, SIGKILL); /* 已经超时:不再多等 300ms */
|
||
int st = 0;
|
||
int reaped = reap_bounded(pid, cmd, &st);
|
||
r->bytes = off;
|
||
r->truncated = trunc;
|
||
if (reaped) {
|
||
if (WIFEXITED(st)) r->exit_code = WEXITSTATUS(st);
|
||
else if (WIFSIGNALED(st)) r->signal = WTERMSIG(st);
|
||
}
|
||
if (timed_out || !reaped) r->status = RUNCMD_TIMEOUT;
|
||
else if (r->signal || r->exit_code < 0) r->status = RUNCMD_EXEC;
|
||
else if (r->exit_code != 0) r->status = RUNCMD_EXIT;
|
||
else if (off > 0) r->status = RUNCMD_OK;
|
||
else r->status = RUNCMD_EMPTY;
|
||
return off;
|
||
}
|
||
|
||
/* 兼容外壳:返回读到的字节数(-1=起不来),语义与 P1 之前一致(调用点无需改动)。 */
|
||
static int run_cmd(char *buf, int cap, int tmo_ms, const char *cmd)
|
||
{
|
||
return run_cmd_ex(buf, cap, tmo_ms, cmd, NULL);
|
||
}
|
||
|
||
/* 失败时的一句话原因(有界;stderr 有料就带上,最多 120B,避免报文过长)。 */
|
||
static const char *run_cmd_why(const runcmd_res_t *r, int *is_fail)
|
||
{
|
||
static char why[160];
|
||
why[0] = 0;
|
||
if (is_fail) *is_fail = 0;
|
||
switch (r->status) {
|
||
case RUNCMD_OK: case RUNCMD_EMPTY: return why;
|
||
case RUNCMD_TIMEOUT: snprintf(why, sizeof why, "超时"); break;
|
||
case RUNCMD_EXIT:
|
||
if (r->exit_code == 127) snprintf(why, sizeof why, "命令不存在(exit 127)");
|
||
else snprintf(why, sizeof why, "exit %d", r->exit_code);
|
||
break;
|
||
case RUNCMD_EXEC:
|
||
if (r->signal) snprintf(why, sizeof why, "被信号 %d 终止", r->signal);
|
||
else snprintf(why, sizeof why, "无法执行");
|
||
break;
|
||
default: snprintf(why, sizeof why, "未知失败"); break;
|
||
}
|
||
if (is_fail) *is_fail = 1;
|
||
if (r->err[0]) {
|
||
char one[121];
|
||
int j = 0;
|
||
for (int i = 0; r->err[i] && j < (int)sizeof one - 1; i++) {
|
||
unsigned char c = (unsigned char)r->err[i];
|
||
if (c == '\n' || c == '\r' || c == '\t') { if (j && one[j-1] != ' ') one[j++] = ' '; }
|
||
else if (c >= 0x20) one[j++] = (char)c;
|
||
}
|
||
one[j] = 0;
|
||
if (one[0]) {
|
||
size_t l = strlen(why);
|
||
snprintf(why + l, sizeof why - l, ": %s", one);
|
||
}
|
||
}
|
||
return why;
|
||
}
|
||
|
||
/* ---------------- JSON 构建(vsnprintf 助手法,截断分析免疫;同 powermgr) ---------------- */
|
||
typedef struct { char *buf; size_t cap, off; } jw_t;
|
||
static void jadd(jw_t *w, const char *fmt, ...)
|
||
{
|
||
if (w->off >= w->cap - 1) return;
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
int n = vsnprintf(w->buf + w->off, w->cap - w->off, fmt, ap);
|
||
va_end(ap);
|
||
if (n > 0) {
|
||
w->off += (size_t)n;
|
||
if (w->off >= w->cap) w->off = w->cap - 1;
|
||
}
|
||
}
|
||
|
||
/* 带引号的字符串值(转义 `"`/`\`/换行)。空串 → ""(不当 null,调用方自决)。 */
|
||
static void jadd_q(jw_t *w, const char *key, const char *val)
|
||
{
|
||
jadd(w, "\"%s\":\"", key);
|
||
for (const char *p = val; *p && w->off < w->cap - 3; p++) {
|
||
if (*p == '"' || *p == '\\') jadd(w, "\\%c", *p);
|
||
else if (*p == '\n') jadd(w, "\\n");
|
||
else if (*p == '\t') jadd(w, "\\t");
|
||
else w->buf[w->off++] = *p;
|
||
}
|
||
jadd(w, "\",");
|
||
}
|
||
|
||
static void jadd_int_or_null(jw_t *w, const char *key, int v)
|
||
{
|
||
if (v < 0) jadd(w, "\"%s\":null,", key);
|
||
else jadd(w, "\"%s\":%d,", key, v);
|
||
}
|
||
|
||
static void jadd_str_or_null(jw_t *w, const char *key, const char *s)
|
||
{
|
||
if (!s || !s[0]) jadd(w, "\"%s\":null,", key);
|
||
else jadd_q(w, key, s);
|
||
}
|
||
|
||
/* ---------------- 动作执行结果容器 ---------------- */
|
||
typedef struct {
|
||
int code;
|
||
char text[MEOW_IPC_MAX - 8];
|
||
} ctl_result_t;
|
||
static void ctl_reset(ctl_result_t *r, int code) { r->code = code; r->text[0] = 0; }
|
||
static void ctl_sets(ctl_result_t *r, int code, const char *s)
|
||
{
|
||
r->code = code;
|
||
snprintf(r->text, sizeof r->text, "%s", s);
|
||
}
|
||
|
||
/* ---------------- 接口统一层:模型(设计 §3 统一模型) ---------------- */
|
||
typedef struct {
|
||
char name[32];
|
||
char kind[8]; /* "lo" | "wifi" | "usb-eth" */
|
||
int up; /* 1=up 0=down */
|
||
char ip[40]; /* ""=null */
|
||
char gw[40]; /* ""=null */
|
||
long rx, tx; /* 累计字节(/proc/net/dev) */
|
||
} iface_t;
|
||
static iface_t g_ifaces[IFACE_MAX];
|
||
typedef struct { char entity[48]; uint32_t rx, tx; } ent_bytes_t;
|
||
static ent_bytes_t g_ent_bytes[ENTITY_MAX]; /* SDK 自愿上报(§4 字节级记账自报通道;声明须在 build_status_json 前) */
|
||
static int g_ent_bytes_n = 0;
|
||
static int g_iface_n = 0;
|
||
|
||
typedef struct {
|
||
int connected; /* 0/1 */
|
||
char state[16]; /* wpa_state 值;""=unknown */
|
||
char ssid[64];
|
||
char ip[40];
|
||
int rssi; /* 信号强度 dBm(可为负);has_rssi=0 时无效 */
|
||
int has_rssi; /* wpa_cli 是否给出 signal= */
|
||
} wifi_t;
|
||
|
||
typedef struct {
|
||
int available;
|
||
int powered;
|
||
char address[32];
|
||
int paired_n; /* -1=null */
|
||
int connected_n; /* 已连接(内核 ACL 权威)计数;-1=null */
|
||
} bt_t;
|
||
|
||
typedef struct {
|
||
char type[16]; /* wlan/bluetooth/... */
|
||
int soft; /* 0/1/-1 */
|
||
int hard; /* 0/1/-1 */
|
||
} rfkill_t;
|
||
static rfkill_t g_rfkill[RFKILL_MAX];
|
||
|
||
static int boot_mode_is_meow(void)
|
||
{
|
||
char buf[16] = "";
|
||
if (read_file_txt(g_boot_mode, buf, sizeof buf) <= 0) return 0; /* 缺失=只读等落地 */
|
||
return strcmp(buf, "meow") == 0;
|
||
}
|
||
|
||
/* kind 判定:0=lo 1=wifi 2=usb-eth(热插拔出现即记录)。 */
|
||
static int iface_parse_kind(const char *name)
|
||
{
|
||
if (strcmp(name, "lo") == 0) return 0;
|
||
if (strcmp(name, g_wifi_if) == 0) return 1;
|
||
return 2;
|
||
}
|
||
static const char *kind_str(int k)
|
||
{
|
||
return k == 0 ? "lo" : (k == 1 ? "wifi" : "usb-eth");
|
||
}
|
||
|
||
/* /proc/net/dev 单接口行 → rx(列1)/tx(列9) 累计字节;返回 0=找到。 */
|
||
static int netdev_iface(const char *ifname, long *rx, long *tx)
|
||
{
|
||
FILE *f = fopen(g_netdev, "r");
|
||
if (!f) return -1;
|
||
char line[256];
|
||
int found = -1;
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *c = strchr(line, ':');
|
||
if (!c) continue;
|
||
*c = 0;
|
||
char *n = line;
|
||
while (*n == ' ' || *n == '\t') n++;
|
||
if (strcmp(n, ifname) != 0) continue;
|
||
char *save = NULL;
|
||
char *tk = strtok_r(c + 1, " \t", &save); /* 列1 = rx bytes */
|
||
long vr = tk ? strtol(tk, NULL, 10) : 0;
|
||
for (int i = 0; i < 8; i++) tk = strtok_r(NULL, " \t", &save); /* 推进到列9 */
|
||
long vt = tk ? strtol(tk, NULL, 10) : 0;
|
||
if (rx) *rx = vr;
|
||
if (tx) *tx = vt;
|
||
found = 0;
|
||
break;
|
||
}
|
||
fclose(f);
|
||
return found;
|
||
}
|
||
|
||
/* 上行接口累计字节(/proc/net/dev)。仅取「非回环且 up」接口求和——本设备即 wlan0;去掉 lo 对
|
||
* 实时速率的虚高污染。依赖调用方先 enum_ifaces(0) 刷新 g_ifaces[] 再取数(daemon_tick 节拍内如此)。
|
||
* 无任何 up 非回环接口 → rx 置 -1(速率未知/无连接,不谎报 0)。 */
|
||
static void uplink_totals(long *rx, long *tx)
|
||
{
|
||
long trx = 0, ttx = 0, seen = 0;
|
||
for (int i = 0; i < g_iface_n; i++) {
|
||
if (iface_parse_kind(g_ifaces[i].name) == 0) continue; /* lo */
|
||
if (!g_ifaces[i].up) continue; /* 只取 up */
|
||
trx += g_ifaces[i].rx; ttx += g_ifaces[i].tx; seen = 1;
|
||
}
|
||
if (rx) *rx = seen ? trx : -1;
|
||
if (tx) *tx = seen ? ttx : 0;
|
||
}
|
||
|
||
/* iface IP:优先 `ip -o -4 addr show dev <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 包装 ---------------- */
|
||
/* hci0 真实 power 态:1=up 0=down -1=未知。sysfs `/sys/class/bluetooth/hci0/power` 优先,
|
||
* 回退 `hciconfig hci0` 行首 UP/DOWN(P1 之后从 bt_status 抽出,写操作复查也用它)。
|
||
* tmo_ms 只管回退那次 run_cmd:写后复查要能把它压进总预算,读路径传 RUNCMD_TMO_MS。 */
|
||
static int bt_powered_now(int tmo_ms)
|
||
{
|
||
char p[192];
|
||
snprintf(p, sizeof p, "%s/hci0/power", g_bt_dir);
|
||
char pw[8] = "";
|
||
if (read_file_txt(p, pw, sizeof pw) > 0 && pw[0]) {
|
||
if (!strcmp(pw, "on")) return 1;
|
||
if (!strcmp(pw, "off")) return 0;
|
||
}
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
|
||
char buf[256];
|
||
if (run_cmd(buf, sizeof buf, tmo_ms, cmd) <= 0) return -1;
|
||
char *ln = buf;
|
||
while (ln && *ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
char *q = ln;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (!strncmp(q, "UP", 2)) return 1;
|
||
if (!strncmp(q, "DOWN", 4)) return 0;
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
static int bt_acl_macs(char macs[][24], int max, int tmo_ms); /* 前向声明(bt_status 早于其定义) */
|
||
static void bt_status(bt_t *b)
|
||
{
|
||
memset(b, 0, sizeof *b);
|
||
b->paired_n = -1;
|
||
b->connected_n = -1;
|
||
char p[192];
|
||
snprintf(p, sizeof p, "%s/hci0", g_bt_dir);
|
||
if (access(p, F_OK) == 0) b->available = 1;
|
||
if (!b->available) return;
|
||
{ int pw = bt_powered_now(RUNCMD_TMO_MS); b->powered = pw > 0 ? 1 : 0; }
|
||
/* address:/sys/.../address 优先,回退 hciconfig "BD Address:" */
|
||
snprintf(p, sizeof p, "%s/hci0/address", g_bt_dir);
|
||
read_file_txt(p, b->address, sizeof b->address);
|
||
if (!b->address[0]) {
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 2>/dev/null", g_hciconfig);
|
||
char buf[256];
|
||
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
|
||
char *q = strstr(buf, "BD Address:");
|
||
if (q) {
|
||
q += 11;
|
||
while (*q == ' ') q++;
|
||
int i = 0;
|
||
while (*q && *q != ' ' && *q != '\n' && i < (int)sizeof b->address - 1) b->address[i++] = *q++;
|
||
b->address[i] = 0;
|
||
}
|
||
}
|
||
}
|
||
/* paired 数:bluetoothctl `paired-devices`(BlueZ 5.54 实测没有 `paired` 子命令,
|
||
* 旧实现 `paired` → "Invalid command in menu main"。`paired-devices` 才是已配对全量;
|
||
* `devices` 含未配对的历史缓存,只用于补名,不能当配对权威)。失败/空 → -1。 */
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired-devices 2>/dev/null", g_bluetoothctl);
|
||
char buf[512];
|
||
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) > 0) {
|
||
int n = 0;
|
||
char *ln = buf;
|
||
while (ln && *ln) {
|
||
char *nl = strchr(ln, '\n');
|
||
if (nl) *nl = 0;
|
||
char *q = ln;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (!strncmp(q, "Device", 6)) n++;
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
b->paired_n = n;
|
||
}
|
||
/* 已连接数:内核 ACL(hcitool con)权威;快速,不受 bluetoothctl 过滤/慢启动影响。 */
|
||
{
|
||
char macs[8][24];
|
||
b->connected_n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS);
|
||
}
|
||
}
|
||
|
||
/* ================================================================
|
||
* P1:蓝牙权威收口(BLUETOOTH_FULL_OVERHAUL_PLAN §六/§八 P1)。
|
||
* 权威路径设计(修复「已连接设备显示不全」根因):
|
||
* - 已连接 = 内核 ACL(hcitool con,快速/权威/不受 bluetoothctl 过滤影响),
|
||
* 名/配对来自 bluetoothctl `paired-devices`/`devices`(蓝虫子缓存)——不再依赖
|
||
* bluetoothctl `devices Connected`(慢 + 只反映 agent 缓存视图,易截断)。
|
||
* - A2DP 详情 = bluealsa-cli `list-pcms` → 每端点 `info <path>`(v4.1.1 没有 `a2dp list`)。
|
||
* - 扫描 = netmgrd 统一生命周期(scan on 才扫、off/超时收尾;缓存 /tmp/meow_bt_scan.txt)。
|
||
* 输出均为文本(m_net_cmd_result_t.text),launcher 只做客户端解析,不再 bluetoothctl popen。
|
||
*
|
||
* M49(2026-11-11)修掉 P1 遗留的三个「部署新件后依然存在」的真 bug:
|
||
* ① `bluetoothctl paired` 子命令不存在(BlueZ 5.54 → `paired-devices`);
|
||
* ② `bluealsa-cli a2dp list` 子命令不存在(v4.1.1 → `list-pcms` + `info`);
|
||
* ③ run_cmd 只回字节数、丢 stderr → 写操作恒报成功(改为四态 + 写后真态复查)。
|
||
* 另修:扫描缓存的实时 `[NEW] Device` 行以 ANSI ESC 开头,旧解析器只认行首 → 实时行全丢。
|
||
* ================================================================ */
|
||
static int bt_hex(int c)
|
||
{
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||
return -1;
|
||
}
|
||
static int bt_mac_eq(const char *a, const char *b)
|
||
{
|
||
for (int i = 0; i < 17; i++) {
|
||
if (bt_hex((unsigned char)a[i]) != bt_hex((unsigned char)b[i])) return 0;
|
||
if (i % 3 == 2 && a[i] != b[i]) return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
/* ---------------- 纯解析函数(宿主单测直接调用;不做 IO) ----------------
|
||
* 真机原始字节(`od -c /tmp/meow_bt_scan.txt`,2026-11-11 15:46):
|
||
* ESC [ 0 ; 9 4 m [bluetooth] ESC [ 0 m # \r ESC [ K [001 ESC [ 0 ; 9 2 m 002 N E W
|
||
* 001 ESC [ 0 m 002 ] D e v i c e 7 9 : 8 5 : ... 7 9 - 8 5 - ...
|
||
* 即:每行前缀是彩色提示符 + `\r\033[K`,事件标签 `[NEW]` 外层还套了 \001/\002 与控制序列,
|
||
* 而 `Device ` 根本不在行首 —— 只认行首的旧解析器把这些实时行全丢了。 */
|
||
|
||
/* 剥掉 ANSI 转义序列与终端控制字符,写 out(NUL 结尾;超长截断)。返回 out。
|
||
* 处理:CSI(ESC [ ... 终止字节 0x40..0x7E,含 ESC[K / ESC[P / 颜色码)、其余 ESC+x 单字节、
|
||
* \r \t 等 C0 控制符、DEL;>=0x80 的字节原样保留(UTF-8 设备名)。 */
|
||
static const char *bt_clean_ansi(const char *in, char *out, size_t cap)
|
||
{
|
||
size_t o = 0;
|
||
if (!out || cap == 0) return out;
|
||
for (const char *p = in; p && *p; p++) {
|
||
unsigned char c = (unsigned char)*p;
|
||
if (c == 0x1b) { /* ESC */
|
||
if (p[1] == '[') { /* CSI:跳到终止字节 */
|
||
p += 2;
|
||
while (*p && !((unsigned char)*p >= 0x40 && (unsigned char)*p <= 0x7e)) p++;
|
||
if (!*p) break;
|
||
continue;
|
||
}
|
||
if (p[1]) p++; /* ESC + 单字节(ESC 7 / ESC ( 等) */
|
||
continue;
|
||
}
|
||
if (c < 0x20 || c == 0x7f) continue; /* \r \t \001 \002 等一律丢弃 */
|
||
if (o + 1 < cap) out[o++] = (char)c;
|
||
}
|
||
out[o] = 0;
|
||
return out;
|
||
}
|
||
|
||
/* 解析 bluetoothctl 输出的一行 → "Device <MAC> <name>"。
|
||
* 输入 raw 可以是任意原始行(含 ANSI/提示符/标签);成功返 1 并写 mac[≥18]/name。
|
||
* 只认 `[NEW] Device `(发现事件)与裸 `Device `(`devices` 列表块);
|
||
* `[CHG]`/`[DEL]` 是属性变更/离开,不是「附近有这台」,返回 0。
|
||
* name 允许为空(截断输出),MAC 不完整(行被截断)返回 0。 */
|
||
static int bt_scan_line_parse(const char *raw, char *mac, size_t mcap, char *name, size_t ncap)
|
||
{
|
||
if (mac && mcap) mac[0] = 0;
|
||
if (name && ncap) name[0] = 0;
|
||
if (!raw) return 0;
|
||
char clean[512];
|
||
bt_clean_ansi(raw, clean, sizeof clean);
|
||
const char *p = clean;
|
||
/* 1) 提示符 "[bluetooth]# " 前缀(事件在它后面) */
|
||
for (;;) {
|
||
const char *pr = strstr(p, "[bluetooth]");
|
||
if (!pr) break;
|
||
const char *q = pr + 11;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (*q != '#') break;
|
||
q++;
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
p = q;
|
||
}
|
||
/* 2) 事件标签:取行内最早出现的那个;只有 NEW 算「发现」 */
|
||
size_t tag_off = 0;
|
||
int tag_kind = -1;
|
||
static const char *const kinds[3] = { "[NEW]", "[CHG]", "[DEL]" };
|
||
for (int i = 0; i < 3; i++) {
|
||
const char *t = strstr(p, kinds[i]);
|
||
if (t && (tag_kind < 0 || (size_t)(t - p) < tag_off)) {
|
||
tag_off = (size_t)(t - p);
|
||
tag_kind = i;
|
||
}
|
||
}
|
||
if (tag_kind > 0) return 0;
|
||
if (tag_kind == 0) p += tag_off + 5;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (strncmp(p, "Device ", 7) != 0) return 0;
|
||
p += 7;
|
||
/* 3) MAC:严格 17 字符 XX:XX:XX:XX:XX:XX,后面必须是空白或行尾 */
|
||
for (int i = 0; i < 17; i++) {
|
||
if (i % 3 == 2) { if (p[i] != ':') return 0; }
|
||
else if (bt_hex((unsigned char)p[i]) < 0) return 0;
|
||
}
|
||
char sep = p[17];
|
||
if (sep && sep != ' ' && sep != '\t') return 0;
|
||
if (mac && mcap > 17) { memcpy(mac, p, 17); mac[17] = 0; }
|
||
p += 17;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
size_t l = strlen(p);
|
||
while (l > 0 && (p[l - 1] == ' ' || p[l - 1] == '\t' || p[l - 1] == '\n' || p[l - 1] == '\r')) l--;
|
||
if (l && name && ncap) {
|
||
if (l > ncap - 1) l = ncap - 1;
|
||
memcpy(name, p, l); name[l] = 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* 解析 `hcitool con` 输出 → ACL MAC 列表(返回数量 ≤max)。
|
||
* 行样本:"\t< ACL 33:DE:2E:21:AD:30 handle 12 state 1 lm MASTER";只认 "< ACL" 行,
|
||
* MAC 不完整(截断)跳过;表头 "Connections:" 自然不匹配。 */
|
||
static int bt_acl_parse(const char *raw, char macs[][24], int max)
|
||
{
|
||
int n = 0;
|
||
if (!raw || max <= 0) return 0;
|
||
const char *line = raw;
|
||
while (line && *line && n < max) {
|
||
const char *nl = strchr(line, '\n');
|
||
const char *e = nl ? nl : line + strlen(line);
|
||
const char *acl = NULL;
|
||
for (const char *q = line; q + 6 <= e; q++) {
|
||
if (!strncmp(q, "< ACL ", 6)) { acl = q + 6; break; }
|
||
}
|
||
if (acl) {
|
||
int ok = 1;
|
||
for (int i = 0; i < 17 && ok; i++) {
|
||
if (i % 3 == 2) { if (acl[i] != ':') ok = 0; }
|
||
else if (bt_hex((unsigned char)acl[i]) < 0) ok = 0;
|
||
}
|
||
if (ok && acl + 17 <= e) {
|
||
memcpy(macs[n], acl, 17); macs[n][17] = 0; n++;
|
||
}
|
||
}
|
||
line = nl ? nl + 1 : NULL;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* bluealsa D-Bus 路径里的设备 MAC:/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc
|
||
* → 33:DE:2E:21:AD:30。成功返 1(mac 至少 18B)。 */
|
||
static int bt_path_mac(const char *path, char *mac, size_t mcap)
|
||
{
|
||
if (!mac || mcap < 18) return 0;
|
||
mac[0] = 0;
|
||
const char *d = path ? strstr(path, "dev_") : NULL;
|
||
if (!d) return 0;
|
||
d += 4;
|
||
char out[18];
|
||
int j = 0;
|
||
for (int g = 0; g < 6; g++) {
|
||
for (int k = 0; k < 2; k++) {
|
||
if (bt_hex((unsigned char)*d) < 0) return 0;
|
||
out[j++] = *d++;
|
||
}
|
||
if (g < 5) {
|
||
if (*d != '_') return 0;
|
||
d++;
|
||
out[j++] = ':';
|
||
}
|
||
}
|
||
out[17] = 0;
|
||
memcpy(mac, out, 18);
|
||
return 1;
|
||
}
|
||
|
||
/* 在 [s, e) 内找子串 needle(不跨行匹配;行内 strstr 会越过 '\n' 命中下一行)。
|
||
* 返回 1=命中。纯函数。 */
|
||
static int bt_mem_has(const char *s, const char *e, const char *needle)
|
||
{
|
||
size_t nl = strlen(needle);
|
||
if (!nl || e <= s) return 0;
|
||
for (const char *q = s; q + nl <= e; q++) {
|
||
if (!memcmp(q, needle, nl)) return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 解析 `bluealsa-cli list-pcms` → A2DP 端点路径(每行一个 /org/bluealsa/... 路径)。
|
||
* 只收 a2dpsrc/a2dpsnk(SCO/HFP 端点不算 A2DP);行尾空白/CR 去掉。返回数量(≤max)。 */
|
||
static int bt_pcm_paths_parse(const char *raw, char paths[][80], int max)
|
||
{
|
||
int n = 0;
|
||
if (!raw || max <= 0) return 0;
|
||
const char *line = raw;
|
||
while (line && *line && n < max) {
|
||
const char *nl = strchr(line, '\n');
|
||
const char *e = nl ? nl : line + strlen(line);
|
||
const char *p = line;
|
||
while (p < e && (*p == ' ' || *p == '\t')) p++;
|
||
while (e > p && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r')) e--;
|
||
if (e - p > 14 && (bt_mem_has(p, e, "/a2dpsrc") || bt_mem_has(p, e, "/a2dpsnk"))) {
|
||
size_t l = (size_t)(e - p);
|
||
if (l > 79) l = 79;
|
||
memcpy(paths[n], p, l); paths[n][l] = 0; n++;
|
||
}
|
||
line = nl ? nl + 1 : NULL;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
typedef struct {
|
||
char codec[16], rate[16], channels[8], bits[8], state[24];
|
||
int running; /* 1=在跑 0=停 -1=未知 */
|
||
int has; /* 至少命中一个字段 → 这份 info 输出可用 */
|
||
} bt_pcm_info_t;
|
||
|
||
/* 取 "Key: value" 里 Key 之后的值(到行尾,去首尾空白)。键必须整词匹配(避免
|
||
* CodecConfiguration 命中 Codec)。value 写 v(可空)。返回 1=命中。 */
|
||
static int bt_kv(const char *line, const char *key, char *v, size_t vcap)
|
||
{
|
||
size_t kl = strlen(key);
|
||
if (v && vcap) v[0] = 0;
|
||
if (strncmp(line, key, kl) != 0) return 0;
|
||
const char *p = line + kl;
|
||
/* 整词判定:键后面必须是分隔符或行尾(避免 "Codec" 命中 "CodecConfiguration") */
|
||
if (*p && *p != ':' && *p != '=' && *p != ' ' && *p != '\t') return 0;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (*p == ':' || *p == '=') { p++; while (*p == ' ' || *p == '\t') p++; }
|
||
if (v && vcap) {
|
||
size_t l = strlen(p);
|
||
while (l > 0 && (p[l - 1] == ' ' || p[l - 1] == '\t' || p[l - 1] == '\r' || p[l - 1] == '\n')) l--;
|
||
if (l > vcap - 1) l = vcap - 1;
|
||
memcpy(v, p, l); v[l] = 0;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* 解析 `bluealsa-cli info <path>` 输出。字段名按 BlueALSA PCM 属性(Codec / Sampling /
|
||
* Channels / Bits per Sample / State / Running / Transport);同时兼容 `Format:` 一行里
|
||
* 打包的 "<rate> Hz, <ch> channels, <bits> bits"。纯函数。 */
|
||
static void bt_pcm_info_parse(const char *raw, bt_pcm_info_t *o)
|
||
{
|
||
memset(o, 0, sizeof *o);
|
||
o->running = -1;
|
||
if (!raw) return;
|
||
char fmt[64] = "";
|
||
const char *line = raw;
|
||
while (line && *line) {
|
||
const char *nl = strchr(line, '\n');
|
||
const char *e = nl ? nl : line + strlen(line);
|
||
char lbuf[256];
|
||
size_t l = (size_t)(e - line);
|
||
if (l > sizeof lbuf - 1) l = sizeof lbuf - 1;
|
||
memcpy(lbuf, line, l); lbuf[l] = 0;
|
||
char clean[256];
|
||
bt_clean_ansi(lbuf, clean, sizeof clean);
|
||
char *p = clean;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
char v[64];
|
||
/* 真机 v4.1.1 的键名(M50 原文):Selected codec / Available codecs / Format: S16_LE /
|
||
* Running: true|false / Channels / Sampling / Transport / Mode / Delay / Volume / Muted。
|
||
* 注意 Available codecs 是空格分隔的**列表**,绝不能当 codec;Selected codec 才是当前值。 */
|
||
if (!o->codec[0] &&
|
||
(bt_kv(p, "Selected codec", v, sizeof v) || bt_kv(p, "Codec", v, sizeof v)) && v[0]) {
|
||
snprintf(o->codec, sizeof o->codec, "%s", v); o->has = 1;
|
||
} else if (!o->rate[0] && bt_kv(p, "Sampling", v, sizeof v) && v[0]) {
|
||
snprintf(o->rate, sizeof o->rate, "%s", v); o->has = 1;
|
||
} else if (!o->channels[0] && bt_kv(p, "Channels", v, sizeof v) && v[0]) {
|
||
snprintf(o->channels, sizeof o->channels, "%s", v); o->has = 1;
|
||
} else if (!o->bits[0] &&
|
||
(bt_kv(p, "Bits per Sample", v, sizeof v) || bt_kv(p, "Bits", v, sizeof v) ||
|
||
bt_kv(p, "Bit depth", v, sizeof v)) && v[0]) {
|
||
snprintf(o->bits, sizeof o->bits, "%s", v); o->has = 1;
|
||
} else if (bt_kv(p, "Format", v, sizeof v) && v[0]) {
|
||
snprintf(fmt, sizeof fmt, "%s", v); o->has = 1;
|
||
} else if (bt_kv(p, "Mode", v, sizeof v) && v[0]) {
|
||
o->has = 1; /* sink/source:本机侧 PCM 模式(不是 codec/不是 MAC) */
|
||
} else if (bt_kv(p, "State", v, sizeof v) && v[0]) {
|
||
snprintf(o->state, sizeof o->state, "%s", v); o->has = 1;
|
||
if (strstr(v, "RUNNING") || strstr(v, "ACTIVE") || strstr(v, "PLAYING")) o->running = 1;
|
||
else if (strstr(v, "IDLE") || strstr(v, "STOP") || strstr(v, "SETUP")) o->running = 0;
|
||
} else if (bt_kv(p, "Running", v, sizeof v) && v[0]) {
|
||
o->has = 1;
|
||
if (!strcmp(v, "true") || !strcmp(v, "yes")) o->running = 1;
|
||
else if (!strcmp(v, "false") || !strcmp(v, "no")) o->running = 0;
|
||
} else if (bt_kv(p, "Transport", v, sizeof v) && v[0]) {
|
||
o->has = 1;
|
||
}
|
||
line = nl ? nl + 1 : NULL;
|
||
}
|
||
/* Format 兜底:真机是 ALSA 格式串(S16_LE / S24_3LE),也可能是
|
||
* "44100 Hz, 2 channels, 16 bits" 这种打包写法。 */
|
||
if (fmt[0]) {
|
||
char *q;
|
||
if (!o->bits[0] && (fmt[0] == 'S' || fmt[0] == 'U')) {
|
||
char *n = fmt + 1;
|
||
int bw = 0;
|
||
while (*n >= '0' && *n <= '9') { bw = bw * 10 + (*n - '0'); n++; }
|
||
if (bw > 0 && bw <= 32) snprintf(o->bits, sizeof o->bits, "%d", bw);
|
||
}
|
||
if (!o->rate[0] && (q = strstr(fmt, "Hz")) != NULL) {
|
||
char *b = q;
|
||
while (b > fmt && (b[-1] == ' ' || b[-1] == '\t')) b--; /* 跳过 "Hz" 前的空格 */
|
||
char *num = b;
|
||
while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--;
|
||
if (num < b && b - num <= 6) {
|
||
size_t l = (size_t)(b - num);
|
||
if (l > sizeof o->rate - 1) l = sizeof o->rate - 1;
|
||
memcpy(o->rate, num, l); o->rate[l] = 0;
|
||
}
|
||
}
|
||
if (!o->channels[0] && (q = strstr(fmt, "channels")) != NULL) {
|
||
char *b = q;
|
||
while (b > fmt && (b[-1] == ' ')) b--;
|
||
char *num = b;
|
||
while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--;
|
||
if (num < b && b - num <= 3) {
|
||
size_t l = (size_t)(b - num);
|
||
memcpy(o->channels, num, l); o->channels[l] = 0;
|
||
}
|
||
}
|
||
if (!o->bits[0] && (q = strstr(fmt, "bits")) != NULL) {
|
||
char *b = q;
|
||
while (b > fmt && (b[-1] == ' ')) b--;
|
||
char *num = b;
|
||
while (num > fmt && num[-1] >= '0' && num[-1] <= '9') num--;
|
||
if (num < b && b - num <= 3) {
|
||
size_t l = (size_t)(b - num);
|
||
memcpy(o->bits, num, l); o->bits[l] = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 内核 ACL 连接 MAC(hcitool con)。返回数(≤max)。
|
||
* tmo_ms 由调用方给:读路径用 RUNCMD_TMO_MS,写后复查只给剩余预算。 */
|
||
static int bt_acl_macs(char macs[][24], int max, int tmo_ms)
|
||
{
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool);
|
||
char buf[1024];
|
||
if (run_cmd(buf, sizeof buf, tmo_ms, cmd) <= 0) return 0;
|
||
return bt_acl_parse(buf, macs, max);
|
||
}
|
||
|
||
/* 写后复查专用三态:1=有 ACL,0=确实没有,-1=**查不到**(命令失败/超时/被 kill)。
|
||
* 必须和「没有」分开:探测超时被当成「没有连接」的话,disconnect 就会在查不到的情况下
|
||
* 报「已断开」——那正是本单要消灭的「假成功」。 */
|
||
static int bt_acl_has_tri(const char *mac, int tmo_ms)
|
||
{
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s con 2>/dev/null", g_hcitool);
|
||
char buf[1024];
|
||
runcmd_res_t rr;
|
||
int n = run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr);
|
||
if (n < 0 || rr.status == RUNCMD_TIMEOUT || rr.status == RUNCMD_EXEC ||
|
||
rr.status == RUNCMD_EXIT) return -1;
|
||
char macs[8][24];
|
||
int cnt = bt_acl_parse(buf, macs, 8);
|
||
for (int i = 0; i < cnt; i++) if (bt_mac_eq(macs[i], mac)) return 1;
|
||
return 0;
|
||
}
|
||
|
||
/* `bluetoothctl info <mac>` 的布尔字段真态(Paired / Connected / Trusted):
|
||
* 1=yes 0=no -1=未知(设备不在 BlueZ 列表里 / 后端失败)。 */
|
||
static int bt_info_flag(const char *mac, const char *key, int tmo_ms)
|
||
{
|
||
char cmd[224];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s info %s 2>&1", g_bluetoothctl, mac);
|
||
char buf[1024];
|
||
runcmd_res_t rr;
|
||
if (run_cmd_ex(buf, sizeof buf, tmo_ms, cmd, &rr) <= 0) return -1;
|
||
char keyp[32];
|
||
snprintf(keyp, sizeof keyp, "%s:", key);
|
||
const char *p = strstr(buf, keyp);
|
||
if (!p) return -1;
|
||
p += strlen(keyp);
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!strncmp(p, "yes", 3)) return 1;
|
||
if (!strncmp(p, "no", 2)) return 0;
|
||
return -1;
|
||
}
|
||
|
||
/* 设备名解析(bluetoothctl `paired-devices`)+`devices` 两次兜底;命中返 1。 */
|
||
static int bt_name_for_mac(const char *mac, char *name, int nlen)
|
||
{
|
||
if (name && nlen > 0) name[0] = 0;
|
||
static const char *subs[] = { "paired-devices", "devices" };
|
||
for (int s = 0; s < 2; s++) {
|
||
char cmd[176];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null",
|
||
g_bluetoothctl, subs[s]);
|
||
char buf[1024];
|
||
if (run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd) <= 0) continue;
|
||
char *line = buf;
|
||
while (line && *line) {
|
||
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||
char m[24], nm[64];
|
||
if (bt_scan_line_parse(line, m, sizeof m, nm, sizeof nm) && bt_mac_eq(m, mac)) {
|
||
if (nm[0]) {
|
||
int l = (int)strlen(nm); if (l > nlen - 1) l = nlen - 1;
|
||
memcpy(name, nm, (size_t)l); name[l] = 0;
|
||
} else {
|
||
memcpy(name, mac, 17); name[17] = 0;
|
||
}
|
||
return 1;
|
||
}
|
||
line = nl ? nl + 1 : NULL;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 已连接设备文本(权威=内核 ACL;名经 paired/devices 兜底)。行格式 "Device <MAC> <Name>"。 */
|
||
static void bt_connected_text(char *buf, int cap)
|
||
{
|
||
int off = 0;
|
||
char macs[8][24];
|
||
int n = bt_acl_macs(macs, 8, RUNCMD_TMO_MS);
|
||
for (int i = 0; i < n; i++) {
|
||
char name[64] = "";
|
||
bt_name_for_mac(macs[i], name, sizeof name);
|
||
int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", macs[i], name);
|
||
if (l <= 0 || off + l >= cap) break;
|
||
off += l;
|
||
}
|
||
buf[off] = 0;
|
||
}
|
||
|
||
/* A2DP 传输详情(M49 重写)。
|
||
* v4.1.1 的 bluealsa-cli **没有** `a2dp list` 子命令(旧实现恒失败 → 恒 "A2DP none")。
|
||
* 正确路径:`list-pcms` 拿端点路径 → 每个 a2dpsrc/a2dpsnk 端点 `info <path>` 取属性。
|
||
* 输出每端点一行:
|
||
* "A2DP <mac> codec=<..> rate=<..> channels=<n> bits=<n> acquired=<0|1> role=<src|snk|->"
|
||
* 前提(真机实测,塔 2026-11-11):端点路径形如 `/org/bluealsa/hci0/dev_XX_../a2dpsrc/sink`,
|
||
* 末端段是 PCM 模式而非 MAC,且**端点易失**——只有 client 打开 PCM 时 bluealsa 才暴露它,
|
||
* 断开后 list-pcms 立刻空、info 报 Object path not found。
|
||
* 所以:本函数是「此刻快照」;没有端点 → "A2DP none"(= 当前无 A2DP 活动,不是错误)。
|
||
* 字段缺失写 "-"(info 输出认得但字段名对不上时也照样出行,至少让 UI 看见端点在);info 查询
|
||
* 失败(端点刚好消失)就跳过该端点;一个都没有 → "A2DP none"。 */
|
||
static void bt_a2dp_query(char *buf, int cap, int budget_ms)
|
||
{
|
||
if (cap <= 0) return;
|
||
buf[0] = 0;
|
||
long deadline = mono_ms() + (budget_ms > 0 ? budget_ms : 1);
|
||
char cmd[176];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list-pcms 2>&1", g_bluealsa_cli);
|
||
char raw[1024];
|
||
runcmd_res_t rr;
|
||
long remain = deadline - mono_ms();
|
||
if (remain <= 0) { snprintf(buf, (size_t)cap, "A2DP none\n"); return; }
|
||
run_cmd_ex(raw, sizeof raw, (int)(remain > BT_A2DP_LIST_TMO_MS ? BT_A2DP_LIST_TMO_MS : remain), cmd, &rr);
|
||
char paths[4][80];
|
||
int np = bt_pcm_paths_parse(raw, paths, 4);
|
||
int off = 0;
|
||
for (int i = 0; i < np; i++) {
|
||
remain = deadline - mono_ms();
|
||
if (remain <= 0) break; /* 总预算用完:剩下的端点不再查(宁缺不卡) */
|
||
char mac[24] = "-";
|
||
if (!bt_path_mac(paths[i], mac, sizeof mac)) continue;
|
||
const char *role = strstr(paths[i], "a2dpsnk") ? "snk" : "src";
|
||
char cmd2[256];
|
||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s info %s 2>&1",
|
||
g_bluealsa_cli, paths[i]);
|
||
char ibuf[1024];
|
||
runcmd_res_t ir;
|
||
run_cmd_ex(ibuf, sizeof ibuf, (int)(remain > BT_A2DP_INFO_TMO_MS ? BT_A2DP_INFO_TMO_MS : remain),
|
||
cmd2, &ir);
|
||
/* 端点消失/查询失败:跳过,不编造。EMPTY(exit 0 无输出)不算失败——端点确实在。 */
|
||
if (ir.status != RUNCMD_OK && ir.status != RUNCMD_EMPTY) continue;
|
||
bt_pcm_info_t info;
|
||
bt_pcm_info_parse(ibuf, &info);
|
||
/* info 成功但字段名一个都没命中(版本差异)→ 仍出行,字段写 "-":
|
||
* 「端点存在」本身是真信息,报 "A2DP none" 会把在用的蓝牙音频说成「没有」。 */
|
||
int l = snprintf(buf + off, (size_t)(cap - off),
|
||
"A2DP %s codec=%s rate=%s channels=%s bits=%s acquired=%d role=%s\n",
|
||
mac, info.codec[0] ? info.codec : "-", info.rate[0] ? info.rate : "-",
|
||
info.channels[0] ? info.channels : "-", info.bits[0] ? info.bits : "-",
|
||
info.running > 0 ? 1 : 0, role);
|
||
if (l <= 0 || off + l >= cap) break;
|
||
off += l;
|
||
}
|
||
if (off == 0) snprintf(buf, (size_t)cap, "A2DP none\n");
|
||
else buf[off] = 0;
|
||
}
|
||
|
||
/* `bt a2dp` 命令入口:总预算 BT_A2DP_TEXT_BUDGET_MS(客户端 1500ms 窗口内)。 */
|
||
static void bt_a2dp_text(char *buf, int cap)
|
||
{
|
||
bt_a2dp_query(buf, cap, BT_A2DP_TEXT_BUDGET_MS);
|
||
}
|
||
|
||
/* status JSON 用的 a2dp 摘要缓存:TTL + 总预算,且**无连接时直接不查**(没 ACL 就没有 A2DP
|
||
* 端点,一次 fork 都不必花)。守护单线程、DNS 代理同一主循环,status 是高频路径。 */
|
||
static long g_bt_a2dp_cache_ms = 0;
|
||
static char g_bt_a2dp_codec[16] = "", g_bt_a2dp_rate[16] = "";
|
||
|
||
/* A2DP 摘要(首个带 codec/rate 的端点);budget_ms 为总预算,失败置空。纯读,不改缓存。 */
|
||
static void bt_a2dp_summary(char *codec, int clen, char *rate, int rlen, int budget_ms)
|
||
{
|
||
if (codec && clen > 0) codec[0] = 0;
|
||
if (rate && rlen > 0) rate[0] = 0;
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
bt_a2dp_query(buf, sizeof buf, budget_ms);
|
||
char *line = buf;
|
||
while (line && *line) {
|
||
char *nl = strchr(line, '\n'); if (nl) *nl = 0;
|
||
if (!strncmp(line, "A2DP ", 5)) {
|
||
const char *c = strstr(line, " codec=");
|
||
const char *r = strstr(line, " rate=");
|
||
if (c && codec) { c += 7; const char *e = c; while (*e && *e != ' ' && *e != '\n') e++;
|
||
int l = (int)(e - c); if (l > clen - 1) l = clen - 1; memcpy(codec, c, (size_t)l); codec[l] = 0; }
|
||
if (r && rate) { r += 6; const char *e = r; while (*e && *e != ' ' && *e != '\n') e++;
|
||
int l = (int)(e - r); if (l > rlen - 1) l = rlen - 1; memcpy(rate, r, (size_t)l); rate[l] = 0; }
|
||
if ((codec && codec[0]) || (rate && rate[0])) return;
|
||
}
|
||
line = nl ? nl + 1 : NULL;
|
||
}
|
||
}
|
||
|
||
/* 附近设备文本:netmgrd 权威扫描缓存(/tmp/meow_bt_scan.txt),行 "Device <MAC> <Name>"(去重)。
|
||
* M49:实时 `[NEW] Device` 行以 ANSI ESC 开头(见 bt_scan_line_parse 注释里的字节证据),
|
||
* 旧实现只认行首 "Device "/"[NEW] Device " → 实时行全丢,只能靠末尾 `devices` 块。
|
||
* `[DEL]` 的处理是「不新增」而不是「删掉已有」:bt_scan_on 每次扫描都用 `>` 整文件覆盖缓存,
|
||
* 所以跨会话不会累积;但**同一次扫描窗口内**先出现后离开的设备仍会留在列表里,且末尾
|
||
* `devices` 块本身含 BlueZ 历史缓存——所以这份列表是「本轮扫描见到过的设备」,不是「此刻在场」。 */
|
||
static void bt_devices_text(char *buf, int cap)
|
||
{
|
||
int off = 0;
|
||
if (cap <= 0) return;
|
||
buf[0] = 0;
|
||
FILE *f = fopen(g_bt_scan_cache, "r");
|
||
if (!f) return;
|
||
char seen[16][24]; int n_seen = 0;
|
||
char line[512];
|
||
while (fgets(line, sizeof line, f) && n_seen < 16) {
|
||
char mac[24], name[64];
|
||
if (!bt_scan_line_parse(line, mac, sizeof mac, name, sizeof name)) continue;
|
||
int dup = 0;
|
||
for (int i = 0; i < n_seen; i++) if (bt_mac_eq(seen[i], mac)) { dup = 1; break; }
|
||
if (dup) continue;
|
||
memcpy(seen[n_seen], mac, 18); seen[n_seen][18] = 0; n_seen++;
|
||
int l = snprintf(buf + off, (size_t)(cap - off), "Device %s %s\n", mac, name);
|
||
if (l <= 0 || off + l >= cap) break;
|
||
off += l;
|
||
}
|
||
fclose(f);
|
||
buf[off] = 0;
|
||
}
|
||
|
||
/* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。
|
||
* M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效,
|
||
* 宿主测试没法覆盖扫描链路)。 */
|
||
static long g_bt_scan_ms = 0;
|
||
static void bt_scan_on(void)
|
||
{
|
||
long t = mono_now();
|
||
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return;
|
||
g_bt_scan_ms = t;
|
||
char cmd[320];
|
||
snprintf(cmd, sizeof cmd,
|
||
"(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | "
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1 &", g_bluetoothctl, g_bt_scan_cache);
|
||
if (system(cmd) != 0) svc_log("bt scan on: spawn failed (%s)", g_bt_scan_cache);
|
||
}
|
||
static void bt_scan_off(void)
|
||
{
|
||
char cmd[192];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan off 2>/dev/null", g_bluetoothctl);
|
||
system(cmd);
|
||
}
|
||
|
||
/* ---------------- rfkill:统一软开关 ---------------- */
|
||
static void rfkill_list(void)
|
||
{
|
||
int n = 0;
|
||
DIR *d = opendir(g_rfkill_dir);
|
||
if (d) {
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) && n < RFKILL_MAX) {
|
||
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
|
||
char p[448];
|
||
rfkill_t *r = &g_rfkill[n];
|
||
memset(r, 0, sizeof *r);
|
||
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
|
||
read_file_txt(p, r->type, sizeof r->type);
|
||
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
|
||
r->soft = read_file_int_def(p, -1);
|
||
snprintf(p, sizeof p, "%s/%s/hard", g_rfkill_dir, de->d_name);
|
||
r->hard = read_file_int_def(p, -1);
|
||
n++;
|
||
}
|
||
closedir(d);
|
||
}
|
||
for (int i = n; i < RFKILL_MAX; i++) memset(&g_rfkill[i], 0, sizeof(rfkill_t));
|
||
}
|
||
|
||
/* 找某类型 rfkill 设备并写 soft=block(1)/unblock(0)。返回 0=ok;-1=无该类型/写失败。 */
|
||
/* rfkill 软件开关。all=0 只写第一个同类型节点(旧行为),all=1 写**全部**同类型节点:
|
||
* 真机有 rfkill0/rfkill3 两个 bluetooth 节点,只写首个会让另一个仍处于 block。 */
|
||
static int rfkill_set_block_ex(const char *type, int block, int all)
|
||
{
|
||
char norm[16] = "";
|
||
if (!strcmp(type, "wifi")) snprintf(norm, sizeof norm, "%s", "wlan");
|
||
else if (!strcmp(type, "bt")) snprintf(norm, sizeof norm, "%s", "bluetooth");
|
||
else snprintf(norm, sizeof norm, "%s", type);
|
||
DIR *d = opendir(g_rfkill_dir);
|
||
int rc = -1;
|
||
if (d) {
|
||
struct dirent *de;
|
||
while ((de = readdir(d))) {
|
||
if (strncmp(de->d_name, "rfkill", 6) != 0) continue;
|
||
char p[448], t[16] = "";
|
||
snprintf(p, sizeof p, "%s/%s/type", g_rfkill_dir, de->d_name);
|
||
if (read_file_txt(p, t, sizeof t) <= 0 || strcmp(t, norm) != 0) continue;
|
||
snprintf(p, sizeof p, "%s/%s/soft", g_rfkill_dir, de->d_name);
|
||
int fd = open(p, O_WRONLY);
|
||
if (fd < 0) continue;
|
||
char one = block ? '1' : '0';
|
||
if (write(fd, &one, 1) == 1) rc = 0;
|
||
close(fd);
|
||
if (!all) break;
|
||
}
|
||
closedir(d);
|
||
}
|
||
return rc;
|
||
}
|
||
static int rfkill_set_block(const char *type, int block) { return rfkill_set_block_ex(type, block, 0); }
|
||
/* 蓝牙用「全节点」版本:任一节点仍 block 就等于没开(M47 finding 的第二个点)。 */
|
||
static int rfkill_set_block_all(const char *type, int block) { return rfkill_set_block_ex(type, block, 1); }
|
||
|
||
/* ---------------- udhcpc:续租/释放(SIGUSR1/SIGUSR2,不改启动路径) ---------------- */
|
||
static int udhcpc_ctl(const char *op, const char *ifname)
|
||
{
|
||
char pidfile[160];
|
||
const char *iff = (ifname && ifname[0]) ? ifname : g_wifi_if;
|
||
snprintf(pidfile, sizeof pidfile, "/var/run/udhcpc.%s.pid", iff);
|
||
char buf[32];
|
||
if (read_file_txt(pidfile, buf, sizeof buf) <= 0) return -1;
|
||
int pid = atoi(buf);
|
||
if (pid <= 0) return -1;
|
||
int sig = (strcmp(op, "release") == 0) ? SIGUSR2 : SIGUSR1;
|
||
return kill(pid, sig) == 0 ? 0 : -1;
|
||
}
|
||
|
||
/* ---------------- IPC 帧读写(同 svc_power 约定,全小端) ---------------- */
|
||
static int x_read_full(int fd, uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t r = read(fd, buf + off, n - off);
|
||
if (r <= 0) return -1;
|
||
off += (size_t)r;
|
||
}
|
||
return 0;
|
||
}
|
||
static int x_write_full(int fd, const uint8_t *buf, size_t n)
|
||
{
|
||
size_t off = 0;
|
||
while (off < n) {
|
||
ssize_t w = write(fd, buf + off, n - off);
|
||
if (w <= 0) return -1;
|
||
off += (size_t)w;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
|
||
{
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
uint32_t magic = MEOW_IPC_MAGIC, rlen = len;
|
||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||
memcpy(hdr + 0, &magic, 4);
|
||
memcpy(hdr + 4, &ver, 2);
|
||
memcpy(hdr + 6, &t, 2);
|
||
memcpy(hdr + 8, &req_id, 4);
|
||
memcpy(hdr + 12, &rlen, 4);
|
||
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
|
||
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
|
||
return 0;
|
||
}
|
||
|
||
static int send_ack(int fd, uint32_t req_id, uint32_t code, uint32_t status)
|
||
{
|
||
m_ack_t a;
|
||
memset(&a, 0, sizeof a);
|
||
a.code = code;
|
||
a.status = status;
|
||
return send_frame(fd, MSG_ACK, req_id, &a, sizeof a);
|
||
}
|
||
|
||
static int send_err(int fd, uint32_t req_id, uint32_t code, const char *msg)
|
||
{
|
||
m_err_t e;
|
||
memset(&e, 0, sizeof e);
|
||
e.code = code;
|
||
uint16_t ml = msg ? (uint16_t)strlen(msg) : 0;
|
||
if (ml > 63) ml = 63;
|
||
e.msg_len = ml;
|
||
if (ml) memcpy(e.msg, msg, ml);
|
||
return send_frame(fd, MSG_ERR, req_id, &e, sizeof e);
|
||
}
|
||
|
||
/* json/text 结果通发(m_net_status_result_t / m_net_iface_result_t / m_net_cmd_result_t 布局同构
|
||
* code(4)+len(2)+pad(2)+payload,payload ≤ MEOW_IPC_MAX-8)。 */
|
||
static int send_payload_result(int fd, uint16_t type, uint32_t req_id, uint32_t code,
|
||
const char *payload, size_t pl)
|
||
{
|
||
m_net_cmd_result_t r; /* 复用布局最大的那个(payload 区 4088B)。 */
|
||
memset(&r, 0, sizeof r);
|
||
r.code = code;
|
||
if (pl > sizeof r.text - 1) pl = sizeof r.text - 1;
|
||
r.text_len = (uint16_t)pl;
|
||
memcpy(r.text, payload, pl);
|
||
r.text[pl] = 0;
|
||
return send_frame(fd, type, req_id, &r, (uint32_t)(8 + pl + 1));
|
||
}
|
||
|
||
static int send_net_status_result(int fd, uint32_t req_id, const char *json)
|
||
{
|
||
return send_payload_result(fd, MSG_NET_STATUS_RESULT, req_id, MEOW_OK, json, strlen(json));
|
||
}
|
||
static int send_net_iface_result(int fd, uint32_t req_id, uint32_t code, const char *json)
|
||
{
|
||
return send_payload_result(fd, MSG_NET_IFACE_RESULT, req_id, code, json, strlen(json));
|
||
}
|
||
static int send_net_wifi_result(int fd, uint32_t req_id, const ctl_result_t *r)
|
||
{
|
||
return send_payload_result(fd, MSG_NET_WIFI_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
|
||
}
|
||
static int send_net_bt_result(int fd, uint32_t req_id, const ctl_result_t *r)
|
||
{
|
||
return send_payload_result(fd, MSG_NET_BT_RESULT, req_id, (uint32_t)r->code, r->text, strlen(r->text));
|
||
}
|
||
|
||
/* ---------------- 守护进程态 ---------------- */
|
||
static int g_mode_meow = 0; /* meow=1 全功能;0=只读降级 */
|
||
static long g_net_last_rx = -1, g_net_last_tx = 0, g_net_last_mono = 0;
|
||
static long g_net_rate_rx = -1, g_net_rate_tx = -1; /* B/s;-1=未知 */
|
||
static char g_prev_if[IFACE_MAX][32];
|
||
static int g_prev_if_up[IFACE_MAX];
|
||
static int g_prev_if_n = 0;
|
||
|
||
/* ---- 期 5:公网/内网流量统计(netlink sock_diag,按 TCP 连接累计字节;UDP 无字节计数,诚实标注)。
|
||
* 全部静态表(禁 malloc);每 2s 节拍在 census_run 内刷新,build_status_json 只读序列化。 ---- */
|
||
typedef struct { char entity[48]; uint64_t pub_rx, pub_tx; /* 远端为公网的 TCP 累计 */
|
||
uint64_t lan_rx, lan_tx; } pub_ent_t; /* 远端为内网/本链路的 TCP 累计 */
|
||
static pub_ent_t g_pub_ent[ENTITY_MAX];
|
||
static int g_pub_ent_n = 0;
|
||
typedef struct { uint32_t inode; uint64_t last_rx, last_tx; long seen_gen; } flow_last_t; /* inode→上拍累计(增量用) */
|
||
static flow_last_t g_flow_last[FLOW_LAST_MAX];
|
||
static int g_flow_last_n = 0;
|
||
static long g_flow_gen = 0; /* 每拍代际(剔陈旧 inode 用) */
|
||
typedef struct { uint32_t remote_ip; uint16_t remote_port; char entity[48]; char domain[48];
|
||
uint64_t rx, tx; long last_seen; } pub_flow_t; /* 公网字节 top 流 */
|
||
static pub_flow_t g_pub_flow[PUB_FLOW_MAX];
|
||
static int g_pub_flow_n = 0;
|
||
static uint64_t g_pub_rx = 0, g_pub_tx = 0; /* 公网总收发累计(TCP) */
|
||
static int g_diag_fd = -1; /* NETLINK_SOCK_DIAG(懒开) */
|
||
static int g_diag_warned = 0; /* 打开失败仅告警一次 */
|
||
|
||
/* ---------------- JSON 构建:status/iface ---------------- */
|
||
static void iface_json_obj(jw_t *w, const iface_t *f)
|
||
{
|
||
jadd(w, "{\"name\":\"%s\",\"kind\":\"%s\",\"up\":%s,", f->name, f->kind,
|
||
f->up ? "true" : "false");
|
||
jadd_str_or_null(w, "ip", f->ip);
|
||
jadd_str_or_null(w, "gw", f->gw);
|
||
jadd(w, "\"rx_bytes\":%ld,\"tx_bytes\":%ld}", f->rx, f->tx);
|
||
}
|
||
|
||
/* status JSON 里的公网/内网流量子块。TOP-K 序列化保住 4096 单帧预算;数据侧仍全量累计。
|
||
* 口径:netlink sock_diag 按 TCP 连接(远端分公网/内网)累计字节;UDP/ARP/mDNS 无字节计数,标 tcp_only。
|
||
* 注意:本函数只闭合 pub_bytes 对象(}, 不闭合外层 status 对象,由调用方收尾)。 */
|
||
static void pub_bytes_json(jw_t *w)
|
||
{
|
||
jadd(w, ",\"pub_bytes\":{");
|
||
jadd(w, "\"source\":\"sock_diag_tcp\",\"tcp_only\":true,");
|
||
jadd(w, "\"total\":{\"rx\":%llu,\"tx\":%llu},", (unsigned long long)g_pub_rx, (unsigned long long)g_pub_tx);
|
||
|
||
/* by_entity:按 pub+lan total 取 TOP-K(量小,线性选择)。 */
|
||
jadd(w, "\"by_entity\":[");
|
||
{
|
||
int sel[PUB_SERVE_ENT], seln = 0;
|
||
for (int i = 0; i < g_pub_ent_n; i++) {
|
||
uint64_t s = g_pub_ent[i].pub_rx + g_pub_ent[i].pub_tx +
|
||
g_pub_ent[i].lan_rx + g_pub_ent[i].lan_tx;
|
||
if (seln < PUB_SERVE_ENT) { sel[seln++] = i; continue; }
|
||
int mi = 0;
|
||
for (int j = 1; j < seln; j++) {
|
||
pub_ent_t *a = &g_pub_ent[sel[j]];
|
||
pub_ent_t *b = &g_pub_ent[sel[mi]];
|
||
if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx <
|
||
b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) mi = j;
|
||
}
|
||
pub_ent_t *m = &g_pub_ent[sel[mi]];
|
||
if (m->pub_rx + m->pub_tx + m->lan_rx + m->lan_tx < s) sel[mi] = i;
|
||
}
|
||
for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++) {
|
||
pub_ent_t *a = &g_pub_ent[sel[i]], *b = &g_pub_ent[sel[j]];
|
||
if (a->pub_rx + a->pub_tx + a->lan_rx + a->lan_tx <
|
||
b->pub_rx + b->pub_tx + b->lan_rx + b->lan_tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; }
|
||
}
|
||
for (int k = 0; k < seln; k++) {
|
||
pub_ent_t *e = &g_pub_ent[sel[k]];
|
||
if (k) jadd(w, ",");
|
||
jadd(w, "{\"entity\":\"%s\",\"pub\":[%llu,%llu],\"lan\":[%llu,%llu]}",
|
||
e->entity, (unsigned long long)e->pub_rx, (unsigned long long)e->pub_tx,
|
||
(unsigned long long)e->lan_rx, (unsigned long long)e->lan_tx);
|
||
}
|
||
}
|
||
jadd(w, "],");
|
||
|
||
/* by_flow:仅公网 top 流。 */
|
||
jadd(w, "\"by_flow\":[");
|
||
{
|
||
int sel[PUB_SERVE_FLOW], seln = 0;
|
||
for (int i = 0; i < g_pub_flow_n; i++) {
|
||
uint64_t s = g_pub_flow[i].rx + g_pub_flow[i].tx;
|
||
if (seln < PUB_SERVE_FLOW) { sel[seln++] = i; continue; }
|
||
int mi = 0;
|
||
for (int j = 1; j < seln; j++)
|
||
if (g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx <
|
||
g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx) mi = j;
|
||
if (g_pub_flow[sel[mi]].rx + g_pub_flow[sel[mi]].tx < s) sel[mi] = i;
|
||
}
|
||
for (int i = 0; i < seln; i++) for (int j = i + 1; j < seln; j++)
|
||
if (g_pub_flow[sel[i]].rx + g_pub_flow[sel[i]].tx <
|
||
g_pub_flow[sel[j]].rx + g_pub_flow[sel[j]].tx) { int t = sel[i]; sel[i] = sel[j]; sel[j] = t; }
|
||
for (int k = 0; k < seln; k++) {
|
||
pub_flow_t *f = &g_pub_flow[sel[k]];
|
||
char ip[32];
|
||
snprintf(ip, sizeof ip, "%u.%u.%u.%u",
|
||
(f->remote_ip >> 24) & 0xff, (f->remote_ip >> 16) & 0xff,
|
||
(f->remote_ip >> 8) & 0xff, f->remote_ip & 0xff);
|
||
if (k) jadd(w, ",");
|
||
jadd(w, "{\"ip\":\"%s\",\"port\":%u,\"entity\":\"%s\",\"domain\":\"%s\",\"rx\":%llu,\"tx\":%llu}",
|
||
ip, (unsigned)f->remote_port, f->entity, f->domain,
|
||
(unsigned long long)f->rx, (unsigned long long)f->tx);
|
||
}
|
||
}
|
||
jadd(w, "]");
|
||
jadd(w, "}");
|
||
}
|
||
|
||
static void build_status_json(char *out, size_t cap)
|
||
{
|
||
jw_t w = { out, cap, 0 };
|
||
out[0] = 0;
|
||
jadd(&w, "{\"mode\":\"%s\",\"boot_ts\":\"", g_mode_meow ? "meow" : "readonly");
|
||
{
|
||
char ts[64]; get_bootts(ts, sizeof ts);
|
||
for (const char *p = ts; *p; p++) w.buf[w.off++] = *p;
|
||
}
|
||
jadd(&w, "\",\"ts\":%ld,", (long)time(NULL));
|
||
|
||
enum_ifaces(1);
|
||
jadd(&w, "\"ifaces\":[");
|
||
for (int i = 0; i < g_iface_n; i++) { if (i) jadd(&w, ","); iface_json_obj(&w, &g_ifaces[i]); }
|
||
jadd(&w, "],");
|
||
|
||
wifi_t wi;
|
||
wifi_status(&wi);
|
||
jadd(&w, "\"wifi\":{");
|
||
jadd_q(&w, "state", wi.state);
|
||
jadd(&w, "\"connected\":%s,", wi.connected ? "true" : "false");
|
||
jadd_str_or_null(&w, "ssid", wi.ssid);
|
||
jadd_str_or_null(&w, "ip", wi.ip);
|
||
if (wi.has_rssi) jadd(&w, "\"rssi\":%d,", wi.rssi);
|
||
else jadd(&w, "\"rssi\":null,");
|
||
jadd(&w, "\"networks\":null},");
|
||
|
||
bt_t bt;
|
||
bt_status(&bt);
|
||
jadd(&w, "\"bt\":{");
|
||
jadd(&w, "\"available\":%s,\"powered\":%s,", bt.available ? "true" : "false",
|
||
bt.powered ? "true" : "false");
|
||
jadd_str_or_null(&w, "address", bt.address);
|
||
jadd_int_or_null(&w, "paired", bt.paired_n);
|
||
jadd_int_or_null(&w, "connected", bt.connected_n);
|
||
/* a2dp_codec/a2dp_rate 是 MeowPanel btCard 的字段(docs/plan/FUTURE_MEOWPANEL.md:48),
|
||
* 但 status 是高频路径且守护单线程(DNS 代理同环)——所以:无连接直接不查(0 次 fork),
|
||
* 有连接时走 TTL 缓存,刷新也只给 BT_A2DP_STATUS_BUDGET_MS。`bt a2dp` 命令另有独立调用。 */
|
||
if (bt.connected_n > 0) {
|
||
long now_s = mono_now();
|
||
if (!g_bt_a2dp_cache_ms || now_s - g_bt_a2dp_cache_ms >= BT_A2DP_CACHE_SEC) {
|
||
g_bt_a2dp_cache_ms = now_s;
|
||
bt_a2dp_summary(g_bt_a2dp_codec, sizeof g_bt_a2dp_codec,
|
||
g_bt_a2dp_rate, sizeof g_bt_a2dp_rate, BT_A2DP_STATUS_BUDGET_MS);
|
||
}
|
||
} else {
|
||
g_bt_a2dp_cache_ms = 0;
|
||
g_bt_a2dp_codec[0] = 0;
|
||
g_bt_a2dp_rate[0] = 0;
|
||
}
|
||
jadd_str_or_null(&w, "a2dp_codec", g_bt_a2dp_codec);
|
||
jadd_str_or_null(&w, "a2dp_rate", g_bt_a2dp_rate);
|
||
jadd(&w, "\"dns\":[");
|
||
{
|
||
char ns[DNS_MAX][40];
|
||
int n = 0;
|
||
dns_read(ns, &n);
|
||
for (int i = 0; i < n; i++) { if (i) jadd(&w, ","); jadd(&w, "\"%s\"", ns[i]); }
|
||
}
|
||
jadd(&w, "]},"); /* 关闭 dns[] + bt 对象 + 逗号(dns 是 bt 末字段,不能留尾逗号;此前漏收 bt 致 rfkill/rate 被吞、root 不闭合) */
|
||
|
||
rfkill_list();
|
||
jadd(&w, "\"rfkill\":[");
|
||
for (int i = 0; i < RFKILL_MAX && g_rfkill[i].type[0]; i++) {
|
||
if (i) jadd(&w, ",");
|
||
jadd(&w, "{\"type\":\"%s\",\"soft\":%d,\"hard\":%d}", g_rfkill[i].type,
|
||
g_rfkill[i].soft, g_rfkill[i].hard);
|
||
}
|
||
jadd(&w, "],");
|
||
|
||
jadd(&w, "\"rate\":{\"rx_bps\":");
|
||
if (g_net_rate_rx < 0) jadd(&w, "null,");
|
||
else jadd(&w, "%ld,", g_net_rate_rx);
|
||
jadd(&w, "\"tx_bps\":");
|
||
if (g_net_rate_tx < 0) jadd(&w, "null},"); /* rate object 闭合 + 逗号(tx_bps 为 rate 末字段) */
|
||
else jadd(&w, "%ld},", g_net_rate_tx);
|
||
|
||
/* 期4:SDK 自愿上报的 per-实体字节统计(§4 字节级记账自报通道);空则 []。 */
|
||
jadd(&w, "\"entity_bytes\":[");
|
||
for (int i = 0; i < g_ent_bytes_n; i++) {
|
||
if (i) jadd(&w, ",");
|
||
jadd(&w, "{\"entity\":\"%s\",\"rx\":%u,\"tx\":%u}", g_ent_bytes[i].entity,
|
||
g_ent_bytes[i].rx, g_ent_bytes[i].tx);
|
||
}
|
||
jadd(&w, "]");
|
||
/* 期5:公网/内网流量(netlink sock_diag)——只闭合 pub_bytes 对象,外层 status 由下方 `}` 收尾。 */
|
||
pub_bytes_json(&w);
|
||
jadd(&w, "}");
|
||
}
|
||
|
||
/* 单/全接口详情 JSON。name 空=全部(数组);否则单对象。返回 MEOW_OK / MEOW_ERR_NOENT。 */
|
||
static int build_iface_json(const char *name, char *out, size_t cap)
|
||
{
|
||
enum_ifaces(1);
|
||
jw_t w = { out, cap, 0 };
|
||
out[0] = 0;
|
||
if (!name || !name[0]) {
|
||
jadd(&w, "[");
|
||
for (int i = 0; i < g_iface_n; i++) {
|
||
if (i) jadd(&w, ",");
|
||
iface_json_obj(&w, &g_ifaces[i]);
|
||
}
|
||
jadd(&w, "]");
|
||
return MEOW_OK;
|
||
}
|
||
for (int i = 0; i < g_iface_n; i++) {
|
||
if (strcmp(g_ifaces[i].name, name) == 0) {
|
||
iface_json_obj(&w, &g_ifaces[i]);
|
||
/* 去掉尾 } 补 dns(期 1 不分接口,统一读 resolv.conf)。 */
|
||
if (w.off >= 2 && w.buf[w.off - 1] == '}') w.buf[--w.off] = 0;
|
||
jadd(&w, ",\"dns\":[");
|
||
char ns[DNS_MAX][40];
|
||
int n = 0;
|
||
dns_read(ns, &n);
|
||
for (int i2 = 0; i2 < n; i2++) { if (i2) jadd(&w, ","); jadd(&w, "\"%s\"", ns[i2]); }
|
||
jadd(&w, "]}");
|
||
return MEOW_OK;
|
||
}
|
||
}
|
||
return MEOW_ERR_NOENT;
|
||
}
|
||
|
||
/* ================= 期 2:观测层(连接普查)+ DNS 代理(NETMGR_DESIGN §4/§5/§8) ================= */
|
||
/* ---- 连接/实体/DNS/规则/事件去重 静态表(禁 malloc) ---- */
|
||
typedef struct {
|
||
uint32_t local_ip; /* dotted-printable uint32(b0<<24|b1<<16|b2<<8|b3) */
|
||
uint32_t remote_ip;
|
||
uint16_t local_port;
|
||
uint16_t remote_port;
|
||
uint8_t state_code; /* tcp state hex;udp 用 0 */
|
||
uint8_t type; /* 0=tcp 1=udp */
|
||
uint32_t inode; /* socket inode(归因键;0=无法归因) */
|
||
int pid; /* 持有 socket 的 pid(0=未知) */
|
||
char entity[48]; /* app:<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") ||
|
||
!strcmp(cmd, "connected") || !strcmp(cmd, "a2dp") || !strcmp(cmd, "devices")) return 0;
|
||
return 1; /* on/off/connect/disconnect/pair/unpair/scan */
|
||
}
|
||
|
||
/* 写级授权决策(纯函数,宿主自测用;handle_conn 复用):读级任意;写级仅系统身份 + MeowOS 模式。
|
||
* 返回 MEOW_OK(放行)或 MEOW_ERR_PERM(拒绝)。 */
|
||
static int wifi_write_authorize(const caller_t *c, int mode_meow, const char *cmd, const char *arg)
|
||
{
|
||
if (!wifi_cmd_is_write(cmd, arg)) return MEOW_OK;
|
||
if (!is_sys_identity(c)) return MEOW_ERR_PERM;
|
||
if (!mode_meow) return MEOW_ERR_PERM;
|
||
return MEOW_OK;
|
||
}
|
||
static int bt_write_authorize(const caller_t *c, int mode_meow, const char *cmd)
|
||
{
|
||
if (!bt_cmd_is_write(cmd)) return MEOW_OK;
|
||
if (!is_sys_identity(c)) return MEOW_ERR_PERM;
|
||
if (!mode_meow) return MEOW_ERR_PERM;
|
||
return MEOW_OK;
|
||
}
|
||
|
||
/* ---------------- WiFi/BT/rfkill/udhcpc 动作执行(dæmon 侧;写级已在上游鉴权) ---------------- */
|
||
static const char *str_capsafe(const char *s, char *tmp, size_t cap)
|
||
{
|
||
size_t n = strlen(s);
|
||
while (n > 0 && (s[0] == ' ' || s[0] == '\t')) { s++; n--; }
|
||
while (n > 0 && (s[n-1] == ' ' || s[n-1] == '\t' || s[n-1] == '\n')) n--;
|
||
if (n >= cap) n = cap - 1;
|
||
memcpy(tmp, s, n); tmp[n] = 0;
|
||
return tmp;
|
||
}
|
||
|
||
/* wpa_cli 单命令封装:拼 PATH 前缀 + 有界收割(RUNCMD_TMO_MS)。返回 run_cmd 读到的字节数。 */
|
||
static int wpa_cmd(char *out, int cap, const char *args)
|
||
{
|
||
char cmd[512];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s 2>/dev/null", g_wpa_cli, args);
|
||
return run_cmd(out, cap, RUNCMD_TMO_MS, cmd);
|
||
}
|
||
|
||
/* WiFi 设置页 bug4-②(2026-09-15):list_networks/scan_results 对非 ASCII 字节输出 \xNN 转义
|
||
* (中文 SSID)——还原为原始字节串再与输入 SSID 做同表示比较;不还原则中文 SSID 永不命中
|
||
* 既有网络 → 新建同名 open 记录顶影原记录。net.c wpa_decode_ssid / netmgr_ipc.c nm_decode_ssid
|
||
* 已有两份同功能实现(后续可收拢,本次先在 netmgrd 补齐)。 */
|
||
static int wpa_hexval(char c)
|
||
{
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||
return -1;
|
||
}
|
||
static void wpa_unescape_ssid(const char *src, char *dst, size_t dcap)
|
||
{
|
||
size_t o = 0;
|
||
while (*src && o + 1 < dcap) {
|
||
if (src[0] == '\\' && src[1] == 'x' && wpa_hexval(src[2]) >= 0 && wpa_hexval(src[3]) >= 0) {
|
||
dst[o++] = (char)((wpa_hexval(src[2]) << 4) | wpa_hexval(src[3]));
|
||
src += 4;
|
||
} else {
|
||
dst[o++] = *src++;
|
||
}
|
||
}
|
||
dst[o] = 0;
|
||
}
|
||
|
||
/* WiFi 设置页 bug4-③(2026-09-15):save_config 结果校验。wpa_supplicant 仅在 conf 含
|
||
* update_config=1 时允许写盘;固件模板 /etc/wpa_supplicant.conf 无此项 → 首次 FAIL 时先
|
||
* `set update_config 1`(运行期开关,wpa_supplicant 标准用法;成功落盘后文件自带
|
||
* update_config=1)再重试一次。返回 1=已持久化;0=仍失败(调用方须在报文/日志明示
|
||
* 「记录未持久化」)。仅 MeowOS 模式调用(写盘红线不变,NETMGR_DESIGN §14)。 */
|
||
static int wifi_save_config(void)
|
||
{
|
||
char out[160];
|
||
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) return 1;
|
||
wpa_cmd(out, sizeof out, "set update_config 1");
|
||
if (wpa_cmd(out, sizeof out, "save_config") > 0 && strstr(out, "OK")) {
|
||
net_log("info", "write", "save_config 初次 FAIL → set update_config 1 重试成功(此后 conf 自带该位)");
|
||
return 1;
|
||
}
|
||
net_log("warn", "write", "save_config 失败(含 set update_config 1 重试):记录未持久化");
|
||
return 0;
|
||
}
|
||
|
||
/* WiFi 设置页 bug1(2026-09-15):连接状态机——有界等待 wpa_state=COMPLETED(每 200ms 查
|
||
* status,上限 timeout_ms,防卡死)。返回 1=已关联(out 带回 ssid/ip 供诚实报文);0=超时。 */
|
||
static int wifi_wait_completed(int timeout_ms, wifi_t *out)
|
||
{
|
||
int waited = 0;
|
||
for (;;) {
|
||
wifi_t w;
|
||
wifi_status(&w);
|
||
if (w.connected) { if (out) *out = w; return 1; }
|
||
if (waited >= timeout_ms) return 0;
|
||
struct timespec ts = { 0, 200 * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
waited += 200;
|
||
}
|
||
}
|
||
|
||
static void wifi_scan(ctl_result_t *r)
|
||
{
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan 2>/dev/null", g_wpa_cli);
|
||
char buf[128];
|
||
run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
|
||
/* P3(WiFi 设置页附带,2026-09-15):触发后 300ms→1500ms 再读结果——真机扫描常 1-2s,
|
||
* 300ms 读到的多是上一轮陈旧结果。IPC 客户端 per-cmd 超时相应放宽(netmgr_ipc scan=3.5s)。 */
|
||
struct timespec ts = { 1, 500 * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
char bufout[MEOW_IPC_MAX - 16];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan_results 2>/dev/null", g_wpa_cli);
|
||
int n = run_cmd(bufout, sizeof bufout, RUNCMD_TMO_MS, cmd);
|
||
if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "scan: no results (wpa_cli 空/失败)"); return; }
|
||
ctl_sets(r, MEOW_OK, bufout);
|
||
}
|
||
|
||
static void wifi_networks(ctl_result_t *r)
|
||
{
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s list_networks 2>/dev/null", g_wpa_cli);
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd);
|
||
if (n <= 0) { ctl_sets(r, MEOW_ERR_NODEV, "networks: no output (wpa_cli 空/失败)"); return; }
|
||
ctl_sets(r, MEOW_OK, buf);
|
||
}
|
||
|
||
/* WiFi 连接(bug1/bug2/bug4 修复,2026-09-15)。hidden=1 走隐藏网络流程(connecth):
|
||
* 对所用 id(新建/命中皆然)追加 scan_ssid=1,并把 COMPLETED 等待上限放宽(主动探测+关联更慢)。
|
||
* 流程:匹配既有(\xNN 还原同表示比较)→ 新建或(命中且带新凭据时)更新 psk →
|
||
* enable+select → 有界等待 wpa_state=COMPLETED(不再无条件报成功)→
|
||
* 成功:enable_network all(消除 select 的 disabled 副作用)+ save_config(校验+重试);
|
||
* 失败:本次新建的网络 remove 回收(防坏记录堆积),报文明示失败原因。 */
|
||
static void wifi_connect_ex(const char *arg, ctl_result_t *r, int hidden)
|
||
{
|
||
char ssid[96] = "", psk[96] = "";
|
||
if (arg && arg[0]) {
|
||
const char *comma = strchr(arg, ',');
|
||
if (comma) {
|
||
size_t sl = (size_t)(comma - arg);
|
||
if (sl >= sizeof ssid) sl = sizeof ssid - 1;
|
||
memcpy(ssid, arg, sl); ssid[sl] = 0;
|
||
snprintf(psk, sizeof psk, "%s", comma + 1);
|
||
} else {
|
||
snprintf(ssid, sizeof ssid, "%s", arg);
|
||
}
|
||
}
|
||
char tmp_ssid[96], tmp_psk[96];
|
||
str_capsafe(ssid, tmp_ssid, sizeof tmp_ssid);
|
||
str_capsafe(psk, tmp_psk, sizeof tmp_psk);
|
||
if (!tmp_ssid[0]) { ctl_sets(r, MEOW_ERR_INVAL, "connect: ssid 缺失"); return; }
|
||
|
||
/* 找既有网络:list_networks → 匹配 ssid(剥引号 + \xNN 还原后同表示比较);命中则复用其 id。 */
|
||
char cmd[256];
|
||
char list[1024];
|
||
int n = wpa_cmd(list, sizeof list, "list_networks");
|
||
int id = -1;
|
||
if (n > 0) {
|
||
char *ln = strtok(list, "\n");
|
||
while (ln) {
|
||
char *tab0 = strchr(ln, '\t');
|
||
if (tab0) {
|
||
int this_id = atoi(ln);
|
||
char *ss = tab0 + 1;
|
||
char *tab1 = strchr(ss, '\t');
|
||
if (tab1) *tab1 = 0;
|
||
char clean[128], unesc[160], norm_clean[128];
|
||
size_t cl = strlen(ss);
|
||
if (cl >= 2 && ss[0] == '"' && ss[cl-1] == '"') {
|
||
memcpy(clean, ss + 1, cl - 2); clean[cl - 2] = 0;
|
||
} else {
|
||
snprintf(clean, sizeof clean, "%s", ss);
|
||
}
|
||
wpa_unescape_ssid(clean, unesc, sizeof unesc); /* bug4-②:中文 SSID 转义还原 */
|
||
str_capsafe(unesc, norm_clean, sizeof norm_clean);
|
||
if (strcmp(norm_clean, tmp_ssid) == 0) id = this_id;
|
||
}
|
||
ln = strtok(NULL, "\n");
|
||
}
|
||
}
|
||
char out[128];
|
||
int created = 0;
|
||
if (id < 0) {
|
||
if (wpa_cmd(out, sizeof out, "add_network") <= 0) {
|
||
ctl_sets(r, MEOW_ERR_NODEV, "connect: add_network 失败 (wpa_cli 不可用?)");
|
||
return;
|
||
}
|
||
id = atoi(out);
|
||
if (id < 0) { ctl_sets(r, MEOW_ERR_INVAL, "connect: add_network 无 id"); return; }
|
||
created = 1;
|
||
snprintf(cmd, sizeof cmd, "set_network %d ssid '\"%s\"'", id, tmp_ssid);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
if (tmp_psk[0]) snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
|
||
else snprintf(cmd, sizeof cmd, "set_network %d key_mgmt NONE", id);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
} else if (tmp_psk[0]) {
|
||
/* bug4-①/bug1-④:命中既有网络且调用方带了新 psk → 先更新凭据再连
|
||
* (旧记录密码错误可经扫描路径重输修复;psk 为空则不动既有凭据)。 */
|
||
snprintf(cmd, sizeof cmd, "set_network %d psk '\"%s\"'", id, tmp_psk);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
}
|
||
if (hidden) {
|
||
/* bug2:隐藏 AP 必须 scan_ssid=1,supplicant 才会主动发 Probe Request(命中既有也要补)。 */
|
||
snprintf(cmd, sizeof cmd, "set_network %d scan_ssid 1", id);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
}
|
||
{ /* enable + select 该网络(复用/新建皆用)。 */
|
||
snprintf(cmd, sizeof cmd, "enable_network %d", id);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
snprintf(cmd, sizeof cmd, "select_network %d", id);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
}
|
||
/* bug1:有界等待 wpa_state=COMPLETED——密码错误/AP 不在范围/握手失败不再误报成功。 */
|
||
wifi_t w;
|
||
int wait_ms = hidden ? g_connecth_wait_ms : g_connect_wait_ms;
|
||
if (!wifi_wait_completed(wait_ms, &w)) {
|
||
if (created) { /* 本次新建且失败 → 回收防坏记录堆积(bug4 联动) */
|
||
snprintf(cmd, sizeof cmd, "remove_network %d", id);
|
||
wpa_cmd(out, sizeof out, cmd);
|
||
if (g_mode_meow) wifi_save_config();
|
||
}
|
||
net_log("info", "write", "wifi connect ssid=%s id=%d 超时未关联(%dms)", tmp_ssid, id, wait_ms);
|
||
char msg[128];
|
||
snprintf(msg, sizeof msg, "连接失败:密码错误或信号不佳(超时未关联)");
|
||
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||
return;
|
||
}
|
||
/* bug4-④:验证成功后 enable_network all 复位其它网络 disabled 态(select_network 副作用),
|
||
* 再 save_config 落盘(bug4-③:结果校验,FAIL 时 set update_config 1 重试)。
|
||
* 写盘仍受 MeowOS 模式门控(红线不变,NETMGR_DESIGN §14)。 */
|
||
int persisted = 0;
|
||
if (g_mode_meow) {
|
||
wpa_cmd(out, sizeof out, "enable_network all");
|
||
persisted = wifi_save_config();
|
||
}
|
||
net_log("info", "write", "wifi connect ssid=%s id=%d 已连接 ip=%s persisted=%d",
|
||
tmp_ssid, id, w.ip[0] ? w.ip : "-", persisted);
|
||
char msg[160];
|
||
snprintf(msg, sizeof msg, "已连接:%s ip=%s%s",
|
||
w.ssid[0] ? w.ssid : tmp_ssid, w.ip[0] ? w.ip : "-",
|
||
(g_mode_meow && !persisted) ? "(记录未持久化)" : "");
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
}
|
||
|
||
static void wifi_onoff(const char *cmd, ctl_result_t *r)
|
||
{
|
||
int block = strcmp(cmd, "off") == 0;
|
||
int rc = rfkill_set_block("wlan", block);
|
||
if (rc == 0) {
|
||
char msg[64];
|
||
snprintf(msg, sizeof msg, "wifi %s: rfkill wlan %s", cmd, block ? "blocked" : "unblocked");
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
} else {
|
||
ctl_sets(r, MEOW_ERR_NODEV, "wifi on/off: 无 wlan rfkill 设备或写失败");
|
||
}
|
||
}
|
||
|
||
static void rfkill_ctl(const char *arg, ctl_result_t *r)
|
||
{
|
||
char tmp[64];
|
||
str_capsafe(arg ? arg : "", tmp, sizeof tmp);
|
||
char *sp = strchr(tmp, ' ');
|
||
if (!sp) { ctl_sets(r, MEOW_ERR_INVAL, "rfkill: 用法 <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_ex(arg, r, 0);
|
||
} else if (!strcmp(cmd, "connecth")) {
|
||
wifi_connect_ex(arg, r, 1); /* bug2:隐藏网络(scan_ssid=1 + 更长 COMPLETED 等待) */
|
||
} else if (!strcmp(cmd, "disconnect")) {
|
||
char cmd2[160];
|
||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s disconnect 2>/dev/null", g_wpa_cli);
|
||
char buf[128];
|
||
int n = run_cmd(buf, sizeof buf, RUNCMD_TMO_MS, cmd2);
|
||
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, "wifi disconnect: 已下发");
|
||
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
|
||
wifi_onoff(cmd, r);
|
||
} else if (!strcmp(cmd, "forget")) {
|
||
int nid = arg ? atoi(arg) : -1;
|
||
if (nid < 0) { ctl_sets(r, MEOW_ERR_INVAL, "wifi forget 需 <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 只读由服务端门控)。
|
||
* bug4-③:save_config 结果校验(FAIL 时 set update_config 1 重试),报文不再谎称已落盘。 */
|
||
int persisted = 0;
|
||
if (g_mode_meow) persisted = wifi_save_config();
|
||
char msg[96];
|
||
snprintf(msg, sizeof msg, "wifi forget: network_id=%d 已移除%s", nid,
|
||
g_mode_meow ? (persisted ? "+save_config" : "(记录未持久化)") : "");
|
||
ctl_sets(r, n >= 0 ? MEOW_OK : MEOW_ERR_NODEV, msg);
|
||
} else if (!strcmp(cmd, "rfkill")) {
|
||
rfkill_ctl(arg, r);
|
||
} else if (!strcmp(cmd, "udhcpc")) {
|
||
udhcpc_ctl_svc(arg, r);
|
||
} else {
|
||
ctl_sets(r, MEOW_ERR_INVAL, "wifi: 未知子命令");
|
||
}
|
||
}
|
||
|
||
/* 取命令输出的一段可读文本(去 ANSI/控制符,连续空白折成一个空格;最多 cap-1 字符)。 */
|
||
static void bt_snip(const char *s, char *dst, size_t cap)
|
||
{
|
||
if (!dst || cap == 0) return;
|
||
dst[0] = 0;
|
||
char tmp[512];
|
||
bt_clean_ansi(s ? s : "", tmp, sizeof tmp);
|
||
size_t o = 0;
|
||
int prev_sp = 0;
|
||
for (size_t i = 0; tmp[i] && o + 1 < cap; i++) {
|
||
char c = tmp[i];
|
||
if (c == ' ' || c == '\t') {
|
||
if (prev_sp) continue;
|
||
c = ' '; prev_sp = 1;
|
||
} else prev_sp = 0;
|
||
dst[o++] = c;
|
||
}
|
||
while (o && dst[o - 1] == ' ') o--;
|
||
dst[o] = 0;
|
||
}
|
||
|
||
/* 探测函数带 tmo:写后复查的**总**预算必须传到每一次 run_cmd 里,否则「两次探测之间看
|
||
* deadline」挡不住探测自身那 1500ms(r1 评审 P1:实测 connect 1528ms / bt on 2424ms,
|
||
* 越过客户端 NM_TMO_DFL=1500 的 IPC 窗口 → 客户端判不可达 → 二次下发 + 假成功)。 */
|
||
typedef int (*bt_probe_fn)(const char *mac, int tmo_ms);
|
||
static int bt_probe_acl(const char *mac, int tmo_ms) { return bt_acl_has_tri(mac, tmo_ms); }
|
||
static int bt_probe_paired(const char *mac, int tmo_ms) { return bt_info_flag(mac, "Paired", tmo_ms); }
|
||
|
||
/* 熔断器状态(只服务 BT 写操作;守护单线程,无需加锁)。 */
|
||
static int g_bt_tmo_streak = 0;
|
||
static long g_bt_tmo_until = 0; /* 冷却截止(mono 秒),0=未熔断 */
|
||
|
||
static int bt_circuit_open(void)
|
||
{
|
||
return (g_bt_tmo_streak >= BT_TMO_STREAK_MAX && g_bt_tmo_until && mono_now() < g_bt_tmo_until);
|
||
}
|
||
static void bt_circuit_note(runcmd_status_t st)
|
||
{
|
||
if (st == RUNCMD_TIMEOUT) {
|
||
if (g_bt_tmo_streak < BT_TMO_STREAK_MAX) g_bt_tmo_streak++;
|
||
if (g_bt_tmo_streak >= BT_TMO_STREAK_MAX) g_bt_tmo_until = mono_now() + BT_TMO_COOLDOWN_SEC;
|
||
} else {
|
||
g_bt_tmo_streak = 0;
|
||
g_bt_tmo_until = 0;
|
||
}
|
||
}
|
||
|
||
/* 写后真态复查:总预算 budget_ms,**每次探测只拿剩余预算**,一旦命中 want 立刻返回。
|
||
* 返回最后一次探测值(1/0/-1=未知)。总耗时 ≤ budget_ms + 一次探测的收尾。 */
|
||
static int bt_poll_probe(bt_probe_fn fn, const char *mac, int want, int budget_ms)
|
||
{
|
||
long deadline = mono_ms() + budget_ms;
|
||
long remain = budget_ms;
|
||
int v = fn(mac, (int)(remain > 0 ? remain : 1));
|
||
while (v != want) {
|
||
remain = deadline - mono_ms();
|
||
if (remain <= 0) break;
|
||
struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
remain = deadline - mono_ms();
|
||
if (remain <= 0) break;
|
||
v = fn(mac, (int)remain);
|
||
}
|
||
return v;
|
||
}
|
||
|
||
/* bt on/off:顺序按 M47 finding 走 —— 开:先解 rfkill(全部节点)再 up(blocked 时 up 必败);
|
||
* 关:先 down 再 block。up 第一次没生效时在**剩余预算内**重试一次(射频刚放开常差一拍)。 */
|
||
static void bt_onoff(const char *cmd, ctl_result_t *r)
|
||
{
|
||
int up = strcmp(cmd, "on") == 0;
|
||
int want = up ? 1 : 0;
|
||
long deadline = mono_ms() + BT_ACT_SHORT_MS + BT_VERIFY_BUDGET_MS;
|
||
char cmd2[192];
|
||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s hci0 %s",
|
||
g_hciconfig, up ? "up" : "down");
|
||
char buf[256];
|
||
buf[0] = 0;
|
||
runcmd_res_t res;
|
||
memset(&res, 0, sizeof res);
|
||
res.exit_code = -1;
|
||
res.status = RUNCMD_EXEC;
|
||
|
||
if (bt_circuit_open()) {
|
||
snprintf(r->text, sizeof r->text,
|
||
"bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)",
|
||
cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now());
|
||
r->code = MEOW_ERR_NODEV;
|
||
return;
|
||
}
|
||
if (up) rfkill_set_block_all("bluetooth", 0); /* 先解 block,否则 up 会被拦下 */
|
||
run_cmd_ex(buf, sizeof buf, BT_ACT_SHORT_MS, cmd2, &res);
|
||
bt_circuit_note(res.status);
|
||
if (!up) rfkill_set_block_all("bluetooth", 1); /* 先 down 再 block */
|
||
|
||
int pw = -1, retried = 0;
|
||
for (;;) {
|
||
long remain = deadline - mono_ms();
|
||
if (remain <= 0) break;
|
||
pw = bt_powered_now((int)remain);
|
||
if (pw == want) break;
|
||
if (up && !retried) { /* up 未生效:剩余预算内重试一次 */
|
||
retried = 1;
|
||
remain = deadline - mono_ms();
|
||
if (remain > 0) {
|
||
run_cmd_ex(buf, sizeof buf, (int)(remain > BT_ACT_SHORT_MS ? BT_ACT_SHORT_MS : remain),
|
||
cmd2, &res);
|
||
continue;
|
||
}
|
||
}
|
||
remain = deadline - mono_ms();
|
||
if (remain <= 0) break;
|
||
struct timespec ts = { 0, BT_VERIFY_GAP_MS * 1000000L };
|
||
nanosleep(&ts, NULL);
|
||
}
|
||
char msg[280];
|
||
if (pw == want) {
|
||
snprintf(msg, sizeof msg, "bt %s: hci0 %s + rfkill %s(power 已复查)",
|
||
cmd, up ? "up" : "down", up ? "unblocked" : "blocked");
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
return;
|
||
}
|
||
int fail = 0;
|
||
const char *why = run_cmd_why(&res, &fail);
|
||
char snip[121];
|
||
bt_snip(buf, snip, sizeof snip);
|
||
snprintf(msg, sizeof msg, "bt %s: 未生效(powered=%s rfkill=%s%s%s%s%s)", cmd,
|
||
pw > 0 ? "on" : pw == 0 ? "off" : "unknown",
|
||
up ? "unblocked" : "blocked",
|
||
why[0] ? ";" : "", why,
|
||
(!why[0] && snip[0]) ? ";输出: " : "", (!why[0] && snip[0]) ? snip : "");
|
||
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||
}
|
||
|
||
/* connect/disconnect/pair/unpair:下发 + 按真态(hcitool con / bluetoothctl info)定稿。
|
||
* 旧实现 `n >= 0 → MEOW_OK` 恒报成功(命令失败也返回 0 字节),这是「点配对提示成功但没配上」的根因。
|
||
* 成功文案不再补设备名:那次 bluetoothctl 查询是 1-2 个额外子进程,会吃掉 1500ms 窗口;
|
||
* UI 本来就按 MAC join 名字(nm_parse_devices)。 */
|
||
static void bt_write_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||
{
|
||
int is_conn = !strcmp(cmd, "connect"), is_disc = !strcmp(cmd, "disconnect");
|
||
int is_pair = !strcmp(cmd, "pair"), is_unpair = !strcmp(cmd, "unpair");
|
||
const char *sub = is_unpair ? "remove" : cmd; /* bluetoothctl 取消配对=remove */
|
||
int tmo = (is_conn || is_pair) ? BT_ACT_TMO_MS : BT_ACT_SHORT_MS;
|
||
if (bt_circuit_open()) { /* 连续超时:不再 fork,如实回熔断 */
|
||
snprintf(r->text, sizeof r->text,
|
||
"bt %s: 熔断中(连续 %d 次命令超时,%ld 秒内不再下发;蓝牙栈可能卡住)",
|
||
cmd, g_bt_tmo_streak, g_bt_tmo_until - mono_now());
|
||
r->code = MEOW_ERR_NODEV;
|
||
return;
|
||
}
|
||
char cmd2[224];
|
||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s %s %s",
|
||
g_bluetoothctl, sub, arg);
|
||
char out[512];
|
||
runcmd_res_t res;
|
||
run_cmd_ex(out, sizeof out, tmo, cmd2, &res);
|
||
bt_circuit_note(res.status);
|
||
int fail = 0;
|
||
const char *why = run_cmd_why(&res, &fail);
|
||
char snip[121];
|
||
bt_snip(out, snip, sizeof snip);
|
||
char msg[300];
|
||
|
||
char tail[220];
|
||
tail[0] = 0;
|
||
if (why[0]) snprintf(tail, sizeof tail, ";%s", why);
|
||
if (snip[0]) {
|
||
size_t l = strlen(tail);
|
||
if (l < sizeof tail - 1) snprintf(tail + l, sizeof tail - l, ";输出: %s", snip);
|
||
}
|
||
|
||
if (is_conn || is_disc) {
|
||
int want = is_conn ? 1 : 0;
|
||
int v = bt_poll_probe(bt_probe_acl, arg, want, BT_VERIFY_BUDGET_MS);
|
||
if (v == want) {
|
||
snprintf(msg, sizeof msg, "bt %s: %s %s(hcitool con 已复查)", cmd,
|
||
is_conn ? "已连接" : "已断开", arg);
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
return;
|
||
}
|
||
snprintf(msg, sizeof msg, "bt %s: 未生效:%s(hcitool con %s%s)", cmd, arg,
|
||
is_conn ? "无该 ACL" : "仍有该 ACL", tail);
|
||
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||
return;
|
||
}
|
||
|
||
/* pair / unpair:以 bluetoothctl info 的 Paired: 字段为准 */
|
||
int want = is_pair ? 1 : 0;
|
||
int v = bt_poll_probe(bt_probe_paired, arg, want, BT_VERIFY_BUDGET_MS);
|
||
int ok = (v == want);
|
||
if (!ok && is_unpair && v < 0 && !fail) ok = 1; /* remove 后设备整体消失(info 查不到)也算已解除 */
|
||
if (ok) {
|
||
snprintf(msg, sizeof msg, "bt %s: %s %s(bluetoothctl info Paired:%s 已复查)", cmd,
|
||
is_pair ? "已配对" : "已解除配对", arg, v > 0 ? "yes" : "no");
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
return;
|
||
}
|
||
snprintf(msg, sizeof msg, "bt %s: 未生效:%s(Paired=%s%s)", cmd, arg,
|
||
v > 0 ? "yes" : v == 0 ? "no" : "unknown", tail);
|
||
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||
}
|
||
|
||
static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||
{
|
||
ctl_reset(r, MEOW_OK);
|
||
if (!strcmp(cmd, "status")) {
|
||
bt_t b;
|
||
bt_status(&b);
|
||
char msg[192];
|
||
snprintf(msg, sizeof msg, "bt available=%d powered=%d address=%s paired=%d connected=%d",
|
||
b.available, b.powered, b.address[0] ? b.address : "-", b.paired_n, b.connected_n);
|
||
ctl_sets(r, MEOW_OK, msg);
|
||
} else if (!strcmp(cmd, "on") || !strcmp(cmd, "off")) {
|
||
bt_onoff(cmd, r);
|
||
} else if (!strcmp(cmd, "paired")) {
|
||
char cmd2[176];
|
||
snprintf(cmd2, sizeof cmd2, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s paired-devices",
|
||
g_bluetoothctl);
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
runcmd_res_t res;
|
||
run_cmd_ex(buf, sizeof buf, RUNCMD_TMO_MS, cmd2, &res);
|
||
if (res.status == RUNCMD_OK) {
|
||
ctl_sets(r, MEOW_OK, buf);
|
||
} else if (res.status == RUNCMD_EMPTY) {
|
||
ctl_sets(r, MEOW_OK, ""); /* 真态:一台都没配对,不是错误 */
|
||
} else {
|
||
int fail = 0;
|
||
const char *why = run_cmd_why(&res, &fail);
|
||
char msg[224];
|
||
snprintf(msg, sizeof msg, "bt paired: 后端失败(%s)", why);
|
||
ctl_sets(r, MEOW_ERR_NODEV, msg);
|
||
}
|
||
} else if (!strcmp(cmd, "connected")) {
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
bt_connected_text(buf, sizeof buf);
|
||
ctl_sets(r, MEOW_OK, buf);
|
||
} else if (!strcmp(cmd, "a2dp")) {
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
bt_a2dp_text(buf, sizeof buf);
|
||
ctl_sets(r, MEOW_OK, buf);
|
||
} else if (!strcmp(cmd, "devices")) {
|
||
char buf[MEOW_IPC_MAX - 16];
|
||
bt_devices_text(buf, sizeof buf);
|
||
ctl_sets(r, MEOW_OK, buf);
|
||
} else if (!strcmp(cmd, "connect") || !strcmp(cmd, "disconnect") ||
|
||
!strcmp(cmd, "pair") || !strcmp(cmd, "unpair")) {
|
||
if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 <mac>", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; }
|
||
bt_write_execute(cmd, arg, r);
|
||
} else if (!strcmp(cmd, "scan")) {
|
||
if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); }
|
||
else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); }
|
||
else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off");
|
||
} else {
|
||
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
|
||
}
|
||
}
|
||
|
||
/* ---------------- 期3 策略命令(IPC 0xEA / CLI policy 共用;写级已在 handle_conn 鉴权) ---------------- */
|
||
/* 从 g_rules 移除某实体(shift 数组),返回 0=存在已删。 */
|
||
static int policy_remove(const char *entity)
|
||
{
|
||
for (int i = 0; i < g_rule_n; i++) {
|
||
if (strcmp(g_rules[i].entity, entity) == 0) {
|
||
for (int j = i; j < g_rule_n - 1; j++) g_rules[j] = g_rules[j + 1];
|
||
memset(&g_rules[g_rule_n - 1], 0, sizeof(rule_t));
|
||
g_rule_n--;
|
||
return 0;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
/* 输出某实体当前行;无规则输出 (none)。 */
|
||
static void policy_print_entity(const rule_t *r, char *out, size_t cap)
|
||
{
|
||
out[0] = 0;
|
||
if (!r) { snprintf(out, cap, "(none)\n"); return; }
|
||
size_t o = 0;
|
||
int l = snprintf(out + o, cap - o, "%s ", r->entity);
|
||
if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
|
||
int any = 0;
|
||
if (r->has_net) { l = snprintf(out + o, cap - o, "net=%s ", r->net_block ? "block" : "allow"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; any = 1; }
|
||
if (r->deny_n || r->allow_n) {
|
||
l = snprintf(out + o, cap - o, "domain="); if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
|
||
int first = 1;
|
||
for (int i = 0; i < r->allow_n; i++) { int la = snprintf(out + o, cap - o, "%s+%s", first ? "" : ",", r->allow[i]); if (la > 0 && o + (size_t)la < cap) o += (size_t)la; first = 0; }
|
||
for (int i = 0; i < r->deny_n; i++) { int ld = snprintf(out + o, cap - o, "%s-%s", first ? "" : ",", r->deny[i]); if (ld > 0 && o + (size_t)ld < cap) o += (size_t)ld; first = 0; }
|
||
any = 1;
|
||
}
|
||
if (!any) { l = snprintf(out + o, cap - o, "(allow)"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l; }
|
||
l = snprintf(out + o, cap - o, "\n"); if (l > 0 && o + (size_t)l < cap) o += (size_t)l;
|
||
}
|
||
|
||
static int policy_cmd(int op, const char *entity, const char *arg, char *out, size_t cap)
|
||
{
|
||
out[0] = 0;
|
||
if (op == NET_POLICY_SHOW) {
|
||
snprintf(out, cap, "# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n",
|
||
g_policy_log_level);
|
||
size_t o = strlen(out);
|
||
if (entity && entity[0]) {
|
||
rule_t *r = policy_find(entity);
|
||
policy_print_entity(r, out + o, cap - o);
|
||
} else {
|
||
for (int i = 0; i < g_rule_n; i++) {
|
||
char l[160]; policy_print_entity(&g_rules[i], l, sizeof l);
|
||
int n = snprintf(out + o, cap - o, "%s", l);
|
||
if (n > 0 && o + (size_t)n < cap) o += (size_t)n;
|
||
}
|
||
}
|
||
return MEOW_OK;
|
||
}
|
||
if (op == NET_POLICY_SET_NET) {
|
||
if (!entity || !entity[0] || !arg || (strcmp(arg, "allow") != 0 && strcmp(arg, "block") != 0))
|
||
{ snprintf(out, cap, "policy set 需 <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;
|
||
int bconn = -1;
|
||
jint(obj, "paired", &bpaired);
|
||
jint(obj, "connected", &bconn);
|
||
char bcodec[24] = "", brate[24] = "";
|
||
jstr(obj, "a2dp_codec", bcodec, sizeof bcodec);
|
||
jstr(obj, "a2dp_rate", brate, sizeof brate);
|
||
printf("bt available=%d powered=%d address=%s paired=%d connected=%d a2dp=%s@%s\n",
|
||
bavail, bpow, baddr[0] ? baddr : "-", bpaired, bconn,
|
||
bcodec[0] ? bcodec : "-", brate[0] ? brate : "-");
|
||
}
|
||
/* rfkill 数组 */
|
||
{
|
||
const char *rf = strstr(js, "\"rfkill\":[");
|
||
if (rf) {
|
||
const char *ctx2 = NULL;
|
||
for (;;) {
|
||
char tmp[96];
|
||
if (next_obj(rf, "type", &ctx2, tmp, sizeof tmp) != 0) break;
|
||
char type[16]; int i = 0;
|
||
while (tmp[i] && tmp[i] != '"' && i < 15) { type[i] = tmp[i]; i++; }
|
||
type[i] = 0;
|
||
int soft = -1, hard = -1;
|
||
jint(tmp, "soft", &soft);
|
||
jint(tmp, "hard", &hard);
|
||
printf("rfkill:%s soft=%d hard=%d\n", type, soft, hard);
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
static int cli_iface(const char *name)
|
||
{
|
||
char js[MEOW_IPC_MAX];
|
||
m_net_iface_req_t q;
|
||
memset(&q, 0, sizeof q);
|
||
if (name) snprintf(q.name, sizeof q.name, "%s", name);
|
||
if (cli_ipc_json(MSG_NET_IFACE_REQ, &q, sizeof q, js, sizeof js) != 0) {
|
||
fprintf(stderr, "meow_net iface: 守护不可达(%s)\n", g_sock);
|
||
return 1;
|
||
}
|
||
printf("%s\n", js);
|
||
return 0;
|
||
}
|
||
|
||
/* ---- 期2 CLI:conns/apps/top/dns(CONNS_RESULT 逐行 `entity|lip:lport|rip:rport|state|age|domain`) ---- */
|
||
static int cli_conns(const char *filter)
|
||
{
|
||
char js[MEOW_IPC_MAX];
|
||
m_net_conns_req_t q;
|
||
memset(&q, 0, sizeof q);
|
||
if (filter) snprintf(q.entity, sizeof q.entity, "%s", filter);
|
||
if (cli_ipc_json(MSG_NET_CONNS_REQ, &q, sizeof q, js, sizeof js) != 0) {
|
||
fprintf(stderr, "meow_net conns: 守护不可达(%s)\n", g_sock);
|
||
return 1;
|
||
}
|
||
int n = 0;
|
||
char *ln = js;
|
||
while (ln && *ln) {
|
||
char *nl = strchr(ln, '\n'); if (nl) *nl = 0;
|
||
char *f[7]; int nf = 0;
|
||
char *save = NULL;
|
||
for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t;
|
||
if (nf >= 6) {
|
||
printf("%-26s %-22s -> %-22s %-9s %5lds %s\n", f[0], f[1], f[2], f[3],
|
||
atol(f[4]), f[5]);
|
||
n++;
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
printf("(%d connection%s%s)\n", n, n == 1 ? "" : "s", filter && filter[0] ? " filtered" : "");
|
||
return 0;
|
||
}
|
||
|
||
typedef struct { char ent[48]; int cnt; long maxage; } ent_grp_t;
|
||
/* 从 conns 文本聚合实体统计(apps/top 共用)。 */
|
||
static int group_conns(const char *js, ent_grp_t *grp, int max)
|
||
{
|
||
int gn = 0;
|
||
char *ln = (char *)js;
|
||
while (ln && *ln && gn < max) {
|
||
char *nl = strchr(ln, '\n'); if (nl) *nl = 0;
|
||
char *f[7]; int nf = 0;
|
||
char *save = NULL;
|
||
for (char *t = strtok_r(ln, "|", &save); t && nf < 7; t = strtok_r(NULL, "|", &save)) f[nf++] = t;
|
||
if (nf >= 5 && f[0][0]) {
|
||
int gi = -1;
|
||
for (int i = 0; i < gn; i++) if (strcmp(grp[i].ent, f[0]) == 0) { gi = i; break; }
|
||
if (gi < 0) {
|
||
if (gn >= max) continue;
|
||
gi = gn++;
|
||
snprintf(grp[gi].ent, sizeof grp[gi].ent, "%s", f[0]);
|
||
grp[gi].cnt = 0; grp[gi].maxage = 0;
|
||
}
|
||
grp[gi].cnt++;
|
||
long a = atol(f[4]);
|
||
if (a > grp[gi].maxage) grp[gi].maxage = a;
|
||
}
|
||
if (!nl) break;
|
||
ln = nl + 1;
|
||
}
|
||
/* 按 cnt 降序 */
|
||
for (int i = 0; i < gn; i++)
|
||
for (int j = i + 1; j < gn; j++)
|
||
if (grp[j].cnt > grp[i].cnt) {
|
||
ent_grp_t t = grp[i]; grp[i] = grp[j]; grp[j] = t;
|
||
}
|
||
return gn;
|
||
}
|
||
|
||
static int cli_apps(void)
|
||
{
|
||
char js[MEOW_IPC_MAX];
|
||
if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) {
|
||
fprintf(stderr, "meow_net apps: 守护不可达(%s)\n", g_sock);
|
||
return 1;
|
||
}
|
||
ent_grp_t grp[64];
|
||
int gn = group_conns(js, grp, 64);
|
||
printf("%-26s %6s %8s\n", "entity", "conns", "max_age");
|
||
for (int i = 0; i < gn; i++)
|
||
printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage);
|
||
return 0;
|
||
}
|
||
|
||
static volatile sig_atomic_t g_cli_stop = 0;
|
||
static void cli_sigint(int sig) { (void)sig; g_cli_stop = 1; }
|
||
|
||
static int cli_top(void)
|
||
{
|
||
g_cli_stop = 0;
|
||
signal(SIGINT, cli_sigint);
|
||
while (!g_cli_stop) {
|
||
char js[MEOW_IPC_MAX];
|
||
if (cli_ipc_json(MSG_NET_CONNS_REQ, NULL, 0, js, sizeof js) != 0) {
|
||
fprintf(stderr, "meow_net top: 守护不可达(%s)\n", g_sock);
|
||
return 1;
|
||
}
|
||
ent_grp_t grp[64];
|
||
int gn = group_conns(js, grp, 64);
|
||
printf("\033[2J\033[H top 连接实体(2s 刷新,Ctrl-C 退)\n");
|
||
printf("%-26s %6s %8s\n", "entity", "conns", "max_age");
|
||
for (int i = 0; i < gn && i < 10; i++)
|
||
printf("%-26s %6d %8lds\n", grp[i].ent, grp[i].cnt, grp[i].maxage);
|
||
for (int i = 0; i < 10 - gn && i < 10; i++) printf("\n");
|
||
fflush(stdout);
|
||
sleep(2);
|
||
}
|
||
printf("\n");
|
||
return 0;
|
||
}
|
||
|
||
static int cli_dns(const char *cmd)
|
||
{
|
||
char out[MEOW_IPC_MAX - 8];
|
||
int code = cli_ipc_cmd(MSG_NET_DNS_CTL, cmd ? cmd : "status", NULL, out, sizeof out);
|
||
printf("%s\n", out);
|
||
return code == MEOW_OK ? 0 : 1;
|
||
}
|
||
|
||
/* 期3 policy:show/set net/domain/del(IPC 0xEA;写级在服务端鉴权系统身份)。 */
|
||
static int cli_policy(int op, const char *entity, const char *arg)
|
||
{
|
||
m_net_policy_t q;
|
||
memset(&q, 0, sizeof q);
|
||
q.op = (uint32_t)op;
|
||
if (entity) snprintf(q.entity, sizeof q.entity, "%s", entity);
|
||
if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg);
|
||
char out[MEOW_IPC_MAX - 8];
|
||
int fd = ipc_connect();
|
||
if (fd < 0) { fprintf(stderr, "meow_net 守护不可达(%s)\n", g_sock); return 1; }
|
||
uint16_t rt; uint32_t code;
|
||
int rc = ipc_xchg(fd, MSG_NET_POLICY, &q, sizeof q, &rt, &code, out, sizeof out);
|
||
close(fd);
|
||
if (rc != 0) { fprintf(stderr, "meow_net policy: IPC 往返失败\n"); return 1; }
|
||
printf("%s", out);
|
||
return code == MEOW_OK ? 0 : 1;
|
||
}
|
||
|
||
static const char *usage(void)
|
||
{
|
||
return "status | iface [name] | wifi scan|networks|connect <ssid>[,psk]|connecth <ssid>[,psk]|disconnect|on|off"
|
||
" | bt status|on|off|paired|connected|a2dp|devices|scan on|off|connect|disconnect|pair|unpair <mac>"
|
||
" | 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") || !strcmp(arg, "connecth")) {
|
||
if (!arg2) { fprintf(stderr, "%s 需 <ssid>[,psk]\n", arg); return 1; }
|
||
return cli_cmd_result(MSG_NET_WIFI_CTL, arg, 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; }
|
||
if (!strcmp(arg, "connect") || !strcmp(arg, "disconnect") ||
|
||
!strcmp(arg, "pair") || !strcmp(arg, "unpair")) {
|
||
if (!arg2) { fprintf(stderr, "bt %s 需 <mac>\n", arg); return 1; }
|
||
return cli_cmd_result(MSG_NET_BT_CTL, arg, arg2);
|
||
}
|
||
if (!strcmp(arg, "scan")) {
|
||
if (arg2 && !strcmp(arg2, "off")) return cli_cmd_result(MSG_NET_BT_CTL, "scan", "off");
|
||
return cli_cmd_result(MSG_NET_BT_CTL, "scan", "on");
|
||
}
|
||
return cli_cmd_result(MSG_NET_BT_CTL, arg, NULL);
|
||
}
|
||
if (!strcmp(cmd, "rfkill")) {
|
||
if (!arg) return cli_status(); /* 读 rfkill 表 → status JSON 的 rfkill[] */
|
||
if (!arg2) { fprintf(stderr, "rfkill 需 <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_HCITOOL")) && e[0]) g_hcitool = e;
|
||
if ((e = getenv("MEOW_BLUEALSA_CLI")) && e[0]) g_bluealsa_cli = e;
|
||
if ((e = getenv("MEOW_BT_SCAN_CACHE")) && e[0]) g_bt_scan_cache = e;
|
||
if ((e = getenv("MEOW_IP")) && e[0]) g_ip = e;
|
||
if ((e = getenv("MEOW_WIFI_IF")) && e[0]) g_wifi_if = e;
|
||
if ((e = getenv("MEOW_NET_SOCK")) && e[0]) g_sock = e;
|
||
if ((e = getenv("MEOW_NET_POLL_SEC")) && atoi(e) > 0) g_poll_sec = atoi(e);
|
||
if ((e = getenv("MEOW_WIFI_CONNECT_WAIT_MS")) && atoi(e) >= 200) g_connect_wait_ms = atoi(e);
|
||
if ((e = getenv("MEOW_WIFI_CONNECTH_WAIT_MS")) && atoi(e) >= 200) g_connecth_wait_ms = atoi(e);
|
||
if ((e = getenv("MEOW_TCP")) && e[0]) snprintf(g_proc_tcp, sizeof g_proc_tcp, "%s", e);
|
||
if ((e = getenv("MEOW_UDP")) && e[0]) snprintf(g_proc_udp, sizeof g_proc_udp, "%s", e);
|
||
if ((e = getenv("MEOW_AMS_SOCK")) && e[0]) snprintf(g_ams_sock, sizeof g_ams_sock, "%s", e);
|
||
if ((e = getenv("MEOW_SVCD_SOCK")) && e[0]) snprintf(g_svcd_sock, sizeof g_svcd_sock, "%s", e);
|
||
if ((e = getenv("MEOW_SVCD_STATUS")) && e[0]) snprintf(g_svcd_status, sizeof g_svcd_status, "%s", e);
|
||
if ((e = getenv("MEOW_NET_POLICY")) && e[0]) snprintf(g_net_policy, sizeof g_net_policy, "%s", e);
|
||
if ((e = getenv("MEOW_DNS_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_port = atoi(e);
|
||
if ((e = getenv("MEOW_DNS_UPSTREAM_PORT")) && atoi(e) > 0 && atoi(e) < 65536) g_dns_upstream_port = atoi(e);
|
||
if ((e = getenv("MEOW_DNS_DISABLE")) && atoi(e) > 0) g_dns_disabled = 1;
|
||
|
||
int i = 1;
|
||
for (; i < argc; i++) {
|
||
if (!strcmp(argv[i], "--sock") && i + 1 < argc) g_sock = argv[++i];
|
||
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||
printf("meow_netmgrd / meow_net (%s)\n", usage());
|
||
return 0;
|
||
}
|
||
else break;
|
||
}
|
||
|
||
/* argv[0] basename=meow_net → 强制 CLI(双形态);meow_netmgrd 空参 → 守护常驻。 */
|
||
const char *base = strrchr(argv[0], '/');
|
||
base = base ? base + 1 : argv[0];
|
||
if (!strcmp(base, "meow_net")) {
|
||
if (i >= argc) return cli_help();
|
||
return cli_run(argc - i, argv + i);
|
||
}
|
||
if (i >= argc) return run_daemon(g_sock); /* svcd 空参 → IPC 常驻 */
|
||
return cli_run(argc - i, argv + i);
|
||
}
|