
字节笔记本
2026年10月5日 · 约 13 分钟读完
Flutter+sing-box:VPN 客户端架构拆解
把 sing-box 的核心(libbox)编译成 AAR,通过 Android VpnService 建 TUN 设备,Flutter 做 UI,三层架构跑通一个真正的 VPN 客户端。
这篇不是教程,是架构拆解:每层干什么、数据怎么流、坑在哪,一次性讲清楚。

一、整体架构:三层洋葱
Flutter UI(Dart)
│ MethodChannel
▼
Android 原生层(Kotlin)
│ JNI / AAR
▼
libbox(Go 编译的 .aar)
│
▼
VpnService(系统 TUN 设备)
│
▼
流量出口三层各司其职:
- Flutter:UI + URI 解析 + 配置生成,纯 Dart,不过原生
- Kotlin:VpnService 生命周期 + TUN 设备创建 + libbox 调用
- libbox:sing-box 核心引擎(Setup/Start/Stop/QueryStats)
为什么要分三层:Flutter 负责跨平台界面的开发效率,Kotlin 负责和 Android 系统打交道,libbox 负责真正的协议实现与流量转发。边界划清楚,每一层都能独立演进。
二、libbox 是什么
sing-box 官方专门抽出来的移动端库(experimental/libbox 目录),把 sing-box 核心封装成极简接口:
| 接口 | 作用 |
|---|---|
Setup(json) | 传入 JSON 配置,初始化 |
Start() | 启动 |
Stop() | 停止 |
QueryStats() | 查询流量统计 |
setTunFd(fd) | 设置 TUN 文件描述符 |
四个方法管一切:初始化、启停、统计、接 TUN。接口面越小,跨语言边界就越薄,能出问题的地方也越少。
三、编译 libbox AAR
前置条件
- Go 1.21+
- Android NDK r25c(别用最新版,sing-box 的 CGO 依赖对 NDK 版本敏感)
- gomobile
编译步骤
# 装 gomobile
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
# 克隆 sing-box
git clone https://github.com/sagernet/sing-box
cd sing-box
# 编译 AAR
gomobile bind -target android \
-androidapi 21 \
-o libbox.aar \
./experimental/libbox产出两个文件:
libbox.aar:编译好的库libbox-sources.jar:源码(IDE 提示用)
放进 Flutter 项目的 android/libs/ 目录。
懒人路径
sing-box 社区有人维护预编译的 libbox AAR,去 SagerNet 的 GitHub Actions 产物里直接下载,省掉编译这一步。
四、项目结构
your_app/
├── lib/ # Flutter UI(Dart)
│ ├── main.dart
│ ├── pages/
│ └── services/
│ └── vpn_channel.dart # MethodChannel 封装
│
├── android/
│ ├── app/src/main/
│ │ ├── kotlin/
│ │ │ ├── MainActivity.kt
│ │ │ ├── VpnService.kt # 核心:建 TUN 设备
│ │ │ └── BoxService.kt # 管理 libbox 生命周期
│ │ └── AndroidManifest.xml
│ └── libs/
│ ├── libbox.aar # sing-box 编译产物
│ └── libbox-sources.jar五、Kotlin 侧核心逻辑
BoxService.kt:管理 libbox 生命周期
import io.nekohasekai.libbox.Libbox
class BoxService {
fun start(configJson: String) {
Libbox.setup(configJson) // 初始化
Libbox.start() // 启动
}
fun stop() {
Libbox.stop()
}
}VpnService.kt:建 TUN 设备
class MyVpnService : VpnService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val builder = Builder()
.addAddress("172.19.0.1", 30) // 虚拟 IP
.addRoute("0.0.0.0", 0) // 全局路由
.addDnsServer("8.8.8.8")
.setMtu(9000)
val tun = builder.establish()
// 把 TUN 文件描述符传给 libbox
Libbox.setTunFd(tun.detachFd())
return START_STICKY
}
}关键在 tun.detachFd():它把文件描述符「交出」给 libbox,libbox 直接读写这个 fd 做数据转发。这是整个架构的接缝:Android 系统建 TUN,libbox 接管 TUN 的数据流。
六、Flutter 侧的 MethodChannel
class VpnChannel {
static const _channel = MethodChannel('com.yourapp/vpn');
static Future<void> start(String configJson) async {
await _channel.invokeMethod('start', {'config': configJson});
}
static Future<void> stop() async {
await _channel.invokeMethod('stop');
}
}Flutter 只管「传配置、说开始/停止」,核心逻辑全在原生层。MethodChannel 是 Flutter 和原生的唯一通道。
七、配置 JSON 从哪来
用户粘贴 vmess:// 或 vless:// URI,Flutter 的 Dart 层解析成 sing-box 格式的 JSON,直接传给 start()。
// 用户粘贴 URI
// vmess://eyJ2IjoiMiIsInBzIjoi...
//
// Dart 解析后生成 sing-box JSON:
// {
// "inbounds": [{ "type": "tun", ... }],
// "outbounds": [{ "type": "vmess", "server": "...", ... }]
// }
final json = parseVmessToSingBox(uri);
await VpnChannel.start(json);这块逻辑全在 Dart 里做,不碰原生层。URI 解析加 JSON 生成是纯字符串处理,改起来不需要重新编译原生代码。
八、数据流完整路径

用户粘贴 vmess:// URI
│
▼ Dart 层解析
sing-box JSON 配置
│ MethodChannel.invokeMethod('start', {config: json})
▼
Kotlin: BoxService.start(json)
│ Libbox.setup(json) + Libbox.start()
▼
Kotlin: MyVpnService onStartCommand
│ Builder.establish() → TUN 设备
│ Libbox.setTunFd(fd)
▼
libbox 接管 TUN fd
│ 所有 App 流量 → TUN → libbox → 加密 → 远程服务器
▼
流量出口(代理服务器)九、最大的坑
坑一:gomobile 编译失败
sing-box 依赖大量 CGO,NDK 版本不对就编译失败。
解法:用 NDK r25c,别追最新版。实在编不出来,直接用 SagerNet GitHub Actions 的预编译 AAR。
坑二:VpnService 权限
Android 的 VpnService 需要用户授权(ACTION_VPN_SETTINGS 意图),第一次启动会弹权限对话框。用户拒绝后要给出引导,不然功能形同虚设。
坑三:前台服务通知
Android 8+ 要求 VpnService 作为前台服务运行,必须挂一个持续通知,不加通知系统会直接杀掉进程。
坑四:libbox 和 TUN fd 的生命周期
TUN fd 只在 VpnService 的生命周期内有效。VpnService 被系统杀掉时 fd 随之释放,libbox 必须相应停止,两者的生命周期必须绑定。
十、延伸方向
这套架构跑通之后,还有几个自然的延伸点:
| 方向 | 说明 |
|---|---|
| 服务端 TCP 调优 | 客户端只负责把流量送出去,链路质量取决于服务端,两侧配合才是完整方案 |
| 其他 Go 项目跨端 | gomobile 是通用方案,任何合适的 Go 库都能用同样思路编译成移动端库 |
| 容器化模拟器测试 | 用 docker-android 这类方案,在容器里跑 Android 模拟器,对客户端做自动化测试 |
十一、动手路径
两条路,取决于你的起点:
路径 A:先跑通 libbox AAR 集成
- 下载预编译 AAR(省掉编译)
- 建一个最简 Flutter 项目
- 把 AAR 放进 android/libs/
- 写最简的 Kotlin VpnService
- 在 Flutter 里加一个「启动」按钮
- 跑通「点按钮、VpnService 启动、libbox 运行」
路径 B:先写 URI 解析的 Dart 代码
- 在 Dart 里写 vmess/vless URI 解析器
- 生成 sing-box 格式 JSON
- 打印 JSON 确认格式正确
- 然后再接原生层
建议路径 A 先跑通「管道」,路径 B 再加「配置生成」。管道通了(数据能从 Flutter 到 libbox 到 TUN),剩下的都是业务逻辑。
本文基于 Flutter 加 sing-box 的 VPN 客户端架构整理。核心技术:libbox(sing-box 移动端库)、gomobile、Android VpnService、Flutter MethodChannel。



