ByteNoteByteNote
Flutter+sing-box:VPN 客户端架构拆解
字

字节笔记本

2026年10月5日 · 约 13 分钟读完

Flutter+sing-box:VPN 客户端架构拆解

API中转
¥120

把 sing-box 的核心(libbox)编译成 AAR,通过 Android VpnService 建 TUN 设备,Flutter 做 UI,三层架构跑通一个真正的 VPN 客户端。

这篇不是教程,是架构拆解:每层干什么、数据怎么流、坑在哪,一次性讲清楚。

Flutter 加 sing-box 的 VPN 客户端三层架构:Flutter UI、Kotlin 原生层、libbox 与 VpnService TUN 设备

一、整体架构:三层洋葱

text
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

编译步骤

bash
# 装 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 产物里直接下载,省掉编译这一步。

四、项目结构

text
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 生命周期

kotlin
import io.nekohasekai.libbox.Libbox

class BoxService {
    fun start(configJson: String) {
        Libbox.setup(configJson)   // 初始化
        Libbox.start()              // 启动
    }

    fun stop() {
        Libbox.stop()
    }
}

VpnService.kt:建 TUN 设备

kotlin
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

dart
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()。

dart
// 用户粘贴 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 生成是纯字符串处理,改起来不需要重新编译原生代码。

八、数据流完整路径

sing-box VPN 客户端完整数据流:从用户粘贴 URI,到 libbox 接管 TUN 转发出站

text
用户粘贴 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 集成

  1. 下载预编译 AAR(省掉编译)
  2. 建一个最简 Flutter 项目
  3. 把 AAR 放进 android/libs/
  4. 写最简的 Kotlin VpnService
  5. 在 Flutter 里加一个「启动」按钮
  6. 跑通「点按钮、VpnService 启动、libbox 运行」

路径 B:先写 URI 解析的 Dart 代码

  1. 在 Dart 里写 vmess/vless URI 解析器
  2. 生成 sing-box 格式 JSON
  3. 打印 JSON 确认格式正确
  4. 然后再接原生层

建议路径 A 先跑通「管道」,路径 B 再加「配置生成」。管道通了(数据能从 Flutter 到 libbox 到 TUN),剩下的都是业务逻辑。


本文基于 Flutter 加 sing-box 的 VPN 客户端架构整理。核心技术:libbox(sing-box 移动端库)、gomobile、Android VpnService、Flutter MethodChannel。

相关文章

分享: