Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/netmgr_ipc.c
T
AskaEth 7b32d775c0 fix(security): M64 P1 三条 root 命令注入 — 外部串改 execv 传 argv、电源键下线、press_ms clamp
P1-1 Wi-Fi SSID/PSK 注入(netmgrd + launcherd 回退两处同步):
- 不再拼 `set_network %d ssid '"%s"'` 交给 sh -c;新增 wpa_exec()/nm_direct_wpa_exec()
  以 fork+execv 传 argv(含 resolve_bin 解析绝对路径,替代 `PATH=...` 前缀)。
- 共用 nm_wifi_args.h:nm_wifi_ssid_ok/n_wifi_psk_ok/nm_wpa_quote/nm_wpa_set_argv,
  两份实现同一套规则(只修一处等于没修)。
- 约束层:SSID 1..32B、禁控制字符;PSK ≤63 或 64 hex、禁控制字符;`"`/`\` 按
  wpa_supplicant 语法转义。shell 元字符不再需要拒绝——execv 下没有 shell 解析。

P1-2 MeowPanel App 安装/导出/卸载注入:
- install/export/remove 改 run_argv()(fork+execv,PATH 只在子进程 setenv)。
- 包名统一 mp_pkg_ok()(仅 [a-z0-9._-],拒 `..`,与 nyai.c valid_pkg 同规则)→ 修 P2-7。
- `{path}` 追加 mp_path_unsafe()(与 mp_fonts/nyai 同一拒绝集)作第二道。

P1-3 /api/key/* 电源键:
- keycode_to_device 删除 KEY_POWER;116 进红线(403),前端遥控板电源/长按电源按钮移除。
- press_ms 全局 clamp 0..1000(单键与批量),`long` 缺省取上限。
- 按下→等待→抬起改独立线程(inject_key_async),不再阻塞单线程 8080 服务。

测试(宿主,反向控制):test_nm_wifi_args.c / test_mp_argsafe.c —— 同一套审计恶意串
(`a' ; id ; '`、`x';id;'`、`/tmp/a';id>/tmp/pwn;'.mpk`、`MeowAPPs/..`)在修复前实现下
注入成功(stdout 含 uid=)/放行,在修复后实现下不执行/拒绝。另含「两处实现同步」检查。

未刷机、未连设备、未发 /api/key 请求;只改源码,未提交二进制。
2026-11-12 08:50:10 +08:00

878 lines
36 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* netmgr_ipc.c — MeowOS launcher → meow_netmgrd IPC 客户端(M6.0 设置页迁移,NETMGR_DESIGN §12)。
*
* 【迁移注释】wifi_page/bt_page 的 WiFi/BT 查询与操作统一走 netmgrd IPC(socket
* /usr/data/meow_net.sock,env MEOW_NET_SOCK 覆盖)。netmgrd 是 WiFi/BT 操作权威
* (IPC 0xE4 WIFI_CTL / 0xE6 BT_CTL);本文件是「设置页客户端 → 权威 IPC」的唯一入口。
* 【双轨过渡】netmgrd 不可达(NETMGR_UNREACH)时,本文件回退到 net.c 既有的直连
* wpa_cli/hciconfig/busybox 读取(保功能);状态栏物理键语义、sdk/amsd/netmgrd 本体不动。
*
* 鉴权:跟 pm_power_ipc 同款 —— 先 MSG_HELLO(app_id=os.meow.launcher,netmgrd 按
* SO_PEERCRED 认出 /usr/bin/meow_launcherd = 系统身份,写级放行),再发 CTL,收 RESULT。
* 只读观测(wlan_connected()/hci_powered() 读 /sys 链路态)仍在 net.c;此处只接权威操作。
*/
#include "meow_launcher_internal.h"
/* M64 P1-1:SSID/PSK 校验与 wpa 字面量转义。与 netmgrd 共用同一头文件——回退路径只修一处
* 等于没修(netmgrd 不可达时攻击者正是走这里),两边必须同一套规则。 */
#include "../meow_netmgrd/nm_wifi_args.h"
#define NETMGR_SOCK_DEFAULT "/usr/data/meow_net.sock"
/* NETMGR_UNREACH 定义在 meow_launcher_internal.h(测试/调用方共用) */
/* M47:net.c 的直连三态探测(-1 未知 / 0 关 / 1 开)。声明在此而非头文件——头文件不在本单
* 改动范围;两处定义与用法同属 launcher 网络线。 */
int net_bt_power_probe(void);
int net_wifi_power_probe(void);
/* IPC 读超时(ms)。netmgrd 单线程 poll,connect/connecth 会阻塞 ~10s/~15s 有界等待
* wpa_state=COMPLETED(bug1),scan 内含 1.5s 触发等待(P3)——客户端 per-command 超时需
* 覆盖这些同步等待,其余读级子命令维持 1.5s 不被卡死。 */
#define NM_TMO_DFL 1500
#define NM_TMO_SCAN 5000
#define NM_TMO_CONNECT 12000
#define NM_TMO_CONNECTH 17000
/* 可达性缓存:netmgrd 不可达时每次 connect 都秒回 ECONNREFUSED,无成本;缓存约 1s 防热循环。 */
static int g_nm_ok = -1; /* -1 未知 / 0 down / 1 up */
static time_t g_nm_ok_ts = 0;
static void nm_cache(int ok) { g_nm_ok = ok; g_nm_ok_ts = time(NULL); }
/* ---- socket 连接 ---- */
static int nm_sock(void)
{
const char *sock = getenv("MEOW_NET_SOCK");
if (!sock || !sock[0]) sock = NETMGR_SOCK_DEFAULT;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) 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);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
return fd;
}
/* ---- 帧收发(复刻 power_menu.c pw_xchg:读超时 1.5s,绝不让子页主循环卡死)---- */
static int nm_w_all(int fd, const uint8_t *b, size_t n)
{
size_t off = 0;
while (off < n) { ssize_t w = write(fd, b + off, n - off); if (w <= 0) return -1; off += (size_t)w; }
return 0;
}
static int nm_r_all(int fd, uint8_t *b, size_t n)
{
size_t off = 0;
while (off < n) { ssize_t r = read(fd, b + off, n - off); if (r <= 0) return -1; off += (size_t)r; }
return 0;
}
static int nm_xchg(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len,
uint8_t *rbuf, uint32_t rcap, uint16_t *rtype, int tmo_ms)
{
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, 4); memcpy(hdr + 12, &rlen, 4);
if (nm_w_all(fd, hdr, sizeof hdr) < 0) return -1;
if (len && pl && nm_w_all(fd, pl, len) < 0) return -1;
struct pollfd pf = { fd, POLLIN, 0 };
int pr = poll(&pf, 1, tmo_ms);
if (pr <= 0) return -1;
uint8_t rhdr[MEOW_IPC_HDR_LEN];
if (nm_r_all(fd, rhdr, sizeof rhdr) < 0) return -1;
uint32_t rmagic, rlen2; uint16_t rver, rt;
memcpy(&rmagic, rhdr, 4); memcpy(&rver, rhdr + 4, 2); memcpy(&rt, rhdr + 6, 2);
memcpy(&rlen2, rhdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return -1;
if (rlen2 > rcap) rlen2 = rcap;
if (rlen2 && nm_r_all(fd, rbuf, rlen2) < 0) return -1;
if (rtype) *rtype = rt;
return 0;
}
static int nm_hello(int fd)
{
m_hello_t h;
memset(&h, 0, sizeof h);
snprintf(h.app_id, sizeof h.app_id, "%s", "os.meow.launcher");
h.version = 1;
uint8_t rbuf[256]; uint16_t rtype = 0;
if (nm_xchg(fd, MSG_HELLO, 0x100, &h, sizeof h, rbuf, sizeof rbuf, &rtype, NM_TMO_DFL) < 0 || rtype != MSG_ACK)
return -1;
m_ack_t ak; memcpy(&ak, rbuf, sizeof ak);
return (ak.code == MEOW_OK) ? 0 : -1;
}
/* 发一条 WIFI_CTL/BT_CTL 子命令,收 RESULT。返回 netmgrd 业务 code;NETMGR_UNREACH=不可达。
* tmo_ms:读超时(connect/connecth/scan 需覆盖 netmgrd 同步等待,见 NM_TMO_*)。
* read_timeout(可空):非空时,「帧收发失败/类型不符」这一支只回写 *read_timeout=1 且**不**
* 把可达性缓存打成 down——调用方据此区分「读超时」与「真不可达」。M47 的 BT 状态查询用它:
* netmgrd 单线程忙时(bt status 自己会跑 hciconfig)状态查询可能超过 1.5s,若据此标 down,
* 紧随的 bt_rows 会闪出「服务未运行(直连模式)」并把后续探测切到 1.8s 的 hciconfig
* (评审 r1 观察;global 超时常量不动,WiFi/DNS 路径行为不变)。 */
static int nm_cmd_t(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
char *text, int tlen, int tmo_ms, int *read_timeout)
{
m_net_cmd_t q;
memset(&q, 0, sizeof q);
snprintf(q.cmd, sizeof q.cmd, "%s", cmd ? cmd : "");
if (arg) snprintf(q.arg, sizeof q.arg, "%s", arg);
int fd = nm_sock();
if (fd < 0) { nm_cache(0); return NETMGR_UNREACH; }
if (nm_hello(fd) < 0) { close(fd); nm_cache(0); return NETMGR_UNREACH; }
uint8_t rbuf[MEOW_IPC_MAX]; uint16_t rtype = 0;
memset(rbuf, 0, sizeof rbuf);
if (nm_xchg(fd, ctl_type, 0x101, &q, sizeof q, rbuf, sizeof rbuf, &rtype, tmo_ms) < 0 || rtype != res_type) {
close(fd);
if (read_timeout) *read_timeout = 1;
else nm_cache(0);
return NETMGR_UNREACH;
}
m_net_cmd_result_t res; memcpy(&res, rbuf, sizeof res);
close(fd);
nm_cache(1);
uint32_t tl = res.text_len;
if (tl >= (uint32_t)tlen) tl = (uint32_t)(tlen - 1);
if (text && tlen > 0) { memcpy(text, res.text, tl); text[tl] = 0; }
return (int)res.code;
}
static int nm_cmd(uint16_t ctl_type, uint16_t res_type, const char *cmd, const char *arg,
char *text, int tlen, int tmo_ms)
{
return nm_cmd_t(ctl_type, res_type, cmd, arg, text, tlen, tmo_ms, NULL);
}
int netmgr_wifi_tmo(const char *cmd, const char *arg, char *text, int tlen, int tmo_ms)
{ return nm_cmd(MSG_NET_WIFI_CTL, MSG_NET_WIFI_RESULT, cmd, arg, text, tlen, tmo_ms); }
int netmgr_wifi(const char *cmd, const char *arg, char *text, int tlen)
{ return netmgr_wifi_tmo(cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_bt(const char *cmd, const char *arg, char *text, int tlen)
{ return nm_cmd(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, cmd, arg, text, tlen, NM_TMO_DFL); }
int netmgr_ok(void)
{
if (g_nm_ok >= 0 && time(NULL) - g_nm_ok_ts < 1) return g_nm_ok;
int fd = nm_sock();
if (fd < 0) { nm_cache(0); return 0; }
close(fd); nm_cache(1); return 1;
}
/* ================= WiFi(IPC 优先,net.c 直连回退)================= */
/* —— net.c 的 wpa_decode_ssid 是 static,这里复制最小实现(本模块自含,避免改 net.c 可见性)—— */
static int nm_hex(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 nm_decode_ssid(const char *src, char *dst, int dlen)
{
int o = 0;
while (*src && o < dlen - 1) {
if (src[0] == '\\' && src[1] == 'x' && nm_hex(src[2]) >= 0 && nm_hex(src[3]) >= 0) {
dst[o++] = (char)((nm_hex(src[2]) << 4) | nm_hex(src[3]));
src += 4;
} else {
dst[o++] = *src++;
}
}
dst[o] = 0;
}
/* P1:拷一个「空格分隔的值 token」到 dst(a2dp 字段 extraction 用)。 */
static void nm_val_field(char **p, char *dst, int dlen)
{
while (**p == ' ' || **p == '\t') (*p)++;
int o = 0;
while (**p && **p != ' ' && **p != '\t' && **p != '\n' && o < dlen - 1) dst[o++] = *(*p)++;
dst[o] = 0;
}
/* —— netmgrd `wifi status` 单行摘要解析:`wifi state=X connected=N ssid=S ip=I rssi=R`。
* 已知字段以「空格+key=」分隔;ssid 可能含空格,需读到下一个已知字段为止。 —— */
static int nm_is_status_key(const char *p)
{
if (!strncmp(p, " ip=", 4) || !strncmp(p, " rssi=", 6) ||
!strncmp(p, " connected=", 11) || !strncmp(p, " state=", 7)) return 1;
return 0;
}
static const char *nm_val(const char *text, const char *key, char *dst, int dlen, int space_ok)
{
const char *p = strstr(text, key);
if (!p) { dst[0] = 0; return p; }
p += strlen(key);
int o = 0;
while (*p && o < dlen - 1) {
if (*p == ' ') {
if (!space_ok) break;
if (nm_is_status_key(p)) break; /* ssid 含空格:停在下一个已知字段 */
}
dst[o++] = *p++;
}
dst[o] = 0;
return p;
}
static int nm_parse_connected(const char *text)
{
char v[8];
nm_val(text, "connected=", v, sizeof v, 0);
return atoi(v);
}
int nm_wifi_connected(void)
{
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return wlan_connected();
if (rc != MEOW_OK) return 0;
return nm_parse_connected(text);
}
void nm_wifi_ssid(char *buf, int len)
{
buf[0] = 0;
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { net_wifi_ssid(buf, len); return; }
if (rc != MEOW_OK) return;
if (!nm_parse_connected(text)) return;
char raw[128];
nm_val(text, "ssid=", raw, sizeof raw, 1);
nm_decode_ssid(raw, buf, len);
}
int nm_wifi_info(char (*info)[48], int max)
{
char text[256];
int rc = netmgr_wifi("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_wifi_info(info, max); /* 回退:完整详情(直连 wpa_cli+ip route) */
if (rc != MEOW_OK || !nm_parse_connected(text)) return 0;
char ssid[40] = "", ip[32] = "", rssi[16] = "";
char raw[128];
nm_val(text, "ssid=", raw, sizeof raw, 1);
nm_decode_ssid(raw, ssid, sizeof ssid);
nm_val(text, "ip=", ip, sizeof ip, 0);
nm_val(text, "rssi=", rssi, sizeof rssi, 0);
int n = 0;
if (ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
if (ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
if (rssi[0]) snprintf(info[n++], 48, "信号: %s dBm", rssi);
if (n == 0) snprintf(info[n++], 48, "已连接(无详情)");
return n;
}
/* —— 解析 netmgrd `wifi networks`(wpa_cli list_networks 原样文本)→ meow_net_t[];
* 直接复用 net.c 既有表头跳行逻辑。返回项数。 —— */
static int nm_parse_saved(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
const char *line = strchr(txt, '\n');
if (line) line++;
while (line && *line && n < max) {
const char *eol = strchr(line, '\n');
const char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p && *p != '#') {
int id = atoi(p);
const char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
while (*sp == ' ' || *sp == '\t') sp++;
const char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
const char *bp = se; while (*bp == ' ' || *bp == '\t') bp++;
const char *be = bp; while (*be && *be != ' ' && *be != '\t' && *be != '\n' && *be != '\r') be++;
if (sp != se) {
char raw[160];
int l = (int)(se - sp);
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, sp, (size_t)l); raw[l] = 0;
out[n].id = id;
nm_decode_ssid(raw, out[n].name, sizeof out[n].name);
if (be > bp && (be - bp) < (int)sizeof out[n].mac) {
memcpy(out[n].mac, bp, (size_t)(be - bp)); out[n].mac[be - bp] = 0;
}
out[n].sec[0] = 0;
n++;
}
}
line = eol ? eol + 1 : NULL;
}
return n;
}
/* —— 解析 netmgrd `wifi scan`(wpa_cli scan_results 原样文本)→ meow_net_t[](按 BSSID 去重、
* 优先具名;补充 sec 安全类型供「新网络录入」判断是否弹 IME)。返回项数。 —— */
static int nm_parse_scan(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char seen_bssid[S_NET_MAX][24];
int named[S_NET_MAX] = {0};
const char *line = strchr(txt, '\n');
if (line) line++;
while (line && *line && n < max) {
const char *eol = strchr(line, '\n');
const char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p) {
char bssid[24] = "";
{ const char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
const char *ssid = "";
char sec[16] = "Open";
const char *br = strchr(p, '[');
if (br) {
/* 安全类型 = 首个方括号内容([WPA-PSK-CCMP] 等);纯 [ESS]/[IBSS] = 开放 */
const char *be = strchr(br + 1, ']');
if (be) {
int sl = (int)(be - (br + 1));
if (sl > 15) sl = 15;
if (sl > 0) {
memcpy(sec, br + 1, (size_t)sl); sec[sl] = 0;
if (!strstr(sec, "WPA") && !strstr(sec, "RSN") &&
!strstr(sec, "WEP") && !strstr(sec, "SAE") && !strstr(sec, "EAP"))
snprintf(sec, sizeof sec, "Open");
}
}
const char *q = br;
while (*q) {
while (*q == ' ' || *q == '\t') q++;
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
else break;
}
ssid = q; while (*ssid == ' ' || *ssid == '\t') ssid++;
}
const char *se = ssid;
while (*se && *se != '\n' && *se != '\r') se++;
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
int has = (se > ssid && *ssid);
int idx = -1;
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
if (idx < 0) {
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
idx = n++;
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
named[idx] = has;
out[idx].id = -1;
snprintf(out[idx].mac, sizeof out[idx].mac, "%s", bssid);
snprintf(out[idx].sec, sizeof out[idx].sec, "%s", sec);
if (has) {
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
} else {
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
}
} else if (has && !named[idx]) {
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
nm_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
named[idx] = 1;
}
}
line = eol ? eol + 1 : NULL;
}
return n;
}
int nm_wifi_saved(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_wifi("networks", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_wifi_saved(out, max); /* 直连回退 */
if (rc != MEOW_OK) return 0;
return nm_parse_saved(text, out, max);
}
int nm_wifi_scan(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_wifi_tmo("scan", NULL, text, sizeof text, NM_TMO_SCAN); /* netmgrd 内含触发+有界等待+读结果 */
if (rc == NETMGR_UNREACH) return net_wifi_scan(out, max);
if (rc != MEOW_OK) return 0;
return nm_parse_scan(text, out, max);
}
void nm_wifi_scan_trigger(void)
{
/* IPC:scan 子命令在 netmgrd 内已一次性「触发+等待+读结果」,此处无需单独触发;
* 直连回退:异步 wpa_cli scan(结果稍后 net_wifi_scan 读)。 */
if (!netmgr_ok()) net_wifi_scan_trigger();
}
/* —— 直连回退:与 netmgrd wifi_connect/connecth 对齐(bug1/bug2/bug4)。
* 匹配既有(\xNN 还原同表示)→ 新建或(命中且带新 psk)更新凭据 → hidden 补 scan_ssid=1 →
* enable+select → 有界等待 wpa_state=COMPLETED → 成功 enable_network all + save_config(校验重试);
* 失败时本次新建的网络回收,报文明示原因。仅在 netmgrd 不可达时走此路径。 */
static const char NM_WPA_PREFIX[] = "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0";
static int nm_direct_wpa(char *out, int cap, const char *args, int tmo)
{
char cmd[384];
snprintf(cmd, sizeof cmd, "%s %s 2>/dev/null", NM_WPA_PREFIX, args);
char *o = popen_read_tmo(tmo, cmd);
if (!o) { out[0] = 0; return -1; }
int l = (int)strlen(o);
if (l >= cap) l = cap - 1;
memcpy(out, o, (size_t)l); out[l] = 0;
free(o);
return l;
}
/* ---- M64 P1-1:外部可控字符串(SSID/PSK)只走这一条,execv 传 argv,不经过 shell ----
* 修复前:nm_direct_wpa("set_network %d ssid '\"%s\"'", id, ssid) → popen(sh -c)。
* 恶意 AP 广播 SSID=`a' ; id ; '` 即可 root 执行。现在引号/分号/反引号只是 argv 里的字节。 */
static int nm_wpa_bin(char *out, size_t cap)
{
static const char *dirs[] = { "/sbin", "/usr/sbin", "/bin", "/usr/bin", NULL };
for (int i = 0; dirs[i]; i++) {
snprintf(out, cap, "%s/wpa_cli", dirs[i]);
if (access(out, X_OK) == 0) return 0;
}
return -1;
}
static long nm_mono_ms(void)
{
struct timespec tp;
clock_gettime(CLOCK_MONOTONIC, &tp);
return tp.tv_sec * 1000L + tp.tv_nsec / 1000000L;
}
/* 执行 `wpa_cli -i wlan0 <args...>`(execv,无 shell)。args 以 NULL 结尾。
* 返回 stdout 字节数(-1=起不来),tmo 内有界收割,语义对齐 nm_direct_wpa。 */
static int nm_direct_wpa_exec(char *out, int cap, char *const *args, int tmo)
{
if (cap > 0) out[0] = 0;
char bin[128];
if (nm_wpa_bin(bin, sizeof bin) < 0) return -1;
char *argv[10];
int n = 0;
argv[n++] = bin;
argv[n++] = "-i";
argv[n++] = "wlan0";
for (int i = 0; args[i] && n < (int)(sizeof argv / sizeof argv[0]) - 1; i++) argv[n++] = args[i];
argv[n] = NULL;
int pfd[2];
if (pipe(pfd) < 0) return -1;
pid_t pid = fork();
if (pid < 0) { close(pfd[0]); close(pfd[1]); return -1; }
if (pid == 0) {
dup2(pfd[1], 1);
int dn = open("/dev/null", O_WRONLY);
if (dn >= 0) { dup2(dn, 2); close(dn); }
close(pfd[0]); close(pfd[1]);
execv(bin, argv);
_exit(127);
}
close(pfd[1]);
int off = 0, timed = 0;
long deadline = nm_mono_ms() + (tmo > 0 ? tmo : 1);
while (off < cap - 1) {
long remain = deadline - nm_mono_ms();
if (remain <= 0) { timed = 1; break; }
struct pollfd pf = { pfd[0], POLLIN, 0 };
int pr = poll(&pf, 1, (int)remain);
if (pr < 0) { if (errno == EINTR) continue; timed = 1; break; }
if (pr == 0) { timed = 1; break; }
ssize_t rd = read(pfd[0], out + off, (size_t)(cap - 1 - off));
if (rd <= 0) break;
off += (int)rd;
}
out[off] = 0;
close(pfd[0]);
if (timed) kill(pid, SIGKILL);
int st = 0;
waitpid(pid, &st, 0);
return off;
}
/* set_network <id> <field> <字符串值>:值按 wpa_supplicant 语法加引号(转义 `"`/`\`),
* 整条命令以 argv 传入。value 必须先过 nm_wifi_ssid_ok()/nm_wifi_psk_ok()。 */
static int nm_direct_wpa_set(char *out, int cap, int id, const char *field, const char *val, int tmo)
{
char idbuf[16], quoted[128];
char *args[5];
if (nm_wpa_set_argv(idbuf, sizeof idbuf, quoted, sizeof quoted, id, field, val, args) < 0) {
if (cap > 0) out[0] = 0;
return -1;
}
return nm_direct_wpa_exec(out, cap, args, tmo);
}
static int nm_direct_save_config(void)
{
char out[128];
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
nm_direct_wpa(out, sizeof out, "set update_config 1", 1500);
if (nm_direct_wpa(out, sizeof out, "save_config", 1500) > 0 && strstr(out, "OK")) return 1;
return 0;
}
static int nm_direct_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{
char out[1024]; char args[256];
/* M64 P1-1:回退路径自己再校验一次(不依赖调用方;两条路径同一套规则)。 */
if (!nm_wifi_ssid_ok(ssid)) {
if (err) snprintf(err, elen, "ssid 非法(长度 1..32 字节,禁控制字符)");
return MEOW_ERR_INVAL;
}
if (!nm_wifi_psk_ok(psk)) {
if (err) snprintf(err, elen, "psk 非法(≤63 字符口令或 64 位 hex,禁控制字符)");
return MEOW_ERR_INVAL;
}
int id = -1;
if (nm_direct_wpa(out, sizeof out, "list_networks", 1500) > 0) {
char *ln = strtok(out, "\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[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);
}
nm_decode_ssid(clean, unesc, sizeof unesc);
char *p = unesc; while (*p == ' ' || *p == '\t') p++;
snprintf(norm, sizeof norm, "%s", p);
size_t nl = strlen(norm);
while (nl > 0 && (norm[nl - 1] == ' ' || norm[nl - 1] == '\t')) norm[--nl] = 0;
if (strcmp(norm, ssid) == 0) { id = this_id; break; }
}
ln = strtok(NULL, "\n");
}
}
int created = 0;
if (id < 0) {
if (nm_direct_wpa(out, sizeof out, "add_network", 1500) <= 0) {
if (err) snprintf(err, elen, "add_network 无 id");
return MEOW_ERR_INVAL;
}
id = atoi(out);
if (id < 0) { if (err) snprintf(err, elen, "add_network 无 id"); return MEOW_ERR_INVAL; }
created = 1;
/* M64 P1-1:ssid/psk 走 execv(nm_direct_wpa_set),不再拼 popen(sh -c)。 */
nm_direct_wpa_set(out, sizeof out, id, "ssid", ssid, 1500);
if (psk && psk[0]) { nm_direct_wpa_set(out, sizeof out, id, "psk", psk, 1500); }
else { snprintf(args, sizeof args, "set_network %d key_mgmt NONE", id); nm_direct_wpa(out, sizeof out, args, 1500); }
} else if (psk && psk[0]) {
nm_direct_wpa_set(out, sizeof out, id, "psk", psk, 1500);
}
if (hidden) { snprintf(args, sizeof args, "set_network %d scan_ssid 1", id); nm_direct_wpa(out, sizeof out, args, 1500); }
snprintf(args, sizeof args, "enable_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
snprintf(args, sizeof args, "select_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500);
int wait_ms = hidden ? 15000 : 10000;
int waited = 0, ok = 0; char ip[40] = "";
for (;;) {
char st[512];
if (nm_direct_wpa(st, sizeof st, "status", 1500) > 0) {
char *ws = strstr(st, "wpa_state=");
if (ws && !strncmp(ws + 10, "COMPLETED", 9)) {
ok = 1;
char *ib = strstr(st, "ip_address=");
if (ib) { ib += 11; char *ie = ib; while (*ie && *ie != '\n' && *ie != '\r') ie++;
int il = (int)(ie - ib); if (il >= (int)sizeof ip) il = (int)sizeof ip - 1;
memcpy(ip, ib, (size_t)il); ip[il] = 0; }
break;
}
}
if (waited >= wait_ms) break;
usleep(200000);
waited += 200;
}
if (!ok) {
if (created) { snprintf(args, sizeof args, "remove_network %d", id); nm_direct_wpa(out, sizeof out, args, 1500); nm_direct_save_config(); }
if (err) snprintf(err, elen, "连接失败:密码错误或信号不佳(超时未关联)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d timeout", ssid, hidden, id);
return MEOW_ERR_NODEV;
}
nm_direct_wpa(out, sizeof out, "enable_network all", 1500);
int persisted = nm_direct_save_config();
if (err) snprintf(err, elen, "已连接:%s ip=%s%s", ssid, ip[0] ? ip : "-", persisted ? "" : "(记录未持久化)");
logmsg("netmgr_ipc: [direct-fallback] wifi connect ssid=%s hidden=%d id=%d ip=%s", ssid, hidden, id, ip[0] ? ip : "-");
return MEOW_OK;
}
int nm_wifi_connect_ex(const char *ssid, const char *psk, int hidden, char *err, int elen)
{
if (!ssid || !ssid[0]) { if (err) snprintf(err, elen, "ssid 缺失"); return MEOW_ERR_INVAL; }
/* M64 P1-1:IPC 路径与直连回退同一套校验(netmgrd 侧还会再校验一次)。 */
if (!nm_wifi_ssid_ok(ssid)) {
if (err) snprintf(err, elen, "ssid 非法(长度 1..32 字节,禁控制字符)");
return MEOW_ERR_INVAL;
}
if (!nm_wifi_psk_ok(psk)) {
if (err) snprintf(err, elen, "psk 非法(≤63 字符口令或 64 位 hex,禁控制字符)");
return MEOW_ERR_INVAL;
}
char arg[132];
snprintf(arg, sizeof arg, "%s,%s", ssid, psk ? psk : ""); /* 带空 psk → netmgrd 复用既有网络 */
char text[192];
int rc = netmgr_wifi_tmo(hidden ? "connecth" : "connect", arg, text, sizeof text,
hidden ? NM_TMO_CONNECTH : NM_TMO_CONNECT);
if (rc == NETMGR_UNREACH) return nm_direct_connect_ex(ssid, psk, hidden, err, elen);
if (err && elen > 0) snprintf(err, elen, "%s", text);
return rc;
}
int nm_wifi_connect(const char *ssid, const char *psk, char *err, int elen)
{ return nm_wifi_connect_ex(ssid, psk, 0, err, elen); }
int nm_wifi_disconnect(char *err, int elen)
{
char text[128];
int rc = netmgr_wifi("disconnect", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 disconnect 2>/dev/null");
popen_read_tmo(1200, cmd);
if (err) snprintf(err, elen, "已下发");
return MEOW_OK;
}
if (err && elen > 0) snprintf(err, elen, "%s", text);
return rc;
}
int nm_wifi_toggle(int on)
{
char text[128];
int rc = netmgr_wifi(on ? "on" : "off", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { s_net_toggle_wifi(on); return MEOW_OK; } /* 直连回退 */
return rc;
}
/* ================= BT(IPC 优先:全量子命令权威收口 P1;不可达回退直连)================= */
static int nm_parse_devices(const char *txt, meow_net_t *out, int max); /* 前向声明 */
int nm_bt_powered(void)
{
char text[128];
int rc = netmgr_bt("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return hci_powered();
if (rc != MEOW_OK) return 0;
char v[8];
nm_val(text, "powered=", v, sizeof v, 0);
return atoi(v);
}
/* ===== M47:设置页开关的「真实态」三态查询(-1 未知 / 0 关 / 1 开)=====
* 与 nm_bt_powered()/nm_wifi_connected() 的区别:那两个把「问不到」塌缩成 0(看起来像
* 「已关」),正是开关显示不可信的根因之一。M47 起设置页开关只用下面两个函数。 */
int nm_bt_power_state(void)
{
char text[192];
int read_timeout = 0;
/* 状态查询走专用入口:读超时 = 「状态未知」,且不把 netmgrd 标成 down(见 nm_cmd_t 注释)。 */
int rc = nm_cmd_t(MSG_NET_BT_CTL, MSG_NET_BT_RESULT, "status", NULL,
text, sizeof text, NM_TMO_DFL, &read_timeout);
if (read_timeout) return -1;
if (rc == NETMGR_UNREACH) return net_bt_power_probe(); /* 直连回退;失败=未知 */
if (rc != MEOW_OK) return -1; /* daemon 报错:不知道就是不知道 */
char v[8];
nm_val(text, "available=", v, sizeof v, 0);
if (v[0] && atoi(v) == 0) return 0; /* 无控制器 → 必为关 */
nm_val(text, "powered=", v, sizeof v, 0);
if (!v[0]) return -1; /* 无 powered 字段 → 未知 */
return atoi(v) ? 1 : 0;
}
/* WiFi 射频真实态(三态)。netmgrd 的 `wifi status` 只报 wpa_state/连接态,不报射频开关
* (见 meow_netmgrd.c wifi_status,字段仅 state/connected/ssid/ip/rssi),无法据此判断
* 「wifi 开关」;而 daemon 的 wifi on/off 恰好就是 rfkill wlan block/unblock,所以射频态
* 取本地 rfkill(回退 wlan0 IFF_UP)——与 daemon 改的是同一份内核状态,且探测失败=未知。 */
int nm_wifi_power_state(void)
{
return net_wifi_power_probe();
}
int nm_bt_onoff(int on)
{
char text[128];
int rc = netmgr_bt(on ? "on" : "off", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { s_net_toggle_bt(on); return MEOW_OK; }
return rc;
}
int nm_bt_status(int *powered, int *paired_n, char *addr, int alen)
{
char text[192];
int rc = netmgr_bt("status", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) { /* 直连回退:读内核/tool */
if (powered) *powered = hci_powered();
if (paired_n) *paired_n = -1;
if (addr && alen > 0) addr[0] = 0;
return rc;
}
if (rc == MEOW_OK) {
char v[8];
if (powered) { nm_val(text, "powered=", v, sizeof v, 0); *powered = atoi(v); }
if (paired_n) { nm_val(text, "paired=", v, sizeof v, 0); *paired_n = atoi(v); }
if (addr && alen > 0) nm_val(text, "address=", addr, alen, 0);
}
return rc;
}
int nm_bt_paired(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("paired", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_paired(out, max);
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
/* ---- P1 收口:连接/断开/配对/扫描/A2DP 全部迁 netmgrd(权威),不可达回退直连 ---- */
/* 通用 `Device <MAC> <Name>` 行解析(netmgrd bt {connected,paired,devices} 文本;launcher 只做客户端)。 */
static int nm_parse_devices(const char *txt, meow_net_t *out, int max)
{
int n = 0;
if (!txt || max <= 0) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char *line = (char *)txt;
while (line && *line && n < max) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
char *p = strstr(line, "Device ");
if (p) {
p += 7;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
char *nm2 = mac; while (*nm2 == ' ' || *nm2 == '\t') nm2++;
int ml = (int)(mac - p); if (ml > 47) ml = 47;
if (ml > 0 && ml < (int)sizeof out[n].mac) {
memcpy(out[n].mac, p, (size_t)ml); out[n].mac[ml] = 0;
}
snprintf(out[n].name, sizeof out[n].name, "%s", *nm2 ? nm2 : p);
out[n].id = -1; out[n].sec[0] = 0;
n++;
}
line = eol ? eol + 1 : (char *)0;
}
return n;
}
int nm_bt_connected(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("connected", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_connected(out, max); /* 直连回退 */
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
int nm_bt_devices(meow_net_t *out, int max)
{
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("devices", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH) return net_bt_scan(out, max); /* 直连回退(读扫描缓存) */
if (rc != MEOW_OK) return 0;
return nm_parse_devices(text, out, max);
}
/* A2DP 端点解析:"A2DP <mac> codec=.. rate=.. channels=.. bits=.. acquired=.. role=.." */
int nm_bt_a2dp(bt_ep_t *eps, int max)
{
if (max <= 0) return 0;
char text[MEOW_IPC_MAX - 32];
int rc = netmgr_bt("a2dp", NULL, text, sizeof text);
if (rc == NETMGR_UNREACH || rc != MEOW_OK) return 0; /* A2DP 详情无直连回退(P2 bluealsa 才接) */
int n = 0;
char *line = text;
while (line && *line && n < max) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
if (!strncmp(line, "A2DP ", 5)) {
char *p = line + 5;
bt_ep_t *e = &eps[n];
memset(e, 0, sizeof *e);
snprintf(e->role, sizeof e->role, "%s", "-");
e->acquired = 0;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n') mac++;
int ml = (int)(mac - p); if (ml > 23) ml = 23;
if (ml > 0) { memcpy(e->mac, p, (size_t)ml); e->mac[ml] = 0; }
char *t = mac;
while (*t) {
while (*t == ' ' || *t == '\t') t++;
if (!strncmp(t, "codec=", 6)) { t += 6; nm_val_field(&t, e->codec, sizeof e->codec); }
else if (!strncmp(t, "rate=", 5)) { t += 5; nm_val_field(&t, e->rate, sizeof e->rate); }
else if (!strncmp(t, "channels=", 9)) { t += 9; nm_val_field(&t, e->chn, sizeof e->chn); }
else if (!strncmp(t, "bits=", 5)) { t += 5; nm_val_field(&t, e->bits, sizeof e->bits); }
else if (!strncmp(t, "acquired=", 9)) { t += 9; e->acquired = atoi(t); }
else if (!strncmp(t, "role=", 5)) { t += 5; nm_val_field(&t, e->role, sizeof e->role); }
while (*t && *t != ' ' && *t != '\t' && *t != '\n') t++;
}
n++;
}
line = eol ? eol + 1 : NULL;
}
return n;
}
/* 直连回退:bluetoothctl <sub> <mac>(带超时)。返回 MEOW_OK/MEOW_ERR_*。 */
static int bt_direct_cmd(const char *sub, const char *mac)
{
if (!mac || !mac[0]) return MEOW_ERR_INVAL;
char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin bluetoothctl %s %s 2>/dev/null", sub, mac);
char *o = popen_read_tmo(8000, cmd);
int ok = o && (strstr(o, "successful") || strstr(o, "Connected") ||
strstr(o, "already") || strstr(o, "Paired"));
if (o) free(o);
return ok ? MEOW_OK : MEOW_ERR_NODEV;
}
int nm_bt_connect(const char *mac)
{
char text[128];
int rc = netmgr_bt("connect", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("connect", mac);
return rc;
}
int nm_bt_disconnect(const char *mac)
{
char text[128];
int rc = netmgr_bt("disconnect", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("disconnect", mac);
return rc;
}
int nm_bt_pair(const char *mac)
{
char text[128];
int rc = netmgr_bt("pair", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("pair", mac);
return rc;
}
int nm_bt_unpair(const char *mac)
{
char text[128];
int rc = netmgr_bt("unpair", mac, text, sizeof text);
if (rc == NETMGR_UNREACH) return bt_direct_cmd("remove", mac);
return rc;
}
void nm_bt_scan_on(void)
{
if (!netmgr_ok()) { net_bt_scan_trigger(); return; } /* 直连回退 */
netmgr_bt("scan", "on", NULL, 0);
}
void nm_bt_scan_off(void)
{
if (!netmgr_ok()) return;
netmgr_bt("scan", "off", NULL, 0);
}