Compare commits

...

24 Commits

Author SHA1 Message Date
chen08209
c9cd80bcb3 Optimize android vpn performance
Add custom primary color and color scheme

Add linux nad windows arm release

Optimize requests and logs page
2025-04-18 16:54:05 +08:00
chen08209
a77b3a35e8 Fix map input page delete issues 2025-04-13 17:32:01 +08:00
chen08209
2d2708d7bd Update changelog 2025-04-08 07:55:45 +00:00
chen08209
ef5f6dbd59 Add rule override
Update core

Optimize more details
2025-04-08 15:35:14 +08:00
chen08209
b6c7b15e3e Update changelog 2025-03-10 10:53:24 +00:00
chen08209
de9c5ba9cc Optimize dashboard performance
Fix some issues
2025-03-10 18:41:42 +08:00
chen08209
2aae00cf68 Fix unselected proxy group delay issues 2025-03-10 18:41:42 +08:00
chen08209
68be2d34a1 Fix asn url issues 2025-03-08 04:22:32 +08:00
chen08209
7895ccf720 Update changelog 2025-03-07 16:03:59 +00:00
chen08209
e92900dbbd Fix tab delay view issues
Fix tray action issues

Fix get profile redirect client ua issues

Fix proxy card delay view issues

Add Russian, Japanese adaptation

Fix some issues
2025-03-07 23:49:27 +08:00
chen08209
eada271c49 Update changelog 2025-03-05 07:22:18 +00:00
chen08209
5dda2854be Fix list form input view issues
Fix traffic view issues
2025-03-05 15:11:19 +08:00
chen08209
5184ed6fc7 Update changelog 2025-03-05 02:36:31 +00:00
chen08209
4e679f776e Optimize performance
Update core

Optimize core stability

Fix linux tun authority check error

Fix some issues
2025-03-05 10:21:51 +08:00
chen08209
96328f66e9 Fix scroll physics error 2025-02-09 16:51:57 +08:00
chen08209
3eb14ab8a1 Update changelog 2025-02-09 08:36:14 +00:00
chen08209
c6266b7917 Add windows storage corruption detection
Fix core crash caused by windows resource manager restart

Optimize logs, requests, access to pages

Fix macos bypass domain issues
2025-02-09 16:23:40 +08:00
chen08209
6c27f2e2f1 Update changelog 2025-02-03 13:28:20 +00:00
chen08209
e04a0094b1 Fix some issues 2025-02-03 21:15:26 +08:00
chen08209
683e6a58ea Update changelog 2025-02-02 11:48:19 +00:00
chen08209
b340feeb49 Update popup menu
Add file editor

Fix android service issues

Optimize desktop background performance

Optimize android main process performance

Optimize delay test

Optimize vpn protect
2025-02-02 19:34:42 +08:00
chen08209
6a39b7ef5a Update changelog 2025-01-10 11:22:18 +00:00
chen08209
35f89fea90 Update core
Fix some issues
2025-01-10 19:08:15 +08:00
chen08209
58acd9c1ab Update changelog 2025-01-09 02:24:30 +00:00
239 changed files with 31448 additions and 16660 deletions

View File

@@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs: jobs:
build: build:
@@ -25,29 +27,27 @@ jobs:
- platform: macos - platform: macos
os: macos-latest os: macos-latest
arch: arm64 arch: arm64
- platform: windows
os: windows-11-arm
arch: arm64
- platform: linux
os: ubuntu-24.04-arm
arch: arm64
steps: steps:
- name: Setup rust
if: startsWith(matrix.os, 'windows-11-arm')
run: |
Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe
.\rustup-init.exe -y --default-toolchain stable
$cargoPath = "$env:USERPROFILE\.cargo\bin"
Add-Content $env:GITHUB_PATH $cargoPath
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Setup JAVA
if: startsWith(matrix.platform,'android')
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 17
- name: Setup NDK
if: startsWith(matrix.platform,'android')
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r26b
add-to-path: true
link-to-sdk: true
- name: Setup Android Signing - name: Setup Android Signing
if: startsWith(matrix.platform,'android') if: startsWith(matrix.platform,'android')
run: | run: |
@@ -56,26 +56,24 @@ jobs:
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 'stable' go-version: '1.24.0'
cache-dependency-path: | cache-dependency-path: |
core/go.sum core/go.sum
- name: Setup Flutter - name: Setup Flutter
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: '3.x' channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
channel: 'stable'
cache: true cache: true
- name: Get Flutter Dependency - name: Get Flutter Dependency
run: flutter pub get run: flutter pub get
- name: Setup - name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -89,14 +87,13 @@ jobs:
needs: [ build ] needs: [ build ]
steps: steps:
- name: Checkout - name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4 uses: actions/checkout@v4
if: ${{ env.IS_STABLE == 'true' }}
with: with:
fetch-depth: 0 fetch-depth: 0
ref: refs/heads/main ref: refs/heads/main
- name: Generate - name: Generate
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate)) tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1) preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
@@ -128,7 +125,7 @@ jobs:
cat NEW_CHANGELOG.md > CHANGELOG.md cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit - name: Commit
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
git add CHANGELOG.md git add CHANGELOG.md
if ! git diff --cached --quiet; then if ! git diff --cached --quiet; then
@@ -207,7 +204,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install requests pip install requests
python release.py python release_telegram.py
- name: Patch release.md - name: Patch release.md
run: | run: |
@@ -215,21 +212,21 @@ jobs:
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Release - name: Release
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./dist/* files: ./dist/*
body_path: './release.md' body_path: './release.md'
- name: Create Fdroid Source Dir - name: Create Fdroid Source Dir
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
run: | run: |
mkdir -p ./tmp mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully" echo "Files copied successfully"
- name: Push to fdroid repo - name: Push to fdroid repo
if: ${{ !contains(github.ref, '+') }} if: ${{ env.IS_STABLE == 'true' }}
uses: cpina/github-action-push-to-another-repository@v1.7.2 uses: cpina/github-action-push-to-another-repository@v1.7.2
env: env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -239,7 +236,7 @@ jobs:
destination-repository-name: FlClash-fdroid-repo destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]' user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com' user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr target-branch: main
commit-message: Update from ${{ github.ref_name }} commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/ target-directory: /tmp/

4
.gitmodules vendored
View File

@@ -1,8 +1,10 @@
[submodule "core/Clash.Meta"] [submodule "core/Clash.Meta"]
path = core/Clash.Meta path = core/Clash.Meta
url = git@github.com:chen08209/Clash.Meta.git url = git@github.com:chen08209/Clash.Meta.git
branch = FlClash-Alpha branch = FlClash
[submodule "plugins/flutter_distributor"] [submodule "plugins/flutter_distributor"]
path = plugins/flutter_distributor path = plugins/flutter_distributor
url = git@github.com:chen08209/flutter_distributor.git url = git@github.com:chen08209/flutter_distributor.git
branch = FlClash branch = FlClash

View File

@@ -1,3 +1,121 @@
## v0.8.81
- Add rule override
- Update core
- Optimize more details
- Update changelog
## v0.8.80
- Optimize dashboard performance
- Fix some issues
- Fix unselected proxy group delay issues
- Fix asn url issues
- Update changelog
## v0.8.79
- Fix tab delay view issues
- Fix tray action issues
- Fix get profile redirect client ua issues
- Fix proxy card delay view issues
- Add Russian, Japanese adaptation
- Fix some issues
- Update changelog
## v0.8.78
- Fix list form input view issues
- Fix traffic view issues
- Update changelog
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74
- Fix some issues
- Update changelog
## v0.8.73
- Update popup menu
- Add file editor
- Fix android service issues
- Optimize desktop background performance
- Optimize android main process performance
- Optimize delay test
- Optimize vpn protect
- Update changelog
## v0.8.72
- Update core
- Fix some issues
- Update changelog
## v0.8.71
- Remake dashboard
- Optimize theme
- Optimize more details
- Update flutter version
- Update changelog
## v0.8.70 ## v0.8.70
- Support better window position memory - Support better window position memory

10
Makefile Normal file
View File

@@ -0,0 +1,10 @@
android_arm64:
dart ./setup.dart android --arch arm64
macos_arm64:
dart ./setup.dart macos --arch arm64
android_app:
dart ./setup.dart android
android_arm64_core:
dart ./setup.dart android --arch arm64 --out core
macos_arm64_core:
dart ./setup.dart macos --arch arm64 --out core

View File

@@ -1,29 +1 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -1,5 +1,3 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins { plugins {
id "com.android.application" id "com.android.application"
id "kotlin-android" id "kotlin-android"
@@ -33,8 +31,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android { android {
namespace "com.follow.clash" namespace "com.follow.clash"
compileSdkVersion 34 compileSdk 35
ndkVersion "27.1.12297006" ndkVersion = "28.0.13004108"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
@@ -48,6 +46,7 @@ android {
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
signingConfigs { signingConfigs {
if (isRelease) { if (isRelease) {
release { release {
@@ -63,7 +62,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.follow.clash" applicationId "com.follow.clash"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 35
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -84,31 +83,15 @@ android {
} }
} }
tasks.register('copyNativeLibs', Copy) {
delete('src/main/jniLibs')
from('../../libclash/android')
into('src/main/jniLibs')
}
tasks.withType(MergeSourceSetFolders).configureEach {
dependsOn copyNativeLibs
}
flutter { flutter {
source '../..' source '../..'
} }
dependencies { dependencies {
implementation project(":core")
implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10' implementation 'com.google.code.gson:gson:2.10.1'
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") { implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude group: "com.google.guava", module: "guava" exclude group: "com.google.guava", module: "guava"
} }
} }
afterEvaluate {
assembleDebug.dependsOn copyNativeLibs
assembleRelease.dependsOn copyNativeLibs
}

View File

@@ -1,13 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<application android:label="FlClash Debug" tools:replace="android:label"> <application
android:icon="@mipmap/ic_launcher"
android:label="FlClash Debug"
tools:replace="android:label">
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
android:label="FlClash Debug" android:label="FlClash Debug"
tools:replace="android:label"> tools:replace="android:label"
</service> tools:targetApi="24" />
</application> </application>
</manifest> </manifest>

View File

@@ -10,20 +10,18 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name="${applicationName}" android:name=".FlClashApplication"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="FlClash"> android:label="FlClash">
@@ -64,9 +62,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- <meta-data--> <meta-data
<!-- android:name="io.flutter.embedding.android.EnableImpeller"--> android:name="io.flutter.embedding.android.EnableImpeller"
<!-- android:value="true" />--> android:value="false" />
<activity <activity
android:name=".TempActivity" android:name=".TempActivity"
@@ -89,7 +87,6 @@
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
android:exported="true" android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name" android:icon="@drawable/ic_stat_name"
android:label="FlClash" android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
@@ -127,7 +124,7 @@
<service <service
android:name=".services.FlClashVpnService" android:name=".services.FlClashVpnService"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse" android:foregroundServiceType="dataSync"
android:permission="android.permission.BIND_VPN_SERVICE"> android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
@@ -140,7 +137,7 @@
<service <service
android:name=".services.FlClashService" android:name=".services.FlClashService"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="dataSync">
<property <property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" /> android:value="service" />

View File

@@ -0,0 +1,18 @@
package com.follow.clash;
import android.app.Application
import android.content.Context
class FlClashApplication : Application() {
companion object {
private lateinit var instance: FlClashApplication
fun getAppContext(): Context {
return instance.applicationContext
}
}
override fun onCreate() {
super.onCreate()
instance = this
}
}

View File

@@ -1,9 +1,7 @@
package com.follow.clash package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
@@ -31,7 +29,7 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
} }
fun getText(text: String): String { suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: "" return getCurrentAppPlugin()?.getText(text) ?: ""
} }
@@ -44,14 +42,14 @@ object GlobalState {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin? return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
} }
fun handleToggle(context: Context) { fun handleToggle() {
val starting = handleStart(context) val starting = handleStart()
if (!starting) { if (!starting) {
handleStop() handleStop()
} }
} }
fun handleStart(context: Context): Boolean { fun handleStart(): Boolean {
if (runState.value == RunState.STOP) { if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING runState.value = RunState.PENDING
runLock.lock() runLock.lock()
@@ -59,7 +57,7 @@ object GlobalState {
if (tilePlugin != null) { if (tilePlugin != null) {
tilePlugin.handleStart() tilePlugin.handleStart()
} else { } else {
initServiceEngine(context) initServiceEngine()
} }
return true return true
} }
@@ -74,6 +72,12 @@ object GlobalState {
} }
} }
fun handleTryDestroy() {
if (flutterEngine == null) {
destroyServiceEngine()
}
}
fun destroyServiceEngine() { fun destroyServiceEngine() {
runLock.withLock { runLock.withLock {
serviceEngine?.destroy() serviceEngine?.destroy()
@@ -81,21 +85,21 @@ object GlobalState {
} }
} }
fun initServiceEngine(context: Context) { fun initServiceEngine() {
if (serviceEngine != null) return if (serviceEngine != null) return
destroyServiceEngine() destroyServiceEngine()
runLock.withLock { runLock.withLock {
serviceEngine = FlutterEngine(context) serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
serviceEngine?.plugins?.add(VpnPlugin()) serviceEngine?.plugins?.add(VpnPlugin)
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin()) serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint( val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"vpnService" "_service"
) )
serviceEngine?.dartExecutor?.executeDartEntrypoint( serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService, vpnService,
if (flutterEngine == null) listOf("quick") else null
) )
} }
} }

View File

@@ -1,10 +1,9 @@
package com.follow.clash package com.follow.clash
import com.follow.clash.core.Core
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@@ -12,8 +11,7 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
flutterEngine.plugins.add(AppPlugin()) flutterEngine.plugins.add(AppPlugin())
flutterEngine.plugins.add(VpnPlugin()) flutterEngine.plugins.add(ServicePlugin)
flutterEngine.plugins.add(ServicePlugin())
flutterEngine.plugins.add(TilePlugin()) flutterEngine.plugins.add(TilePlugin())
GlobalState.flutterEngine = flutterEngine GlobalState.flutterEngine = flutterEngine
} }

View File

@@ -9,7 +9,7 @@ class TempActivity : Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
when (intent.action) { when (intent.action) {
wrapAction("START") -> { wrapAction("START") -> {
GlobalState.handleStart(applicationContext) GlobalState.handleStart()
} }
wrapAction("STOP") -> { wrapAction("STOP") -> {
@@ -17,7 +17,7 @@ class TempActivity : Activity() {
} }
wrapAction("CHANGE") -> { wrapAction("CHANGE") -> {
GlobalState.handleToggle(applicationContext) GlobalState.handleToggle()
} }
} }
finishAndRemoveTask() finishAndRemoveTask()

View File

@@ -97,7 +97,6 @@ fun String.toCIDR(): CIDR {
return CIDR(address, prefixLength) return CIDR(address, prefixLength)
} }
fun ConnectivityManager.resolveDns(network: Network?): List<String> { fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf() val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) } return properties.dnsServers.map { it.asSocketAddressText(53) }
@@ -143,7 +142,6 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
} }
} }
private fun numericToTextFormat(src: ByteArray): String { private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39) val sb = StringBuilder(39)
for (i in 0 until 8) { for (i in 0 until 8) {

View File

@@ -4,5 +4,5 @@ data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, val isSystem: Boolean,
val firstInstallTime: Long, val lastUpdateTime: Long,
) )

View File

@@ -1,7 +1,7 @@
package com.follow.clash.models package com.follow.clash.models
data class Process( data class Process(
val id: Int, val id: String,
val metadata: Metadata, val metadata: Metadata,
) )

View File

@@ -7,6 +7,7 @@ enum class AccessControlMode {
} }
data class AccessControl( data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode, val mode: AccessControlMode,
val acceptList: List<String>, val acceptList: List<String>,
val rejectList: List<String>, val rejectList: List<String>,
@@ -17,7 +18,7 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions( data class VpnOptions(
val enable: Boolean, val enable: Boolean,
val port: Int, val port: Int,
val accessControl: AccessControl?, val accessControl: AccessControl,
val allowBypass: Boolean, val allowBypass: Boolean,
val systemProxy: Boolean, val systemProxy: Boolean,
val bypassDomain: List<String>, val bypassDomain: List<String>,
@@ -25,4 +26,9 @@ data class VpnOptions(
val ipv4Address: String, val ipv4Address: String,
val ipv6Address: String, val ipv6Address: String,
val dnsServerAddress: String, val dnsServerAddress: String,
)
data class StartForegroundParams(
val title: String,
val content: String,
) )

View File

@@ -3,7 +3,6 @@ package com.follow.clash.plugins
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo import android.content.pm.ComponentInfo
@@ -19,6 +18,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.R import com.follow.clash.R
import com.follow.clash.extensions.awaitResult import com.follow.clash.extensions.awaitResult
@@ -37,16 +37,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.lang.ref.WeakReference
import java.util.zip.ZipFile import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var activity: Activity? = null private var activityRef: WeakReference<Activity>? = null
private lateinit var context: Context
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
@@ -121,21 +119,27 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
private fun initShortcuts(label: String) { private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(context, "toggle") val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
.setShortLabel(label) .setShortLabel(label)
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round)) .setIcon(
.setIntent(context.getActionIntent("CHANGE")) IconCompat.createWithResource(
FlClashApplication.getAppContext(),
R.mipmap.ic_launcher_round
)
)
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
.build() .build()
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut)) ShortcutManagerCompat.setDynamicShortcuts(
FlClashApplication.getAppContext(),
listOf(shortcut)
)
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
scope.cancel() scope.cancel()
@@ -143,14 +147,14 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun tip(message: String?) { private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) { if (GlobalState.flutterEngine == null) {
Toast.makeText(context, message, Toast.LENGTH_LONG).show() Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
} }
} }
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"moveTaskToBack" -> { "moveTaskToBack" -> {
activity?.moveTaskToBack(true) activityRef?.get()?.moveTaskToBack(true)
result.success(true) result.success(true)
} }
@@ -192,7 +196,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
if (iconMap["default"] == null) { if (iconMap["default"] == null) {
iconMap["default"] = iconMap["default"] =
context.packageManager?.defaultActivityIcon?.getBase64() FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
} }
result.success(iconMap["default"]) result.success(iconMap["default"])
return@launch return@launch
@@ -221,8 +225,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun openFile(path: String) { private fun openFile(path: String) {
val file = File(path) val file = File(path)
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
context, FlClashApplication.getAppContext(),
"${context.packageName}.fileProvider", "${FlClashApplication.getAppContext().packageName}.fileProvider",
file file
) )
@@ -234,13 +238,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val flags = val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = context.packageManager.queryIntentActivities( val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY intent, PackageManager.MATCH_DEFAULT_ONLY
) )
for (resolveInfo in resInfoList) { for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName val packageName = resolveInfo.activityInfo.packageName
context.grantUriPermission( FlClashApplication.getAppContext().grantUriPermission(
packageName, packageName,
uri, uri,
flags flags
@@ -248,19 +252,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
try { try {
activity?.startActivity(intent) activityRef?.get()?.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
println(e) println(e)
} }
} }
private fun updateExcludeFromRecents(value: Boolean?) { private fun updateExcludeFromRecents(value: Boolean?) {
val am = getSystemService(context, ActivityManager::class.java) val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull { val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activity?.taskId it.taskInfo.taskId == activityRef?.get()?.taskId
} else { } else {
it.taskInfo.id == activity?.taskId it.taskInfo.id == activityRef?.get()?.taskId
} }
} }
@@ -272,7 +276,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private suspend fun getPackageIcon(packageName: String): String? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = context.packageManager val packageManager = FlClashApplication.getAppContext().packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
iconMap[packageName] = try { iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64() packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -285,19 +289,21 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun getPackages(): List<Package> { private fun getPackages(): List<Package> {
val packageManager = context.packageManager val packageManager = FlClashApplication.getAppContext().packageManager
if (packages.isNotEmpty()) return packages if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
it.packageName != context.packageName ?.filter {
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true it.packageName != FlClashApplication.getAppContext().packageName && (
|| it.packageName == "android" it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android"
)
}?.map { }?.map {
Package( Package(
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo?.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
firstInstallTime = it.firstInstallTime lastUpdateTime = it.lastUpdateTime
) )
}?.let { packages.addAll(it) } }?.let { packages.addAll(it) }
return packages return packages
@@ -317,43 +323,45 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
} }
fun requestVpnPermission(context: Context, callBack: () -> Unit) { fun requestVpnPermission(callBack: () -> Unit) {
vpnCallBack = callBack vpnCallBack = callBack
val intent = VpnService.prepare(context) val intent = VpnService.prepare(FlClashApplication.getAppContext())
if (intent != null) { if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return return
} }
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
fun requestNotificationsPermission(context: Context) { fun requestNotificationsPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission( val permission = ContextCompat.checkSelfPermission(
context, FlClashApplication.getAppContext(),
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) )
if (permission != PackageManager.PERMISSION_GRANTED) { if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return if (isBlockNotification) return
if (activity == null) return if (activityRef?.get() == null) return
ActivityCompat.requestPermissions( activityRef?.get()?.let {
activity!!, ActivityCompat.requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS), it,
NOTIFICATION_PERMISSION_REQUEST_CODE arrayOf(Manifest.permission.POST_NOTIFICATIONS),
) NOTIFICATION_PERMISSION_REQUEST_CODE
return )
return
}
} }
} }
} }
fun getText(text: String): String? { suspend fun getText(text: String): String? {
return runBlocking { return withContext(Dispatchers.Default) {
channel.awaitResult<String>("getText", text) channel.awaitResult<String>("getText", text)
} }
} }
private fun isChinaPackage(packageName: String): Boolean { private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
skipPrefixList.forEach { skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false if (packageName == it || packageName.startsWith("$it.")) return false
} }
@@ -373,7 +381,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()) PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
) )
} else { } else {
@Suppress("DEPRECATION") packageManager.getPackageInfo( packageManager.getPackageInfo(
packageName, packageManagerFlags packageName, packageManagerFlags
) )
} }
@@ -385,31 +393,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}.forEach { }.forEach {
if (it.name.matches(chinaAppRegex)) return true if (it.name.matches(chinaAppRegex)) return true
} }
ZipFile(File(packageInfo.applicationInfo.publicSourceDir)).use { packageInfo.applicationInfo?.publicSourceDir?.let {
for (packageEntry in it.entries()) { ZipFile(File(it)).use {
if (packageEntry.name.startsWith("firebase-")) return false for (packageEntry in it.entries()) {
} if (packageEntry.name.startsWith("firebase-")) return false
for (packageEntry in it.entries()) {
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
".dex"
))
) {
continue
} }
if (packageEntry.size > 15000000) { for (packageEntry in it.entries()) {
return true if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
} ".dex"
val input = it.getInputStream(packageEntry).buffered() ))
val dexFile = try { ) {
DexBackedDexFile.fromInputStream(null, input) continue
} catch (e: Exception) { }
return false if (packageEntry.size > 15000000) {
} return true
for (clazz in dexFile.classes) { }
val clazzName = val input = it.getInputStream(packageEntry).buffered()
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".") val dexFile = try {
.replace("$", ".") DexBackedDexFile.fromInputStream(null, input)
if (clazzName.matches(chinaAppRegex)) return true } catch (e: Exception) {
return false
}
for (clazz in dexFile.classes) {
val clazzName =
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
.replace("$", ".")
if (clazzName.matches(chinaAppRegex)) return true
}
} }
} }
} }
@@ -420,28 +430,28 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activityRef = WeakReference(binding.activity)
binding.addActivityResultListener(::onActivityResult) binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activity = null activityRef = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity activityRef = WeakReference(binding.activity)
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
activity = null activityRef = null
} }
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) { if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) { if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine(context) GlobalState.initServiceEngine()
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
} }

View File

@@ -1,20 +1,18 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import android.content.Context
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions
import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service") flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
flutterMethodChannel.setMethodCallHandler(this) flutterMethodChannel.setMethodCallHandler(this)
} }
@@ -24,9 +22,22 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
"startVpn" -> {
val data = call.argument<String>("data")
val options = Gson().fromJson(data, VpnOptions::class.java)
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
result.success(true)
}
"stopVpn" -> {
GlobalState.getCurrentVPNPlugin()?.handleStop()
result.success(true)
}
"init" -> { "init" -> {
GlobalState.getCurrentAppPlugin()?.requestNotificationsPermission(context) GlobalState.getCurrentAppPlugin()
GlobalState.initServiceEngine(context) ?.requestNotificationsPermission()
GlobalState.initServiceEngine()
result.success(true) result.success(true)
} }
@@ -41,7 +52,6 @@ class ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun handleDestroy() { private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.stop()
GlobalState.destroyServiceEngine() GlobalState.destroyServiceEngine()
} }
} }

View File

@@ -1,14 +1,13 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin, class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
} }
fun handleStart() { fun handleStart() {
onStart?.let { it() }
channel.invokeMethod("start", null) channel.invokeMethod("start", null)
} }
fun handleStop() { fun handleStop() {
channel.invokeMethod("stop", null) channel.invokeMethod("stop", null)
onStop?.let { it() }
} }
private fun handleDetached() { private fun handleDetached() {

View File

@@ -11,13 +11,15 @@ import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.follow.clash.BaseServiceInterface import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.extensions.getProtocol import com.follow.clash.core.Core
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.resolveDns import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface
import com.follow.clash.services.FlClashService import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson import com.google.gson.Gson
@@ -26,41 +28,47 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.InetSocketAddress import java.net.InetSocketAddress
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel private lateinit var flutterMethodChannel: MethodChannel
private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null private var flClashService: BaseServiceInterface? = null
private lateinit var options: VpnOptions private var options: VpnOptions? = null
private var isBind: Boolean = false
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null
private val uidPageNameMap = mutableMapOf<Int, String>()
private val connectivity by lazy { private val connectivity by lazy {
context.getSystemService<ConnectivityManager>() FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
} }
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
isBind = true
flClashService = when (service) { flClashService = when (service) {
is FlClashVpnService.LocalBinder -> service.getService() is FlClashVpnService.LocalBinder -> service.getService()
is FlClashService.LocalBinder -> service.getService() is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder") else -> throw Exception("invalid binder")
} }
start() handleStartService()
} }
override fun onServiceDisconnected(arg: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
isBind = false
flClashService = null flClashService = null
} }
} }
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext
scope.launch { scope.launch {
registerNetworkCallback() registerNetworkCallback()
} }
@@ -77,88 +85,35 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
when (call.method) { when (call.method) {
"start" -> { "start" -> {
val data = call.argument<String>("data") val data = call.argument<String>("data")
options = Gson().fromJson(data, VpnOptions::class.java) result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
when (options.enable) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
} }
"stop" -> { "stop" -> {
stop() handleStop()
result.success(true) result.success(true)
} }
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null) {
if (flClashService is FlClashVpnService) {
(flClashService as FlClashVpnService).protect(fd)
}
result.success(true)
} else {
result.success(false)
}
}
"startForeground" -> {
val title = call.argument<String>("title") as String
val content = call.argument<String>("content") as String
startForeground(title, content)
result.success(true)
}
"resolverProcess" -> {
val data = call.argument<String>("data")
val process = if (data != null) Gson().fromJson(
data, Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
return
}
val protocol = metadata.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages = context.packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
} }
} }
fun handleStart(options: VpnOptions): Boolean {
if (options.enable != this.options?.enable) {
this.flClashService = null
}
this.options = options
when (options.enable) {
true -> handleStartVpn()
false -> handleStartService()
}
return true
}
private fun handleStartVpn() { private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) { GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
start() handleStartService()
} }
} }
@@ -177,16 +132,6 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
flutterMethodChannel.invokeMethod("dnsChanged", dns) flutterMethodChannel.invokeMethod("dnsChanged", dns)
} }
} }
// if (flClashService is FlClashVpnService) {
// val network = networks.maxByOrNull { net ->
// connectivity?.getNetworkCapabilities(net)?.let { cap ->
// TRANSPORT_PRIORITY.indexOfFirst { cap.hasTransport(it) }
// } ?: -1
// }
// network?.let {
// (flClashService as FlClashVpnService).updateUnderlyingNetworks(arrayOf(network))
// }
// }
} }
private val callback = object : ConnectivityManager.NetworkCallback() { private val callback = object : ConnectivityManager.NetworkCallback() {
@@ -218,14 +163,42 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
onUpdateNetwork() onUpdateNetwork()
} }
private fun startForeground(title: String, content: String) { private suspend fun startForeground() {
GlobalState.runLock.withLock { GlobalState.runLock.lock()
try {
if (GlobalState.runState.value != RunState.START) return if (GlobalState.runState.value != RunState.START) return
flClashService?.startForeground(title, content) val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
val startForegroundParams = Gson().fromJson(
data, StartForegroundParams::class.java
)
if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams
flClashService?.startForeground(
startForegroundParams.title,
startForegroundParams.content,
)
}
} finally {
GlobalState.runLock.unlock()
} }
} }
private fun start() { private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
startForeground()
delay(1000)
}
}
}
private fun stopForegroundJob() {
timerJob?.cancel()
timerJob = null
}
private fun handleStartService() {
if (flClashService == null) { if (flClashService == null) {
bindService() bindService()
return return
@@ -233,28 +206,61 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options) val fd = flClashService?.start(options!!)
flutterMethodChannel.invokeMethod( Core.startTun(
"started", fd fd = fd ?: 0,
protect = this::protect,
resolverProcess = this::resolverProcess,
) )
startForegroundJob()
} }
} }
fun stop() { private fun protect(fd: Int): Boolean {
return (flClashService as? FlClashVpnService)?.protect(fd) == true
}
private fun resolverProcess(
protocol: Int,
source: InetSocketAddress,
target: InetSocketAddress,
uid: Int,
): String {
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
} else {
uid
}
if (nextUid == -1) {
return ""
}
if (!uidPageNameMap.containsKey(nextUid)) {
uidPageNameMap[nextUid] =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
?.first() ?: ""
}
return uidPageNameMap[nextUid] ?: ""
}
fun handleStop() {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP GlobalState.runState.value = RunState.STOP
stopForegroundJob()
Core.stopTun()
flClashService?.stop() flClashService?.stop()
GlobalState.handleTryDestroy()
} }
GlobalState.destroyServiceEngine()
} }
private fun bindService() { private fun bindService() {
val intent = when (options.enable) { if (isBind) {
true -> Intent(context, FlClashVpnService::class.java) FlClashApplication.getAppContext().unbindService(connection)
false -> Intent(context, FlClashService::class.java)
} }
context.bindService(intent, connection, Context.BIND_AUTO_CREATE) val intent = when (options?.enable == true) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
}
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
} }

View File

@@ -1,10 +1,12 @@
package com.follow.clash package com.follow.clash.services
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
interface BaseServiceInterface { interface BaseServiceInterface {
fun start(options: VpnOptions): Int fun start(options: VpnOptions): Int
fun stop() fun stop()
fun startForeground(title: String, content: String)
suspend fun startForeground(title: String, content: String)
} }

View File

@@ -7,19 +7,19 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.async
class FlClashService : Service(), BaseServiceInterface { class FlClashService : Service(), BaseServiceInterface {
@@ -42,45 +42,56 @@ class FlClashService : Service(), BaseServiceInterface {
private val notificationId: Int = 1 private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
val intent = Intent(this, MainActivity::class.java) CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { val intent = Intent(
PendingIntent.getActivity( this@FlClashService, MainActivity::class.java
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
} else {
PendingIntent.getActivity( val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
this, PendingIntent.getActivity(
0, this@FlClashService,
intent, 0,
PendingIntent.FLAG_UPDATE_CURRENT intent,
) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} )
with(NotificationCompat.Builder(this, CHANNEL)) { } else {
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name) PendingIntent.getActivity(
setContentTitle("FlClash") this@FlClashService,
setContentIntent(pendingIntent) 0,
setCategory(NotificationCompat.CATEGORY_SERVICE) intent,
priority = NotificationCompat.PRIORITY_MIN PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { )
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE }
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
addAction(
0,
stopText, // 使用 suspend 函数获取的文本
getActionPendingIntent("STOP")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
addAction(
0,
GlobalState.getText("stop"),
getActionPendingIntent("STOP")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
} }
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
override fun start(options: VpnOptions) = 0 override fun start(options: VpnOptions) = 0
override fun stop() { override fun stop() {
@@ -90,25 +101,30 @@ class FlClashService : Service(), BaseServiceInterface {
} }
} }
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) { @SuppressLint("ForegroundServiceType")
CoroutineScope(Dispatchers.Default).launch { override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL) var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) { if (channel == null) {
channel = channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel) manager?.createNotificationChannel(channel)
}
} }
val notification = }
notificationBuilder.setContentTitle(title).setContentText(content).build() val notification =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { getNotificationBuilder()
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) .setContentTitle(title)
} else { .setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
} else {
startForeground(notificationId, notification)
} }
} }
} }

View File

@@ -66,7 +66,7 @@ class FlClashTileService : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
activityTransfer() activityTransfer()
GlobalState.handleToggle(applicationContext) GlobalState.handleToggle()
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -6,8 +6,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.net.Network
import android.net.ProxyInfo import android.net.ProxyInfo
import android.net.VpnService import android.net.VpnService
import android.os.Binder import android.os.Binder
@@ -17,7 +16,6 @@ import android.os.Parcel
import android.os.RemoteException import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R import com.follow.clash.R
@@ -28,14 +26,16 @@ import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FlClashVpnService : VpnService(), BaseServiceInterface { class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
GlobalState.initServiceEngine(applicationContext) GlobalState.initServiceEngine()
} }
override fun start(options: VpnOptions): Int { override fun start(options: VpnOptions): Int {
@@ -45,9 +45,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress() val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) { if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i -> try {
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}") routeAddress.forEach { i ->
addRoute(i.address, i.prefixLength) Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
} }
} else { } else {
addRoute("0.0.0.0", 0) addRoute("0.0.0.0", 0)
@@ -58,9 +65,16 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress() val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) { if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i -> try {
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}") routeAddress.forEach { i ->
addRoute(i.address, i.prefixLength) Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
} }
} else { } else {
addRoute("::", 0) addRoute("::", 0)
@@ -68,17 +82,19 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
options.accessControl?.let { accessControl -> options.accessControl.let { accessControl ->
when (accessControl.mode) { if (accessControl.enable) {
AccessControlMode.acceptSelected -> { when (accessControl.mode) {
(accessControl.acceptList + packageName).forEach { AccessControlMode.acceptSelected -> {
addAllowedApplication(it) (accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
} }
}
AccessControlMode.rejectSelected -> { AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach { (accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it) addDisallowedApplication(it)
}
} }
} }
} }
@@ -105,12 +121,6 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
fun updateUnderlyingNetworks(networks: Array<Network>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
this.setUnderlyingNetworks(networks)
}
}
override fun stop() { override fun stop() {
stopSelf() stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -122,68 +132,77 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1 private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy { private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
val intent = Intent(this, MainActivity::class.java) CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity( PendingIntent.getActivity(
this, this@FlClashVpnService,
0, 0,
intent, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
} else { } else {
PendingIntent.getActivity( PendingIntent.getActivity(
this, this@FlClashVpnService,
0, 0,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
} }
with(NotificationCompat.Builder(this, CHANNEL)) { with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
setSmallIcon(R.drawable.ic_stat_name) setSmallIcon(R.drawable.ic_stat_name)
setContentTitle("FlClash") setContentTitle("FlClash")
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MIN priority = NotificationCompat.PRIORITY_MIN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0,
stopText,
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
setOngoing(true)
addAction(
0,
GlobalState.getText("stop"),
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
} }
@SuppressLint("ForegroundServiceType", "WrongConstant") private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
override fun startForeground(title: String, content: String) { return notificationBuilderDeferred.await()
CoroutineScope(Dispatchers.Default).launch { }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) @SuppressLint("ForegroundServiceType")
var channel = manager?.getNotificationChannel(CHANNEL) override suspend fun startForeground(title: String, content: String) {
if (channel == null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channel = val manager = getSystemService(NotificationManager::class.java)
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW) var channel = manager?.getNotificationChannel(CHANNEL)
manager?.createNotificationChannel(channel) if (channel == null) {
} channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
} }
val notification = }
notificationBuilder val notification =
.setContentTitle(title) getNotificationBuilder()
.setContentText(content) .setContentTitle(title)
.build() .setContentText(content)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { .build()
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
} else { try {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} catch (_: Exception) {
startForeground(notificationId, notification) startForeground(notificationId, notification)
} }
} else {
startForeground(notificationId, notification)
} }
} }

View File

@@ -24,6 +24,7 @@ subprojects {
} }
subprojects { subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
project.evaluationDependsOn(':core')
} }
tasks.register("clean", Delete) { tasks.register("clean", Delete) {

1
android/core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,65 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.follow.clash.core"
compileSdk = 35
ndkVersion = "28.0.13004108"
defaultConfig {
minSdk = 21
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
}
}
externalNativeBuild {
cmake {
path("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
val copyNativeLibs by tasks.register<Copy>("copyNativeLibs") {
doFirst {
delete("src/main/jniLibs")
}
from("../../libclash/android")
into("src/main/jniLibs")
}
afterEvaluate {
tasks.named("preBuild") {
dependsOn(copyNativeLibs)
}
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
}

View File

21
android/core/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,45 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
add_compile_options(-O3)
add_compile_options(-flto)
add_compile_options(-g0)
add_compile_options(-ffunction-sections -fdata-sections)
add_compile_options(-fno-exceptions -fno-rtti)
add_link_options(
-flto
-Wl,--gc-sections
-Wl,--strip-all
-Wl,--exclude-libs=ALL
)
endif ()
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
message("LIB_CLASH_PATH ${LIB_CLASH_PATH}")
if (EXISTS ${LIB_CLASH_PATH})
message("Found libclash.so for ABI ${ANDROID_ABI}")
add_compile_definitions(LIBCLASH)
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
clash)
else ()
message("Not found libclash.so for ABI ${ANDROID_ABI}")
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})
endif ()

View File

@@ -0,0 +1,75 @@
#include <jni.h>
#ifdef LIBCLASH
#include <jni.h>
#include <string>
#include "jni_helper.h"
#include "libclash.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
auto interface = new_global(cb);
startTUN(fd, interface);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
stopTun();
}
static jmethodID m_tun_interface_protect;
static jmethodID m_tun_interface_resolve_process;
static void release_jni_object_impl(void *obj) {
ATTACH_JNI();
del_global((jobject) obj);
}
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
ATTACH_JNI();
env->CallVoidMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_protect,
(jint) fd);
}
static const char*
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
const char *source,
const char *target,
int uid) {
ATTACH_JNI();
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
(jmethodID) m_tun_interface_resolve_process,
(jint) protocol,
(jstring) new_string(source),
(jstring) new_string(target),
(jint) uid);
return get_string(packageName);
}
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
initialize_jni(vm, env);
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
registerCallbacks(&call_tun_interface_protect_impl,
&call_tun_interface_resolve_process_impl,
&release_jni_object_impl);
return JNI_VERSION_1_6;
}
#endif

View File

@@ -0,0 +1,70 @@
#include "jni_helper.h"
#include <malloc.h>
#include <cstring>
static JavaVM *global_vm;
static jclass c_string;
static jmethodID m_new_string;
static jmethodID m_get_bytes;
void initialize_jni(JavaVM *vm, JNIEnv *env) {
global_vm = vm;
c_string = (jclass) new_global(find_class("java/lang/String"));
m_new_string = find_method(c_string, "<init>", "([B)V");
m_get_bytes = find_method(c_string, "getBytes", "()[B");
}
JavaVM *global_java_vm() {
return global_vm;
}
char *jni_get_string(JNIEnv *env, jstring str) {
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
int length = env->GetArrayLength(array);
char *content = (char *) malloc(length + 1);
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
auto length = (int) strlen(str);
jbyteArray array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
return (jstring) env->NewObject(c_string, m_new_string, array);
}
int jni_catch_exception(JNIEnv *env) {
int result = env->ExceptionCheck();
if (result) {
env->ExceptionDescribe();
env->ExceptionClear();
}
return result;
}
void jni_attach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
jni->require_release = 0;
return;
}
if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) {
abort();
}
jni->require_release = 1;
}
void jni_detach_thread(struct scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (jni->require_release) {
vm->DetachCurrentThread();
}
}
void release_string(char **str) {
free(*str);
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <jni.h>
#include <cstdint>
#include <cstdlib>
#include <malloc.h>
struct scoped_jni {
JNIEnv *env;
int require_release;
};
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
extern jstring jni_new_string(JNIEnv *env, const char *str);
extern char *jni_get_string(JNIEnv *env, jstring str);
extern int jni_catch_exception(JNIEnv *env);
extern void jni_attach_thread(struct scoped_jni *jni);
extern void jni_detach_thread(struct scoped_jni *env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
struct scoped_jni _jni; \
jni_attach_thread(&_jni); \
JNIEnv *env = _jni.env
#define scoped_string __attribute__((cleanup(release_string))) char*
#define find_class(name) env->FindClass(name)
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
#define new_global(obj) env->NewGlobalRef(obj)
#define del_global(obj) env->DeleteGlobalRef(obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)

View File

@@ -0,0 +1,50 @@
package com.follow.clash.core
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.URL
data object Core {
private external fun startTun(
fd: Int,
cb: TunInterface
)
private fun parseInetSocketAddress(address: String): InetSocketAddress {
val url = URL("https://$address")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}
fun startTun(
fd: Int,
protect: (Int) -> Boolean,
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String
) {
startTun(fd, object : TunInterface {
override fun protect(fd: Int) {
protect(fd)
}
override fun resolverProcess(
protocol: Int,
source: String,
target: String,
uid: Int
): String {
return resolverProcess(
protocol,
parseInetSocketAddress(source),
parseInetSocketAddress(target),
uid,
)
}
});
}
external fun stopTun()
init {
System.loadLibrary("core")
}
}

View File

@@ -0,0 +1,9 @@
package com.follow.clash.core
import androidx.annotation.Keep
@Keep
interface TunInterface {
fun protect(fd: Int)
fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String
}

View File

@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
kotlin_version=1.9.22 kotlin_version=1.9.22
agp_version=8.2.1 agp_version=8.9.1

View File

@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip

View File

@@ -24,3 +24,4 @@ plugins {
} }
include ":app" include ":app"
include ':core'

Binary file not shown.

Binary file not shown.

View File

@@ -5,6 +5,7 @@ targets:
options: options:
build_extensions: build_extensions:
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart' '^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
freezed: freezed:
options: options:
build_extensions: build_extensions:

View File

@@ -1,32 +1,177 @@
//go:build !cgo
package main package main
import ( import (
"encoding/json" "encoding/json"
) )
func (action Action) Json() ([]byte, error) { type Action struct {
data, err := json.Marshal(action) Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
DefaultValue interface{} `json:"default-value"`
}
type ActionResult struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
}
func (result ActionResult) Json() ([]byte, error) {
data, err := json.Marshal(result)
return data, err return data, err
} }
func (action Action) callback(data interface{}) bool { func (action Action) getResult(data interface{}) []byte {
if conn == nil { resultAction := ActionResult{
return false
}
sendAction := Action{
Id: action.Id, Id: action.Id,
Method: action.Method, Method: action.Method,
Data: data, Data: data,
} }
res, err := sendAction.Json() res, _ := resultAction.Json()
if err != nil { return res
return false }
}
_, err = conn.Write(append(res, []byte("\n")...)) func handleAction(action *Action, result func(data interface{})) {
if err != nil { switch action.Method {
return false case initClashMethod:
} paramsString := action.Data.(string)
return true result(handleInitClash(paramsString))
return
case getIsInitMethod:
result(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
result(true)
return
case shutdownMethod:
result(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
result(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
result(handleUpdateConfig(data))
return
case getProxiesMethod:
result(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
result(value)
})
return
case getTrafficMethod:
result(handleGetTraffic())
return
case getTotalTrafficMethod:
result(handleGetTotalTraffic())
return
case resetTrafficMethod:
handleResetTraffic()
result(true)
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
result(value)
})
return
case getConnectionsMethod:
result(handleGetConnections())
return
case closeConnectionsMethod:
result(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
result(handleCloseConnection(id))
return
case getExternalProvidersMethod:
result(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
result(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
result(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
result(value)
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
result(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
result(value)
})
return
case startLogMethod:
handleStartLog()
result(true)
return
case stopLogMethod:
handleStopLog()
result(true)
return
case startListenerMethod:
result(handleStartListener())
return
case stopListenerMethod:
result(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
result(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
result(value)
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
})
return
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
result(true)
default:
handle := nextHandle(action, result)
if handle {
return
} else {
result(action.DefaultValue)
}
}
} }

77
core/android_bride.go Normal file
View File

@@ -0,0 +1,77 @@
//go:build android && cgo
package main
/*
#include <stdlib.h>
typedef void (*release_object_func)(void *obj);
typedef void (*protect_func)(void *tun_interface, int fd);
typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
static void protect(protect_func fn, void *tun_interface, int fd) {
if (fn) {
fn(tun_interface, fd);
}
}
static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) {
if (fn) {
return fn(tun_interface, protocol, source, target, uid);
}
return "";
}
static void release_object(release_object_func fn, void *obj) {
if (fn) {
return fn(obj);
}
}
*/
import "C"
import (
"unsafe"
)
var (
globalCallbacks struct {
releaseObjectFunc C.release_object_func
protectFunc C.protect_func
resolveProcessFunc C.resolve_process_func
}
)
func protect(callback unsafe.Pointer, fd int) {
if globalCallbacks.protectFunc != nil {
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
}
}
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
if globalCallbacks.resolveProcessFunc == nil {
return ""
}
s := C.CString(source)
defer C.free(unsafe.Pointer(s))
t := C.CString(target)
defer C.free(unsafe.Pointer(t))
res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid))
defer C.free(unsafe.Pointer(res))
return C.GoString(res)
}
func releaseObject(callback unsafe.Pointer) {
if globalCallbacks.releaseObjectFunc == nil {
return
}
C.release_object(globalCallbacks.releaseObjectFunc, callback)
}
//export registerCallbacks
func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) {
globalCallbacks.protectFunc = markSocketFunc
globalCallbacks.resolveProcessFunc = resolveProcessFunc
globalCallbacks.releaseObjectFunc = releaseObjectFunc
}

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -25,15 +24,27 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"sync" "sync"
) )
func splitByMultipleSeparators(s string) interface{} {
isSeparator := func(r rune) bool {
return r == ',' || r == ' ' || r == ';'
}
parts := strings.FieldsFunc(s, isSeparator)
if len(parts) > 1 {
return parts
}
return s
}
var ( var (
version = 0
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
@@ -43,11 +54,6 @@ func (a ExternalProviders) Len() int { return len(a) }
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name } func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func readFile(path string) ([]byte, error) { func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
@@ -86,16 +92,6 @@ func getRawConfigWithId(id string) *config.RawConfig {
continue continue
} }
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value) mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
if configParams.TestURL != nil {
if mapping["health-check"] != nil {
hc := mapping["health-check"].(map[string]any)
if hc != nil {
if hc["url"] != nil {
hc["url"] = *configParams.TestURL
}
}
}
}
} }
for _, mapping := range prof.RuleProvider { for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string) value, exist := mapping["path"].(string)
@@ -177,9 +173,19 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof return prof
} }
func genHosts(hosts, patchHosts map[string]any) { func attachHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts { for k, v := range patchHosts {
hosts[k] = v if str, ok := v.(string); ok {
hosts[k] = splitByMultipleSeparators(str)
}
}
}
func updatePatchDns(dns config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
}
} }
} }
@@ -190,26 +196,25 @@ func trimArr(arr []string) (r []string) {
return return
} }
func overrideRules(rules *[]string) { func overrideRules(rules, patchRules []string) []string {
var target = "" target := ""
for _, line := range *rules { for _, line := range rules {
rule := trimArr(strings.Split(line, ",")) rule := trimArr(strings.Split(line, ","))
l := len(rule) if len(rule) != 2 {
if l != 2 { continue
return
} }
if strings.ToUpper(rule[0]) == "MATCH" { if strings.EqualFold(rule[0], "MATCH") {
target = rule[1] target = rule[1]
break break
} }
} }
if target == "" { if target == "" {
return return rules
} }
var rulesExt = lo.Map(ips, func(ip string, index int) string { rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target) return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
}) })
*rules = append(rulesExt, *rules...) return append(append(rulesExt, patchRules...), rules...)
} }
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -232,6 +237,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Tun.Device = patchConfig.Tun.Device targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl
@@ -242,15 +248,20 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
for idx := range targetConfig.ProxyGroup { for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = "" targetConfig.ProxyGroup[idx]["url"] = ""
} }
genHosts(targetConfig.Hosts, patchConfig.Hosts) attachHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns { if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} else { } else {
if targetConfig.DNS.Enable == false { if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true targetConfig.DNS.Enable = true
} }
} }
overrideRules(&targetConfig.Rule) if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
} }
func patchConfig() { func patchConfig() {
@@ -352,8 +363,6 @@ func applyConfig(rawConfig *config.RawConfig) error {
if configParams.IsPatch { if configParams.IsPatch {
patchConfig() patchConfig()
} else { } else {
handleCloseConnectionsUnLock()
runtime.GC()
hub.ApplyConfig(currentConfig) hub.ApplyConfig(currentConfig)
patchSelectGroup() patchSelectGroup()
} }

View File

@@ -1,18 +1,24 @@
package main package main
import ( import (
"encoding/json"
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"time" "time"
) )
type InitParams struct {
HomeDir string `json:"home-dir"`
Version int `json:"version"`
}
type ConfigExtendedParams struct { type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"` IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"` IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"` SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"` TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"` OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
} }
type GenerateConfigParams struct { type GenerateConfigParams struct {
@@ -28,14 +34,10 @@ type ChangeProxyParams struct {
type TestDelayParams struct { type TestDelayParams struct {
ProxyName string `json:"proxy-name"` ProxyName string `json:"proxy-name"`
TestUrl string `json:"test-url"`
Timeout int64 `json:"timeout"` Timeout int64 `json:"timeout"`
} }
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type ExternalProvider struct { type ExternalProvider struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
@@ -74,19 +76,20 @@ const (
stopLogMethod Method = "stopLog" stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener" startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener" stopListenerMethod Method = "stopListener"
updateDnsMethod Method = "updateDns"
setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
) )
type Method string type Method string
type Action struct {
Id string `json:"id"`
Method Method `json:"method"`
Data interface{} `json:"data"`
}
type MessageType string type MessageType string
type Delay struct { type Delay struct {
Url string `json:"url"`
Name string `json:"name"` Name string `json:"name"`
Value int32 `json:"value"` Value int32 `json:"value"`
} }
@@ -96,17 +99,14 @@ type Message struct {
Data interface{} `json:"data"` Data interface{} `json:"data"`
} }
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
const ( const (
LogMessage MessageType = "log" LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay" DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request" RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded" LoadedMessage MessageType = "loaded"
) )
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}

View File

@@ -1,21 +1,14 @@
module core module core
go 1.21 go 1.20
replace github.com/metacubex/mihomo => ./Clash.Meta replace github.com/metacubex/mihomo => ./Clash.Meta
require github.com/metacubex/mihomo v1.17.1
require ( require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/samber/lo v1.49.1
github.com/sagernet/cors v1.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
) )
replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297
require ( require (
github.com/3andne/restls-client-go v0.1.6 // indirect github.com/3andne/restls-client-go v0.1.6 // indirect
github.com/RyuaNerin/go-krypto v1.2.4 // indirect github.com/RyuaNerin/go-krypto v1.2.4 // indirect
@@ -26,49 +19,55 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/enfein/mieru/v3 v3.13.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect github.com/gofrs/uuid/v5 v5.3.1 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.19.0 // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect github.com/metacubex/chacha v0.1.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 // indirect github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 // indirect github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 // indirect github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.8-alpha.4 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -76,19 +75,18 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.0-alpha.13 // indirect github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/samber/lo v1.47.0 github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -97,22 +95,24 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.28.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect; indirect` gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
) )

View File

@@ -24,14 +24,17 @@ github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
@@ -40,12 +43,11 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -57,33 +59,32 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -96,48 +97,50 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
github.com/metacubex/chacha v0.1.0/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4 h1:CgdUBRxmNlxEGkp35HwvgQ10jwOOUJKWdOxpi8yWi8o= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
github.com/metacubex/quic-go v0.47.1-0.20240909010619-6b38f24bfcc4/go.mod h1:Y7yRGqFE6UQL/3aKPYmiYdjfVkeujJaStP4+jiZMcN8= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297 h1:YG/JkwGPbca5rUtEMHIu8ZuqzR7BSVm1iqY8hNoMeMA= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/sing v0.0.0-20240724044459-6f3cf5896297/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1 h1:ypfofGDZbP8p3Y4P/m74JYu7sQViesi3c8nbmT6cS0Y= github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
github.com/metacubex/sing-tun v0.2.7-0.20240729131039-ed03f557dee1/go.mod h1:olbEx9yVcaw5tHTNlRamRoxmMKcvDvcVS1YLnQGzvWE= github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3 h1:xg71VmzLS6ByAzi/57phwDvjE+dLLs+ozH00k4DnOns= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20240924052438-b0976fc59ea3/go.mod h1:6nitcmzPDL3MXnLdhu6Hm126Zk4S1fBbX3P7jxUxSFw= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
@@ -152,8 +155,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
@@ -166,20 +169,19 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -189,17 +191,10 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -229,8 +224,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -239,11 +234,11 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -255,20 +250,18 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
@@ -276,8 +269,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"core/state"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
@@ -33,9 +34,15 @@ var (
currentConfig *config.Config currentConfig *config.Config
) )
func handleInitClash(homeDirStr string) bool { func handleInitClash(paramsString string) bool {
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
return false
}
version = params.Version
if !isInit { if !isInit {
constant.SetHomeDir(homeDirStr) constant.SetHomeDir(params.HomeDir)
isInit = true isInit = true
} }
return isInit return isInit
@@ -149,8 +156,8 @@ func handleChangeProxy(data string, fn func(string string)) {
}() }()
} }
func handleGetTraffic(onlyProxy bool) string { func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(onlyProxy) up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -163,8 +170,8 @@ func handleGetTraffic(onlyProxy bool) string {
return string(data) return string(data)
} }
func handleGetTotalTraffic(onlyProxy bool) string { func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(onlyProxy) up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -177,6 +184,15 @@ func handleGetTotalTraffic(onlyProxy bool) string {
return string(data) return string(data)
} }
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() { func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic() statistic.DefaultManager.ResetStatistic()
} }
@@ -213,7 +229,14 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
return false, nil return false, nil
} }
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus) testUrl := constant.DefaultTestURL
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 { if err != nil || delay == 0 {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
@@ -240,17 +263,6 @@ func handleGetConnections() string {
return string(data) return string(data)
} }
func handleCloseConnectionsUnLock() bool {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
return false
}
return true
})
return true
}
func handleCloseConnections() bool { func handleCloseConnections() bool {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
@@ -395,7 +407,7 @@ func handleStartLog() {
Type: LogMessage, Type: LogMessage,
Data: logData, Data: logData,
} }
SendMessage(*message) sendMessage(*message)
} }
}() }()
} }
@@ -426,9 +438,14 @@ func handleGetMemory(fn func(value string)) {
}() }()
} }
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func init() { func init() {
adapter.UrlTestHook = func(name string, delay uint16) { adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{
Url: url,
Name: name, Name: name,
} }
if delay == 0 { if delay == 0 {
@@ -436,19 +453,19 @@ func init() {
} else { } else {
delayData.Value = int32(delay) delayData.Value = int32(delay)
} }
SendMessage(Message{ sendMessage(Message{
Type: DelayMessage, Type: DelayMessage,
Data: delayData, Data: delayData,
}) })
} }
statistic.DefaultRequestNotify = func(c statistic.Tracker) { statistic.DefaultRequestNotify = func(c statistic.Tracker) {
SendMessage(Message{ sendMessage(Message{
Type: RequestMessage, Type: RequestMessage,
Data: c, Data: c,
}) })
} }
executor.DefaultProviderLoadedHook = func(providerName string) { executor.DefaultProviderLoadedHook = func(providerName string) {
SendMessage(Message{ sendMessage(Message{
Type: LoadedMessage, Type: LoadedMessage,
Data: providerName, Data: providerName,
}) })

View File

@@ -8,18 +8,30 @@ package main
import "C" import "C"
import ( import (
bridge "core/dart-bridge" bridge "core/dart-bridge"
"encoding/json"
"unsafe" "unsafe"
) )
var messagePort int64 = -1
//export initNativeApiBridge //export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) { func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api) bridge.InitDartApi(api)
} }
//export initMessage //export attachMessagePort
func initMessage(port C.longlong) { func attachMessagePort(mPort C.longlong) {
i := int64(port) messagePort = int64(mPort)
Port = i }
//export getTraffic
func getTraffic() *C.char {
return C.CString(handleGetTraffic())
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
return C.CString(handleGetTotalTraffic())
} }
//export freeCString //export freeCString
@@ -27,9 +39,32 @@ func freeCString(s *C.char) {
C.free(unsafe.Pointer(s)) C.free(unsafe.Pointer(s))
} }
//export initClash //export invokeAction
func initClash(homeDirStr *C.char) bool { func invokeAction(paramsChar *C.char, port C.longlong) {
return handleInitClash(C.GoString(homeDirStr)) params := C.GoString(paramsChar)
i := int64(port)
var action = &Action{}
err := json.Unmarshal([]byte(params), action)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.getResult(res)))
} }
//export startListener //export startListener
@@ -41,159 +76,3 @@ func startListener() {
func stopListener() { func stopListener() {
handleStopListener() handleStopListener()
} }
//export getIsInit
func getIsInit() bool {
return handleGetIsInit()
}
//export shutdownClash
func shutdownClash() bool {
return handleShutdown()
}
//export forceGc
func forceGc() {
handleForceGc()
}
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
bridge.SendToPort(i, handleValidateConfig(bytes))
}()
}
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export getProxies
func getProxies() *C.char {
return C.CString(handleGetProxies())
}
//export changeProxy
func changeProxy(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
handleChangeProxy(paramsString, func(value string) {
bridge.SendToPort(i, value)
})
}
//export getTraffic
func getTraffic(port C.int) *C.char {
onlyProxy := int(port) == 1
return C.CString(handleGetTraffic(onlyProxy))
}
//export getTotalTraffic
func getTotalTraffic(port C.int) *C.char {
onlyProxy := int(port) == 1
return C.CString(handleGetTotalTraffic(onlyProxy))
}
//export resetTraffic
func resetTraffic() {
handleResetTraffic()
}
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
handleAsyncTestDelay(paramsString, func(value string) {
bridge.SendToPort(i, value)
})
}
//export getConnections
func getConnections() *C.char {
return C.CString(handleGetConnections())
}
//export getMemory
func getMemory(port C.longlong) {
i := int64(port)
handleGetMemory(func(value string) {
bridge.SendToPort(i, value)
})
}
//export closeConnections
func closeConnections() {
handleCloseConnections()
}
//export closeConnection
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
handleCloseConnection(connectionId)
}
//export getExternalProviders
func getExternalProviders() *C.char {
return C.CString(handleGetExternalProviders())
}
//export getExternalProvider
func getExternalProvider(externalProviderNameChar *C.char) *C.char {
externalProviderName := C.GoString(externalProviderNameChar)
return C.CString(handleGetExternalProvider(externalProviderName))
}
//export updateGeoData
func updateGeoData(geoTypeChar *C.char, geoNameChar *C.char, port C.longlong) {
i := int64(port)
geoType := C.GoString(geoTypeChar)
geoName := C.GoString(geoNameChar)
handleUpdateGeoData(geoType, geoName, func(value string) {
bridge.SendToPort(i, value)
})
}
//export updateExternalProvider
func updateExternalProvider(providerNameChar *C.char, port C.longlong) {
i := int64(port)
providerName := C.GoString(providerNameChar)
handleUpdateExternalProvider(providerName, func(value string) {
bridge.SendToPort(i, value)
})
}
//export getCountryCode
func getCountryCode(ipChar *C.char, port C.longlong) {
ip := C.GoString(ipChar)
i := int64(port)
handleGetCountryCode(ip, func(value string) {
bridge.SendToPort(i, value)
})
}
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) {
i := int64(port)
providerName := C.GoString(providerNameChar)
data := []byte(C.GoString(dataChar))
handleSideLoadExternalProvider(providerName, data, func(value string) {
bridge.SendToPort(i, value)
})
}
//export startLog
func startLog() {
handleStartLog()
}
//export stopLog
func stopLog() {
handleStopLog()
}

View File

@@ -4,6 +4,8 @@ package main
import "C" import "C"
import ( import (
"context"
bridge "core/dart-bridge"
"core/platform" "core/platform"
"core/state" "core/state"
t "core/tun" t "core/tun"
@@ -16,255 +18,247 @@ import (
"github.com/metacubex/mihomo/dns" "github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun" "github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"syscall" "syscall"
"time" "time"
"unsafe"
) )
type ProcessMap struct { type TunHandler struct {
m sync.Map listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
} }
type FdMap struct { func (t *TunHandler) close() {
m sync.Map _ = t.limit.Acquire(context.TODO(), 4)
defer t.limit.Release(4)
removeTunHook()
if t.listener != nil {
_ = t.listener.Close()
}
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
} }
type Fd struct { func (t *TunHandler) handleProtect(fd int) {
Id int64 `json:"id"` _ = t.limit.Acquire(context.Background(), 1)
Value int64 `json:"value"` defer t.limit.Release(1)
if t.listener == nil {
return
}
protect(t.callback, fd)
}
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
} }
var ( var (
tunListener *sing_tun.Listener tunLock sync.Mutex
fdMap FdMap runTime *time.Time
fdCounter int64 = 0 errBlocked = errors.New("blocked")
counter int64 = 0 tunHandler *TunHandler
processMap ProcessMap
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
) )
func (cm *ProcessMap) Store(key int64, value string) { func handleStopTun() {
cm.m.Store(key, value) tunLock.Lock()
} defer tunLock.Unlock()
runTime = nil
func (cm *ProcessMap) Load(key int64) (string, bool) { if tunHandler != nil {
value, ok := cm.m.Load(key) tunHandler.close()
if !ok || value == nil {
return "", false
} }
return value.(string), true
} }
func (cm *FdMap) Store(key int64) { func handleStartTun(fd int, callback unsafe.Pointer) bool {
cm.m.Store(key, struct{}{}) handleStopTun()
} now := time.Now()
runTime = &now
func (cm *FdMap) Load(key int64) bool { if fd != 0 {
_, ok := cm.m.Load(key)
return ok
}
//export startTUN
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
if fd == 0 {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
now := time.Now() tunHandler = &TunHandler{
runTime = &now callback: callback,
SendMessage(Message{ limit: semaphore.NewWeighted(4),
Type: StartedMessage, }
Data: strconv.FormatInt(runTime.UnixMilli(), 10), initTunHook()
}) tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
f := int(fd)
tunListener, _ = t.Start(f, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil { if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address()) log.Infoln("TUN address: %v", tunListener.Address())
} else {
removeTunHook()
return false
} }
now := time.Now() tunHandler.listener = tunListener
runTime = &now
}()
}
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
} }
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10)) return true
} }
//export stopTun func handleGetRunTime() string {
func stopTun() { if runTime == nil {
removeSocketHook() return ""
go func() { }
tunLock.Lock() return strconv.FormatInt(runTime.UnixMilli(), 10)
defer tunLock.Unlock()
runTime = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
} }
//export setFdMap func initTunHook() {
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
func markSocket(fd Fd) {
SendMessage(Message{
Type: ProtectMessage,
Data: fd,
})
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error { dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() { if platform.ShouldBlockConnection() {
return errBlocked return errBlocked
} }
return conn.Control(func(fd uintptr) { return conn.Control(func(fd uintptr) {
fdInt := int64(fd) tunHandler.handleProtect(int(fd))
timeout := time.After(500 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
}) })
} }
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) { process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil { src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork return "", process.ErrInvalidNetwork
} }
id := atomic.AddInt64(&counter, 1) return tunHandler.handleResolveProcess(src, dst), nil
timeout := time.After(200 * time.Millisecond)
SendMessage(Message{
Type: ProcessMessage,
Data: Process{
Id: id,
Metadata: metadata,
},
})
for {
select {
case <-timeout:
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
} }
} }
//export setProcessMap func removeTunHook() {
func setProcessMap(s *C.char) { dialer.DefaultSocketHook = nil
if s == nil { process.DefaultPackageNameResolver = nil
return
}
paramsString := C.GoString(s)
go func() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
} }
//export getCurrentProfileName func handleGetAndroidVpnOptions() string {
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
options := state.AndroidVpnOptions{ options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable, Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort, Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address, Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(), Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl, AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.SystemProxy, SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass, AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: state.CurrentState.RouteAddress, RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain, BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(), DnsServerAddress: state.GetDnsServerAddress(),
} }
data, err := json.Marshal(options) data, err := json.Marshal(options)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return C.CString("") return ""
} }
return C.CString(string(data)) return string(data)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(strings.Split(value, ","))
dns.FlushCacheWithDefaultResolver()
}()
}
func handleGetCurrentProfileName() string {
if state.CurrentState == nil {
return ""
}
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method {
case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result(true)
return true
case getRunTimeMethod:
result(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
result(handleGetCurrentProfileName())
return true
}
return false
}
//export quickStart
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(paramsString)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool {
return handleStartTun(int(fd), callback)
}
//export getRunTime
func getRunTime() *C.char {
return C.CString(handleGetRunTime())
}
//export stopTun
func stopTun() {
handleStopTun()
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName())
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
return C.CString(handleGetAndroidVpnOptions())
} }
//export setState //export setState
func setState(s *C.char) { func setState(s *C.char) {
paramsString := C.GoString(s) paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState) handleSetState(paramsString)
if err != nil {
return
}
} }
//export updateDns //export updateDns
func updateDns(s *C.char) { func updateDns(s *C.char) {
dnsList := C.GoString(s) dnsList := C.GoString(s)
go func() { handleUpdateDns(dnsList)
log.Infoln("[DNS] updateDns %s", dnsList)
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver()
}()
} }

7
core/lib_no_android.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build !android && cgo
package main
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

View File

@@ -1,13 +0,0 @@
//go:build !cgo
package main
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
Action{
Method: messageMethod,
}.callback(s)
}

View File

@@ -1,47 +0,0 @@
//go:build cgo
package main
import (
bridge "core/dart-bridge"
)
var (
Port int64 = -1
ServicePort int64 = -1
)
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
}

176
core/platform/procfs.go Normal file
View File

@@ -0,0 +1,176 @@
//go:build linux
// +build linux
package platform
import (
"bufio"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"strings"
"unsafe"
)
var netIndexOfLocal = -1
var netIndexOfUid = -1
var nativeEndian binary.ByteOrder
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
return -1
}
network := source.Network()
if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") {
network = network[:len(network)-1]
}
path := "/proc/net/" + network
var sIP net.IP
var sPort int
switch s := source.(type) {
case *net.TCPAddr:
sIP = s.IP
sPort = s.Port
case *net.UDPAddr:
sIP = s.IP
sPort = s.Port
default:
return -1
}
sIP = sIP.To16()
if sIP == nil {
return -1
}
uid := doQuery(path+"6", sIP, sPort)
if uid == -1 {
sIP = sIP.To4()
if sIP == nil {
return -1
}
uid = doQuery(path, sIP, sPort)
}
return uid
}
func doQuery(path string, sIP net.IP, sPort int) int {
file, err := os.Open(path)
if err != nil {
return -1
}
defer func(file *os.File) {
_ = file.Close()
}(file)
reader := bufio.NewReader(file)
var bytes [2]byte
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
for {
row, _, err := reader.ReadLine()
if err != nil {
return -1
}
fields := strings.Fields(string(row))
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
continue
}
if strings.EqualFold(local, fields[netIndexOfLocal]) {
uid, err := strconv.Atoi(fields[netIndexOfUid])
if err != nil {
return -1
}
return uid
}
}
}
func nativeEndianIP(ip net.IP) []byte {
result := make([]byte, len(ip))
for i := 0; i < len(ip); i += 4 {
value := binary.BigEndian.Uint32(ip[i:])
nativeEndian.PutUint32(result[i:], value)
}
return result
}
func init() {
file, err := os.Open("/proc/net/tcp")
if err != nil {
return
}
defer func(file *os.File) {
_ = file.Close()
}(file)
reader := bufio.NewReader(file)
header, _, err := reader.ReadLine()
if err != nil {
return
}
columns := strings.Fields(string(header))
var txQueue, rxQueue, tr, tmWhen bool
for idx, col := range columns {
offset := 0
if txQueue && rxQueue {
offset--
}
if tr && tmWhen {
offset--
}
switch col {
case "tx_queue":
txQueue = true
case "rx_queue":
rxQueue = true
case "tr":
tr = true
case "tm->when":
tmWhen = true
case "local_address":
netIndexOfLocal = idx + offset
case "uid":
netIndexOfUid = idx + offset
}
}
}
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}

View File

@@ -10,10 +10,29 @@ import (
"strconv" "strconv"
) )
var conn net.Conn = nil var conn net.Conn
func sendMessage(message Message) {
res, err := message.Json()
if err != nil {
return
}
send(Action{
Method: messageMethod,
}.getResult(res))
}
func send(data []byte) {
if conn == nil {
return
}
_, _ = conn.Write(append(data, []byte("\n")...))
}
func startServer(arg string) { func startServer(arg string) {
_, err := strconv.Atoi(arg) _, err := strconv.Atoi(arg)
if err != nil { if err != nil {
conn, err = net.Dial("unix", arg) conn, err = net.Dial("unix", arg)
} else { } else {
@@ -42,132 +61,12 @@ func startServer(arg string) {
return return
} }
go handleAction(action) go handleAction(action, func(data interface{}) {
send(action.getResult(data))
})
} }
} }
func handleAction(action *Action) { func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method { return false
case initClashMethod:
data := action.Data.(string)
action.callback(handleInitClash(data))
return
case getIsInitMethod:
action.callback(handleGetIsInit())
return
case forceGcMethod:
handleForceGc()
return
case shutdownMethod:
action.callback(handleShutdown())
return
case validateConfigMethod:
data := []byte(action.Data.(string))
action.callback(handleValidateConfig(data))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
action.callback(handleUpdateConfig(data))
return
case getProxiesMethod:
action.callback(handleGetProxies())
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
action.callback(value)
})
return
case getTrafficMethod:
data := action.Data.(bool)
action.callback(handleGetTraffic(data))
return
case getTotalTrafficMethod:
data := action.Data.(bool)
action.callback(handleGetTotalTraffic(data))
return
case resetTrafficMethod:
handleResetTraffic()
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
action.callback(value)
})
return
case getConnectionsMethod:
action.callback(handleGetConnections())
return
case closeConnectionsMethod:
action.callback(handleCloseConnections())
return
case closeConnectionMethod:
id := action.Data.(string)
action.callback(handleCloseConnection(id))
return
case getExternalProvidersMethod:
action.callback(handleGetExternalProviders())
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
action.callback(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
action.callback(err.Error())
return
}
geoType := params["geoType"]
geoName := params["geoName"]
handleUpdateGeoData(geoType, geoName, func(value string) {
action.callback(value)
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
action.callback(value)
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
action.callback(err.Error())
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
action.callback(value)
})
return
case startLogMethod:
handleStartLog()
return
case stopLogMethod:
handleStopLog()
return
case startListenerMethod:
action.callback(handleStartListener())
return
case stopListenerMethod:
action.callback(handleStopListener())
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
action.callback(value)
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
action.callback(value)
})
return
}
} }

View File

@@ -1,7 +1,7 @@
//go:build android && cgo
package state package state
import "net/netip"
var DefaultIpv4Address = "172.19.0.1/30" var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2" var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126" var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
@@ -13,13 +13,14 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"` BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"` RouteAddress []netip.Prefix `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"` Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"` Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"` DnsServerAddress string `json:"dnsServerAddress"`
} }
type AccessControl struct { type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"` Mode string `json:"mode"`
AcceptList []string `json:"acceptList"` AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"` RejectList []string `json:"rejectList"`
@@ -31,20 +32,23 @@ type AndroidVpnRawOptions struct {
AccessControl *AccessControl `json:"accessControl"` AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"` AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"` SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"` Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
} }
type State struct { type State struct {
AndroidVpnRawOptions VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"currentProfileName"` CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
} }
var CurrentState = &State{} var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
func GetIpv6Address() string { func GetIpv6Address() string {
if CurrentState.Ipv6 { if CurrentState.VpnProps.Ipv6 {
return DefaultIpv6Address return DefaultIpv6Address
} else { } else {
return "" return ""

View File

@@ -33,7 +33,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
} }
prefix4 = append(prefix4, tempPrefix4) prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix var prefix6 []netip.Prefix
if state.CurrentState.Ipv6 { if state.CurrentState.VpnProps.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil { if err != nil {
log.Errorln("startTUN error:", err) log.Errorln("startTUN error:", err)

View File

@@ -7,57 +7,27 @@ import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart'; import 'models/models.dart';
import 'pages/pages.dart'; import 'pages/pages.dart';
runAppWithPreferences( class Application extends ConsumerStatefulWidget {
Widget child, {
required AppState appState,
required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
const Application({ const Application({
super.key, super.key,
}); });
@override @override
State<Application> createState() => ApplicationState(); ConsumerState<Application> createState() => ApplicationState();
} }
class ApplicationState extends State<Application> { class ApplicationState extends ConsumerState<Application> {
late SystemColorSchemes systemColorSchemes; late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
@@ -73,16 +43,8 @@ class ApplicationState extends State<Application> {
ColorScheme _getAppColorScheme({ ColorScheme _getAppColorScheme({
required Brightness brightness, required Brightness brightness,
int? primaryColor, int? primaryColor,
required SystemColorSchemes systemColorSchemes,
}) { }) {
if (primaryColor != null) { return ref.read(genColorSchemeProvider(brightness));
return ColorScheme.fromSeed(
seedColor: Color(primaryColor),
brightness: brightness,
);
} else {
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
}
} }
@override @override
@@ -90,12 +52,11 @@ class ApplicationState extends State<Application> {
super.initState(); super.initState();
_autoUpdateGroupTask(); _autoUpdateGroupTask();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
globalState.appController = AppController(context); globalState.appController = AppController(context, ref);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
globalState.appController = AppController(currentContext); globalState.appController = AppController(currentContext, ref);
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
@@ -113,13 +74,13 @@ class ApplicationState extends State<Application> {
} }
_autoUpdateProfilesTask() { _autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(seconds: 5), () async { _autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles(); await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
}); });
} }
_buildPlatformWrap(Widget child) { _buildPlatformState(Widget child) {
if (system.isDesktop) { if (system.isDesktop) {
return WindowManager( return WindowManager(
child: TrayManager( child: TrayManager(
@@ -138,33 +99,44 @@ class ApplicationState extends State<Application> {
); );
} }
_buildPage(Widget page) { _buildState(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_buildWrap(Widget child) {
return AppStateManager( return AppStateManager(
child: ClashManager( child: ClashManager(
child: ConnectivityManager( child: ConnectivityManager(
onConnectivityChanged: globalState.appController.updateLocalIp, onConnectivityChanged: () {
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
child: child, child: child,
), ),
), ),
); );
} }
_buildPlatformApp(Widget child) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: child,
);
}
return VpnManager(
child: child,
);
}
_buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
),
);
}
_updateSystemColorSchemes( _updateSystemColorSchemes(
ColorScheme? lightDynamic, ColorScheme? lightDynamic,
ColorScheme? darkDynamic, ColorScheme? darkDynamic,
) { ) {
systemColorSchemes = SystemColorSchemes( systemColorSchemes = ColorSchemes(
lightColorScheme: lightDynamic, lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic, darkColorScheme: darkDynamic,
); );
@@ -175,21 +147,18 @@ class ApplicationState extends State<Application> {
@override @override
Widget build(context) { Widget build(context) {
return _buildWrap( return _buildPlatformState(
_buildPlatformWrap( _buildState(
Selector2<AppState, Config, ApplicationSelectorState>( Consumer(
selector: (_, appState, config) => ApplicationSelectorState( builder: (_, ref, child) {
locale: config.appSetting.locale, final locale =
themeMode: config.themeProps.themeMode, ref.watch(appSettingProvider.select((state) => state.locale));
primaryColor: config.themeProps.primaryColor, final themeProps = ref.watch(themeSettingProvider);
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey, navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
@@ -198,43 +167,32 @@ class ApplicationState extends State<Application> {
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate
], ],
builder: (_, child) { builder: (_, child) {
return MessageManager( return AppEnvManager(
child: LayoutBuilder( child: _buildPlatformApp(
builder: (_, container) { _buildApp(child!),
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
), ),
); );
}, },
scrollBehavior: BaseScrollBehavior(), scrollBehavior: BaseScrollBehavior(),
title: appName, title: appName,
locale: other.getLocaleForString(state.locale), locale: utils.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales, supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode, themeMode: themeProps.themeMode,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.light, brightness: Brightness.light,
systemColorSchemes: systemColorSchemes, primaryColor: themeProps.primaryColor,
primaryColor: state.primaryColor,
), ),
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme, pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme( colorScheme: _getAppColorScheme(
brightness: Brightness.dark, brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes, primaryColor: themeProps.primaryColor,
primaryColor: state.primaryColor, ).toPureBlack(themeProps.pureBlack),
).toPrueBlack(state.prueBlack),
), ),
home: child, home: child,
); );
@@ -252,7 +210,7 @@ class ApplicationState extends State<Application> {
linkManager.destroy(); linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel(); _autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel(); _autoUpdateProfilesTaskTimer?.cancel();
await clashService?.destroy(); await clashCore.destroy();
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();
await globalState.appController.handleExit(); await globalState.appController.handleExit();
super.dispose(); super.dispose();

View File

@@ -8,12 +8,13 @@ import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
class ClashCore { class ClashCore {
static ClashCore? _instance; static ClashCore? _instance;
late ClashInterface clashInterface; late ClashHandlerInterface clashInterface;
ClashCore._internal() { ClashCore._internal() {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -28,8 +29,12 @@ class ClashCore {
return _instance!; return _instance!;
} }
Future<void> _initGeo() async { Future<bool> preload() {
final homePath = await appPath.getHomeDirPath(); return clashInterface.preload();
}
static Future<void> initGeo() async {
final homePath = await appPath.homeDirPath;
final homeDir = Directory(homePath); final homeDir = Directory(homePath);
final isExists = await homeDir.exists(); final isExists = await homeDir.exists();
if (!isExists) { if (!isExists) {
@@ -59,13 +64,19 @@ class ClashCore {
} }
} }
Future<bool> init({ Future<bool> init() async {
required ClashConfig clashConfig, await initGeo();
required Config config, final homeDirPath = await appPath.homeDirPath;
}) async { return await clashInterface.init(
await _initGeo(); InitParams(
final homeDirPath = await appPath.getHomeDirPath(); homeDir: homeDirPath,
return await clashInterface.init(homeDirPath); version: globalState.appState.version,
),
);
}
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
} }
shutdown() async { shutdown() async {
@@ -135,6 +146,9 @@ class ClashCore {
Future<List<ExternalProvider>> getExternalProviders() async { Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProviders(); await clashInterface.getExternalProviders();
if (externalProvidersRawString.isEmpty) {
return [];
}
return Isolate.run<List<ExternalProvider>>( return Isolate.run<List<ExternalProvider>>(
() { () {
final externalProviders = final externalProviders =
@@ -152,7 +166,7 @@ class ClashCore {
String externalProviderName) async { String externalProviderName) async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName); await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString == null) { if (externalProvidersRawString.isEmpty) {
return null; return null;
} }
if (externalProvidersRawString.isEmpty) { if (externalProvidersRawString.isEmpty) {
@@ -161,11 +175,8 @@ class ClashCore {
return ExternalProvider.fromJson(json.decode(externalProvidersRawString)); return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
} }
Future<String> updateGeoData({ Future<String> updateGeoData(UpdateGeoDataParams params) {
required String geoType, return clashInterface.updateGeoData(params);
required String geoName,
}) {
return clashInterface.updateGeoData(geoType: geoType, geoName: geoName);
} }
Future<String> sideLoadExternalProvider({ Future<String> sideLoadExternalProvider({
@@ -190,13 +201,16 @@ class ClashCore {
await clashInterface.stopListener(); await clashInterface.stopListener();
} }
Future<Delay> getDelay(String proxyName) async { Future<Delay> getDelay(String url, String proxyName) async {
final data = await clashInterface.asyncTestDelay(proxyName); final data = await clashInterface.asyncTestDelay(url, proxyName);
return Delay.fromJson(json.decode(data)); return Delay.fromJson(json.decode(data));
} }
Future<Traffic> getTraffic(bool value) async { Future<Traffic> getTraffic() async {
final trafficString = await clashInterface.getTraffic(value); final trafficString = await clashInterface.getTraffic();
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString)); return Traffic.fromMap(json.decode(trafficString));
} }
@@ -211,16 +225,30 @@ class ClashCore {
); );
} }
Future<Traffic> getTotalTraffic(bool value) async { Future<Traffic> getTotalTraffic() async {
final totalTrafficString = await clashInterface.getTotalTraffic(value); final totalTrafficString = await clashInterface.getTotalTraffic();
if (totalTrafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(totalTrafficString)); return Traffic.fromMap(json.decode(totalTrafficString));
} }
Future<int> getMemory() async { Future<int> getMemory() async {
final value = await clashInterface.getMemory(); final value = await clashInterface.getMemory();
if (value.isEmpty) {
return 0;
}
return int.parse(value); return int.parse(value);
} }
Future<ClashConfigSnippet?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
}
resetTraffic() { resetTraffic() {
clashInterface.resetTraffic(); clashInterface.resetTraffic();
} }
@@ -236,6 +264,10 @@ class ClashCore {
requestGc() { requestGc() {
clashInterface.forceGc(); clashInterface.forceGc();
} }
destroy() async {
await clashInterface.destroy();
}
} }
final clashCore = ClashCore(); final clashCore = ClashCore();

View File

@@ -2348,6 +2348,97 @@ class ClashFFI {
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value; set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
void protect(
protect_func fn,
ffi.Pointer<ffi.Void> tun_interface,
int fd,
) {
return _protect(
fn,
tun_interface,
fd,
);
}
late final _protectPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
protect_func, ffi.Pointer<ffi.Void>, ffi.Int)>>('protect');
late final _protect = _protectPtr
.asFunction<void Function(protect_func, ffi.Pointer<ffi.Void>, int)>();
ffi.Pointer<ffi.Char> resolve_process(
resolve_process_func fn,
ffi.Pointer<ffi.Void> tun_interface,
int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
int uid,
) {
return _resolve_process(
fn,
tun_interface,
protocol,
source,
target,
uid,
);
}
late final _resolve_processPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
resolve_process_func,
ffi.Pointer<ffi.Void>,
ffi.Int,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
ffi.Int)>>('resolve_process');
late final _resolve_process = _resolve_processPtr.asFunction<
ffi.Pointer<ffi.Char> Function(
resolve_process_func,
ffi.Pointer<ffi.Void>,
int,
ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>,
int)>();
void release_object(
release_object_func fn,
ffi.Pointer<ffi.Void> obj,
) {
return _release_object(
fn,
obj,
);
}
late final _release_objectPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
release_object_func, ffi.Pointer<ffi.Void>)>>('release_object');
late final _release_object = _release_objectPtr
.asFunction<void Function(release_object_func, ffi.Pointer<ffi.Void>)>();
void registerCallbacks(
protect_func markSocketFunc,
resolve_process_func resolveProcessFunc,
release_object_func releaseObjectFunc,
) {
return _registerCallbacks(
markSocketFunc,
resolveProcessFunc,
releaseObjectFunc,
);
}
late final _registerCallbacksPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(protect_func, resolve_process_func,
release_object_func)>>('registerCallbacks');
late final _registerCallbacks = _registerCallbacksPtr.asFunction<
void Function(protect_func, resolve_process_func, release_object_func)>();
void initNativeApiBridge( void initNativeApiBridge(
ffi.Pointer<ffi.Void> api, ffi.Pointer<ffi.Void> api,
) { ) {
@@ -2362,18 +2453,39 @@ class ClashFFI {
late final _initNativeApiBridge = _initNativeApiBridgePtr late final _initNativeApiBridge = _initNativeApiBridgePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>(); .asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void initMessage( void attachMessagePort(
int port, int mPort,
) { ) {
return _initMessage( return _attachMessagePort(
port, mPort,
); );
} }
late final _initMessagePtr = late final _attachMessagePortPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'initMessage'); 'attachMessagePort');
late final _initMessage = _initMessagePtr.asFunction<void Function(int)>(); late final _attachMessagePort =
_attachMessagePortPtr.asFunction<void Function(int)>();
ffi.Pointer<ffi.Char> getTraffic() {
return _getTraffic();
}
late final _getTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getTraffic');
late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getTotalTraffic() {
return _getTotalTraffic();
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void freeCString( void freeCString(
ffi.Pointer<ffi.Char> s, ffi.Pointer<ffi.Char> s,
@@ -2389,19 +2501,22 @@ class ClashFFI {
late final _freeCString = late final _freeCString =
_freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _freeCStringPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
int initClash( void invokeAction(
ffi.Pointer<ffi.Char> homeDirStr, ffi.Pointer<ffi.Char> paramsChar,
int port,
) { ) {
return _initClash( return _invokeAction(
homeDirStr, paramsChar,
port,
); );
} }
late final _initClashPtr = late final _invokeActionPtr = _lookup<
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>( ffi.NativeFunction<
'initClash'); ffi.Void Function(
late final _initClash = ffi.Pointer<ffi.Char>, ffi.LongLong)>>('invokeAction');
_initClashPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>(); late final _invokeAction =
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void startListener() { void startListener() {
return _startListener(); return _startListener();
@@ -2419,317 +2534,43 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopListener');
late final _stopListener = _stopListenerPtr.asFunction<void Function()>(); late final _stopListener = _stopListenerPtr.asFunction<void Function()>();
int getIsInit() { void quickStart(
return _getIsInit(); ffi.Pointer<ffi.Char> initParamsChar,
} ffi.Pointer<ffi.Char> paramsChar,
ffi.Pointer<ffi.Char> stateParamsChar,
late final _getIsInitPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('getIsInit');
late final _getIsInit = _getIsInitPtr.asFunction<int Function()>();
int shutdownClash() {
return _shutdownClash();
}
late final _shutdownClashPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
void forceGc() {
return _forceGc();
}
late final _forceGcPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('forceGc');
late final _forceGc = _forceGcPtr.asFunction<void Function()>();
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port, int port,
) { ) {
return _validateConfig( return _quickStart(
s, initParamsChar,
paramsChar,
stateParamsChar,
port, port,
); );
} }
late final _validateConfigPtr = _lookup< late final _quickStartPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
late final _validateConfig = _validateConfigPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void updateConfig(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _updateConfig(
s,
port,
);
}
late final _updateConfigPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateConfig');
late final _updateConfig =
_updateConfigPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getProxies() {
return _getProxies();
}
late final _getProxiesPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getProxies');
late final _getProxies =
_getProxiesPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void changeProxy(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _changeProxy(
s,
port,
);
}
late final _changeProxyPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('changeProxy');
late final _changeProxy =
_changeProxyPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getTraffic(
int port,
) {
return _getTraffic(
port,
);
}
late final _getTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTraffic');
late final _getTraffic =
_getTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
ffi.Pointer<ffi.Char> getTotalTraffic(
int port,
) {
return _getTotalTraffic(
port,
);
}
late final _getTotalTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'getTotalTraffic');
late final _getTotalTraffic =
_getTotalTrafficPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
void resetTraffic() {
return _resetTraffic();
}
late final _resetTrafficPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('resetTraffic');
late final _resetTraffic = _resetTrafficPtr.asFunction<void Function()>();
void asyncTestDelay(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _asyncTestDelay(
s,
port,
);
}
late final _asyncTestDelayPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('asyncTestDelay');
late final _asyncTestDelay = _asyncTestDelayPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getConnections() {
return _getConnections();
}
late final _getConnectionsPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getConnections');
late final _getConnections =
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void getMemory(
int port,
) {
return _getMemory(
port,
);
}
late final _getMemoryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
void closeConnections() {
return _closeConnections();
}
late final _closeConnectionsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
late final _closeConnections =
_closeConnectionsPtr.asFunction<void Function()>();
void closeConnection(
ffi.Pointer<ffi.Char> id,
) {
return _closeConnection(
id,
);
}
late final _closeConnectionPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'closeConnection');
late final _closeConnection =
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getExternalProviders() {
return _getExternalProviders();
}
late final _getExternalProvidersPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getExternalProviders');
late final _getExternalProviders =
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
ffi.Pointer<ffi.Char> getExternalProvider(
ffi.Pointer<ffi.Char> externalProviderNameChar,
) {
return _getExternalProvider(
externalProviderNameChar,
);
}
late final _getExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Char>)>>('getExternalProvider');
late final _getExternalProvider = _getExternalProviderPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void updateGeoData(
ffi.Pointer<ffi.Char> geoTypeChar,
ffi.Pointer<ffi.Char> geoNameChar,
int port,
) {
return _updateGeoData(
geoTypeChar,
geoNameChar,
port,
);
}
late final _updateGeoDataPtr = _lookup<
ffi.NativeFunction< ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateGeoData'); ffi.Pointer<ffi.Char>, ffi.LongLong)>>('quickStart');
late final _updateGeoData = _updateGeoDataPtr.asFunction< late final _quickStart = _quickStartPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>(); void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, int)>();
void updateExternalProvider( int startTUN(
ffi.Pointer<ffi.Char> providerNameChar,
int port,
) {
return _updateExternalProvider(
providerNameChar,
port,
);
}
late final _updateExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void getCountryCode(
ffi.Pointer<ffi.Char> ipChar,
int port,
) {
return _getCountryCode(
ipChar,
port,
);
}
late final _getCountryCodePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
late final _getCountryCode = _getCountryCodePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerNameChar,
ffi.Pointer<ffi.Char> dataChar,
int port,
) {
return _sideLoadExternalProvider(
providerNameChar,
dataChar,
port,
);
}
late final _sideLoadExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('sideLoadExternalProvider');
late final _sideLoadExternalProvider =
_sideLoadExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void startLog() {
return _startLog();
}
late final _startLogPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('startLog');
late final _startLog = _startLogPtr.asFunction<void Function()>();
void stopLog() {
return _stopLog();
}
late final _stopLogPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopLog');
late final _stopLog = _stopLogPtr.asFunction<void Function()>();
void startTUN(
int fd, int fd,
int port, ffi.Pointer<ffi.Void> callback,
) { ) {
return _startTUN( return _startTUN(
fd, fd,
port, callback,
); );
} }
late final _startTUNPtr = late final _startTUNPtr = _lookup<
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int, ffi.LongLong)>>( ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
'startTUN'); 'startTUN');
late final _startTUN = _startTUNPtr.asFunction<void Function(int, int)>(); late final _startTUN =
_startTUNPtr.asFunction<int Function(int, ffi.Pointer<ffi.Void>)>();
ffi.Pointer<ffi.Char> getRunTime() { ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime(); return _getRunTime();
@@ -2749,32 +2590,6 @@ class ClashFFI {
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun'); _lookup<ffi.NativeFunction<ffi.Void Function()>>('stopTun');
late final _stopTun = _stopTunPtr.asFunction<void Function()>(); late final _stopTun = _stopTunPtr.asFunction<void Function()>();
void setFdMap(
int fd,
) {
return _setFdMap(
fd,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Long)>>('setFdMap');
late final _setFdMap = _setFdMapPtr.asFunction<void Function(int)>();
void setProcessMap(
ffi.Pointer<ffi.Char> s,
) {
return _setProcessMap(
s,
);
}
late final _setProcessMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setProcessMap');
late final _setProcessMap =
_setProcessMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() { ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName(); return _getCurrentProfileName();
} }
@@ -3974,6 +3789,31 @@ typedef mode_t = __darwin_mode_t;
typedef __darwin_mode_t = __uint16_t; typedef __darwin_mode_t = __uint16_t;
typedef __uint16_t = ffi.UnsignedShort; typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int; typedef Dart__uint16_t = int;
typedef protect_func = ffi.Pointer<ffi.NativeFunction<protect_funcFunction>>;
typedef protect_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> tun_interface, ffi.Int fd);
typedef Dartprotect_funcFunction = void Function(
ffi.Pointer<ffi.Void> tun_interface, int fd);
typedef resolve_process_func
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
typedef resolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Void> tun_interface,
ffi.Int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
ffi.Int uid);
typedef Dartresolve_process_funcFunction = ffi.Pointer<ffi.Char> Function(
ffi.Pointer<ffi.Void> tun_interface,
int protocol,
ffi.Pointer<ffi.Char> source,
ffi.Pointer<ffi.Char> target,
int uid);
typedef release_object_func
= ffi.Pointer<ffi.NativeFunction<release_object_funcFunction>>;
typedef release_object_funcFunction = ffi.Void Function(
ffi.Pointer<ffi.Void> obj);
typedef Dartrelease_object_funcFunction = void Function(
ffi.Pointer<ffi.Void> obj);
final class GoInterface extends ffi.Struct { final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t; external ffi.Pointer<ffi.Void> t;
@@ -4211,6 +4051,8 @@ const int __MAC_15_0 = 150000;
const int __MAC_15_1 = 150100; const int __MAC_15_1 = 150100;
const int __MAC_15_2 = 150200;
const int __IPHONE_2_0 = 20000; const int __IPHONE_2_0 = 20000;
const int __IPHONE_2_1 = 20100; const int __IPHONE_2_1 = 20100;
@@ -4373,6 +4215,8 @@ const int __IPHONE_18_0 = 180000;
const int __IPHONE_18_1 = 180100; const int __IPHONE_18_1 = 180100;
const int __IPHONE_18_2 = 180200;
const int __WATCHOS_1_0 = 10000; const int __WATCHOS_1_0 = 10000;
const int __WATCHOS_2_0 = 20000; const int __WATCHOS_2_0 = 20000;
@@ -4471,6 +4315,8 @@ const int __WATCHOS_11_0 = 110000;
const int __WATCHOS_11_1 = 110100; const int __WATCHOS_11_1 = 110100;
const int __WATCHOS_11_2 = 110200;
const int __TVOS_9_0 = 90000; const int __TVOS_9_0 = 90000;
const int __TVOS_9_1 = 90100; const int __TVOS_9_1 = 90100;
@@ -4571,6 +4417,8 @@ const int __TVOS_18_0 = 180000;
const int __TVOS_18_1 = 180100; const int __TVOS_18_1 = 180100;
const int __TVOS_18_2 = 180200;
const int __BRIDGEOS_2_0 = 20000; const int __BRIDGEOS_2_0 = 20000;
const int __BRIDGEOS_3_0 = 30000; const int __BRIDGEOS_3_0 = 30000;
@@ -4627,6 +4475,8 @@ const int __BRIDGEOS_9_0 = 90000;
const int __BRIDGEOS_9_1 = 90100; const int __BRIDGEOS_9_1 = 90100;
const int __BRIDGEOS_9_2 = 90200;
const int __DRIVERKIT_19_0 = 190000; const int __DRIVERKIT_19_0 = 190000;
const int __DRIVERKIT_20_0 = 200000; const int __DRIVERKIT_20_0 = 200000;
@@ -4657,6 +4507,8 @@ const int __DRIVERKIT_24_0 = 240000;
const int __DRIVERKIT_24_1 = 240100; const int __DRIVERKIT_24_1 = 240100;
const int __DRIVERKIT_24_2 = 240200;
const int __VISIONOS_1_0 = 10000; const int __VISIONOS_1_0 = 10000;
const int __VISIONOS_1_1 = 10100; const int __VISIONOS_1_1 = 10100;
@@ -4667,6 +4519,8 @@ const int __VISIONOS_2_0 = 20000;
const int __VISIONOS_2_1 = 20100; const int __VISIONOS_2_1 = 20100;
const int __VISIONOS_2_2 = 20200;
const int MAC_OS_X_VERSION_10_0 = 1000; const int MAC_OS_X_VERSION_10_0 = 1000;
const int MAC_OS_X_VERSION_10_1 = 1010; const int MAC_OS_X_VERSION_10_1 = 1010;
@@ -4793,9 +4647,11 @@ const int MAC_OS_VERSION_15_0 = 150000;
const int MAC_OS_VERSION_15_1 = 150100; const int MAC_OS_VERSION_15_1 = 150100;
const int MAC_OS_VERSION_15_2 = 150200;
const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000; const int __MAC_OS_X_VERSION_MIN_REQUIRED = 150000;
const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150100; const int __MAC_OS_X_VERSION_MAX_ALLOWED = 150200;
const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1; const int __ENABLE_LEGACY_MAC_AVAILABILITY = 1;

View File

@@ -1,19 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
mixin ClashInterface { mixin ClashInterface {
FutureOr<bool> init(String homeDir); Future<bool> init(InitParams params);
FutureOr<void> shutdown(); Future<bool> preload();
FutureOr<bool> get isInit; Future<bool> shutdown();
forceGc(); Future<bool> get isInit;
Future<bool> forceGc();
FutureOr<String> validateConfig(String data); FutureOr<String> validateConfig(String data);
Future<String> asyncTestDelay(String proxyName); Future<String> asyncTestDelay(String url, String proxyName);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams); FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
@@ -29,10 +35,7 @@ mixin ClashInterface {
FutureOr<String>? getExternalProvider(String externalProviderName); FutureOr<String>? getExternalProvider(String externalProviderName);
Future<String> updateGeoData({ Future<String> updateGeoData(UpdateGeoDataParams params);
required String geoType,
required String geoName,
});
Future<String> sideLoadExternalProvider({ Future<String> sideLoadExternalProvider({
required String providerName, required String providerName,
@@ -41,9 +44,9 @@ mixin ClashInterface {
Future<String> updateExternalProvider(String providerName); Future<String> updateExternalProvider(String providerName);
FutureOr<String> getTraffic(bool value); FutureOr<String> getTraffic();
FutureOr<String> getTotalTraffic(bool value); FutureOr<String> getTotalTraffic();
FutureOr<String> getCountryCode(String ip); FutureOr<String> getCountryCode(String ip);
@@ -60,4 +63,359 @@ mixin ClashInterface {
FutureOr<bool> closeConnection(String id); FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id);
Future<bool> setState(CoreState state);
}
mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
// Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName();
Future<DateTime?> getRunTime();
}
abstract class ClashHandlerInterface with ClashInterface {
Map<String, Completer> callbackCompleterMap = {};
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
Future.value(false);
handleResult(ActionResult result) async {
final completer = callbackCompleterMap[result.id];
try {
switch (result.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
case ActionMethod.setState:
completer?.complete(result.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
completer?.complete(result.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(result.data as String);
completer?.complete(true);
return;
default:
final isHandled = await nextHandleResult(result, completer);
if (isHandled) {
return;
}
completer?.complete(result.data);
}
} catch (_) {
commonPrint.log(result.id);
}
}
sendMessage(String message);
reStart();
FutureOr<bool> destroy();
Future<T> invoke<T>({
required ActionMethod method,
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
}) async {
final id = "${method.name}#${utils.id}";
callbackCompleterMap[id] = Completer<T>();
dynamic defaultValue;
if (T == String) {
defaultValue = "";
}
if (T == bool) {
defaultValue = false;
}
sendMessage(
json.encode(
Action(
id: id,
method: method,
data: data,
defaultValue: defaultValue,
),
),
);
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
timeout: timeout,
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout ??
() {
return defaultValue;
},
functionName: id,
);
}
@override
Future<bool> init(InitParams params) {
return invoke<bool>(
method: ActionMethod.initClash,
data: json.encode(params),
);
}
@override
Future<bool> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
shutdown() async {
return await invoke<bool>(
method: ActionMethod.shutdown,
);
}
@override
Future<bool> get isInit {
return invoke<bool>(
method: ActionMethod.getIsInit,
);
}
@override
Future<bool> forceGc() {
return invoke<bool>(
method: ActionMethod.forceGc,
);
}
@override
FutureOr<String> validateConfig(String data) {
return invoke<String>(
method: ActionMethod.validateConfig,
data: data,
);
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: Duration(minutes: 2),
);
}
@override
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
timeout: Duration(seconds: 5),
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData(UpdateGeoDataParams params) {
return invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(params),
timeout: Duration(minutes: 1));
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
"providerName": providerName,
"data": data,
}),
);
}
@override
Future<String> updateExternalProvider(String providerName) {
return invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
timeout: Duration(minutes: 1),
);
}
@override
FutureOr<String> getConnections() {
return invoke<String>(
method: ActionMethod.getConnections,
);
}
@override
Future<bool> closeConnections() {
return invoke<bool>(
method: ActionMethod.closeConnections,
);
}
@override
Future<bool> closeConnection(String id) {
return invoke<bool>(
method: ActionMethod.closeConnection,
data: id,
);
}
@override
Future<String> getProfile(String id) {
return invoke<String>(
method: ActionMethod.getProfile,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic() {
return invoke<String>(
method: ActionMethod.getTotalTraffic,
);
}
@override
FutureOr<String> getTraffic() {
return invoke<String>(
method: ActionMethod.getTraffic,
);
}
@override
resetTraffic() {
invoke(method: ActionMethod.resetTraffic);
}
@override
startLog() {
invoke(method: ActionMethod.startLog);
}
@override
stopLog() {
invoke<bool>(
method: ActionMethod.stopLog,
);
}
@override
Future<bool> startListener() {
return invoke<bool>(
method: ActionMethod.startListener,
);
}
@override
stopListener() {
return invoke<bool>(
method: ActionMethod.stopListener,
);
}
@override
Future<String> asyncTestDelay(String url, String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
"test-url": url,
};
return invoke<String>(
method: ActionMethod.asyncTestDelay,
data: json.encode(delayParams),
timeout: Duration(
milliseconds: 6000,
),
onTimeout: () {
return json.encode(
Delay(
name: proxyName,
value: -1,
url: url,
),
);
},
);
}
@override
FutureOr<String> getCountryCode(String ip) {
return invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return invoke<String>(
method: ActionMethod.getMemory,
);
}
} }

View File

@@ -3,28 +3,58 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/constant.dart'; import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/state.dart';
import 'generated/clash_ffi.dart'; import 'generated/clash_ffi.dart';
import 'interface.dart'; import 'interface.dart';
class ClashLib with ClashInterface { class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
static ClashLib? _instance; static ClashLib? _instance;
final receiver = ReceivePort(); Completer<bool> _canSendCompleter = Completer();
SendPort? sendPort;
late final ClashFFI clashFFI; final receiverPort = ReceivePort();
late final DynamicLibrary lib;
ClashLib._internal() { ClashLib._internal() {
lib = DynamicLibrary.open("libclash.so"); _initService();
clashFFI = ClashFFI(lib); }
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData, @override
); preload() {
return _canSendCompleter.future;
}
_initService() async {
await service?.destroy();
_registerMainPort(receiverPort.sendPort);
receiverPort.listen((message) {
if (message is SendPort) {
if (_canSendCompleter.isCompleted) {
sendPort = null;
_canSendCompleter = Completer();
}
sendPort = message;
_canSendCompleter.complete(true);
} else {
handleResult(
ActionResult.fromJson(json.decode(
message,
)),
);
}
});
await service?.init();
}
_registerMainPort(SendPort sendPort) {
IsolateNameServer.removePortNameMapping(mainIsolate);
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
} }
factory ClashLib() { factory ClashLib() {
@@ -32,227 +62,132 @@ class ClashLib with ClashInterface {
return _instance!; return _instance!;
} }
initMessage() { @override
clashFFI.initMessage( Future<bool> nextHandleResult(result, completer) async {
receiver.sendPort.nativePort, switch (result.method) {
); case ActionMethod.setFdMap:
case ActionMethod.setProcessMap:
case ActionMethod.stopTun:
case ActionMethod.updateDns:
completer?.complete(result.data as bool);
return true;
case ActionMethod.getRunTime:
case ActionMethod.startTun:
case ActionMethod.getAndroidVpnOptions:
case ActionMethod.getCurrentProfileName:
completer?.complete(result.data as String);
return true;
default:
return false;
}
} }
@override @override
bool init(String homeDir) { destroy() async {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>(); await service?.destroy();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
@override
shutdown() async {
clashFFI.shutdownClash();
lib.close();
}
@override
bool get isInit => clashFFI.getIsInit() == 1;
@override
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getProxies() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return proxiesRawString;
}
@override
String getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return externalProvidersRawString;
}
@override
String getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
return externalProviderRawString;
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
@override
Future<String> updateExternalProvider(String providerName) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}
@override
Future<String> changeProxy(ChangeProxyParams changeProxyParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
@override
String getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsString = connectionsDataRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(connectionsDataRaw);
return connectionsString;
}
@override
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
return true; return true;
} }
@override @override
closeConnections() { reStart() {
clashFFI.closeConnections(); _initService();
}
@override
Future<bool> shutdown() async {
await super.shutdown();
destroy();
return true; return true;
} }
@override @override
startListener() async { sendMessage(String message) async {
clashFFI.startListener(); await _canSendCompleter.future;
return true; sendPort?.send(message);
} }
@override @override
stopListener() async { Future<bool> setFdMap(int fd) {
clashFFI.stopListener(); return invoke<bool>(
return true; method: ActionMethod.setFdMap,
data: json.encode(fd),
);
} }
@override @override
Future<String> asyncTestDelay(String proxyName) { Future<bool> setProcessMap(item) {
final delayParams = { return invoke<bool>(
"proxy-name": proxyName, method: ActionMethod.setProcessMap,
"timeout": httpTimeoutDuration.inMilliseconds, data: item,
}; );
}
// @override
// Future<bool> stopTun() {
// return invoke<bool>(
// method: ActionMethod.stopTun,
// );
// }
@override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
final res = await invoke<String>(
method: ActionMethod.getAndroidVpnOptions,
);
if (res.isEmpty) {
return null;
}
return AndroidVpnOptions.fromJson(json.decode(res));
}
@override
Future<bool> updateDns(String value) {
return invoke<bool>(
method: ActionMethod.updateDns,
data: value,
);
}
@override
Future<DateTime?> getRunTime() async {
final runTimeString = await invoke<String>(
method: ActionMethod.getRunTime,
);
if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
@override
Future<String> getCurrentProfileName() {
return invoke<String>(
method: ActionMethod.getCurrentProfileName,
);
}
}
class ClashLibHandler {
static ClashLibHandler? _instance;
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
ClashLibHandler._internal() {
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashLibHandler() {
_instance ??= ClashLibHandler._internal();
return _instance!;
}
Future<String> invokeAction(String actionParams) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -261,109 +196,27 @@ class ClashLib with ClashInterface {
receiver.close(); receiver.close();
} }
}); });
final delayParamsChar = final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
json.encode(delayParams).toNativeUtf8().cast<Char>(); clashFFI.invokeAction(
clashFFI.asyncTestDelay( actionParamsChar,
delayParamsChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(delayParamsChar); malloc.free(actionParamsChar);
return completer.future; return completer.future;
} }
@override attachMessagePort(int messagePort) {
String getTraffic(bool value) { clashFFI.attachMessagePort(
final trafficRaw = clashFFI.getTraffic(value ? 1 : 0); messagePort,
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
return trafficString;
}
@override
String getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic(value ? 1 : 0);
clashFFI.freeCString(trafficRaw);
return trafficRaw.cast<Utf8>().toDartString();
}
@override
void resetTraffic() {
clashFFI.resetTraffic();
}
@override
void startLog() {
clashFFI.startLog();
}
@override
stopLog() {
clashFFI.stopLog();
}
@override
forceGc() {
clashFFI.forceGc();
}
@override
FutureOr<String> getCountryCode(String ip) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final ipChar = ip.toNativeUtf8().cast<Char>();
clashFFI.getCountryCode(
ipChar,
receiver.sendPort.nativePort,
); );
malloc.free(ipChar);
return completer.future;
}
@override
FutureOr<String> getMemory() {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.getMemory(receiver.sendPort.nativePort);
return completer.future;
}
/// Android
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
stopTun() {
clashFFI.stopTun();
} }
updateDns(String dns) { updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>(); final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar); clashFFI.updateDns(dnsChar);
malloc.free(dnsChar); malloc.free(dnsChar);
} }
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) { setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>(); final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar); clashFFI.setState(stateChar);
@@ -384,8 +237,65 @@ class ClashLib with ClashInterface {
return AndroidVpnOptions.fromJson(vpnOptions); return AndroidVpnOptions.fromJson(vpnOptions);
} }
setFdMap(int fd) { Traffic getTraffic() {
clashFFI.setFdMap(fd); final trafficRaw = clashFFI.getTraffic();
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString));
}
Traffic getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString));
}
startListener() async {
clashFFI.startListener();
return true;
}
stopListener() async {
clashFFI.stopListener();
return true;
}
Future<String> quickStart(
InitParams initParams,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state);
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart(
initParamsChar,
paramsChar,
stateParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(initParamsChar);
malloc.free(paramsChar);
malloc.free(stateParamsChar);
return completer.future;
} }
DateTime? getRunTime() { DateTime? getRunTime() {
@@ -397,4 +307,5 @@ class ClashLib with ClashInterface {
} }
} }
final clashLib = Platform.isAndroid ? ClashLib() : null; ClashLib? get clashLib =>
Platform.isAndroid && !globalState.isService ? ClashLib() : null;

View File

@@ -1,18 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class ClashMessage { class ClashMessage {
final controller = StreamController(); final controller = StreamController<String>();
ClashMessage._() { ClashMessage._() {
clashLib?.receiver.listen(controller.add);
controller.stream.listen( controller.stream.listen(
(message) { (message) {
if(message.isEmpty){
return;
}
final m = AppMessage.fromJson(json.decode(message)); final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) { for (final AppMessageListener listener in _listeners) {
switch (m.type) { switch (m.type) {
@@ -25,9 +26,6 @@ class ClashMessage {
case AppMessageType.request: case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data)); listener.onRequest(Connection.fromJson(m.data));
break; break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded: case AppMessageType.loaded:
listener.onLoaded(m.data); listener.onLoaded(m.data);
break; break;

View File

@@ -3,20 +3,19 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart'; import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/core.dart'; import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
class ClashService with ClashInterface { class ClashService extends ClashHandlerInterface {
static ClashService? _instance; static ClashService? _instance;
Completer<ServerSocket> serverCompleter = Completer(); Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer(); Completer<Socket> socketCompleter = Completer();
Map<String, Completer> callbackCompleterMap = {}; bool isStarting = false;
Process? process; Process? process;
@@ -26,52 +25,66 @@ class ClashService with ClashInterface {
} }
ClashService._internal() { ClashService._internal() {
_createServer(); _initServer();
startCore(); reStart();
} }
_createServer() async { _initServer() async {
final address = !Platform.isWindows runZonedGuarded(() async {
? InternetAddress( final address = !Platform.isWindows
unixSocketPath, ? InternetAddress(
type: InternetAddressType.unix, unixSocketPath,
) type: InternetAddressType.unix,
: InternetAddress( )
localhost, : InternetAddress(
type: InternetAddressType.IPv4, localhost,
); type: InternetAddressType.IPv4,
await _deleteSocketFile(); );
final server = await ServerSocket.bind( await _deleteSocketFile();
address, final server = await ServerSocket.bind(
0, address,
shared: true, 0,
); shared: true,
serverCompleter.complete(server); );
await for (final socket in server) { serverCompleter.complete(server);
await _destroySocket(); await for (final socket in server) {
socketCompleter.complete(socket); await _destroySocket();
socket socketCompleter.complete(socket);
.transform( socket
StreamTransformer<Uint8List, String>.fromHandlers( .transform(
handleData: (Uint8List data, EventSink<String> sink) { StreamTransformer<Uint8List, String>.fromHandlers(
sink.add(utf8.decode(data, allowMalformed: true)); handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
}, },
), );
) }
.transform(LineSplitter()) }, (error, stack) {
.listen( commonPrint.log(error.toString());
(data) { if(error is SocketException){
_handleAction( globalState.showNotifier(error.toString());
Action.fromJson( globalState.appController.restartCore();
json.decode(data.trim()), }
), });
);
},
);
}
} }
startCore() async { @override
reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) { if (process != null) {
await shutdown(); await shutdown();
} }
@@ -93,6 +106,21 @@ class ClashService with ClashInterface {
], ],
); );
process!.stdout.listen((_) {}); process!.stdout.listen((_) {});
isStarting = false;
}
@override
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
return true;
}
@override
sendMessage(String message) async {
final socket = await socketCompleter.future;
socket.writeln(message);
} }
_deleteSocketFile() async { _deleteSocketFile() async {
@@ -112,327 +140,21 @@ class ClashService with ClashInterface {
} }
} }
_handleAction(Action action) {
final completer = callbackCompleterMap[action.id];
switch (action.method) {
case ActionMethod.initClash:
case ActionMethod.shutdown:
case ActionMethod.getIsInit:
case ActionMethod.startListener:
case ActionMethod.resetTraffic:
case ActionMethod.closeConnections:
case ActionMethod.closeConnection:
case ActionMethod.stopListener:
completer?.complete(action.data as bool);
return;
case ActionMethod.changeProxy:
case ActionMethod.getProxies:
case ActionMethod.getTraffic:
case ActionMethod.getTotalTraffic:
case ActionMethod.asyncTestDelay:
case ActionMethod.getConnections:
case ActionMethod.getExternalProviders:
case ActionMethod.getExternalProvider:
case ActionMethod.validateConfig:
case ActionMethod.updateConfig:
case ActionMethod.updateGeoData:
case ActionMethod.updateExternalProvider:
case ActionMethod.sideLoadExternalProvider:
case ActionMethod.getCountryCode:
case ActionMethod.getMemory:
completer?.complete(action.data as String);
return;
case ActionMethod.message:
clashMessage.controller.add(action.data as String);
return;
case ActionMethod.forceGc:
case ActionMethod.startLog:
case ActionMethod.stopLog:
return;
}
}
Future<T> _invoke<T>({
required ActionMethod method,
dynamic data,
Duration? timeout,
FutureOr<T> Function()? onTimeout,
}) async {
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
callbackCompleterMap[id] = Completer<T>();
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
return (callbackCompleterMap[id] as Completer<T>).safeFuture(
timeout: timeout,
onLast: () {
callbackCompleterMap.remove(id);
},
onTimeout: onTimeout ??
() {
if (T is String) {
return "" as T;
}
if (T is bool) {
return false as T;
}
return null as T;
},
functionName: id,
);
}
_prueInvoke({
required ActionMethod method,
dynamic data,
}) async {
final id = "${method.name}#${other.id}";
final socket = await socketCompleter.future;
socket.writeln(
json.encode(
Action(
id: id,
method: method,
data: data,
),
),
);
}
@override
Future<bool> init(String homeDir) {
return _invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
);
}
@override @override
shutdown() async { shutdown() async {
await _invoke<bool>(
method: ActionMethod.shutdown,
);
if (Platform.isWindows) { if (Platform.isWindows) {
await request.stopCoreByHelper(); await request.stopCoreByHelper();
} }
await _destroySocket(); await _destroySocket();
process?.kill(); process?.kill();
process = null; process = null;
return true;
} }
@override @override
Future<bool> get isInit { Future<bool> preload() async {
return _invoke<bool>( await serverCompleter.future;
method: ActionMethod.getIsInit, return true;
);
}
@override
forceGc() {
_prueInvoke(method: ActionMethod.forceGc);
}
@override
FutureOr<String> validateConfig(String data) {
return _invoke<String>(
method: ActionMethod.validateConfig,
data: data,
);
}
@override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await _invoke<String>(
method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams),
timeout: const Duration(seconds: 20),
);
}
@override
Future<String> getProxies() {
return _invoke<String>(
method: ActionMethod.getProxies,
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return _invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return _invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return _invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
return _invoke<String>(
method: ActionMethod.updateGeoData,
data: json.encode(
{
"geoType": geoType,
"geoName": geoName,
},
),
);
}
@override
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return _invoke<String>(
method: ActionMethod.sideLoadExternalProvider,
data: json.encode({
"providerName": providerName,
"data": data,
}),
);
}
@override
Future<String> updateExternalProvider(String providerName) {
return _invoke<String>(
method: ActionMethod.updateExternalProvider,
data: providerName,
);
}
@override
FutureOr<String> getConnections() {
return _invoke<String>(
method: ActionMethod.getConnections,
);
}
@override
Future<bool> closeConnections() {
return _invoke<bool>(
method: ActionMethod.closeConnections,
);
}
@override
Future<bool> closeConnection(String id) {
return _invoke<bool>(
method: ActionMethod.closeConnection,
data: id,
);
}
@override
FutureOr<String> getTotalTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTotalTraffic,
data: value,
);
}
@override
FutureOr<String> getTraffic(bool value) {
return _invoke<String>(
method: ActionMethod.getTraffic,
data: value,
);
}
@override
resetTraffic() {
_prueInvoke(method: ActionMethod.resetTraffic);
}
@override
startLog() {
_prueInvoke(method: ActionMethod.startLog);
}
@override
stopLog() {
_prueInvoke(method: ActionMethod.stopLog);
}
@override
Future<bool> startListener() {
return _invoke<bool>(
method: ActionMethod.startListener,
);
}
@override
stopListener() {
return _invoke<bool>(
method: ActionMethod.stopListener,
);
}
@override
Future<String> asyncTestDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
return _invoke<String>(
method: ActionMethod.asyncTestDelay,
data: json.encode(delayParams),
timeout: Duration(
milliseconds: 6000,
),
onTimeout: () {
return json.encode(
Delay(
name: proxyName,
value: -1,
),
);
},
);
}
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
}
@override
FutureOr<String> getCountryCode(String ip) {
return _invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return _invoke<String>(
method: ActionMethod.getMemory,
);
} }
} }

View File

@@ -1,28 +1,92 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension ColorExtension on Color { extension ColorExtension on Color {
Color get opacity80 {
Color get toLight { return withAlpha(204);
return withOpacity(0.8);
} }
Color get toLighter { Color get opacity60 {
return withOpacity(0.6); return withAlpha(153);
} }
Color get toSoft { Color get opacity50 {
return withOpacity(0.12); return withAlpha(128);
} }
Color get toLittle { Color get opacity38 {
return withOpacity(0.03); return withAlpha(97);
} }
Color darken([double amount = .1]) { Color get opacity30 {
assert(amount >= 0 && amount <= 1); return withAlpha(77);
final hsl = HSLColor.fromColor(this); }
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor(); Color get opacity15 {
return withAlpha(38);
}
Color get opacity10 {
return withAlpha(15);
}
Color get opacity3 {
return withAlpha(76);
}
Color get opacity0 {
return withAlpha(0);
}
int get value32bit {
return _floatToInt8(a) << 24 |
_floatToInt8(r) << 16 |
_floatToInt8(g) << 8 |
_floatToInt8(b) << 0;
}
int get alpha8bit => (0xff000000 & value32bit) >> 24;
int get red8bit => (0x00ff0000 & value32bit) >> 16;
int get green8bit => (0x0000ff00 & value32bit) >> 8;
int get blue8bit => (0x000000ff & value32bit) >> 0;
int _floatToInt8(double x) {
return (x * 255.0).round() & 0xff;
}
Color lighten([double amount = 10]) {
if (amount <= 0) return this;
if (amount > 100) return Colors.white;
final HSLColor hsl = this == const Color(0xFF000000)
? HSLColor.fromColor(this).withSaturation(0)
: HSLColor.fromColor(this);
return hsl
.withLightness(min(1, max(0, hsl.lightness + amount / 100)))
.toColor();
}
String get hex {
final value = toARGB32();
final red = (value >> 16) & 0xFF;
final green = (value >> 8) & 0xFF;
final blue = value & 0xFF;
return '#${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}'
.toUpperCase();
}
Color darken([final int amount = 10]) {
if (amount <= 0) return this;
if (amount > 100) return Colors.black;
final HSLColor hsl = HSLColor.fromColor(this);
return hsl
.withLightness(min(1, max(0, hsl.lightness - amount / 100)))
.toColor();
} }
Color blendDarken( Color blendDarken(
@@ -51,11 +115,11 @@ extension ColorExtension on Color {
} }
extension ColorSchemeExtension on ColorScheme { extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
? copyWith( ? copyWith(
surface: Colors.black, surface: Colors.black,
surfaceContainer: surfaceContainer.darken( surfaceContainer: surfaceContainer.darken(
0.05, 5,
), ),
) )
: this; : this;

View File

@@ -19,7 +19,7 @@ export 'navigation.dart';
export 'navigator.dart'; export 'navigator.dart';
export 'network.dart'; export 'network.dart';
export 'num.dart'; export 'num.dart';
export 'other.dart'; export 'utils.dart';
export 'package.dart'; export 'package.dart';
export 'path.dart'; export 'path.dart';
export 'picker.dart'; export 'picker.dart';
@@ -34,3 +34,6 @@ export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart'; export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
const appName = "FlClash"; const appName = "FlClash";
const appHelperService = "FlClashHelperService"; const appHelperService = "FlClashHelperService";
const coreName = "clash.meta"; const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash"; const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890; const helperPort = 47890;
@@ -19,6 +21,11 @@ const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16, vertical: 16,
horizontal: 16, horizontal: 16,
); );
double textScaleFactor = min(
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
1.2,
);
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
@@ -33,16 +40,6 @@ final double kHeaderHeight = system.isDesktop
? 40 ? 40
: 28 : 28
: 0; : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles"; const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1"; const localhost = "127.0.0.1";
const clashConfigKey = "clash_config"; const clashConfigKey = "clash_config";
@@ -53,10 +50,8 @@ const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090"; const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600; const maxMobileWidth = 600;
const maxLaptopWidth = 840; const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204"; const defaultTestUrl = "https://www.gstatic.com/generate_204";
final filter = ImageFilter.blur( final commonFilter = ImageFilter.blur(
sigmaX: 5, sigmaX: 5,
sigmaY: 5, sigmaY: 5,
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
@@ -73,7 +68,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>(); const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryIterableEquality = const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>(); IterableEquality<MapEntry<String, String>>();
const stringAndIntQMapEquality = MapEquality<String, int?>(); const delayMapEquality = MapEquality<String, Map<String, int?>>();
const stringSetEquality = SetEquality<String>(); const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>(); const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
@@ -83,8 +78,22 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3], ViewMode.desktop: [4, 3],
}; };
const defaultPrimaryColor = Colors.brown; const defaultPrimaryColor = 0xFF795548;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0); return max(lines * 84 * textScaleFactor + (lines - 1) * 16, 0);
} }
final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [
defaultPrimaryColor,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
0XFFD8C0C3,
0XFF665390,
];

View File

@@ -22,4 +22,23 @@ extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme; ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme; TextTheme get textTheme => Theme.of(this).textTheme;
T? findLastStateOfType<T extends State>() {
T? state;
visitor(Element element) {
if(!element.mounted){
return;
}
if(element is StatefulElement){
if (element.state is T) {
state = element.state as T;
}
}
element.visitChildren(visitor);
}
visitor(this as Element);
return state;
}
} }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
class Debouncer { class Debouncer {
Map<dynamic, Timer> operators = {}; final Map<dynamic, Timer?> _operations = {};
call( call(
dynamic tag, dynamic tag,
@@ -9,14 +9,15 @@ class Debouncer {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
}) { }) {
final timer = operators[tag]; final timer = _operations[tag];
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
} }
operators[tag] = Timer( _operations[tag] = Timer(
duration, duration,
() { () {
operators.remove(tag); _operations[tag]?.cancel();
_operations.remove(tag);
Function.apply( Function.apply(
func, func,
args, args,
@@ -26,8 +27,61 @@ class Debouncer {
} }
cancel(dynamic tag) { cancel(dynamic tag) {
operators[tag]?.cancel(); _operations[tag]?.cancel();
_operations[tag] = null;
} }
} }
class Throttler {
final Map<dynamic, Timer?> _operations = {};
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer(); final debouncer = Debouncer();
final throttler = Throttler();

View File

@@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart';
extension CompleterExt<T> on Completer<T> { extension CompleterExt<T> on Completer<T> {
safeFuture({ safeFuture({
Duration? timeout, Duration? timeout,
@@ -8,8 +10,8 @@ extension CompleterExt<T> on Completer<T> {
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
required String functionName, required String functionName,
}) { }) {
final realTimeout = timeout ?? const Duration(seconds: 6); final realTimeout = timeout ?? const Duration(seconds: 30);
Timer(realTimeout + Duration(milliseconds: 1000), () { Timer(realTimeout + commonDuration, () {
if (onLast != null) { if (onLast != null) {
onLast(); onLast();
} }

View File

@@ -1,26 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final port = globalState.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null;
commonPrint.log("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
}
@override @override
HttpClient createHttpClient(SecurityContext? context) { HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context); final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true; client.findProxy = handleFindProxy;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client; return client;
} }
} }

View File

@@ -65,3 +65,12 @@ extension DoubleListExt on List<double> {
return -1; return -1;
} }
} }
extension MapExt<K, V> on Map<K, V> {
getCacheValue(K key, V defaultValue) {
if (this[key] == null) {
this[key] = defaultValue;
}
return this[key];
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart'; import 'constant.dart';
@@ -34,6 +35,9 @@ class AutoLaunch {
} }
updateStatus(bool isAutoLaunch) async { updateStatus(bool isAutoLaunch) async {
if(kDebugMode){
return;
}
if (await isEnable == isAutoLaunch) return; if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) { if (isAutoLaunch == true) {
enable(); enable();

View File

@@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'print.dart';
typedef InstallConfigCallBack = void Function(String url); typedef InstallConfigCallBack = void Function(String url);
@@ -15,11 +16,11 @@ class LinkManager {
} }
initAppLinksListen(installConfigCallBack) async { initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen"); commonPrint.log("initAppLinksListen");
destroy(); destroy();
subscription = _appLinks.uriLinkStream.listen( subscription = _appLinks.uriLinkStream.listen(
(uri) { (uri) {
debugPrint('onAppLink: $uri'); commonPrint.log('onAppLink: $uri');
if (uri.host == 'install-config') { if (uri.host == 'install-config') {
final parameters = uri.queryParameters; final parameters = uri.queryParameters;
final url = parameters['url']; final url = parameters['url'];

View File

@@ -1,3 +1,72 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list}) : _list = list ?? [];
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
clear() {
_list.clear();
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
FixedList<T> copyWith() {
return FixedList(
maxLength,
list: _list,
);
}
}
class FixedMap<K, V> {
int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
return value;
}
clear() {
_map.clear();
_queue.clear();
}
updateMaxSize(int size){
maxSize = size;
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> { extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); return where((item) => list.contains(item)).toList();
@@ -17,8 +86,8 @@ extension ListExtension<T> on List<T> {
} }
List<T> safeSublist(int start) { List<T> safeSublist(int start) {
if(start <= 0) return this; if (start <= 0) return this;
if(start > length) return []; if (start > length) return [];
return sublist(start); return sublist(start);
} }
} }

View File

@@ -15,7 +15,7 @@ class SingleInstanceLock {
Future<bool> acquire() async { Future<bool> acquire() async {
try { try {
final lockFilePath = await appPath.getLockFilePath(); final lockFilePath = await appPath.lockFilePath;
final lockFile = File(lockFilePath); final lockFile = File(lockFilePath);
await lockFile.create(); await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write); _accessFile = await lockFile.open(mode: FileMode.write);

View File

@@ -4,20 +4,28 @@ import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScale;
late BuildContext context; final BuildContext context;
Measure.of(this.context) Measure.of(this.context)
: _textScale = TextScaler.linear( : _textScale = TextScaler.linear(
WidgetsBinding.instance.platformDispatcher.textScaleFactor, textScaleFactor,
); );
Size computeTextSize(Text text) { Size computeTextSize(
Text text, {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style), text: TextSpan(
text: text.data,
style: text.style,
),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScale,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout(); )..layout(
maxWidth: maxWidth,
);
return textPainter.size; return textPainter.size;
} }

53
lib/common/mixin.dart Normal file
View File

@@ -0,0 +1,53 @@
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) {
state = value;
}
@override
bool updateShouldNotify(previous, next) {
final res = super.updateShouldNotify(previous, next);
if (res) {
onUpdate(next);
}
return res;
}
onUpdate(T value) {}
}
mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() {
initPageState();
}
initPageState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final commonScaffoldState = context.commonScaffoldState;
commonScaffoldState?.actions = actions;
commonScaffoldState?.floatingActionButton = floatingActionButton;
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
commonScaffoldState?.updateSearchState(
(_) => onSearch != null
? AppBarSearchState(
onSearch: onSearch!,
)
: null,
);
});
}
void onPageHidden() {}
List<Widget> get actions => [];
Widget? get floatingActionButton => null;
Function(String)? get onSearch => null;
Function(List<String>)? get onKeywordsUpdate => null;
}

View File

@@ -6,55 +6,82 @@ import 'package:flutter/material.dart';
class Navigation { class Navigation {
static Navigation? _instance; static Navigation? _instance;
getItems({ List<NavigationItem> getItems({
bool openLogs = false, bool openLogs = false,
bool hasProxies = false, bool hasProxies = false,
}) { }) {
return [ return [
const NavigationItem( const NavigationItem(
icon: Icon(Icons.space_dashboard), icon: Icon(Icons.space_dashboard),
label: "dashboard", label: PageLabel.dashboard,
fragment: DashboardFragment(), keep: false,
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard),
),
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.rocket), icon: const Icon(Icons.article),
label: "proxies", label: PageLabel.proxies,
fragment: const ProxiesFragment(), fragment: const ProxiesFragment(
key: GlobalObjectKey(
PageLabel.proxies,
),
),
modes: hasProxies modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop] ? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [], : [],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.folder), icon: Icon(Icons.folder),
label: "profiles", label: PageLabel.profiles,
fragment: ProfilesFragment(), fragment: ProfilesFragment(
key: GlobalObjectKey(
PageLabel.profiles,
),
),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: "requests", label: PageLabel.requests,
fragment: RequestsFragment(), fragment: RequestsFragment(
key: GlobalObjectKey(
PageLabel.requests,
),
),
description: "requestsDesc", description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.ballot),
label: "connections", label: PageLabel.connections,
fragment: ConnectionsFragment(), fragment: ConnectionsFragment(
key: GlobalObjectKey(
PageLabel.connections,
),
),
description: "connectionsDesc", description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.storage), icon: Icon(Icons.storage),
label: "resources", label: PageLabel.resources,
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,
fragment: Resources(), fragment: Resources(
modes: [NavigationItemMode.desktop, NavigationItemMode.more], key: GlobalObjectKey(
PageLabel.resources,
),
),
modes: [NavigationItemMode.more],
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),
label: "logs", label: PageLabel.logs,
fragment: const LogsFragment(), fragment: const LogsFragment(
key: GlobalObjectKey(
PageLabel.logs,
),
),
description: "logsDesc", description: "logsDesc",
modes: openLogs modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more] ? [NavigationItemMode.desktop, NavigationItemMode.more]
@@ -62,8 +89,12 @@ class Navigation {
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.construction), icon: Icon(Icons.construction),
label: "tools", label: PageLabel.tools,
fragment: ToolsFragment(), fragment: ToolsFragment(
key: GlobalObjectKey(
PageLabel.tools,
),
),
modes: [NavigationItemMode.desktop, NavigationItemMode.mobile], modes: [NavigationItemMode.desktop, NavigationItemMode.mobile],
), ),
]; ];

View File

@@ -1,8 +1,18 @@
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BaseNavigator { class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async { static Future<T?> push<T>(BuildContext context, Widget child) async {
if (globalState.appState.viewMode != ViewMode.mobile) {
return await Navigator.of(context).push<T>(
CommonDesktopRoute(
builder: (context) => child,
),
);
}
return await Navigator.of(context).push<T>( return await Navigator.of(context).push<T>(
CommonRoute( CommonRoute(
builder: (context) => child, builder: (context) => child,
@@ -11,6 +21,46 @@ class BaseNavigator {
} }
} }
class CommonDesktopRoute<T> extends PageRoute<T> {
final Widget Function(BuildContext context) builder;
CommonDesktopRoute({
required this.builder,
});
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final Widget result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: FadeTransition(
opacity: animation,
child: result,
),
);
}
@override
bool get maintainState => true;
@override
Duration get transitionDuration => Duration(milliseconds: 200);
@override
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
}
class CommonRoute<T> extends MaterialPageRoute<T> { class CommonRoute<T> extends MaterialPageRoute<T> {
CommonRoute({ CommonRoute({
required super.builder, required super.builder,
@@ -20,7 +70,7 @@ class CommonRoute<T> extends MaterialPageRoute<T> {
Duration get transitionDuration => const Duration(milliseconds: 500); Duration get transitionDuration => const Duration(milliseconds: 500);
@override @override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300); Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -144,7 +194,7 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
_primaryPositionCurve = CurvedAnimation( _primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation, parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut, curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.easeInOut, reverseCurve: Curves.fastEaseInToSlowEaseOut.flipped,
); );
_secondaryPositionCurve = CurvedAnimation( _secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation, parent: widget.secondaryRouteAnimation,
@@ -168,9 +218,8 @@ class _CommonPageTransitionState extends State<CommonPageTransition> {
begin: const _CommonEdgeShadowDecoration(), begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration( end: _CommonEdgeShadowDecoration(
<Color>[ <Color>[
widget.context.colorScheme.inverseSurface.withOpacity( widget.context.colorScheme.inverseSurface
0.06, .withValues(alpha: 0.02),
),
Colors.transparent, Colors.transparent,
], ],
), ),
@@ -224,7 +273,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 0.05 * configuration.size!.width; final double shadowWidth = 1 * configuration.size!.width;
final double shadowHeight = configuration.size!.height; final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1); final double bandWidth = shadowWidth / (colors.length - 1);

View File

@@ -2,8 +2,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension NumExt on num { extension NumExt on num {
String fixed({digit = 2}) { String fixed({decimals = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit); String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), '');
if (formatted.endsWith('.')) {
formatted = formatted.substring(0, formatted.length - 1);
}
}
return formatted;
} }
} }

View File

@@ -48,35 +48,40 @@ class AppPath {
return join(executableDirPath, "$appHelperService$executableExtension"); return join(executableDirPath, "$appHelperService$executableExtension");
} }
Future<String> getDownloadDirPath() async { Future<String> get downloadDirPath async {
final directory = await downloadDir.future; final directory = await downloadDir.future;
return directory.path; return directory.path;
} }
Future<String> getHomeDirPath() async { Future<String> get homeDirPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return directory.path; return directory.path;
} }
Future<String> getLockFilePath() async { Future<String> get lockFilePath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, "FlClash.lock"); return join(directory.path, "FlClash.lock");
} }
Future<String> getProfilesPath() async { Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future;
return join(directory.path, "shared_preferences.json");
}
Future<String> get profilesPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String?> getProfilePath(String? id) async { Future<String?> getProfilePath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "$id.yaml"); return join(directory, "$id.yaml");
} }
Future<String?> getProvidersPath(String? id) async { Future<String?> getProvidersPath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await getProfilesPath(); final directory = await profilesPath;
return join(directory, "providers", id); return join(directory, "providers", id);
} }

View File

@@ -4,13 +4,14 @@ import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class Picker { class Picker {
Future<PlatformFile?> pickerFile() async { Future<PlatformFile?> pickerFile() async {
final filePickerResult = await FilePicker.platform.pickFiles( final filePickerResult = await FilePicker.platform.pickFiles(
withData: true, withData: true,
allowMultiple: false, allowMultiple: false,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
); );
return filePickerResult?.files.first; return filePickerResult?.files.first;
} }
@@ -18,7 +19,7 @@ class Picker {
Future<String?> saveFile(String fileName, Uint8List bytes) async { Future<String?> saveFile(String fileName, Uint8List bytes) async {
final path = await FilePicker.platform.saveFile( final path = await FilePicker.platform.saveFile(
fileName: fileName, fileName: fileName,
initialDirectory: await appPath.getDownloadDirPath(), initialDirectory: await appPath.downloadDirPath,
bytes: Platform.isAndroid ? bytes : null, bytes: Platform.isAndroid ? bytes : null,
); );
if (!Platform.isAndroid && path != null) { if (!Platform.isAndroid && path != null) {
@@ -30,9 +31,14 @@ class Picker {
Future<String?> pickerConfigQRCode() async { Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery); final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes(); if (xFile == null) {
if (bytes == null) return null; return null;
final result = await other.parseQRCode(bytes); }
final controller = MobileScannerController();
final capture = await controller.analyzeImage(xFile.path, formats: [
BarcodeFormat.qrCode,
]);
final result = capture?.barcodes.first.rawValue;
if (result == null || !result.isUrl) { if (result == null || !result.isUrl) {
throw appLocalizations.pleaseUploadValidQrcode; throw appLocalizations.pleaseUploadValidQrcode;
} }

View File

@@ -1,19 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:fl_clash/models/models.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/models.dart';
import 'constant.dart'; import 'constant.dart';
class Preferences { class Preferences {
static Preferences? _instance; static Preferences? _instance;
Completer<SharedPreferences> sharedPreferencesCompleter = Completer(); Completer<SharedPreferences?> sharedPreferencesCompleter = Completer();
Future<bool> get isInit async =>
await sharedPreferencesCompleter.future != null;
Preferences._internal() { Preferences._internal() {
SharedPreferences.getInstance() SharedPreferences.getInstance()
.then((value) => sharedPreferencesCompleter.complete(value)); .then((value) => sharedPreferencesCompleter.complete(value))
.onError((_, __) => sharedPreferencesCompleter.complete(null));
} }
factory Preferences() { factory Preferences() {
@@ -23,50 +26,38 @@ class Preferences {
Future<ClashConfig?> getClashConfig() async { Future<ClashConfig?> getClashConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final clashConfigString = preferences.getString(clashConfigKey); final clashConfigString = preferences?.getString(clashConfigKey);
if (clashConfigString == null) return null; if (clashConfigString == null) return null;
final clashConfigMap = json.decode(clashConfigString); final clashConfigMap = json.decode(clashConfigString);
try { return ClashConfig.fromJson(clashConfigMap);
return ClashConfig.fromJson(clashConfigMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
}
Future<bool> saveClashConfig(ClashConfig clashConfig) async {
final preferences = await sharedPreferencesCompleter.future;
return preferences.setString(
clashConfigKey,
json.encode(clashConfig),
);
} }
Future<Config?> getConfig() async { Future<Config?> getConfig() async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
final configString = preferences.getString(configKey); final configString = preferences?.getString(configKey);
if (configString == null) return null; if (configString == null) return null;
final configMap = json.decode(configString); final configMap = json.decode(configString);
try { return Config.compatibleFromJson(configMap);
return Config.fromJson(configMap);
} catch (e) {
debugPrint(e.toString());
return null;
}
} }
Future<bool> saveConfig(Config config) async { Future<bool> saveConfig(Config config) async {
final preferences = await sharedPreferencesCompleter.future; final preferences = await sharedPreferencesCompleter.future;
return preferences.setString( return await preferences?.setString(
configKey, configKey,
json.encode(config), json.encode(config),
); ) ??
false;
}
clearClashConfig() async {
final preferences = await sharedPreferencesCompleter.future;
preferences?.remove(clashConfigKey);
} }
clearPreferences() async { clearPreferences() async {
final sharedPreferencesIns = await sharedPreferencesCompleter.future; final sharedPreferencesIns = await sharedPreferencesCompleter.future;
sharedPreferencesIns.clear(); sharedPreferencesIns?.clear();
} }
} }
final preferences = Preferences(); final preferences = Preferences();

31
lib/common/print.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class CommonPrint {
static CommonPrint? _instance;
CommonPrint._internal();
factory CommonPrint() {
_instance ??= CommonPrint._internal();
return _instance!;
}
log(String? text) {
final payload = "[FlClash] $text";
debugPrint(payload);
if (globalState.isService) {
return;
}
globalState.appController.addLog(
Log(
logLevel: LogLevel.info,
payload: payload,
),
);
}
}
final commonPrint = CommonPrint();

View File

@@ -14,15 +14,13 @@ class Protocol {
void register(String scheme) { void register(String scheme) {
String protocolRegKey = 'Software\\Classes\\$scheme'; String protocolRegKey = 'Software\\Classes\\$scheme';
RegistryValue protocolRegValue = const RegistryValue( RegistryValue protocolRegValue = RegistryValue.string(
'URL Protocol', 'URL Protocol',
RegistryValueType.string,
'', '',
); );
String protocolCmdRegKey = 'shell\\open\\command'; String protocolCmdRegKey = 'shell\\open\\command';
RegistryValue protocolCmdRegValue = RegistryValue( RegistryValue protocolCmdRegValue = RegistryValue.string(
'', '',
RegistryValueType.string,
'"${Platform.resolvedExecutable}" "%1"', '"${Platform.resolvedExecutable}" "%1"',
); );
final regKey = Registry.currentUser.createKey(protocolRegKey); final regKey = Registry.currentUser.createKey(protocolRegKey);
@@ -31,4 +29,4 @@ class Protocol {
} }
} }
final protocol = Protocol(); final protocol = Protocol();

57
lib/common/render.dart Normal file
View File

@@ -0,0 +1,57 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/scheduler.dart';
class Render {
static Render? _instance;
bool _isPaused = false;
final _dispatcher = SchedulerBinding.instance.platformDispatcher;
FrameCallback? _beginFrame;
VoidCallback? _drawFrame;
Render._internal();
factory Render() {
_instance ??= Render._internal();
return _instance!;
}
active() {
resume();
pause();
}
pause() {
throttler.call(
DebounceTag.renderPause,
_pause,
duration: Duration(seconds: 5),
);
}
resume() {
throttler.cancel(DebounceTag.renderPause);
_resume();
}
void _pause() async {
if (_isPaused) return;
_isPaused = true;
_beginFrame = _dispatcher.onBeginFrame;
_drawFrame = _dispatcher.onDrawFrame;
_dispatcher.onBeginFrame = null;
_dispatcher.onDrawFrame = null;
commonPrint.log("pause");
}
void _resume() {
if (!_isPaused) return;
_isPaused = false;
_dispatcher.onBeginFrame = _beginFrame;
_dispatcher.onDrawFrame = _drawFrame;
_dispatcher.scheduleFrame();
commonPrint.log("resume");
}
}
final Render? render = system.isDesktop ? Render() : null;

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -11,33 +11,35 @@ import 'package:flutter/cupertino.dart';
class Request { class Request {
late final Dio _dio; late final Dio _dio;
late final Dio _clashDio;
String? userAgent; String? userAgent;
Request() { Request() {
_dio = Dio(); _dio = Dio(
_dio.interceptors.add( BaseOptions(
InterceptorsWrapper( headers: {
onRequest: (options, handler) { "User-Agent": browserUa,
return handler.next(options); // 继续请求
}, },
), ),
); );
_clashDio = Dio();
_clashDio.httpClientAdapter = IOHttpClientAdapter(createHttpClient: () {
final client = HttpClient();
client.findProxy = (Uri uri) {
client.userAgent = globalState.ua;
return FlClashHttpOverrides.handleFindProxy(uri);
};
return client;
});
} }
Future<Response> getFileResponseForUrl(String url) async { Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio final response = await _clashDio.get(
.get( url,
url, options: Options(
options: Options( responseType: ResponseType.bytes,
headers: { ),
"User-Agent": globalState.appController.clashConfig.globalUa );
},
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration * 6,
);
return response; return response;
} }
@@ -66,36 +68,43 @@ class Request {
final remoteVersion = data['tag_name']; final remoteVersion = data['tag_name'];
final version = globalState.packageInfo.version; final version = globalState.packageInfo.version;
final hasUpdate = final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0; utils.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return null; if (!hasUpdate) return null;
return data; return data;
} }
final List<String> _ipInfoSources = [ final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/?fields=ip&output=csv", "https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://ipinfo.io/ip", "https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ifconfig.me/ip/", "https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
]; "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources) { for (final source in _ipInfoSources.entries) {
try { try {
final response = await _dio final response = await Dio()
.get<String>( .get<Map<String, dynamic>>(
source, source.key,
cancelToken: cancelToken, cancelToken: cancelToken,
options: Options(
responseType: ResponseType.json,
),
) )
.timeout(httpTimeoutDuration); .timeout(
Duration(
seconds: 30,
),
);
if (response.statusCode != 200 || response.data == null) { if (response.statusCode != 200 || response.data == null) {
continue; continue;
} }
final ipInfo = await clashCore.getCountryCode(response.data!); if (response.data == null) {
if (ipInfo == null && source != _ipInfoSources.last) {
continue; continue;
} }
return ipInfo; return source.value(response.data!);
} catch (e) { } catch (e) {
debugPrint("checkIp error ===> $e"); commonPrint.log("checkIp error ===> $e");
if (e is DioException && e.type == DioExceptionType.cancel) { if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled"; throw "cancelled";
} }

View File

@@ -1,6 +1,8 @@
import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/widgets/scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class BaseScrollBehavior extends MaterialScrollBehavior { class BaseScrollBehavior extends MaterialScrollBehavior {
@@ -33,10 +35,101 @@ class ShowBarScrollBehavior extends BaseScrollBehavior {
Widget child, Widget child,
ScrollableDetails details, ScrollableDetails details,
) { ) {
return Scrollbar( return CommonAutoHiddenScrollBar(
interactive: true,
controller: details.controller, controller: details.controller,
child: child, child: child,
); );
} }
} }
class NextClampingScrollPhysics extends ClampingScrollPhysics {
const NextClampingScrollPhysics({super.parent});
@override
NextClampingScrollPhysics applyTo(ScrollPhysics? ancestor) {
return NextClampingScrollPhysics(parent: buildParent(ancestor));
}
@override
Simulation? createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = toleranceFor(position);
if (position.outOfRange) {
double? end;
if (position.pixels > position.maxScrollExtent) {
end = position.maxScrollExtent;
}
if (position.pixels < position.minScrollExtent) {
end = position.minScrollExtent;
}
assert(end != null);
return ScrollSpringSimulation(
spring,
end!,
end,
min(0.0, velocity),
tolerance: tolerance,
);
}
if (velocity.abs() < tolerance.velocity) {
return null;
}
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent) {
return null;
}
if (velocity < 0.0 && position.pixels <= position.minScrollExtent) {
return null;
}
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
}
class ReverseScrollController extends ScrollController {
ReverseScrollController({
super.initialScrollOffset,
super.keepScrollOffset,
super.debugLabel,
});
@override
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return ReverseScrollPosition(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class ReverseScrollPosition extends ScrollPositionWithSingleContext {
ReverseScrollPosition({
required super.physics,
required super.context,
super.initialPixels = 0.0,
super.keepScrollOffset,
super.oldPosition,
super.debugLabel,
});
bool _isInit = false;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!_isInit) {
correctPixels(maxScrollExtent);
_isInit = true;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
}
}

0
lib/common/state.dart Normal file
View File

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'print.dart';
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
@@ -43,8 +43,17 @@ extension StringExtension on String {
RegExp(this); RegExp(this);
return true; return true;
} catch (e) { } catch (e) {
debugPrint(e.toString()); commonPrint.log(e.toString());
return false; return false;
} }
} }
} }
extension StringExtensionSafe on String? {
String getSafeValue(String defaultValue) {
if (this == null || this!.isEmpty) {
return defaultValue;
}
return this!;
}
}

View File

@@ -46,7 +46,7 @@ class System {
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]); final result = await Process.run('stat', ['-c', '%U:%G %A', corePath]);
final output = result.stdout.trim(); final output = result.stdout.trim();
if (output.startsWith('root:') && output.contains('rwx')) { if (output.startsWith('root:') && output.contains('rws')) {
return true; return true;
} }
return false; return false;

View File

@@ -1,15 +1,20 @@
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'color.dart'; import 'color.dart';
extension TextStyleExtension on TextStyle { extension TextStyleExtension on TextStyle {
TextStyle get toLight => copyWith(color: color?.toLight); TextStyle get toLight => copyWith(color: color?.opacity80);
TextStyle get toLighter => copyWith(color: color?.toLighter); TextStyle get toLighter => copyWith(color: color?.opacity60);
TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500); TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500);
TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); TextStyle get toBold => copyWith(fontWeight: FontWeight.bold);
TextStyle get toJetBrainsMono => copyWith(
fontFamily: FontFamily.jetBrainsMono.value,
);
TextStyle adjustSize(int size) => copyWith( TextStyle adjustSize(int size) => copyWith(
fontSize: fontSize! + size, fontSize: fontSize! + size,
); );

39
lib/common/theme.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class CommonTheme {
final BuildContext context;
final Map<String, Color> _colorMap;
CommonTheme.of(this.context) : _colorMap = {};
Color get darkenSecondaryContainer {
return _colorMap.getCacheValue(
"darkenSecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.1),
);
}
Color get darkenSecondaryContainerLighter {
return _colorMap.getCacheValue(
"darkenSecondaryContainerLighter",
context.colorScheme.secondaryContainer
.blendDarken(context, factor: 0.1)
.opacity60,
);
}
Color get darken2SecondaryContainer {
return _colorMap.getCacheValue(
"darken2SecondaryContainer",
context.colorScheme.secondaryContainer.blendDarken(context, factor: 0.2),
);
}
Color get darken3PrimaryContainer {
return _colorMap.getCacheValue(
"darken3PrimaryContainer",
context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/utils.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
@@ -10,7 +11,6 @@ import 'package:tray_manager/tray_manager.dart';
import 'app_localizations.dart'; import 'app_localizations.dart';
import 'constant.dart'; import 'constant.dart';
import 'other.dart';
import 'window.dart'; import 'window.dart';
class Tray { class Tray {
@@ -25,7 +25,7 @@ class Tray {
await trayManager.destroy(); await trayManager.destroy();
} }
await trayManager.setIcon( await trayManager.setIcon(
other.getTrayIconPath( utils.getTrayIconPath(
brightness: brightness ?? brightness: brightness ??
WidgetsBinding.instance.platformDispatcher.platformBrightness, WidgetsBinding.instance.platformDispatcher.platformBrightness,
), ),
@@ -39,10 +39,7 @@ class Tray {
} }
update({ update({
required AppState appState, required TrayState trayState,
required AppFlowingState appFlowingState,
required Config config,
required ClashConfig clashConfig,
bool focus = false, bool focus = false,
}) async { }) async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -50,7 +47,7 @@ class Tray {
} }
if (!Platform.isLinux) { if (!Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
@@ -63,9 +60,7 @@ class Tray {
); );
menuItems.add(showMenuItem); menuItems.add(showMenuItem);
final startMenuItem = MenuItem.checkbox( final startMenuItem = MenuItem.checkbox(
label: appFlowingState.isStart label: trayState.isStart ? appLocalizations.stop : appLocalizations.start,
? appLocalizations.stop
: appLocalizations.start,
onClick: (_) async { onClick: (_) async {
globalState.appController.updateStart(); globalState.appController.updateStart();
}, },
@@ -80,23 +75,22 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.changeMode(mode); globalState.appController.changeMode(mode);
}, },
checked: mode == clashConfig.mode, checked: mode == trayState.mode,
), ),
); );
} }
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
if (!Platform.isWindows) { if (!Platform.isWindows) {
final groups = appState.currentGroups; for (final group in trayState.groups) {
for (final group in groups) {
List<MenuItem> subMenuItems = []; List<MenuItem> subMenuItems = [];
for (final proxy in group.all) { for (final proxy in group.all) {
subMenuItems.add( subMenuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: proxy.name, label: proxy.name,
checked: appState.selectedMap[group.name] == proxy.name, checked: trayState.selectedMap[group.name] == proxy.name,
onClick: (_) { onClick: (_) {
final appController = globalState.appController; final appController = globalState.appController;
appController.config.updateCurrentSelectedMap( appController.updateCurrentSelectedMap(
group.name, group.name,
proxy.name, proxy.name,
); );
@@ -117,18 +111,18 @@ class Tray {
), ),
); );
} }
if (groups.isNotEmpty) { if (trayState.groups.isNotEmpty) {
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
} }
} }
if (appFlowingState.isStart) { if (trayState.isStart) {
menuItems.add( menuItems.add(
MenuItem.checkbox( MenuItem.checkbox(
label: appLocalizations.tun, label: appLocalizations.tun,
onClick: (_) { onClick: (_) {
globalState.appController.updateTun(); globalState.appController.updateTun();
}, },
checked: clashConfig.tun.enable, checked: trayState.tunEnable,
), ),
); );
menuItems.add( menuItems.add(
@@ -137,7 +131,7 @@ class Tray {
onClick: (_) { onClick: (_) {
globalState.appController.updateSystemProxy(); globalState.appController.updateSystemProxy();
}, },
checked: config.networkProps.systemProxy, checked: trayState.systemProxy,
), ),
); );
menuItems.add(MenuItem.separator()); menuItems.add(MenuItem.separator());
@@ -147,12 +141,12 @@ class Tray {
onClick: (_) async { onClick: (_) async {
globalState.appController.updateAutoLaunch(); globalState.appController.updateAutoLaunch();
}, },
checked: config.appSetting.autoLaunch, checked: trayState.autoLaunch,
); );
final copyEnvVarMenuItem = MenuItem( final copyEnvVarMenuItem = MenuItem(
label: appLocalizations.copyEnvVar, label: appLocalizations.copyEnvVar,
onClick: (_) async { onClick: (_) async {
await _copyEnv(clashConfig.mixedPort); await _copyEnv(trayState.port);
}, },
); );
menuItems.add(autoStartMenuItem); menuItems.add(autoStartMenuItem);
@@ -169,12 +163,25 @@ class Tray {
await trayManager.setContextMenu(menu); await trayManager.setContextMenu(menu);
if (Platform.isLinux) { if (Platform.isLinux) {
await _updateSystemTray( await _updateSystemTray(
brightness: appState.brightness, brightness: trayState.brightness,
force: focus, force: focus,
); );
} }
} }
updateTrayTitle([Traffic? traffic]) async {
// if (!Platform.isMacOS) {
// return;
// }
// if (traffic == null) {
// await trayManager.setTitle("");
// } else {
// await trayManager.setTitle(
// "${traffic.up.shortShow} ↑ \n${traffic.down.shortShow} ↓",
// );
// }
}
Future<void> _copyEnv(int port) async { Future<void> _copyEnv(int port) async {
final url = "http://127.0.0.1:$port"; final url = "http://127.0.0.1:$port";

View File

@@ -1,17 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:zxing2/qrcode.dart';
class Other { class Utils {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
if (delay == null) return null; if (delay == null) return null;
if (delay < 0) return Colors.red; if (delay < 0) return Colors.red;
@@ -34,6 +30,26 @@ class Other {
); );
} }
String generateRandomString({int minLength = 10, int maxLength = 100}) {
const latinChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final random = Random();
int length = minLength + random.nextInt(maxLength - minLength + 1);
String result = '';
for (int i = 0; i < length; i++) {
if (random.nextBool()) {
result +=
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
} else {
result += latinChars[random.nextInt(latinChars.length)];
}
}
return result;
}
String get uuidV4 { String get uuidV4 {
final Random random = Random(); final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256)); final bytes = List.generate(16, (_) => random.nextInt(256));
@@ -165,30 +181,6 @@ class Other {
: ""; : "";
} }
Future<String?> parseQRCode(Uint8List? bytes) {
return Isolate.run<String?>(() {
if (bytes == null) return null;
img.Image? image = img.decodeImage(bytes);
LuminanceSource source = RGBLuminanceSource(
image!.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
return result.text;
} catch (_) {
return null;
}
});
}
String? getFileNameForDisposition(String? disposition) { String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null; if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition); final parseValue = HeaderValue.parse(disposition);
@@ -241,6 +233,63 @@ class Other {
return max((viewWidth / 350).floor(), 1); return max((viewWidth / 350).floor(), 1);
} }
final _indexPrimary = [
50,
100,
200,
300,
400,
500,
600,
700,
800,
850,
900,
];
_createPrimarySwatch(Color color) {
final Map<int, Color> swatch = <int, Color>{};
final int a = color.alpha8bit;
final int r = color.red8bit;
final int g = color.green8bit;
final int b = color.blue8bit;
for (final int strength in _indexPrimary) {
final double ds = 0.5 - strength / 1000;
swatch[strength] = Color.fromARGB(
a,
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
);
}
swatch[50] = swatch[50]!.lighten(18);
swatch[100] = swatch[100]!.lighten(16);
swatch[200] = swatch[200]!.lighten(14);
swatch[300] = swatch[300]!.lighten(10);
swatch[400] = swatch[400]!.lighten(6);
swatch[700] = swatch[700]!.darken(2);
swatch[800] = swatch[800]!.darken(3);
swatch[900] = swatch[900]!.darken(4);
return MaterialColor(color.value32bit, swatch);
}
List<Color> getMaterialColorShades(Color color) {
final swatch = _createPrimarySwatch(color);
return <Color>[
if (swatch[50] != null) swatch[50]!,
if (swatch[100] != null) swatch[100]!,
if (swatch[200] != null) swatch[200]!,
if (swatch[300] != null) swatch[300]!,
if (swatch[400] != null) swatch[400]!,
if (swatch[500] != null) swatch[500]!,
if (swatch[600] != null) swatch[600]!,
if (swatch[700] != null) swatch[700]!,
if (swatch[800] != null) swatch[800]!,
if (swatch[850] != null) swatch[850]!,
if (swatch[900] != null) swatch[900]!,
];
}
String getBackupFileName() { String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip"; return "${appName}_backup_${DateTime.now().show}.zip";
} }
@@ -249,11 +298,6 @@ class Other {
return "${appName}_${DateTime.now().show}.log"; return "${appName}_${DateTime.now().show}.log";
} }
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async { Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list( List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false, includeLoopback: false,
@@ -281,4 +325,4 @@ class Other {
} }
} }
final other = Other(); final utils = Utils();

View File

@@ -1,13 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart'; import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class Window { class Window {
init(WindowProps props, int version) async { init(int version) async {
final props = globalState.config.windowProps;
final acquire = await singleInstanceLock.acquire(); final acquire = await singleInstanceLock.acquire();
if (!acquire) { if (!acquire) {
exit(0); exit(0);
@@ -20,10 +21,12 @@ class Window {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions( WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height), size: Size(props.width, props.height),
minimumSize: const Size(380, 500), minimumSize: const Size(380, 400),
); );
if (!Platform.isMacOS || version > 10) { if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden); await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
}
if (!Platform.isMacOS) {
final left = props.left ?? 0; final left = props.left ?? 0;
final top = props.top ?? 0; final top = props.top ?? 0;
final right = left + props.width; final right = left + props.width;
@@ -60,13 +63,16 @@ class Window {
} }
show() async { show() async {
render?.resume();
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false); await windowManager.setSkipTaskbar(false);
} }
Future<bool> isVisible() async { Future<bool> get isVisible async {
return await windowManager.isVisible(); final value = await windowManager.isVisible();
commonPrint.log("window visible check: $value");
return value;
} }
close() async { close() async {
@@ -74,6 +80,7 @@ class Window {
} }
hide() async { hide() async {
render?.pause();
await windowManager.hide(); await windowManager.hide();
await windowManager.setSkipTaskbar(true); await windowManager.setSkipTaskbar(true);
} }

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/cupertino.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
class Windows { class Windows {
@@ -54,7 +53,7 @@ class Windows {
calloc.free(argumentsPtr); calloc.free(argumentsPtr);
calloc.free(operationPtr); calloc.free(operationPtr);
debugPrint("[Windows] runas: $command $arguments resultCode:$result"); commonPrint.log("windows runas: $command $arguments resultCode:$result");
if (result < 42) { if (result < 42) {
return false; return false;

Some files were not shown because too many files have changed in this diff Show More