Compare commits

...

3 Commits

Author SHA1 Message Date
chen08209 c2f3b13deb Update ndk version 2025-04-15 16:52:52 +08:00
chen08209 edc4714488 cache 2025-04-15 16:39:20 +08:00
chen08209 aec325a19a Optimize android vpn performance
Optimize more details
2025-04-15 15:54:36 +08:00
48 changed files with 1301 additions and 528 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
id: setup-ndk id: setup-ndk
with: with:
ndk-version: r26b ndk-version: r28
add-to-path: true add-to-path: true
link-to-sdk: true link-to-sdk: true
-4
View File
@@ -6,9 +6,5 @@
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
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main
+8
View File
@@ -0,0 +1,8 @@
android_arm64:
dart ./setup.dart android --arch arm64
macos_arm64:
dart ./setup.dart macos --arch arm64
android_arm64_core:
dart ./setup.dart android --arch arm64 --out core
macos_arm64_core:
dart ./setup.dart macos --arch arm64 --out core
+5 -23
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"
@@ -34,7 +32,6 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android { android {
namespace "com.follow.clash" namespace "com.follow.clash"
compileSdkVersion 35 compileSdkVersion 35
ndkVersion "27.1.12297006"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
@@ -48,6 +45,7 @@ android {
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
signingConfigs { signingConfigs {
if (isRelease) { if (isRelease) {
release { release {
@@ -84,31 +82,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
}
+10 -6
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>
@@ -1,5 +1,6 @@
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
@@ -52,7 +52,6 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun handleDestroy() { private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.handleStop()
GlobalState.destroyServiceEngine() GlobalState.destroyServiceEngine()
} }
} }
@@ -14,10 +14,9 @@ import androidx.core.content.getSystemService
import com.follow.clash.FlClashApplication 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.core.Core
import com.follow.clash.extensions.awaitResult import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getProtocol
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.StartForegroundParams
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface import com.follow.clash.services.BaseServiceInterface
@@ -40,10 +39,12 @@ import kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel private lateinit var flutterMethodChannel: MethodChannel
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 lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null private var timerJob: Job? = null
private val uidPageNameMap = mutableMapOf<Int, String>()
private val connectivity by lazy { private val connectivity by lazy {
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>() FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
@@ -51,6 +52,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
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()
@@ -60,6 +62,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
override fun onServiceDisconnected(arg: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
isBind = false
flClashService = null flClashService = null
} }
} }
@@ -90,62 +93,6 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(true) result.success(true)
} }
"setProtect" -> {
val fd = call.argument<Int>("fd")
if (fd != null && flClashService is FlClashVpnService) {
try {
(flClashService as FlClashVpnService).protect(fd)
result.success(true)
} catch (e: RuntimeException) {
result.success(false)
}
} else {
result.success(false)
}
}
"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 =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> { else -> {
result.notImplemented() result.notImplemented()
} }
@@ -153,6 +100,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
fun handleStart(options: VpnOptions): Boolean { fun handleStart(options: VpnOptions): Boolean {
if (options.enable != this.options?.enable) {
this.flClashService = null
}
this.options = options this.options = options
when (options.enable) { when (options.enable) {
true -> handleStartVpn() true -> handleStartVpn()
@@ -162,10 +112,9 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun handleStartVpn() { private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin() GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
?.requestVpnPermission { handleStartService()
handleStartService() }
}
} }
fun requestGc() { fun requestGc() {
@@ -235,6 +184,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
private fun startForegroundJob() { private fun startForegroundJob() {
stopForegroundJob()
timerJob = CoroutineScope(Dispatchers.Main).launch { timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) { while (isActive) {
startForeground() startForeground()
@@ -256,26 +206,58 @@ data object 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(); startForegroundJob()
} }
} }
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() { 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() stopForegroundJob()
Core.stopTun()
flClashService?.stop() flClashService?.stop()
GlobalState.handleTryDestroy() GlobalState.handleTryDestroy()
} }
} }
private fun bindService() { private fun bindService() {
val intent = when (options.enable) { if (isBind) {
FlClashApplication.getAppContext().unbindService(connection)
}
val intent = when (options?.enable == true) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java) true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java) false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
} }
+1
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
View File
@@ -0,0 +1 @@
/build
+72
View File
@@ -0,0 +1,72 @@
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"
}
}
tasks.register<Copy>("copyNativeLibs") {
doFirst {
delete("src/main/jniLibs")
}
from("../../libclash/android")
into("src/main/jniLibs")
}
tasks.withType<MergeSourceSetFolders>().configureEach {
dependsOn("copyNativeLibs")
}
afterEvaluate {
tasks.named("assembleDebug").configure {
dependsOn("copyNativeLibs")
}
tasks.named("assembleRelease").configure {
dependsOn("copyNativeLibs")
}
}
dependencies {
implementation("androidx.core:core-ktx:1.16.0")
}
View File
+21
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
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
+41
View File
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
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")
if (EXISTS ${LIB_CLASH_PATH})
message(STATUS "Found libclash.so for ABI ${ANDROID_ABI}")
add_definitions(-D_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 ()
add_library(${CMAKE_PROJECT_NAME} SHARED
jni_helper.cpp
core.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})
endif ()
+88
View File
@@ -0,0 +1,88 @@
#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;
}
#else
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
}
#endif
+70
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);
}
+39
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)
@@ -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")
}
}
@@ -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
}
+1
View File
@@ -24,3 +24,4 @@ plugins {
} }
include ":app" include ":app"
include ':core'
+2 -2
View File
@@ -35,8 +35,8 @@ func (action Action) getResult(data interface{}) []byte {
func handleAction(action *Action, result func(data interface{})) { func handleAction(action *Action, result func(data interface{})) {
switch action.Method { switch action.Method {
case initClashMethod: case initClashMethod:
data := action.Data.(string) paramsString := action.Data.(string)
result(handleInitClash(data)) result(handleInitClash(paramsString))
return return
case getIsInitMethod: case getIsInitMethod:
result(handleGetIsInit()) result(handleGetIsInit())
+77
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
}
+1 -1
View File
@@ -41,6 +41,7 @@ func splitByMultipleSeparators(s string) interface{} {
} }
var ( var (
version = 0
isRunning = false isRunning = false
runLock sync.Mutex runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"} ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
@@ -274,7 +275,6 @@ func patchConfig() {
dialer.DefaultInterface.Store(general.Interface) dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay) adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode) tunnel.SetMode(general.Mode)
tunnel.UpdateRules(currentConfig.Rules, currentConfig.SubRules, currentConfig.RuleProviders)
log.SetLevel(general.LogLevel) log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6 resolver.DisableIPv6 = !general.IPv6
+5 -21
View File
@@ -7,6 +7,11 @@ import (
"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"`
@@ -71,11 +76,7 @@ const (
stopLogMethod Method = "stopLog" stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener" startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener" stopListenerMethod Method = "stopListener"
startTunMethod Method = "startTun"
stopTunMethod Method = "stopTun"
updateDnsMethod Method = "updateDns" updateDnsMethod Method = "updateDns"
setProcessMapMethod Method = "setProcessMap"
setFdMapMethod Method = "setFdMap"
setStateMethod Method = "setState" setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions" getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime" getRunTimeMethod Method = "getRunTime"
@@ -109,20 +110,3 @@ func (message *Message) Json() (string, error) {
data, err := json.Marshal(message) data, err := json.Marshal(message)
return string(data), err return string(data), err
} }
type InvokeMessage struct {
Type InvokeType `json:"type"`
Data interface{} `json:"data"`
}
type InvokeType string
const (
ProtectInvoke InvokeType = "protect"
ProcessInvoke InvokeType = "process"
)
func (message *InvokeMessage) Json() string {
data, _ := json.Marshal(message)
return string(data)
}
+8 -2
View File
@@ -34,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
+90 -184
View File
@@ -4,6 +4,7 @@ package main
import "C" import "C"
import ( import (
"context"
bridge "core/dart-bridge" bridge "core/dart-bridge"
"core/platform" "core/platform"
"core/state" "core/state"
@@ -11,121 +12,116 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process" "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
"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"
"syscall" "syscall"
"time" "time"
"unsafe"
) )
type Fd struct { type TunHandler struct {
Id string `json:"id"` listener *sing_tun.Listener
Value int64 `json:"value"` callback unsafe.Pointer
limit *semaphore.Weighted
} }
type Process struct { func (t *TunHandler) close() {
Id string `json:"id"` _ = t.limit.Acquire(context.TODO(), 4)
Metadata *constant.Metadata `json:"metadata"` defer t.limit.Release(4)
} removeTunHook()
if t.listener != nil {
type ProcessMapItem struct { _ = t.listener.Close()
Id string `json:"id"`
Value string `json:"value"`
}
type InvokeManager struct {
invokeMap sync.Map
chanMap map[string]chan struct{}
chanLock sync.Mutex
}
func NewInvokeManager() *InvokeManager {
return &InvokeManager{
chanMap: make(map[string]chan struct{}),
} }
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
} }
func (m *InvokeManager) completer(id string, value string) { func (t *TunHandler) handleProtect(fd int) {
m.invokeMap.Store(id, value) _ = t.limit.Acquire(context.Background(), 1)
m.chanLock.Lock() defer t.limit.Release(1)
if ch, ok := m.chanMap[id]; ok {
close(ch) if t.listener == nil {
delete(m.chanMap, id) return
} }
m.chanLock.Unlock()
protect(t.callback, fd)
} }
func (m *InvokeManager) await(id string) string { func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
m.chanLock.Lock() _ = t.limit.Acquire(context.Background(), 1)
if _, ok := m.chanMap[id]; !ok { defer t.limit.Release(1)
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
timeout := time.After(500 * time.Millisecond) if t.listener == nil {
select {
case <-ch:
res, ok := m.invokeMap.Load(id)
m.invokeMap.Delete(id)
if ok {
return res.(string)
} else {
return ""
}
case <-timeout:
m.completer(id, "")
return "" 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 (
invokePort int64 = -1 tunLock sync.Mutex
tunListener *sing_tun.Listener runTime *time.Time
fdInvokeMap = NewInvokeManager() errBlocked = errors.New("blocked")
processInvokeMap = NewInvokeManager() tunHandler *TunHandler
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
) )
func handleStartTun(fd int) string {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
if fd == 0 {
now := time.Now()
runTime = &now
} else {
initSocketHook()
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
}
return handleGetRunTime()
}
func handleStopTun() { func handleStopTun() {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
removeSocketHook()
runTime = nil runTime = nil
if tunListener != nil { if tunHandler != nil {
log.Infoln("TUN close") tunHandler.close()
_ = tunListener.Close()
} }
} }
func handleStartTun(fd int, callback unsafe.Pointer) bool {
handleStopTun()
now := time.Now()
runTime = &now
if fd != 0 {
tunLock.Lock()
defer tunLock.Unlock()
tunHandler = &TunHandler{
callback: callback,
limit: semaphore.NewWeighted(4),
}
initTunHook()
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
} else {
removeTunHook()
return false
}
tunHandler.listener = tunListener
}
return true
}
func handleGetRunTime() string { func handleGetRunTime() string {
if runTime == nil { if runTime == nil {
return "" return ""
@@ -133,83 +129,29 @@ func handleGetRunTime() string {
return strconv.FormatInt(runTime.UnixMilli(), 10) return strconv.FormatInt(runTime.UnixMilli(), 10)
} }
func handleSetProcessMap(params string) { func initTunHook() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(params), processMapItem)
if err == nil {
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
}
}
//export attachInvokePort
func attachInvokePort(mPort C.longlong) {
invokePort = int64(mPort)
}
func sendInvokeMessage(message InvokeMessage) {
if invokePort == -1 {
return
}
bridge.SendToPort(invokePort, message.Json())
}
func handleMarkSocket(fd Fd) {
sendInvokeMessage(InvokeMessage{
Type: ProtectInvoke,
Data: fd,
})
}
func handleParseProcess(process Process) {
sendInvokeMessage(InvokeMessage{
Type: ProcessInvoke,
Data: process,
})
}
func handleSetFdMap(id string) {
go func() {
fdInvokeMap.completer(id, "")
}()
}
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))
id := utils.NewUUIDV1().String()
handleMarkSocket(Fd{
Id: id,
Value: fdInt,
})
fdInvokeMap.await(id)
}) })
} }
}
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 := utils.NewUUIDV1().String() return tunHandler.handleResolveProcess(src, dst), nil
handleParseProcess(Process{
Id: id,
Metadata: metadata,
})
return processInvokeMap.await(id), nil
} }
} }
func removeTunHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func handleGetAndroidVpnOptions() string { func handleGetAndroidVpnOptions() string {
tunLock.Lock() tunLock.Lock()
defer tunLock.Unlock() defer tunLock.Unlock()
@@ -250,16 +192,6 @@ func handleGetCurrentProfileName() string {
func nextHandle(action *Action, result func(data interface{})) bool { func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method { switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
result(handleStartTun(fd))
return true
case stopTunMethod:
handleStopTun()
result(true)
return true
case getAndroidVpnOptionsMethod: case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions()) result(handleGetAndroidVpnOptions())
return true return true
@@ -268,16 +200,6 @@ func nextHandle(action *Action, result func(data interface{})) bool {
handleUpdateDns(data) handleUpdateDns(data)
result(true) result(true)
return true return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
result(true)
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
result(true)
return true
case getRunTimeMethod: case getRunTimeMethod:
result(handleGetRunTime()) result(handleGetRunTime())
return true return true
@@ -289,13 +211,13 @@ func nextHandle(action *Action, result func(data interface{})) bool {
} }
//export quickStart //export quickStart
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) { func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port) i := int64(port)
dir := C.GoString(dirChar) paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar)) bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar) stateParams := C.GoString(stateParamsChar)
go func() { go func() {
res := handleInitClash(dir) res := handleInitClash(paramsString)
if res == false { if res == false {
bridge.SendToPort(i, "init error") bridge.SendToPort(i, "init error")
} }
@@ -305,9 +227,8 @@ func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, po
} }
//export startTUN //export startTUN
func startTUN(fd C.int) *C.char { func startTUN(fd C.int, callback unsafe.Pointer) bool {
f := int(fd) return handleStartTun(int(fd), callback)
return C.CString(handleStartTun(f))
} }
//export getRunTime //export getRunTime
@@ -320,12 +241,6 @@ func stopTun() {
handleStopTun() handleStopTun()
} }
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
}
//export getCurrentProfileName //export getCurrentProfileName
func getCurrentProfileName() *C.char { func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName()) return C.CString(handleGetCurrentProfileName())
@@ -347,12 +262,3 @@ func updateDns(s *C.char) {
dnsList := C.GoString(s) dnsList := C.GoString(s)
handleUpdateDns(dnsList) handleUpdateDns(dnsList)
} }
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
}
+176
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
}
}
+7 -1
View File
@@ -8,6 +8,7 @@ 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';
@@ -66,7 +67,12 @@ class ClashCore {
Future<bool> init() async { Future<bool> init() async {
await initGeo(); await initGeo();
final homeDirPath = await appPath.homeDirPath; final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(homeDirPath); return await clashInterface.init(
InitParams(
homeDir: homeDirPath,
version: globalState.appState.version,
),
);
} }
Future<bool> setState(CoreState state) async { Future<bool> setState(CoreState state) async {
+144 -50
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,
) { ) {
@@ -2443,28 +2534,14 @@ 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()>();
void attachInvokePort(
int mPort,
) {
return _attachInvokePort(
mPort,
);
}
late final _attachInvokePortPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>(
'attachInvokePort');
late final _attachInvokePort =
_attachInvokePortPtr.asFunction<void Function(int)>();
void quickStart( void quickStart(
ffi.Pointer<ffi.Char> dirChar, ffi.Pointer<ffi.Char> initParamsChar,
ffi.Pointer<ffi.Char> paramsChar, ffi.Pointer<ffi.Char> paramsChar,
ffi.Pointer<ffi.Char> stateParamsChar, ffi.Pointer<ffi.Char> stateParamsChar,
int port, int port,
) { ) {
return _quickStart( return _quickStart(
dirChar, initParamsChar,
paramsChar, paramsChar,
stateParamsChar, stateParamsChar,
port, port,
@@ -2479,19 +2556,21 @@ class ClashFFI {
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.Pointer<ffi.Char>, int)>(); ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> startTUN( int startTUN(
int fd, int fd,
ffi.Pointer<ffi.Void> callback,
) { ) {
return _startTUN( return _startTUN(
fd, fd,
callback,
); );
} }
late final _startTUNPtr = late final _startTUNPtr = _lookup<
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>( ffi.NativeFunction<GoUint8 Function(ffi.Int, ffi.Pointer<ffi.Void>)>>(
'startTUN'); 'startTUN');
late final _startTUN = late final _startTUN =
_startTUNPtr.asFunction<ffi.Pointer<ffi.Char> Function(int)>(); _startTUNPtr.asFunction<int Function(int, ffi.Pointer<ffi.Void>)>();
ffi.Pointer<ffi.Char> getRunTime() { ffi.Pointer<ffi.Char> getRunTime() {
return _getRunTime(); return _getRunTime();
@@ -2511,20 +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(
ffi.Pointer<ffi.Char> fdIdChar,
) {
return _setFdMap(
fdIdChar,
);
}
late final _setFdMapPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'setFdMap');
late final _setFdMap =
_setFdMapPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getCurrentProfileName() { ffi.Pointer<ffi.Char> getCurrentProfileName() {
return _getCurrentProfileName(); return _getCurrentProfileName();
} }
@@ -2572,20 +2637,6 @@ class ClashFFI {
'updateDns'); 'updateDns');
late final _updateDns = late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>(); _updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
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>)>();
} }
final class __mbstate_t extends ffi.Union { final class __mbstate_t extends ffi.Union {
@@ -3738,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;
@@ -3758,6 +3834,8 @@ final class GoSlice extends ffi.Struct {
typedef GoInt = GoInt64; typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong; typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int; typedef DartGoInt64 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
const int __has_safe_buffers = 1; const int __has_safe_buffers = 1;
@@ -3973,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;
@@ -4135,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;
@@ -4233,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;
@@ -4333,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;
@@ -4389,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;
@@ -4419,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;
@@ -4429,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;
@@ -4555,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;
+4 -6
View File
@@ -7,7 +7,7 @@ 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 {
Future<bool> init(String homeDir); Future<bool> init(InitParams params);
Future<bool> preload(); Future<bool> preload();
@@ -74,12 +74,10 @@ mixin AndroidClashInterface {
Future<bool> setProcessMap(ProcessMapItem item); Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> stopTun(); // Future<bool> stopTun();
Future<bool> updateDns(String value); Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
Future<AndroidVpnOptions?> getAndroidVpnOptions(); Future<AndroidVpnOptions?> getAndroidVpnOptions();
Future<String> getCurrentProfileName(); Future<String> getCurrentProfileName();
@@ -191,10 +189,10 @@ abstract class ClashHandlerInterface with ClashInterface {
} }
@override @override
Future<bool> init(String homeDir) { Future<bool> init(InitParams params) {
return invoke<bool>( return invoke<bool>(
method: ActionMethod.initClash, method: ActionMethod.initClash,
data: homeDir, data: json.encode(params),
); );
} }
+14 -57
View File
@@ -122,25 +122,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
); );
} }
@override // @override
Future<DateTime?> startTun(int fd) async { // Future<bool> stopTun() {
final res = await invoke<String>( // return invoke<bool>(
method: ActionMethod.startTun, // method: ActionMethod.stopTun,
data: json.encode(fd), // );
); // }
if (res.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
}
@override
Future<bool> stopTun() {
return invoke<bool>(
method: ActionMethod.stopTun,
);
}
@override @override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async { Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
@@ -224,37 +211,12 @@ class ClashLibHandler {
); );
} }
attachInvokePort(int invokePort) {
clashFFI.attachInvokePort(
invokePort,
);
}
DateTime? startTun(int fd) {
final runTimeRaw = clashFFI.startTUN(fd);
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
stopTun() {
clashFFI.stopTun();
}
updateDns(String dns) { updateDns(String dns) {
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);
@@ -305,17 +267,11 @@ class ClashLibHandler {
return true; return true;
} }
setFdMap(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.setFdMap(idChar);
malloc.free(idChar);
}
Future<String> quickStart( Future<String> quickStart(
String homeDir, InitParams initParams,
UpdateConfigParams updateConfigParams, UpdateConfigParams updateConfigParams,
CoreState state, CoreState state,
) { ) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -325,17 +281,18 @@ class ClashLibHandler {
} }
}); });
final params = json.encode(updateConfigParams); final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state); final stateParams = json.encode(state);
final homeChar = homeDir.toNativeUtf8().cast<Char>(); final initParamsChar = initValue.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>(); final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>(); final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart( clashFFI.quickStart(
homeChar, initParamsChar,
paramsChar, paramsChar,
stateParamsChar, stateParamsChar,
receiver.sendPort.nativePort, receiver.sendPort.nativePort,
); );
malloc.free(homeChar); malloc.free(initParamsChar);
malloc.free(paramsChar); malloc.free(paramsChar);
malloc.free(stateParamsChar); malloc.free(stateParamsChar);
return completer.future; return completer.future;
+10 -7
View File
@@ -30,8 +30,9 @@ class AppController {
AppController(this.context, WidgetRef ref) : _ref = ref; AppController(this.context, WidgetRef ref) : _ref = ref;
updateClashConfigDebounce() { updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, () { debouncer.call(DebounceTag.updateClashConfig, () async {
updateClashConfig(true); final isPatch = globalState.appState.needApply ? false : true;
await updateClashConfig(isPatch);
}); });
} }
@@ -70,7 +71,7 @@ class AppController {
restartCore() async { restartCore() async {
await clashService?.reStart(); await clashService?.reStart();
await initCore(); await _initCore();
if (_ref.read(runTimeProvider.notifier).isStart) { if (_ref.read(runTimeProvider.notifier).isStart) {
await globalState.handleStart(); await globalState.handleStart();
@@ -100,7 +101,6 @@ class AppController {
_ref.read(trafficsProvider.notifier).clear(); _ref.read(trafficsProvider.notifier).clear();
_ref.read(totalTrafficProvider.notifier).value = Traffic(); _ref.read(totalTrafficProvider.notifier).value = Traffic();
_ref.read(runTimeProvider.notifier).value = null; _ref.read(runTimeProvider.notifier).value = null;
// tray.updateTrayTitle(null);
addCheckIpNumDebounce(); addCheckIpNumDebounce();
} }
} }
@@ -283,6 +283,9 @@ class AppController {
final res = await clashCore.updateConfig( final res = await clashCore.updateConfig(
globalState.getUpdateConfigParams(isPatch), globalState.getUpdateConfigParams(isPatch),
); );
if (isPatch == false) {
_ref.read(needApplyProvider.notifier).value = false;
}
if (res.isNotEmpty) throw res; if (res.isNotEmpty) throw res;
lastTunEnable = enableTun; lastTunEnable = enableTun;
lastProfileModified = await profile?.profileLastModified; lastProfileModified = await profile?.profileLastModified;
@@ -417,7 +420,7 @@ class AppController {
Map<String, dynamic>? data, Map<String, dynamic>? data,
bool handleError = false, bool handleError = false,
}) async { }) async {
if(globalState.isPre){ if (globalState.isPre) {
return; return;
} }
if (data != null) { if (data != null) {
@@ -478,7 +481,7 @@ class AppController {
await handleExit(); await handleExit();
} }
Future<void> initCore() async { Future<void> _initCore() async {
final isInit = await clashCore.isInit; final isInit = await clashCore.isInit;
if (!isInit) { if (!isInit) {
await clashCore.setState( await clashCore.setState(
@@ -492,7 +495,7 @@ class AppController {
init() async { init() async {
await _handlePreference(); await _handlePreference();
await _handlerDisclaimer(); await _handlerDisclaimer();
await initCore(); await _initCore();
await _initStatus(); await _initStatus();
updateTray(true); updateTray(true);
autoLaunch?.updateStatus( autoLaunch?.updateStatus(
@@ -45,6 +45,7 @@ class _OverrideProfileState extends State<OverrideProfile> {
} }
_handleSave(WidgetRef ref, OverrideData overrideData) { _handleSave(WidgetRef ref, OverrideData overrideData) {
ref.read(needApplyProvider.notifier).value = true;
ref.read(profilesProvider.notifier).updateProfile( ref.read(profilesProvider.notifier).updateProfile(
widget.profileId, widget.profileId,
(state) => state.copyWith( (state) => state.copyWith(
+7 -46
View File
@@ -5,7 +5,7 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui'; import 'dart:ui';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/tile.dart';
import 'package:fl_clash/plugins/vpn.dart'; import 'package:fl_clash/plugins/vpn.dart';
@@ -17,7 +17,6 @@ import 'application.dart';
import 'clash/core.dart'; import 'clash/core.dart';
import 'clash/lib.dart'; import 'clash/lib.dart';
import 'common/common.dart'; import 'common/common.dart';
import 'models/models.dart';
Future<void> main() async { Future<void> main() async {
globalState.isService = false; globalState.isService = false;
@@ -47,7 +46,6 @@ Future<void> _service(List<String> flags) async {
onStop: () async { onStop: () async {
await app?.tip(appLocalizations.stopVpn); await app?.tip(appLocalizations.stopVpn);
clashLibHandler.stopListener(); clashLibHandler.stopListener();
clashLibHandler.stopTun();
await vpn?.stop(); await vpn?.stop();
exit(0); exit(0);
}, },
@@ -64,43 +62,11 @@ Future<void> _service(List<String> flags) async {
vpn?.addListener( vpn?.addListener(
_VpnListenerWithService( _VpnListenerWithService(
onStarted: (int fd) {
commonPrint.log("vpn started fd: $fd");
final time = clashLibHandler.startTun(fd);
commonPrint.log("vpn start tun time: $time");
},
onDnsChanged: (String dns) { onDnsChanged: (String dns) {
clashLibHandler.updateDns(dns); clashLibHandler.updateDns(dns);
}, },
), ),
); );
final invokeReceiverPort = ReceivePort();
clashLibHandler.attachInvokePort(
invokeReceiverPort.sendPort.nativePort,
);
invokeReceiverPort.listen(
(message) async {
final invokeMessage = InvokeMessage.fromJson(json.decode(message));
switch (invokeMessage.type) {
case InvokeMessageType.protect:
final fd = Fd.fromJson(invokeMessage.data);
await vpn?.setProtect(fd.value);
clashLibHandler.setFdMap(fd.id);
case InvokeMessageType.process:
final process = ProcessData.fromJson(invokeMessage.data);
final processName = await vpn?.resolverProcess(process) ?? "";
clashLibHandler.setProcessMap(
ProcessMapItem(
id: process.id,
value: processName,
),
);
}
},
);
if (!quickStart) { if (!quickStart) {
_handleMainIpc(clashLibHandler); _handleMainIpc(clashLibHandler);
} else { } else {
@@ -108,9 +74,13 @@ Future<void> _service(List<String> flags) async {
await ClashCore.initGeo(); await ClashCore.initGeo();
app?.tip(appLocalizations.startVpn); app?.tip(appLocalizations.startVpn);
final homeDirPath = await appPath.homeDirPath; final homeDirPath = await appPath.homeDirPath;
final version = await system.version;
clashLibHandler clashLibHandler
.quickStart( .quickStart(
homeDirPath, InitParams(
homeDir: homeDirPath,
version: version,
),
globalState.getUpdateConfigParams(), globalState.getUpdateConfigParams(),
globalState.getCoreState(), globalState.getCoreState(),
) )
@@ -165,20 +135,11 @@ class _TileListenerWithService with TileListener {
@immutable @immutable
class _VpnListenerWithService with VpnListener { class _VpnListenerWithService with VpnListener {
final Function(int fd) _onStarted;
final Function(String dns) _onDnsChanged; final Function(String dns) _onDnsChanged;
const _VpnListenerWithService({ const _VpnListenerWithService({
required Function(int fd) onStarted,
required Function(String dns) onDnsChanged, required Function(String dns) onDnsChanged,
}) : _onStarted = onStarted, }) : _onDnsChanged = onDnsChanged;
_onDnsChanged = onDnsChanged;
@override
void onStarted(int fd) {
super.onStarted(fd);
_onStarted(fd);
}
@override @override
void onDnsChanged(String dns) { void onDnsChanged(String dns) {
+1
View File
@@ -31,6 +31,7 @@ class AppState with _$AppState {
required FixedList<Log> logs, required FixedList<Log> logs,
required FixedList<Traffic> traffics, required FixedList<Traffic> traffics,
required Traffic totalTraffic, required Traffic totalTraffic,
@Default(false) bool needApply,
}) = _AppState; }) = _AppState;
} }
+11
View File
@@ -82,6 +82,17 @@ class UpdateConfigParams with _$UpdateConfigParams {
_$UpdateConfigParamsFromJson(json); _$UpdateConfigParamsFromJson(json);
} }
@freezed
class InitParams with _$InitParams {
const factory InitParams({
@JsonKey(name: "home-dir") required String homeDir,
required int version,
}) = _InitParams;
factory InitParams.fromJson(Map<String, Object?> json) =>
_$InitParamsFromJson(json);
}
@freezed @freezed
class ChangeProxyParams with _$ChangeProxyParams { class ChangeProxyParams with _$ChangeProxyParams {
const factory ChangeProxyParams({ const factory ChangeProxyParams({
+50 -26
View File
@@ -35,6 +35,7 @@ mixin _$AppState {
FixedList<Log> get logs => throw _privateConstructorUsedError; FixedList<Log> get logs => throw _privateConstructorUsedError;
FixedList<Traffic> get traffics => throw _privateConstructorUsedError; FixedList<Traffic> get traffics => throw _privateConstructorUsedError;
Traffic get totalTraffic => throw _privateConstructorUsedError; Traffic get totalTraffic => throw _privateConstructorUsedError;
bool get needApply => throw _privateConstructorUsedError;
/// Create a copy of AppState /// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -66,7 +67,8 @@ abstract class $AppStateCopyWith<$Res> {
int version, int version,
FixedList<Log> logs, FixedList<Log> logs,
FixedList<Traffic> traffics, FixedList<Traffic> traffics,
Traffic totalTraffic}); Traffic totalTraffic,
bool needApply});
$ColorSchemesCopyWith<$Res> get colorSchemes; $ColorSchemesCopyWith<$Res> get colorSchemes;
} }
@@ -104,6 +106,7 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState>
Object? logs = null, Object? logs = null,
Object? traffics = null, Object? traffics = null,
Object? totalTraffic = null, Object? totalTraffic = null,
Object? needApply = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
isInit: null == isInit isInit: null == isInit
@@ -178,6 +181,10 @@ class _$AppStateCopyWithImpl<$Res, $Val extends AppState>
? _value.totalTraffic ? _value.totalTraffic
: totalTraffic // ignore: cast_nullable_to_non_nullable : totalTraffic // ignore: cast_nullable_to_non_nullable
as Traffic, as Traffic,
needApply: null == needApply
? _value.needApply
: needApply // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val); ) as $Val);
} }
@@ -218,7 +225,8 @@ abstract class _$$AppStateImplCopyWith<$Res>
int version, int version,
FixedList<Log> logs, FixedList<Log> logs,
FixedList<Traffic> traffics, FixedList<Traffic> traffics,
Traffic totalTraffic}); Traffic totalTraffic,
bool needApply});
@override @override
$ColorSchemesCopyWith<$Res> get colorSchemes; $ColorSchemesCopyWith<$Res> get colorSchemes;
@@ -255,6 +263,7 @@ class __$$AppStateImplCopyWithImpl<$Res>
Object? logs = null, Object? logs = null,
Object? traffics = null, Object? traffics = null,
Object? totalTraffic = null, Object? totalTraffic = null,
Object? needApply = null,
}) { }) {
return _then(_$AppStateImpl( return _then(_$AppStateImpl(
isInit: null == isInit isInit: null == isInit
@@ -329,6 +338,10 @@ class __$$AppStateImplCopyWithImpl<$Res>
? _value.totalTraffic ? _value.totalTraffic
: totalTraffic // ignore: cast_nullable_to_non_nullable : totalTraffic // ignore: cast_nullable_to_non_nullable
as Traffic, as Traffic,
needApply: null == needApply
? _value.needApply
: needApply // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
} }
@@ -354,7 +367,8 @@ class _$AppStateImpl implements _AppState {
required this.version, required this.version,
required this.logs, required this.logs,
required this.traffics, required this.traffics,
required this.totalTraffic}) required this.totalTraffic,
this.needApply = false})
: _packages = packages, : _packages = packages,
_delayMap = delayMap, _delayMap = delayMap,
_groups = groups, _groups = groups,
@@ -429,10 +443,13 @@ class _$AppStateImpl implements _AppState {
final FixedList<Traffic> traffics; final FixedList<Traffic> traffics;
@override @override
final Traffic totalTraffic; final Traffic totalTraffic;
@override
@JsonKey()
final bool needApply;
@override @override
String toString() { String toString() {
return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic)'; return 'AppState(isInit: $isInit, pageLabel: $pageLabel, packages: $packages, colorSchemes: $colorSchemes, sortNum: $sortNum, viewSize: $viewSize, delayMap: $delayMap, groups: $groups, checkIpNum: $checkIpNum, brightness: $brightness, runTime: $runTime, providers: $providers, localIp: $localIp, requests: $requests, version: $version, logs: $logs, traffics: $traffics, totalTraffic: $totalTraffic, needApply: $needApply)';
} }
@override @override
@@ -466,30 +483,34 @@ class _$AppStateImpl implements _AppState {
(identical(other.traffics, traffics) || (identical(other.traffics, traffics) ||
other.traffics == traffics) && other.traffics == traffics) &&
(identical(other.totalTraffic, totalTraffic) || (identical(other.totalTraffic, totalTraffic) ||
other.totalTraffic == totalTraffic)); other.totalTraffic == totalTraffic) &&
(identical(other.needApply, needApply) ||
other.needApply == needApply));
} }
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hashAll([
runtimeType, runtimeType,
isInit, isInit,
pageLabel, pageLabel,
const DeepCollectionEquality().hash(_packages), const DeepCollectionEquality().hash(_packages),
colorSchemes, colorSchemes,
sortNum, sortNum,
viewSize, viewSize,
const DeepCollectionEquality().hash(_delayMap), const DeepCollectionEquality().hash(_delayMap),
const DeepCollectionEquality().hash(_groups), const DeepCollectionEquality().hash(_groups),
checkIpNum, checkIpNum,
brightness, brightness,
runTime, runTime,
const DeepCollectionEquality().hash(_providers), const DeepCollectionEquality().hash(_providers),
localIp, localIp,
requests, requests,
version, version,
logs, logs,
traffics, traffics,
totalTraffic); totalTraffic,
needApply
]);
/// Create a copy of AppState /// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -519,7 +540,8 @@ abstract class _AppState implements AppState {
required final int version, required final int version,
required final FixedList<Log> logs, required final FixedList<Log> logs,
required final FixedList<Traffic> traffics, required final FixedList<Traffic> traffics,
required final Traffic totalTraffic}) = _$AppStateImpl; required final Traffic totalTraffic,
final bool needApply}) = _$AppStateImpl;
@override @override
bool get isInit; bool get isInit;
@@ -557,6 +579,8 @@ abstract class _AppState implements AppState {
FixedList<Traffic> get traffics; FixedList<Traffic> get traffics;
@override @override
Traffic get totalTraffic; Traffic get totalTraffic;
@override
bool get needApply;
/// Create a copy of AppState /// Create a copy of AppState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
+172
View File
@@ -1151,6 +1151,178 @@ abstract class _UpdateConfigParams implements UpdateConfigParams {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
InitParams _$InitParamsFromJson(Map<String, dynamic> json) {
return _InitParams.fromJson(json);
}
/// @nodoc
mixin _$InitParams {
@JsonKey(name: "home-dir")
String get homeDir => throw _privateConstructorUsedError;
int get version => throw _privateConstructorUsedError;
/// Serializes this InitParams to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of InitParams
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$InitParamsCopyWith<InitParams> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $InitParamsCopyWith<$Res> {
factory $InitParamsCopyWith(
InitParams value, $Res Function(InitParams) then) =
_$InitParamsCopyWithImpl<$Res, InitParams>;
@useResult
$Res call({@JsonKey(name: "home-dir") String homeDir, int version});
}
/// @nodoc
class _$InitParamsCopyWithImpl<$Res, $Val extends InitParams>
implements $InitParamsCopyWith<$Res> {
_$InitParamsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of InitParams
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? homeDir = null,
Object? version = null,
}) {
return _then(_value.copyWith(
homeDir: null == homeDir
? _value.homeDir
: homeDir // ignore: cast_nullable_to_non_nullable
as String,
version: null == version
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$InitParamsImplCopyWith<$Res>
implements $InitParamsCopyWith<$Res> {
factory _$$InitParamsImplCopyWith(
_$InitParamsImpl value, $Res Function(_$InitParamsImpl) then) =
__$$InitParamsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({@JsonKey(name: "home-dir") String homeDir, int version});
}
/// @nodoc
class __$$InitParamsImplCopyWithImpl<$Res>
extends _$InitParamsCopyWithImpl<$Res, _$InitParamsImpl>
implements _$$InitParamsImplCopyWith<$Res> {
__$$InitParamsImplCopyWithImpl(
_$InitParamsImpl _value, $Res Function(_$InitParamsImpl) _then)
: super(_value, _then);
/// Create a copy of InitParams
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? homeDir = null,
Object? version = null,
}) {
return _then(_$InitParamsImpl(
homeDir: null == homeDir
? _value.homeDir
: homeDir // ignore: cast_nullable_to_non_nullable
as String,
version: null == version
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$InitParamsImpl implements _InitParams {
const _$InitParamsImpl(
{@JsonKey(name: "home-dir") required this.homeDir,
required this.version});
factory _$InitParamsImpl.fromJson(Map<String, dynamic> json) =>
_$$InitParamsImplFromJson(json);
@override
@JsonKey(name: "home-dir")
final String homeDir;
@override
final int version;
@override
String toString() {
return 'InitParams(homeDir: $homeDir, version: $version)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$InitParamsImpl &&
(identical(other.homeDir, homeDir) || other.homeDir == homeDir) &&
(identical(other.version, version) || other.version == version));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, homeDir, version);
/// Create a copy of InitParams
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$InitParamsImplCopyWith<_$InitParamsImpl> get copyWith =>
__$$InitParamsImplCopyWithImpl<_$InitParamsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$InitParamsImplToJson(
this,
);
}
}
abstract class _InitParams implements InitParams {
const factory _InitParams(
{@JsonKey(name: "home-dir") required final String homeDir,
required final int version}) = _$InitParamsImpl;
factory _InitParams.fromJson(Map<String, dynamic> json) =
_$InitParamsImpl.fromJson;
@override
@JsonKey(name: "home-dir")
String get homeDir;
@override
int get version;
/// Create a copy of InitParams
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$InitParamsImplCopyWith<_$InitParamsImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ChangeProxyParams _$ChangeProxyParamsFromJson(Map<String, dynamic> json) { ChangeProxyParams _$ChangeProxyParamsFromJson(Map<String, dynamic> json) {
return _ChangeProxyParams.fromJson(json); return _ChangeProxyParams.fromJson(json);
} }
+12
View File
@@ -100,6 +100,18 @@ Map<String, dynamic> _$$UpdateConfigParamsImplToJson(
'params': instance.params, 'params': instance.params,
}; };
_$InitParamsImpl _$$InitParamsImplFromJson(Map<String, dynamic> json) =>
_$InitParamsImpl(
homeDir: json['home-dir'] as String,
version: (json['version'] as num).toInt(),
);
Map<String, dynamic> _$$InitParamsImplToJson(_$InitParamsImpl instance) =>
<String, dynamic>{
'home-dir': instance.homeDir,
'version': instance.version,
};
_$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson( _$ChangeProxyParamsImpl _$$ChangeProxyParamsImplFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$ChangeProxyParamsImpl( _$ChangeProxyParamsImpl(
-1
View File
@@ -31,7 +31,6 @@ class Service {
Future<bool?> startVpn() async { Future<bool?> startVpn() async {
final options = await clashLib?.getAndroidVpnOptions(); final options = await clashLib?.getAndroidVpnOptions();
// commonPrint.log("$options");
return await methodChannel.invokeMethod<bool>("startVpn", { return await methodChannel.invokeMethod<bool>("startVpn", {
'data': json.encode(options), 'data': json.encode(options),
}); });
-16
View File
@@ -8,8 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
abstract mixin class VpnListener { abstract mixin class VpnListener {
void onStarted(int fd) {}
void onDnsChanged(String dns) {} void onDnsChanged(String dns) {}
} }
@@ -32,10 +30,6 @@ class Vpn {
default: default:
for (final VpnListener listener in _listeners) { for (final VpnListener listener in _listeners) {
switch (call.method) { switch (call.method) {
case "started":
final fd = call.arguments as int;
listener.onStarted(fd);
break;
case "dnsChanged": case "dnsChanged":
final dns = call.arguments as String; final dns = call.arguments as String;
listener.onDnsChanged(dns); listener.onDnsChanged(dns);
@@ -62,16 +56,6 @@ class Vpn {
return await methodChannel.invokeMethod<bool>("stop"); return await methodChannel.invokeMethod<bool>("stop");
} }
Future<bool?> setProtect(int fd) async {
return await methodChannel.invokeMethod<bool?>("setProtect", {'fd': fd});
}
Future<String?> resolverProcess(ProcessData process) async {
return await methodChannel.invokeMethod<String>("resolverProcess", {
"data": json.encode(process),
});
}
void addListener(VpnListener listener) { void addListener(VpnListener listener) {
_listeners.add(listener); _listeners.add(listener);
} }
+15
View File
@@ -356,3 +356,18 @@ class DelayDataSource extends _$DelayDataSource with AutoDisposeNotifierMixin {
} }
} }
} }
@riverpod
class NeedApply extends _$NeedApply with AutoDisposeNotifierMixin {
@override
bool build() {
return globalState.appState.needApply;
}
@override
onUpdate(value) {
globalState.appState = globalState.appState.copyWith(
needApply: value,
);
}
}
+14
View File
@@ -336,5 +336,19 @@ final delayDataSourceProvider =
); );
typedef _$DelayDataSource = AutoDisposeNotifier<DelayMap>; typedef _$DelayDataSource = AutoDisposeNotifier<DelayMap>;
String _$needApplyHash() => r'62ff248d67b0525c6a55e556fbd29a2044e97766';
/// See also [NeedApply].
@ProviderFor(NeedApply)
final needApplyProvider = AutoDisposeNotifierProvider<NeedApply, bool>.internal(
NeedApply.new,
name: r'needApplyProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$needApplyHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$NeedApply = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
-1
View File
@@ -110,7 +110,6 @@ class GlobalState {
Future handleStop() async { Future handleStop() async {
startTime = null; startTime = null;
await clashCore.stopListener(); await clashCore.stopListener();
await clashLib?.stopTun();
await service?.stopVpn(); await service?.stopVpn();
stopUpdateTasks(); stopUpdateTasks();
} }
+1 -1
View File
@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.81+202504081 version: 0.8.82+202504152
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
+6 -2
View File
@@ -199,6 +199,7 @@ class Build {
required Mode mode, required Mode mode,
required Target target, required Target target,
Arch? arch, Arch? arch,
bool stable = false,
}) async { }) async {
final isLib = mode == Mode.lib; final isLib = mode == Mode.lib;
@@ -237,7 +238,9 @@ class Build {
if (isLib) { if (isLib) {
env["CGO_ENABLED"] = "1"; env["CGO_ENABLED"] = "1";
env["CC"] = _getCc(item); env["CC"] = _getCc(item);
env["CFLAGS"] = "-O3 -Werror"; if (stable) {
env["CFLAGS"] = "-O3 -Werror";
}
} else { } else {
env["CGO_ENABLED"] = "0"; env["CGO_ENABLED"] = "0";
} }
@@ -245,7 +248,7 @@ class Build {
final execLines = [ final execLines = [
"go", "go",
"build", "build",
"-ldflags=-w -s", if (stable) "-ldflags=-w -s",
"-tags=$tags", "-tags=$tags",
if (isLib) "-buildmode=c-shared", if (isLib) "-buildmode=c-shared",
"-o", "-o",
@@ -470,6 +473,7 @@ class BuildCommand extends Command {
target: target, target: target,
arch: arch, arch: arch,
mode: mode, mode: mode,
stable: env == "stable"
); );
if (target == Target.windows) { if (target == Target.windows) {