mihomo-cli 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,71 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.1] - 2026-09-01
4
+
5
+ 一轮全量代码审查的修复集:1 项新发现的高危数据丢失,以及上轮审查遗留的 9 项「仍待处理」。全部经实际复现确认,修复后回归验证。
6
+
7
+ ### 修复
8
+
9
+ - **并发操作会静默丢数据(高危)** - `settings.json` 无跨进程锁,两个 CLI 进程各自读旧全量再写回,后写者把先写者的改动整块抹掉,**而先写者已经打印了「已添加」**。实测 6 个并发 `sub add` 丢 3 条;慢速 `sub add`(跨网络下载)期间执行 `tunnel add`,隧道被抹成 `null`——订阅与隧道同住一个文件,互不相干的命令互相摧毁。触发条件很日常:机场慢时 `sub add` 要跑十几秒,此间在另一个终端做点别的即可
10
+
11
+ 修法是新增文件锁(`O_EXCL`)把整个「读-改-写」圈起来。仅「写前重读盘」不够——读与写之间仍有窗口,实测仍丢 3 条。持锁超过 10s 视为进程已崩溃并强夺,避免一次崩溃永久锁死后续所有命令
12
+
13
+ - **内核下载来源未钉死(供应链)** - `--mirror-all` 下连 GitHub API 都走镜像,`browser_download_url` 完全由镜像说了算,而此前对非 github 的 URL 原样放行,镜像返回任意主机地址即可让 CLI 下载任意二进制;该产物随后 `chmod 755` 并在 TUN/daemon 下**以 root 运行**。现加下载 host 白名单 + 强制 https(校验在加镜像前缀之前),curl 补 `--proto '=https' --proto-redir '=https'`(实测 `-L` 会跟着 302 降级到明文 http 并落盘)与 `--max-filesize`,下载后比对 `asset.size`
14
+
15
+ - **归档 symlink 成员可 chmod 任意文件** - 名为 `mihomo`、指向任意路径的符号链接,其条目名完全合法(不含 `..`、非绝对路径),能通过路径穿越守卫,随后被当成二进制 `chmod 755`——实测把 `chmod 600` 的文件改成了 755。现同时校验条目**类型**(拒符号/硬链接)并把遍历的 `statSync` 改为 `lstatSync`
16
+
17
+ - **畸形流量响应头会抹掉已有用量数据** - `Subscription-Userinfo` 返回 `garbage` 这类无有效字段的头时,解析结果 `{}` 是 truthy,导致 upload/download/total/expire 四个字段被 `undefined` 覆盖。反直觉的是**没有响应头反而安全**(整块跳过)。另修 `expire=abc` 被塞 0 而显示成「永久有效」(垃圾值朝最误导的方向失败)、`total=1e999` 写成 `null`、负数原样入库使百分比失真
18
+
19
+ - **时钟偏移导致订阅永不自动更新** - `updated_at` 落在未来(系统时间被改过、跨时区调时)时时间差恒为负,`needsAutoUpdate` 恒 false,订阅静默过期到失联
20
+
21
+ - **`daemon off` 在 plist 被手动删除后是空操作** - 用户 `sudo rm` 掉 plist 后任务仍处装载状态,此前只看文件存在就直接返回,永不执行 `launchctl bootout`,于是 KeepAlive 继续把内核拉起——杀掉立刻重生,且 CLI 无任何途径卸载。现判据改为「plist 不在**且**无 root 内核在跑」
22
+
23
+ - **`daemon on` 遗留 root 属主 pid 文件** - 使后续 `daemon off` → `start` 撞上残留检查而拒绝启动,这个死胡同完全由 CLI 自身的 on/off 循环造成
24
+
25
+ - **热重载信任 9090 上的任意响应** - 该端口被其他服务占用且对 PUT 返回 2xx 时,CLI 打印「已重启 (保活)」而 daemon 内核仍跑旧配置,配置变更静默未生效。现先确认应答方确为 mihomo
26
+
27
+ - **`tunnel up` 期间 Ctrl+C 泄漏 ssh 进程** - 等待转发建立最长 20 秒,其间中断会留下孤儿 ssh 进程和一份声称健康的运行态文件(实测持久残留、不自愈),随后 `tunnel up` 报「已在运行」而 `status` 报「假活」,自相矛盾。现在该窗口内注册一次性清理,启动成功后立即注销以保持「隧道活过 CLI 退出」的语义
28
+
29
+ - **`__proto__` 作订阅名时缓存永远写不进** - 该名称通过校验,但在普通对象上赋值是设置原型而非自有属性,落盘为 `{}`,`updated_at` 永远缺失 → 每次 `start` 都重新下载该订阅
30
+
31
+ - **配置校验的四个静默空洞** - 均无任何告警:proxy 与 proxy-group 同名(两者都留下,mihomo 因重复名拒绝加载);`use` 引用不存在的 provider(从不校验,且使该组免于删除);节点池为空 + `include-all` 的空组保留(实际无任何出口);`proxies` 写成字符串时整体跳过校验、非法结构原样落盘
32
+
33
+ - **`--mirror` 接受非法 scheme** - 此前用 `startsWith('http')` 判断,放行 `httpfoo://x` 与明文 `http://`,并把 `ftp://e.test` 拼成 `https://ftp://e.test/`。现改用 `URL` 解析 + https 白名单
34
+
35
+ - **sudo 中间脚本的权限位不重放** - `writeFileSync` 的 `mode` 只在创建新文件时生效,前次崩溃残留的同名文件会保留旧权限(实测 0666 重写后仍是 0666),而该文件下一步就交给 sudo 执行。两处写后补显式 `chmodSync`
36
+
37
+ ### 测试
38
+
39
+ - 单测 145 → 165:文件锁 6 项(含用真实子进程验证跨进程互斥、陈旧锁强夺、异常路径释放)、`parseUserInfo` 边界 8 项、配置校验空洞 6 项(含「正常配置不产生告警」的回归用例)
40
+
41
+ ## [3.8.0] - 2026-09-01
42
+
43
+ ### 新增
44
+
45
+ - **ssh 隧道出口(`tunnel` 子命令,别名 `ssh`)** - 管理 `ssh -D` 动态转发进程的生命周期,把可 ssh 登录的机器变成本地 SOCKS5 出口。分流仍交给覆写机制,本功能补的是「隧道断了 mihomo 不知情、会一直往死端口送流量」这一环
46
+
47
+ ```bash
48
+ mihomo tunnel add work --host m4 --port 1080
49
+ mihomo tunnel up|down|status [名字]
50
+ mihomo tunnel rm <名字>
51
+ ```
52
+
53
+ - **随 `start` 一并拉起**:默认带 `auto` 标记,`start` 顺带启动、`stop` 连带停止(`--no-tunnel` 跳过,`add --no-auto` 不参与)
54
+ - **隧道失败不影响内核启动**:只影响内网分流那部分规则,故仅打印显眼的黄色警告并附上 ssh 给出的原因,其余流量照常
55
+ - **`stop` 只停自己起的**:手动 `tunnel up` 起的隧道带 `manual` 标记,不会被 `mihomo stop` 带走,避免下次 start 又起一个而累积僵尸进程
56
+ - **`status` 真实探测端口**:能识别「进程还在但转发已死」的假活——那正是最误导的形态,此时 mihomo 仍在往死端口送流量
57
+ - **起之前先检测端口占用**,不盲启后失败
58
+ - **`add` 生成覆写模板** `overwrite.tunnel-<名字>.yaml`(已建好 socks5 节点与 select 分组,分流规则留白待填),生成后完全由用户维护,CLI 不再改写,`tunnel rm` 也不删它
59
+ - 新增 `reset tunnel` 目标(先停进程再删运行态,反序会导致 ssh 进程失联且再也停不掉)
60
+
61
+ 安全边界:`-D` 恒绑 `127.0.0.1` 且不提供绑定地址开关(绑 `0.0.0.0` 会让同一 WiFi 下任何设备经本机进内网);`--host` 拒绝 `-` 开头的值(`-oProxyCommand=...` 等同任意命令执行);ssh 参数固定带 `ExitOnForwardFailure`/`BatchMode`/`ConnectTimeout`/`ServerAlive*`
62
+
63
+ 暂不做自动重连保活:断线依靠 `ServerAliveInterval` 让进程自退,再用 `tunnel status` 查出来
64
+
65
+ ### 修复
66
+
67
+ - **`reset --full` 会留下空的 `settings.json`** - 新增的 `tunnel` 目标其 `onAfter` 会 `writeSettings` 清空隧道列表,若排在 `settings` 之后就会把刚删掉的文件重建成 `{}`,与「已重置: 设置」矛盾。现移到 `settings` 之前(与 `subs` 同理),并加单测锁定该顺序约束——这是 v3.7.0 已修过一次的同类问题(当时是参数顺序),换个目标又复发了
68
+
3
69
  ## [3.7.0] - 2026-08-22
4
70
 
5
71
  ### 修复
package/README.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - 🔄 **智能重启** - `sub use` 切换订阅、`ow on/off` 切换覆写后自动重启
15
15
  - 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
16
16
  - 🛡️ **进程保活** - 基于 launchd(root),崩溃/开机自动拉起,代理后台常驻(`daemon on`)
17
+ - 🔌 **ssh 隧道出口** - 管理 `ssh -D` 进程生命周期,把内网机器变成本地 SOCKS5 出口,随 `start` 一并拉起
17
18
  - 🔄 **双模式支持** - Mixed 模式和 TUN 透明代理模式
18
19
  - 📊 **状态监控** - 查看运行状态、内存占用、订阅流量与到期时间
19
20
  - 📝 **日志管理** - 实时日志 + 历史日志归档(自动轮转,保留7天)
@@ -126,11 +127,16 @@ mihomo ui yacd # YACD
126
127
  | --------------------------------- | ------------------------------------------------------------------- |
127
128
  | `mihomo kernel [--mirror [镜像]]` | 更新内核(默认直连,`--mirror` 使用镜像;更新后运行中实例需重启生效) |
128
129
  | `mihomo daemon [on\|off\|status]` | 进程保活:开机自启 + 崩溃自动重启(仅 Mixed 模式,on/off 需管理员密码) |
130
+ | `mihomo tunnel` | 列出 ssh 隧道及真实状态(别名 `ssh`/`tunnels`) |
131
+ | `mihomo tunnel add <名字> --host <主机> --port <端口> [--no-auto]` | 添加隧道并生成覆写模板(默认随 start 拉起) |
132
+ | `mihomo tunnel up\|down [名字]` | 启动/停止隧道(无参即全部) |
133
+ | `mihomo tunnel status [名字]` | 查看隧道状态(真实探测端口,能识别「假活」) |
134
+ | `mihomo tunnel rm <名字> [-y]` | 删除隧道(不删覆写文件) |
129
135
  | `mihomo update` | 更新 mihomo-cli(先查 npm 最新版,已是最新则跳过重装) |
130
136
  | `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
131
137
  | `mihomo dir` | 显示数据目录位置 |
132
138
  | `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `data`, `runtime`, `kernel`) |
133
- | `mihomo reset [目标...] [--full] [-y]` | 重置用户数据(可用目标:`subs`, `logs`, `data`, `runtime`, `settings`, `kernel`, `overwrites`, `daemon`;`--full` 删全部,`-y` 跳过确认) |
139
+ | `mihomo reset [目标...] [--full] [-y]` | 重置用户数据(可用目标:`subs`, `logs`, `data`, `runtime`, `settings`, `kernel`, `overwrites`, `daemon`, `tunnel`;`--full` 删全部,`-y` 跳过确认) |
134
140
  | `mihomo version` | 显示版本信息 |
135
141
  | `mihomo help` | 显示帮助信息 |
136
142
 
@@ -143,7 +149,7 @@ mihomo ui yacd # YACD
143
149
  - `mhm`
144
150
  - `mh`
145
151
 
146
- 子命令组亦有别名:`subscription` = `sub`/`subs`/`subscriptions`,`directory` = `dir`/`dirs`/`directories`,`overwrite` = `ow`
152
+ 子命令组亦有别名:`subscription` = `sub`/`subs`/`subscriptions`,`directory` = `dir`/`dirs`/`directories`,`overwrite` = `ow`,`tunnel` = `ssh`/`tunnels`(`tun` 已被 TUN 模式占用)
147
153
 
148
154
  ### 快捷命令
149
155
 
@@ -173,6 +179,54 @@ mihomo ui yacd # YACD
173
179
  - 需要 sudo / 管理员权限
174
180
  - 首次使用会自动配置 DNS 和路由
175
181
 
182
+ ## ssh 隧道出口
183
+
184
+ 把一台可 ssh 登录的机器(如公司内网的机器)变成本地 SOCKS5 出口,配合覆写规则即可只让内网域名走它,其余流量照常走订阅节点。本功能负责的是 **ssh 进程的生命周期管理**——隧道断了 mihomo 并不知情,会一直往死端口送流量。
185
+
186
+ ```bash
187
+ mihomo tunnel add work --host m4 --port 1080 # m4 是 ~/.ssh/config 里的别名
188
+ mihomo tunnel up work # 启动
189
+ mihomo tunnel status # 查看状态(真实探测端口)
190
+ mihomo tunnel down work # 停止
191
+ ```
192
+
193
+ `add` 会生成一份覆写模板 `overwrite.tunnel-<名字>.yaml`,其中已建好 socks5 节点与分组,
194
+ **分流规则留白待你填写**(CLI 无从知道你的内网域名):
195
+
196
+ ```yaml
197
+ # 取消注释并改成你的内网域名/网段
198
+ +rules:
199
+ - DOMAIN-SUFFIX,example.internal,Tunnel-work
200
+ - IP-CIDR,10.0.0.0/8,Tunnel-work
201
+ ```
202
+
203
+ 该文件生成后**完全由你维护**,CLI 不会再改写;`tunnel rm` 也不会删它。改完执行 `mihomo start` 生效。
204
+
205
+ ### 与 start / stop 的联动
206
+
207
+ 隧道默认带 `auto` 标记,`mihomo start` 会顺带拉起、`mihomo stop` 会连带停止(`--no-tunnel` 可跳过;`add` 时加 `--no-auto` 则不参与)。
208
+
209
+ - **隧道起不来不会让 `start` 失败**——它只影响内网分流那部分规则,其余流量正常,但会打印显眼的黄色警告并附上 ssh 给出的原因
210
+ - **`stop` 只停自己起的**:手动 `tunnel up` 起的隧道不会被 `mihomo stop` 带走,避免下次 start 又起一个而累积僵尸进程
211
+
212
+ ### 状态的三种形态
213
+
214
+ `tunnel status` 会**真实探测端口是否在监听**,而不是只看进程在不在:
215
+
216
+ | 状态 | 含义 |
217
+ | --- | --- |
218
+ | 运行中 | 进程在且端口在监听,可正常使用 |
219
+ | **假活** | 进程还在但端口不通——最需要警惕的形态,此时 mihomo 仍在往死端口送流量 |
220
+ | 未运行 | 进程不在 |
221
+
222
+ ### 安全边界
223
+
224
+ - `-D` **恒绑 `127.0.0.1`**,不提供绑定地址开关:绑 `0.0.0.0` 会让同一 WiFi 下任何设备都能经本机进入内网
225
+ - ssh 参数固定带 `ExitOnForwardFailure`/`BatchMode`/`ConnectTimeout`/`ServerAlive*`,分别防「假活」、无 TTY 挂死、久等、断线后端口成僵尸
226
+ - `--host` 拒绝以 `-` 开头的值(`-oProxyCommand=...` 会被 ssh 当选项解析,等同任意命令执行)
227
+
228
+ > 暂不做自动重连保活。断线依靠 `ServerAliveInterval` 让 ssh 进程自行退出,再用 `tunnel status` 查出来。
229
+
176
230
  ## 进程保活
177
231
 
178
232
  默认情况下,mihomo 内核在后台独立运行,但如果内核崩溃、被系统 kill(如内存不足)、或重启/重新登录后,代理就会失效且不会自动恢复。进程保活用 macOS 原生的 **launchd** 解决这个问题。
@@ -279,6 +333,7 @@ mihomo test --timeout=3000 # 长选项 + 等号
279
333
  ├── settings.json # 用户设置(订阅列表等)
280
334
  ├── overwrite.yaml # 覆写配置(主文件,可选)
281
335
  ├── overwrite.*.yaml # 覆写配置(扩展文件,如 overwrite.dns.yaml)
336
+ ├── overwrite.tunnel-*.yaml # 隧道覆写(首次由 tunnel add 生成,此后由你维护)
282
337
  ├── subscriptions/
283
338
  │ ├── cache.json # 订阅动态缓存(更新时间、流量、到期时间等)
284
339
  │ └── <name>.yaml # 订阅原始配置
@@ -286,8 +341,11 @@ mihomo test --timeout=3000 # 长选项 + 等号
286
341
  │ └── mihomo # mihomo 内核二进制
287
342
  ├── logs/
288
343
  │ ├── mihomo.log # 当前日志
289
- └── mihomo.YYYY-MM-DD_HH-MM-SS.log # 归档日志
344
+ ├── mihomo.YYYY-MM-DD_HH-MM-SS.log # 归档日志
345
+ │ └── tunnel-<name>.log # 隧道 ssh 输出(每次启动覆写)
290
346
  ├── data/ # mihomo 运行数据(GeoIP 等,由内核自行管理)
347
+ ├── tunnel/ # 隧道运行态(stop 不清除,故不放在 runtime/)
348
+ │ └── <name>.json # PID、谁启动的、启动时间
291
349
  └── runtime/ # 运行时临时文件(stop 自动清除)
292
350
  ├── pid # 进程 PID
293
351
  ├── config.yaml # 运行时生成的配置