@trim21/personal-pi-extensions 0.1.456 → 0.1.460
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/package.json +2 -1
- package/src/bwrap/README.md +92 -0
- package/src/bwrap/core.ts +17 -4
- package/src/bwrap/runtime.ts +9 -0
- package/vendor/mihomo-linux-x64 +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.460",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
+
"vendor",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"publishConfig": {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# bwrap 沙箱与网络栈架构
|
|
2
|
+
|
|
3
|
+
本文档描述 `net-allowlist` 模式下的进程模型、网络路径与生命周期管理。
|
|
4
|
+
基础沙箱(bwrap 文件系统隔离)见 `core.ts` / `sandbox.ts`;本文聚焦网络栈
|
|
5
|
+
(`network-stack.ts` / `holder.ts` / `mihomo-config.ts`)。
|
|
6
|
+
|
|
7
|
+
## 进程模型
|
|
8
|
+
|
|
9
|
+
`net-allowlist` 模式下,一个沙箱 session 的常驻进程树(宿主侧视角,共 4 个):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pi 进程(network-stack.ts)
|
|
13
|
+
├─ ① unshare -Urnp --fork --kill-child=SIGTERM -- node holder.js …
|
|
14
|
+
│ 创建 user+net+pid 三个 ns;--fork 后由子进程 exec node;
|
|
15
|
+
│ 自己留在原地 wait;--kill-child=SIGTERM 给 ② 设 PDEATHSIG。
|
|
16
|
+
│ 持 exit-fd 写端(fd 3)。
|
|
17
|
+
│ └─ ② node holder.js (pid-ns 的 init,即该 pid-ns 里的 PID 1)
|
|
18
|
+
│ 读 stdin(EOF 自杀)、等 tap0、拉起 ③;持 exit-fd 写端 fd 3。
|
|
19
|
+
│ └─ ③ mihomo (netns 内:TUN "Meta" + 策略路由 + fakeip DNS)
|
|
20
|
+
└─ ④ slirp4netns -c --userns-path=…/ns/user --netns-type=pid <①的pid> tap0 -e 3
|
|
21
|
+
必须在宿主 netns 启动(原因见「设计约束」);持 exit-fd 读端 + tapfd。
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
每条命令的短命子树(命令结束即退,与常驻栈无关):
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
nsenter -U -n --preserve-credentials -t <①的pid> \
|
|
28
|
+
-- bwrap --unshare-user --unshare-pid … -- bash -lc '<command>'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
nsenter 进入 holder 的 userns/netns,bwrap 在里面再嵌套创建自己的 user/pid
|
|
32
|
+
ns 跑命令。一个 session 内 N 条命令复用同一套常驻栈。
|
|
33
|
+
|
|
34
|
+
## 网络路径
|
|
35
|
+
|
|
36
|
+
- **mihomo(③)**:TUN(`auto-route` + `strict-route`)+ fakeip +
|
|
37
|
+
deny-by-default。allowlist 域名进 `fake-ip-filter`(真实解析),DNS 层
|
|
38
|
+
`DOMAIN-SUFFIX,…,DIRECT`;连接层未命中 allowlist 的流量 `MATCH,REJECT`。
|
|
39
|
+
注意:fakeip 对不在 filter 里的域名**直接本地应答**,不会走到
|
|
40
|
+
`dns.rules` 的 REJECT——未允许域名是先拿 fakeip、连接层再被拒。
|
|
41
|
+
- **slirp4netns(④)**:egress NAT。它 fork helper 进 netns 创建 tap0 并把
|
|
42
|
+
tapfd 传回主进程,真正的出站 socket 在宿主 netns。
|
|
43
|
+
- **interface-name: tap0**:mihomo 出站静态绑定 slirp 接口。不能用
|
|
44
|
+
`auto-detect-interface` 顶替——启动瞬间 tap0 可能尚未就绪,monitor 事后
|
|
45
|
+
纠正但 DNS 拨号已走错接口,上游查询进自己的 TUN 被 `dns-hijack` 自劫持。
|
|
46
|
+
- **mihomo `-d <uuid 目录>`**:cache.db 等落盘位置,放
|
|
47
|
+
`<agentDir>/tmp/mihomo-<uuid>/`,每次启动独立目录避免并发争抢 bbolt 锁。
|
|
48
|
+
|
|
49
|
+
## 生命周期与清理
|
|
50
|
+
|
|
51
|
+
exit-fd(socketpair)是 slirp4netns 与 holder 之间唯一的生命周期绑定:
|
|
52
|
+
读端给 slirp4netns(`-e 3`),写端由 holder 进程持有(network-stack 经
|
|
53
|
+
`unshare` 的额外 stdio 传入 fd 3)。Node 对额外 stdio pipe 没有公开的 fd
|
|
54
|
+
访问器(`stdio[3].fd` 恒为 undefined),只能经 `_handle.fd` 取原始 fd 再
|
|
55
|
+
dup 给 slirp4netns,且仅在子进程存活期间有效。
|
|
56
|
+
|
|
57
|
+
| 触发 | 清理链路 |
|
|
58
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| 正常 `stop()` | SIGTERM ④(先杀,它 pin 着 netns)→ SIGTERM ① → `--kill-child` 转发给 ② → ② 杀 ③ 退出 → 内核清 pid ns |
|
|
60
|
+
| pi 进程被 SIGKILL | stdin 写端关闭 → ② EOF 自杀 → pid ns 清理 → exit-fd 写端关闭 → ④ HUP 自杀 → tapfd 释放 → netns 销毁 |
|
|
61
|
+
| 单独 kill ① | PDEATHSIG → ② SIGTERM → 同上;① wait 结束退出 → 写端全关 → ④ 退 |
|
|
62
|
+
| 单独 kill ② | pid-ns init 死 → 内核清 ③;① 退出 → 写端全关 → ④ 退 |
|
|
63
|
+
|
|
64
|
+
四条路径下常驻进程全部收敛、netns 引用归零。
|
|
65
|
+
|
|
66
|
+
## 设计约束与教训
|
|
67
|
+
|
|
68
|
+
1. **slirp4netns 必须在宿主 netns 启动**。它的 egress socket 决定出站视角;
|
|
69
|
+
若留在沙盒 netns 里(holder 内启动),出站流量会被 mihomo 的 TUN 策略
|
|
70
|
+
路由 + `dns-hijack any:53` 自劫持成环:上游 DNS 查询自己劫自己,
|
|
71
|
+
mihomo 对劫持查询回 SERVFAIL(源 IP 伪装成原目的地址),allowlist 域名
|
|
72
|
+
全部 `ENOTFOUND`,而未 allowlist 域名反而"正常"(fakeip 本地应答)。
|
|
73
|
+
宿主侧启动后必须用 exit-fd 绑定生命周期,否则 holder 死后 slirp4netns
|
|
74
|
+
持 tapfd 泄漏 netns。
|
|
75
|
+
2. **tap fd pin 住 netns**:slirp4netns 持有 tapfd 期间 netns 不会销毁,
|
|
76
|
+
所以任何架构下 slirp4netns 的终止都必须显式保证(stop() / exit-fd)。
|
|
77
|
+
3. **fakeip 短路**:`dns.rules` 的 REJECT 拦不住 fakeip 应答,deny-by-default
|
|
78
|
+
实际由连接层 `MATCH,REJECT` 兜底。诊断时不要把"未允许域名能解析出
|
|
79
|
+
198.18.x.x"当成 DNS 层放行。
|
|
80
|
+
4. **诊断手段**:`pnpm sandbox --verbose` 透传 holder(mihomo/slirp4netns)
|
|
81
|
+
日志;`nsenter -U -n --preserve-credentials -t <holderPid>` 可手动进入
|
|
82
|
+
netns 用 AF_PACKET 抓 tap0 / 检查 `ip rule`(注意:沙盒里看不到宿主机
|
|
83
|
+
进程,宿主机诊断必须在沙盒外做)。
|
|
84
|
+
|
|
85
|
+
## 调试入口
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# 诊断执行(与扩展同一条代码路径)
|
|
89
|
+
pnpm sandbox --verbose -- '<command>'
|
|
90
|
+
# 修改 holder.ts 后需重新构建构建产物 holder.js
|
|
91
|
+
pnpm build:holder
|
|
92
|
+
```
|
package/src/bwrap/core.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ChildProcess, spawn } from "node:child_process";
|
|
|
2
2
|
import { constants, type Dirent, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { access as fsAccess, readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
8
|
import { type BashOperations, getAgentDir, getShellConfig } from "@earendil-works/pi-coding-agent";
|
|
@@ -241,16 +242,28 @@ function findCommandInPath(name: string, hint: string): string {
|
|
|
241
242
|
throw new Error(hint);
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
// npm 包内自带的静态编译 mihomo(linux-x64),发布时由 CI 下载到 vendor/;
|
|
246
|
+
// 源码方式运行(vendor 不存在)时自然跳过,走 PATH 查找。
|
|
247
|
+
const VENDORED_MIHOMO_PATH = fileURLToPath(
|
|
248
|
+
new URL("../../vendor/mihomo-linux-x64", import.meta.url),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
export function findMihomo(override?: string, bundledPath = VENDORED_MIHOMO_PATH): string {
|
|
245
252
|
if (override) {
|
|
246
253
|
if (!existsSync(override)) {
|
|
247
254
|
throw new Error(`mihomo not found at configured path: ${override}`);
|
|
248
255
|
}
|
|
249
256
|
return override;
|
|
250
257
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
// 优先全局 PATH(用户自装的 mihomo),没有时回退到包内自带二进制
|
|
259
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
260
|
+
const candidate = join(directory, "mihomo");
|
|
261
|
+
if (existsSync(candidate)) return candidate;
|
|
262
|
+
}
|
|
263
|
+
if (existsSync(bundledPath)) return bundledPath;
|
|
264
|
+
throw new Error(
|
|
265
|
+
"mihomo not found in PATH and the package ships no bundled binary. " +
|
|
266
|
+
"Install it from https://github.com/MetaCubeX/mihomo",
|
|
254
267
|
);
|
|
255
268
|
}
|
|
256
269
|
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { commandPatternsFor } from "./approval-suggest.js";
|
|
|
28
28
|
import {
|
|
29
29
|
type BwrapMode,
|
|
30
30
|
findBwrap,
|
|
31
|
+
findMihomo,
|
|
31
32
|
getBwrapConfigPaths,
|
|
32
33
|
loadBwrapConfig,
|
|
33
34
|
resolveBwrap,
|
|
@@ -273,6 +274,8 @@ export class BwrapRuntime {
|
|
|
273
274
|
private resolved: ResolvedBwrap | undefined;
|
|
274
275
|
private sandboxDisabled = false;
|
|
275
276
|
private bwrapUnavailable = false;
|
|
277
|
+
/** net-allowlist 首次执行时解析一次 mihomo 路径,之后随 runtime 复用,不逐命令扫描 PATH。 */
|
|
278
|
+
private mihomoPath: string | undefined;
|
|
276
279
|
|
|
277
280
|
setup(pi: ExtensionAPI): void {
|
|
278
281
|
pi.registerFlag("no-bwrap", {
|
|
@@ -389,6 +392,12 @@ export class BwrapRuntime {
|
|
|
389
392
|
}
|
|
390
393
|
// 不经沙箱的三种情形:Windows(无 bubblewrap)、审批通过的全权限、allow-all 模式
|
|
391
394
|
const local = isWindows || needsApproval || !runtime.bwrapEnabled;
|
|
395
|
+
// mihomoPath 是 ResolvedBwrap 的 override 语义:首次 net-allowlist 执行时解析
|
|
396
|
+
// 一次存入私有字段,之后写回 resolved 直达 createNetworkStack,不逐命令扫描 PATH
|
|
397
|
+
if (runtime.network && runtime.networkAllowlist.length > 0) {
|
|
398
|
+
runtime.mihomoPath ??= this.mihomoPath ?? findMihomo();
|
|
399
|
+
this.mihomoPath = runtime.mihomoPath;
|
|
400
|
+
}
|
|
392
401
|
await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
|
|
393
402
|
const { onUpdate } = request;
|
|
394
403
|
|
|
Binary file
|