Compare commits

...

16 Commits

Author SHA1 Message Date
chen08209
060e1b5392 Optimize desktop view
Optimize logs, requests, connection pages

Optimize windows tray auto hide

Update core
2025-07-25 15:20:17 +08:00
chen08209
1477f9bd9c Fix windows tun issues
Optimize android get system dns

Optimize more details
2025-06-15 18:44:19 +08:00
chen08209
a06e813249 Update changelog 2025-06-07 16:08:38 +00:00
chen08209
afbc5adb05 Support override script
Support proxies search

Support svg display

Optimize config persistence

Add some scenes auto close connections

Update core

Optimize more details
2025-06-07 23:52:27 +08:00
chen08209
76c9f08d4a Fix issues that TUN repeat failed to open. 2025-05-01 22:12:05 +08:00
chen08209
f83a8e0cce Update changelog 2025-05-01 13:03:36 +00:00
chen08209
f5544f1af7 Fix windows service verify issues 2025-05-01 20:45:23 +08:00
chen08209
eeb543780a Update changelog 2025-04-30 16:20:40 +00:00
chen08209
676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00
chen08209
fb9d0cb22c Add issues template 2025-04-19 22:57:07 +08:00
chen08209
e7eb312254 Update changelog 2025-04-18 09:09:33 +00:00
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
275 changed files with 28391 additions and 12273 deletions

57
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: 问题反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的问题 / Report the issue you are experiencing
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 问题描述 / Describe the bug
description: 详细清晰地描述你遇到的问题,并配合截图 / Describe the problem you encountered in detail and clearly, and provide screenshots
validations:
required: true
- type: textarea
attributes:
label: 软件版本 / Version
description: 请提供FlClash的具体版本 / Please provide the specific version of FlClash.
validations:
required: true
- type: textarea
attributes:
label: 复现步骤 / To Reproduce
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
attributes:
label: 操作系统 / OS
options:
- Android
- Windows
- MacOS
- Linux
validations:
required: true
- type: input
attributes:
label: 操作系统版本 / OS Version
description: 请提供你的操作系统版本Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
validations:
required: true
- type: textarea
attributes:
label: 日志(勿上传日志文件,请粘贴日志内容) / Log (Do not upload the log file, paste the log content directly)
description: 请提供完整或相关部分的Debug日志请在“软件左侧菜单”->“设置”->“日志等级”调整到debug / Please provide a complete or relevant Debug log (please adjust it to debug in the left menu of software-> Settings-> Log Level)
validations:
required: true

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/+G-veVtwBOl4wODc1
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group

View File

@@ -0,0 +1,42 @@
name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
1. 请务必给issue填写一个简洁明了的标题以便他人快速检索
2. 请确保[已有的问题](https://github.com/chen08209/FlClash/issues?q=is%3Aissue) 中没有人提交过相似issue否则请在已有的issue下进行讨论
3. 请务必按照模板规范详细描述问题否则issue将会被直接关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
2. Please make sure there is no similar issue in the [existing issues](https://github.com/chen08209/FlClash/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please describe the problem in detail according to the template specification, otherwise issue will be closed directly.
- type: textarea
id: description
attributes:
label: 功能描述 / Feature description
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
validations:
required: true
- type: textarea
attributes:
label: 使用场景 / Use case
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations:
required: true
- type: checkboxes
id: os-labels
attributes:
label: 适用系统 / Target OS
description: 请选择该功能适用的操作系统(至少选择一个) / Please select the operating system(s) for this feature request (select at least one)
options:
- label: Android
- label: Windows
- label: MacOS
- label: Linux
validations:
required: true

View File

@@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs: jobs:
build: build:
@@ -17,7 +19,7 @@ jobs:
os: windows-latest os: windows-latest
arch: amd64 arch: amd64
- platform: linux - platform: linux
os: ubuntu-latest os: ubuntu-22.04
arch: amd64 arch: amd64
- platform: macos - platform: macos
os: macos-13 os: macos-13
@@ -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,25 +56,32 @@ 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 Master
if: startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: 'master'
cache: true cache: true
- name: Setup Flutter
if: ${{ !(startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) }}
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
# flutter-version: 3.29.3
- 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
@@ -88,14 +95,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)
@@ -127,7 +133,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
@@ -203,32 +209,41 @@ jobs:
env: env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }} TAG: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
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: |
version=$(echo "${{ github.ref_name }}" | sed 's/^v//') version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md
- name: Generate sha256
if: env.IS_STABLE == 'true'
run: |
cd ./dist
for file in $(find . -type f -not -name "*.sha256"); do
sha256sum "$file" > "${file}.sha256"
done
- 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 }}
@@ -238,7 +253,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/

6
.gitmodules vendored
View File

@@ -1,14 +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
[submodule "plugins/tray_manager"]
path = plugins/tray_manager
url = git@github.com:chen08209/tray_manager.git
branch = main

View File

@@ -1,3 +1,95 @@
## v0.8.85
- Support override script
- Support proxies search
- Support svg display
- Optimize config persistence
- Add some scenes auto close connections
- Update core
- Optimize more details
## v0.8.84
- Fix windows service verify issues
- Update changelog
## v0.8.83
- Add windows server mode start process verify
- Add linux deb dependencies
- Add backup recovery strategy select
- Support custom text scaling
- Optimize the display of different text scale
- Optimize windows setup experience
- Optimize startTun performance
- Optimize android tv experience
- Optimize default option
- Optimize computed text size
- Optimize hyperOS freeform window
- Add developer mode
- Update core
- Optimize more details
- Add issues template
- Update changelog
## v0.8.82
- Optimize android vpn performance
- Add custom primary color and color scheme
- Add linux nad windows arm release
- Optimize requests and logs page
- Fix map input page delete issues
- Update changelog
## 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 ## v0.8.79
- Fix tab delay view issues - Fix tab delay view issues

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

@@ -41,8 +41,8 @@ on Mobile:
⚠️ Make sure to install the following dependencies before using them ⚠️ Make sure to install the following dependencies before using them
```bash ```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0 sudo apt-get install libkeybinder-3.0-dev
``` ```
### Android ### Android

View File

@@ -41,8 +41,8 @@ on Mobile:
⚠️ 使用前请确保安装以下依赖 ⚠️ 使用前请确保安装以下依赖
```bash ```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev sudo apt-get install libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0 sudo apt-get install libkeybinder-3.0-dev
``` ```
### Android ### Android

View File

@@ -1,8 +1,8 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/l10n/intl/**
linter: linter:
rules: rules:
prefer_single_quotes: true
analyzer:
plugins:
- custom_lint

View File

@@ -1,114 +0,0 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def defStoreFile = file("keystore.jks")
def defStorePassword = localProperties.getProperty('storePassword')
def defKeyAlias = localProperties.getProperty('keyAlias')
def defKeyPassword = localProperties.getProperty('keyPassword')
def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias != null && defKeyPassword != null
android {
namespace "com.follow.clash"
compileSdkVersion 34
ndkVersion "27.1.12297006"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
signingConfigs {
if (isRelease) {
release {
storeFile defStoreFile
storePassword defStorePassword
keyAlias defKeyAlias
keyPassword defKeyPassword
}
}
}
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix '.debug'
}
release {
if (isRelease) {
signingConfig signingConfigs.release
} else {
signingConfig signingConfigs.debug
}
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}
tasks.register('copyNativeLibs', Copy) {
delete('src/main/jniLibs')
from('../../libclash/android')
into('src/main/jniLibs')
}
tasks.withType(MergeSourceSetFolders).configureEach {
dependsOn copyNativeLibs
}
flutter {
source '../..'
}
dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'com.google.code.gson:gson:2.10'
implementation("com.android.tools.smali:smali-dexlib2:3.0.7") {
exclude group: "com.google.guava", module: "guava"
}
}
afterEvaluate {
assembleDebug.dependsOn copyNativeLibs
assembleRelease.dependsOn copyNativeLibs
}

View File

@@ -0,0 +1,93 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
val localPropertiesFile = rootProject.file("local.properties")
val localProperties = Properties().apply {
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { load(it) }
}
}
val mStoreFile: File = file("keystore.jks")
val mStorePassword: String? = localProperties.getProperty("storePassword")
val mKeyAlias: String? = localProperties.getProperty("keyAlias")
val mKeyPassword: String? = localProperties.getProperty("keyPassword")
val isRelease = mStoreFile.exists()
&& mStorePassword != null
&& mKeyAlias != null
&& mKeyPassword != null
android {
namespace = "com.follow.clash"
compileSdk = 35
ndkVersion = "28.0.13004108"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.follow.clash"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
if (isRelease) {
create("release") {
storeFile = mStoreFile
storePassword = mStorePassword
keyAlias = mKeyAlias
keyPassword = mKeyPassword
}
}
}
buildTypes {
debug {
isMinifyEnabled = false
applicationIdSuffix = ".debug"
}
release {
isMinifyEnabled = true
isDebuggable = false
signingConfig = if (isRelease) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation(project(":core"))
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.google.code.gson:gson:2.10.1")
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
exclude(group = "com.google.guava", module = "guava")
}
}

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

@@ -7,23 +7,25 @@
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<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=".FlClashApplication" android:name=".FlClashApplication"
android:banner="@mipmap/ic_banner"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="FlClash"> android:label="FlClash">
@@ -46,6 +48,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" /> <action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
@@ -64,7 +67,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" /> <meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<activity <activity
android:name=".TempActivity" android:name=".TempActivity"
@@ -87,8 +92,7 @@
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
android:exported="true" android:exported="true"
android:foregroundServiceType="specialUse" android:icon="@drawable/ic"
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"
tools:targetApi="n"> tools:targetApi="n">
@@ -125,7 +129,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" />
@@ -138,7 +142,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

@@ -1,7 +1,7 @@
package com.follow.clash; package com.follow.clash;
import android.app.Application import android.app.Application
import android.content.Context; import android.content.Context
class FlClashApplication : Application() { class FlClashApplication : Application() {
companion object { companion object {

View File

@@ -1,14 +1,16 @@
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
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
@@ -22,6 +24,10 @@ enum class RunState {
object GlobalState { object GlobalState {
val runLock = ReentrantLock() val runLock = ReentrantLock()
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null private var serviceEngine: FlutterEngine? = null
@@ -31,6 +37,15 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
} }
fun syncStatus() {
CoroutineScope(Dispatchers.Default).launch {
val status = getCurrentVPNPlugin()?.getStatus() ?: false
withContext(Dispatchers.Main){
runState.value = if (status) RunState.START else RunState.STOP
}
}
}
suspend fun getText(text: String): String { suspend fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: "" return getCurrentAppPlugin()?.getText(text) ?: ""
} }

View File

@@ -17,6 +17,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() { override fun onDestroy() {
GlobalState.flutterEngine = null GlobalState.flutterEngine = null
GlobalState.runState.value = RunState.STOP
super.onDestroy() super.onDestroy()
} }
} }

View File

@@ -27,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
suspend fun Drawable.getBase64(): String { suspend fun Drawable.getBase64(): String {
val drawable = this val drawable = this
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
@@ -105,7 +104,7 @@ fun ConnectivityManager.resolveDns(network: Network?): List<String> {
fun InetAddress.asSocketAddressText(port: Int): String { fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) { return when (this) {
is Inet6Address -> is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port" "[${numericToTextFormat(this)}]:$port"
is Inet4Address -> is Inet4Address ->
"${this.hostAddress}:$port" "${this.hostAddress}:$port"
@@ -142,7 +141,8 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
} }
} }
private fun numericToTextFormat(src: ByteArray): String { private fun numericToTextFormat(address: Inet6Address): String {
val src = address.address
val sb = StringBuilder(39) val sb = StringBuilder(39)
for (i in 0 until 8) { for (i in 0 until 8) {
sb.append( sb.append(
@@ -155,6 +155,10 @@ private fun numericToTextFormat(src: ByteArray): String {
sb.append(":") sb.append(":")
} }
} }
if (address.scopeId > 0) {
sb.append("%")
sb.append(address.scopeId)
}
return sb.toString() return sb.toString()
} }

View File

@@ -3,6 +3,7 @@ package com.follow.clash.models
data class Package( data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, val system: Boolean,
val internet: Boolean,
val lastUpdateTime: Long, val lastUpdateTime: Long,
) )

View File

@@ -291,19 +291,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun getPackages(): List<Package> { private fun getPackages(): List<Package> {
val packageManager = FlClashApplication.getAppContext().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 != FlClashApplication.getAppContext().packageName ?.filter {
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true it.packageName != FlClashApplication.getAppContext().packageName || it.packageName == "android"
|| 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, system = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
lastUpdateTime = it.lastUpdateTime lastUpdateTime = it.lastUpdateTime,
) internet = it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
}?.let { packages.addAll(it) } )
}?.let { packages.addAll(it) }
return packages return packages
} }
@@ -353,7 +353,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
suspend fun getText(text: String): String? { suspend fun getText(text: String): String? {
return withContext(Dispatchers.Default){ return withContext(Dispatchers.Default) {
channel.awaitResult<String>("getText", text) channel.awaitResult<String>("getText", text)
} }
} }
@@ -391,31 +391,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
}
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import com.follow.clash.FlClashApplication
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.models.VpnOptions import com.follow.clash.models.VpnOptions
import com.google.gson.Gson import com.google.gson.Gson
@@ -52,8 +51,8 @@ data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
} }
private fun handleDestroy() { private fun handleDestroy() {
GlobalState.getCurrentVPNPlugin()?.handleStop()
GlobalState.destroyServiceEngine() GlobalState.destroyServiceEngine()
} }
} }

View File

@@ -10,15 +10,13 @@ import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log
import androidx.core.content.getSystemService 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
@@ -41,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>()
@@ -52,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()
@@ -61,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
} }
} }
@@ -91,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()
} }
@@ -154,6 +100,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
fun handleStart(options: VpnOptions): Boolean { fun handleStart(options: VpnOptions): Boolean {
onUpdateNetwork();
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()
@@ -163,10 +113,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() {
@@ -220,8 +169,10 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
try { try {
if (GlobalState.runState.value != RunState.START) return if (GlobalState.runState.value != RunState.START) return
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams") val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
val startForegroundParams = Gson().fromJson( val startForegroundParams = if (data != null) Gson().fromJson(
data, StartForegroundParams::class.java data, StartForegroundParams::class.java
) else StartForegroundParams(
title = "", content = ""
) )
if (lastStartForegroundParams != startForegroundParams) { if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams lastStartForegroundParams = startForegroundParams
@@ -236,6 +187,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()
@@ -249,6 +201,13 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
timerJob = null timerJob = null
} }
suspend fun getStatus(): Boolean? {
return withContext(Dispatchers.Default) {
flutterMethodChannel.awaitResult<Boolean>("status", null)
}
}
private fun handleStartService() { private fun handleStartService() {
if (flClashService == null) { if (flClashService == null) {
bindService() bindService()
@@ -257,26 +216,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()
flClashService?.stop() flClashService?.stop()
stopForegroundJob()
Core.stopTun()
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)
} }

View File

@@ -1,6 +1,25 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.Notification
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
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.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
interface BaseServiceInterface { interface BaseServiceInterface {
@@ -9,4 +28,69 @@ interface BaseServiceInterface {
fun stop() fun stop()
suspend fun startForeground(title: String, content: String) suspend fun startForeground(title: String, content: String)
}
fun Service.createFlClashNotificationBuilder(): Deferred<NotificationCompat.Builder> =
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(this@createFlClashNotificationBuilder, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@createFlClashNotificationBuilder, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(
NotificationCompat.Builder(
this@createFlClashNotificationBuilder, GlobalState.NOTIFICATION_CHANNEL
)
) {
setSmallIcon(R.drawable.ic)
setContentTitle("FlClash")
setContentIntent(pendingIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0, stopText, getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
}
}
@SuppressLint("ForegroundServiceType")
fun Service.startForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(GlobalState.NOTIFICATION_CHANNEL)
if (channel == null) {
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL, "SERVICE_CHANNEL", NotificationManager.IMPORTANCE_LOW
)
manager?.createNotificationChannel(channel)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
startForeground(
GlobalState.NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} catch (_: Exception) {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
} else {
startForeground(GlobalState.NOTIFICATION_ID, notification)
}
} }

View File

@@ -1,29 +1,51 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
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.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.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
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.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
class FlClashService : Service(), BaseServiceInterface { class FlClashService : Service(), BaseServiceInterface {
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private var cachedBuilder: NotificationCompat.Builder? = null
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
}
return cachedBuilder!!
}
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc()
}
private val binder = LocalBinder() private val binder = LocalBinder()
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@@ -38,87 +60,8 @@ class FlClashService : Service(), BaseServiceInterface {
return super.onUnbind(intent) return super.onUnbind(intent)
} }
private val CHANNEL = "FlClash" override fun onDestroy() {
stop()
private val notificationId: Int = 1 super.onDestroy()
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
CoroutineScope(Dispatchers.Main).async {
val stopText = GlobalState.getText("stop")
val intent = Intent(
this@FlClashService, MainActivity::class.java
)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this@FlClashService,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@FlClashService,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
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)
}
}
}
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
override fun start(options: VpnOptions) = 0
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
getNotificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
} }
} }

View File

@@ -33,6 +33,7 @@ class FlClashTileService : TileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
GlobalState.syncStatus()
GlobalState.runState.value?.let { updateTile(it) } GlobalState.runState.value?.let { updateTile(it) }
GlobalState.runState.observeForever(observer) GlobalState.runState.observeForever(observer)
} }

View File

@@ -1,12 +1,7 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
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,18 +12,13 @@ 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.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.extensions.getIpv4RouteAddress import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.extensions.toCIDR 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
@@ -43,28 +33,59 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) { if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR() val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength) addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${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)
} }
} else {
addRoute("0.0.0.0", 0)
} }
if (options.ipv6Address.isNotEmpty()) { try {
val cidr = options.ipv6Address.toCIDR() if (options.ipv6Address.isNotEmpty()) {
addAddress(cidr.address, cidr.prefixLength) val cidr = options.ipv6Address.toCIDR()
val routeAddress = options.getIpv6RouteAddress() Log.d(
if (routeAddress.isNotEmpty()) { "addAddress6",
routeAddress.forEach { i -> "address: ${cidr.address} prefixLength:${cidr.prefixLength}"
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}") )
addRoute(i.address, i.prefixLength) addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute6",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("::", 0)
}
} else {
addRoute("::", 0)
} }
} else {
addRoute("::", 0)
} }
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
} }
addDnsServer(options.dnsServerAddress) addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
@@ -114,78 +135,22 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
private val CHANNEL = "FlClash" private var cachedBuilder: NotificationCompat.Builder? = null
private val notificationId: Int = 1 private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy { cachedBuilder = createFlClashNotificationBuilder().await()
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) {
PendingIntent.getActivity(
this@FlClashVpnService,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this@FlClashVpnService,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
setSmallIcon(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
}
setOngoing(true)
addAction(
0,
stopText,
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
} }
return cachedBuilder!!
} }
private suspend fun getNotificationBuilder(): NotificationCompat.Builder { @SuppressLint("ForegroundServiceType")
return notificationBuilderDeferred.await()
}
@SuppressLint("ForegroundServiceType", "WrongConstant")
override suspend fun startForeground(title: String, content: String) { override suspend fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForeground(
val manager = getSystemService(NotificationManager::class.java) notificationBuilder()
var channel = manager?.getNotificationChannel(CHANNEL)
if (channel == null) {
channel =
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
manager?.createNotificationChannel(channel)
}
}
val notification =
getNotificationBuilder()
.setContentTitle(title) .setContentTitle(title)
.setContentText(content) .setContentText(content).build()
.build() )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(notificationId, notification)
}
} }
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="240dp"
android:height="240dp"
android:viewportWidth="240"
android:viewportHeight="240"
tools:ignore="VectorRaster">
<path
android:pathData="M48.1,80.89L168.44,11.41c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0l-120.34,69.48c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M78.98,134.37l60.18,-34.74c11.07,-6.39 25.23,-2.59 31.63,8.48h0c6.4,11.07 2.61,25.24 -8.47,31.64l-60.18,34.74c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64h0Z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M109.86,187.86h0c11.08,-6.4 25.24,-2.6 31.64,8.48 0,0 0,0 0,0h0c6.4,11.08 2.6,25.24 -8.48,31.64 0,0 0,0 0,0h0c-11.08,6.4 -25.24,2.6 -31.64,-8.48 0,0 0,0 0,0h0c-6.4,-11.08 -2.6,-25.24 8.48,-31.64 0,0 0,0 0,0Z"
android:fillColor="#FFFFFF"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,32 +0,0 @@
buildscript {
ext.kotlin_version = "${kotlin_version}"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:$agp_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

22
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,22 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

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

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

View File

@@ -0,0 +1,65 @@
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.argumentsWithVarargAsSingleArray
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
}
buildTypes {
release {
isJniDebuggable = false
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"
}
}
kotlinOptions {
jvmTarget = "17"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation("androidx.annotation:annotation-jvm:1.9.1")
}
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)
}
}

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,49 @@
cmake_minimum_required(VERSION 3.22.1)
project("core")
message("CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}")
message("CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE}")
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
)
add_compile_options(-fvisibility=hidden -fvisibility-inlines-hidden)
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,72 @@
#ifdef LIBCLASH
#include <jni.h>
#include "jni_helper.h"
#include "libclash.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject, const jint fd, jobject cb) {
const auto interface = new_global(cb);
startTUN(fd, interface);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_follow_clash_core_Core_stopTun(JNIEnv *) {
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(static_cast<jobject>(obj));
}
static void call_tun_interface_protect_impl(void *tun_interface, const int fd) {
ATTACH_JNI();
env->CallVoidMethod(static_cast<jobject>(tun_interface),
m_tun_interface_protect,
fd);
}
static const char *
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
const char *source,
const char *target,
const int uid) {
ATTACH_JNI();
const auto packageName = reinterpret_cast<jstring>(env->CallObjectMethod(static_cast<jobject>(tun_interface),
m_tun_interface_resolve_process,
protocol,
new_string(source),
new_string(target),
uid));
return get_string(packageName);
}
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env = nullptr;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
initialize_jni(vm, env);
const auto 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,71 @@
#include "jni_helper.h"
#include <cstdlib>
#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 = reinterpret_cast<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) {
const auto array = reinterpret_cast<jbyteArray>(env->CallObjectMethod(str, m_get_bytes));
const int length = env->GetArrayLength(array);
const auto content = static_cast<char *>(malloc(length + 1));
env->GetByteArrayRegion(array, 0, length, reinterpret_cast<jbyte *>(content));
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
const auto length = static_cast<int>(strlen(str));
const auto array = env->NewByteArray(length);
env->SetByteArrayRegion(array, 0, length, reinterpret_cast<const jbyte *>(str));
return reinterpret_cast<jstring>(env->NewObject(c_string, m_new_string, array));
}
int jni_catch_exception(JNIEnv *env) {
const int result = env->ExceptionCheck();
if (result) {
env->ExceptionDescribe();
env->ExceptionClear();
}
return result;
}
void jni_attach_thread(scoped_jni *jni) {
JavaVM *vm = global_java_vm();
if (vm->GetEnv(reinterpret_cast<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(const scoped_jni *env) {
JavaVM *vm = global_java_vm();
if (env->require_release) {
vm->DetachCurrentThread();
}
}
void release_string(char **str) {
free(*str);
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <jni.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(scoped_jni *jni);
extern void jni_detach_thread(const scoped_jni *env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
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.2

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

@@ -1,26 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "$agp_version" apply false
id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false
}
include ":app"

View File

@@ -0,0 +1,29 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
}
include(":app")
include(":core")

View File

@@ -13,7 +13,6 @@
"resourcesDesc": "External resource related info", "resourcesDesc": "External resource related info",
"trafficUsage": "Traffic usage", "trafficUsage": "Traffic usage",
"coreInfo": "Core info", "coreInfo": "Core info",
"nullCoreInfoDesc": "Unable to obtain core info",
"networkSpeed": "Network speed", "networkSpeed": "Network speed",
"outboundMode": "Outbound mode", "outboundMode": "Outbound mode",
"networkDetection": "Network detection", "networkDetection": "Network detection",
@@ -22,7 +21,6 @@
"noProxy": "No proxy", "noProxy": "No proxy",
"noProxyDesc": "Please create a profile or add a valid profile", "noProxyDesc": "Please create a profile or add a valid profile",
"nullProfileDesc": "No profile, Please add a profile", "nullProfileDesc": "No profile, Please add a profile",
"nullLogsDesc": "No logs",
"settings": "Settings", "settings": "Settings",
"language": "Language", "language": "Language",
"defaultText": "Default", "defaultText": "Default",
@@ -123,7 +121,6 @@
"project": "Project", "project": "Project",
"core": "Core", "core": "Core",
"tabAnimation": "Tab animation", "tabAnimation": "Tab animation",
"tabAnimationDesc": "When enabled, the home tab will add a toggle animation",
"desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", "desc": "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.",
"startVpn": "Starting VPN...", "startVpn": "Starting VPN...",
"stopVpn": "Stopping VPN...", "stopVpn": "Stopping VPN...",
@@ -150,8 +147,6 @@
"addressHelp": "WebDAV server address", "addressHelp": "WebDAV server address",
"addressTip": "Please enter a valid WebDAV address", "addressTip": "Please enter a valid WebDAV address",
"password": "Password", "password": "Password",
"passwordTip": "Password cannot be empty",
"accountTip": "Account cannot be empty",
"checkUpdate": "Check for updates", "checkUpdate": "Check for updates",
"discoverNewVersion": "Discover the new version", "discoverNewVersion": "Discover the new version",
"checkUpdateError": "The current application is already the latest version", "checkUpdateError": "The current application is already the latest version",
@@ -181,14 +176,11 @@
"requests": "Requests", "requests": "Requests",
"requestsDesc": "View recently request records", "requestsDesc": "View recently request records",
"findProcessMode": "Find process", "findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init", "init": "Init",
"infiniteTime": "Long term effective", "infiniteTime": "Long term effective",
"expirationTime": "Expiration time", "expirationTime": "Expiration time",
"connections": "Connections", "connections": "Connections",
"connectionsDesc": "View current connections data", "connectionsDesc": "View current connections data",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIP": "Intranet IP", "intranetIP": "Intranet IP",
"view": "View", "view": "View",
"cut": "Cut", "cut": "Cut",
@@ -221,7 +213,6 @@
"autoCloseConnectionsDesc": "Auto close connections after change node", "autoCloseConnectionsDesc": "Auto close connections after change node",
"onlyStatisticsProxy": "Only statistics proxy", "onlyStatisticsProxy": "Only statistics proxy",
"onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic", "onlyStatisticsProxyDesc": "When turned on, only statistics proxy traffic",
"deleteProfileTip": "Sure you want to delete the current profile?",
"pureBlackMode": "Pure black mode", "pureBlackMode": "Pure black mode",
"keepAliveIntervalDesc": "Tcp keep alive interval", "keepAliveIntervalDesc": "Tcp keep alive interval",
"entries": " entries", "entries": " entries",
@@ -249,11 +240,9 @@
"stop": "Stop", "stop": "Stop",
"appDesc": "Processing app related settings", "appDesc": "Processing app related settings",
"vpnDesc": "Modify VPN related settings", "vpnDesc": "Modify VPN related settings",
"generalDesc": "Overwrite general settings",
"dnsDesc": "Update DNS related settings", "dnsDesc": "Update DNS related settings",
"key": "Key", "key": "Key",
"value": "Value", "value": "Value",
"notEmpty": "Cannot be empty",
"hostsDesc": "Add Hosts", "hostsDesc": "Add Hosts",
"vpnTip": "Changes take effect after restarting the VPN", "vpnTip": "Changes take effect after restarting the VPN",
"vpnEnableDesc": "Auto routes all system traffic through VpnService", "vpnEnableDesc": "Auto routes all system traffic through VpnService",
@@ -340,11 +329,97 @@
"fileIsUpdate": "The file has been modified. Do you want to save the changes?", "fileIsUpdate": "The file has been modified. Do you want to save the changes?",
"profileHasUpdate": "The profile has been modified. Do you want to disable auto update?", "profileHasUpdate": "The profile has been modified. Do you want to disable auto update?",
"hasCacheChange": "Do you want to cache the changes?", "hasCacheChange": "Do you want to cache the changes?",
"nullProxies": "No proxies",
"copySuccess": "Copy success", "copySuccess": "Copy success",
"copyLink": "Copy link", "copyLink": "Copy link",
"exportFile": "Export file", "exportFile": "Export file",
"cacheCorrupt": "The cache is corrupt. Do you want to clear it?", "cacheCorrupt": "The cache is corrupt. Do you want to clear it?",
"detectionTip": "Relying on third-party api is for reference only", "detectionTip": "Relying on third-party api is for reference only",
"listen": "Listen" "listen": "Listen",
"undo": "undo",
"redo": "redo",
"none": "none",
"basicConfig": "Basic configuration",
"basicConfigDesc": "Modify the basic configuration globally",
"selectedCountTitle": "{count} items have been selected",
"addRule": "Add rule",
"ruleName": "Rule name",
"content": "Content",
"subRule": "Sub rule",
"ruleTarget": "Rule target",
"sourceIp": "Source IP",
"noResolve": "No resolve IP",
"getOriginRules": "Get original rules",
"overrideOriginRules": "Override the original rule",
"addedOriginRules": "Attach on the original rules",
"enableOverride": "Enable override",
"saveChanges": "Do you want to save the changes?",
"generalDesc": "Modify general settings",
"findProcessModeDesc": "There is a certain performance loss after opening",
"tabAnimationDesc": "Effective only in mobile view",
"saveTip": "Are you sure you want to save?",
"colorSchemes": "Color schemes",
"palette": "Palette",
"tonalSpotScheme": "TonalSpot",
"fidelityScheme": "Fidelity",
"monochromeScheme": "Monochrome",
"neutralScheme": "Neutral",
"vibrantScheme": "Vibrant",
"expressiveScheme": "Expressive",
"contentScheme": "Content",
"rainbowScheme": "Rainbow",
"fruitSaladScheme": "FruitSalad",
"developerMode": "Developer mode",
"developerModeEnableTip": "Developer mode is enabled.",
"messageTest": "Message test",
"messageTestTip": "This is a message.",
"crashTest": "Crash test",
"clearData": "Clear Data",
"textScale": "Text Scaling",
"internet": "Internet",
"systemApp": "System APP",
"noNetworkApp": "No network APP",
"contactMe": "Contact me",
"recoveryStrategy": "Recovery strategy",
"recoveryStrategy_override": "Override",
"recoveryStrategy_compatible": "Compatible",
"logsTest": "Logs test",
"emptyTip": "{label} cannot be empty",
"urlTip": "{label} must be a url",
"numberTip": "{label} must be a number",
"interval": "Interval",
"existsTip": "Current {label} already exists",
"deleteTip": "Are you sure you want to delete the current {label}?",
"deleteMultipTip": "Are you sure you want to delete the selected {label}?",
"nullTip": "No {label} at the moment",
"script": "Script",
"color": "Color",
"rename": "Rename",
"unnamed": "Unnamed",
"pleaseEnterScriptName": "Please enter a script name",
"overrideInvalidTip": "Does not take effect in script mode",
"mixedPort": "Mixed Port",
"socksPort": "Socks Port",
"redirPort": "Redir Port",
"tproxyPort": "Tproxy Port",
"portTip": "{label} must be between 1024 and 49151",
"portConflictTip": "Please enter a different port",
"import": "Import",
"importFile": "Import from file",
"importUrl": "Import from URL",
"autoSetSystemDns": "Auto set system DNS",
"details": "{label} details",
"creationTime": "Creation time",
"progress": "Progress",
"host": "Host",
"destination": "Destination",
"destinationGeoIP": "Destination GeoIP",
"destinationIPASN": "Destination IPASN",
"specialProxy": "Special proxy",
"specialRules": "special rules",
"remoteDestination": "Remote destination",
"networkType": "Network type",
"proxyChains": "Proxy chains",
"log": "Log",
"connection": "Connection",
"request": "Request"
} }

View File

@@ -13,7 +13,6 @@
"resourcesDesc": "外部リソース関連情報", "resourcesDesc": "外部リソース関連情報",
"trafficUsage": "トラフィック使用量", "trafficUsage": "トラフィック使用量",
"coreInfo": "コア情報", "coreInfo": "コア情報",
"nullCoreInfoDesc": "コア情報を取得できません",
"networkSpeed": "ネットワーク速度", "networkSpeed": "ネットワーク速度",
"outboundMode": "アウトバウンドモード", "outboundMode": "アウトバウンドモード",
"networkDetection": "ネットワーク検出", "networkDetection": "ネットワーク検出",
@@ -22,7 +21,6 @@
"noProxy": "プロキシなし", "noProxy": "プロキシなし",
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください", "noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
"nullProfileDesc": "プロファイルがありません。追加してください", "nullProfileDesc": "プロファイルがありません。追加してください",
"nullLogsDesc": "ログがありません",
"settings": "設定", "settings": "設定",
"language": "言語", "language": "言語",
"defaultText": "デフォルト", "defaultText": "デフォルト",
@@ -123,7 +121,6 @@
"project": "プロジェクト", "project": "プロジェクト",
"core": "コア", "core": "コア",
"tabAnimation": "タブアニメーション", "tabAnimation": "タブアニメーション",
"tabAnimationDesc": "有効化するとホームタブに切り替えアニメーションを追加",
"desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。", "desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
"startVpn": "VPNを開始中...", "startVpn": "VPNを開始中...",
"stopVpn": "VPNを停止中...", "stopVpn": "VPNを停止中...",
@@ -150,8 +147,6 @@
"addressHelp": "WebDAVサーバーアドレス", "addressHelp": "WebDAVサーバーアドレス",
"addressTip": "有効なWebDAVアドレスを入力", "addressTip": "有効なWebDAVアドレスを入力",
"password": "パスワード", "password": "パスワード",
"passwordTip": "パスワードは必須です",
"accountTip": "アカウントは必須です",
"checkUpdate": "更新を確認", "checkUpdate": "更新を確認",
"discoverNewVersion": "新バージョンを発見", "discoverNewVersion": "新バージョンを発見",
"checkUpdateError": "アプリは最新版です", "checkUpdateError": "アプリは最新版です",
@@ -181,14 +176,11 @@
"requests": "リクエスト", "requests": "リクエスト",
"requestsDesc": "最近のリクエスト記録を表示", "requestsDesc": "最近のリクエスト記録を表示",
"findProcessMode": "プロセス検出", "findProcessMode": "プロセス検出",
"findProcessModeDesc": "有効化するとフラッシュバックのリスクあり",
"init": "初期化", "init": "初期化",
"infiniteTime": "長期有効", "infiniteTime": "長期有効",
"expirationTime": "有効期限", "expirationTime": "有効期限",
"connections": "接続", "connections": "接続",
"connectionsDesc": "現在の接続データを表示", "connectionsDesc": "現在の接続データを表示",
"nullRequestsDesc": "リクエストなし",
"nullConnectionsDesc": "接続なし",
"intranetIP": "イントラネットIP", "intranetIP": "イントラネットIP",
"view": "表示", "view": "表示",
"cut": "切り取り", "cut": "切り取り",
@@ -221,7 +213,6 @@
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる", "autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
"onlyStatisticsProxy": "プロキシのみ統計", "onlyStatisticsProxy": "プロキシのみ統計",
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計", "onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
"deleteProfileTip": "現在のプロファイルを削除しますか?",
"pureBlackMode": "純黒モード", "pureBlackMode": "純黒モード",
"keepAliveIntervalDesc": "TCPキープアライブ間隔", "keepAliveIntervalDesc": "TCPキープアライブ間隔",
"entries": " エントリ", "entries": " エントリ",
@@ -249,11 +240,9 @@
"stop": "停止", "stop": "停止",
"appDesc": "アプリ関連設定の処理", "appDesc": "アプリ関連設定の処理",
"vpnDesc": "VPN関連設定の変更", "vpnDesc": "VPN関連設定の変更",
"generalDesc": "一般設定の上書き",
"dnsDesc": "DNS関連設定の更新", "dnsDesc": "DNS関連設定の更新",
"key": "キー", "key": "キー",
"value": "値", "value": "値",
"notEmpty": "空欄不可",
"hostsDesc": "ホストを追加", "hostsDesc": "ホストを追加",
"vpnTip": "変更はVPN再起動後に有効", "vpnTip": "変更はVPN再起動後に有効",
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング", "vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
@@ -340,11 +329,98 @@
"fileIsUpdate": "ファイルが変更されました。保存しますか?", "fileIsUpdate": "ファイルが変更されました。保存しますか?",
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?", "profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
"hasCacheChange": "変更をキャッシュしますか?", "hasCacheChange": "変更をキャッシュしますか?",
"nullProxies": "プロキシなし",
"copySuccess": "コピー成功", "copySuccess": "コピー成功",
"copyLink": "リンクをコピー", "copyLink": "リンクをコピー",
"exportFile": "ファイルをエクスポート", "exportFile": "ファイルをエクスポート",
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?", "cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
"detectionTip": "サードパーティAPIに依存参考値", "detectionTip": "サードパーティAPIに依存参考値",
"listen": "リスン" "listen": "リスン",
"undo": "元に戻す",
"redo": "やり直す",
"none": "なし",
"basicConfig": "基本設定",
"basicConfigDesc": "基本設定をグローバルに変更",
"selectedCountTitle": "{count} 項目が選択されています",
"addRule": "ルールを追加",
"ruleName": "ルール名",
"content": "内容",
"subRule": "サブルール",
"ruleTarget": "ルール対象",
"sourceIp": "送信元IP",
"noResolve": "IPを解決しない",
"getOriginRules": "元のルールを取得",
"overrideOriginRules": "元のルールを上書き",
"addedOriginRules": "元のルールに追加",
"enableOverride": "上書きを有効化",
"saveChanges": "変更を保存しますか?",
"generalDesc": "一般設定を変更",
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
"tabAnimationDesc": "モバイル表示でのみ有効",
"saveTip": "保存してもよろしいですか?",
"colorSchemes": "カラースキーム",
"palette": "パレット",
"tonalSpotScheme": "トーンスポット",
"fidelityScheme": "ハイファイデリティー",
"monochromeScheme": "モノクローム",
"neutralScheme": "ニュートラル",
"vibrantScheme": "ビブラント",
"expressiveScheme": "エクスプレッシブ",
"contentScheme": "コンテンツテーマ",
"rainbowScheme": "レインボー",
"fruitSaladScheme": "フルーツサラダ",
"developerMode": "デベロッパーモード",
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
"messageTest": "メッセージテスト",
"messageTestTip": "これはメッセージです。",
"crashTest": "クラッシュテスト",
"clearData": "データを消去",
"zoom": "ズーム",
"textScale": "テキストスケーリング",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ",
"contactMe": "連絡する",
"recoveryStrategy": "リカバリー戦略",
"recoveryStrategy_override": "オーバーライド",
"recoveryStrategy_compatible": "互換性",
"logsTest": "ログテスト",
"emptyTip": "{label}は空欄にできません",
"urlTip": "{label}はURLである必要があります",
"numberTip": "{label}は数字でなければなりません",
"interval": "インターバル",
"existsTip": "現在の{label}は既に存在しています",
"deleteTip": "現在の{label}を削除してもよろしいですか?",
"deleteMultipTip": "選択された{label}を削除してもよろしいですか?",
"nullTip": "現在{label}はありません",
"script": "スクリプト",
"color": "カラー",
"rename": "リネーム",
"unnamed": "無題",
"pleaseEnterScriptName": "スクリプト名を入力してください",
"overrideInvalidTip": "スクリプトモードでは有効になりません",
"mixedPort": "混合ポート",
"socksPort": "Socksポート",
"redirPort": "Redirポート",
"tproxyPort": "Tproxyポート",
"portTip": "{label} は 1024 から 49151 の間でなければなりません",
"portConflictTip": "別のポートを入力してください",
"import": "インポート",
"importFile": "ファイルからインポート",
"importUrl": "URLからインポート",
"autoSetSystemDns": "オートセットシステムDNS",
"details": "{label}詳細",
"creationTime": "作成時間",
"progress": "進捗",
"host": "ホスト",
"destination": "宛先",
"destinationGeoIP": "宛先地理情報",
"destinationIPASN": "宛先IP ASN",
"specialProxy": "特殊プロキシ",
"specialRules": "特殊ルール",
"remoteDestination": "リモート宛先",
"networkType": "ネットワーク種別",
"proxyChains": "プロキシチェーン",
"log": "ログ",
"connection": "接続",
"request": "リクエスト"
} }

View File

@@ -13,7 +13,6 @@
"resourcesDesc": "Информация, связанная с внешними ресурсами", "resourcesDesc": "Информация, связанная с внешними ресурсами",
"trafficUsage": "Использование трафика", "trafficUsage": "Использование трафика",
"coreInfo": "Информация о ядре", "coreInfo": "Информация о ядре",
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
"networkSpeed": "Скорость сети", "networkSpeed": "Скорость сети",
"outboundMode": "Режим исходящего трафика", "outboundMode": "Режим исходящего трафика",
"networkDetection": "Обнаружение сети", "networkDetection": "Обнаружение сети",
@@ -22,7 +21,6 @@
"noProxy": "Нет прокси", "noProxy": "Нет прокси",
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль", "noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль", "nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
"nullLogsDesc": "Нет логов",
"settings": "Настройки", "settings": "Настройки",
"language": "Язык", "language": "Язык",
"defaultText": "По умолчанию", "defaultText": "По умолчанию",
@@ -123,7 +121,6 @@
"project": "Проект", "project": "Проект",
"core": "Ядро", "core": "Ядро",
"tabAnimation": "Анимация вкладок", "tabAnimation": "Анимация вкладок",
"tabAnimationDesc": "При включении домашняя вкладка добавит анимацию переключения",
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.", "desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
"startVpn": "Запуск VPN...", "startVpn": "Запуск VPN...",
"stopVpn": "Остановка VPN...", "stopVpn": "Остановка VPN...",
@@ -150,8 +147,6 @@
"addressHelp": "Адрес сервера WebDAV", "addressHelp": "Адрес сервера WebDAV",
"addressTip": "Пожалуйста, введите действительный адрес WebDAV", "addressTip": "Пожалуйста, введите действительный адрес WebDAV",
"password": "Пароль", "password": "Пароль",
"passwordTip": "Пароль не может быть пустым",
"accountTip": "Аккаунт не может быть пустым",
"checkUpdate": "Проверить обновления", "checkUpdate": "Проверить обновления",
"discoverNewVersion": "Обнаружена новая версия", "discoverNewVersion": "Обнаружена новая версия",
"checkUpdateError": "Текущее приложение уже является последней версией", "checkUpdateError": "Текущее приложение уже является последней версией",
@@ -181,14 +176,11 @@
"requests": "Запросы", "requests": "Запросы",
"requestsDesc": "Просмотр последних записей запросов", "requestsDesc": "Просмотр последних записей запросов",
"findProcessMode": "Режим поиска процесса", "findProcessMode": "Режим поиска процесса",
"findProcessModeDesc": "Есть риск сбоя после включения",
"init": "Инициализация", "init": "Инициализация",
"infiniteTime": "Долгосрочное действие", "infiniteTime": "Долгосрочное действие",
"expirationTime": "Время истечения", "expirationTime": "Время истечения",
"connections": "Соединения", "connections": "Соединения",
"connectionsDesc": "Просмотр текущих данных о соединениях", "connectionsDesc": "Просмотр текущих данных о соединениях",
"nullRequestsDesc": "Нет запросов",
"nullConnectionsDesc": "Нет соединений",
"intranetIP": "Внутренний IP", "intranetIP": "Внутренний IP",
"view": "Просмотр", "view": "Просмотр",
"cut": "Вырезать", "cut": "Вырезать",
@@ -221,7 +213,6 @@
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла", "autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
"onlyStatisticsProxy": "Только статистика прокси", "onlyStatisticsProxy": "Только статистика прокси",
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси", "onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
"pureBlackMode": "Чисто черный режим", "pureBlackMode": "Чисто черный режим",
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения", "keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
"entries": " записей", "entries": " записей",
@@ -249,11 +240,9 @@
"stop": "Стоп", "stop": "Стоп",
"appDesc": "Обработка настроек, связанных с приложением", "appDesc": "Обработка настроек, связанных с приложением",
"vpnDesc": "Изменение настроек, связанных с VPN", "vpnDesc": "Изменение настроек, связанных с VPN",
"generalDesc": "Переопределение общих настроек",
"dnsDesc": "Обновление настроек, связанных с DNS", "dnsDesc": "Обновление настроек, связанных с DNS",
"key": "Ключ", "key": "Ключ",
"value": "Значение", "value": "Значение",
"notEmpty": "Не может быть пустым",
"hostsDesc": "Добавить Hosts", "hostsDesc": "Добавить Hosts",
"vpnTip": "Изменения вступят в силу после перезапуска VPN", "vpnTip": "Изменения вступят в силу после перезапуска VPN",
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService", "vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
@@ -340,11 +329,98 @@
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?", "fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?", "profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
"hasCacheChange": "Хотите сохранить изменения в кэше?", "hasCacheChange": "Хотите сохранить изменения в кэше?",
"nullProxies": "Нет прокси",
"copySuccess": "Копирование успешно", "copySuccess": "Копирование успешно",
"copyLink": "Копировать ссылку", "copyLink": "Копировать ссылку",
"exportFile": "Экспорт файла", "exportFile": "Экспорт файла",
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?", "cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
"detectionTip": "Опирается на сторонний API, только для справки", "detectionTip": "Опирается на сторонний API, только для справки",
"listen": "Слушать" "listen": "Слушать",
"undo": "Отменить",
"redo": "Повторить",
"none": "Нет",
"basicConfig": "Базовая конфигурация",
"basicConfigDesc": "Глобальное изменение базовых настроек",
"selectedCountTitle": "Выбрано {count} элементов",
"addRule": "Добавить правило",
"ruleName": "Название правила",
"content": "Содержание",
"subRule": "Подправило",
"ruleTarget": "Цель правила",
"sourceIp": "Исходный IP",
"noResolve": "Не разрешать IP",
"getOriginRules": "Получить оригинальные правила",
"overrideOriginRules": "Переопределить оригинальное правило",
"addedOriginRules": "Добавить к оригинальным правилам",
"enableOverride": "Включить переопределение",
"saveChanges": "Сохранить изменения?",
"generalDesc": "Изменение общих настроек",
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
"tabAnimationDesc": "Действительно только в мобильном виде",
"saveTip": "Вы уверены, что хотите сохранить?",
"colorSchemes": "Цветовые схемы",
"palette": "Палитра",
"tonalSpotScheme": "Тональный акцент",
"fidelityScheme": "Точная передача",
"monochromeScheme": "Монохром",
"neutralScheme": "Нейтральные",
"vibrantScheme": "Яркие",
"expressiveScheme": "Экспрессивные",
"contentScheme": "Контентная тема",
"rainbowScheme": "Радужные",
"fruitSaladScheme": "Фруктовый микс",
"developerMode": "Режим разработчика",
"developerModeEnableTip": "Режим разработчика активирован.",
"messageTest": "Тестирование сообщения",
"messageTestTip": "Это сообщение.",
"crashTest": "Тест на сбои",
"clearData": "Очистить данные",
"zoom": "Масштаб",
"textScale": "Масштабирование текста",
"internet": "Интернет",
"systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети",
"contactMe": "Свяжитесь со мной",
"recoveryStrategy": "Стратегия восстановления",
"recoveryStrategy_override": "Переопределение",
"recoveryStrategy_compatible": "Совместимый",
"logsTest": "Тест журналов",
"emptyTip": "{label} не может быть пустым",
"urlTip": "{label} должен быть URL",
"numberTip": "{label} должно быть числом",
"interval": "Интервал",
"existsTip": "Текущий {label} уже существует",
"deleteTip": "Вы уверены, что хотите удалить текущий {label}?",
"deleteMultipTip": "Вы уверены, что хотите удалить выбранные {label}?",
"nullTip": "Сейчас {label} нет",
"script": "Скрипт",
"color": "Цвет",
"rename": "Переименовать",
"unnamed": "Без имени",
"pleaseEnterScriptName": "Пожалуйста, введите название скрипта",
"overrideInvalidTip": "В скриптовом режиме не действует",
"mixedPort": "Смешанный порт",
"socksPort": "Socks-порт",
"redirPort": "Redir-порт",
"tproxyPort": "Tproxy-порт",
"portTip": "{label} должен быть числом от 1024 до 49151",
"portConflictTip": "Введите другой порт",
"import": "Импорт",
"importFile": "Импорт из файла",
"importUrl": "Импорт по URL",
"autoSetSystemDns": "Автоматическая настройка системного DNS",
"details": "Детали {}",
"creationTime": "Время создания",
"progress": "Прогресс",
"host": "Хост",
"destination": "Назначение",
"destinationGeoIP": "Геолокация назначения",
"destinationIPASN": "ASN назначения",
"specialProxy": "Специальный прокси",
"specialRules": "Специальные правила",
"remoteDestination": "Удалённое назначение",
"networkType": "Тип сети",
"proxyChains": "Цепочки прокси",
"log": "Журнал",
"connection": "Соединение",
"request": "Запрос"
} }

View File

@@ -13,7 +13,6 @@
"resourcesDesc": "外部资源相关信息", "resourcesDesc": "外部资源相关信息",
"trafficUsage": "流量统计", "trafficUsage": "流量统计",
"coreInfo": "内核信息", "coreInfo": "内核信息",
"nullCoreInfoDesc": "无法获取内核信息",
"networkSpeed": "网络速度", "networkSpeed": "网络速度",
"outboundMode": "出站模式", "outboundMode": "出站模式",
"networkDetection": "网络检测", "networkDetection": "网络检测",
@@ -22,7 +21,6 @@
"noProxy": "暂无代理", "noProxy": "暂无代理",
"noProxyDesc": "请创建配置文件或者添加有效配置文件", "noProxyDesc": "请创建配置文件或者添加有效配置文件",
"nullProfileDesc": "没有配置文件,请先添加配置文件", "nullProfileDesc": "没有配置文件,请先添加配置文件",
"nullLogsDesc": "暂无日志",
"settings": "设置", "settings": "设置",
"language": "语言", "language": "语言",
"defaultText": "默认", "defaultText": "默认",
@@ -123,7 +121,6 @@
"project": "项目", "project": "项目",
"core": "内核", "core": "内核",
"tabAnimation": "选项卡动画", "tabAnimation": "选项卡动画",
"tabAnimationDesc": "开启后,主页选项卡将添加切换动画",
"desc": "基于ClashMeta的多平台代理客户端简单易用开源无广告。", "desc": "基于ClashMeta的多平台代理客户端简单易用开源无广告。",
"startVpn": "正在启动VPN...", "startVpn": "正在启动VPN...",
"stopVpn": "正在停止VPN...", "stopVpn": "正在停止VPN...",
@@ -150,8 +147,6 @@
"addressHelp": "WebDAV服务器地址", "addressHelp": "WebDAV服务器地址",
"addressTip": "请输入有效的WebDAV地址", "addressTip": "请输入有效的WebDAV地址",
"password": "密码", "password": "密码",
"passwordTip": "密码不能为空",
"accountTip": "账号不能为空",
"checkUpdate": "检查更新", "checkUpdate": "检查更新",
"discoverNewVersion": "发现新版本", "discoverNewVersion": "发现新版本",
"checkUpdateError": "当前应用已经是最新版了", "checkUpdateError": "当前应用已经是最新版了",
@@ -169,7 +164,7 @@
"externalControllerDesc": "开启后将可以通过9090端口控制Clash内核", "externalControllerDesc": "开启后将可以通过9090端口控制Clash内核",
"ipv6Desc": "开启后将可以接收IPv6流量", "ipv6Desc": "开启后将可以接收IPv6流量",
"app": "应用", "app": "应用",
"general": "基础", "general": "常规",
"vpnSystemProxyDesc": "为VpnService附加HTTP代理", "vpnSystemProxyDesc": "为VpnService附加HTTP代理",
"systemProxyDesc": "设置系统代理", "systemProxyDesc": "设置系统代理",
"unifiedDelay": "统一延迟", "unifiedDelay": "统一延迟",
@@ -181,14 +176,11 @@
"requests": "请求", "requests": "请求",
"requestsDesc": "查看最近请求记录", "requestsDesc": "查看最近请求记录",
"findProcessMode": "查找进程", "findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化", "init": "初始化",
"infiniteTime": "长期有效", "infiniteTime": "长期有效",
"expirationTime": "到期时间", "expirationTime": "到期时间",
"connections": "连接", "connections": "连接",
"connectionsDesc": "查看当前连接数据", "connectionsDesc": "查看当前连接数据",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIP": "内网 IP", "intranetIP": "内网 IP",
"view": "查看", "view": "查看",
"cut": "剪切", "cut": "剪切",
@@ -221,7 +213,6 @@
"autoCloseConnectionsDesc": "切换节点后自动关闭连接", "autoCloseConnectionsDesc": "切换节点后自动关闭连接",
"onlyStatisticsProxy": "仅统计代理", "onlyStatisticsProxy": "仅统计代理",
"onlyStatisticsProxyDesc": "开启后,将只统计代理流量", "onlyStatisticsProxyDesc": "开启后,将只统计代理流量",
"deleteProfileTip": "确定要删除当前配置吗?",
"pureBlackMode": "纯黑模式", "pureBlackMode": "纯黑模式",
"keepAliveIntervalDesc": "TCP保持活动间隔", "keepAliveIntervalDesc": "TCP保持活动间隔",
"entries": "个条目", "entries": "个条目",
@@ -249,11 +240,9 @@
"stop": "暂停", "stop": "暂停",
"appDesc": "处理应用相关设置", "appDesc": "处理应用相关设置",
"vpnDesc": "修改VPN相关设置", "vpnDesc": "修改VPN相关设置",
"generalDesc": "覆写基础设置",
"dnsDesc": "更新DNS相关设置", "dnsDesc": "更新DNS相关设置",
"key": "键", "key": "键",
"value": "值", "value": "值",
"notEmpty": "不能为空",
"hostsDesc": "追加Hosts", "hostsDesc": "追加Hosts",
"vpnTip": "重启VPN后改变生效", "vpnTip": "重启VPN后改变生效",
"vpnEnableDesc": "通过VpnService自动路由系统所有流量", "vpnEnableDesc": "通过VpnService自动路由系统所有流量",
@@ -340,11 +329,98 @@
"fileIsUpdate": "文件有修改,是否保存修改", "fileIsUpdate": "文件有修改,是否保存修改",
"profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ", "profileHasUpdate": "配置文件已经修改,是否关闭自动更新 ",
"hasCacheChange": "是否缓存修改", "hasCacheChange": "是否缓存修改",
"nullProxies": "暂无代理",
"copySuccess": "复制成功", "copySuccess": "复制成功",
"copyLink": "复制链接", "copyLink": "复制链接",
"exportFile": "导出文件", "exportFile": "导出文件",
"cacheCorrupt": "缓存已损坏,是否清空?", "cacheCorrupt": "缓存已损坏,是否清空?",
"detectionTip": "依赖第三方api仅供参考", "detectionTip": "依赖第三方api仅供参考",
"listen": "监听" "listen": "监听",
"undo": "撤销",
"redo": "重做",
"none": "无",
"basicConfig": "基本配置",
"basicConfigDesc": "全局修改基本配置",
"selectedCountTitle": "已选择 {count} 项",
"addRule": "添加规则",
"ruleName": "规则名称",
"content": "内容",
"subRule": "子规则",
"ruleTarget": "规则目标",
"sourceIp": "源IP",
"noResolve": "不解析IP",
"getOriginRules": "获取原始规则",
"overrideOriginRules": "覆盖原始规则",
"addedOriginRules": "附加到原始规则",
"enableOverride": "启用覆写",
"saveChanges": "是否保存更改?",
"generalDesc": "修改通用设置",
"findProcessModeDesc": "开启后会有一定性能损耗",
"tabAnimationDesc": "仅在移动视图中有效",
"saveTip": "确定要保存吗?",
"colorSchemes": "配色方案",
"palette": "调色板",
"tonalSpotScheme": "调性点缀",
"fidelityScheme": "高保真",
"monochromeScheme": "单色",
"neutralScheme": "中性",
"vibrantScheme": "活力",
"expressiveScheme": "表现力",
"contentScheme": "内容主题",
"rainbowScheme": "彩虹",
"fruitSaladScheme": "果缤纷",
"developerMode": "开发者模式",
"developerModeEnableTip": "开发者模式已启用。",
"messageTest": "消息测试",
"messageTestTip": "这是一条消息。",
"crashTest": "崩溃测试",
"clearData": "清除数据",
"zoom": "缩放",
"textScale": "文本缩放",
"internet": "互联网",
"systemApp": "系统应用",
"noNetworkApp": "无网络应用",
"contactMe": "联系我",
"recoveryStrategy": "恢复策略",
"recoveryStrategy_override": "覆盖",
"recoveryStrategy_compatible": "兼容",
"logsTest": "日志测试",
"emptyTip": "{label}不能为空",
"urlTip": "{label}必须为URL",
"numberTip": "{label}必须为数字",
"interval": "间隔",
"existsTip": "{label}当前已存在",
"deleteTip": "确定删除当前{label}吗?",
"deleteMultipTip": "确定删除选中的{label}吗?",
"nullTip": "暂无{label}",
"script": "脚本",
"color": "颜色",
"rename": "重命名",
"unnamed": "未命名",
"pleaseEnterScriptName": "请输入脚本名称",
"overrideInvalidTip": "在脚本模式下不生效",
"mixedPort": "混合端口",
"socksPort": "Socks端口",
"redirPort": "Redir端口",
"tproxyPort": "Tproxy端口",
"portTip": "{label} 必须在 1024 到 49151 之间",
"portConflictTip": "请输入不同的端口",
"import": "导入",
"importFile": "通过文件导入",
"importUrl": "通过URL导入",
"autoSetSystemDns": "自动设置系统DNS",
"details": "{label}详情",
"creationTime": "创建时间",
"progress": "进度",
"host": "主机",
"destination": "目标地址",
"destinationGeoIP": "目标地理定位",
"destinationIPASN": "目标IP ASN",
"specialProxy": "特殊代理",
"specialRules": "特殊规则",
"remoteDestination": "远程目标",
"networkType": "网络类型",
"proxyChains": "代理链",
"log": "日志",
"connection": "连接",
"request": "请求"
} }

Binary file not shown.

View File

@@ -5,16 +5,17 @@ import (
) )
type Action struct { type Action struct {
Id string `json:"id"` Id string `json:"id"`
Method Method `json:"method"` Method Method `json:"method"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
DefaultValue interface{} `json:"default-value"`
} }
type ActionResult struct { type ActionResult struct {
Id string `json:"id"` Id string `json:"id"`
Method Method `json:"method"` Method Method `json:"method"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
Code int `json:"code"`
Port int64
} }
func (result ActionResult) Json() ([]byte, error) { func (result ActionResult) Json() ([]byte, error) {
@@ -22,99 +23,117 @@ func (result ActionResult) Json() ([]byte, error) {
return data, err return data, err
} }
func (action Action) getResult(data interface{}) []byte { func (result ActionResult) success(data interface{}) {
resultAction := ActionResult{ result.Code = 0
Id: action.Id, result.Data = data
Method: action.Method, result.send()
Data: data,
}
res, _ := resultAction.Json()
return res
} }
func handleAction(action *Action, result func(data interface{})) { func (result ActionResult) error(data interface{}) {
result.Code = -1
result.Data = data
result.send()
}
func handleAction(action *Action, result ActionResult) {
switch action.Method { switch action.Method {
case initClashMethod: case initClashMethod:
data := action.Data.(string) paramsString := action.Data.(string)
result(handleInitClash(data)) result.success(handleInitClash(paramsString))
return return
case getIsInitMethod: case getIsInitMethod:
result(handleGetIsInit()) result.success(handleGetIsInit())
return return
case forceGcMethod: case forceGcMethod:
handleForceGc() handleForceGc()
result(true) result.success(true)
return return
case shutdownMethod: case shutdownMethod:
result(handleShutdown()) result.success(handleShutdown())
return return
case validateConfigMethod: case validateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))
result(handleValidateConfig(data)) result.success(handleValidateConfig(data))
return return
case updateConfigMethod: case updateConfigMethod:
data := []byte(action.Data.(string)) data := []byte(action.Data.(string))
result(handleUpdateConfig(data)) result.success(handleUpdateConfig(data))
return
case setupConfigMethod:
data := []byte(action.Data.(string))
result.success(handleSetupConfig(data))
return return
case getProxiesMethod: case getProxiesMethod:
result(handleGetProxies()) result.success(handleGetProxies())
return return
case changeProxyMethod: case changeProxyMethod:
data := action.Data.(string) data := action.Data.(string)
handleChangeProxy(data, func(value string) { handleChangeProxy(data, func(value string) {
result(value) result.success(value)
}) })
return return
case getTrafficMethod: case getTrafficMethod:
result(handleGetTraffic()) result.success(handleGetTraffic())
return return
case getTotalTrafficMethod: case getTotalTrafficMethod:
result(handleGetTotalTraffic()) result.success(handleGetTotalTraffic())
return return
case resetTrafficMethod: case resetTrafficMethod:
handleResetTraffic() handleResetTraffic()
result(true) result.success(true)
return return
case asyncTestDelayMethod: case asyncTestDelayMethod:
data := action.Data.(string) data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) { handleAsyncTestDelay(data, func(value string) {
result(value) result.success(value)
}) })
return return
case getConnectionsMethod: case getConnectionsMethod:
result(handleGetConnections()) result.success(handleGetConnections())
return return
case closeConnectionsMethod: case closeConnectionsMethod:
result(handleCloseConnections()) result.success(handleCloseConnections())
return
case resetConnectionsMethod:
result.success(handleResetConnections())
return
case getConfigMethod:
path := action.Data.(string)
config, err := handleGetConfig(path)
if err != nil {
result.error(err)
return
}
result.success(config)
return return
case closeConnectionMethod: case closeConnectionMethod:
id := action.Data.(string) id := action.Data.(string)
result(handleCloseConnection(id)) result.success(handleCloseConnection(id))
return return
case getExternalProvidersMethod: case getExternalProvidersMethod:
result(handleGetExternalProviders()) result.success(handleGetExternalProviders())
return return
case getExternalProviderMethod: case getExternalProviderMethod:
externalProviderName := action.Data.(string) externalProviderName := action.Data.(string)
result(handleGetExternalProvider(externalProviderName)) result.success(handleGetExternalProvider(externalProviderName))
case updateGeoDataMethod: case updateGeoDataMethod:
paramsString := action.Data.(string) paramsString := action.Data.(string)
var params = map[string]string{} var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params) err := json.Unmarshal([]byte(paramsString), &params)
if err != nil { if err != nil {
result(err.Error()) result.success(err.Error())
return return
} }
geoType := params["geo-type"] geoType := params["geo-type"]
geoName := params["geo-name"] geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) { handleUpdateGeoData(geoType, geoName, func(value string) {
result(value) result.success(value)
}) })
return return
case updateExternalProviderMethod: case updateExternalProviderMethod:
providerName := action.Data.(string) providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) { handleUpdateExternalProvider(providerName, func(value string) {
result(value) result.success(value)
}) })
return return
case sideLoadExternalProviderMethod: case sideLoadExternalProviderMethod:
@@ -122,56 +141,48 @@ func handleAction(action *Action, result func(data interface{})) {
var params = map[string]string{} var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params) err := json.Unmarshal([]byte(paramsString), &params)
if err != nil { if err != nil {
result(err.Error()) result.success(err.Error())
return return
} }
providerName := params["providerName"] providerName := params["providerName"]
data := params["data"] data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) { handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
result(value) result.success(value)
}) })
return return
case startLogMethod: case startLogMethod:
handleStartLog() handleStartLog()
result(true) result.success(true)
return return
case stopLogMethod: case stopLogMethod:
handleStopLog() handleStopLog()
result(true) result.success(true)
return return
case startListenerMethod: case startListenerMethod:
result(handleStartListener()) result.success(handleStartListener())
return return
case stopListenerMethod: case stopListenerMethod:
result(handleStopListener()) result.success(handleStopListener())
return return
case getCountryCodeMethod: case getCountryCodeMethod:
ip := action.Data.(string) ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) { handleGetCountryCode(ip, func(value string) {
result(value) result.success(value)
}) })
return return
case getMemoryMethod: case getMemoryMethod:
handleGetMemory(func(value string) { handleGetMemory(func(value string) {
result(value) result.success(value)
})
return
case getProfileMethod:
profileId := action.Data.(string)
handleGetMemory(func(value string) {
result(handleGetProfile(profileId))
}) })
return return
case setStateMethod: case setStateMethod:
data := action.Data.(string) data := action.Data.(string)
handleSetState(data) handleSetState(data)
result(true) result.success(true)
case crashMethod:
result.success(true)
handleCrash()
default: default:
handle := nextHandle(action, result) 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

@@ -1,9 +1,10 @@
package main package main
import ( import (
b "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -21,26 +22,16 @@ import (
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider" rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os" "os"
"path/filepath"
"strings"
"sync" "sync"
) )
func splitByComma(s string) interface{} {
parts := strings.Split(s, ",")
if len(parts) > 1 {
return parts
}
return s
}
var ( var (
isRunning = false currentConfig *config.Config
runLock sync.Mutex version = 0
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"} isRunning = false
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) runLock sync.Mutex
mBatch, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
) )
type ExternalProviders []ExternalProvider type ExternalProviders []ExternalProvider
@@ -49,55 +40,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 readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, err
}
func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
func getProfileProvidersPath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "providers", id)
}
func getRawConfigWithId(id string) *config.RawConfig {
path := getProfilePath(id)
bytes, err := readFile(path)
if err != nil {
log.Errorln("profile is not exist")
return config.DefaultRawConfig()
}
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
log.Errorln("unmarshalRawConfig error %v", err)
return config.DefaultRawConfig()
}
for _, mapping := range prof.ProxyProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
if !exist {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
}
return prof
}
func getExternalProvidersRaw() map[string]cp.Provider { func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider) eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() { for n, p := range tunnel.Providers() {
@@ -162,140 +104,15 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
} }
} }
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig { func updateListeners() {
prof := getRawConfigWithId(profileId)
overwriteConfig(prof, cfg)
return prof
}
func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
if str, ok := v.(string); ok {
hosts[k] = splitByComma(str)
}
}
}
func modPatchDns(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, splitByComma(str))
}
}
}
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
rule := trimArr(strings.Split(line, ","))
l := len(rule)
if l != 2 {
return
}
if strings.ToUpper(rule[0]) == "MATCH" {
target = rule[1]
break
}
}
if target == "" {
return
}
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
})
*rules = append(rulesExt, *rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
targetConfig.ExternalController = patchConfig.ExternalController
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = patchConfig.FindProcessMode
targetConfig.AllowLan = patchConfig.AllowLan
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
targetConfig.Tun.Stack = patchConfig.Tun.Stack
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
targetConfig.GeodataLoader = patchConfig.GeodataLoader
targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
}
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
modPatchDns(&patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
overrideRules(&targetConfig.Rule)
}
func patchConfig() {
log.Infoln("[Apply] patch")
general := currentConfig.General
controller := currentConfig.Controller
tls := currentConfig.TLS
tunnel.SetSniffing(general.Sniffing)
tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface)
adapter.UnifiedDelay.Store(general.UnifiedDelay)
tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6
route.ReCreateServer(&route.Config{
Addr: controller.ExternalController,
TLSAddr: controller.ExternalControllerTLS,
UnixAddr: controller.ExternalControllerUnix,
PipeAddr: controller.ExternalControllerPipe,
Secret: controller.Secret,
Certificate: tls.Certificate,
PrivateKey: tls.PrivateKey,
DohServer: controller.ExternalDohServer,
IsDebug: false,
Cors: route.Cors{
AllowOrigins: controller.Cors.AllowOrigins,
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
},
})
}
func updateListeners(force bool) {
if !isRunning { if !isRunning {
return return
} }
general := currentConfig.General if currentConfig == nil {
listeners := currentConfig.Listeners return
if force == true {
stopListeners()
} }
listeners := currentConfig.Listeners
general := currentConfig.General
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan) listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -319,11 +136,7 @@ func stopListeners() {
listener.StopListener() listener.StopListener()
} }
func patchSelectGroup() { func patchSelectGroup(mapping map[string]string) {
mapping := configParams.SelectedMap
if mapping == nil {
return
}
for name, proxy := range tunnel.ProxiesWithProviders() { for name, proxy := range tunnel.ProxiesWithProviders() {
outbound, ok := proxy.(*adapter.Proxy) outbound, ok := proxy.(*adapter.Proxy)
if !ok { if !ok {
@@ -344,20 +157,102 @@ func patchSelectGroup() {
} }
} }
func applyConfig(rawConfig *config.RawConfig) error { func defaultSetupParams() *SetupParams {
return &SetupParams{
Config: config.DefaultRawConfig(),
TestURL: "https://www.gstatic.com/generate_204",
SelectedMap: map[string]string{},
}
}
func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, err
}
func updateConfig(params *UpdateParams) {
runLock.Lock()
defer runLock.Unlock()
general := currentConfig.General
if params.MixedPort != nil {
general.MixedPort = *params.MixedPort
}
if params.Sniffing != nil {
general.Sniffing = *params.Sniffing
tunnel.SetSniffing(general.Sniffing)
}
if params.FindProcessMode != nil {
general.FindProcessMode = *params.FindProcessMode
tunnel.SetFindProcessMode(general.FindProcessMode)
}
if params.TCPConcurrent != nil {
general.TCPConcurrent = *params.TCPConcurrent
dialer.SetTcpConcurrent(general.TCPConcurrent)
}
if params.Interface != nil {
general.Interface = *params.Interface
dialer.DefaultInterface.Store(general.Interface)
}
if params.UnifiedDelay != nil {
general.UnifiedDelay = *params.UnifiedDelay
adapter.UnifiedDelay.Store(general.UnifiedDelay)
}
if params.Mode != nil {
general.Mode = *params.Mode
tunnel.SetMode(general.Mode)
}
if params.LogLevel != nil {
general.LogLevel = *params.LogLevel
log.SetLevel(general.LogLevel)
}
if params.IPv6 != nil {
general.IPv6 = *params.IPv6
resolver.DisableIPv6 = !general.IPv6
}
if params.ExternalController != nil {
currentConfig.Controller.ExternalController = *params.ExternalController
route.ReCreateServer(&route.Config{
Addr: currentConfig.Controller.ExternalController,
})
}
if params.Tun != nil {
general.Tun.Enable = params.Tun.Enable
general.Tun.AutoRoute = *params.Tun.AutoRoute
general.Tun.Device = *params.Tun.Device
general.Tun.RouteAddress = *params.Tun.RouteAddress
general.Tun.DNSHijack = *params.Tun.DNSHijack
general.Tun.Stack = *params.Tun.Stack
}
updateListeners()
}
func setupConfig(params *SetupParams) error {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
var err error var err error
currentConfig, err = config.ParseRawConfig(rawConfig) constant.DefaultTestURL = params.TestURL
currentConfig, err = config.ParseRawConfig(params.Config)
if err != nil { if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig()) currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
} }
if configParams.IsPatch { hub.ApplyConfig(currentConfig)
patchConfig() patchSelectGroup(params.SelectedMap)
} else { updateListeners()
hub.ApplyConfig(currentConfig) return err
patchSelectGroup() }
}
updateListeners(false) func UnmarshalJson(data []byte, v any) error {
decoder := json.NewDecoder(b.NewReader(data))
decoder.UseNumber()
err := decoder.Decode(v)
return err return err
} }

View File

@@ -3,22 +3,48 @@ package main
import ( import (
"encoding/json" "encoding/json"
"github.com/metacubex/mihomo/adapter/provider" "github.com/metacubex/mihomo/adapter/provider"
P "github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"net/netip"
"time" "time"
) )
type ConfigExtendedParams struct { type InitParams struct {
IsPatch bool `json:"is-patch"` HomeDir string `json:"home-dir"`
IsCompatible bool `json:"is-compatible"` Version int `json:"version"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
} }
type GenerateConfigParams struct { type SetupParams struct {
ProfileId string `json:"profile-id"` Config *config.RawConfig `json:"config"`
Config config.RawConfig `json:"config" ` SelectedMap map[string]string `json:"selected-map"`
Params ConfigExtendedParams `json:"params"` TestURL string `json:"test-url"`
}
type UpdateParams struct {
Tun *tunSchema `json:"tun"`
AllowLan *bool `json:"allow-lan"`
MixedPort *int `json:"mixed-port"`
FindProcessMode *P.FindProcessMode `json:"find-process-mode"`
Mode *tunnel.TunnelMode `json:"mode"`
LogLevel *log.LogLevel `json:"log-level"`
IPv6 *bool `json:"ipv6"`
Sniffing *bool `json:"sniffing"`
TCPConcurrent *bool `json:"tcp-concurrent"`
ExternalController *string `json:"external-controller"`
Interface *string `json:"interface-name"`
UnifiedDelay *bool `json:"unified-delay"`
}
type tunSchema struct {
Enable bool `yaml:"enable" json:"enable"`
Device *string `yaml:"device" json:"device"`
Stack *constant.TUNStack `yaml:"stack" json:"stack"`
DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute *bool `yaml:"auto-route" json:"auto-route"`
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
} }
type ChangeProxyParams struct { type ChangeProxyParams struct {
@@ -58,6 +84,7 @@ const (
asyncTestDelayMethod Method = "asyncTestDelay" asyncTestDelayMethod Method = "asyncTestDelay"
getConnectionsMethod Method = "getConnections" getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections" closeConnectionsMethod Method = "closeConnections"
resetConnectionsMethod Method = "resetConnectionsMethod"
closeConnectionMethod Method = "closeConnection" closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders" getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider" getExternalProviderMethod Method = "getExternalProvider"
@@ -70,16 +97,14 @@ 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"
getCurrentProfileNameMethod Method = "getCurrentProfileName" getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile" crashMethod Method = "crash"
setupConfigMethod Method = "setupConfig"
getConfigMethod Method = "getConfig"
) )
type Method string type Method string
@@ -108,20 +133,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)
}

View File

@@ -6,12 +6,12 @@ replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
github.com/samber/lo v1.49.1 golang.org/x/sync v0.11.0
) )
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.3.0 // indirect
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/ajg/form v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
@@ -20,13 +20,13 @@ require (
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.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ebitengine/purego v0.8.2 // indirect github.com/ebitengine/purego v0.8.3 // indirect
github.com/enfein/mieru/v3 v3.11.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-20250325060835-cd0defd64358 // 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.9.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.2.1 // 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
@@ -35,7 +35,7 @@ require (
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.1 // indirect github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/google/btree v1.1.3 // 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
@@ -50,21 +50,27 @@ require (
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/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bart v0.20.5 // 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.1 // indirect github.com/metacubex/chacha v0.1.5 // indirect
github.com/metacubex/fswatch 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-20241126021258-5b028898cc5a // indirect github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 // indirect
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect github.com/metacubex/sing-mux v0.3.2 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d // indirect
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect github.com/metacubex/sing-vmess v0.2.2 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.63 // 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
@@ -74,17 +80,11 @@ 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.5.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/sagernet/cors v1.2.1 // indirect github.com/sagernet/cors v1.2.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/samber/lo v1.50.0 // indirect
github.com/sagernet/sing v0.5.2 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // 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
@@ -106,7 +106,6 @@ require (
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.35.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.7.0 // indirect

View File

@@ -1,8 +1,8 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@@ -26,12 +26,12 @@ 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.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/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.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw= 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-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/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=
@@ -39,8 +39,8 @@ github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBE
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=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
@@ -59,8 +59,8 @@ 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.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0= github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.3.2/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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -97,36 +97,49 @@ 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.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.20.5/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.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c= github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
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-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
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/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 h1:Y9ebcKya6ow7VHoESCN5+l4zZvg5eaL2IhI5LLCQxQA=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92/go.mod h1:/squZ38pXrYjqtg8qn+joVvwbpGNYQNp8yxKsMVbCto=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d h1:Ey3A1tA8lVkRbK1FDmwuWj/57Nr8JMdpoVqe45mFzJg=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8= github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d/go.mod h1:+ukTd0OPFglT3bnKAYTJWYPbuox6HYNXE235r5tHdUk=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 h1:4zWKqxTx75TbfW2EmlQ3hxM6RTRg2PYOAVMCnU4I61I=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 h1:HWKsf92BqLYqugATFIJ3hYiEBZ7JF6AoqyvqF39afuI=
github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
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.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@@ -153,31 +166,16 @@ 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.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.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/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
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/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
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/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
@@ -189,9 +187,16 @@ 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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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=
@@ -251,7 +256,7 @@ golang.org/x/sys v0.1.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.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=

View File

@@ -10,6 +10,7 @@ import (
"github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
@@ -28,15 +29,19 @@ import (
var ( var (
isInit = false isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{} externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event] logSubscriber observable.Subscription[log.Event]
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
@@ -46,7 +51,8 @@ func handleStartListener() bool {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
isRunning = true isRunning = true
updateListeners(true) updateListeners()
resolver.ResetConnection()
return true return true
} }
@@ -85,30 +91,10 @@ func handleValidateConfig(bytes []byte) string {
return "" return ""
} }
func handleUpdateConfig(bytes []byte) string { func handleGetProxies() map[string]constant.Proxy {
var params = &GenerateConfigParams{}
err := json.Unmarshal(bytes, params)
if err != nil {
return err.Error()
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
err = applyConfig(prof)
if err != nil {
return err.Error()
}
return ""
}
func handleGetProxies() string {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
data, err := json.Marshal(tunnel.ProxiesWithProviders()) return tunnel.ProxiesWithProviders()
if err != nil {
return ""
}
return string(data)
} }
func handleChangeProxy(data string, fn func(string string)) { func handleChangeProxy(data string, fn func(string string)) {
@@ -178,21 +164,12 @@ func handleGetTotalTraffic() 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()
} }
func handleAsyncTestDelay(paramsString string, fn func(string)) { func handleAsyncTestDelay(paramsString string, fn func(string)) {
b.Go(paramsString, func() (bool, error) { mBatch.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{} var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params) err := json.Unmarshal([]byte(paramsString), params)
if err != nil { if err != nil {
@@ -260,6 +237,11 @@ func handleGetConnections() string {
func handleCloseConnections() bool { func handleCloseConnections() bool {
runLock.Lock() runLock.Lock()
defer runLock.Unlock() defer runLock.Unlock()
closeConnections()
return true
}
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool { statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close() err := c.Close()
if err != nil { if err != nil {
@@ -267,6 +249,12 @@ func handleCloseConnections() bool {
} }
return true return true
}) })
}
func handleResetConnections() bool {
runLock.Lock()
defer runLock.Unlock()
resolver.ResetConnection()
return true return true
} }
@@ -436,6 +424,47 @@ func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState) _ = json.Unmarshal([]byte(params), state.CurrentState)
} }
func handleGetConfig(path string) (*config.RawConfig, error) {
bytes, err := readFile(path)
if err != nil {
return nil, err
}
prof, err := config.UnmarshalRawConfig(bytes)
if err != nil {
return nil, err
}
return prof, nil
}
func handleCrash() {
panic("handle invoke crash")
}
func handleUpdateConfig(bytes []byte) string {
var params = &UpdateParams{}
err := json.Unmarshal(bytes, params)
if err != nil {
return err.Error()
}
updateConfig(params)
return ""
}
func handleSetupConfig(bytes []byte) string {
var params = defaultSetupParams()
err := UnmarshalJson(bytes, params)
if err != nil {
log.Errorln("unmarshalRawConfig error %v", err)
_ = setupConfig(defaultSetupParams())
return err.Error()
}
err = setupConfig(params)
if err != nil {
return err.Error()
}
return ""
}
func init() { func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) { adapter.UrlTestHook = func(url string, name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{

View File

@@ -39,6 +39,14 @@ func freeCString(s *C.char) {
C.free(unsafe.Pointer(s)) C.free(unsafe.Pointer(s))
} }
func (result ActionResult) send() {
data, err := result.Json()
if err != nil {
return
}
bridge.SendToPort(result.Port, string(data))
}
//export invokeAction //export invokeAction
func invokeAction(paramsChar *C.char, port C.longlong) { func invokeAction(paramsChar *C.char, port C.longlong) {
params := C.GoString(paramsChar) params := C.GoString(paramsChar)
@@ -49,22 +57,38 @@ func invokeAction(paramsChar *C.char, port C.longlong) {
bridge.SendToPort(i, err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
go handleAction(action, func(data interface{}) { result := ActionResult{
bridge.SendToPort(i, string(action.getResult(data))) Id: action.Id,
}) Method: action.Method,
Port: i,
}
go handleAction(action, result)
} }
func sendMessage(message Message) { func sendMessage(message Message) {
if messagePort == -1 { if messagePort == -1 {
return return
} }
res, err := message.Json() result := ActionResult{
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod, Method: messageMethod,
}.getResult(res))) Port: messagePort,
Data: message,
}
result.send()
}
//export getConfig
func getConfig(s *C.char) *C.char {
path := C.GoString(s)
config, err := handleGetConfig(path)
if err != nil {
return C.CString("")
}
marshal, err := json.Marshal(config)
if err != nil {
return C.CString("")
}
return C.CString(string(marshal))
} }
//export startListener //export startListener

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,118 +12,111 @@ 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) {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
if fd != 0 {
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())
tunHandler.listener = tunListener
} else {
removeTunHook()
}
} }
} }
@@ -133,83 +127,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()
@@ -248,66 +188,48 @@ func handleGetCurrentProfileName() string {
return state.CurrentState.CurrentProfileName return state.CurrentState.CurrentProfileName
} }
func nextHandle(action *Action, result func(data interface{})) bool { func nextHandle(action *Action, result ActionResult) 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.success(handleGetAndroidVpnOptions())
return true return true
case updateDnsMethod: case updateDnsMethod:
data := action.Data.(string) data := action.Data.(string)
handleUpdateDns(data) handleUpdateDns(data)
result(true) result.success(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 return true
case getRunTimeMethod: case getRunTimeMethod:
result(handleGetRunTime()) result.success(handleGetRunTime())
return true return true
case getCurrentProfileNameMethod: case getCurrentProfileNameMethod:
result(handleGetCurrentProfileName()) result.success(handleGetCurrentProfileName())
return true return true
} }
return false return false
} }
//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")
} }
handleSetState(stateParams) handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes)) bridge.SendToPort(i, handleSetupConfig(bytes))
}() }()
} }
//export startTUN //export startTUN
func startTUN(fd C.int) *C.char { func startTUN(fd C.int, callback unsafe.Pointer) bool {
f := int(fd) go func() {
return C.CString(handleStartTun(f)) handleStartTun(int(fd), callback)
}()
return true
} }
//export getRunTime //export getRunTime
@@ -317,13 +239,9 @@ func getRunTime() *C.char {
//export stopTun //export stopTun
func stopTun() { func stopTun() {
handleStopTun() go func() {
} handleStopTun()
}()
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
} }
//export getCurrentProfileName //export getCurrentProfileName
@@ -347,12 +265,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
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

@@ -12,14 +12,20 @@ import (
var conn net.Conn var conn net.Conn
func sendMessage(message Message) { func (result ActionResult) send() {
res, err := message.Json() data, err := result.Json()
if err != nil { if err != nil {
return return
} }
send(Action{ send(data)
}
func sendMessage(message Message) {
result := ActionResult{
Method: messageMethod, Method: messageMethod,
}.getResult(res)) Data: message,
}
result.send()
} }
func send(data []byte) { func send(data []byte) {
@@ -61,12 +67,15 @@ func startServer(arg string) {
return return
} }
go handleAction(action, func(data interface{}) { result := ActionResult{
send(action.getResult(data)) Id: action.Id,
}) Method: action.Method,
}
go handleAction(action, result)
} }
} }
func nextHandle(action *Action, result func(data interface{})) bool { func nextHandle(action *Action, result ActionResult) bool {
return false return false
} }

View File

@@ -24,7 +24,6 @@ type AccessControl struct {
Mode string `json:"mode"` Mode string `json:"mode"`
AcceptList []string `json:"acceptList"` AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"` RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
} }
type AndroidVpnRawOptions struct { type AndroidVpnRawOptions struct {

View File

@@ -1,20 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart'; 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/config.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:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'controller.dart'; import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart'; import 'pages/pages.dart';
class Application extends ConsumerStatefulWidget { class Application extends ConsumerStatefulWidget {
@@ -27,7 +26,6 @@ class Application extends ConsumerStatefulWidget {
} }
class ApplicationState extends ConsumerState<Application> { class ApplicationState extends ConsumerState<Application> {
late ColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer; Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer; Timer? _autoUpdateProfilesTaskTimer;
@@ -43,16 +41,8 @@ class ApplicationState extends ConsumerState<Application> {
ColorScheme _getAppColorScheme({ ColorScheme _getAppColorScheme({
required Brightness brightness, required Brightness brightness,
int? primaryColor, int? primaryColor,
required ColorSchemes systemColorSchemes,
}) { }) {
if (primaryColor != null) { return ref.read(genColorSchemeProvider(brightness));
return ColorScheme.fromSeed(
seedColor: Color(primaryColor),
brightness: brightness,
);
} else {
return systemColorSchemes.getColorSchemeForBrightness(brightness);
}
} }
@override @override
@@ -61,16 +51,6 @@ class ApplicationState extends ConsumerState<Application> {
_autoUpdateGroupTask(); _autoUpdateGroupTask();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
globalState.appController = AppController(context, ref); globalState.appController = AppController(context, ref);
globalState.measure = Measure.of(context);
// ref.listenManual(themeSettingProvider.select((state) => state.fontFamily),
// (prev, next) {
// if (prev != next) {
// globalState.measure = Measure.of(
// context,
// fontFamily: next.value,
// );
// }
// }, fireImmediately: true);
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) {
@@ -82,7 +62,7 @@ class ApplicationState extends ConsumerState<Application> {
}); });
} }
_autoUpdateGroupTask() { void _autoUpdateGroupTask() {
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () { _autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupsDebounce(); globalState.appController.updateGroupsDebounce();
@@ -91,14 +71,14 @@ class ApplicationState extends ConsumerState<Application> {
}); });
} }
_autoUpdateProfilesTask() { void _autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async { _autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles(); await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask(); _autoUpdateProfilesTask();
}); });
} }
_buildPlatformWrap(Widget child) { Widget _buildPlatformState(Widget child) {
if (system.isDesktop) { if (system.isDesktop) {
return WindowManager( return WindowManager(
child: TrayManager( child: TrayManager(
@@ -117,22 +97,14 @@ class ApplicationState extends ConsumerState<Application> {
); );
} }
_buildPage(Widget page) { Widget _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: () { onConnectivityChanged: (results) async {
if (!results.contains(ConnectivityResult.vpn)) {
clashCore.closeConnections();
}
globalState.appController.updateLocalIp(); globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce(); globalState.appController.addCheckIpNumDebounce();
}, },
@@ -142,77 +114,76 @@ class ApplicationState extends ConsumerState<Application> {
); );
} }
_updateSystemColorSchemes( Widget _buildPlatformApp(Widget child) {
ColorScheme? lightDynamic, if (system.isDesktop) {
ColorScheme? darkDynamic, return WindowHeaderContainer(
) { child: child,
systemColorSchemes = ColorSchemes( );
lightColorScheme: lightDynamic, }
darkColorScheme: darkDynamic, return VpnManager(
child: child,
);
}
Widget _buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
),
); );
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
});
} }
@override @override
Widget build(context) { Widget build(context) {
return _buildPlatformWrap( return _buildPlatformState(
_buildWrap( _buildState(
Consumer( Consumer(
builder: (_, ref, child) { builder: (_, ref, child) {
final locale = final locale =
ref.watch(appSettingProvider.select((state) => state.locale)); ref.watch(appSettingProvider.select((state) => state.locale));
final themeProps = ref.watch(themeSettingProvider); final themeProps = ref.watch(themeSettingProvider);
return DynamicColorBuilder( return MaterialApp(
builder: (lightDynamic, darkDynamic) { debugShowCheckedModeBanner: false,
_updateSystemColorSchemes(lightDynamic, darkDynamic); navigatorKey: globalState.navigatorKey,
return MaterialApp( localizationsDelegates: const [
navigatorKey: globalState.navigatorKey, AppLocalizations.delegate,
localizationsDelegates: const [ GlobalMaterialLocalizations.delegate,
AppLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate
GlobalCupertinoLocalizations.delegate, ],
GlobalWidgetsLocalizations.delegate builder: (_, child) {
], return AppEnvManager(
builder: (_, child) { child: _buildApp(
return MessageManager( AppSidebarContainer(
child: LayoutBuilder( child: _buildPlatformApp(
builder: (_, container) { child!,
globalState.appController.updateViewWidth(
container.maxWidth,
);
return _buildPage(child!);
},
), ),
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
), ),
), ),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child,
); );
}, },
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: utils.getLocaleForString(locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: themeProps.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
primaryColor: themeProps.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
primaryColor: themeProps.primaryColor,
).toPureBlack(themeProps.pureBlack),
),
home: child!,
); );
}, },
child: const HomePage(), child: const HomePage(),

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';
@@ -16,7 +17,7 @@ class ClashCore {
late ClashHandlerInterface clashInterface; late ClashHandlerInterface clashInterface;
ClashCore._internal() { ClashCore._internal() {
if (Platform.isAndroid) { if (system.isAndroid) {
clashInterface = clashLib!; clashInterface = clashLib!;
} else { } else {
clashInterface = clashService!; clashInterface = clashService!;
@@ -65,15 +66,25 @@ class ClashCore {
Future<bool> init() async { Future<bool> init() async {
await initGeo(); await initGeo();
if (globalState.config.appSetting.openLogs) {
clashCore.startLog();
} else {
clashCore.stopLog();
}
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 {
return await clashInterface.setState(state); return await clashInterface.setState(state);
} }
shutdown() async { Future<void> shutdown() async {
await clashInterface.shutdown(); await clashInterface.shutdown();
} }
@@ -83,60 +94,64 @@ class ClashCore {
return clashInterface.validateConfig(data); return clashInterface.validateConfig(data);
} }
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async { Future<String> updateConfig(UpdateParams updateParams) async {
return await clashInterface.updateConfig(updateConfigParams); return await clashInterface.updateConfig(updateParams);
}
Future<String> setupConfig(SetupParams setupParams) async {
return await clashInterface.setupConfig(setupParams);
} }
Future<List<Group>> getProxiesGroups() async { Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies(); final proxies = await clashInterface.getProxies();
return Isolate.run<List<Group>>(() { if (proxies.isEmpty) return [];
if (proxiesRawString.isEmpty) return []; final groupNames = [
final proxies = (json.decode(proxiesRawString) ?? {}) as Map; UsedProxy.GLOBAL.name,
if (proxies.isEmpty) return []; ...(proxies[UsedProxy.GLOBAL.name]['all'] as List).where((e) {
final groupNames = [ final proxy = proxies[e] ?? {};
UsedProxy.GLOBAL.name, return GroupTypeExtension.valueList.contains(proxy['type']);
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) { })
final proxy = proxies[e] ?? {}; ];
return GroupTypeExtension.valueList.contains(proxy['type']); final groupsRaw = groupNames.map((groupName) {
}) final group = proxies[groupName];
]; group['all'] = ((group['all'] ?? []) as List)
final groupsRaw = groupNames.map((groupName) {
final group = proxies[groupName];
group["all"] = ((group["all"] ?? []) as List)
.map(
(name) => proxies[name],
)
.where((proxy) => proxy != null)
.toList();
return group;
}).toList();
return groupsRaw
.map( .map(
(e) => Group.fromJson(e), (name) => proxies[name],
) )
.where((proxy) => proxy != null)
.toList(); .toList();
}); return group;
}).toList();
return groupsRaw
.map(
(e) => Group.fromJson(e),
)
.toList();
} }
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async { FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams); return await clashInterface.changeProxy(changeProxyParams);
} }
Future<List<Connection>> getConnections() async { Future<List<TrackerInfo>> getConnections() async {
final res = await clashInterface.getConnections(); final res = await clashInterface.getConnections();
final connectionsData = json.decode(res) as Map; final connectionsData = json.decode(res) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? []; final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList(); return connectionsRaw.map((e) => TrackerInfo.fromJson(e)).toList();
} }
closeConnection(String id) { void closeConnection(String id) {
clashInterface.closeConnection(id); clashInterface.closeConnection(id);
} }
closeConnections() { void closeConnections() {
clashInterface.closeConnections(); clashInterface.closeConnections();
} }
void resetConnections() {
clashInterface.resetConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async { Future<List<ExternalProvider>> getExternalProviders() async {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProviders(); await clashInterface.getExternalProviders();
@@ -187,11 +202,11 @@ class ClashCore {
return clashInterface.updateExternalProvider(providerName); return clashInterface.updateExternalProvider(providerName);
} }
startListener() async { Future<void> startListener() async {
await clashInterface.startListener(); await clashInterface.startListener();
} }
stopListener() async { Future<void> stopListener() async {
await clashInterface.stopListener(); await clashInterface.stopListener();
} }
@@ -200,6 +215,16 @@ class ClashCore {
return Delay.fromJson(json.decode(data)); return Delay.fromJson(json.decode(data));
} }
Future<Map<String, dynamic>> getConfig(String id) async {
final profilePath = await appPath.getProfilePath(id);
final res = await clashInterface.getConfig(profilePath);
if (res.isSuccess) {
return res.data as Map<String, dynamic>;
} else {
throw res.message;
}
}
Future<Traffic> getTraffic() async { Future<Traffic> getTraffic() async {
final trafficString = await clashInterface.getTraffic(); final trafficString = await clashInterface.getTraffic();
if (trafficString.isEmpty) { if (trafficString.isEmpty) {
@@ -235,31 +260,23 @@ class ClashCore {
return int.parse(value); return int.parse(value);
} }
Future<ClashConfigSnippet?> getProfile(String id) async { void resetTraffic() {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return ClashConfigSnippet.fromJson(json.decode(res));
}
resetTraffic() {
clashInterface.resetTraffic(); clashInterface.resetTraffic();
} }
startLog() { void startLog() {
clashInterface.startLog(); clashInterface.startLog();
} }
stopLog() { void stopLog() {
clashInterface.stopLog(); clashInterface.stopLog();
} }
requestGc() { Future<void> requestGc() async {
clashInterface.forceGc(); await clashInterface.forceGc();
} }
destroy() async { Future<void> destroy() async {
await clashInterface.destroy(); await clashInterface.destroy();
} }
} }

View File

@@ -154,18 +154,18 @@ class ClashFFI {
late final _setrlimit = late final _setrlimit =
_setrlimitPtr.asFunction<int Function(int, ffi.Pointer<rlimit>)>(); _setrlimitPtr.asFunction<int Function(int, ffi.Pointer<rlimit>)>();
int wait1( int wait$1(
ffi.Pointer<ffi.Int> arg0, ffi.Pointer<ffi.Int> arg0,
) { ) {
return _wait1( return _wait$1(
arg0, arg0,
); );
} }
late final _wait1Ptr = late final _wait$1Ptr =
_lookup<ffi.NativeFunction<pid_t Function(ffi.Pointer<ffi.Int>)>>('wait'); _lookup<ffi.NativeFunction<pid_t Function(ffi.Pointer<ffi.Int>)>>('wait');
late final _wait1 = late final _wait$1 =
_wait1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>(); _wait$1Ptr.asFunction<int Function(ffi.Pointer<ffi.Int>)>();
int waitpid( int waitpid(
int arg0, int arg0,
@@ -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,
) { ) {
@@ -2427,6 +2518,20 @@ class ClashFFI {
late final _invokeAction = late final _invokeAction =
_invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>(); _invokeActionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
ffi.Pointer<ffi.Char> getConfig(
ffi.Pointer<ffi.Char> s,
) {
return _getConfig(
s,
);
}
late final _getConfigPtr = _lookup<
ffi.NativeFunction<
ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>>('getConfig');
late final _getConfig = _getConfigPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
void startListener() { void startListener() {
return _startListener(); return _startListener();
} }
@@ -2443,28 +2548,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 +2570,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 +2604,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,22 +2651,31 @@ 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>)>();
} }
typedef __int8_t = ffi.SignedChar;
typedef Dart__int8_t = int;
typedef __uint8_t = ffi.UnsignedChar;
typedef Dart__uint8_t = int;
typedef __int16_t = ffi.Short;
typedef Dart__int16_t = int;
typedef __uint16_t = ffi.UnsignedShort;
typedef Dart__uint16_t = int;
typedef __int32_t = ffi.Int;
typedef Dart__int32_t = int;
typedef __uint32_t = ffi.UnsignedInt;
typedef Dart__uint32_t = int;
typedef __int64_t = ffi.LongLong;
typedef Dart__int64_t = int;
typedef __uint64_t = ffi.UnsignedLongLong;
typedef Dart__uint64_t = int;
typedef __darwin_intptr_t = ffi.Long;
typedef Dart__darwin_intptr_t = int;
typedef __darwin_natural_t = ffi.UnsignedInt;
typedef Dart__darwin_natural_t = int;
typedef __darwin_ct_rune_t = ffi.Int;
typedef Dart__darwin_ct_rune_t = int;
final class __mbstate_t extends ffi.Union { final class __mbstate_t extends ffi.Union {
@ffi.Array.multi([128]) @ffi.Array.multi([128])
external ffi.Array<ffi.Char> __mbstate8; external ffi.Array<ffi.Char> __mbstate8;
@@ -2596,6 +2684,46 @@ final class __mbstate_t extends ffi.Union {
external int _mbstateL; external int _mbstateL;
} }
typedef __darwin_mbstate_t = __mbstate_t;
typedef __darwin_ptrdiff_t = ffi.Long;
typedef Dart__darwin_ptrdiff_t = int;
typedef __darwin_size_t = ffi.UnsignedLong;
typedef Dart__darwin_size_t = int;
typedef __builtin_va_list = ffi.Pointer<ffi.Char>;
typedef __darwin_va_list = __builtin_va_list;
typedef __darwin_wchar_t = ffi.Int;
typedef Dart__darwin_wchar_t = int;
typedef __darwin_rune_t = __darwin_wchar_t;
typedef __darwin_wint_t = ffi.Int;
typedef Dart__darwin_wint_t = int;
typedef __darwin_clock_t = ffi.UnsignedLong;
typedef Dart__darwin_clock_t = int;
typedef __darwin_socklen_t = __uint32_t;
typedef __darwin_ssize_t = ffi.Long;
typedef Dart__darwin_ssize_t = int;
typedef __darwin_time_t = ffi.Long;
typedef Dart__darwin_time_t = int;
typedef __darwin_blkcnt_t = __int64_t;
typedef __darwin_blksize_t = __int32_t;
typedef __darwin_dev_t = __int32_t;
typedef __darwin_fsblkcnt_t = ffi.UnsignedInt;
typedef Dart__darwin_fsblkcnt_t = int;
typedef __darwin_fsfilcnt_t = ffi.UnsignedInt;
typedef Dart__darwin_fsfilcnt_t = int;
typedef __darwin_gid_t = __uint32_t;
typedef __darwin_id_t = __uint32_t;
typedef __darwin_ino64_t = __uint64_t;
typedef __darwin_ino_t = __darwin_ino64_t;
typedef __darwin_mach_port_name_t = __darwin_natural_t;
typedef __darwin_mach_port_t = __darwin_mach_port_name_t;
typedef __darwin_mode_t = __uint16_t;
typedef __darwin_off_t = __int64_t;
typedef __darwin_pid_t = __int32_t;
typedef __darwin_sigset_t = __uint32_t;
typedef __darwin_suseconds_t = __int32_t;
typedef __darwin_uid_t = __uint32_t;
typedef __darwin_useconds_t = __uint32_t;
final class __darwin_pthread_handler_rec extends ffi.Struct { final class __darwin_pthread_handler_rec extends ffi.Struct {
external ffi external ffi
.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>> .Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>
@@ -2680,6 +2808,48 @@ final class _opaque_pthread_t extends ffi.Struct {
external ffi.Array<ffi.Char> __opaque; external ffi.Array<ffi.Char> __opaque;
} }
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
typedef __darwin_pthread_cond_t = _opaque_pthread_cond_t;
typedef __darwin_pthread_condattr_t = _opaque_pthread_condattr_t;
typedef __darwin_pthread_key_t = ffi.UnsignedLong;
typedef Dart__darwin_pthread_key_t = int;
typedef __darwin_pthread_mutex_t = _opaque_pthread_mutex_t;
typedef __darwin_pthread_mutexattr_t = _opaque_pthread_mutexattr_t;
typedef __darwin_pthread_once_t = _opaque_pthread_once_t;
typedef __darwin_pthread_rwlock_t = _opaque_pthread_rwlock_t;
typedef __darwin_pthread_rwlockattr_t = _opaque_pthread_rwlockattr_t;
typedef __darwin_pthread_t = ffi.Pointer<_opaque_pthread_t>;
typedef __darwin_nl_item = ffi.Int;
typedef Dart__darwin_nl_item = int;
typedef __darwin_wctrans_t = ffi.Int;
typedef Dart__darwin_wctrans_t = int;
typedef __darwin_wctype_t = __uint32_t;
typedef u_int8_t = ffi.UnsignedChar;
typedef Dartu_int8_t = int;
typedef u_int16_t = ffi.UnsignedShort;
typedef Dartu_int16_t = int;
typedef u_int32_t = ffi.UnsignedInt;
typedef Dartu_int32_t = int;
typedef u_int64_t = ffi.UnsignedLongLong;
typedef Dartu_int64_t = int;
typedef register_t = ffi.Int64;
typedef Dartregister_t = int;
typedef user_addr_t = u_int64_t;
typedef user_size_t = u_int64_t;
typedef user_ssize_t = ffi.Int64;
typedef Dartuser_ssize_t = int;
typedef user_long_t = ffi.Int64;
typedef Dartuser_long_t = int;
typedef user_ulong_t = u_int64_t;
typedef user_time_t = ffi.Int64;
typedef Dartuser_time_t = int;
typedef user_off_t = ffi.Int64;
typedef Dartuser_off_t = int;
typedef syscall_arg_t = u_int64_t;
typedef ptrdiff_t = __darwin_ptrdiff_t;
typedef rsize_t = __darwin_size_t;
typedef wint_t = __darwin_wint_t;
final class _GoString_ extends ffi.Struct { final class _GoString_ extends ffi.Struct {
external ffi.Pointer<ffi.Char> p; external ffi.Pointer<ffi.Char> p;
@@ -2687,10 +2857,6 @@ final class _GoString_ extends ffi.Struct {
external int n; external int n;
} }
typedef ptrdiff_t = __darwin_ptrdiff_t;
typedef __darwin_ptrdiff_t = ffi.Long;
typedef Dart__darwin_ptrdiff_t = int;
enum idtype_t { enum idtype_t {
P_ALL(0), P_ALL(0),
P_PID(1), P_PID(1),
@@ -2703,10 +2869,15 @@ enum idtype_t {
0 => P_ALL, 0 => P_ALL,
1 => P_PID, 1 => P_PID,
2 => P_PGID, 2 => P_PGID,
_ => throw ArgumentError("Unknown value for idtype_t: $value"), _ => throw ArgumentError('Unknown value for idtype_t: $value'),
}; };
} }
typedef pid_t = __darwin_pid_t;
typedef id_t = __darwin_id_t;
typedef sig_atomic_t = ffi.Int;
typedef Dartsig_atomic_t = int;
final class __darwin_arm_exception_state extends ffi.Struct { final class __darwin_arm_exception_state extends ffi.Struct {
@__uint32_t() @__uint32_t()
external int __exception; external int __exception;
@@ -2718,9 +2889,6 @@ final class __darwin_arm_exception_state extends ffi.Struct {
external int __far; external int __far;
} }
typedef __uint32_t = ffi.UnsignedInt;
typedef Dart__uint32_t = int;
final class __darwin_arm_exception_state64 extends ffi.Struct { final class __darwin_arm_exception_state64 extends ffi.Struct {
@__uint64_t() @__uint64_t()
external int __far; external int __far;
@@ -2732,9 +2900,6 @@ final class __darwin_arm_exception_state64 extends ffi.Struct {
external int __exception; external int __exception;
} }
typedef __uint64_t = ffi.UnsignedLongLong;
typedef Dart__uint64_t = int;
final class __darwin_arm_exception_state64_v2 extends ffi.Struct { final class __darwin_arm_exception_state64_v2 extends ffi.Struct {
@__uint64_t() @__uint64_t()
external int __far; external int __far;
@@ -2863,6 +3028,9 @@ final class __darwin_mcontext32 extends ffi.Struct {
final class __darwin_mcontext64 extends ffi.Opaque {} final class __darwin_mcontext64 extends ffi.Opaque {}
typedef mcontext_t = ffi.Pointer<__darwin_mcontext64>;
typedef pthread_attr_t = __darwin_pthread_attr_t;
final class __darwin_sigaltstack extends ffi.Struct { final class __darwin_sigaltstack extends ffi.Struct {
external ffi.Pointer<ffi.Void> ss_sp; external ffi.Pointer<ffi.Void> ss_sp;
@@ -2873,8 +3041,7 @@ final class __darwin_sigaltstack extends ffi.Struct {
external int ss_flags; external int ss_flags;
} }
typedef __darwin_size_t = ffi.UnsignedLong; typedef stack_t = __darwin_sigaltstack;
typedef Dart__darwin_size_t = int;
final class __darwin_ucontext extends ffi.Struct { final class __darwin_ucontext extends ffi.Struct {
@ffi.Int() @ffi.Int()
@@ -2893,7 +3060,9 @@ final class __darwin_ucontext extends ffi.Struct {
external ffi.Pointer<__darwin_mcontext64> uc_mcontext; external ffi.Pointer<__darwin_mcontext64> uc_mcontext;
} }
typedef __darwin_sigset_t = __uint32_t; typedef ucontext_t = __darwin_ucontext;
typedef sigset_t = __darwin_sigset_t;
typedef uid_t = __darwin_uid_t;
final class sigval extends ffi.Union { final class sigval extends ffi.Union {
@ffi.Int() @ffi.Int()
@@ -2917,9 +3086,6 @@ final class sigevent extends ffi.Struct {
external ffi.Pointer<pthread_attr_t> sigev_notify_attributes; external ffi.Pointer<pthread_attr_t> sigev_notify_attributes;
} }
typedef pthread_attr_t = __darwin_pthread_attr_t;
typedef __darwin_pthread_attr_t = _opaque_pthread_attr_t;
final class __siginfo extends ffi.Struct { final class __siginfo extends ffi.Struct {
@ffi.Int() @ffi.Int()
external int si_signo; external int si_signo;
@@ -2950,12 +3116,7 @@ final class __siginfo extends ffi.Struct {
external ffi.Array<ffi.UnsignedLong> __pad; external ffi.Array<ffi.UnsignedLong> __pad;
} }
typedef pid_t = __darwin_pid_t; typedef siginfo_t = __siginfo;
typedef __darwin_pid_t = __int32_t;
typedef __int32_t = ffi.Int;
typedef Dart__int32_t = int;
typedef uid_t = __darwin_uid_t;
typedef __darwin_uid_t = __uint32_t;
final class __sigaction_u extends ffi.Union { final class __sigaction_u extends ffi.Union {
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>> external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
@@ -2969,7 +3130,7 @@ final class __sigaction_u extends ffi.Union {
} }
final class __sigaction extends ffi.Struct { final class __sigaction extends ffi.Struct {
external __sigaction_u __sigaction_u1; external __sigaction_u __sigaction_u$1;
external ffi.Pointer< external ffi.Pointer<
ffi.NativeFunction< ffi.NativeFunction<
@@ -2983,11 +3144,8 @@ final class __sigaction extends ffi.Struct {
external int sa_flags; external int sa_flags;
} }
typedef siginfo_t = __siginfo;
typedef sigset_t = __darwin_sigset_t;
final class sigaction extends ffi.Struct { final class sigaction extends ffi.Struct {
external __sigaction_u __sigaction_u1; external __sigaction_u __sigaction_u$1;
@sigset_t() @sigset_t()
external int sa_mask; external int sa_mask;
@@ -2996,6 +3154,10 @@ final class sigaction extends ffi.Struct {
external int sa_flags; external int sa_flags;
} }
typedef sig_tFunction = ffi.Void Function(ffi.Int);
typedef Dartsig_tFunction = void Function(int);
typedef sig_t = ffi.Pointer<ffi.NativeFunction<sig_tFunction>>;
final class sigvec extends ffi.Struct { final class sigvec extends ffi.Struct {
external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>> external ffi.Pointer<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>
sv_handler; sv_handler;
@@ -3014,6 +3176,43 @@ final class sigstack extends ffi.Struct {
external int ss_onstack; external int ss_onstack;
} }
typedef int_least8_t = ffi.Int8;
typedef Dartint_least8_t = int;
typedef int_least16_t = ffi.Int16;
typedef Dartint_least16_t = int;
typedef int_least32_t = ffi.Int32;
typedef Dartint_least32_t = int;
typedef int_least64_t = ffi.Int64;
typedef Dartint_least64_t = int;
typedef uint_least8_t = ffi.Uint8;
typedef Dartuint_least8_t = int;
typedef uint_least16_t = ffi.Uint16;
typedef Dartuint_least16_t = int;
typedef uint_least32_t = ffi.Uint32;
typedef Dartuint_least32_t = int;
typedef uint_least64_t = ffi.Uint64;
typedef Dartuint_least64_t = int;
typedef int_fast8_t = ffi.Int8;
typedef Dartint_fast8_t = int;
typedef int_fast16_t = ffi.Int16;
typedef Dartint_fast16_t = int;
typedef int_fast32_t = ffi.Int32;
typedef Dartint_fast32_t = int;
typedef int_fast64_t = ffi.Int64;
typedef Dartint_fast64_t = int;
typedef uint_fast8_t = ffi.Uint8;
typedef Dartuint_fast8_t = int;
typedef uint_fast16_t = ffi.Uint16;
typedef Dartuint_fast16_t = int;
typedef uint_fast32_t = ffi.Uint32;
typedef Dartuint_fast32_t = int;
typedef uint_fast64_t = ffi.Uint64;
typedef Dartuint_fast64_t = int;
typedef intmax_t = ffi.Long;
typedef Dartintmax_t = int;
typedef uintmax_t = ffi.UnsignedLong;
typedef Dartuintmax_t = int;
final class timeval extends ffi.Struct { final class timeval extends ffi.Struct {
@__darwin_time_t() @__darwin_time_t()
external int tv_sec; external int tv_sec;
@@ -3022,9 +3221,7 @@ final class timeval extends ffi.Struct {
external int tv_usec; external int tv_usec;
} }
typedef __darwin_time_t = ffi.Long; typedef rlim_t = __uint64_t;
typedef Dart__darwin_time_t = int;
typedef __darwin_suseconds_t = __int32_t;
final class rusage extends ffi.Struct { final class rusage extends ffi.Struct {
external timeval ru_utime; external timeval ru_utime;
@@ -3074,6 +3271,8 @@ final class rusage extends ffi.Struct {
external int ru_nivcsw; external int ru_nivcsw;
} }
typedef rusage_info_t = ffi.Pointer<ffi.Void>;
final class rusage_info_v0 extends ffi.Struct { final class rusage_info_v0 extends ffi.Struct {
@ffi.Array.multi([16]) @ffi.Array.multi([16])
external ffi.Array<ffi.Uint8> ri_uuid; external ffi.Array<ffi.Uint8> ri_uuid;
@@ -3679,6 +3878,8 @@ final class rusage_info_v6 extends ffi.Struct {
external ffi.Array<ffi.Uint64> ri_reserved; external ffi.Array<ffi.Uint64> ri_reserved;
} }
typedef rusage_info_current = rusage_info_v6;
final class rlimit extends ffi.Struct { final class rlimit extends ffi.Struct {
@rlim_t() @rlim_t()
external int rlim_cur; external int rlim_cur;
@@ -3687,8 +3888,6 @@ final class rlimit extends ffi.Struct {
external int rlim_max; external int rlim_max;
} }
typedef rlim_t = __uint64_t;
final class proc_rlimit_control_wakeupmon extends ffi.Struct { final class proc_rlimit_control_wakeupmon extends ffi.Struct {
@ffi.Uint32() @ffi.Uint32()
external int wm_flags; external int wm_flags;
@@ -3697,11 +3896,11 @@ final class proc_rlimit_control_wakeupmon extends ffi.Struct {
external int wm_rate; external int wm_rate;
} }
typedef id_t = __darwin_id_t;
typedef __darwin_id_t = __uint32_t;
final class wait extends ffi.Opaque {} final class wait extends ffi.Opaque {}
typedef ct_rune_t = __darwin_ct_rune_t;
typedef rune_t = __darwin_rune_t;
final class div_t extends ffi.Struct { final class div_t extends ffi.Struct {
@ffi.Int() @ffi.Int()
external int quot; external int quot;
@@ -3733,11 +3932,59 @@ final class _malloc_zone_t extends ffi.Opaque {}
typedef malloc_zone_t = _malloc_zone_t; typedef malloc_zone_t = _malloc_zone_t;
typedef dev_t = __darwin_dev_t; typedef dev_t = __darwin_dev_t;
typedef __darwin_dev_t = __int32_t;
typedef mode_t = __darwin_mode_t; typedef mode_t = __darwin_mode_t;
typedef __darwin_mode_t = __uint16_t; typedef release_object_funcFunction = ffi.Void Function(
typedef __uint16_t = ffi.UnsignedShort; ffi.Pointer<ffi.Void> obj);
typedef Dart__uint16_t = int; typedef Dartrelease_object_funcFunction = void Function(
ffi.Pointer<ffi.Void> obj);
typedef release_object_func
= ffi.Pointer<ffi.NativeFunction<release_object_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 protect_func = ffi.Pointer<ffi.NativeFunction<protect_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 resolve_process_func
= ffi.Pointer<ffi.NativeFunction<resolve_process_funcFunction>>;
typedef GoInt8 = ffi.SignedChar;
typedef DartGoInt8 = int;
typedef GoUint8 = ffi.UnsignedChar;
typedef DartGoUint8 = int;
typedef GoInt16 = ffi.Short;
typedef DartGoInt16 = int;
typedef GoUint16 = ffi.UnsignedShort;
typedef DartGoUint16 = int;
typedef GoInt32 = ffi.Int;
typedef DartGoInt32 = int;
typedef GoUint32 = ffi.UnsignedInt;
typedef DartGoUint32 = int;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
typedef GoUint64 = ffi.UnsignedLongLong;
typedef DartGoUint64 = int;
typedef GoInt = GoInt64;
typedef GoUint = GoUint64;
typedef GoUintptr = ffi.Size;
typedef DartGoUintptr = int;
typedef GoFloat32 = ffi.Float;
typedef DartGoFloat32 = double;
typedef GoFloat64 = ffi.Double;
typedef DartGoFloat64 = double;
typedef GoString = _GoString_;
typedef GoMap = ffi.Pointer<ffi.Void>;
typedef GoChan = ffi.Pointer<ffi.Void>;
final class GoInterface extends ffi.Struct { final class GoInterface extends ffi.Struct {
external ffi.Pointer<ffi.Void> t; external ffi.Pointer<ffi.Void> t;
@@ -3755,10 +4002,6 @@ final class GoSlice extends ffi.Struct {
external int cap; external int cap;
} }
typedef GoInt = GoInt64;
typedef GoInt64 = ffi.LongLong;
typedef DartGoInt64 = int;
const int __has_safe_buffers = 1; const int __has_safe_buffers = 1;
const int __DARWIN_ONLY_64_BIT_INO_T = 1; const int __DARWIN_ONLY_64_BIT_INO_T = 1;
@@ -3973,6 +4216,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 +4380,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 +4480,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 +4582,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 +4640,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 +4672,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 +4684,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 +4812,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,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:isolate';
import 'package:fl_clash/clash/message.dart'; import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
@@ -7,7 +8,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();
@@ -19,11 +20,15 @@ mixin ClashInterface {
FutureOr<String> validateConfig(String data); FutureOr<String> validateConfig(String data);
FutureOr<Result> getConfig(String path);
Future<String> asyncTestDelay(String url, String proxyName); Future<String> asyncTestDelay(String url, String proxyName);
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams); FutureOr<String> updateConfig(UpdateParams updateParams);
FutureOr<String> getProxies(); FutureOr<String> setupConfig(SetupParams setupParams);
FutureOr<Map> getProxies();
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams); FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
@@ -52,11 +57,13 @@ mixin ClashInterface {
FutureOr<String> getMemory(); FutureOr<String> getMemory();
resetTraffic(); FutureOr<void> resetTraffic();
startLog(); FutureOr<void> startLog();
stopLog(); FutureOr<void> stopLog();
Future<bool> crash();
FutureOr<String> getConnections(); FutureOr<String> getConnections();
@@ -64,22 +71,14 @@ mixin ClashInterface {
FutureOr<bool> closeConnections(); FutureOr<bool> closeConnections();
FutureOr<String> getProfile(String id); FutureOr<bool> resetConnections();
Future<bool> setState(CoreState state); Future<bool> setState(CoreState state);
} }
mixin AndroidClashInterface { mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
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();
@@ -90,60 +89,29 @@ mixin AndroidClashInterface {
abstract class ClashHandlerInterface with ClashInterface { abstract class ClashHandlerInterface with ClashInterface {
Map<String, Completer> callbackCompleterMap = {}; Map<String, Completer> callbackCompleterMap = {};
Future<bool> nextHandleResult(ActionResult result, Completer? completer) => Future<void> handleResult(ActionResult result) async {
Future.value(false);
handleResult(ActionResult result) async {
final completer = callbackCompleterMap[result.id]; final completer = callbackCompleterMap[result.id];
try { try {
switch (result.method) { 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: case ActionMethod.message:
clashMessage.controller.add(result.data as String); clashMessage.controller.add(result.data);
completer?.complete(true); completer?.complete(true);
return; return;
case ActionMethod.getConfig:
completer?.complete(result.toResult);
return;
default: default:
final isHandled = await nextHandleResult(result, completer);
if (isHandled) {
return;
}
completer?.complete(result.data); completer?.complete(result.data);
return;
} }
} catch (_) { } catch (e) {
commonPrint.log(result.id); commonPrint.log('${result.id} error $e');
} }
} }
sendMessage(String message); void sendMessage(String message);
reStart(); FutureOr<void> reStart();
FutureOr<bool> destroy(); FutureOr<bool> destroy();
@@ -152,18 +120,21 @@ abstract class ClashHandlerInterface with ClashInterface {
dynamic data, dynamic data,
Duration? timeout, Duration? timeout,
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,
T? defaultValue,
}) async { }) async {
final id = "${method.name}#${other.id}"; final id = '${method.name}#${utils.id}';
callbackCompleterMap[id] = Completer<T>(); callbackCompleterMap[id] = Completer<T>();
dynamic defaultValue; dynamic mDefaultValue = defaultValue;
if (mDefaultValue == null) {
if (T == String) { if (T == String) {
defaultValue = ""; mDefaultValue = '';
} } else if (T == bool) {
if (T == bool) { mDefaultValue = false;
defaultValue = false; } else if (T == Map) {
mDefaultValue = {};
}
} }
sendMessage( sendMessage(
@@ -172,7 +143,6 @@ abstract class ClashHandlerInterface with ClashInterface {
id: id, id: id,
method: method, method: method,
data: data, data: data,
defaultValue: defaultValue,
), ),
), ),
); );
@@ -184,17 +154,17 @@ abstract class ClashHandlerInterface with ClashInterface {
}, },
onTimeout: onTimeout ?? onTimeout: onTimeout ??
() { () {
return defaultValue; return mDefaultValue;
}, },
functionName: id, functionName: id,
); );
} }
@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),
); );
} }
@@ -210,6 +180,7 @@ abstract class ClashHandlerInterface with ClashInterface {
shutdown() async { shutdown() async {
return await invoke<bool>( return await invoke<bool>(
method: ActionMethod.shutdown, method: ActionMethod.shutdown,
timeout: Duration(seconds: 1),
); );
} }
@@ -236,17 +207,45 @@ abstract class ClashHandlerInterface with ClashInterface {
} }
@override @override
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async { Future<String> updateConfig(UpdateParams updateParams) async {
return await invoke<String>( return await invoke<String>(
method: ActionMethod.updateConfig, method: ActionMethod.updateConfig,
data: json.encode(updateConfigParams), data: json.encode(updateParams),
timeout: Duration(minutes: 2), timeout: Duration(minutes: 2),
); );
} }
@override @override
Future<String> getProxies() { Future<Result> getConfig(String path) async {
return invoke<String>( final res = await invoke<Result>(
method: ActionMethod.getConfig,
data: path,
timeout: Duration(minutes: 2),
defaultValue: Result.success({}),
);
return res;
}
@override
Future<String> setupConfig(SetupParams setupParams) async {
final data = await Isolate.run(() => json.encode(setupParams));
return await invoke<String>(
method: ActionMethod.setupConfig,
data: data,
timeout: Duration(minutes: 2),
);
}
@override
Future<bool> crash() {
return invoke<bool>(
method: ActionMethod.crash,
);
}
@override
Future<Map> getProxies() {
return invoke<Map>(
method: ActionMethod.getProxies, method: ActionMethod.getProxies,
timeout: Duration(seconds: 5), timeout: Duration(seconds: 5),
); );
@@ -291,8 +290,8 @@ abstract class ClashHandlerInterface with ClashInterface {
return invoke<String>( return invoke<String>(
method: ActionMethod.sideLoadExternalProvider, method: ActionMethod.sideLoadExternalProvider,
data: json.encode({ data: json.encode({
"providerName": providerName, 'providerName': providerName,
"data": data, 'data': data,
}), }),
); );
} }
@@ -321,17 +320,16 @@ abstract class ClashHandlerInterface with ClashInterface {
} }
@override @override
Future<bool> closeConnection(String id) { Future<bool> resetConnections() {
return invoke<bool>( return invoke<bool>(
method: ActionMethod.closeConnection, method: ActionMethod.resetConnections,
data: id,
); );
} }
@override @override
Future<String> getProfile(String id) { Future<bool> closeConnection(String id) {
return invoke<String>( return invoke<bool>(
method: ActionMethod.getProfile, method: ActionMethod.closeConnection,
data: id, data: id,
); );
} }
@@ -384,9 +382,9 @@ abstract class ClashHandlerInterface with ClashInterface {
@override @override
Future<String> asyncTestDelay(String url, String proxyName) { Future<String> asyncTestDelay(String url, String proxyName) {
final delayParams = { final delayParams = {
"proxy-name": proxyName, 'proxy-name': proxyName,
"timeout": httpTimeoutDuration.inMilliseconds, 'timeout': httpTimeoutDuration.inMilliseconds,
"test-url": url, 'test-url': url,
}; };
return invoke<String>( return invoke<String>(
method: ActionMethod.asyncTestDelay, method: ActionMethod.asyncTestDelay,

View File

@@ -1,12 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:ui'; 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/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/plugins/service.dart'; import 'package:fl_clash/plugins/service.dart';
@@ -30,7 +29,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
return _canSendCompleter.future; return _canSendCompleter.future;
} }
_initService() async { Future<void> _initService() async {
await service?.destroy(); await service?.destroy();
_registerMainPort(receiverPort.sendPort); _registerMainPort(receiverPort.sendPort);
receiverPort.listen((message) { receiverPort.listen((message) {
@@ -52,7 +51,7 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
await service?.init(); await service?.init();
} }
_registerMainPort(SendPort sendPort) { void _registerMainPort(SendPort sendPort) {
IsolateNameServer.removePortNameMapping(mainIsolate); IsolateNameServer.removePortNameMapping(mainIsolate);
IsolateNameServer.registerPortWithName(sendPort, mainIsolate); IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
} }
@@ -62,26 +61,6 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
return _instance!; return _instance!;
} }
@override
Future<bool> nextHandleResult(result, completer) async {
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
destroy() async { destroy() async {
await service?.destroy(); await service?.destroy();
@@ -106,41 +85,12 @@ class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
sendPort?.send(message); sendPort?.send(message);
} }
@override // @override
Future<bool> setFdMap(int fd) { // Future<bool> stopTun() {
return invoke<bool>( // return invoke<bool>(
method: ActionMethod.setFdMap, // method: ActionMethod.stopTun,
data: json.encode(fd), // );
); // }
}
@override
Future<bool> setProcessMap(item) {
return invoke<bool>(
method: ActionMethod.setProcessMap,
data: item,
);
}
@override
Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>(
method: ActionMethod.startTun,
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 {
@@ -188,7 +138,7 @@ class ClashLibHandler {
late final DynamicLibrary lib; late final DynamicLibrary lib;
ClashLibHandler._internal() { ClashLibHandler._internal() {
lib = DynamicLibrary.open("libclash.so"); lib = DynamicLibrary.open('libclash.so');
clashFFI = ClashFFI(lib); clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge( clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData, NativeApi.initializeApiDLData,
@@ -218,44 +168,19 @@ class ClashLibHandler {
return completer.future; return completer.future;
} }
attachMessagePort(int messagePort) { void attachMessagePort(int messagePort) {
clashFFI.attachMessagePort( clashFFI.attachMessagePort(
messagePort, messagePort,
); );
} }
attachInvokePort(int invokePort) { void updateDns(String dns) {
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) {
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) { void setState(CoreState state) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
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);
malloc.free(stateChar); malloc.free(stateChar);
@@ -295,27 +220,44 @@ class ClashLibHandler {
return Traffic.fromMap(json.decode(trafficString)); return Traffic.fromMap(json.decode(trafficString));
} }
startListener() async { Future<bool> startListener() async {
clashFFI.startListener(); clashFFI.startListener();
return true; return true;
} }
stopListener() async { Future<bool> stopListener() async {
clashFFI.stopListener(); clashFFI.stopListener();
return true; return true;
} }
setFdMap(String id) { DateTime? getRunTime() {
final idChar = id.toNativeUtf8().cast<Char>(); final runTimeRaw = clashFFI.getRunTime();
clashFFI.setFdMap(idChar); final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
malloc.free(idChar); if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
Future<Map<String, dynamic>> getConfig(String id) async {
final path = await appPath.getProfilePath(id);
final pathChar = path.toNativeUtf8().cast<Char>();
final configRaw = clashFFI.getConfig(pathChar);
final configString = configRaw.cast<Utf8>().toDartString();
if (configString.isEmpty) {
return {};
}
final config = json.decode(configString);
malloc.free(pathChar);
clashFFI.freeCString(configRaw);
return config;
} }
Future<String> quickStart( Future<String> quickStart(
String homeDir, InitParams initParams,
UpdateConfigParams updateConfigParams, SetupParams setupParams,
CoreState state, CoreState state,
) { ) {
final completer = Completer<String>(); final completer = Completer<String>();
final receiver = ReceivePort(); final receiver = ReceivePort();
receiver.listen((message) { receiver.listen((message) {
@@ -324,31 +266,27 @@ class ClashLibHandler {
receiver.close(); receiver.close();
} }
}); });
final params = json.encode(updateConfigParams); final params = json.encode(setupParams);
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;
} }
DateTime? getRunTime() {
final runTimeRaw = clashFFI.getRunTime();
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
} }
ClashLib? get clashLib => ClashLib? get clashLib =>
Platform.isAndroid && !globalState.isService ? ClashLib() : null; system.isAndroid && !globalState.isService ? ClashLib() : null;
ClashLibHandler? get clashLibHandler =>
system.isAndroid && globalState.isService ? ClashLibHandler() : null;

View File

@@ -1,20 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
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<String>(); final controller = StreamController<Map<String, Object?>>();
ClashMessage._() { ClashMessage._() {
controller.stream.listen( controller.stream.listen(
(message) { (message) {
if(message.isEmpty){ if (message.isEmpty) {
return; return;
} }
final m = AppMessage.fromJson(json.decode(message)); final m = AppMessage.fromJson(message);
for (final AppMessageListener listener in _listeners) { for (final AppMessageListener listener in _listeners) {
switch (m.type) { switch (m.type) {
case AppMessageType.log: case AppMessageType.log:
@@ -24,7 +23,7 @@ class ClashMessage {
listener.onDelay(Delay.fromJson(m.data)); listener.onDelay(Delay.fromJson(m.data));
break; break;
case AppMessageType.request: case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data)); listener.onRequest(TrackerInfo.fromJson(m.data));
break; break;
case AppMessageType.loaded: case AppMessageType.loaded:
listener.onLoaded(m.data); listener.onLoaded(m.data);

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
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';
@@ -29,9 +28,9 @@ class ClashService extends ClashHandlerInterface {
reStart(); reStart();
} }
_initServer() async { Future<void> _initServer() async {
runZonedGuarded(() async { runZonedGuarded(() async {
final address = !Platform.isWindows final address = !system.isWindows
? InternetAddress( ? InternetAddress(
unixSocketPath, unixSocketPath,
type: InternetAddressType.unix, type: InternetAddressType.unix,
@@ -51,29 +50,24 @@ class ClashService extends ClashHandlerInterface {
await _destroySocket(); await _destroySocket();
socketCompleter.complete(socket); socketCompleter.complete(socket);
socket socket
.transform( .transform(uint8ListToListIntConverter)
StreamTransformer<Uint8List, String>.fromHandlers( .transform(utf8.decoder)
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter()) .transform(LineSplitter())
.listen( .listen(
(data) { (data) {
handleResult( handleResult(
ActionResult.fromJson( ActionResult.fromJson(
json.decode(data.trim()), json.decode(data.trim()),
), ),
);
},
); );
},
);
} }
}, (error, stack) { }, (error, stack) {
commonPrint.log(error.toString()); commonPrint.log(error.toString());
if(error is SocketException){ if (error is SocketException) {
globalState.showNotifier(error.toString()); globalState.showNotifier(error.toString());
globalState.appController.restartCore(); // globalState.appController.restartCore();
} }
}); });
} }
@@ -89,15 +83,14 @@ class ClashService extends ClashHandlerInterface {
await shutdown(); await shutdown();
} }
final serverSocket = await serverCompleter.future; final serverSocket = await serverCompleter.future;
final arg = Platform.isWindows final arg = system.isWindows
? "${serverSocket.port}" ? '${serverSocket.port}'
: serverSocket.address.address; : serverSocket.address.address;
bool isSuccess = false; if (system.isWindows && await system.checkIsAdmin()) {
if (Platform.isWindows && await system.checkIsAdmin()) { final isSuccess = await request.startCoreByHelper(arg);
isSuccess = await request.startCoreByHelper(arg); if (isSuccess) {
} return;
if (isSuccess) { }
return;
} }
process = await Process.start( process = await Process.start(
appPath.corePath, appPath.corePath,
@@ -105,7 +98,13 @@ class ClashService extends ClashHandlerInterface {
arg, arg,
], ],
); );
process!.stdout.listen((_) {}); process?.stdout.listen((_) {});
process?.stderr.listen((e) {
final error = utf8.decode(e);
if (error.isNotEmpty) {
commonPrint.log(error);
}
});
isStarting = false; isStarting = false;
} }
@@ -123,8 +122,8 @@ class ClashService extends ClashHandlerInterface {
socket.writeln(message); socket.writeln(message);
} }
_deleteSocketFile() async { Future<void> _deleteSocketFile() async {
if (!Platform.isWindows) { if (!system.isWindows) {
final file = File(unixSocketPath); final file = File(unixSocketPath);
if (await file.exists()) { if (await file.exists()) {
await file.delete(); await file.delete();
@@ -132,7 +131,7 @@ class ClashService extends ClashHandlerInterface {
} }
} }
_destroySocket() async { Future<void> _destroySocket() async {
if (socketCompleter.isCompleted) { if (socketCompleter.isCompleted) {
final lastSocket = await socketCompleter.future; final lastSocket = await socketCompleter.future;
await lastSocket.close(); await lastSocket.close();
@@ -142,7 +141,7 @@ class ClashService extends ClashHandlerInterface {
@override @override
shutdown() async { shutdown() async {
if (Platform.isWindows) { if (system.isWindows) {
await request.stopCoreByHelper(); await request.stopCoreByHelper();
} }
await _destroySocket(); await _destroySocket();

View File

@@ -1,14 +1,14 @@
import 'dart:io';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'system.dart';
class Android { class Android {
init() async { Future<void> init() async {
app?.onExit = () async { app?.onExit = () async {
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();
}; };
} }
} }
final android = Platform.isAndroid ? Android() : null; final android = system.isAndroid ? Android() : null;

View File

@@ -1,10 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:archive/archive_io.dart'; import 'package:archive/archive_io.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
extension ArchiveExt on Archive { extension ArchiveExt on Archive {
addDirectoryToArchive(String dirPath, String parentPath) { void addDirectoryToArchive(String dirPath, String parentPath) {
final dir = Directory(dirPath); final dir = Directory(dirPath);
final entities = dir.listSync(recursive: false); final entities = dir.listSync(recursive: false);
for (final entity in entities) { for (final entity in entities) {
@@ -19,7 +20,7 @@ extension ArchiveExt on Archive {
} }
} }
add<T>(String name, T raw) { void add<T>(String name, T raw) {
final data = json.encode(raw); final data = json.encode(raw);
addFile( addFile(
ArchiveFile(name, data.length, data), ArchiveFile(name, data.length, data),

View File

@@ -1,27 +1,96 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension ColorExtension on Color { extension ColorExtension on Color {
Color get toLight { Color get opacity80 {
return withOpacity(0.8); return withAlpha(204);
} }
Color get toLighter { Color get opacity60 {
return withOpacity(0.6); return withAlpha(153);
} }
Color get toSoft { Color get opacity50 {
return withOpacity(0.15); 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 opacity12 {
return withAlpha(31);
}
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(
@@ -54,7 +123,7 @@ extension ColorSchemeExtension on ColorScheme {
? copyWith( ? copyWith(
surface: Colors.black, surface: Colors.black,
surfaceContainer: surfaceContainer.darken( surfaceContainer: surfaceContainer.darken(
0.05, 5,
), ),
) )
: this; : this;

View File

@@ -3,7 +3,9 @@ export 'app_localizations.dart';
export 'color.dart'; export 'color.dart';
export 'constant.dart'; export 'constant.dart';
export 'context.dart'; export 'context.dart';
export 'converter.dart';
export 'datetime.dart'; export 'datetime.dart';
export 'fixed.dart';
export 'function.dart'; export 'function.dart';
export 'future.dart'; export 'future.dart';
export 'http.dart'; export 'http.dart';
@@ -12,28 +14,26 @@ export 'iterable.dart';
export 'keyboard.dart'; export 'keyboard.dart';
export 'launch.dart'; export 'launch.dart';
export 'link.dart'; export 'link.dart';
export 'list.dart';
export 'lock.dart'; export 'lock.dart';
export 'measure.dart'; export 'measure.dart';
export 'mixin.dart';
export 'navigation.dart'; 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 'package.dart'; export 'package.dart';
export 'path.dart'; export 'path.dart';
export 'picker.dart'; export 'picker.dart';
export 'preferences.dart'; export 'preferences.dart';
export 'print.dart';
export 'protocol.dart'; export 'protocol.dart';
export 'proxy.dart'; export 'proxy.dart';
export 'render.dart';
export 'request.dart'; export 'request.dart';
export 'scroll.dart'; export 'scroll.dart';
export 'string.dart'; export 'string.dart';
export 'system.dart'; export 'system.dart';
export 'text.dart'; export 'text.dart';
export 'tray.dart'; export 'tray.dart';
export 'utils.dart';
export 'window.dart'; export 'window.dart';
export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
@@ -8,53 +7,58 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart'; 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 = 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"; '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;
const helperTag = "2024125"; const maxTextScale = 1.4;
const baseInfoEdgeInsets = EdgeInsets.symmetric( const minTextScale = 0.8;
vertical: 16, final baseInfoEdgeInsets = EdgeInsets.symmetric(
horizontal: 16, vertical: 16.ap,
horizontal: 16.ap,
); );
final defaultTextScaleFactor =
WidgetsBinding.instance.platformDispatcher.textScaleFactor;
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);
const midDuration = Duration(milliseconds: 200);
const commonDuration = Duration(milliseconds: 300); const commonDuration = Duration(milliseconds: 300);
const defaultUpdateDuration = Duration(days: 1); const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb"; const mmdbFileName = 'geoip.metadb';
const asnFileName = "ASN.mmdb"; const asnFileName = 'ASN.mmdb';
const geoIpFileName = "GeoIP.dat"; const geoIpFileName = 'GeoIP.dat';
const geoSiteFileName = "GeoSite.dat"; const geoSiteFileName = 'GeoSite.dat';
final double kHeaderHeight = system.isDesktop final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS ? !system.isMacOS
? 40 ? 40
: 28 : 28
: 0; : 0;
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';
const configKey = "config"; const configKey = 'config';
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300; const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash"; 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 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,
); );
const navigationItemListEquality = ListEquality<NavigationItem>(); const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>(); const trackerInfoListEquality = ListEquality<TrackerInfo>();
const stringListEquality = ListEquality<String>(); const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>(); const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>(); const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>(); const externalProviderListEquality = ListEquality<ExternalProvider>();
@@ -73,12 +77,33 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3], ViewMode.desktop: [4, 3],
}; };
const defaultPrimaryColor = Colors.brown; const proxiesListStoreKey = PageStorageKey<String>('proxies_list');
const toolsStoreKey = PageStorageKey<String>('tools');
const profilesStoreKey = PageStorageKey<String>('profiles');
const defaultPrimaryColor = 0XFFD8C0C3;
double getWidgetHeight(num lines) { double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0); return max(lines * 84 + (lines - 1) * 16, 0).ap;
} }
final mainIsolate = "FlClashMainIsolate"; const maxLength = 1000;
final serviceIsolate = "FlClashServiceIsolate"; final mainIsolate = 'FlClashMainIsolate';
final serviceIsolate = 'FlClashServiceIsolate';
const defaultPrimaryColors = [
0xFF795548,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
defaultPrimaryColor,
0XFF665390,
];
const scriptTemplate = '''
const main = (config) => {
return config;
}''';

View File

@@ -1,4 +1,4 @@
import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/manager/message_manager.dart';
import 'package:fl_clash/widgets/scaffold.dart'; import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -7,10 +7,40 @@ extension BuildContextExtension on BuildContext {
return findAncestorStateOfType<CommonScaffoldState>(); return findAncestorStateOfType<CommonScaffoldState>();
} }
showNotifier(String text) { Future<void>? showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text); return findAncestorStateOfType<MessageManagerState>()?.message(text);
} }
void showSnackBar(
String message, {
SnackBarAction? action,
}) {
final width = viewWidth;
EdgeInsets margin;
if (width < 600) {
margin = const EdgeInsets.only(
bottom: 16,
right: 16,
left: 16,
);
} else {
margin = EdgeInsets.only(
bottom: 16,
left: 16,
right: width - 316,
);
}
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
action: action,
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(milliseconds: 1500),
margin: margin,
),
);
}
Size get appSize { Size get appSize {
return MediaQuery.of(this).size; return MediaQuery.of(this).size;
} }
@@ -27,10 +57,10 @@ extension BuildContextExtension on BuildContext {
T? state; T? state;
visitor(Element element) { visitor(Element element) {
if(!element.mounted){ if (!element.mounted) {
return; return;
} }
if(element is StatefulElement){ if (element is StatefulElement) {
if (element.state is T) { if (element.state is T) {
state = element.state as T; state = element.state as T;
} }
@@ -42,3 +72,18 @@ extension BuildContextExtension on BuildContext {
return state; return state;
} }
} }
class BackHandleInherited extends InheritedWidget {
final Function handleBack;
const BackHandleInherited(
{super.key, required this.handleBack, required super.child});
static BackHandleInherited? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<BackHandleInherited>();
@override
bool updateShouldNotify(BackHandleInherited oldWidget) {
return handleBack != oldWidget.handleBack;
}
}

32
lib/common/converter.dart Normal file
View File

@@ -0,0 +1,32 @@
import 'dart:convert';
import 'dart:typed_data';
class Uint8ListToListIntConverter extends Converter<Uint8List, List<int>> {
@override
List<int> convert(Uint8List input) {
return input.toList();
}
@override
Sink<Uint8List> startChunkedConversion(Sink<List<int>> sink) {
return _Uint8ListToListIntConverterSink(sink);
}
}
class _Uint8ListToListIntConverterSink implements Sink<Uint8List> {
const _Uint8ListToListIntConverterSink(this._target);
final Sink<List<int>> _target;
@override
void add(Uint8List data) {
_target.add(data.toList());
}
@override
void close() {
_target.close();
}
}
final uint8ListToListIntConverter = Uint8ListToListIntConverter();

View File

@@ -17,26 +17,34 @@ extension DateTimeExtension on DateTime {
final difference = currentDateTime.difference(this); final difference = currentDateTime.difference(this);
final days = difference.inDays; final days = difference.inDays;
if (days >= 365) { if (days >= 365) {
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}"; return '${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}';
} }
if (days >= 30) { if (days >= 30) {
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}"; return '${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}';
} }
if (days >= 1) { if (days >= 1) {
return "$days ${appLocalizations.days}${appLocalizations.ago}"; return '$days ${appLocalizations.days}${appLocalizations.ago}';
} }
final hours = difference.inHours; final hours = difference.inHours;
if (hours >= 1) { if (hours >= 1) {
return "$hours ${appLocalizations.hours}${appLocalizations.ago}"; return '$hours ${appLocalizations.hours}${appLocalizations.ago}';
} }
final minutes = difference.inMinutes; final minutes = difference.inMinutes;
if (minutes >= 1) { if (minutes >= 1) {
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}"; return '$minutes ${appLocalizations.minutes}${appLocalizations.ago}';
} }
return appLocalizations.just; return appLocalizations.just;
} }
String get show { String get show {
return toIso8601String().substring(0, 10); return toString().substring(0, 10);
}
String get showFull {
return toString().substring(0, 19);
}
String get showTime {
return toString().substring(10, 19);
} }
} }

View File

@@ -38,18 +38,18 @@ class DAVClient {
} }
} }
get root => "/$appName"; String get root => '/$appName';
get backupFile => "$root/$fileName"; String get backupFile => '$root/$fileName';
backup(Uint8List data) async { Future<bool> backup(Uint8List data) async {
await client.mkdir("$root"); await client.mkdir(root);
await client.write("$backupFile", data); await client.write(backupFile, data);
return true; return true;
} }
Future<List<int>> recovery() async { Future<List<int>> recovery() async {
await client.mkdir("$root"); await client.mkdir(root);
final data = await client.read(backupFile); final data = await client.read(backupFile);
return data; return data;
} }

81
lib/common/fixed.dart Normal file
View File

@@ -0,0 +1,81 @@
import 'iterable.dart';
typedef ValueCallback<T> = T Function();
class FixedList<T> {
final int maxLength;
final List<T> _list;
FixedList(this.maxLength, {List<T>? list})
: _list = (list ?? [])..truncate(maxLength);
void add(T item) {
_list.add(item);
_list.truncate(maxLength);
}
void 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 maxLength;
late Map<K, V> _map;
FixedMap(this.maxLength, {Map<K, V>? map}) {
_map = map ?? {};
}
V updateCacheValue(K key, ValueCallback<V> callback) {
final realValue = _map.updateCacheValue(
key,
callback,
);
_adjustMap();
return realValue;
}
void clear() {
_map.clear();
}
void updateMaxLength(int size) {
maxLength = size;
_adjustMap();
}
void updateMap(Map<K, V> map) {
_map = map;
_adjustMap();
}
void _adjustMap() {
if (_map.length > maxLength) {
_map = Map.fromEntries(
map.entries.toList()..truncate(maxLength),
);
}
}
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);
}

View File

@@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
class Debouncer { import 'package:fl_clash/enum/enum.dart';
final Map<dynamic, Timer> _operations = {};
call( class Debouncer {
dynamic tag, final Map<FunctionTag, Timer?> _operations = {};
void call(
FunctionTag tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
@@ -26,16 +28,17 @@ class Debouncer {
); );
} }
cancel(dynamic tag) { void cancel(dynamic tag) {
_operations[tag]?.cancel(); _operations[tag]?.cancel();
_operations[tag] = null;
} }
} }
class Throttler { class Throttler {
final Map<dynamic, Timer> _operations = {}; final Map<FunctionTag, Timer?> _operations = {};
call( bool call(
String tag, FunctionTag tag,
Function func, { Function func, {
List<dynamic>? args, List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600), Duration duration = const Duration(milliseconds: 600),
@@ -58,8 +61,9 @@ class Throttler {
return false; return false;
} }
cancel(dynamic tag) { void cancel(dynamic tag) {
_operations[tag]?.cancel(); _operations[tag]?.cancel();
_operations[tag] = null;
} }
} }
@@ -77,7 +81,7 @@ Future<T> retry<T>({
} }
attempts++; attempts++;
} }
throw "unknown error"; throw 'unknown error';
} }
final debouncer = Debouncer(); final debouncer = Debouncer();

View File

@@ -4,7 +4,7 @@ import 'dart:ui';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
extension CompleterExt<T> on Completer<T> { extension CompleterExt<T> on Completer<T> {
safeFuture({ Future<T> safeFuture({
Duration? timeout, Duration? timeout,
VoidCallback? onLast, VoidCallback? onLast,
FutureOr<T> Function()? onTimeout, FutureOr<T> Function()? onTimeout,

View File

@@ -6,18 +6,19 @@ import 'package:fl_clash/state.dart';
class FlClashHttpOverrides extends HttpOverrides { class FlClashHttpOverrides extends HttpOverrides {
static String handleFindProxy(Uri url) { static String handleFindProxy(Uri url) {
if ([localhost].contains(url.host)) { if ([localhost].contains(url.host)) {
return "DIRECT"; return 'DIRECT';
} }
final port = globalState.config.patchClashConfig.mixedPort; final port = globalState.config.patchClashConfig.mixedPort;
final isStart = globalState.appState.runTime != null; final isStart = globalState.appState.runTime != null;
commonPrint.log("find $url proxy:$isStart"); commonPrint.log('find $url proxy:$isStart');
if (!isStart) return "DIRECT"; if (!isStart) return 'DIRECT';
return "PROXY localhost:$port"; 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 = handleFindProxy;
return client; return client;
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class IconsExt{ class IconsExt {
static const IconData target = static const IconData target = IconData(0xe900, fontFamily: 'Icons');
IconData(0xe900, fontFamily: "Icons"); }
}

View File

@@ -38,6 +38,53 @@ extension IterableExt<T> on Iterable<T> {
count++; count++;
} }
} }
Iterable<T> takeLast({int count = 50}) {
if (count <= 0) return Iterable.empty();
return count >= length ? this : toList().skip(length - count);
}
}
extension ListExt<T> on List<T> {
void truncate(int maxLength) {
if (maxLength == 0) {
return;
}
if (length > maxLength) {
removeRange(0, length - maxLength);
}
}
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start, [int? end]) {
if (start <= 0) return this;
if (start > length) return [];
if (end != null) {
return sublist(start, end.clamp(start, length));
}
return sublist(start);
}
T safeGet(int index) {
if (length > index) return this[index];
return last;
}
} }
extension DoubleListExt on List<double> { extension DoubleListExt on List<double> {
@@ -65,3 +112,12 @@ extension DoubleListExt on List<double> {
return -1; return -1;
} }
} }
extension MapExt<K, V> on Map<K, V> {
V updateCacheValue(K key, V Function() callback) {
if (this[key] == null) {
this[key] = callback();
}
return this[key]!;
}
}

View File

@@ -1,8 +1,8 @@
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:uni_platform/uni_platform.dart'; import 'package:uni_platform/uni_platform.dart';
import 'system.dart';
final Map<PhysicalKeyboardKey, String> _knownKeyLabels = final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
<PhysicalKeyboardKey, String>{ <PhysicalKeyboardKey, String>{
PhysicalKeyboardKey.keyA: 'A', PhysicalKeyboardKey.keyA: 'A',
@@ -79,14 +79,14 @@ final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
PhysicalKeyboardKey.arrowLeft: '', PhysicalKeyboardKey.arrowLeft: '',
PhysicalKeyboardKey.arrowDown: '', PhysicalKeyboardKey.arrowDown: '',
PhysicalKeyboardKey.arrowUp: '', PhysicalKeyboardKey.arrowUp: '',
PhysicalKeyboardKey.controlLeft: "CTRL", PhysicalKeyboardKey.controlLeft: 'CTRL',
PhysicalKeyboardKey.shiftLeft: 'SHIFT', PhysicalKeyboardKey.shiftLeft: 'SHIFT',
PhysicalKeyboardKey.altLeft: "ALT", PhysicalKeyboardKey.altLeft: 'ALT',
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '' : 'WIN', PhysicalKeyboardKey.metaLeft: system.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.controlRight: "CTRL", PhysicalKeyboardKey.controlRight: 'CTRL',
PhysicalKeyboardKey.shiftRight: 'SHIFT', PhysicalKeyboardKey.shiftRight: 'SHIFT',
PhysicalKeyboardKey.altRight: "ALT", PhysicalKeyboardKey.altRight: 'ALT',
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '' : 'WIN', PhysicalKeyboardKey.metaRight: system.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.fn: 'FN', PhysicalKeyboardKey.fn: 'FN',
}; };
@@ -101,6 +101,3 @@ extension KeyboardKeyExt on KeyboardKey {
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown'; return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
} }
} }

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';
@@ -33,7 +34,10 @@ class AutoLaunch {
return await launchAtStartup.disable(); return await launchAtStartup.disable();
} }
updateStatus(bool isAutoLaunch) async { Future<void> 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

@@ -15,8 +15,9 @@ class LinkManager {
_appLinks = AppLinks(); _appLinks = AppLinks();
} }
initAppLinksListen(installConfigCallBack) async { Future<void> initAppLinksListen(
commonPrint.log("initAppLinksListen"); Function(String url) installConfigCallBack) async {
commonPrint.log('initAppLinksListen');
destroy(); destroy();
subscription = _appLinks.uriLinkStream.listen( subscription = _appLinks.uriLinkStream.listen(
(uri) { (uri) {
@@ -32,7 +33,7 @@ class LinkManager {
); );
} }
destroy() { void destroy() {
if (subscription != null) { if (subscription != null) {
subscription?.cancel(); subscription?.cancel();
subscription = null; subscription = null;

View File

@@ -1,87 +0,0 @@
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> {
final 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);
}
clear() {
_map.clear();
_queue.clear();
}
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> {
List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList();
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}

View File

@@ -3,15 +3,15 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; final TextScaler _textScaler;
final BuildContext context; final BuildContext context;
final String? _fontFamily; final Map<String, dynamic> _measureMap;
Measure.of(this.context, {String? fontFamily}) Measure.of(this.context, double textScaleFactor)
: _textScale = TextScaler.linear( : _measureMap = {},
WidgetsBinding.instance.platformDispatcher.textScaleFactor, _textScaler = TextScaler.linear(
), textScaleFactor,
_fontFamily = fontFamily ?? ""; );
Size computeTextSize( Size computeTextSize(
Text text, { Text text, {
@@ -20,12 +20,10 @@ class Measure {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan( text: TextSpan(
text: text.data, text: text.data,
style: text.style?.copyWith( style: text.style,
fontFamily: _fontFamily,
),
), ),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: _textScaler,
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout( )..layout(
maxWidth: maxWidth, maxWidth: maxWidth,
@@ -33,81 +31,87 @@ class Measure {
return textPainter.size; return textPainter.size;
} }
double? _bodyMediumHeight;
Size? _bodyLargeSize;
double? _bodySmallHeight;
double? _labelSmallHeight;
double? _labelMediumHeight;
double? _titleLargeHeight;
double? _titleMediumHeight;
double get bodyMediumHeight { double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'bodyMediumHeight',
"X", () => computeTextSize(
style: context.textTheme.bodyMedium, Text(
), 'X',
).height; style: context.textTheme.bodyMedium,
return _bodyMediumHeight!; ),
).height,
);
} }
Size get bodyLargeSize { double get bodyLargeHeight {
_bodyLargeSize ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'bodyLargeHeight',
"X", () => computeTextSize(
style: context.textTheme.bodyLarge, Text(
), 'X',
style: context.textTheme.bodyLarge,
),
).height,
); );
return _bodyLargeSize!;
} }
double get bodySmallHeight { double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'bodySmallHeight',
"X", () => computeTextSize(
style: context.textTheme.bodySmall, Text(
), 'X',
).height; style: context.textTheme.bodySmall,
return _bodySmallHeight!; ),
).height,
);
} }
double get labelSmallHeight { double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'labelSmallHeight',
"X", () => computeTextSize(
style: context.textTheme.labelSmall, Text(
), 'X',
).height; style: context.textTheme.labelSmall,
return _labelSmallHeight!; ),
).height,
);
} }
double get labelMediumHeight { double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'labelMediumHeight',
"X", () => computeTextSize(
style: context.textTheme.labelMedium, Text(
), 'X',
).height; style: context.textTheme.labelMedium,
return _labelMediumHeight!; ),
).height,
);
} }
double get titleLargeHeight { double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'titleLargeHeight',
"X", () => computeTextSize(
style: context.textTheme.titleLarge, Text(
), 'X',
).height; style: context.textTheme.titleLarge,
return _titleLargeHeight!; ),
).height,
);
} }
double get titleMediumHeight { double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize( return _measureMap.updateCacheValue(
Text( 'titleMediumHeight',
"X", () => computeTextSize(
style: context.textTheme.titleMedium, Text(
), 'X',
).height; style: context.textTheme.titleMedium,
return _titleMediumHeight!; ),
).height,
);
} }
} }

View File

@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:riverpod/riverpod.dart'; import 'package:riverpod/riverpod.dart';
import 'context.dart';
mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> { mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
set value(T value) { set value(T value) {
@@ -16,31 +14,31 @@ mixin AutoDisposeNotifierMixin<T> on AutoDisposeNotifier<T> {
return res; return res;
} }
onUpdate(T value) {} void onUpdate(T value) {}
} }
mixin PageMixin<T extends StatefulWidget> on State<T> { // mixin PageMixin<T extends StatefulWidget> on State<T> {
void onPageShow() { // initPageState() {
initPageState(); // WidgetsBinding.instance.addPostFrameCallback((_) {
} // final commonScaffoldState = context.commonScaffoldState;
// commonScaffoldState?.actions = actions;
initPageState() { // commonScaffoldState?.floatingActionButton = floatingActionButton;
WidgetsBinding.instance.addPostFrameCallback((_) { // commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate;
final commonScaffoldState = context.commonScaffoldState; // commonScaffoldState?.updateSearchState(
commonScaffoldState?.actions = actions; // (_) => onSearch != null
commonScaffoldState?.floatingActionButton = floatingActionButton; // ? AppBarSearchState(
commonScaffoldState?.onSearch = onSearch; // onSearch: onSearch!,
commonScaffoldState?.onKeywordsUpdate = onKeywordsUpdate; // )
}); // : null,
} // );
// });
void onPageHidden() {} // }
//
List<Widget> get actions => []; // List<Widget> get actions => [];
//
Widget? get floatingActionButton => null; // Widget? get floatingActionButton => null;
//
Function(String)? get onSearch => null; // Function(String)? get onSearch => null;
//
Function(List<String>)? get onKeywordsUpdate => null; // Function(List<String>)? get onKeywordsUpdate => null;
} // }

View File

@@ -1,7 +1,9 @@
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/fragments/fragments.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/views/views.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Navigation { class Navigation {
static Navigation? _instance; static Navigation? _instance;
@@ -11,63 +13,69 @@ class Navigation {
bool hasProxies = false, bool hasProxies = false,
}) { }) {
return [ return [
const NavigationItem( NavigationItem(
keep: false,
icon: Icon(Icons.space_dashboard), icon: Icon(Icons.space_dashboard),
label: PageLabel.dashboard, label: PageLabel.dashboard,
keep: false, builder: (_) => const DashboardView(
fragment: DashboardFragment(
key: GlobalObjectKey(PageLabel.dashboard), key: GlobalObjectKey(PageLabel.dashboard),
), ),
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.article), icon: const Icon(Icons.article),
label: PageLabel.proxies, label: PageLabel.proxies,
fragment: const ProxiesFragment( builder: (_) => ProviderScope(
key: GlobalObjectKey( overrides: [
PageLabel.proxies, queryProvider.overrideWith(
() => Query(),
),
],
child: const ProxiesView(
key: GlobalObjectKey(
PageLabel.proxies,
),
), ),
), ),
modes: hasProxies modes: hasProxies
? [NavigationItemMode.mobile, NavigationItemMode.desktop] ? [NavigationItemMode.mobile, NavigationItemMode.desktop]
: [], : [],
), ),
const NavigationItem( NavigationItem(
icon: Icon(Icons.folder), icon: Icon(Icons.folder),
label: PageLabel.profiles, label: PageLabel.profiles,
fragment: ProfilesFragment( builder: (_) => const ProfilesView(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.profiles, PageLabel.profiles,
), ),
), ),
), ),
const NavigationItem( NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: PageLabel.requests, label: PageLabel.requests,
fragment: RequestsFragment( builder: (_) => const RequestsView(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.requests, PageLabel.requests,
), ),
), ),
description: "requestsDesc", description: 'requestsDesc',
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.ballot),
label: PageLabel.connections, label: PageLabel.connections,
fragment: ConnectionsFragment( builder: (_) => const ConnectionsView(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.connections, PageLabel.connections,
), ),
), ),
description: "connectionsDesc", description: 'connectionsDesc',
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( NavigationItem(
icon: Icon(Icons.storage), icon: Icon(Icons.storage),
label: PageLabel.resources, label: PageLabel.resources,
description: "resourcesDesc", description: 'resourcesDesc',
keep: false, builder: (_) => const ResourcesView(
fragment: Resources(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.resources, PageLabel.resources,
), ),
@@ -77,20 +85,20 @@ class Navigation {
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),
label: PageLabel.logs, label: PageLabel.logs,
fragment: const LogsFragment( builder: (_) => const LogsView(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.logs, PageLabel.logs,
), ),
), ),
description: "logsDesc", description: 'logsDesc',
modes: openLogs modes: openLogs
? [NavigationItemMode.desktop, NavigationItemMode.more] ? [NavigationItemMode.desktop, NavigationItemMode.more]
: [], : [],
), ),
const NavigationItem( NavigationItem(
icon: Icon(Icons.construction), icon: Icon(Icons.construction),
label: PageLabel.tools, label: PageLabel.tools,
fragment: ToolsFragment( builder: (_) => const ToolsView(
key: GlobalObjectKey( key: GlobalObjectKey(
PageLabel.tools, PageLabel.tools,
), ),

View File

@@ -19,6 +19,21 @@ class BaseNavigator {
), ),
); );
} }
// static Future<T?> modal<T>(BuildContext context, Widget child) async {
// if (globalState.appState.viewMode != ViewMode.mobile) {
// return await globalState.showCommonDialog<T>(
// child: CommonModal(
// child: child,
// ),
// );
// }
// return await Navigator.of(context).push<T>(
// CommonRoute(
// builder: (context) => child,
// ),
// );
// }
} }
class CommonDesktopRoute<T> extends PageRoute<T> { class CommonDesktopRoute<T> extends PageRoute<T> {
@@ -70,7 +85,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: 250); Duration get reverseTransitionDuration => const Duration(milliseconds: 500);
} }
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
@@ -194,7 +209,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,
@@ -218,9 +233,7 @@ 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.withValues(alpha: 0.02),
0.06,
),
Colors.transparent, Colors.transparent,
], ],
), ),
@@ -274,7 +287,7 @@ class _CommonEdgeShadowPainter extends BoxPainter {
return; return;
} }
final double shadowWidth = 0.03 * 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

@@ -1,8 +1,9 @@
import 'package:fl_clash/state.dart';
import 'package:flutter/foundation.dart'; 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({decimals = 2}) { String fixed({int decimals = 2}) {
String formatted = toStringAsFixed(decimals); String formatted = toStringAsFixed(decimals);
if (formatted.contains('.')) { if (formatted.contains('.')) {
formatted = formatted.replaceAll(RegExp(r'0*$'), ''); formatted = formatted.replaceAll(RegExp(r'0*$'), '');
@@ -12,10 +13,14 @@ extension NumExt on num {
} }
return formatted; return formatted;
} }
double get ap {
return this * (1 + (globalState.theme.textScaleFactor - 1) * 0.5);
}
} }
extension DoubleExt on double { extension DoubleExt on double {
moreOrEqual(double value) { bool moreOrEqual(double value) {
return this > value || (value - this).abs() < precisionErrorTolerance + 1; return this > value || (value - this).abs() < precisionErrorTolerance + 1;
} }
} }
@@ -41,7 +46,7 @@ extension OffsetExt on Offset {
} }
extension RectExt on Rect { extension RectExt on Rect {
doRectIntersect(Rect rect) { bool doRectIntersect(Rect rect) {
return left < rect.right && return left < rect.right &&
right > rect.left && right > rect.left &&
top < rect.bottom && top < rect.bottom &&

View File

@@ -6,8 +6,8 @@ import 'common.dart';
extension PackageInfoExtension on PackageInfo { extension PackageInfoExtension on PackageInfo {
String get ua => [ String get ua => [
"$appName/v$version", '$appName/v$version',
"clash-verge", 'clash-verge',
"Platform/${Platform.operatingSystem}", 'Platform/${Platform.operatingSystem}',
].join(" "); ].join(' ');
} }

View File

@@ -1,11 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'constant.dart';
class AppPath { class AppPath {
static AppPath? _instance; static AppPath? _instance;
Completer<Directory> dataDir = Completer(); Completer<Directory> dataDir = Completer();
@@ -32,7 +31,7 @@ class AppPath {
} }
String get executableExtension { String get executableExtension {
return Platform.isWindows ? ".exe" : ""; return system.isWindows ? '.exe' : '';
} }
String get executableDirPath { String get executableDirPath {
@@ -41,11 +40,11 @@ class AppPath {
} }
String get corePath { String get corePath {
return join(executableDirPath, "FlClashCore$executableExtension"); return join(executableDirPath, 'FlClashCore$executableExtension');
} }
String get helperPath { String get helperPath {
return join(executableDirPath, "$appHelperService$executableExtension"); return join(executableDirPath, '$appHelperService$executableExtension');
} }
Future<String> get downloadDirPath async { Future<String> get downloadDirPath async {
@@ -60,12 +59,12 @@ class AppPath {
Future<String> get lockFilePath 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> get sharedPreferencesPath async { Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future; final directory = await dataDir.future;
return join(directory.path, "shared_preferences.json"); return join(directory.path, 'shared_preferences.json');
} }
Future<String> get profilesPath async { Future<String> get profilesPath async {
@@ -73,16 +72,33 @@ class AppPath {
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;
final directory = await profilesPath; final directory = await profilesPath;
return join(directory, "$id.yaml"); return join(directory, '$id.yaml');
} }
Future<String?> getProvidersPath(String? id) async { Future<String> getProvidersDirPath(String id) async {
if (id == null) return null;
final directory = await profilesPath; final directory = await profilesPath;
return join(directory, "providers", id); return join(
directory,
'providers',
id,
);
}
Future<String> getProvidersFilePath(
String id,
String type,
String url,
) async {
final directory = await profilesPath;
return join(
directory,
'providers',
id,
type,
url.toMd5(),
);
} }
Future<String> get tempPath async { Future<String> get tempPath async {

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