feat: 桌面端 PoC M1 — sing-box 子进程 TUN 接线 [tsk_SLCsjNgtmng3]
实现桌面端(macOS PoC)内核子进程管理,打通 sing-box TUN 模式全链路:
### client/lib/bridge/kernel_process.dart(完整实现,替换原有 stub)
- ClashApiClient: HTTP 客户端,支持 /connections / /proxies / /traffic(SSE) / /traffic(plain)
- Bearer Token 鉴权;getConnections 用于就绪探测与流量统计;getTraffic SSE 读首帧后断开
- KernelProcess 接口: 新增 statusStream / statsStream 至接口定义
- DesktopKernelProcess:
- spawn(configPath): 解析 Clash API 端口/secret → sudo sing-box run(macOS PoC)→
轮询 /connections 等待就绪(20s 超时)→ emit connecting→on
- kill(): SIGTERM + 等待 gracePeriod(5s) → SIGKILL → emit off
- 意外退出: emit error(UI 可一键重连,不崩溃)
- 统计轮询: 每秒 GET /connections,差分算 uploadSpeed/downloadSpeed
- 二进制解析: ENV > exe同目录 > macOS Bundle Resources > 开发目录 > /usr/local/bin
- 辅助函数: generateClashApiPort(高位随机)、generateClashApiSecret(32B hex)
### client/lib/bridge/desktop_vpn_bridge.dart(新文件)
- DesktopVpnBridge implements VpnBridge:
- start(configJson): injectClashApi(注入随机端口+secret)→ writeConfig(0600)→ kernel.spawn
- stop(): kernel.kill(5s)
- statusStream / statsStream: 代理 KernelProcess 事件流
- selectOutbound: Clash API PUT /proxies/{group}
- getActiveOutbound: 从 /proxies 读 now 字段
- configDirOverride: 测试注入支持
- injectClashApi: 静态方法,尊重已有 clash_api 配置,合并保留 experimental 其他字段
### client/test/bridge/kernel_process_test.dart(新文件)
- ClashApiClient 完整测试: headers / 解析 / PUT body / DELETE / 非 200 抛 HttpException
- generateClashApiPort / generateClashApiSecret 生成范围和格式测试
- DesktopVpnBridge.injectClashApi: 注入 / 尊重已有 / 保留字段 / 保留其他 experimental / 非法 JSON
- DesktopVpnBridge + FakeKernelProcess 集成: connecting→on / stop→off / statsStream / 意外退出 / spawn失败
### app/kernel/poc/(新目录)
- reality_client.config.json.tmpl: VLESS+REALITY+TUN 客户端配置模板
- TUN inbound: auto_route + strict_route(macOS kill-switch 基础保护)
- DNS: 防泄露(remote via VPN + cn 直连)
- experimental.clash_api: 随机端口 + secret 占位符
- gen-poc-config.sh: 渲染模板为可用 JSON(从环境变量读 REALITY 参数)
- README.md: 完整 PoC 接线与 M1 验收步骤
提权说明(macOS PoC):
· sudo 提权(开发机需配 /etc/sudoers.d/pangolin-singbox 或有 sudo 缓存)
· 正式版: SMJobBless Helper + 公证(BACKLOG-11D-HELPER)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# Pangolin 桌面端 PoC M1 — 接线指南
|
||||
|
||||
**任务**: tsk_SLCsjNgtmng3 · M1 首发打通(macOS TUN + REALITY)
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 1. 下载 sing-box 二进制
|
||||
|
||||
```bash
|
||||
# macOS Apple Silicon
|
||||
cd app/kernel
|
||||
./fetch-desktop-bin.sh darwin arm64
|
||||
|
||||
# macOS Intel
|
||||
./fetch-desktop-bin.sh darwin amd64
|
||||
```
|
||||
|
||||
产物: `app/kernel/dist/desktop/darwin-arm64/sing-box`
|
||||
|
||||
### 2. 配置 TUN 提权(macOS PoC)
|
||||
|
||||
sing-box TUN 模式需要创建 `utun` 接口,必须有 root 权限。PoC 阶段使用 `sudo`:
|
||||
|
||||
```bash
|
||||
# 方式 A: sudoers 免密白名单(推荐,避免每次输密码)
|
||||
SINGBOX_PATH="$(pwd)/app/kernel/dist/desktop/darwin-arm64/sing-box"
|
||||
echo "$(whoami) ALL=(root) NOPASSWD: ${SINGBOX_PATH}" | sudo tee /etc/sudoers.d/pangolin-singbox
|
||||
sudo chmod 440 /etc/sudoers.d/pangolin-singbox
|
||||
|
||||
# 方式 B: 用 setuid(不推荐用于正式版)
|
||||
sudo chown root "${SINGBOX_PATH}"
|
||||
sudo chmod u+s "${SINGBOX_PATH}"
|
||||
```
|
||||
|
||||
> **正式版说明**: macOS 正式版应使用 SMJobBless 注册特权 Helper Daemon,
|
||||
> 并完成 Apple 公证 (notarization)。此 PoC 路径记录在 BACKLOG-11D-HELPER。
|
||||
|
||||
### 3. 获取 REALITY 服务端参数
|
||||
|
||||
从部署节点的 EC2 拿 REALITY 公钥和 UUID:
|
||||
|
||||
```bash
|
||||
# EC2 上 Xray REALITY 密钥对(deploy/xray/secrets/ 或 Bitwarden)
|
||||
ssh ec2 "cat ~/pangolin/xray/secrets/reality_keys.json 2>/dev/null || echo '不存在,需手动生成'"
|
||||
|
||||
# 若未生成,在 EC2 上运行:
|
||||
# docker run --rm ghcr.io/xtls/xray-core x25519 | tee /tmp/reality_keys.txt
|
||||
```
|
||||
|
||||
### 4. 渲染 PoC 配置
|
||||
|
||||
```bash
|
||||
export SERVER_HOST="18.136.60.128" # EC2 公网 IP
|
||||
export SERVER_PORT="11443" # Xray VLESS 端口
|
||||
export REALITY_UUID="your-uuid-here"
|
||||
export REALITY_PUBLIC_KEY="your-x25519-public-key"
|
||||
export REALITY_SHORT_ID="your-short-id"
|
||||
export REALITY_SNI="www.apple.com"
|
||||
|
||||
./app/kernel/poc/gen-poc-config.sh --out /tmp/pangolin-poc-config.json
|
||||
```
|
||||
|
||||
## 验收测试
|
||||
|
||||
### M1 验收步骤
|
||||
|
||||
```bash
|
||||
# 1. 设置二进制路径
|
||||
export PANGOLIN_SINGBOX_BIN="$(pwd)/app/kernel/dist/desktop/darwin-arm64/sing-box"
|
||||
|
||||
# 2. 用渲染后的 config 启动(PoC 会自动 sudo)
|
||||
sudo "${PANGOLIN_SINGBOX_BIN}" run -c /tmp/pangolin-poc-config.json &
|
||||
KERNEL_PID=$!
|
||||
|
||||
# 3. 等待就绪(Clash API)
|
||||
sleep 3
|
||||
|
||||
# 4. 验证出口 IP
|
||||
curl -s https://ifconfig.me # 应显示节点 IP(18.136.60.128 或接近)
|
||||
|
||||
# 5. DNS 泄露检测
|
||||
dig @8.8.8.8 google.com # 通过 VPN 解析
|
||||
dig google.com # 应走 sing-box DNS(不泄露本地 ISP)
|
||||
|
||||
# 6. 流量统计(Clash API)
|
||||
CLASH_PORT="$(cat /tmp/pangolin-poc-config.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['experimental']['clash_api']['external_controller'].split(':')[1])")"
|
||||
CLASH_SECRET="$(cat /tmp/pangolin-poc-config.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['experimental']['clash_api']['secret'])")"
|
||||
|
||||
curl -s -H "Authorization: Bearer ${CLASH_SECRET}" "http://127.0.0.1:${CLASH_PORT}/connections"
|
||||
|
||||
# 7. 停止(还原路由/DNS)
|
||||
kill "${KERNEL_PID}"
|
||||
sleep 2
|
||||
|
||||
# 8. 验证还原
|
||||
netstat -rn | grep utun # 不应出现 pangolin TUN 条目
|
||||
scutil --dns | head -20 # DNS 应回到系统默认
|
||||
```
|
||||
|
||||
### Dart 集成测试
|
||||
|
||||
```bash
|
||||
cd client
|
||||
flutter test test/bridge/kernel_process_test.dart -v
|
||||
```
|
||||
|
||||
## 架构说明
|
||||
|
||||
```
|
||||
DesktopVpnBridge.start(configJson)
|
||||
│
|
||||
├─ _injectClashApi() 注入随机端口+secret 到 experimental.clash_api
|
||||
├─ _writeConfig() 写 ~/Library/Application Support/com.pangolin.vpn/kernel/
|
||||
│
|
||||
└─ DesktopKernelProcess.spawn(configPath)
|
||||
│
|
||||
├─ _resolveBinaryPath() PANGOLIN_SINGBOX_BIN / 开发目录 / /usr/local/bin
|
||||
├─ Process.start(['sudo', binPath, 'run', '-c', configPath])
|
||||
├─ _waitForClashApi() 轮询 GET /connections 直到就绪(20s 超时)
|
||||
├─ _startStatsPoll() 每秒轮询 /connections → VpnStatsEvent
|
||||
└─ emit VpnStatus.on
|
||||
```
|
||||
|
||||
## 已知限制(PoC 阶段)
|
||||
|
||||
| 限制 | 原因 | 正式版方案 |
|
||||
|------|------|------------|
|
||||
| sudo 提权 | TUN 需 root | SMJobBless Helper + 公证 (BACKLOG-11D-HELPER) |
|
||||
| 单 REALITY outbound | 节点选优未实现 | URLTest 多节点 (11G) |
|
||||
| 统计用 /connections 轮询 | SSE 流实现更复杂 | /traffic SSE 订阅 |
|
||||
| Windows Wintun 未测 | 主验收平台 macOS | M1 验收后补 |
|
||||
| 无 kill-switch | strict_route 做基础保护 | 11G |
|
||||
| Android/iOS | 归 11E/11F | NEPacket/VpnService |
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# gen-poc-config.sh — 渲染 REALITY 客户端 PoC 配置(tsk_SLCsjNgtmng3)
|
||||
#
|
||||
# 用法: ./gen-poc-config.sh [--out <output.json>]
|
||||
#
|
||||
# 从环境变量读取参数(须全部提供,无 --干跑 模式):
|
||||
# SERVER_HOST EC2 IP(如 18.136.60.128)
|
||||
# SERVER_PORT Xray VLESS 监听端口(如 11443)
|
||||
# REALITY_UUID 客户端 UUID
|
||||
# REALITY_PUBLIC_KEY REALITY 公钥
|
||||
# REALITY_SHORT_ID REALITY shortId
|
||||
# REALITY_SNI TLS SNI(如 www.apple.com)
|
||||
#
|
||||
# 可选:
|
||||
# CLASH_API_PORT Clash API 端口(默认随机 49152-65535)
|
||||
# CLASH_API_SECRET Clash API secret(默认随机 32 字节 hex)
|
||||
#
|
||||
# 产物默认写到 /tmp/pangolin-poc-config.json(权限 0600)。
|
||||
#
|
||||
# 注意:REALITY 私钥等参数绝不入库,仅在运行时从 EC2 secrets/ 或 Bitwarden 取。
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TMPL="${SCRIPT_DIR}/reality_client.config.json.tmpl"
|
||||
OUT="${1:---out}"
|
||||
if [[ "${OUT}" == "--out" ]]; then
|
||||
OUT="${2:-/tmp/pangolin-poc-config.json}"
|
||||
shift 2 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── 参数检查 ──────────────────────────────────────────────────────
|
||||
|
||||
_require() {
|
||||
local name="$1"
|
||||
if [[ -z "${!name:-}" ]]; then
|
||||
printf '✗ 环境变量 %s 未设置\n' "$name" >&2
|
||||
printf ' 示例: export %s=<value>\n' "$name" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
_require SERVER_HOST
|
||||
_require SERVER_PORT
|
||||
_require REALITY_UUID
|
||||
_require REALITY_PUBLIC_KEY
|
||||
_require REALITY_SHORT_ID
|
||||
|
||||
REALITY_SNI="${REALITY_SNI:-www.apple.com}"
|
||||
|
||||
# 生成随机 Clash API 端口和 secret(若未指定)
|
||||
if [[ -z "${CLASH_API_PORT:-}" ]]; then
|
||||
CLASH_API_PORT="$(python3 -c 'import random; print(random.randint(49152,65535))')"
|
||||
fi
|
||||
if [[ -z "${CLASH_API_SECRET:-}" ]]; then
|
||||
CLASH_API_SECRET="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
|
||||
fi
|
||||
|
||||
# ── 渲染模板 ──────────────────────────────────────────────────────
|
||||
|
||||
printf '==> 渲染配置 → %s\n' "$OUT"
|
||||
|
||||
sed \
|
||||
-e "s|__SERVER_HOST__|${SERVER_HOST}|g" \
|
||||
-e "s|__SERVER_PORT__|${SERVER_PORT}|g" \
|
||||
-e "s|__UUID__|${REALITY_UUID}|g" \
|
||||
-e "s|__REALITY_PUBLIC_KEY__|${REALITY_PUBLIC_KEY}|g" \
|
||||
-e "s|__REALITY_SHORT_ID__|${REALITY_SHORT_ID}|g" \
|
||||
-e "s|__REALITY_SNI__|${REALITY_SNI}|g" \
|
||||
-e "s|__CLASH_API_PORT__|${CLASH_API_PORT}|g" \
|
||||
-e "s|__CLASH_API_SECRET__|${CLASH_API_SECRET}|g" \
|
||||
"${TMPL}" > "${OUT}"
|
||||
|
||||
chmod 600 "${OUT}"
|
||||
|
||||
printf '✓ 已写入: %s\n' "$OUT"
|
||||
printf ' Clash API: http://127.0.0.1:%s (secret len=%d)\n' \
|
||||
"$CLASH_API_PORT" "${#CLASH_API_SECRET}"
|
||||
printf ' Server: %s:%s\n' "$SERVER_HOST" "$SERVER_PORT"
|
||||
printf '\n提示: 使用此配置测试前,请确认 sing-box 有 TUN 权限:\n'
|
||||
printf ' sudo sh -c "echo \"%%(whoami)s ALL=(root) NOPASSWD: /path/to/sing-box\" >> /etc/sudoers.d/pangolin"\n'
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"_comment": "sing-box 客户端配置模板 — VLESS+REALITY+TUN(PoC tsk_SLCsjNgtmng3)",
|
||||
"_usage": "渲染脚本: app/kernel/poc/gen-poc-config.sh;占位符一律 __UPPER_SNAKE__",
|
||||
|
||||
"log": {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "dns-remote",
|
||||
"address": "tls://1.1.1.1",
|
||||
"address_resolver": "dns-local",
|
||||
"detour": "reality-out"
|
||||
},
|
||||
{
|
||||
"tag": "dns-local",
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
},
|
||||
{
|
||||
"tag": "dns-block",
|
||||
"address": "rcode://success"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{ "outbound": "any", "server": "dns-local" },
|
||||
{ "geosite": "cn", "server": "dns-local" },
|
||||
{ "geoip": "private", "server": "dns-local" }
|
||||
],
|
||||
"final": "dns-remote",
|
||||
"independent_cache": true
|
||||
},
|
||||
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"tag": "tun-in",
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"inet6_address": "fdfe:dcba:9876::1/126",
|
||||
"mtu": 1492,
|
||||
"auto_route": true,
|
||||
"strict_route": true,
|
||||
"stack": "system",
|
||||
"sniff": true,
|
||||
"sniff_override_destination": false
|
||||
}
|
||||
],
|
||||
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "reality-out",
|
||||
"server": "__SERVER_HOST__",
|
||||
"server_port": __SERVER_PORT__,
|
||||
"uuid": "__UUID__",
|
||||
"flow": "xtls-rprx-vision",
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": "__REALITY_SNI__",
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": "chrome"
|
||||
},
|
||||
"reality": {
|
||||
"enabled": true,
|
||||
"public_key": "__REALITY_PUBLIC_KEY__",
|
||||
"short_id": "__REALITY_SHORT_ID__"
|
||||
}
|
||||
},
|
||||
"packet_encoding": "xudp"
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
|
||||
"route": {
|
||||
"rules": [
|
||||
{ "protocol": "dns", "outbound": "dns-out" },
|
||||
{ "geosite": "cn", "outbound": "direct" },
|
||||
{ "geoip": "cn", "outbound": "direct" },
|
||||
{ "geoip": "private", "outbound": "direct" }
|
||||
],
|
||||
"final": "reality-out",
|
||||
"auto_detect_interface": true
|
||||
},
|
||||
|
||||
"experimental": {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:__CLASH_API_PORT__",
|
||||
"secret": "__CLASH_API_SECRET__"
|
||||
},
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"path": "/tmp/pangolin-poc.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user