feat(android): libbox 隧道重写对接真实 1.13.12 API,APK 编译通过(Task 5)

原生层照 PoC 旧/想象 API 写,与真实 sing-box 1.13.12 libbox 差距是架构级:

- 包名 libbox(非 go.libbox);无 BoxService,改 Libbox.setup + CommandServer 模型

- PangolinVpnService 重写:实现完整 PlatformInterface(15法)+CommandServerHandler

- 新增 DefaultNetworkMonitor(上报默认接口,缺则报 no available network)

- openTun 适配真实 TunOptions(RoutePrefixIterator/getMTU/StringBox DNS)

- 统计/切节点经 CommandClient(uplinkTotal/selectOutbound);MainActivity 接线

构建环境矩阵打通:Gradle 8.7(Java21)+声明式插件迁移+AGP 8.6+Kotlin 2.2.0+占位图标

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-22 19:28:24 +08:00
parent 2a8426c357
commit 126c06833d
10 changed files with 512 additions and 381 deletions
+8 -28
View File
@@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@@ -21,10 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
namespace "com.pangolin.pangolin_vpn"
compileSdkVersion flutter.compileSdkVersion
@@ -57,17 +54,6 @@ android {
signingConfig signingConfigs.debug
}
}
// libbox.aar 包含 arm64-v8a / armeabi-v7a / x86_64 三个 ABI
// 若只需调试,可缩减 abiFilters 以加快构建速度。
// splits {
// abi {
// enable true
// reset()
// include 'arm64-v8a', 'x86_64'
// universalApk true
// }
// }
}
flutter {
@@ -75,21 +61,15 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// ── sing-box libboxgomobile AAR)──────────────────────────────
// 产物由 app/kernel/build-android.sh 生成:
// cd app/kernel && ./build-android.sh
// 产物路径(相对于本 build.gradle):../../../app/kernel/dist/android/libbox.aar
//
// 若文件不存在,构建会报错:请先运行 build-android.sh 生成产物。
// 路径说明:client/android/app → ../../.. → repo root → app/kernel/dist/android/
// 路径:client/android/app → ../../.. → repo root → app/kernel/dist/android/
// Kotlin stdlib 由 kotlin-android 插件自动引入,无需显式声明。
def libboxAar = file("${projectDir}/../../../app/kernel/dist/android/libbox.aar")
if (libboxAar.exists()) {
implementation files(libboxAar)
} else {
// AAR 尚未构建 — 保留占位;IDE 会报红线,但不影响非 libbox 代码编辑。
// 运行 `app/kernel/build-android.sh` 后重新 sync 即可。
logger.warn("⚠ libbox.aar not found at ${libboxAar.absolutePath}")
logger.warn(" Run: cd app/kernel && ./build-android.sh")
}
@@ -23,7 +23,7 @@
<application
android:label="穿山甲"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@drawable/ic_launcher">
<!-- 主 Activity -->
<activity
@@ -0,0 +1,124 @@
package com.pangolin.pangolin_vpn
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.util.Log
import libbox.InterfaceUpdateListener
import java.net.NetworkInterface
/**
* DefaultNetworkMonitor — 上报「底层默认网络」给 sing-box 内核。
*
* sing-box 在 Android 上不自己枚举接口(PlatformInterface.usePlatformAutoDetectInterfaceControl=true),
* 它通过 [InterfaceUpdateListener.updateDefaultInterface] 得知当前默认出口接口(名字+index),
* 据此把出站 socket 绑定到底层物理网络(避免 TUN 路由环路)。
*
* **缺这个监听 → 内核启动期拿不到默认接口 → 报 "no available network interface",隧道连不上。**
* 故 [start] 必须在 CommandServer.startOrReloadService 之前调用,并把首个默认网络同步上报。
*
* 自包含实现:直接用 ConnectivityManager.registerNetworkCallbackAPI 21+),
* 不依赖 SFA 的 Application/协程框架。
*/
object DefaultNetworkMonitor {
private const val TAG = "PangolinNetMonitor"
@Volatile private var listener: InterfaceUpdateListener? = null
@Volatile private var defaultNetwork: Network? = null
private var connectivity: ConnectivityManager? = null
private var callback: ConnectivityManager.NetworkCallback? = null
/** 注册默认网络回调,并立即同步一次当前网络。幂等。 */
fun start(context: Context) {
if (callback != null) return
val cm = context.applicationContext
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivity = cm
val cb = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
defaultNetwork = network
notifyUpdate(network)
}
override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) {
defaultNetwork = network
notifyUpdate(network)
}
override fun onLost(network: Network) {
if (defaultNetwork == network) {
defaultNetwork = null
notifyUpdate(null)
}
}
}
callback = cb
// 要求一个具备 INTERNET 能力的网络(等价于默认网络)。registerNetworkCallback 支持 API 21+。
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
try {
cm.registerNetworkCallback(request, cb)
} catch (e: Exception) {
Log.e(TAG, "registerNetworkCallback failed: $e")
}
// 同步当前活动网络(回调可能不会立刻触发 onAvailable)。
val active = cm.activeNetwork
if (active != null) {
defaultNetwork = active
notifyUpdate(active)
}
}
fun stop() {
val cm = connectivity
val cb = callback
if (cm != null && cb != null) {
try { cm.unregisterNetworkCallback(cb) } catch (_: Exception) {}
}
callback = null
connectivity = null
defaultNetwork = null
}
/** 内核注册/注销监听(PlatformInterface.start/closeDefaultInterfaceMonitor 调用)。 */
fun setListener(l: InterfaceUpdateListener?) {
listener = l
if (l != null) notifyUpdate(defaultNetwork)
}
/** 把网络的接口名 + index 上报内核;网络为 null 时上报空("",-1)。 */
private fun notifyUpdate(network: Network?) {
val l = listener ?: return
if (network == null) {
l.updateDefaultInterface("", -1, false, false)
return
}
val cm = connectivity ?: return
// LinkProperties 可能短暂为 null,重试几次拿接口名。
for (i in 0 until 10) {
val lp = cm.getLinkProperties(network)
val name = lp?.interfaceName
if (name == null) {
try { Thread.sleep(100) } catch (_: InterruptedException) {}
continue
}
val index = try {
NetworkInterface.getByName(name)?.index ?: -1
} catch (_: Exception) {
-1
}
l.updateDefaultInterface(name, index, false, false)
return
}
Log.w(TAG, "notifyUpdate: interface name unavailable after retries")
}
}
@@ -68,14 +68,14 @@ class MainActivity : FlutterActivity() {
}
"selectOutbound" -> {
val tag = call.arguments as? String ?: "auto"
Log.d(TAG, "selectOutbound: tag=$tag (stub — 11G)")
// TODO(11G): 通过 libbox CommandClient 切换出口
PangolinVpnService.instance?.selectOutbound(tag)
Log.d(TAG, "selectOutbound: tag=$tag")
result.success(null)
}
"getActiveOutbound" -> {
Log.d(TAG, "getActiveOutbound (stub — 11G)")
// TODO(11G): 从 libbox CommandClient 查询当前出口 tag
result.success("auto")
val active = PangolinVpnService.instance?.currentActiveOutbound() ?: "auto"
Log.d(TAG, "getActiveOutbound -> $active")
result.success(active)
}
"setKillSwitch" -> {
val on = call.arguments as? Boolean ?: false
@@ -6,57 +6,69 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.TrafficStats
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.OsConstants
import android.util.Log
import go.libbox.BoxService
import go.libbox.CommandClient
import go.libbox.CommandClientHandler
import go.libbox.CommandClientOptions
import go.libbox.Libbox
import go.libbox.OutboundGroupIterator
import go.libbox.PlatformInterface
import go.libbox.ProcessInfo
import go.libbox.StatusMessage
import go.libbox.StringIterator
import go.libbox.TunOptions
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import go.Seq
import libbox.CommandClient
import libbox.CommandClientHandler
import libbox.CommandClientOptions
import libbox.CommandServer
import libbox.CommandServerHandler
import libbox.ConnectionEvents
import libbox.ConnectionOwner
import libbox.InterfaceUpdateListener
import libbox.Libbox
import libbox.LocalDNSTransport
import libbox.LogIterator
import libbox.NetworkInterfaceIterator
import libbox.Notification as LibboxNotification
import libbox.OutboundGroupIterator
import libbox.OverrideOptions
import libbox.PlatformInterface
import libbox.SetupOptions
import libbox.StatusMessage
import libbox.StringIterator
import libbox.SystemProxyStatus
import libbox.TunOptions
import libbox.WIFIState
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
/**
* PangolinVpnService — VPN 前台服务,封装 sing-box libbox 隧道。
*
* ## 生命周期
* START → handleStart() → 后台线程 → startLibbox() → [BoxService.start()]
* → VpnEventBus.postStatus("on")
* STOP → doStop() → [BoxService.close()] → 关 TUN fd → postStatus("off")
* REVOKE → onRevoke() → postStatus("error") → doStop()
* ## 架构(sing-box 1.13.x libboxCommandServer 模型)
* Libbox.setup(SetupOptions) —— 进程级一次,设可写目录 + 命令 socket
* CommandServer(handler, platformInterface).start()
* commandServer.startOrReloadService(configJson, OverrideOptions()) —— 起内核,回调 openTun
* 停止:closeService() → close()
*
* ## 通信
* Dart 通过 [MainActivity] MethodChannel 发送 start/stop intent
* 状态与统计经 [VpnEventBus] 推回 Flutter EventChannel。
* 本类同时实现 [PlatformInterface]openTun/protect/接口监听…)与 [CommandServerHandler]。
* 统计与切节点经独立的 [CommandClient](同进程连内核命令 socket)
*
* ## 注意:libbox.aar API
* 依赖 app/kernel/dist/android/libbox.aar(由 build-android.sh 产出)。
* 方法签名注释标注 "// libbox API" 处若编译失败,请对照实际 libbox.aar 中
* go.libbox.* 的 javadoc 微调参数名;整体架构不变。
* ## 与 Dart 通信
* 状态/统计经 [VpnEventBus] 推回 Flutter EventChannel(契约见 lib/bridge/vpn_bridge.dart)。
* selectOutbound/getActiveOutbound 由 [MainActivity] 通过 [instance] 调用。
*/
class PangolinVpnService : VpnService() {
class PangolinVpnService : VpnService(), PlatformInterface, CommandServerHandler {
companion object {
private const val TAG = "PangolinVpnService"
const val ACTION_START = "com.pangolin.vpn.START"
const val ACTION_STOP = "com.pangolin.vpn.STOP"
const val ACTION_STOP = "com.pangolin.vpn.STOP"
const val EXTRA_CONFIG = "config_json"
private const val NOTIFICATION_CHANNEL_ID = "pangolin_vpn_channel"
private const val NOTIFICATION_ID = 1001
/** 同进程内供 MainActivity 调用 selectOutbound/getActiveOutbound。 */
@Volatile var instance: PangolinVpnService? = null
private set
fun startVpn(context: Context, configJson: String) {
val intent = Intent(context, PangolinVpnService::class.java).apply {
action = ACTION_START
@@ -67,7 +79,6 @@ class PangolinVpnService : VpnService() {
} else {
context.startService(intent)
}
Log.d(TAG, "startVpn: intent dispatched, configLen=${configJson.length}")
}
fun stopVpn(context: Context) {
@@ -75,37 +86,35 @@ class PangolinVpnService : VpnService() {
action = ACTION_STOP
}
context.startService(intent)
Log.d(TAG, "stopVpn: intent dispatched")
}
}
// ── 内核与 TUN 状态 ───────────────────────────────────────────
@Volatile private var commandServer: CommandServer? = null
@Volatile private var statsClient: CommandClient? = null
@Volatile private var tunPfd: ParcelFileDescriptor? = null
/** libbox BoxService 内核实例(主线程以外创建,通过 volatile 保证可见性) */
@Volatile private var boxService: BoxService? = null
/** 最近一次 writeGroups 缓存的「代理组当前出口」与 urltest 延迟,供 getActiveOutbound/stats 用。 */
@Volatile private var activeOutbound: String = "auto"
@Volatile private var latestUrltest: List<Pair<String, Int>> = emptyList()
/** TUN 文件描述符持有者(VpnService.Builder.establish() 返回值) */
@Volatile var currentTunPfd: ParcelFileDescriptor? = null
/** 缓存的「可手动选择的出口组」tag(selector 组)。selectOutbound 据此切换。 */
@Volatile private var selectableGroup: String = ""
/** libbox CommandClient(统计与状态轮询) */
@Volatile private var commandClient: CommandClient? = null
/** 兜底统计定时器(CommandClient 不可用时启用 TrafficStats 方案) */
private var statsExecutor: ScheduledExecutorService? = null
/** 防止 doStop 重入 */
private val stopping = AtomicBoolean(false)
private var libboxSetupDone = false
// ── 生命周期 ────────────────────────────────────────────────
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate")
instance = this
// gomobile 绑定需要 Android Context(否则部分调用崩溃)。
Seq.setContext(applicationContext)
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "onStartCommand: action=${intent?.action}")
return when (intent?.action) {
ACTION_START -> {
val configJson = intent.getStringExtra(EXTRA_CONFIG) ?: "{}"
@@ -116,23 +125,19 @@ class PangolinVpnService : VpnService() {
doStop()
START_NOT_STICKY
}
else -> {
Log.w(TAG, "onStartCommand: unknown action, falling through")
START_NOT_STICKY
}
else -> START_NOT_STICKY
}
}
override fun onRevoke() {
// 系统撤销 VPN 权限(如用户在设置里关闭)→ 推 error 再清理
Log.w(TAG, "onRevoke: permission revoked by system")
VpnEventBus.postStatus("error")
doStop()
}
override fun onDestroy() {
Log.i(TAG, "onDestroy")
doStop()
instance = null
super.onDestroy()
}
@@ -143,12 +148,12 @@ class PangolinVpnService : VpnService() {
startForeground(NOTIFICATION_ID, buildNotification("正在连接…"))
VpnEventBus.postStatus("connecting")
// libbox 内核启动必须在后台线程(不能阻塞 onStartCommand
// 内核启动必须在后台线程(不能阻塞 onStartCommandopenTun 回调会同步等 TUN 建好)。
Thread {
try {
startLibbox(configJson)
startKernel(configJson)
} catch (e: Exception) {
Log.e(TAG, "startLibbox failed: $e", e)
Log.e(TAG, "startKernel failed: $e", e)
VpnEventBus.postStatus("error")
updateNotification("连接失败")
doStop()
@@ -156,305 +161,311 @@ class PangolinVpnService : VpnService() {
}.apply { name = "pangolin-libbox-start" }.start()
}
/**
* 在后台线程内:
* 1. 构造 PlatformInterfaceinner class,持有 VpnService 引用)
* 2. 新建 BoxService 并 start()(内核会回调 openTun 建 TUN 接口)
* 3. 启动 CommandClient 轮询统计
* 4. 推送 "on" 状态
*/
private fun startLibbox(configJson: String) {
val platform = PangolinPlatformInterface()
private fun startKernel(configJson: String) {
ensureLibboxSetup()
// ── libbox API: Libbox.newBoxService(platformInterface, configJson, needBuildConfig)
// 如果编译报错,可能 API 为: new BoxService(platform, configJson)
// 或: BoxService.newService(platform, configJson)
val service = Libbox.newBoxService(platform, configJson, false) // libbox API
service.start() // libbox API
boxService = service
Log.i(TAG, "BoxService started")
// 先起默认接口监听,再起内核——否则内核启动期拿不到默认出口,报 no available network interface。
DefaultNetworkMonitor.start(applicationContext)
// 启动统计客户端
startCommandClientOrFallback()
val server = Libbox.newCommandServer(this, this)
server.start()
commandServer = server
server.startOrReloadService(configJson, OverrideOptions())
Log.i(TAG, "sing-box service started")
startStatsClient()
// 更新通知与状态
updateNotification("加速已开启")
VpnEventBus.postStatus("on")
}
// ── 统计:CommandClient + TrafficStats 兜底 ───────────────────
private fun ensureLibboxSetup() {
if (libboxSetupDone) return
val base = filesDir.absolutePath
val work = filesDir.absolutePath
val temp = cacheDir.absolutePath
val options = SetupOptions().apply {
basePath = base
workingPath = work
tempPath = temp
}
Libbox.setup(options)
libboxSetupDone = true
Log.i(TAG, "Libbox.setup done (base=$base)")
}
/**
* 优先使用 libbox CommandClient 获取精确统计;若连接失败则退回 Android TrafficStats。
*/
private fun startCommandClientOrFallback() {
// ── 统计 + 切节点:CommandClient ──────────────────────────────
private fun startStatsClient() {
try {
startCommandClient()
val options = CommandClientOptions().apply {
statusInterval = 1_000_000_000L // 1s(纳秒)
addCommand(Libbox.CommandStatus)
addCommand(Libbox.CommandGroup) // 取出口组 → getActiveOutbound + urltest 延迟
}
val client = Libbox.newCommandClient(StatsHandler(), options)
client.connect()
statsClient = client
Log.i(TAG, "stats CommandClient connected")
} catch (e: Exception) {
Log.w(TAG, "CommandClient unavailable ($e), falling back to TrafficStats")
startTrafficStatsFallback()
Log.w(TAG, "stats CommandClient failed: $e")
}
}
private fun startCommandClient() {
val options = CommandClientOptions()
// Command 0 = STATUS(含流量统计);间隔 1 秒(纳秒)
// libbox API: options.Command / options.StatusInterval 为 gomobile 暴露的字段
options.command = 0 // libbox API field
options.statusInterval = 1_000_000_000L // libbox API field: nanoseconds
val handler = object : CommandClientHandler {
override fun connected() {
Log.d(TAG, "CommandClient connected")
}
override fun disconnected(message: String) {
Log.d(TAG, "CommandClient disconnected: $message")
// 断开后不改变 VPN 状态——隧道仍在运行,仅统计中断
}
override fun writeLog(message: String) {
Log.v("libbox", message)
}
override fun writeStatus(status: StatusMessage) {
// libbox API: StatusMessage 提供累计流量与瞬时速率
val stats = mapOf<String, Any>(
"uploadBytes" to (status.uploadTotal()), // libbox API
"downloadBytes" to (status.downloadTotal()), // libbox API
"uploadSpeed" to (status.uploadSpeed().toDouble()), // libbox API
"downloadSpeed" to (status.downloadSpeed().toDouble()), // libbox API
"urltestResults" to emptyList<Any>()
)
VpnEventBus.postStats(stats)
}
override fun writeGroups(groups: OutboundGroupIterator) { /* 暂不处理 urltest 延迟 */ }
override fun initializeClashMode(modeList: StringIterator, currentMode: String) {}
override fun updateClashMode(newMode: String) {}
/** 由 MainActivity(同进程)调用:切换出口节点到 [tag](在缓存的可选组内)。 */
fun selectOutbound(tag: String) {
val group = selectableGroup
if (group.isEmpty()) {
Log.w(TAG, "selectOutbound: 无可选出口组(config 可能只有 urltest,无 selector)")
return
}
try {
statsClient?.selectOutbound(group, tag)
Log.i(TAG, "selectOutbound(group=$group, tag=$tag)")
} catch (e: Exception) {
Log.w(TAG, "selectOutbound failed: $e")
}
// libbox API: Libbox.newCommandClient(handler, options)
val client = Libbox.newCommandClient(handler, options) // libbox API
client.connect() // libbox API
commandClient = client
Log.i(TAG, "CommandClient started")
}
/**
* 兜底:用 Android TrafficStats API 按 UID 统计网络流量。
* 由于所有流量都走 VPN,UID 统计近似等于隧道流量。
*/
private fun startTrafficStatsFallback() {
val uid = android.os.Process.myUid()
var lastRx = TrafficStats.getUidRxBytes(uid)
var lastTx = TrafficStats.getUidTxBytes(uid)
var totalRx = 0L
var totalTx = 0L
fun currentActiveOutbound(): String = activeOutbound
statsExecutor = Executors.newSingleThreadScheduledExecutor().also { exec ->
exec.scheduleAtFixedRate({
if (stopping.get()) return@scheduleAtFixedRate
try {
val rx = TrafficStats.getUidRxBytes(uid).let { if (it < 0) 0L else it }
val tx = TrafficStats.getUidTxBytes(uid).let { if (it < 0) 0L else it }
val rxDiff = (rx - lastRx).coerceAtLeast(0)
val txDiff = (tx - lastTx).coerceAtLeast(0)
totalRx += rxDiff
totalTx += txDiff
lastRx = rx
lastTx = tx
private inner class StatsHandler : CommandClientHandler {
override fun connected() { Log.d(TAG, "stats client connected") }
override fun disconnected(message: String?) { Log.d(TAG, "stats client disconnected: $message") }
override fun clearLogs() {}
override fun writeLogs(messageList: LogIterator?) {}
override fun setDefaultLogLevel(level: Int) {}
override fun initializeClashMode(modeList: StringIterator?, currentMode: String?) {}
override fun updateClashMode(newMode: String?) {}
override fun writeConnectionEvents(message: ConnectionEvents?) {}
val stats = mapOf<String, Any>(
"uploadBytes" to totalTx,
"downloadBytes" to totalRx,
"uploadSpeed" to txDiff.toDouble(),
"downloadSpeed" to rxDiff.toDouble(),
"urltestResults" to emptyList<Any>()
)
VpnEventBus.postStats(stats)
} catch (e: Exception) {
Log.w(TAG, "TrafficStats error: $e")
override fun writeStatus(message: StatusMessage) {
val stats = mapOf<String, Any>(
"uploadBytes" to message.uplinkTotal,
"downloadBytes" to message.downlinkTotal,
"uploadSpeed" to message.uplink.toDouble(),
"downloadSpeed" to message.downlink.toDouble(),
"urltestResults" to latestUrltest.map {
mapOf<String, Any>("tag" to it.first, "delayMs" to it.second)
}
}, 1L, 1L, TimeUnit.SECONDS)
)
VpnEventBus.postStats(stats)
}
override fun writeGroups(groups: OutboundGroupIterator?) {
if (groups == null) return
val urltest = mutableListOf<Pair<String, Int>>()
var selected = activeOutbound
try {
while (groups.hasNext()) {
val group = groups.next()
// 记录可选组的当前出口(如 selector / urltest 的 selected)。
if (group.selected.isNotEmpty()) selected = group.selected
if (group.selectable) selectableGroup = group.tag
val items = group.items
while (items.hasNext()) {
val item = items.next()
urltest.add(item.tag to item.getURLTestDelay())
}
}
} catch (e: Exception) {
Log.w(TAG, "writeGroups parse error: $e")
}
latestUrltest = urltest
activeOutbound = selected
}
Log.i(TAG, "TrafficStats fallback started, uid=$uid")
}
// ── 停止流程 ─────────────────────────────────────────────────
/**
* 幂等停止:关闭统计 → 关闭 BoxService → 关闭 TUN fd → 停前台通知。
* 由多个入口(ACTION_STOP / onRevoke / onDestroy)调用,通过 [stopping] 防止重入。
*/
private fun doStop() {
if (!stopping.compareAndSet(false, true)) {
Log.d(TAG, "doStop: already stopping, skip")
return
}
Log.i(TAG, "doStop: shutting down tunnel")
if (!stopping.compareAndSet(false, true)) return
Log.i(TAG, "doStop")
// 1. 停统计(先于 BoxService,避免最后一帧读到已关闭的 fd)
statsExecutor?.apply {
shutdownNow()
try { awaitTermination(500, TimeUnit.MILLISECONDS) } catch (_: InterruptedException) {}
}
statsExecutor = null
try { statsClient?.disconnect() } catch (_: Exception) {}
statsClient = null
try {
commandClient?.disconnect() // libbox API
} catch (e: Exception) {
Log.w(TAG, "commandClient.disconnect failed: $e")
}
commandClient = null
try { commandServer?.closeService() } catch (e: Exception) { Log.w(TAG, "closeService: $e") }
try { commandServer?.close() } catch (e: Exception) { Log.w(TAG, "close: $e") }
commandServer = null
// 2. 关闭 BoxService 内核(会在内部关闭它持有的 TUN fd 引用)
try {
boxService?.close() // libbox API
} catch (e: Exception) {
Log.w(TAG, "boxService.close failed: $e")
}
boxService = null
DefaultNetworkMonitor.stop()
// 3. 关闭 ParcelFileDescriptor(持有底层 fd;必须在 BoxService.close 之后)
try {
currentTunPfd?.close()
} catch (e: Exception) {
Log.w(TAG, "tunPfd.close failed: $e")
}
currentTunPfd = null
try { tunPfd?.close() } catch (_: Exception) {}
tunPfd = null
// 4. 停前台 + 自我停止
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf()
VpnEventBus.postStatus("off")
Log.i(TAG, "doStop: done")
}
// ── PlatformInterfaceinner class,持有 VpnService.Builder 访问权)────
// ══════════════════════════════════════════════════════════════
// PlatformInterface 实现(1.13.12 共 15 法)
// ══════════════════════════════════════════════════════════════
/**
* sing-box libbox 平台回调实现。
*
* [openTun]: 用 TunOptions 配置 VpnService.Builder,调用 establish() 取 TUN fd。
* [autoDetectInterfaceControl]: 调用 VpnService.protect(fd) 防止 TUN 流量形成环路。
*/
private inner class PangolinPlatformInterface : PlatformInterface {
override fun openTun(options: TunOptions): Int {
if (prepare(this) != null) throw Exception("android: missing vpn permission")
/**
* libbox 回调 — 创建 TUN 接口并返回 fd。
*
* 注:TunOptions 的具体 getter 方法名(inet4Address / mtu 等)
* 需与实际 libbox.aar 中 go.libbox.TunOptions 的 javadoc 对齐。
* 若方法不存在,请使用 options 对应的实际方法名替换。
*/
override fun openTun(options: TunOptions): Int {
val builder = Builder() // VpnService.Builderinner class,此处可直接调用)
val builder = Builder()
.setSession("Pangolin")
.setMtu(options.getMTU())
// ── 地址配置 ──────────────────────────────────────────
// libbox API: TunOptions.inet4Address() 返回 CIDR 字符串(如 "172.19.0.1/30"
// 若 API 名不同(如 getInet4Address())请对应修改
val inet4 = tryGetInet4(options)
if (inet4.isNotEmpty()) {
val slash = inet4.indexOf('/')
if (slash > 0) {
builder.addAddress(inet4.substring(0, slash), inet4.substring(slash + 1).toInt())
} else {
builder.addAddress(inet4, 30)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) builder.setMetered(false)
// 地址(RoutePrefixIterator
val inet4 = options.inet4Address
while (inet4.hasNext()) {
val a = inet4.next(); builder.addAddress(a.address(), a.prefix())
}
val inet6 = options.inet6Address
while (inet6.hasNext()) {
val a = inet6.next(); builder.addAddress(a.address(), a.prefix())
}
if (options.autoRoute) {
// DNS1.13.12:单个 StringBox
try {
val dns = options.getDNSServerAddress()?.getValue()
if (!dns.isNullOrEmpty()) builder.addDnsServer(dns)
} catch (e: Exception) {
Log.w(TAG, "addDnsServer: $e")
}
// 路由:优先 inet4RouteAddress;为空则全量 0.0.0.0/0。
val r4 = options.inet4RouteAddress
if (r4.hasNext()) {
while (r4.hasNext()) { val a = r4.next(); builder.addRoute(a.address(), a.prefix()) }
} else if (options.inet4Address.hasNext()) {
builder.addRoute("0.0.0.0", 0)
}
val r6 = options.inet6RouteAddress
if (r6.hasNext()) {
while (r6.hasNext()) { val a = r6.next(); builder.addRoute(a.address(), a.prefix()) }
} else if (options.inet6Address.hasNext()) {
builder.addRoute("::", 0)
}
// 分应用(include/exclude package
val inc = options.includePackage
while (inc.hasNext()) {
try { builder.addAllowedApplication(inc.next()) } catch (e: Exception) { Log.w(TAG, "allow app: $e") }
}
val exc = options.excludePackage
while (exc.hasNext()) {
try { builder.addDisallowedApplication(exc.next()) } catch (e: Exception) { Log.w(TAG, "disallow app: $e") }
}
}
val pfd = builder.establish() ?: throw Exception("establish() returned null — 未授权或已被撤销")
tunPfd = pfd
Log.i(TAG, "openTun: fd=${pfd.fd}, mtu=${options.mtu}")
return pfd.fd
}
override fun autoDetectInterfaceControl(fd: Int) {
if (!protect(fd)) throw Exception("VpnService.protect(fd=$fd) failed")
}
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
override fun useProcFS(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
override fun startDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(listener)
}
override fun closeDefaultInterfaceMonitor(listener: InterfaceUpdateListener) {
DefaultNetworkMonitor.setListener(null)
}
override fun getInterfaces(): NetworkInterfaceIterator {
val list = mutableListOf<libbox.NetworkInterface>()
try {
for (ni in Collections.list(java.net.NetworkInterface.getNetworkInterfaces())) {
val box = libbox.NetworkInterface()
box.name = ni.name
box.index = ni.index
runCatching { box.setMTU(ni.mtu) }
box.addresses = StringArray(
ni.interfaceAddresses.mapNotNull { ia ->
val host = ia.address?.hostAddress ?: return@mapNotNull null
"$host/${ia.networkPrefixLength}"
}.iterator()
)
var flags = 0
runCatching {
if (ni.isUp) flags = flags or OsConstants.IFF_UP or OsConstants.IFF_RUNNING
if (ni.isLoopback) flags = flags or OsConstants.IFF_LOOPBACK
if (ni.isPointToPoint) flags = flags or OsConstants.IFF_POINTOPOINT
if (ni.supportsMulticast()) flags = flags or OsConstants.IFF_MULTICAST
}
} else {
// 兜底:使用 PoC 静态配置的地址
builder.addAddress("172.19.0.1", 30)
box.flags = flags
box.type = Libbox.InterfaceTypeOther
list.add(box)
}
val inet6 = tryGetInet6(options)
if (inet6.isNotEmpty()) {
val slash = inet6.indexOf('/')
if (slash > 0) {
builder.addAddress(inet6.substring(0, slash), inet6.substring(slash + 1).toInt())
}
}
// ── MTU ────────────────────────────────────────────────
val mtu = tryGetMtu(options)
builder.setMtu(mtu)
// ── 路由:将所有流量导入 TUNsing-box auto_route = true)────
builder.addRoute("0.0.0.0", 0) // IPv4 全部流量
builder.addRoute("::", 0) // IPv6 全部流量
// ── DNSsing-box 内置 Fake-IP 地址 ───────────────────
// sing-box 在 172.18.0.0/15 运行 Fake-IP,设置匹配 DNS 服务器
builder.addDnsServer("198.18.0.2")
builder.setSession("Pangolin")
builder.setBlocking(true)
val pfd = builder.establish()
?: throw Exception("VpnService.Builder.establish() returned null — 可能未授权")
currentTunPfd = pfd
Log.i(TAG, "openTun: fd=${pfd.fd}, inet4=$inet4, mtu=$mtu")
return pfd.fd
}
/**
* libbox 回调 — 调用 VpnService.protect() 使 fd 跳过 TUN,防止路由环路。
*
* 注:gomobile 将 Go `error` 返回翻译为抛出 Exception(无返回值)。
*/
override fun autoDetectInterfaceControl(fd: Int) {
if (!protect(fd)) {
throw Exception("VpnService.protect(fd=$fd) failed")
}
}
override fun writeLog(message: String) {
Log.v("libbox", message)
}
/** 使用 Android VpnService 的 protect() 机制,不需要平台独立的路由探测 */
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
/** 使用 Android 系统的网络变化监听,不使用 libbox 内置监听 */
override fun usePlatformDefaultInterfaceMonitor(): Boolean = false
/** 不使用 libbox 内置接口枚举 */
override fun usePlatformInterfaceGetter(): Boolean = false
/**
* 进程信息查询(用于 sing-box 的应用规则)。
* PoC 阶段不需要,返回 null 即可(libbox 会跳过进程匹配规则)。
*/
override fun findProcessInfo(
networkType: Int,
srcIP: String,
srcPort: Int,
destIP: String,
destPort: Int,
): ProcessInfo? = null
// ── TunOptions 安全 getter(兜底处理 API 名不匹配问题)──
/** 安全读取 inet4Address,若 API 不匹配返回空串,由调用方用兜底值 */
private fun tryGetInet4(options: TunOptions): String = try {
// libbox API: options.inet4Address() — 若名称不同请修改
options.inet4Address()
} catch (e: Exception) {
Log.w(TAG, "TunOptions.inet4Address() failed: $e")
""
Log.w(TAG, "getInterfaces: $e")
}
return InterfaceArray(list.iterator())
}
private fun tryGetInet6(options: TunOptions): String = try {
options.inet6Address()
} catch (e: Exception) {
""
}
override fun findConnectionOwner(
ipProtocol: Int, sourceAddress: String, sourcePort: Int,
destinationAddress: String, destinationPort: Int,
): ConnectionOwner {
// 我们的配置不含进程匹配规则,核心不会调用;保守抛出。
throw Exception("findConnectionOwner not supported")
}
private fun tryGetMtu(options: TunOptions): Int = try {
// libbox API: options.mtu() returns Int32/Int
options.mtu().toInt().coerceIn(576, 65535)
} catch (e: Exception) {
9000 // 兜底:与 PoC 配置对齐
}
override fun includeAllNetworks(): Boolean = false
override fun clearDNSCache() {}
override fun readWIFIState(): WIFIState? = null
override fun localDNSTransport(): LocalDNSTransport? = null
override fun systemCertificates(): StringIterator = StringArray(emptyList<String>().iterator())
override fun underNetworkExtension(): Boolean = false
override fun sendNotification(notification: LibboxNotification) {
Log.d(TAG, "libbox notification: ${notification.title} / ${notification.body}")
}
// ══════════════════════════════════════════════════════════════
// CommandServerHandler 实现(5 法)
// ══════════════════════════════════════════════════════════════
override fun serviceReload() { Log.d(TAG, "serviceReload") }
override fun serviceStop() { doStop() }
override fun getSystemProxyStatus(): SystemProxyStatus = SystemProxyStatus().apply {
available = false
enabled = false
}
override fun setSystemProxyEnabled(isEnabled: Boolean) {}
override fun writeDebugMessage(message: String?) { Log.d("sing-box", message ?: "") }
// ── gomobile 迭代器适配 ───────────────────────────────────────
private class StringArray(private val it: Iterator<String>) : StringIterator {
override fun len(): Int = 0 // 内核不依赖精确长度
override fun hasNext(): Boolean = it.hasNext()
override fun next(): String = it.next()
}
private class InterfaceArray(
private val it: Iterator<libbox.NetworkInterface>
) : NetworkInterfaceIterator {
override fun hasNext(): Boolean = it.hasNext()
override fun next(): libbox.NetworkInterface = it.next()
}
// ── 通知 ─────────────────────────────────────────────────────
@@ -462,46 +473,36 @@ class PangolinVpnService : VpnService() {
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"穿山甲加速",
NotificationManager.IMPORTANCE_LOW
NOTIFICATION_CHANNEL_ID, "穿山甲加速", NotificationManager.IMPORTANCE_LOW
).apply {
description = "穿山甲加速状态"
setShowBadge(false)
}
getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
private fun updateNotification(contentText: String) {
getSystemService(NotificationManager::class.java)
.notify(NOTIFICATION_ID, buildNotification(contentText))
private fun updateNotification(text: String) {
getSystemService(NotificationManager::class.java).notify(NOTIFICATION_ID, buildNotification(text))
}
private fun buildNotification(contentText: String): Notification {
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
@Suppress("DEPRECATION")
PendingIntent.FLAG_UPDATE_CURRENT
@Suppress("DEPRECATION") PendingIntent.FLAG_UPDATE_CURRENT
}
val openIntent = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
pendingFlags
this, 0, Intent(this, MainActivity::class.java), pendingFlags
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
@Suppress("DEPRECATION") Notification.Builder(this)
}
return builder
.setContentTitle("穿山甲")
.setContentText(contentText) // "加速已开启" / "正在连接…" / "连接失败"
.setContentText(contentText)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(openIntent)
.setOngoing(true)
@@ -0,0 +1,16 @@
<!-- 占位启动图标(vector,全 API 可用)。正式图标待设计接入后替换。 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0F1117"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#E0884F"
android:pathData="M54,28 L80,82 L28,82 Z" />
<path
android:fillColor="#0F1117"
android:pathData="M54,52 L66,76 L42,76 Z" />
</vector>
+2 -15
View File
@@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
@@ -18,12 +5,12 @@ allprojects {
}
}
rootProject.buildDir = '../build'
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
+4
View File
@@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+22 -8
View File
@@ -1,11 +1,25 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.2.0" apply false
}
include ":app"