merge: Android M2 VPN 隧道 PangolinVpnService [tsk_w1NvZdmwGPyd]

This commit is contained in:
wangjia
2026-06-15 20:56:37 +08:00
6 changed files with 645 additions and 46 deletions
+30 -1
View File
@@ -45,7 +45,8 @@ android {
defaultConfig {
applicationId "com.pangolin.pangolin_vpn"
minSdkVersion flutter.minSdkVersion
// libbox requires minSdk 21 (gomobile -androidapi 21)
minSdkVersion Math.max(flutter.minSdkVersion as Integer, 21)
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -56,6 +57,17 @@ 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 {
@@ -64,4 +76,21 @@ 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/
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")
}
}
@@ -1,11 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限(实际隧道流量) -->
<!-- 网络权限(隧道流量) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 前台服务(VPN 持续运行所需,API 28+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!--
Android 14 (API 34+) 要求前台服务必须声明类型。
VPN 不属于任何预定义类型,故使用 specialUse。
Play Store 上架时需在 specialUse 说明中填写 VPN 用途(国内合规绕过此字段)。
-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--
允许应用请求豁免电池优化(后台 30 分钟不被杀必须)。
不能在运行时动态请求,必须在 manifest 声明。
-->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:label="穿山甲"
android:name="${applicationName}"
@@ -30,18 +44,27 @@
</activity>
<!--
VPN 服务(骨架)
VPN 服务。
android:permission="android.permission.BIND_VPN_SERVICE" 确保只有系统可 bind。
intent-filter android.net.VpnService 是 VpnService 规范要求。
TODO(11E): 启用后在设备上需用户授权 VPN 权限(VpnService.prepare()
android:foregroundServiceType="specialUse" — Android 14 (API 34) 强制要求
-->
<service
android:name=".PangolinVpnService"
android:exported="false"
android:permission="android.permission.BIND_VPN_SERVICE">
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
<!--
Android 14 specialUse 前台服务类型需在此声明用途说明。
若目标 API < 34 可忽略此 meta-data;保留不影响低版本兼容性。
-->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service>
<meta-data
@@ -0,0 +1,101 @@
{
"_comment": "PoC M2 单节点测试配置(11E / tsk_w1NvZdmwGPyd",
"_usage": "填入真实节点参数后,在 Dart 侧 VpnNativeBridge.start(configJson) 传入",
"log": {
"level": "warn",
"timestamp": true
},
"inbounds": [
{
"type": "tun",
"tag": "tun-in",
"address": ["172.19.0.1/30"],
"mtu": 9000,
"auto_route": true,
"strict_route": true,
"stack": "system"
}
],
"outbounds": [
{
"type": "vless",
"tag": "reality-out",
"server": "__NODE_HOST__",
"server_port": 11443,
"uuid": "__REALITY_UUID__",
"flow": "xtls-rprx-vision",
"tls": {
"enabled": true,
"server_name": "www.apple.com",
"utls": {
"enabled": true,
"fingerprint": "chrome"
},
"reality": {
"enabled": true,
"public_key": "__REALITY_PUBLIC_KEY__",
"short_id": "__REALITY_SHORT_ID__"
}
}
},
{
"type": "hysteria2",
"tag": "hy2-out",
"server": "__NODE_HOST__",
"server_port": 443,
"password": "__HY2_PASSWORD__",
"tls": {
"enabled": true,
"insecure": true,
"alpn": ["h3"]
}
},
{
"type": "urltest",
"tag": "auto",
"outbounds": ["reality-out", "hy2-out"],
"url": "https://www.gstatic.com/generate_204",
"interval": "3m",
"tolerance": 50
},
{
"type": "block",
"tag": "block"
},
{
"type": "direct",
"tag": "direct"
}
],
"route": {
"rules": [
{
"ip_cidr": [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"127.0.0.0/8"
],
"outbound": "direct"
}
],
"final": "auto",
"auto_detect_interface": true
},
"dns": {
"servers": [
{
"tag": "remote",
"address": "tls://8.8.8.8",
"detour": "auto"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"final": "remote",
"strategy": "ipv4_only"
}
}
@@ -1,5 +1,12 @@
package com.pangolin.pangolin_vpn
import android.app.Activity
import android.content.Intent
import android.net.VpnService
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -14,8 +21,14 @@ import io.flutter.plugin.common.MethodChannel
* EventChannel : pangolin/vpn/status
* EventChannel : pangolin/vpn/stats
*
* MethodChannel 路由到 PangolinVpnService(通过 Intent)。
* EventChannel 由 VpnEventBus 驱动(Service → EventBus → EventSink → Flutter)。
* ## VPN 权限流程(Android 要求)
* 1. [VpnService.prepare] 返回非 null Intent → 需要用户授权
* 2. 启动授权 Activityresult code 存 [pendingConfigJson]
* 3. [onActivityResult] 回调 RESULT_OK → 正式启动 [PangolinVpnService]
* RESULT_CANCELED → 推送 error 状态到 Flutter
*
* ## 电池优化
* 首次连接时引导用户豁免电池优化,确保后台 30 分钟不被杀。
*/
class MainActivity : FlutterActivity() {
@@ -24,9 +37,13 @@ class MainActivity : FlutterActivity() {
private const val VPN_CHANNEL = "pangolin/vpn"
private const val STATUS_CHANNEL = "pangolin/vpn/status"
private const val STATS_CHANNEL = "pangolin/vpn/stats"
private const val VPN_PERMISSION_REQUEST_CODE = 100
}
// 持有 EventSink 引用,以便 VpnEventBus 回调时转发
/** 等待 VPN 授权时缓存的 configJson,授权完成后使用 */
private var pendingConfigJson: String? = null
private var statusEventSink: EventChannel.EventSink? = null
private var statsEventSink: EventChannel.EventSink? = null
@@ -40,31 +57,30 @@ class MainActivity : FlutterActivity() {
when (call.method) {
"start" -> {
val configJson = call.arguments as? String ?: "{}"
PangolinVpnService.startVpn(this, configJson)
result.success(null)
handleStartCall(configJson, result)
}
"stop" -> {
PangolinVpnService.stopVpn(this)
result.success(null)
}
"getStatus" -> {
// TODO(11E): 从 VpnService 查询真实状态
result.success("off")
result.success(VpnEventBus.currentStatus)
}
"selectOutbound" -> {
val tag = call.arguments as? String ?: "auto"
Log.d(TAG, "selectOutbound: tag=$tag")
// TODO(11E): 发送 IPC 到 VpnService → sing-box outbound selector
Log.d(TAG, "selectOutbound: tag=$tag (stub — 11G)")
// TODO(11G): 通过 libbox CommandClient 切换出口
result.success(null)
}
"getActiveOutbound" -> {
// TODO(11E): 从 VpnService 查询当前出口 tag
Log.d(TAG, "getActiveOutbound (stub — 11G)")
// TODO(11G): 从 libbox CommandClient 查询当前出口 tag
result.success("auto")
}
"setKillSwitch" -> {
val on = call.arguments as? Boolean ?: false
Log.d(TAG, "setKillSwitch: on=$on")
// TODO(11E): 配置 VpnService allowedApplications / blockingMode
Log.d(TAG, "setKillSwitch: on=$on (stub — 11G)")
// TODO(11G): 配置 VPN allowedApplications / blockingMode
result.success(null)
}
else -> result.notImplemented()
@@ -82,6 +98,8 @@ class MainActivity : FlutterActivity() {
statusEventSink?.success(status)
}
})
// 立即推送当前状态(订阅时的初始值)
events.success(VpnEventBus.currentStatus)
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "status channel: onCancel")
@@ -112,6 +130,73 @@ class MainActivity : FlutterActivity() {
)
}
// ── VPN 权限 + 电池优化 ───────────────────────────────────────
/**
* start() MethodChannel 调用入口:
* - 若 VpnService.prepare() 需要用户授权,先弹授权 Activity(result 异步回调)
* - 若已授权,直接启动服务
*/
private fun handleStartCall(configJson: String, result: MethodChannel.Result) {
// 同步检查:是否需要 VPN 授权弹窗
val prepareIntent = VpnService.prepare(this)
if (prepareIntent != null) {
Log.i(TAG, "VPN permission not granted, launching prepare intent")
pendingConfigJson = configJson
@Suppress("DEPRECATION")
startActivityForResult(prepareIntent, VPN_PERMISSION_REQUEST_CODE)
// 先返回 null 给 Dart,实际连接状态由 EventChannel 驱动
result.success(null)
} else {
// 已授权,直接启动
requestBatteryOptimizationExemptionIfNeeded()
PangolinVpnService.startVpn(this, configJson)
result.success(null)
}
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
val config = pendingConfigJson
pendingConfigJson = null
if (resultCode == Activity.RESULT_OK && config != null) {
Log.i(TAG, "VPN permission granted, starting service")
requestBatteryOptimizationExemptionIfNeeded()
PangolinVpnService.startVpn(this, config)
} else {
Log.w(TAG, "VPN permission denied (resultCode=$resultCode)")
VpnEventBus.postStatus("error")
}
}
@Suppress("DEPRECATION")
super.onActivityResult(requestCode, resultCode, data)
}
/**
* 引导用户豁免电池优化(首次连接时弹一次)。
* 豁免后后台 30 分钟内系统不会主动杀服务。
*
* 注:BATTERY_OPTIMIZATION_EXEMPTED 不影响应用核心功能,
* 用户可在设置内随时撤销——VPN 仍可运行,但有被杀风险。
*/
private fun requestBatteryOptimizationExemptionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pm = getSystemService(PowerManager::class.java)
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
try {
startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:$packageName")
})
} catch (e: Exception) {
Log.w(TAG, "Cannot launch battery optimization settings: $e")
}
}
}
}
// ── 清理 ─────────────────────────────────────────────────────
override fun onDestroy() {
statusEventSink = null
statsEventSink = null
@@ -6,20 +6,44 @@ 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.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 java.util.concurrent.atomic.AtomicBoolean
/**
* PangolinVpnService — VPN 服务骨架(空壳)
* PangolinVpnService — VPN 前台服务,封装 sing-box libbox 隧道。
*
* 生命周期占位日志 + 前台通知;实际隧道建立由 11E 完成。
* ## 生命周期
* START → handleStart() → 后台线程 → startLibbox() → [BoxService.start()]
* → VpnEventBus.postStatus("on")
* STOP → doStop() → [BoxService.close()] → 关 TUN fd → postStatus("off")
* REVOKE → onRevoke() → postStatus("error") → doStop()
*
* 通信
* - [MainActivity] 通过 [startVpn] / [stopVpn] 发送 Intent 启动/停止本服务
* - 状态回调通过 [VpnEventBus] (Application 级 LiveData) 传到 MainActivity EventSink
* ## 通信
* Dart 通过 [MainActivity] MethodChannel 发送 start/stop intent。
* 状态与统计经 [VpnEventBus] 推回 Flutter EventChannel。
*
* TODO(11E): 在 onStartCommand 中初始化 libbox / sing-box TUN 隧道。
* ## 注意:libbox.aar API
* 依赖 app/kernel/dist/android/libbox.aar(由 build-android.sh 产出)。
* 方法签名注释标注 "// libbox API" 处若编译失败,请对照实际 libbox.aar 中
* go.libbox.* 的 javadoc 微调参数名;整体架构不变。
*/
class PangolinVpnService : VpnService() {
@@ -43,7 +67,7 @@ class PangolinVpnService : VpnService() {
} else {
context.startService(intent)
}
Log.d(TAG, "startVpn: intent dispatched")
Log.d(TAG, "startVpn: intent dispatched, configLen=${configJson.length}")
}
fun stopVpn(context: Context) {
@@ -55,6 +79,23 @@ class PangolinVpnService : VpnService() {
}
}
// ── 内核与 TUN 状态 ───────────────────────────────────────────
/** libbox BoxService 内核实例(主线程以外创建,通过 volatile 保证可见性) */
@Volatile private var boxService: BoxService? = null
/** TUN 文件描述符持有者(VpnService.Builder.establish() 返回值) */
@Volatile var currentTunPfd: ParcelFileDescriptor? = null
/** libbox CommandClient(统计与状态轮询) */
@Volatile private var commandClient: CommandClient? = null
/** 兜底统计定时器(CommandClient 不可用时启用 TrafficStats 方案) */
private var statsExecutor: ScheduledExecutorService? = null
/** 防止 doStop 重入 */
private val stopping = AtomicBoolean(false)
// ── 生命周期 ────────────────────────────────────────────────
override fun onCreate() {
@@ -72,65 +113,377 @@ class PangolinVpnService : VpnService() {
START_STICKY
}
ACTION_STOP -> {
handleStop()
doStop()
START_NOT_STICKY
}
else -> {
Log.w(TAG, "onStartCommand: unknown action=${intent?.action}")
Log.w(TAG, "onStartCommand: unknown action, falling through")
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")
handleStop()
doStop()
super.onDestroy()
}
override fun onRevoke() {
Log.w(TAG, "onRevoke: VPN permission revoked by system")
// TODO(11E): 通知 Dart 侧 status=error
handleStop()
}
// ── 内部处理 ────────────────────────────────────────────────
// ── 启动流程 ─────────────────────────────────────────────────
private fun handleStart(configJson: String) {
Log.i(TAG, "handleStart: config=${configJson.take(80)}")
stopping.set(false)
startForeground(NOTIFICATION_ID, buildNotification("正在连接…"))
// TODO(11E): 解析 configJson → 构建 VPN 接口 → 启动 sing-box 内核
// 状态变化后通过 VpnEventBus.postStatus("connecting") / ("on") 通知 MainActivity
VpnEventBus.postStatus("connecting")
// libbox 内核启动必须在后台线程(不能阻塞 onStartCommand
Thread {
try {
startLibbox(configJson)
} catch (e: Exception) {
Log.e(TAG, "startLibbox failed: $e", e)
VpnEventBus.postStatus("error")
updateNotification("连接失败")
doStop()
}
}.apply { name = "pangolin-libbox-start" }.start()
}
private fun handleStop() {
Log.i(TAG, "handleStop")
/**
* 在后台线程内:
* 1. 构造 PlatformInterfaceinner class,持有 VpnService 引用)
* 2. 新建 BoxService 并 start()(内核会回调 openTun 建 TUN 接口)
* 3. 启动 CommandClient 轮询统计
* 4. 推送 "on" 状态
*/
private fun startLibbox(configJson: String) {
val platform = PangolinPlatformInterface()
// ── 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")
// 启动统计客户端
startCommandClientOrFallback()
// 更新通知与状态
updateNotification("加速已开启")
VpnEventBus.postStatus("on")
}
// ── 统计:CommandClient + TrafficStats 兜底 ───────────────────
/**
* 优先使用 libbox CommandClient 获取精确统计;若连接失败则退回 Android TrafficStats。
*/
private fun startCommandClientOrFallback() {
try {
startCommandClient()
} catch (e: Exception) {
Log.w(TAG, "CommandClient unavailable ($e), falling back to TrafficStats")
startTrafficStatsFallback()
}
}
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) {}
}
// 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
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
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")
}
}, 1L, 1L, TimeUnit.SECONDS)
}
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")
// 1. 停统计(先于 BoxService,避免最后一帧读到已关闭的 fd)
statsExecutor?.apply {
shutdownNow()
try { awaitTermination(500, TimeUnit.MILLISECONDS) } catch (_: InterruptedException) {}
}
statsExecutor = null
try {
commandClient?.disconnect() // libbox API
} catch (e: Exception) {
Log.w(TAG, "commandClient.disconnect failed: $e")
}
commandClient = null
// 2. 关闭 BoxService 内核(会在内部关闭它持有的 TUN fd 引用)
try {
boxService?.close() // libbox API
} catch (e: Exception) {
Log.w(TAG, "boxService.close failed: $e")
}
boxService = null
// 3. 关闭 ParcelFileDescriptor(持有底层 fd;必须在 BoxService.close 之后)
try {
currentTunPfd?.close()
} catch (e: Exception) {
Log.w(TAG, "tunPfd.close failed: $e")
}
currentTunPfd = null
// 4. 停前台 + 自我停止
@Suppress("DEPRECATION")
stopForeground(true)
stopSelf()
// TODO(11E): 停止 sing-box 内核 → 关闭 TUN fd
VpnEventBus.postStatus("off")
Log.i(TAG, "doStop: done")
}
// ── 前台通知 ────────────────────────────────────────────────
// ── PlatformInterfaceinner class,持有 VpnService.Builder 访问权)────
/**
* sing-box libbox 平台回调实现。
*
* [openTun]: 用 TunOptions 配置 VpnService.Builder,调用 establish() 取 TUN fd。
* [autoDetectInterfaceControl]: 调用 VpnService.protect(fd) 防止 TUN 流量形成环路。
*/
private inner class PangolinPlatformInterface : PlatformInterface {
/**
* 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,此处可直接调用)
// ── 地址配置 ──────────────────────────────────────────
// 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)
}
} else {
// 兜底:使用 PoC 静态配置的地址
builder.addAddress("172.19.0.1", 30)
}
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")
""
}
private fun tryGetInet6(options: TunOptions): String = try {
options.inet6Address()
} catch (e: Exception) {
""
}
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 配置对齐
}
}
// ── 通知 ─────────────────────────────────────────────────────
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"穿山甲 VPN",
"穿山甲加速",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "穿山甲 VPN 隧道状态"
description = "穿山甲加速状态"
setShowBadge(false)
}
val nm = getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)
getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
}
}
private fun updateNotification(contentText: String) {
getSystemService(NotificationManager::class.java)
.notify(NOTIFICATION_ID, buildNotification(contentText))
}
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
}
val openIntent = PendingIntent.getActivity(
@@ -148,7 +501,7 @@ class PangolinVpnService : VpnService() {
return builder
.setContentTitle("穿山甲")
.setContentText(contentText)
.setContentText(contentText) // "加速已开启" / "正在连接…" / "连接失败"
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentIntent(openIntent)
.setOngoing(true)
@@ -27,11 +27,19 @@ object VpnEventBus {
@Volatile private var statusListener: StatusListener? = null
@Volatile private var statsListener: StatsListener? = null
/**
* 当前 VPN 状态快照,供 getStatus() 一次性查询使用。
* 值:off | connecting | on | error
*/
@Volatile var currentStatus: String = "off"
private set
fun setStatusListener(l: StatusListener?) { statusListener = l }
fun setStatsListener(l: StatsListener?) { statsListener = l }
/** 由 PangolinVpnService(可能在工作线程)调用 */
fun postStatus(status: String) {
currentStatus = status
mainHandler.post { statusListener?.onStatus(status) }
}