- CommandReceiver: exported broadcast receiver for adb CLI control - start/stop/status/set_config commands - Usage: adb shell am broadcast -a com.sing.vpn.CMD -n com.sing.vpn/.CommandReceiver --es cmd start - SingConfig: add controlplane.tailscale.com → direct route rule to prevent tailnet control traffic from going through proxy
11 KiB
11 KiB
Sing VPN — 架构文档
概述
Sing 是一个 Android VPN 客户端,基于 sing-box 引擎,支持 SOCKS5、Shadowsocks、VMess、VLESS(含 Reality + uTLS)协议。
核心架构
┌─────────────────────────────────────────────────────┐
│ Android App (Kotlin) │
│ │
│ ┌──────────┐ ┌───────┐ ┌────────┐ │
│ │ Overview │ │ Nodes │ │ Config │ (Compose) │
│ │ Screen │ │Screen │ │ Screen │ │
│ └────┬─────┘ └───┬───┘ └───┬────┘ │
│ │ │ │ │
│ ┌────┴────────────┴──────────┴──────────────────┐ │
│ │ MainActivity │ │
│ │ (ComponentActivity + Compose Nav) │ │
│ └────────────────────┬──────────────────────────┘ │
│ │ │
│ ┌────────────────────┴──────────────────────────┐ │
│ │ SingVpnService │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ VpnService.Builder.establish() │ │ │
│ │ │ → TUN fd (owned by Java, never closed │ │ │
│ │ │ until stopVpn) │ │ │
│ │ └──────────────┬───────────────────────────┘ │ │
│ │ │ fork + exec (keepFd=tunFd) │ │
│ └─────────────────┼──────────────────────────────┘ │
│ │ │
├────────────────────┼──────────────────────────────────┤
│ Child Process ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ sing-box (libsingbox.so) │ │
│ │ │ │
│ │ tun inbound ──→ file_descriptor: N │ │
│ │ │ (reads TUN fd directly) │ │
│ │ ▼ │ │
│ │ route rules ──→ sniff → hijack-dns → proxy │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ outbound ────→ socks/ss/vmess/vless │ │
│ │ │ → upstream server │ │
│ │ ▼ │ │
│ │ dns ─────────→ remote (DoT) / local (UDP) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ sing-box helper (libsingbox.so) │ │
│ │ │ │
│ │ mixed inbound :10802 │ │
│ │ clash_api :9090 ←── Nodes tab 查询延迟 │ │
│ │ urltest group ───→ 自动测速所有节点 │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
进程模型
| 进程 | 生命周期 | 用途 |
|---|---|---|
| App 主进程 | Activity 存活期间 | UI、VPN Service、fd 管理 |
| sing-box active | VPN 连接期间 | 实际流量代理 |
| sing-box helper | App 启动后常驻 | 节点测速(Clash API) |
关键设计决策
1. file_descriptor 模式(而非 libbox.aar)
sing-box 源码 patch:在 option/tun.go 和 protocol/tun/inbound.go 各加 1 行,暴露 file_descriptor 字段给 tun inbound 配置。
为什么不用 libbox.aar:
- libbox.aar 稳定性不足,API 变动频繁
- binary 模式更可控:fork+exec,进程隔离,崩溃不影响主 app
- 只需 2 行 patch,维护成本极低
为什么不用 tun2socks 桥接:
- v0.1 用了 tun2socks(fd → SOCKS5 → sing-box mixed inbound)
- 多一层转发,多一个进程,多一个故障点
- file_descriptor 让 sing-box 直接读 TUN,单进程,启动 0.17s vs 0.88s
2. 节点切换:SIGKILL + restart
为什么不用 SIGHUP 热重载:
sing-box 支持 SIGHUP 重载配置,但与 file_descriptor 模式不兼容:
| 方案 | 失败原因 |
|---|---|
| SIGHUP 原版 | sing-tun Close() 关闭 fd,新实例拿到已失效 fd |
| SIGHUP + patch Close() 跳过关闭 | Go os.File GC finalizer 仍会关闭 fd |
| SIGHUP + sing-tun dup | 可行但需维护 fork,侵入性大 |
| SIGHUP + Java dup | dup 的 fd 在父进程,子进程访问不到 |
SIGKILL + restart 方案:
1. SIGKILL(9) 旧 sing-box → 内核直接杀死,不执行 Close(),fd 不被关
2. fork+exec 新 sing-box → 继承同一个 TUN fd
3. 间隔 ~5ms,内核 TUN 队列缓冲期间的数据包
核心原理:TUN fd 由 Java ParcelFileDescriptor 持有,子进程被杀不影响父进程的 fd。
3. Helper 进程 + Clash API 测速
App 启动 → NodeTester.start() → fork sing-box helper
config: mixed inbound :10802 + clash_api :9090 + urltest group
Nodes tab 可见 → GET http://127.0.0.1:9090/proxies → 读取 delay
颜色: <300ms 绿 / 300-600ms 橙 / >600ms 或 timeout 红
不依赖 VPN 连接,app 启动即可测速。
4. DNS 分流
{
"dns": {
"servers": [
{"tag": "remote", "type": "tls", "server": "8.8.8.8", "detour": "proxy"},
{"tag": "local", "type": "udp", "server": "223.5.5.5"}
],
"rules": [
{"domain_suffix": [".cn"], "server": "local"},
{"domain_keyword": ["baidu","bilibili",...], "server": "local"}
],
"final": "remote"
}
}
- 国内域名 → local DNS (223.5.5.5, UDP 直连)
- 国外域名 → remote DNS (8.8.8.8, DoT 走代理)
- route rules:
{"action": "sniff"}→{"protocol": "dns", "action": "hijack-dns"}拦截 DNS
5. fork_exec.c 安全守卫
// keepFd=-1 时不操作 fd(sing-box helper 不需要 TUN fd)
if (keepFd >= 0)
fcntl(keepFd, F_SETFD, 0); // 清除 CLOEXEC
// 关闭所有 fd 3+ 除了 keepFd(防止 fd 泄漏到子进程)
while ((entry = readdir(d)) != NULL) {
int fd = atoi(entry->d_name);
if (fd > 2 && fd != keepFd && fd != dirFd)
close(fd);
}
文件结构
android/app/src/main/
├── java/com/sing/vpn/
│ ├── MainActivity.kt # Compose entry, Navigation, VPN 权限
│ ├── ui/
│ │ ├── theme/
│ │ │ ├── Color.kt # Linear 风格深色色板
│ │ │ ├── Type.kt # 字体定义 (Sans + Mono)
│ │ │ └── Theme.kt # SingTheme composable
│ │ ├── components/
│ │ │ ├── Components.kt # SectionHeader, KeyValueRow, ToggleRow, etc.
│ │ │ ├── NodeRow.kt # NodeRow, PeerRow
│ │ │ └── ConnectionRow.kt # ConnectionRow, TrafficCard
│ │ └── screens/
│ │ ├── OverviewScreen.kt # 状态面板 + 流量 + 最近连接
│ │ ├── NodesScreen.kt # 节点列表 + 测速 + 导入
│ │ ├── ConfigScreen.kt # 分组配置
│ │ ├── ConnectionsScreen.kt # 活跃连接列表
│ │ ├── LogsScreen.kt # 实时日志
│ │ └── EditRulesScreen.kt # 自定义规则编辑
│ ├── SingVpnService.kt # VPN 生命周期, TUN fd, 进程管理
│ ├── SingConfig.kt # 动态生成 sing-box JSON 配置
│ ├── Node.kt # 节点数据类 (含 Reality 字段)
│ ├── NodeStore.kt # SharedPreferences 存储
│ ├── NodeParser.kt # URI/JSON/订阅 解析
│ ├── NodeTester.kt # Helper 进程 + Clash API 测速
│ ├── MiniSing.kt # JNI bridge (Go c-shared)
│ ├── ForkExec.kt # JNI wrapper (fork+exec)
│ ├── BootReceiver.kt # 开机自动连接
│ └── Util.kt # formatBytes 等工具
├── cpp/fork_exec.c # fork+exec, fd 管理, 信号发送
├── jniLibs/arm64-v8a/
│ ├── libsingbox.so # sing-box binary (patched, ~36MB)
│ ├── libminising.so # mini-sing Go c-shared library
│ └── libforkexec.so # JNI native lib
└── res/ # Splash theme, strings
编译
# Android APK
cd /home/ubuntu/sing/android && ./gradlew assembleDebug
# sing-box (如需重新编译)
cd /home/ubuntu/sing/sing-box
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
CC=/opt/android-ndk-r27/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang \
go build -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api" \
-trimpath -ldflags="-s -w" \
-o ../android/app/src/main/jniLibs/arm64-v8a/libsingbox.so ./cmd/sing-box
sing-box 源码 patch
sing-box (option/tun.go):
// +1 行: 添加 FileDescriptor 字段
FileDescriptor int `json:"file_descriptor,omitempty"`
sing-box (protocol/tun/inbound.go):
// +1 行: 传递到 tun.Options
FileDescriptor: options.FileDescriptor,
sing-box (go.mod):
// sing-tun 使用本地 clone (无修改)
replace github.com/sagernet/sing-tun => ../sing-tun
版本历史
| 版本 | 架构 | 关键变更 |
|---|---|---|
| v0.1 | TUN → tun2socks → SOCKS5 直连 | 单页面, 单节点 |
| v0.2 | TUN → tun2socks → sing-box mixed → outbound | 4 Tab UI, 多节点多协议 |
| v0.2+ | TUN → sing-box tun (fd://) → outbound | 去掉 tun2socks, 单进程 |
| v0.5 | + Jetpack Compose UI, Linear 深色主题 | 3 Tab Compose, 去掉 Fragment + XML |
| v0.3 | + helper 进程 + Clash API 测速 + SIGKILL 切换 | DNS 分流, 节点测速, Reality |