Files
sing-android/ARCHITECTURE.md
Sing Dev 1106f61cf0 Add CommandReceiver for adb control, fix tailscale routing
- 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
2026-04-02 15:31:15 +08:00

11 KiB
Raw Permalink Blame History

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.goprotocol/tun/inbound.go 各加 1 行,暴露 file_descriptor 字段给 tun inbound 配置。

为什么不用 libbox.aar

  • libbox.aar 稳定性不足API 变动频繁
  • binary 模式更可控fork+exec进程隔离崩溃不影响主 app
  • 只需 2 行 patch维护成本极低

为什么不用 tun2socks 桥接

  • v0.1 用了 tun2socksfd → 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 时不操作 fdsing-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