Skip to content

Commit c9cd80b

Browse files
committed
Optimize android vpn performance
Add custom primary color and color scheme Add linux nad windows arm release Optimize requests and logs page
1 parent a77b3a3 commit c9cd80b

File tree

100 files changed

+3078
-994
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+3078
-994
lines changed

.github/workflows/build.yaml

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,29 +27,27 @@ jobs:
2727
- platform: macos
2828
os: macos-latest
2929
arch: arm64
30+
- platform: windows
31+
os: windows-11-arm
32+
arch: arm64
33+
- platform: linux
34+
os: ubuntu-24.04-arm
35+
arch: arm64
3036

3137
steps:
38+
- name: Setup rust
39+
if: startsWith(matrix.os, 'windows-11-arm')
40+
run: |
41+
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
42+
.\rustup-init.exe -y --default-toolchain stable
43+
$cargoPath = "$env:USERPROFILE\.cargo\bin"
44+
Add-Content $env:GITHUB_PATH $cargoPath
45+
3246
- name: Checkout
3347
uses: actions/checkout@v4
3448
with:
3549
submodules: recursive
3650

37-
- name: Setup JAVA
38-
if: startsWith(matrix.platform,'android')
39-
uses: actions/setup-java@v4
40-
with:
41-
distribution: 'zulu'
42-
java-version: 17
43-
44-
- name: Setup NDK
45-
if: startsWith(matrix.platform,'android')
46-
uses: nttld/setup-ndk@v1
47-
id: setup-ndk
48-
with:
49-
ndk-version: r26b
50-
add-to-path: true
51-
link-to-sdk: true
52-
5351
- name: Setup Android Signing
5452
if: startsWith(matrix.platform,'android')
5553
run: |
@@ -58,18 +56,17 @@ jobs:
5856
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
5957
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
6058
61-
6259
- name: Setup Go
6360
uses: actions/setup-go@v5
6461
with:
65-
go-version: 'stable'
62+
go-version: '1.24.0'
6663
cache-dependency-path: |
6764
core/go.sum
6865
6966
- name: Setup Flutter
7067
uses: subosito/flutter-action@v2
7168
with:
72-
channel: stable
69+
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
7370
cache: true
7471

7572
- name: Get Flutter Dependency

.gitmodules

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,5 @@
66
path = plugins/flutter_distributor
77
url = [email protected]:chen08209/flutter_distributor.git
88
branch = FlClash
9-
[submodule "plugins/tray_manager"]
10-
path = plugins/tray_manager
11-
url = [email protected]:chen08209/tray_manager.git
12-
branch = main
139

1410

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
android_arm64:
2+
dart ./setup.dart android --arch arm64
3+
macos_arm64:
4+
dart ./setup.dart macos --arch arm64
5+
android_app:
6+
dart ./setup.dart android
7+
android_arm64_core:
8+
dart ./setup.dart android --arch arm64 --out core
9+
macos_arm64_core:
10+
dart ./setup.dart macos --arch arm64 --out core

android/app/build.gradle

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import com.android.build.gradle.tasks.MergeSourceSetFolders
2-
31
plugins {
42
id "com.android.application"
53
id "kotlin-android"
@@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
3331

3432
android {
3533
namespace "com.follow.clash"
36-
compileSdkVersion 35
37-
ndkVersion "27.1.12297006"
34+
compileSdk 35
35+
ndkVersion = "28.0.13004108"
3836

3937
compileOptions {
4038
sourceCompatibility JavaVersion.VERSION_17
@@ -48,6 +46,7 @@ android {
4846
sourceSets {
4947
main.java.srcDirs += 'src/main/kotlin'
5048
}
49+
5150
signingConfigs {
5251
if (isRelease) {
5352
release {
@@ -84,31 +83,15 @@ android {
8483
}
8584
}
8685

87-
tasks.register('copyNativeLibs', Copy) {
88-
delete('src/main/jniLibs')
89-
from('../../libclash/android')
90-
into('src/main/jniLibs')
91-
}
92-
93-
tasks.withType(MergeSourceSetFolders).configureEach {
94-
dependsOn copyNativeLibs
95-
}
96-
9786
flutter {
9887
source '../..'
9988
}
10089

10190
dependencies {
91+
implementation project(":core")
10292
implementation 'androidx.core:core-splashscreen:1.0.1'
103-
implementation 'com.google.code.gson:gson:2.10'
104-
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
93+
implementation 'com.google.code.gson:gson:2.10.1'
94+
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
10595
exclude group: "com.google.guava", module: "guava"
10696
}
107-
}
108-
109-
110-
afterEvaluate {
111-
assembleDebug.dependsOn copyNativeLibs
112-
113-
assembleRelease.dependsOn copyNativeLibs
114-
}
97+
}
Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:tools="http://schemas.android.com/tools">
23
<!-- The INTERNET permission is required for development. Specifically,
34
the Flutter tool needs it to communicate with the running application
45
to allow setting breakpoints, to provide hot reload, etc.
56
-->
6-
<application android:label="FlClash Debug" tools:replace="android:label">
7+
<application
8+
android:icon="@mipmap/ic_launcher"
9+
android:label="FlClash Debug"
10+
tools:replace="android:label">
711
<service
8-
android:name=".services.FlClashTileService"
9-
android:label="FlClash Debug"
10-
tools:replace="android:label">
11-
</service>
12+
android:name=".services.FlClashTileService"
13+
android:label="FlClash Debug"
14+
tools:replace="android:label"
15+
tools:targetApi="24" />
1216
</application>
1317
</manifest>

android/app/src/main/kotlin/com/follow/clash/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.follow.clash
22

3+
import com.follow.clash.core.Core
34
import com.follow.clash.plugins.AppPlugin
45
import com.follow.clash.plugins.ServicePlugin
56
import com.follow.clash.plugins.TilePlugin

android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
5252
}
5353

5454
private fun handleDestroy() {
55-
GlobalState.getCurrentVPNPlugin()?.handleStop()
5655
GlobalState.destroyServiceEngine()
5756
}
5857
}

android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import androidx.core.content.getSystemService
1414
import com.follow.clash.FlClashApplication
1515
import com.follow.clash.GlobalState
1616
import com.follow.clash.RunState
17+
import com.follow.clash.core.Core
1718
import com.follow.clash.extensions.awaitResult
18-
import com.follow.clash.extensions.getProtocol
1919
import com.follow.clash.extensions.resolveDns
20-
import com.follow.clash.models.Process
2120
import com.follow.clash.models.StartForegroundParams
2221
import com.follow.clash.models.VpnOptions
2322
import com.follow.clash.services.BaseServiceInterface
@@ -40,17 +39,20 @@ import kotlin.concurrent.withLock
4039
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
4140
private lateinit var flutterMethodChannel: MethodChannel
4241
private var flClashService: BaseServiceInterface? = null
43-
private lateinit var options: VpnOptions
42+
private var options: VpnOptions? = null
43+
private var isBind: Boolean = false
4444
private lateinit var scope: CoroutineScope
4545
private var lastStartForegroundParams: StartForegroundParams? = null
4646
private var timerJob: Job? = null
47+
private val uidPageNameMap = mutableMapOf<Int, String>()
4748

4849
private val connectivity by lazy {
4950
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
5051
}
5152

5253
private val connection = object : ServiceConnection {
5354
override fun onServiceConnected(className: ComponentName, service: IBinder) {
55+
isBind = true
5456
flClashService = when (service) {
5557
is FlClashVpnService.LocalBinder -> service.getService()
5658
is FlClashService.LocalBinder -> service.getService()
@@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
6062
}
6163

6264
override fun onServiceDisconnected(arg: ComponentName) {
65+
isBind = false
6366
flClashService = null
6467
}
6568
}
@@ -90,69 +93,16 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
9093
result.success(true)
9194
}
9295

93-
"setProtect" -> {
94-
val fd = call.argument<Int>("fd")
95-
if (fd != null && flClashService is FlClashVpnService) {
96-
try {
97-
(flClashService as FlClashVpnService).protect(fd)
98-
result.success(true)
99-
} catch (e: RuntimeException) {
100-
result.success(false)
101-
}
102-
} else {
103-
result.success(false)
104-
}
105-
}
106-
107-
"resolverProcess" -> {
108-
val data = call.argument<String>("data")
109-
val process = if (data != null) Gson().fromJson(
110-
data, Process::class.java
111-
) else null
112-
val metadata = process?.metadata
113-
if (metadata == null) {
114-
result.success(null)
115-
return
116-
}
117-
val protocol = metadata.getProtocol()
118-
if (protocol == null) {
119-
result.success(null)
120-
return
121-
}
122-
scope.launch {
123-
withContext(Dispatchers.Default) {
124-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
125-
result.success(null)
126-
return@withContext
127-
}
128-
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
129-
val dst = InetSocketAddress(
130-
metadata.destinationIP.ifEmpty { metadata.host },
131-
metadata.destinationPort
132-
)
133-
val uid = try {
134-
connectivity?.getConnectionOwnerUid(protocol, src, dst)
135-
} catch (_: Exception) {
136-
null
137-
}
138-
if (uid == null || uid == -1) {
139-
result.success(null)
140-
return@withContext
141-
}
142-
val packages =
143-
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
144-
result.success(packages?.first())
145-
}
146-
}
147-
}
148-
14996
else -> {
15097
result.notImplemented()
15198
}
15299
}
153100
}
154101

155102
fun handleStart(options: VpnOptions): Boolean {
103+
if (options.enable != this.options?.enable) {
104+
this.flClashService = null
105+
}
156106
this.options = options
157107
when (options.enable) {
158108
true -> handleStartVpn()
@@ -162,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
162112
}
163113

164114
private fun handleStartVpn() {
165-
GlobalState.getCurrentAppPlugin()
166-
?.requestVpnPermission {
167-
handleStartService()
168-
}
115+
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
116+
handleStartService()
117+
}
169118
}
170119

171120
fun requestGc() {
@@ -235,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
235184
}
236185

237186
private fun startForegroundJob() {
187+
stopForegroundJob()
238188
timerJob = CoroutineScope(Dispatchers.Main).launch {
239189
while (isActive) {
240190
startForeground()
@@ -256,26 +206,58 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
256206
GlobalState.runLock.withLock {
257207
if (GlobalState.runState.value == RunState.START) return
258208
GlobalState.runState.value = RunState.START
259-
val fd = flClashService?.start(options)
260-
flutterMethodChannel.invokeMethod(
261-
"started", fd
209+
val fd = flClashService?.start(options!!)
210+
Core.startTun(
211+
fd = fd ?: 0,
212+
protect = this::protect,
213+
resolverProcess = this::resolverProcess,
262214
)
263-
startForegroundJob();
215+
startForegroundJob()
264216
}
265217
}
266218

219+
private fun protect(fd: Int): Boolean {
220+
return (flClashService as? FlClashVpnService)?.protect(fd) == true
221+
}
222+
223+
private fun resolverProcess(
224+
protocol: Int,
225+
source: InetSocketAddress,
226+
target: InetSocketAddress,
227+
uid: Int,
228+
): String {
229+
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
230+
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
231+
} else {
232+
uid
233+
}
234+
if (nextUid == -1) {
235+
return ""
236+
}
237+
if (!uidPageNameMap.containsKey(nextUid)) {
238+
uidPageNameMap[nextUid] =
239+
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
240+
?.first() ?: ""
241+
}
242+
return uidPageNameMap[nextUid] ?: ""
243+
}
244+
267245
fun handleStop() {
268246
GlobalState.runLock.withLock {
269247
if (GlobalState.runState.value == RunState.STOP) return
270248
GlobalState.runState.value = RunState.STOP
271249
stopForegroundJob()
250+
Core.stopTun()
272251
flClashService?.stop()
273252
GlobalState.handleTryDestroy()
274253
}
275254
}
276255

277256
private fun bindService() {
278-
val intent = when (options.enable) {
257+
if (isBind) {
258+
FlClashApplication.getAppContext().unbindService(connection)
259+
}
260+
val intent = when (options?.enable == true) {
279261
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
280262
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
281263
}

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ subprojects {
2424
}
2525
subprojects {
2626
project.evaluationDependsOn(':app')
27+
project.evaluationDependsOn(':core')
2728
}
2829

2930
tasks.register("clean", Delete) {

0 commit comments

Comments
 (0)