Compare commits

..

37 Commits

Author SHA1 Message Date
chen08209
72bef5f672 Add android shortcuts
Fix init params issues

Fix dynamic color issues

Optimize navigator animate

Optimize window init

Optimize fab

Optimize save
2024-11-09 17:42:08 +08:00
chen08209
526ccdf3ad Fix the collapse issues
Add fontFamily options
2024-10-26 16:52:10 +08:00
chen08209
8282a9a474 Update core version
Update flutter version

Optimize ip check

Optimize url-test
2024-10-26 01:46:55 +08:00
chen08209
dfa6d31673 Update release message
Init auto gen changelog
2024-10-12 17:53:40 +08:00
chen08209
89bbbc6864 Fix windows tray issues
Fix urltest issues

Add auto changelog
2024-10-12 15:06:55 +08:00
chen08209
a3e1b38201 Fix windows admin auto launch issues
Add android vpn options

Support proxies icon configuration

Optimize android immersion display

Fix some issues
2024-10-12 15:06:55 +08:00
chen08209
4e3dc45f13 Optimize ip detection
Support android vpn ipv6 inbound switch

Support log export

Optimize more details
2024-10-12 15:06:55 +08:00
chen08209
13d31cf708 Fix android system dns issues
Optimize dns default option

Fix some issues
2024-10-12 15:06:54 +08:00
chen08209
62a7772b92 Update readme 2024-10-12 15:06:54 +08:00
chen08209
043648f998 Fix build error2 2024-09-17 23:06:32 +08:00
chen08209
3eb26e8061 Fix build error 2024-09-17 22:42:10 +08:00
chen08209
5d6bd6466f Support desktop hotkey
Support android ipv6 inbound

Support android system dns

fix some bugs
2024-09-17 21:42:13 +08:00
chen08209
4e766d9407 Fix delete profile error 2024-09-09 09:49:22 +08:00
chen08209
80f8aa22ee Fix submit error 2 2024-09-08 21:38:58 +08:00
chen08209
97714e8b25 Fix submit error 2024-09-08 21:22:02 +08:00
chen08209
50bf4170d9 Optimize DNS strategy
Fix the problem that the tray is not displayed in some cases

Optimize tray

Update core

Fix some error
2024-09-08 20:58:02 +08:00
chen08209
79efa67df3 Fix tun update issues 2024-09-02 16:42:47 +08:00
chen08209
ac397393a0 Add DNS override
Fixed some bugs
Optimize more detail
2024-09-02 16:09:51 +08:00
chen08209
b685165230 Add Hosts override 2024-09-02 16:09:27 +08:00
chen08209
402221aaa2 fix android tip error
fix windows auto launch error
2024-08-26 20:35:38 +08:00
chen08209
f6d9ed11d9 Fix windows tray issues
Optimize windows logic
2024-08-25 23:40:13 +08:00
chen08209
c38a671d57 Optimize app logic
Support windows administrator auto launch

Support android close vpn
2024-08-22 19:56:19 +08:00
chen08209
75af47aead Change flutter version 2024-08-15 14:34:02 +08:00
chen08209
8dafe3b0ec Support profiles sort
Support windows country flags display

Optimize proxies page and profiles page columns
2024-08-15 14:18:33 +08:00
chen08209
813198a21d Update flutter version 2024-08-11 17:45:57 +08:00
chen08209
68dd262fef Update version 2024-08-11 17:09:31 +08:00
chen08209
5ef020db73 Update timeout time 2024-08-11 17:08:51 +08:00
chen08209
e3c9035903 Update access control page
Fix bug
2024-08-11 17:08:46 +08:00
chen08209
7fc54c5295 Optimize provider page
Optimize delay test

Support local backup and recovery
2024-08-05 18:17:05 +08:00
chen08209
00a78b5fb4 Fix android tile service issues 2024-08-01 23:51:28 +08:00
chen08209
8cdaf30de0 Fix linux core build error 2024-07-31 21:24:31 +08:00
chen08209
f39b9cf933 Add proxy-only traffic statistics
Update core

Optimize more details
2024-07-31 21:05:16 +08:00
chen08209
9df1ff46c2 Merge pull request #140 from txyyh/main
添加自建 F-Droid 仓库相关 workflow
2024-07-29 16:48:04 +08:00
txyyh
fcbbbdc698 Rename readme fingerprint 2024-07-29 16:45:12 +08:00
txyyh
3ba8355772 Rename workflow deploy repo name 2024-07-29 16:42:25 +08:00
txyyh
f6b97f82ae Add download guide to README 2024-07-29 16:39:52 +08:00
txyyh
13ac20f273 Add push release files to fdroid-repo 2024-07-29 16:39:52 +08:00
333 changed files with 24849 additions and 49133 deletions

View File

@@ -1,57 +0,0 @@
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

View File

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

View File

@@ -1,42 +0,0 @@
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

@@ -31,10 +31,9 @@
</td>
</tr>
<tr>
<td>macOS</td>
<td>macOS (v10.15+)</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-arm64.dmg"><img src="https://img.shields.io/badge/DMG-Apple%20Silicon-%23000000.svg?logo=apple"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Intel%20X64-%2300A9E0.svg?logo=apple"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Universal-ea005e.svg?logo=apple"></a><br>
</td>
</tr>
<tr>

View File

@@ -4,8 +4,6 @@ on:
push:
tags:
- 'v*'
env:
IS_STABLE: ${{ !contains(github.ref, '-') }}
jobs:
build:
@@ -27,27 +25,48 @@ jobs:
- platform: macos
os: macos-latest
arch: arm64
- platform: windows
os: windows-11-arm
arch: arm64
- platform: linux
os: ubuntu-24.04-arm
arch: arm64
steps:
- name: Setup rust
if: startsWith(matrix.os, 'windows-11-arm')
- name: Setup Mingw64
if: startsWith(matrix.platform,'windows')
uses: msys2/setup-msys2@v2
with:
msystem: mingw64
install: mingw-w64-x86_64-gcc
update: true
- name: Set Mingw64 Env
if: startsWith(matrix.platform,'windows')
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
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Check Matrix
run: |
echo "Running on ${{ matrix.os }}"
echo "Arch: ${{ runner.arch }}"
gcc --version
- name: Checkout
uses: actions/checkout@v4
with:
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
if: startsWith(matrix.platform,'android')
run: |
@@ -56,24 +75,26 @@ jobs:
echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> android/local.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/local.properties
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24.0'
go-version: 'stable'
cache-dependency-path: |
core/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: ${{ (startsWith(matrix.os, 'windows-11-arm') || startsWith(matrix.os, 'ubuntu-24.04-arm')) && 'master' || 'stable' }}
flutter-version: '3.x'
channel: 'stable'
cache: true
- name: Get Flutter Dependency
run: flutter pub get
- name: Setup
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }}
- name: Upload
uses: actions/upload-artifact@v4
@@ -82,68 +103,6 @@ jobs:
path: ./dist
overwrite: true
changelog:
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Checkout
uses: actions/checkout@v4
if: ${{ env.IS_STABLE == 'true' }}
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ env.IS_STABLE == 'true' }}
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
if: ${{ env.IS_STABLE == 'true' }}
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
upload:
permissions: write-all
@@ -201,41 +160,32 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }}
RUN_ID: ${{ github.run_id }}
run: |
python -m pip install --upgrade pip
pip install requests
python release_telegram.py
python release.py
- name: Patch release.md
run: |
version=$(echo "${{ github.ref_name }}" | sed 's/^v//')
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
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
if: ${{ env.IS_STABLE == 'true' }}
if: ${{ !contains(github.ref, '+') }}
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
@@ -245,7 +195,6 @@ jobs:
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: main
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/
target-directory: /tmp/

45
.github/workflows/change.yaml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: change
on:
push:
branches:
- 'main'
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate
run: |
tags=$(git tag --sort=creatordate)
previous=""
if [ ! -f CHANGELOG.md ]; then
echo "" > CHANGELOG.md
else
previous=$(grep -oP '^## \K.*' CHANGELOG.md | tail -n 1)
fi
for tag in $tags; do
if [ -n "$previous" ]; then
echo "## $tag" >> CHANGELOG.md
git log --pretty=format:"* %s (%h)" "$previous..$tag" >> CHANGELOG.md
echo -e "\n" >> CHANGELOG.md
fi
previous=$tag
done
- name: Commit
run: |
if git diff --cached --quiet; then
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add CHANGELOG.md
git commit -m "Update Changelog"
git push
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -5,11 +5,9 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related

4
.gitmodules vendored
View File

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

View File

@@ -1,220 +1,4 @@
## 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
- Fix tab delay view issues
- Fix tray action issues
- Fix get profile redirect client ua issues
- Fix proxy card delay view issues
- Add Russian, Japanese adaptation
- Fix some issues
- Update changelog
## v0.8.78
- Fix list form input view issues
- Fix traffic view issues
- Update changelog
## v0.8.77
- Optimize performance
- Update core
- Optimize core stability
- Fix linux tun authority check error
- Fix some issues
- Fix scroll physics error
- Update changelog
## v0.8.75
- Add windows storage corruption detection
- Fix core crash caused by windows resource manager restart
- Optimize logs, requests, access to pages
- Fix macos bypass domain issues
- Update changelog
## v0.8.74
- Fix some issues
- Update changelog
## v0.8.73
- Update popup menu
- Add file editor
- Fix android service issues
- Optimize desktop background performance
- Optimize android main process performance
- Optimize delay test
- Optimize vpn protect
- Update changelog
## v0.8.72
- Update core
- Fix some issues
- Update changelog
## v0.8.71
- Remake dashboard
- Optimize theme
- Optimize more details
- Update flutter version
- Update changelog
## v0.8.70
- Support better window position memory
- Add windows arm64 and linux arm64 build script
- Optimize some details
## v0.8.69
- Remake desktop
- Optimize change proxy
- Optimize network check
- Fix fallback issues
- Optimize lots of details
- Update change.yaml
- Fix android tile issues
- Fix windows tray issues
- Support setting bypassDomain
- Update flutter version
- Fix android service issues
- Fix macos dock exit button issues
- Add route address setting
- Optimize provider view
- Update changelog
- Update CHANGELOG.md
## v0.8.67
- Add android shortcuts
- Fix init params issues
- Fix dynamic color issues
- Optimize navigator animate
- Optimize window init
- Optimize fab
- Optimize save
## v0.8.66
- Fix the collapse issues
- Add fontFamily options
## v0.8.65
- Update core version
- Update flutter version
- Optimize ip check
- Optimize url-test
## v0.8.64
- Update release message
- Init auto gen changelog
- Fix windows tray issues
- Fix urltest issues
- Add auto changelog
## v0.8.63
- Fix windows admin auto launch issues
@@ -226,6 +10,8 @@
- Fix some issues
## v0.8.62
- Optimize ip detection
- Support android vpn ipv6 inbound switch
@@ -242,6 +28,12 @@
- Update readme
- Update README.md 2
- Update README.md 2
- Update README.md
## v0.8.60
- Fix build error2
@@ -669,8 +461,7 @@
## v0.8.12
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the
application to flash back.
- Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
- Fix edit profile error
@@ -835,4 +626,5 @@
- update mobile_scanner
- Initial commit
- Initial commit

View File

@@ -1,10 +0,0 @@
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

@@ -34,29 +34,6 @@ on Mobile:
✨ Support subscription link, Dark mode
## Use
### Linux
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
Support the following actions
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -93,7 +70,7 @@ Support the following actions
3. Run build script
```bash
dart .\setup.dart windows --arch <arm64 | amd64>
dart .\setup.dart
```
- linux
@@ -103,7 +80,7 @@ Support the following actions
2. Run build script
```bash
dart .\setup.dart linux --arch <arm64 | amd64>
dart .\setup.dart
```
- macOS
@@ -113,7 +90,7 @@ Support the following actions
2. Run build script
```bash
dart .\setup.dart macos --arch <arm64 | amd64>
dart .\setup.dart
```
## Star

View File

@@ -10,6 +10,7 @@
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端简单易用开源无广告。
on Desktop:
@@ -34,29 +35,6 @@ on Mobile:
✨ 支持一键导入订阅, 深色模式
## Use
### Linux
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
支持下列操作
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -93,7 +71,7 @@ on Mobile:
3. 运行构建脚本
```bash
dart .\setup.dart windows --arch <arm64 | amd64>
dart .\setup.dart
```
- linux
@@ -103,7 +81,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart linux --arch <arm64 | amd64>
dart .\setup.dart
```
- macOS
@@ -113,7 +91,7 @@ on Mobile:
2. 运行构建脚本
```bash
dart .\setup.dart macos --arch <arm64 | amd64>
dart .\setup.dart
```
## Star History

View File

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

View File

@@ -1,3 +1,5 @@
import com.android.build.gradle.tasks.MergeSourceSetFolders
plugins {
id "com.android.application"
id "kotlin-android"
@@ -31,8 +33,8 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android {
namespace "com.follow.clash"
compileSdk 35
ndkVersion = "28.0.13004108"
compileSdkVersion 34
ndkVersion "27.1.12297006"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -46,7 +48,6 @@ android {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
signingConfigs {
if (isRelease) {
release {
@@ -62,7 +63,7 @@ android {
defaultConfig {
applicationId "com.follow.clash"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
@@ -83,15 +84,31 @@ android {
}
}
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 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") {
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

@@ -1,17 +1,13 @@
<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 Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<application
android:icon="@mipmap/ic_launcher"
android:label="FlClash Debug"
tools:replace="android:label">
<application android:label="FlClash Debug" tools:replace="android:label">
<service
android:name=".services.FlClashTileService"
android:label="FlClash Debug"
tools:replace="android:label"
tools:targetApi="24" />
android:name=".services.FlClashTileService"
android:label="FlClash Debug"
tools:replace="android:label">
</service>
</application>
</manifest>

View File

@@ -10,18 +10,20 @@
<uses-permission android:name="android.permission.INTERNET" />
<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.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".FlClashApplication"
android:name="${applicationName}"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash">
@@ -44,7 +46,6 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
@@ -63,18 +64,14 @@
</intent-filter>
</activity>
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />-->
<activity
android:name=".TempActivity"
android:exported="true"
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.START" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.STOP" />
@@ -88,10 +85,10 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
@@ -125,7 +122,7 @@
<service
android:name=".services.FlClashVpnService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:foregroundServiceType="specialUse"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -138,7 +135,7 @@
<service
android:name=".services.FlClashService"
android:exported="false"
android:foregroundServiceType="dataSync">
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
package com.follow.clash
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector
@@ -18,12 +20,10 @@ enum class RunState {
object GlobalState {
private val lock = ReentrantLock()
val runLock = ReentrantLock()
const val NOTIFICATION_CHANNEL = "FlClash"
const val NOTIFICATION_ID = 1
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
var flutterEngine: FlutterEngine? = null
private var serviceEngine: FlutterEngine? = null
@@ -33,7 +33,7 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}
suspend fun getText(text: String): String {
fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: ""
}
@@ -46,64 +46,47 @@ object GlobalState {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}
fun handleToggle() {
val starting = handleStart()
if (!starting) {
handleStop()
}
}
fun handleStart(): Boolean {
fun handleToggle(context: Context) {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
runLock.lock()
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine()
initServiceEngine(context)
}
return true
} else {
handleStop()
}
return false
}
fun handleStop() {
if (runState.value == RunState.START) {
runState.value = RunState.PENDING
runLock.lock()
getCurrentTilePlugin()?.handleStop()
}
}
fun handleTryDestroy() {
if (flutterEngine == null) {
destroyServiceEngine()
}
}
fun destroyServiceEngine() {
runLock.withLock {
serviceEngine?.destroy()
serviceEngine = null
}
serviceEngine?.destroy()
serviceEngine = null
}
fun initServiceEngine() {
fun initServiceEngine(context: Context) {
if (serviceEngine != null) return
destroyServiceEngine()
runLock.withLock {
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
serviceEngine?.plugins?.add(VpnPlugin)
lock.withLock {
destroyServiceEngine()
serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"_service"
"vpnService"
)
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
if (flutterEngine == null) listOf("quick") else null
)
}
}

View File

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

View File

@@ -8,16 +8,12 @@ class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
wrapAction("START") -> {
GlobalState.handleStart()
}
wrapAction("STOP") -> {
GlobalState.handleStop()
}
wrapAction("CHANGE") -> {
GlobalState.handleToggle()
GlobalState.handleToggle(applicationContext)
}
}
finishAndRemoveTask()

View File

@@ -15,7 +15,6 @@ import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.TempActivity
import com.follow.clash.models.CIDR
import com.follow.clash.models.Metadata
import com.follow.clash.models.VpnOptions
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -23,10 +22,10 @@ import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Drawable.getBase64(): String {
val drawable = this
return withContext(Dispatchers.IO) {
@@ -43,40 +42,6 @@ fun Metadata.getProtocol(): Int? {
return null
}
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv4()
}.map {
it.toCIDR()
}
}
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
return routeAddress.filter {
it.isIpv6()
}.map {
it.toCIDR()
}
}
fun String.isIpv4(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 4
}
fun String.isIpv6(): Boolean {
val parts = split("/")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid CIDR format")
}
val address = InetAddress.getByName(parts[0])
return address.address.size == 16
}
fun String.toCIDR(): CIDR {
val parts = split("/")
if (parts.size != 2) {
@@ -96,6 +61,7 @@ fun String.toCIDR(): CIDR {
return CIDR(address, prefixLength)
}
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) }
@@ -113,7 +79,7 @@ fun InetAddress.asSocketAddressText(port: Int): String {
}
}
fun Context.wrapAction(action: String): String {
fun Context.wrapAction(action: String):String{
return "${this.packageName}.action.$action"
}
@@ -141,6 +107,7 @@ fun Context.getActionPendingIntent(action: String): PendingIntent {
}
}
private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
for (i in 0 until 8) {
@@ -177,19 +144,4 @@ suspend fun <T> MethodChannel.awaitResult(
}
})
}
}
fun ReentrantLock.safeLock() {
if (this.isLocked) {
return
}
this.lock()
}
fun ReentrantLock.safeUnlock() {
if (!this.isLocked) {
return
}
this.unlock()
}

View File

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

View File

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

View File

@@ -3,11 +3,11 @@ package com.follow.clash.models
import java.net.InetAddress
enum class AccessControlMode {
acceptSelected, rejectSelected,
acceptSelected,
rejectSelected,
}
data class AccessControl(
val enable: Boolean,
val mode: AccessControlMode,
val acceptList: List<String>,
val rejectList: List<String>,
@@ -18,17 +18,11 @@ data class CIDR(val address: InetAddress, val prefixLength: Int)
data class VpnOptions(
val enable: Boolean,
val port: Int,
val accessControl: AccessControl,
val accessControl: AccessControl?,
val allowBypass: Boolean,
val systemProxy: Boolean,
val bypassDomain: List<String>,
val routeAddress: List<String>,
val ipv4Address: String,
val ipv6Address: String,
val dnsServerAddress: String,
)
data class StartForegroundParams(
val title: String,
val content: String,
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,98 +0,0 @@
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 io.flutter.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
interface BaseServiceInterface {
fun start(options: VpnOptions): Int
fun stop()
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_stat_name)
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) {
Log.d("[FlClash]","createNotificationChannel===>")
channel = NotificationChannel(
GlobalState.NOTIFICATION_CHANNEL, "FlClash", 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,51 +1,26 @@
package com.follow.clash.services
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.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions
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()
inner class LocalBinder : Binder() {
@@ -60,8 +35,75 @@ class FlClashService : Service(), BaseServiceInterface {
return super.onUnbind(intent)
}
override fun onDestroy() {
stop()
super.onDestroy()
private val CHANNEL = "FlClash"
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, 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,
GlobalState.getText("stop"),
getActionPendingIntent("CHANGE")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
}
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 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 =
notificationBuilder.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

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

View File

@@ -1,8 +1,13 @@
package com.follow.clash.services
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.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.net.Network
import android.net.ProxyInfo
import android.net.VpnService
import android.os.Binder
@@ -10,23 +15,26 @@ import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.MainActivity
import com.follow.clash.R
import com.follow.clash.TempActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onCreate() {
super.onCreate()
GlobalState.initServiceEngine()
GlobalState.initServiceEngine(applicationContext)
}
override fun start(options: VpnOptions): Int {
@@ -34,75 +42,26 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
if (options.ipv4Address.isNotEmpty()) {
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
Log.d(
"addAddress",
"address: ${cidr.address} prefixLength:${cidr.prefixLength}"
)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
try {
routeAddress.forEach { i ->
Log.d(
"addRoute4",
"address: ${i.address} prefixLength:${i.prefixLength}"
)
addRoute(i.address, i.prefixLength)
}
} catch (_: Exception) {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
} else {
addRoute("0.0.0.0", 0)
}
try {
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
Log.d(
"addAddress6",
"address: ${cidr.address} prefixLength:${cidr.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)
}
}
}catch (_:Exception){
Log.d(
"addAddress6",
"IPv6 is not supported."
)
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
addRoute("::", 0)
}
addDnsServer(options.dnsServerAddress)
setMtu(9000)
options.accessControl.let { accessControl ->
if (accessControl.enable) {
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
options.accessControl?.let { accessControl ->
when (accessControl.mode) {
AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach {
addAllowedApplication(it)
}
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
AccessControlMode.rejectSelected -> {
(accessControl.rejectList - packageName).forEach {
addDisallowedApplication(it)
}
}
}
@@ -129,6 +88,12 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
}
fun updateUnderlyingNetworks(networks: Array<Network>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
this.setUnderlyingNetworks(networks)
}
}
override fun stop() {
stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -136,22 +101,73 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
}
}
private var cachedBuilder: NotificationCompat.Builder? = null
private val CHANNEL = "FlClash"
private suspend fun notificationBuilder(): NotificationCompat.Builder {
if (cachedBuilder == null) {
cachedBuilder = createFlClashNotificationBuilder().await()
private val notificationId: Int = 1
private val notificationBuilder: NotificationCompat.Builder by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
with(NotificationCompat.Builder(this, 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,
GlobalState.getText("stop"),
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
}
return cachedBuilder!!
}
@SuppressLint("ForegroundServiceType")
override suspend fun startForeground(title: String, content: String) {
startForeground(
notificationBuilder()
.setContentTitle(title)
.setContentText(content).build()
)
@SuppressLint("ForegroundServiceType", "WrongConstant")
override fun startForeground(title: String, content: String) {
CoroutineScope(Dispatchers.Default).launch {
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 =
notificationBuilder
.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)
}
}
}
override fun onTrimMemory(level: Int) {

View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
# 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,50 +0,0 @@
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

@@ -1,9 +0,0 @@
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.enableJetifier=true
kotlin_version=1.9.22
agp_version=8.9.1
agp_version=8.2.1

View File

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

View File

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

View File

@@ -1,400 +0,0 @@
{
"rule": "ルール",
"global": "グローバル",
"direct": "ダイレクト",
"dashboard": "ダッシュボード",
"proxies": "プロキシ",
"profile": "プロファイル",
"profiles": "プロファイル一覧",
"tools": "ツール",
"logs": "ログ",
"logsDesc": "ログキャプチャ記録",
"resources": "リソース",
"resourcesDesc": "外部リソース関連情報",
"trafficUsage": "トラフィック使用量",
"coreInfo": "コア情報",
"nullCoreInfoDesc": "コア情報を取得できません",
"networkSpeed": "ネットワーク速度",
"outboundMode": "アウトバウンドモード",
"networkDetection": "ネットワーク検出",
"upload": "アップロード",
"download": "ダウンロード",
"noProxy": "プロキシなし",
"noProxyDesc": "プロファイルを作成するか、有効なプロファイルを追加してください",
"nullProfileDesc": "プロファイルがありません。追加してください",
"nullLogsDesc": "ログがありません",
"settings": "設定",
"language": "言語",
"defaultText": "デフォルト",
"more": "詳細",
"other": "その他",
"about": "について",
"en": "英語",
"ja": "日本語",
"ru": "ロシア語",
"zh_CN": "簡体字中国語",
"theme": "テーマ",
"themeDesc": "ダークモードの設定、色の調整",
"override": "上書き",
"overrideDesc": "プロキシ関連設定を上書き",
"allowLan": "LANを許可",
"allowLanDesc": "LAN経由でのプロキシアクセスを許可",
"tun": "TUN",
"tunDesc": "管理者モードでのみ有効",
"minimizeOnExit": "終了時に最小化",
"minimizeOnExitDesc": "システムの終了イベントを変更",
"autoLaunch": "自動起動",
"autoLaunchDesc": "システムの自動起動に従う",
"silentLaunch": "バックグラウンド起動",
"silentLaunchDesc": "バックグラウンドで起動",
"autoRun": "自動実行",
"autoRunDesc": "アプリ起動時に自動実行",
"logcat": "ログキャット",
"logcatDesc": "無効化するとログエントリを非表示",
"autoCheckUpdate": "自動更新チェック",
"autoCheckUpdateDesc": "起動時に更新を自動チェック",
"accessControl": "アクセス制御",
"accessControlDesc": "アプリケーションのプロキシアクセスを設定",
"application": "アプリケーション",
"applicationDesc": "アプリ関連設定を変更",
"edit": "編集",
"confirm": "確認",
"update": "更新",
"add": "追加",
"save": "保存",
"delete": "削除",
"years": "年",
"months": "月",
"hours": "時間",
"days": "日",
"minutes": "分",
"seconds": "秒",
"ago": "前",
"just": "たった今",
"qrcode": "QRコード",
"qrcodeDesc": "QRコードをスキャンしてプロファイルを取得",
"url": "URL",
"urlDesc": "URL経由でプロファイルを取得",
"file": "ファイル",
"fileDesc": "プロファイルを直接アップロード",
"name": "名前",
"profileNameNullValidationDesc": "プロファイル名を入力してください",
"profileUrlNullValidationDesc": "プロファイルURLを入力してください",
"profileUrlInvalidValidationDesc": "有効なプロファイルURLを入力してください",
"autoUpdate": "自動更新",
"autoUpdateInterval": "自動更新間隔(分)",
"profileAutoUpdateIntervalNullValidationDesc": "自動更新間隔を入力してください",
"profileAutoUpdateIntervalInvalidValidationDesc": "有効な間隔形式を入力してください",
"themeMode": "テーマモード",
"themeColor": "テーマカラー",
"preview": "プレビュー",
"auto": "自動",
"light": "ライト",
"dark": "ダーク",
"importFromURL": "URLからインポート",
"submit": "送信",
"doYouWantToPass": "通過させますか?",
"create": "作成",
"defaultSort": "デフォルト順",
"delaySort": "遅延順",
"nameSort": "名前順",
"pleaseUploadFile": "ファイルをアップロードしてください",
"pleaseUploadValidQrcode": "有効なQRコードをアップロードしてください",
"blacklistMode": "ブラックリストモード",
"whitelistMode": "ホワイトリストモード",
"filterSystemApp": "システムアプリを除外",
"cancelFilterSystemApp": "システムアプリの除外を解除",
"selectAll": "すべて選択",
"cancelSelectAll": "全選択解除",
"appAccessControl": "アプリアクセス制御",
"accessControlAllowDesc": "選択したアプリのみVPNを許可",
"accessControlNotAllowDesc": "選択したアプリをVPNから除外",
"selected": "選択済み",
"unableToUpdateCurrentProfileDesc": "現在のプロファイルを更新できません",
"noMoreInfoDesc": "追加情報なし",
"profileParseErrorDesc": "プロファイル解析エラー",
"proxyPort": "プロキシポート",
"proxyPortDesc": "Clashのリスニングポートを設定",
"port": "ポート",
"logLevel": "ログレベル",
"show": "表示",
"exit": "終了",
"systemProxy": "システムプロキシ",
"project": "プロジェクト",
"core": "コア",
"tabAnimation": "タブアニメーション",
"desc": "ClashMetaベースのマルチプラットフォームプロキシクライアント。シンプルで使いやすく、オープンソースで広告なし。",
"startVpn": "VPNを開始中...",
"stopVpn": "VPNを停止中...",
"discovery": "新しいバージョンを発見",
"compatible": "互換モード",
"compatibleDesc": "有効化すると一部機能を失いますが、Clashの完全サポートを獲得",
"notSelectedTip": "現在のプロキシグループは選択できません",
"tip": "ヒント",
"backupAndRecovery": "バックアップと復元",
"backupAndRecoveryDesc": "WebDAVまたはファイルでデータを同期",
"account": "アカウント",
"backup": "バックアップ",
"recovery": "復元",
"recoveryProfiles": "プロファイルのみ復元",
"recoveryAll": "全データ復元",
"recoverySuccess": "復元成功",
"backupSuccess": "バックアップ成功",
"noInfo": "情報なし",
"pleaseBindWebDAV": "WebDAVをバインドしてください",
"bind": "バインド",
"connectivity": "接続性:",
"webDAVConfiguration": "WebDAV設定",
"address": "アドレス",
"addressHelp": "WebDAVサーバーアドレス",
"addressTip": "有効なWebDAVアドレスを入力",
"password": "パスワード",
"passwordTip": "パスワードは必須です",
"accountTip": "アカウントは必須です",
"checkUpdate": "更新を確認",
"discoverNewVersion": "新バージョンを発見",
"checkUpdateError": "アプリは最新版です",
"goDownload": "ダウンロードへ",
"unknown": "不明",
"geoData": "地域データ",
"externalResources": "外部リソース",
"checking": "確認中...",
"country": "国",
"checkError": "確認エラー",
"search": "検索",
"allowBypass": "アプリがVPNをバイパスすることを許可",
"allowBypassDesc": "有効化すると一部アプリがVPNをバイパス",
"externalController": "外部コントローラー",
"externalControllerDesc": "有効化するとClashコアをポート9090で制御可能",
"ipv6Desc": "有効化するとIPv6トラフィックを受信可能",
"app": "アプリ",
"general": "一般",
"vpnSystemProxyDesc": "HTTPプロキシをVpnServiceに接続",
"systemProxyDesc": "HTTPプロキシをVpnServiceに接続",
"unifiedDelay": "統一遅延",
"unifiedDelayDesc": "ハンドシェイクなどの余分な遅延を削除",
"tcpConcurrent": "TCP並列処理",
"tcpConcurrentDesc": "TCP並列処理を許可",
"geodataLoader": "Geo低メモリモード",
"geodataLoaderDesc": "有効化するとGeo低メモリローダーを使用",
"requests": "リクエスト",
"requestsDesc": "最近のリクエスト記録を表示",
"findProcessMode": "プロセス検出",
"init": "初期化",
"infiniteTime": "長期有効",
"expirationTime": "有効期限",
"connections": "接続",
"connectionsDesc": "現在の接続データを表示",
"nullRequestsDesc": "リクエストなし",
"nullConnectionsDesc": "接続なし",
"intranetIP": "イントラネットIP",
"view": "表示",
"cut": "切り取り",
"copy": "コピー",
"paste": "貼り付け",
"testUrl": "URLテスト",
"sync": "同期",
"exclude": "最近のタスクから非表示",
"excludeDesc": "アプリがバックグラウンド時に最近のタスクから非表示",
"oneColumn": "1列",
"twoColumns": "2列",
"threeColumns": "3列",
"fourColumns": "4列",
"expand": "標準",
"shrink": "縮小",
"min": "最小化",
"tab": "タブ",
"list": "リスト",
"delay": "遅延",
"style": "スタイル",
"size": "サイズ",
"sort": "並び替え",
"columns": "列",
"proxiesSetting": "プロキシ設定",
"proxyGroup": "プロキシグループ",
"go": "移動",
"externalLink": "外部リンク",
"otherContributors": "その他の貢献者",
"autoCloseConnections": "接続を自動閉じる",
"autoCloseConnectionsDesc": "ノード変更後に接続を自動閉じる",
"onlyStatisticsProxy": "プロキシのみ統計",
"onlyStatisticsProxyDesc": "有効化するとプロキシトラフィックのみ統計",
"deleteProfileTip": "現在のプロファイルを削除しますか?",
"pureBlackMode": "純黒モード",
"keepAliveIntervalDesc": "TCPキープアライブ間隔",
"entries": " エントリ",
"local": "ローカル",
"remote": "リモート",
"remoteBackupDesc": "WebDAVにデータをバックアップ",
"remoteRecoveryDesc": "WebDAVからデータを復元",
"localBackupDesc": "ローカルにデータをバックアップ",
"localRecoveryDesc": "ファイルからデータを復元",
"mode": "モード",
"time": "時間",
"source": "ソース",
"allApps": "全アプリ",
"onlyOtherApps": "サードパーティアプリのみ",
"action": "アクション",
"intelligentSelected": "インテリジェント選択",
"clipboardImport": "クリップボードからインポート",
"clipboardExport": "クリップボードにエクスポート",
"layout": "レイアウト",
"tight": "密",
"standard": "標準",
"loose": "疎",
"profilesSort": "プロファイルの並び替え",
"start": "開始",
"stop": "停止",
"appDesc": "アプリ関連設定の処理",
"vpnDesc": "VPN関連設定の変更",
"dnsDesc": "DNS関連設定の更新",
"key": "キー",
"value": "値",
"notEmpty": "空欄不可",
"hostsDesc": "ホストを追加",
"vpnTip": "変更はVPN再起動後に有効",
"vpnEnableDesc": "VpnService経由で全システムトラフィックをルーティング",
"options": "オプション",
"loopback": "ループバック解除ツール",
"loopbackDesc": "UWPループバック解除用",
"providers": "プロバイダー",
"proxyProviders": "プロキシプロバイダー",
"ruleProviders": "ルールプロバイダー",
"overrideDns": "DNS上書き",
"overrideDnsDesc": "有効化するとプロファイルのDNS設定を上書き",
"status": "ステータス",
"statusDesc": "無効時はシステムDNSを使用",
"preferH3Desc": "DOHのHTTP/3を優先使用",
"respectRules": "ルール尊重",
"respectRulesDesc": "DNS接続がルールに従うproxy-server-nameserverの設定が必要",
"dnsMode": "DNSモード",
"fakeipRange": "Fakeip範囲",
"fakeipFilter": "Fakeipフィルター",
"defaultNameserver": "デフォルトネームサーバー",
"defaultNameserverDesc": "DNSサーバーの解決用",
"nameserver": "ネームサーバー",
"nameserverDesc": "ドメイン解決用",
"useHosts": "ホストを使用",
"useSystemHosts": "システムホストを使用",
"nameserverPolicy": "ネームサーバーポリシー",
"nameserverPolicyDesc": "対応するネームサーバーポリシーを指定",
"proxyNameserver": "プロキシネームサーバー",
"proxyNameserverDesc": "プロキシノード解決用ドメイン",
"fallback": "フォールバック",
"fallbackDesc": "通常はオフショアDNSを使用",
"fallbackFilter": "フォールバックフィルター",
"geoipCode": "GeoIPコード",
"ipcidr": "IPCIDR",
"domain": "ドメイン",
"reset": "リセット",
"action_view": "表示/非表示",
"action_start": "開始/停止",
"action_mode": "モード切替",
"action_proxy": "システムプロキシ",
"action_tun": "TUN",
"disclaimer": "免責事項",
"disclaimerDesc": "本ソフトウェアは学習交流や科学研究などの非営利目的でのみ使用されます。商用利用は厳禁です。いかなる商用活動も本ソフトウェアとは無関係です。",
"agree": "同意",
"hotkeyManagement": "ホットキー管理",
"hotkeyManagementDesc": "キーボードでアプリを制御",
"pressKeyboard": "キーボードを押してください",
"inputCorrectHotkey": "正しいホットキーを入力",
"hotkeyConflict": "ホットキー競合",
"remove": "削除",
"noHotKey": "ホットキーなし",
"noNetwork": "ネットワークなし",
"ipv6InboundDesc": "IPv6インバウンドを許可",
"exportLogs": "ログをエクスポート",
"exportSuccess": "エクスポート成功",
"iconStyle": "アイコンスタイル",
"onlyIcon": "アイコンのみ",
"noIcon": "なし",
"stackMode": "スタックモード",
"network": "ネットワーク",
"networkDesc": "ネットワーク関連設定の変更",
"bypassDomain": "バイパスドメイン",
"bypassDomainDesc": "システムプロキシ有効時のみ適用",
"resetTip": "リセットを確定",
"regExp": "正規表現",
"icon": "アイコン",
"iconConfiguration": "アイコン設定",
"noData": "データなし",
"adminAutoLaunch": "管理者自動起動",
"adminAutoLaunchDesc": "管理者モードで起動",
"fontFamily": "フォントファミリー",
"systemFont": "システムフォント",
"toggle": "トグル",
"system": "システム",
"routeMode": "ルートモード",
"routeMode_bypassPrivate": "プライベートルートをバイパス",
"routeMode_config": "設定を使用",
"routeAddress": "ルートアドレス",
"routeAddressDesc": "ルートアドレスを設定",
"pleaseInputAdminPassword": "管理者パスワードを入力",
"copyEnvVar": "環境変数をコピー",
"memoryInfo": "メモリ情報",
"cancel": "キャンセル",
"fileIsUpdate": "ファイルが変更されました。保存しますか?",
"profileHasUpdate": "プロファイルが変更されました。自動更新を無効化しますか?",
"hasCacheChange": "変更をキャッシュしますか?",
"nullProxies": "プロキシなし",
"copySuccess": "コピー成功",
"copyLink": "リンクをコピー",
"exportFile": "ファイルをエクスポート",
"cacheCorrupt": "キャッシュが破損しています。クリアしますか?",
"detectionTip": "サードパーティAPIに依存参考値",
"listen": "リスン",
"keyExists": "現在のキーは既に存在します",
"valueExists": "現在の値は既に存在します",
"undo": "元に戻す",
"redo": "やり直す",
"none": "なし",
"basicConfig": "基本設定",
"basicConfigDesc": "基本設定をグローバルに変更",
"selectedCountTitle": "{count} 項目が選択されています",
"addRule": "ルールを追加",
"ruleProviderEmptyTip": "ルールプロバイダーは必須です",
"ruleName": "ルール名",
"content": "内容",
"contentEmptyTip": "内容は必須です",
"subRule": "サブルール",
"subRuleEmptyTip": "サブルールの内容は必須です",
"ruleTarget": "ルール対象",
"ruleTargetEmptyTip": "ルール対象は必須です",
"sourceIp": "送信元IP",
"noResolve": "IPを解決しない",
"getOriginRules": "元のルールを取得",
"overrideOriginRules": "元のルールを上書き",
"addedOriginRules": "元のルールに追加",
"enableOverride": "上書きを有効化",
"deleteRuleTip": "選択したルールを削除しますか?",
"saveChanges": "変更を保存しますか?",
"generalDesc": "一般設定を変更",
"findProcessModeDesc": "有効化するとパフォーマンスが若干低下します",
"tabAnimationDesc": "モバイル表示でのみ有効",
"saveTip": "保存してもよろしいですか?",
"deleteColorTip": "現在の色を削除しますか?",
"colorExists": "この色は既に存在します",
"colorSchemes": "カラースキーム",
"palette": "パレット",
"tonalSpotScheme": "トーンスポット",
"fidelityScheme": "ハイファイデリティー",
"monochromeScheme": "モノクローム",
"neutralScheme": "ニュートラル",
"vibrantScheme": "ビブラント",
"expressiveScheme": "エクスプレッシブ",
"contentScheme": "コンテンツテーマ",
"rainbowScheme": "レインボー",
"fruitSaladScheme": "フルーツサラダ",
"developerMode": "デベロッパーモード",
"developerModeEnableTip": "デベロッパーモードが有効になりました。",
"messageTest": "メッセージテスト",
"messageTestTip": "これはメッセージです。",
"crashTest": "クラッシュテスト",
"clearData": "データを消去",
"zoom": "ズーム",
"textScale": "テキストスケーリング",
"internet": "インターネット",
"systemApp": "システムアプリ",
"noNetworkApp": "ネットワークなしアプリ"
}

View File

@@ -1,400 +0,0 @@
{
"rule": "Правило",
"global": "Глобальный",
"direct": "Прямой",
"dashboard": "Панель управления",
"proxies": "Прокси",
"profile": "Профиль",
"profiles": "Профили",
"tools": "Инструменты",
"logs": "Логи",
"logsDesc": "Записи захвата логов",
"resources": "Ресурсы",
"resourcesDesc": "Информация, связанная с внешними ресурсами",
"trafficUsage": "Использование трафика",
"coreInfo": "Информация о ядре",
"nullCoreInfoDesc": "Не удалось получить информацию о ядре",
"networkSpeed": "Скорость сети",
"outboundMode": "Режим исходящего трафика",
"networkDetection": "Обнаружение сети",
"upload": "Загрузка",
"download": "Скачивание",
"noProxy": "Нет прокси",
"noProxyDesc": "Пожалуйста, создайте профиль или добавьте действительный профиль",
"nullProfileDesc": "Нет профиля, пожалуйста, добавьте профиль",
"nullLogsDesc": "Нет логов",
"settings": "Настройки",
"language": "Язык",
"defaultText": "По умолчанию",
"more": "Еще",
"other": "Другое",
"about": "О программе",
"en": "Английский",
"ja": "Японский",
"ru": "Русский",
"zh_CN": "Упрощенный китайский",
"theme": "Тема",
"themeDesc": "Установить темный режим, настроить цвет",
"override": "Переопределить",
"overrideDesc": "Переопределить конфигурацию, связанную с прокси",
"allowLan": "Разрешить LAN",
"allowLanDesc": "Разрешить доступ к прокси через локальную сеть",
"tun": "TUN",
"tunDesc": "действительно только в режиме администратора",
"minimizeOnExit": "Свернуть при выходе",
"minimizeOnExitDesc": "Изменить стандартное событие выхода из системы",
"autoLaunch": "Автозапуск",
"autoLaunchDesc": "Следовать автозапуску системы",
"silentLaunch": "Тихий запуск",
"silentLaunchDesc": "Запуск в фоновом режиме",
"autoRun": "Автозапуск",
"autoRunDesc": "Автоматический запуск при открытии приложения",
"logcat": "Logcat",
"logcatDesc": "Отключение скроет запись логов",
"autoCheckUpdate": "Автопроверка обновлений",
"autoCheckUpdateDesc": "Автоматически проверять обновления при запуске приложения",
"accessControl": "Контроль доступа",
"accessControlDesc": "Настройка доступа приложений к прокси",
"application": "Приложение",
"applicationDesc": "Изменение настроек, связанных с приложением",
"edit": "Редактировать",
"confirm": "Подтвердить",
"update": "Обновить",
"add": "Добавить",
"save": "Сохранить",
"delete": "Удалить",
"years": "Лет",
"months": "Месяцев",
"hours": "Часов",
"days": "Дней",
"minutes": "Минут",
"seconds": "Секунд",
"ago": " назад",
"just": "Только что",
"qrcode": "QR-код",
"qrcodeDesc": "Сканируйте QR-код для получения профиля",
"url": "URL",
"urlDesc": "Получить профиль через URL",
"file": "Файл",
"fileDesc": "Прямая загрузка профиля",
"name": "Имя",
"profileNameNullValidationDesc": "Пожалуйста, введите имя профиля",
"profileUrlNullValidationDesc": "Пожалуйста, введите URL профиля",
"profileUrlInvalidValidationDesc": "Пожалуйста, введите действительный URL профиля",
"autoUpdate": "Автообновление",
"autoUpdateInterval": "Интервал автообновления (минуты)",
"profileAutoUpdateIntervalNullValidationDesc": "Пожалуйста, введите интервал времени для автообновления",
"profileAutoUpdateIntervalInvalidValidationDesc": "Пожалуйста, введите действительный формат интервала времени",
"themeMode": "Режим темы",
"themeColor": "Цвет темы",
"preview": "Предпросмотр",
"auto": "Авто",
"light": "Светлый",
"dark": "Темный",
"importFromURL": "Импорт из URL",
"submit": "Отправить",
"doYouWantToPass": "Вы хотите пропустить",
"create": "Создать",
"defaultSort": "Сортировка по умолчанию",
"delaySort": "Сортировка по задержке",
"nameSort": "Сортировка по имени",
"pleaseUploadFile": "Пожалуйста, загрузите файл",
"pleaseUploadValidQrcode": "Пожалуйста, загрузите действительный QR-код",
"blacklistMode": "Режим черного списка",
"whitelistMode": "Режим белого списка",
"filterSystemApp": "Фильтровать системные приложения",
"cancelFilterSystemApp": "Отменить фильтрацию системных приложений",
"selectAll": "Выбрать все",
"cancelSelectAll": "Отменить выбор всего",
"appAccessControl": "Контроль доступа приложений",
"accessControlAllowDesc": "Разрешить только выбранным приложениям доступ к VPN",
"accessControlNotAllowDesc": "Выбранные приложения будут исключены из VPN",
"selected": "Выбрано",
"unableToUpdateCurrentProfileDesc": "невозможно обновить текущий профиль",
"noMoreInfoDesc": "Нет дополнительной информации",
"profileParseErrorDesc": "ошибка разбора профиля",
"proxyPort": "Порт прокси",
"proxyPortDesc": "Установить порт прослушивания Clash",
"port": "Порт",
"logLevel": "Уровень логов",
"show": "Показать",
"exit": "Выход",
"systemProxy": "Системный прокси",
"project": "Проект",
"core": "Ядро",
"tabAnimation": "Анимация вкладок",
"desc": "Многоплатформенный прокси-клиент на основе ClashMeta, простой и удобный в использовании, с открытым исходным кодом и без рекламы.",
"startVpn": "Запуск VPN...",
"stopVpn": "Остановка VPN...",
"discovery": "Обнаружена новая версия",
"compatible": "Режим совместимости",
"compatibleDesc": "Включение приведет к потере части функциональности приложения, но обеспечит полную поддержку Clash.",
"notSelectedTip": "Текущая группа прокси не может быть выбрана.",
"tip": "подсказка",
"backupAndRecovery": "Резервное копирование и восстановление",
"backupAndRecoveryDesc": "Синхронизация данных через WebDAV или файл",
"account": "Аккаунт",
"backup": "Резервное копирование",
"recovery": "Восстановление",
"recoveryProfiles": "Только восстановление профилей",
"recoveryAll": "Восстановить все данные",
"recoverySuccess": "Восстановление успешно",
"backupSuccess": "Резервное копирование успешно",
"noInfo": "Нет информации",
"pleaseBindWebDAV": "Пожалуйста, привяжите WebDAV",
"bind": "Привязать",
"connectivity": "Связь:",
"webDAVConfiguration": "Конфигурация WebDAV",
"address": "Адрес",
"addressHelp": "Адрес сервера WebDAV",
"addressTip": "Пожалуйста, введите действительный адрес WebDAV",
"password": "Пароль",
"passwordTip": "Пароль не может быть пустым",
"accountTip": "Аккаунт не может быть пустым",
"checkUpdate": "Проверить обновления",
"discoverNewVersion": "Обнаружена новая версия",
"checkUpdateError": "Текущее приложение уже является последней версией",
"goDownload": "Перейти к загрузке",
"unknown": "Неизвестно",
"geoData": "Геоданные",
"externalResources": "Внешние ресурсы",
"checking": "Проверка...",
"country": "Страна",
"checkError": "Ошибка проверки",
"search": "Поиск",
"allowBypass": "Разрешить приложениям обходить VPN",
"allowBypassDesc": "Некоторые приложения могут обходить VPN при включении",
"externalController": "Внешний контроллер",
"externalControllerDesc": "При включении ядро Clash можно контролировать на порту 9090",
"ipv6Desc": "При включении будет возможно получать IPv6 трафик",
"app": "Приложение",
"general": "Общие",
"vpnSystemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
"systemProxyDesc": "Прикрепить HTTP-прокси к VpnService",
"unifiedDelay": "Унифицированная задержка",
"unifiedDelayDesc": "Убрать дополнительные задержки, такие как рукопожатие",
"tcpConcurrent": "TCP параллелизм",
"tcpConcurrentDesc": "Включение позволит использовать параллелизм TCP",
"geodataLoader": "Режим низкого потребления памяти для геоданных",
"geodataLoaderDesc": "Включение будет использовать загрузчик геоданных с низким потреблением памяти",
"requests": "Запросы",
"requestsDesc": "Просмотр последних записей запросов",
"findProcessMode": "Режим поиска процесса",
"init": "Инициализация",
"infiniteTime": "Долгосрочное действие",
"expirationTime": "Время истечения",
"connections": "Соединения",
"connectionsDesc": "Просмотр текущих данных о соединениях",
"nullRequestsDesc": "Нет запросов",
"nullConnectionsDesc": "Нет соединений",
"intranetIP": "Внутренний IP",
"view": "Просмотр",
"cut": "Вырезать",
"copy": "Копировать",
"paste": "Вставить",
"testUrl": "Тест URL",
"sync": "Синхронизация",
"exclude": "Скрыть из последних задач",
"excludeDesc": "Когда приложение находится в фоновом режиме, оно скрыто из последних задач",
"oneColumn": "Один столбец",
"twoColumns": "Два столбца",
"threeColumns": "Три столбца",
"fourColumns": "Четыре столбца",
"expand": "Стандартный",
"shrink": "Сжать",
"min": "Мин",
"tab": "Вкладка",
"list": "Список",
"delay": "Задержка",
"style": "Стиль",
"size": "Размер",
"sort": "Сортировка",
"columns": "Столбцы",
"proxiesSetting": "Настройка прокси",
"proxyGroup": "Группа прокси",
"go": "Перейти",
"externalLink": "Внешняя ссылка",
"otherContributors": "Другие участники",
"autoCloseConnections": "Автоматическое закрытие соединений",
"autoCloseConnectionsDesc": "Автоматически закрывать соединения после смены узла",
"onlyStatisticsProxy": "Только статистика прокси",
"onlyStatisticsProxyDesc": "При включении будет учитываться только трафик прокси",
"deleteProfileTip": "Вы уверены, что хотите удалить текущий профиль?",
"pureBlackMode": "Чисто черный режим",
"keepAliveIntervalDesc": "Интервал поддержания TCP-соединения",
"entries": " записей",
"local": "Локальный",
"remote": "Удаленный",
"remoteBackupDesc": "Резервное копирование локальных данных на WebDAV",
"remoteRecoveryDesc": "Восстановление данных с WebDAV",
"localBackupDesc": "Резервное копирование локальных данных на локальный диск",
"localRecoveryDesc": "Восстановление данных из файла",
"mode": "Режим",
"time": "Время",
"source": "Источник",
"allApps": "Все приложения",
"onlyOtherApps": "Только сторонние приложения",
"action": "Действие",
"intelligentSelected": "Интеллектуальный выбор",
"clipboardImport": "Импорт из буфера обмена",
"clipboardExport": "Экспорт в буфер обмена",
"layout": "Макет",
"tight": "Плотный",
"standard": "Стандартный",
"loose": "Свободный",
"profilesSort": "Сортировка профилей",
"start": "Старт",
"stop": "Стоп",
"appDesc": "Обработка настроек, связанных с приложением",
"vpnDesc": "Изменение настроек, связанных с VPN",
"dnsDesc": "Обновление настроек, связанных с DNS",
"key": "Ключ",
"value": "Значение",
"notEmpty": "Не может быть пустым",
"hostsDesc": "Добавить Hosts",
"vpnTip": "Изменения вступят в силу после перезапуска VPN",
"vpnEnableDesc": "Автоматически направляет весь системный трафик через VpnService",
"options": "Опции",
"loopback": "Инструмент разблокировки Loopback",
"loopbackDesc": "Используется для разблокировки Loopback UWP",
"providers": "Провайдеры",
"proxyProviders": "Провайдеры прокси",
"ruleProviders": "Провайдеры правил",
"overrideDns": "Переопределить DNS",
"overrideDnsDesc": "Включение переопределит настройки DNS в профиле",
"status": "Статус",
"statusDesc": "Системный DNS будет использоваться при выключении",
"preferH3Desc": "Приоритетное использование HTTP/3 для DOH",
"respectRules": "Соблюдение правил",
"respectRulesDesc": "DNS-соединение следует правилам, необходимо настроить proxy-server-nameserver",
"dnsMode": "Режим DNS",
"fakeipRange": "Диапазон Fakeip",
"fakeipFilter": "Фильтр Fakeip",
"defaultNameserver": "Сервер имен по умолчанию",
"defaultNameserverDesc": "Для разрешения DNS-сервера",
"nameserver": "Сервер имен",
"nameserverDesc": "Для разрешения домена",
"useHosts": "Использовать hosts",
"useSystemHosts": "Использовать системные hosts",
"nameserverPolicy": "Политика сервера имен",
"nameserverPolicyDesc": "Указать соответствующую политику сервера имен",
"proxyNameserver": "Прокси-сервер имен",
"proxyNameserverDesc": "Домен для разрешения прокси-узлов",
"fallback": "Резервный",
"fallbackDesc": "Обычно используется оффшорный DNS",
"fallbackFilter": "Фильтр резервного DNS",
"geoipCode": "Код Geoip",
"ipcidr": "IPCIDR",
"domain": "Домен",
"reset": "Сброс",
"action_view": "Показать/Скрыть",
"action_start": "Старт/Стоп",
"action_mode": "Переключить режим",
"action_proxy": "Системный прокси",
"action_tun": "TUN",
"disclaimer": "Отказ от ответственности",
"disclaimerDesc": "Это программное обеспечение используется только в некоммерческих целях, таких как учебные обмены и научные исследования. Запрещено использовать это программное обеспечение в коммерческих целях. Любая коммерческая деятельность, если таковая имеется, не имеет отношения к этому программному обеспечению.",
"agree": "Согласен",
"hotkeyManagement": "Управление горячими клавишами",
"hotkeyManagementDesc": "Использование клавиатуры для управления приложением",
"pressKeyboard": "Пожалуйста, нажмите клавишу.",
"inputCorrectHotkey": "Пожалуйста, введите правильную горячую клавишу",
"hotkeyConflict": "Конфликт горячих клавиш",
"remove": "Удалить",
"noHotKey": "Нет горячей клавиши",
"noNetwork": "Нет сети",
"ipv6InboundDesc": "Разрешить входящий IPv6",
"exportLogs": "Экспорт логов",
"exportSuccess": "Экспорт успешен",
"iconStyle": "Стиль иконки",
"onlyIcon": "Только иконка",
"noIcon": "Нет иконки",
"stackMode": "Режим стека",
"network": "Сеть",
"networkDesc": "Изменение настроек, связанных с сетью",
"bypassDomain": "Обход домена",
"bypassDomainDesc": "Действует только при включенном системном прокси",
"resetTip": "Убедитесь, что хотите сбросить",
"regExp": "Регулярное выражение",
"icon": "Иконка",
"iconConfiguration": "Конфигурация иконки",
"noData": "Нет данных",
"adminAutoLaunch": "Автозапуск с правами администратора",
"adminAutoLaunchDesc": "Запуск с правами администратора при загрузке системы",
"fontFamily": "Семейство шрифтов",
"systemFont": "Системный шрифт",
"toggle": "Переключить",
"system": "Система",
"routeMode": "Режим маршрутизации",
"routeMode_bypassPrivate": "Обход частных адресов маршрутизации",
"routeMode_config": "Использовать конфигурацию",
"routeAddress": "Адрес маршрутизации",
"routeAddressDesc": "Настройка адреса прослушивания маршрутизации",
"pleaseInputAdminPassword": "Пожалуйста, введите пароль администратора",
"copyEnvVar": "Копирование переменных окружения",
"memoryInfo": "Информация о памяти",
"cancel": "Отмена",
"fileIsUpdate": "Файл был изменен. Хотите сохранить изменения?",
"profileHasUpdate": "Профиль был изменен. Хотите отключить автообновление?",
"hasCacheChange": "Хотите сохранить изменения в кэше?",
"nullProxies": "Нет прокси",
"copySuccess": "Копирование успешно",
"copyLink": "Копировать ссылку",
"exportFile": "Экспорт файла",
"cacheCorrupt": "Кэш поврежден. Хотите очистить его?",
"detectionTip": "Опирается на сторонний API, только для справки",
"listen": "Слушать",
"keyExists": "Текущий ключ уже существует",
"valueExists": "Текущее значение уже существует",
"undo": "Отменить",
"redo": "Повторить",
"none": "Нет",
"basicConfig": "Базовая конфигурация",
"basicConfigDesc": "Глобальное изменение базовых настроек",
"selectedCountTitle": "Выбрано {count} элементов",
"addRule": "Добавить правило",
"ruleProviderEmptyTip": "Поставщик правил не может быть пустым",
"ruleName": "Название правила",
"content": "Содержание",
"contentEmptyTip": "Содержание не может быть пустым",
"subRule": "Подправило",
"subRuleEmptyTip": "Содержание подправила не может быть пустым",
"ruleTarget": "Цель правила",
"ruleTargetEmptyTip": "Цель правила не может быть пустой",
"sourceIp": "Исходный IP",
"noResolve": "Не разрешать IP",
"getOriginRules": "Получить оригинальные правила",
"overrideOriginRules": "Переопределить оригинальное правило",
"addedOriginRules": "Добавить к оригинальным правилам",
"enableOverride": "Включить переопределение",
"deleteRuleTip": "Вы уверены, что хотите удалить выбранное правило?",
"saveChanges": "Сохранить изменения?",
"generalDesc": "Изменение общих настроек",
"findProcessModeDesc": "При включении возможны небольшие потери производительности",
"tabAnimationDesc": "Действительно только в мобильном виде",
"saveTip": "Вы уверены, что хотите сохранить?",
"deleteColorTip": "Удалить текущий цвет?",
"colorExists": "Этот цвет уже существует",
"colorSchemes": "Цветовые схемы",
"palette": "Палитра",
"tonalSpotScheme": "Тональный акцент",
"fidelityScheme": "Точная передача",
"monochromeScheme": "Монохром",
"neutralScheme": "Нейтральные",
"vibrantScheme": "Яркие",
"expressiveScheme": "Экспрессивные",
"contentScheme": "Контентная тема",
"rainbowScheme": "Радужные",
"fruitSaladScheme": "Фруктовый микс",
"developerMode": "Режим разработчика",
"developerModeEnableTip": "Режим разработчика активирован.",
"messageTest": "Тестирование сообщения",
"messageTestTip": "Это сообщение.",
"crashTest": "Тест на сбои",
"clearData": "Очистить данные",
"zoom": "Масштаб",
"textScale": "Масштабирование текста",
"internet": "Интернет",
"systemApp": "Системное приложение",
"noNetworkApp": "Приложение без сети"
}

Binary file not shown.

View File

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

View File

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

View File

@@ -1,77 +0,0 @@
//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,23 @@
package main
import "C"
import (
"context"
"core/state"
"errors"
"fmt"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/hub/route"
"github.com/samber/lo"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -13,40 +27,52 @@ import (
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os"
"path/filepath"
"strings"
"sync"
)
func splitByMultipleSeparators(s string) interface{} {
isSeparator := func(r rune) bool {
return r == ',' || r == ' ' || r == ';'
}
parts := strings.FieldsFunc(s, isSeparator)
if len(parts) > 1 {
return parts
}
return s
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
}
var (
version = 0
isRunning = false
runLock sync.Mutex
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
)
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
GroupName *string `json:"group-name"`
ProxyName *string `json:"proxy-name"`
}
type TestDelayParams struct {
ProxyName string `json:"proxy-name"`
Timeout int64 `json:"timeout"`
}
type ProcessMapItem struct {
Id int64 `json:"id"`
Value string `json:"value"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
UpdateAt time.Time `json:"update-at"`
}
type ExternalProviders []ExternalProvider
@@ -54,6 +80,32 @@ 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) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
var b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
func restartExecutable(execPath string) {
var err error
executor.Shutdown()
if runtime.GOOS == "windows" {
cmd := exec.Command(execPath, os.Args[1:]...)
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Start()
if err != nil {
log.Fatalln("restarting: %s", err)
}
os.Exit(0)
}
log.Infoln("restarting: %q %q", execPath, os.Args[1:])
err = syscall.Exec(execPath, os.Args, os.Environ())
if err != nil {
log.Fatalln("restarting: %s", err)
}
}
func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
@@ -66,6 +118,19 @@ func readFile(path string) ([]byte, error) {
return data, err
}
func removeFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
err = os.Remove(absPath)
if err != nil {
return err
}
return nil
}
func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
}
@@ -78,6 +143,7 @@ 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)
@@ -91,6 +157,16 @@ func getRawConfigWithId(id string) *config.RawConfig {
continue
}
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
if configParams.TestURL != nil {
if mapping["health-check"] != nil {
hc := mapping["health-check"].(map[string]any)
if hc != nil {
if hc["url"] != nil {
hc["url"] = *configParams.TestURL
}
}
}
}
}
for _, mapping := range prof.RuleProvider {
value, exist := mapping["path"].(string)
@@ -122,13 +198,12 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider)
return &ExternalProvider{
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
UpdateAt: psp.UpdatedAt(),
Path: psp.Vehicle().Path(),
SubscriptionInfo: psp.GetSubscriptionInfo(),
Name: psp.Name(),
Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(),
Count: psp.Count(),
Path: psp.Vehicle().Path(),
UpdateAt: psp.UpdatedAt(),
}, nil
case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider)
@@ -137,8 +212,8 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
Type: rsp.Type().String(),
VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(),
UpdateAt: rsp.UpdatedAt(),
Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt(),
}, nil
default:
return nil, errors.New("not external provider")
@@ -172,19 +247,155 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof
}
func attachHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
if str, ok := v.(string); ok {
hosts[k] = splitByMultipleSeparators(str)
}
}
}
//func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
// for _, v := range s {
// initVal = f(initVal, v)
// }
// return initVal
//}
//
//func Map[T, U any](slice []T, fn func(T) U) []U {
// result := make([]U, len(slice))
// for i, v := range slice {
// result[i] = fn(v)
// }
// return result
//}
//
//func replaceFromMap(s string, m map[string]string) string {
// for k, v := range m {
// s = strings.ReplaceAll(s, k, v)
// }
// return s
//}
//
//func removeDuplicateFromSlice[T any](slice []T) []T {
// result := make([]T, 0)
// seen := make(map[any]struct{})
// for _, value := range slice {
// if _, ok := seen[value]; !ok {
// result = append(result, value)
// seen[value] = struct{}{}
// }
// }
// return result
//}
func updatePatchDns(dns config.RawDNS) {
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
if str, ok := pair.Value.(string); ok {
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
}
//func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) {
// var replacements = map[string]string{}
// var selectArr []map[string]any
// var urlTestArr []map[string]any
// var fallbackArr []map[string]any
// for _, group := range *proxyGroup {
// switch group["type"] {
// case "select":
// selectArr = append(selectArr, group)
// replacements[group["name"].(string)] = "Proxy"
// break
// case "url-test":
// urlTestArr = append(urlTestArr, group)
// replacements[group["name"].(string)] = "Auto"
// break
// case "fallback":
// fallbackArr = append(fallbackArr, group)
// replacements[group["name"].(string)] = "Fallback"
// break
// default:
// break
// }
// }
//
// ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Proxy" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
//
// AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Auto" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// AutoProxies = removeDuplicateFromSlice(AutoProxies)
//
// FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
// if cur["proxies"] == nil {
// return res
// }
// for _, proxyName := range cur["proxies"].([]interface{}) {
// if str, ok := proxyName.(string); ok {
// str = replaceFromMap(str, replacements)
// if str != "Fallback" {
// res = append(res, str)
// }
// }
// }
// return res
// })
//
// FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
//
// var computedProxyGroup []map[string]any
//
// if len(ProxyProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Proxy",
// "type": "select",
// "proxies": ProxyProxies,
// })
// }
//
// if len(AutoProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Auto",
// "type": "url-test",
// "proxies": AutoProxies,
// })
// }
//
// if len(FallbackProxies) > 0 {
// computedProxyGroup = append(computedProxyGroup,
// map[string]any{
// "name": "Fallback",
// "type": "fallback",
// "proxies": FallbackProxies,
// })
// }
//
// computedRule := Map(*rule, func(value string) string {
// return replaceFromMap(value, replacements)
// })
//
// *proxyGroup = computedProxyGroup
// *rule = computedRule
//}
func genHosts(hosts, patchHosts map[string]any) {
for k, v := range patchHosts {
hosts[k] = v
}
}
@@ -195,25 +406,28 @@ func trimArr(arr []string) (r []string) {
return
}
func overrideRules(rules, patchRules []string) []string {
target := ""
for _, line := range rules {
var ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"}
func overrideRules(rules *[]string) {
var target = ""
for _, line := range *rules {
rule := trimArr(strings.Split(line, ","))
if len(rule) != 2 {
continue
l := len(rule)
if l != 2 {
return
}
if strings.EqualFold(rule[0], "MATCH") {
if strings.ToUpper(rule[0]) == "MATCH" {
target = rule[1]
break
}
}
if target == "" {
return rules
return
}
rulesExt := lo.Map(ips, func(ip string, _ int) string {
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
var rulesExt = lo.Map(ips, func(ip string, index int) string {
return fmt.Sprintf("DOMAIN %s %s", ip, target)
})
return append(append(rulesExt, patchRules...), rules...)
*rules = append(rulesExt, *rules...)
}
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -236,7 +450,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
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
@@ -247,27 +460,29 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
updatePatchDns(patchConfig.DNS)
targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
}
if configParams.OverrideRule {
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
} else {
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
}
overrideRules(&targetConfig.Rule)
//if runtime.GOOS == "android" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, "dhcp://"+dns.SystemDNSPlaceholder)
//} else if runtime.GOOS == "windows" {
// targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder)
//}
//if configParams.IsCompatible == false {
// targetConfig.ProxyProvider = make(map[string]map[string]any)
// targetConfig.RuleProvider = make(map[string]map[string]any)
// generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule)
//}
}
func patchConfig() {
func patchConfig(general *config.General, controller *config.Controller, tls *config.TLS) {
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)
@@ -294,15 +509,17 @@ func patchConfig() {
})
}
func updateListeners(force bool) {
var isRunning = false
var runLock sync.Mutex
func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
if !isRunning {
return
}
general := currentConfig.General
listeners := currentConfig.Listeners
if force == true {
stopListeners()
}
runLock.Lock()
defer runLock.Unlock()
stopListeners()
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -351,20 +568,20 @@ func patchSelectGroup() {
}
}
func applyConfig(rawConfig *config.RawConfig) error {
runLock.Lock()
defer runLock.Unlock()
var err error
currentConfig, err = config.ParseRawConfig(rawConfig)
func applyConfig() error {
cfg, err := config.ParseRawConfig(state.CurrentRawConfig)
if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if configParams.IsPatch {
patchConfig()
patchConfig(cfg.General, cfg.Controller, cfg.TLS)
} else {
hub.ApplyConfig(currentConfig)
closeConnections()
runtime.GC()
hub.ApplyConfig(cfg)
patchSelectGroup()
}
updateListeners(false)
updateListeners(cfg.General, cfg.Listeners)
externalProviders = getExternalProvidersRaw()
return err
}

View File

@@ -1,113 +0,0 @@
package main
import (
"encoding/json"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/config"
"time"
)
type InitParams struct {
HomeDir string `json:"home-dir"`
Version int `json:"version"`
}
type ConfigExtendedParams struct {
IsPatch bool `json:"is-patch"`
IsCompatible bool `json:"is-compatible"`
SelectedMap map[string]string `json:"selected-map"`
TestURL *string `json:"test-url"`
OverrideDns bool `json:"override-dns"`
OverrideRule bool `json:"override-rule"`
}
type GenerateConfigParams struct {
ProfileId string `json:"profile-id"`
Config config.RawConfig `json:"config" `
Params ConfigExtendedParams `json:"params"`
}
type ChangeProxyParams struct {
GroupName *string `json:"group-name"`
ProxyName *string `json:"proxy-name"`
}
type TestDelayParams struct {
ProxyName string `json:"proxy-name"`
TestUrl string `json:"test-url"`
Timeout int64 `json:"timeout"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
Count int `json:"count"`
Path string `json:"path"`
UpdateAt time.Time `json:"update-at"`
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
}
const (
messageMethod Method = "message"
initClashMethod Method = "initClash"
getIsInitMethod Method = "getIsInit"
forceGcMethod Method = "forceGc"
shutdownMethod Method = "shutdown"
validateConfigMethod Method = "validateConfig"
updateConfigMethod Method = "updateConfig"
getProxiesMethod Method = "getProxies"
changeProxyMethod Method = "changeProxy"
getTrafficMethod Method = "getTraffic"
getTotalTrafficMethod Method = "getTotalTraffic"
resetTrafficMethod Method = "resetTraffic"
asyncTestDelayMethod Method = "asyncTestDelay"
getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections"
closeConnectionMethod Method = "closeConnection"
getExternalProvidersMethod Method = "getExternalProviders"
getExternalProviderMethod Method = "getExternalProvider"
getCountryCodeMethod Method = "getCountryCode"
getMemoryMethod Method = "getMemory"
updateGeoDataMethod Method = "updateGeoData"
updateExternalProviderMethod Method = "updateExternalProvider"
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"
startLogMethod Method = "startLog"
stopLogMethod Method = "stopLog"
startListenerMethod Method = "startListener"
stopListenerMethod Method = "stopListener"
updateDnsMethod Method = "updateDns"
setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
getProfileMethod Method = "getProfile"
crashMethod Method = "crash"
)
type Method string
type MessageType string
type Delay struct {
Url string `json:"url"`
Name string `json:"name"`
Value int32 `json:"value"`
}
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
const (
LogMessage MessageType = "log"
DelayMessage MessageType = "delay"
RequestMessage MessageType = "request"
LoadedMessage MessageType = "loaded"
)
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}

View File

@@ -1,5 +1,3 @@
//go:build cgo
package dart_bridge
/*

View File

@@ -1,7 +0,0 @@
//go:build !cgo
package dart_bridge
func SendToPort(port int64, msg string) bool {
return false
}

20
core/dns.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build android
package main
import "C"
import (
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/log"
"strings"
)
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
go func() {
log.Infoln("[DNS] updateDns %s", dnsList)
dns.UpdateSystemDNS(strings.Split(dnsList, ","))
dns.FlushCacheWithDefaultResolver()
}()
}

View File

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

View File

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

View File

@@ -1,81 +1,82 @@
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/state"
"encoding/json"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"net"
"runtime"
"sort"
"strconv"
"time"
)
var (
isInit = false
configParams = ConfigExtendedParams{}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
var configParams = ConfigExtendedParams{}
func handleInitClash(paramsString string) bool {
var params = InitParams{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
return false
}
version = params.Version
var externalProviders = map[string]cp.Provider{}
var isInit = false
//export start
func start() {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
}
//export stop
func stop() {
runLock.Lock()
go func() {
defer runLock.Unlock()
isRunning = false
stopListeners()
}()
}
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit {
constant.SetHomeDir(params.HomeDir)
constant.SetHomeDir(C.GoString(homeDirStr))
isInit = true
}
return isInit
}
func handleStartListener() bool {
runLock.Lock()
defer runLock.Unlock()
isRunning = true
updateListeners(true)
return true
}
func handleStopListener() bool {
runLock.Lock()
defer runLock.Unlock()
isRunning = false
listener.StopListener()
return true
}
func handleGetIsInit() bool {
//export getIsInit
func getIsInit() bool {
return isInit
}
func handleForceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
//export restartClash
func restartClash() bool {
execPath, _ := os.Executable()
go restartExecutable(execPath)
return true
}
func handleShutdown() bool {
//export shutdownClash
func shutdownClash() bool {
stopListeners()
executor.Shutdown()
runtime.GC()
@@ -83,81 +84,106 @@ func handleShutdown() bool {
return true
}
func handleValidateConfig(bytes []byte) string {
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
return err.Error()
}
return ""
}
func handleUpdateConfig(bytes []byte) string {
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()
defer runLock.Unlock()
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return ""
}
return string(data)
}
func handleChangeProxy(data string, fn func(string string)) {
runLock.Lock()
//export forceGc
func forceGc() {
go func() {
defer runLock.Unlock()
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(data), params)
if err != nil {
fn(err.Error())
return
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
fn("Not found group")
return
}
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
fn("Group is not selectable")
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
if err != nil {
fn(err.Error())
return
}
fn("")
return
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}
func handleGetTraffic() string {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
//export validateConfig
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(s))
go func() {
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
var updateLock sync.Mutex
//export updateConfig
func updateConfig(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
go func() {
updateLock.Lock()
defer updateLock.Unlock()
var params = &GenerateConfigParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
configParams = params.Params
prof := decorationConfig(params.ProfileId, params.Config)
state.CurrentRawConfig = prof
err = applyConfig()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export clearEffect
func clearEffect(s *C.char) {
id := C.GoString(s)
go func() {
_ = removeFile(getProfilePath(id))
_ = removeFile(getProfileProvidersPath(id))
}()
}
//export getProxies
func getProxies() *C.char {
data, err := json.Marshal(tunnel.ProxiesWithProviders())
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export changeProxy
func changeProxy(s *C.char) {
paramsString := C.GoString(s)
var params = &ChangeProxyParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
log.Infoln("Unmarshal ChangeProxyParams %v", err)
}
groupName := *params.GroupName
proxyName := *params.ProxyName
proxies := tunnel.ProxiesWithProviders()
group, ok := proxies[groupName]
if !ok {
return
}
adapterProxy := group.(*adapter.Proxy)
selector, ok := adapterProxy.ProxyAdapter.(outboundgroup.SelectAble)
if !ok {
return
}
if proxyName == "" {
selector.ForceSet(proxyName)
} else {
err = selector.Set(proxyName)
}
if err == nil {
log.Infoln("[SelectAble] %s selected %s", groupName, proxyName)
}
}
//export getTraffic
func getTraffic() *C.char {
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -165,13 +191,14 @@ func handleGetTraffic() string {
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleGetTotalTraffic() string {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
//export getTotalTraffic
func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyProxy)
traffic := map[string]int64{
"up": up,
"down": down,
@@ -179,36 +206,31 @@ func handleGetTotalTraffic() string {
data, err := json.Marshal(traffic)
if err != nil {
fmt.Println("Error:", err)
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleGetProfile(profileId string) string {
prof := getRawConfigWithId(profileId)
data, err := json.Marshal(prof)
if err != nil {
return ""
}
return string(data)
}
func handleResetTraffic() {
//export resetTraffic
func resetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
func handleAsyncTestDelay(paramsString string, fn func(string)) {
//export asyncTestDelay
func asyncTestDelay(s *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(s)
b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params)
if err != nil {
fn("")
bridge.SendToPort(i, "")
return false, nil
}
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil {
fn("")
bridge.SendToPort(i, "")
return false, nil
}
@@ -225,47 +247,52 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if proxy == nil {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
}
testUrl := constant.DefaultTestURL
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delayData.Url = testUrl
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if err != nil || delay == 0 {
delayData.Value = -1
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
}
delayData.Value = int32(delay)
data, _ := json.Marshal(delayData)
fn(string(data))
bridge.SendToPort(i, string(data))
return false, nil
})
}
func handleGetConnections() string {
runLock.Lock()
defer runLock.Unlock()
//export getVersionInfo
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export getConnections
func getConnections() *C.char {
snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot)
if err != nil {
fmt.Println("Error:", err)
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleCloseConnections() bool {
runLock.Lock()
defer runLock.Unlock()
//export closeConnections
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
@@ -273,24 +300,43 @@ func handleCloseConnections() bool {
}
return true
})
return true
}
func handleCloseConnection(connectionId string) bool {
runLock.Lock()
defer runLock.Unlock()
//export closeConnection
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return false
return
}
_ = c.Close()
return true
}
func handleGetExternalProviders() string {
runLock.Lock()
defer runLock.Unlock()
externalProviders = getExternalProvidersRaw()
//export getProviders
func getProviders() *C.char {
data, err := json.Marshal(tunnel.Providers())
var msg *C.char
if err != nil {
msg = C.CString("")
return msg
}
msg = C.CString(string(data))
return msg
}
//export getProvider
func getProvider(name *C.char) *C.char {
providerName := C.GoString(name)
providers := tunnel.Providers()
data, err := json.Marshal(providers[providerName])
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export getExternalProviders
func getExternalProviders() *C.char {
eps := make([]ExternalProvider, 0)
for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p)
@@ -302,154 +348,124 @@ func handleGetExternalProviders() string {
sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps)
if err != nil {
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleGetExternalProvider(externalProviderName string) string {
runLock.Lock()
defer runLock.Unlock()
//export getExternalProvider
func getExternalProvider(name *C.char) *C.char {
externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName]
if !exist {
return ""
return C.CString("")
}
e, err := toExternalProvider(externalProvider)
if err != nil {
return ""
return C.CString("")
}
data, err := json.Marshal(e)
if err != nil {
return ""
return C.CString("")
}
return string(data)
return C.CString(string(data))
}
func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) {
//export updateGeoData
func updateGeoData(geoType *C.char, geoName *C.char, port C.longlong) {
i := int64(port)
geoTypeString := C.GoString(geoType)
geoNameString := C.GoString(geoName)
go func() {
path := constant.Path.Resolve(geoName)
switch geoType {
path := constant.Path.Resolve(geoNameString)
switch geoTypeString {
case "MMDB":
err := updater.UpdateMMDBWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := updater.UpdateASNWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := updater.UpdateGeoIpWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := updater.UpdateGeoSiteWithPath(path)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
}
fn("")
bridge.SendToPort(i, "")
}()
}
func handleUpdateExternalProvider(providerName string, fn func(value string)) {
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, port C.longlong) {
i := int64(port)
providerNameString := C.GoString(providerName)
go func() {
externalProvider, exist := externalProviders[providerName]
externalProvider, exist := externalProviders[providerNameString]
if !exist {
fn("external provider is not exist")
bridge.SendToPort(i, "external provider is not exist")
return
}
err := externalProvider.Update()
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
fn("")
bridge.SendToPort(i, "")
}()
}
func handleSideLoadExternalProvider(providerName string, data []byte, fn func(value string)) {
//export sideLoadExternalProvider
func sideLoadExternalProvider(providerName *C.char, data *C.char, port C.longlong) {
i := int64(port)
bytes := []byte(C.GoString(data))
providerNameString := C.GoString(providerName)
go func() {
runLock.Lock()
defer runLock.Unlock()
externalProvider, exist := externalProviders[providerName]
externalProvider, exist := externalProviders[providerNameString]
if !exist {
fn("external provider is not exist")
bridge.SendToPort(i, "external provider is not exist")
return
}
err := sideUpdateExternalProvider(externalProvider, data)
err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil {
fn(err.Error())
bridge.SendToPort(i, err.Error())
return
}
fn("")
bridge.SendToPort(i, "")
}()
}
func handleStartLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
logSubscriber = log.Subscribe()
go func() {
for logData := range logSubscriber {
if logData.LogLevel < log.Level() {
continue
}
message := &Message{
Type: LogMessage,
Data: logData,
}
sendMessage(*message)
}
}()
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
}
func handleStopLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
//export initMessage
func initMessage(port C.longlong) {
i := int64(port)
Port = i
}
func handleGetCountryCode(ip string, fn func(value string)) {
go func() {
runLock.Lock()
defer runLock.Unlock()
codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip))
if len(codes) == 0 {
fn("")
return
}
fn(codes[0])
}()
}
func handleGetMemory(fn func(value string)) {
go func() {
fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10))
}()
}
func handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
func handleCrash() {
panic("handle invoke crash")
//export freeCString
func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
provider.HealthcheckHook = func(name string, delay uint16) {
delayData := &Delay{
Url: url,
Name: name,
}
if delay == 0 {
@@ -457,19 +473,19 @@ func init() {
} else {
delayData.Value = int32(delay)
}
sendMessage(Message{
SendMessage(Message{
Type: DelayMessage,
Data: delayData,
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
sendMessage(Message{
SendMessage(Message{
Type: RequestMessage,
Data: c,
})
}
executor.DefaultProviderLoadedHook = func(providerName string) {
sendMessage(Message{
SendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})

View File

@@ -1,78 +0,0 @@
//go:build cgo
package main
/*
#include <stdlib.h>
*/
import "C"
import (
bridge "core/dart-bridge"
"encoding/json"
"unsafe"
)
var messagePort int64 = -1
//export initNativeApiBridge
func initNativeApiBridge(api unsafe.Pointer) {
bridge.InitDartApi(api)
}
//export attachMessagePort
func attachMessagePort(mPort C.longlong) {
messagePort = int64(mPort)
}
//export getTraffic
func getTraffic() *C.char {
return C.CString(handleGetTraffic())
}
//export getTotalTraffic
func getTotalTraffic() *C.char {
return C.CString(handleGetTotalTraffic())
}
//export freeCString
func freeCString(s *C.char) {
C.free(unsafe.Pointer(s))
}
//export invokeAction
func invokeAction(paramsChar *C.char, port C.longlong) {
params := C.GoString(paramsChar)
i := int64(port)
var action = &Action{}
err := json.Unmarshal([]byte(params), action)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
go handleAction(action, func(data interface{}) {
bridge.SendToPort(i, string(action.getResult(data)))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.getResult(res)))
}
//export startListener
func startListener() {
handleStartListener()
}
//export stopListener
func stopListener() {
handleStopListener()
}

View File

@@ -1,267 +0,0 @@
//go:build android && cgo
package main
import "C"
import (
"context"
bridge "core/dart-bridge"
"core/platform"
"core/state"
t "core/tun"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
"net"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
type TunHandler struct {
listener *sing_tun.Listener
callback unsafe.Pointer
limit *semaphore.Weighted
}
func (t *TunHandler) close() {
_ = t.limit.Acquire(context.TODO(), 4)
defer t.limit.Release(4)
removeTunHook()
if t.listener != nil {
_ = t.listener.Close()
}
if t.callback != nil {
releaseObject(t.callback)
}
t.callback = nil
t.listener = nil
}
func (t *TunHandler) handleProtect(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return
}
protect(t.callback, fd)
}
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.listener == nil {
return ""
}
var protocol int
uid := -1
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
if version < 29 {
uid = platform.QuerySocketUidFromProcFs(source, target)
}
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
}
var (
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
tunHandler *TunHandler
)
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tunHandler != nil {
tunHandler.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()
}
}
}
func handleGetRunTime() string {
if runTime == nil {
return ""
}
return strconv.FormatInt(runTime.UnixMilli(), 10)
}
func initTunHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
tunHandler.handleProtect(int(fd))
})
}
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
return tunHandler.handleResolveProcess(src, dst), nil
}
}
func removeTunHook() {
dialer.DefaultSocketHook = nil
process.DefaultPackageNameResolver = nil
}
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.VpnProps.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.VpnProps.AccessControl,
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
RouteAddress: currentConfig.General.Tun.RouteAddress,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return ""
}
return string(data)
}
func handleUpdateDns(value string) {
go func() {
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(strings.Split(value, ","))
dns.FlushCacheWithDefaultResolver()
}()
}
func handleGetCurrentProfileName() string {
if state.CurrentState == nil {
return ""
}
return state.CurrentState.CurrentProfileName
}
func nextHandle(action *Action, result func(data interface{})) bool {
switch action.Method {
case getAndroidVpnOptionsMethod:
result(handleGetAndroidVpnOptions())
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
result(true)
return true
case getRunTimeMethod:
result(handleGetRunTime())
return true
case getCurrentProfileNameMethod:
result(handleGetCurrentProfileName())
return true
}
return false
}
//export quickStart
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
paramsString := C.GoString(initParamsChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(paramsString)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int, callback unsafe.Pointer) bool {
go func() {
handleStartTun(int(fd), callback)
}()
return true
}
//export getRunTime
func getRunTime() *C.char {
return C.CString(handleGetRunTime())
}
//export stopTun
func stopTun() {
go func() {
handleStopTun()
}()
}
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
return C.CString(handleGetCurrentProfileName())
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
return C.CString(handleGetAndroidVpnOptions())
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
handleSetState(paramsString)
}
//export updateDns
func updateDns(s *C.char) {
dnsList := C.GoString(s)
handleUpdateDns(dnsList)
}

View File

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

38
core/log.go Normal file
View File

@@ -0,0 +1,38 @@
package main
import "C"
import (
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/log"
)
var logSubscriber observable.Subscription[log.Event]
//export startLog
func startLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
logSubscriber = log.Subscribe()
go func() {
for logData := range logSubscriber {
if logData.LogLevel < log.Level() {
continue
}
message := &Message{
Type: LogMessage,
Data: logData,
}
SendMessage(*message)
}
}()
}
//export stopLog
func stopLog() {
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
}

View File

@@ -1,17 +1,10 @@
//go:build !cgo
package main
import "C"
import (
"fmt"
"os"
)
func main() {
args := os.Args
if len(args) <= 1 {
fmt.Println("Arguments error")
os.Exit(1)
}
startServer(args[1])
fmt.Println("init clash")
}

View File

@@ -1,8 +0,0 @@
//go:build cgo
package main
import "C"
func main() {
}

77
core/message.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
bridge "core/dart-bridge"
"encoding/json"
"github.com/metacubex/mihomo/constant"
)
var Port int64
var ServicePort int64
type MessageType string
const (
LogMessage MessageType = "log"
ProtectMessage MessageType = "protect"
DelayMessage MessageType = "delay"
ProcessMessage MessageType = "process"
RequestMessage MessageType = "request"
StartedMessage MessageType = "started"
LoadedMessage MessageType = "loaded"
)
type Delay struct {
Name string `json:"name"`
Value int32 `json:"value"`
}
type Process struct {
Id int64 `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type Message struct {
Type MessageType `json:"type"`
Data interface{} `json:"data"`
}
func (message *Message) Json() (string, error) {
data, err := json.Marshal(message)
return string(data), err
}
func SendMessage(message Message) {
s, err := message.Json()
if err != nil {
return
}
if handler, ok := messageHandlers[message.Type]; ok {
handler(s)
} else {
sendToPort(s)
}
}
var messageHandlers = map[MessageType]func(string) bool{
ProtectMessage: sendToServicePort,
ProcessMessage: sendToServicePort,
StartedMessage: conditionalSend,
LoadedMessage: conditionalSend,
}
func sendToPort(s string) bool {
return bridge.SendToPort(Port, s)
}
func sendToServicePort(s string) bool {
return bridge.SendToPort(ServicePort, s)
}
func conditionalSend(s string) bool {
isSuccess := sendToPort(s)
if !isSuccess {
return sendToServicePort(s)
}
return isSuccess
}

View File

@@ -1,4 +1,4 @@
//go:build android && cgo
//go:build android
package platform

View File

@@ -1,176 +0,0 @@
//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
}
}

81
core/process.go Normal file
View File

@@ -0,0 +1,81 @@
//go:build android
package main
import "C"
import (
"encoding/json"
"errors"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"sync"
"sync/atomic"
"time"
)
type ProcessMap struct {
m sync.Map
}
func (cm *ProcessMap) Store(key int64, value string) {
cm.m.Store(key, value)
}
func (cm *ProcessMap) Load(key int64) (string, bool) {
value, ok := cm.m.Load(key)
if !ok || value == nil {
return "", false
}
return value.(string), true
}
var counter int64 = 0
var processMap ProcessMap
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := atomic.AddInt64(&counter, 1)
timeout := time.After(200 * time.Millisecond)
SendMessage(Message{
Type: ProcessMessage,
Data: Process{
Id: id,
Metadata: metadata,
},
})
for {
select {
case <-timeout:
return "", errors.New("package resolver timeout")
default:
value, exists := processMap.Load(id)
if exists {
return value, nil
}
time.Sleep(20 * time.Millisecond)
}
}
}
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
go func() {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(paramsString), processMapItem)
if err == nil {
processMap.Store(processMapItem.Id, processMapItem.Value)
}
}()
}

View File

@@ -1,72 +0,0 @@
//go:build !cgo
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"strconv"
)
var conn net.Conn
func sendMessage(message Message) {
res, err := message.Json()
if err != nil {
return
}
send(Action{
Method: messageMethod,
}.getResult(res))
}
func send(data []byte) {
if conn == nil {
return
}
_, _ = conn.Write(append(data, []byte("\n")...))
}
func startServer(arg string) {
_, err := strconv.Atoi(arg)
if err != nil {
conn, err = net.Dial("unix", arg)
} else {
conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", arg))
}
if err != nil {
panic(err.Error())
}
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
reader := bufio.NewReader(conn)
for {
data, err := reader.ReadString('\n')
if err != nil {
return
}
var action = &Action{}
err = json.Unmarshal([]byte(data), action)
if err != nil {
return
}
go handleAction(action, func(data interface{}) {
send(action.getResult(data))
})
}
}
func nextHandle(action *Action, result func(data interface{})) bool {
return false
}

46
core/state.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import "C"
import (
"core/state"
"encoding/json"
"fmt"
)
//export getCurrentProfileName
func getCurrentProfileName() *C.char {
if state.CurrentState == nil {
return C.CString("")
}
return C.CString(state.CurrentState.CurrentProfileName)
}
//export getAndroidVpnOptions
func getAndroidVpnOptions() *C.char {
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: state.CurrentRawConfig.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
BypassDomain: state.CurrentState.BypassDomain,
DnsServerAddress: state.GetDnsServerAddress(),
}
data, err := json.Marshal(options)
if err != nil {
fmt.Println("Error:", err)
return C.CString("")
}
return C.CString(string(data))
}
//export setState
func setState(s *C.char) {
paramsString := C.GoString(s)
err := json.Unmarshal([]byte(paramsString), state.CurrentState)
if err != nil {
return
}
}

View File

@@ -1,11 +1,13 @@
package state
import "net/netip"
import "github.com/metacubex/mihomo/config"
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
var CurrentRawConfig = config.DefaultRawConfig()
type AndroidVpnOptions struct {
Enable bool `json:"enable"`
Port int `json:"port"`
@@ -13,17 +15,16 @@ type AndroidVpnOptions struct {
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
RouteAddress []netip.Prefix `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
}
type AccessControl struct {
Enable bool `json:"enable"`
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidVpnRawOptions struct {
@@ -32,22 +33,19 @@ type AndroidVpnRawOptions struct {
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
type State struct {
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
CurrentProfileName string `json:"current-profile-name"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
BypassDomain []string `json:"bypass-domain"`
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
OnlyProxy bool `json:"onlyProxy"`
}
var CurrentState = &State{
OnlyStatisticsProxy: false,
CurrentProfileName: "",
}
var CurrentState = &State{}
func GetIpv6Address() string {
if CurrentState.VpnProps.Ipv6 {
if CurrentState.Ipv6 {
return DefaultIpv6Address
} else {
return ""
@@ -55,5 +53,7 @@ func GetIpv6Address() string {
}
func GetDnsServerAddress() string {
//prefix, _ := netip.ParsePrefix(DefaultIpv4Address)
//return prefix.Addr().String()
return DefaultDnsAddress
}

157
core/tun.go Normal file
View File

@@ -0,0 +1,157 @@
//go:build android
package main
import "C"
import (
"core/platform"
t "core/tun"
"errors"
"github.com/metacubex/mihomo/listener/sing_tun"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
)
var tunLock sync.Mutex
var runTime *time.Time
type FdMap struct {
m sync.Map
}
func (cm *FdMap) Store(key int64) {
cm.m.Store(key, struct{}{})
}
func (cm *FdMap) Load(key int64) bool {
_, ok := cm.m.Load(key)
return ok
}
var (
tunListener *sing_tun.Listener
fdMap FdMap
fdCounter int64 = 0
)
//export startTUN
func startTUN(fd C.int, port C.longlong) {
i := int64(port)
ServicePort = i
if fd == 0 {
tunLock.Lock()
defer tunLock.Unlock()
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
return
}
initSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
f := int(fd)
tunListener, _ = t.Start(f)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
SendMessage(Message{
Type: StartedMessage,
Data: strconv.FormatInt(runTime.UnixMilli(), 10),
})
}()
}
//export getRunTime
func getRunTime() *C.char {
if runTime == nil {
return C.CString("")
}
return C.CString(strconv.FormatInt(runTime.UnixMilli(), 10))
}
//export stopTun
func stopTun() {
removeSocketHook()
go func() {
tunLock.Lock()
defer tunLock.Unlock()
runTime = nil
if tunListener != nil {
_ = tunListener.Close()
}
}()
}
var errBlocked = errors.New("blocked")
//export setFdMap
func setFdMap(fd C.long) {
fdInt := int64(fd)
go func() {
fdMap.Store(fdInt)
}()
}
type Fd struct {
Id int64 `json:"id"`
Value int64 `json:"value"`
}
func markSocket(fd Fd) {
SendMessage(Message{
Type: ProtectMessage,
Data: fd,
})
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
fdInt := int64(fd)
timeout := time.After(500 * time.Millisecond)
id := atomic.AddInt64(&fdCounter, 1)
markSocket(Fd{
Id: id,
Value: fdInt,
})
for {
select {
case <-timeout:
return
default:
exists := fdMap.Load(id)
if exists {
return
}
time.Sleep(20 * time.Millisecond)
}
}
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

View File

@@ -1,11 +1,10 @@
//go:build android && cgo
//go:build android
package tun
import "C"
import (
"core/state"
"github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
@@ -24,7 +23,7 @@ type Props struct {
Dns6 string `json:"dns6"`
}
func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener, error) {
func Start(fd int) (*sing_tun.Listener, error) {
var prefix4 []netip.Prefix
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
if err != nil {
@@ -33,7 +32,7 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
}
prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix
if state.CurrentState.VpnProps.Ipv6 {
if state.CurrentState.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
if err != nil {
log.Errorln("startTUN error:", err)
@@ -47,8 +46,8 @@ func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener,
options := LC.Tun{
Enable: true,
Device: device,
Stack: stack,
Device: state.CurrentRawConfig.Tun.Device,
Stack: state.CurrentRawConfig.Tun.Stack,
DNSHijack: dnsHijack,
AutoRoute: false,
AutoDetectInterface: false,

View File

@@ -1,59 +1,105 @@
import 'dart:async';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:animations/animations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/manager/hotkey_manager.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:provider/provider.dart';
import 'controller.dart';
import 'models/models.dart';
import 'pages/pages.dart';
class Application extends ConsumerStatefulWidget {
runAppWithPreferences(
Widget child, {
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<ClashConfig>(
create: (_) => clashConfig,
),
ChangeNotifierProvider<Config>(
create: (_) => config,
),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => AppFlowingState(),
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState,
update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode;
appState?.selectedMap = config.currentSelectedMap;
return appState!;
},
)
],
child: child,
));
}
class Application extends StatefulWidget {
const Application({
super.key,
});
@override
ConsumerState<Application> createState() => ApplicationState();
State<Application> createState() => ApplicationState();
}
class ApplicationState extends ConsumerState<Application> {
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? timer;
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: CommonPageTransitionsBuilder(),
TargetPlatform.windows: CommonPageTransitionsBuilder(),
TargetPlatform.linux: CommonPageTransitionsBuilder(),
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
TargetPlatform.android: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.windows: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.linux: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
TargetPlatform.macOS: SharedAxisPageTransitionsBuilder(
transitionType: SharedAxisTransitionType.horizontal,
),
},
);
ColorScheme _getAppColorScheme({
required Brightness brightness,
int? primaryColor,
required SystemColorSchemes systemColorSchemes,
}) {
return ref.read(genColorSchemeProvider(brightness));
if (primaryColor != null) {
return ColorScheme.fromSeed(
seedColor: Color(primaryColor),
brightness: brightness,
);
} else {
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
}
}
@override
void initState() {
super.initState();
_autoUpdateGroupTask();
_autoUpdateProfilesTask();
globalState.appController = AppController(context, ref);
_initTimer();
globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) {
globalState.appController = AppController(currentContext, ref);
globalState.appController = AppController(currentContext);
}
await globalState.appController.init();
globalState.appController.initLink();
@@ -61,29 +107,29 @@ class ApplicationState extends ConsumerState<Application> {
});
}
_autoUpdateGroupTask() {
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
_initTimer() {
_cancelTimer();
timer = Timer.periodic(const Duration(milliseconds: 20000), (_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupsDebounce();
_autoUpdateGroupTask();
globalState.appController.updateGroupDebounce();
});
});
}
_autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask();
});
_cancelTimer() {
if (timer != null) {
timer?.cancel();
timer = null;
}
}
_buildPlatformState(Widget child) {
_buildApp(Widget app) {
if (system.isDesktop) {
return WindowManager(
child: TrayManager(
child: HotKeyManager(
child: ProxyManager(
child: child,
child: app,
),
),
),
@@ -91,94 +137,105 @@ class ApplicationState extends ConsumerState<Application> {
}
return AndroidManager(
child: TileManager(
child: child,
child: app,
),
);
}
_buildState(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
onConnectivityChanged: () {
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
child: child,
),
),
);
}
_buildPlatformApp(Widget child) {
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: child,
child: page,
);
}
return VpnManager(
child: child,
child: page,
);
}
_buildApp(Widget child) {
return MessageManager(
child: ThemeManager(
child: child,
),
_updateSystemColorSchemes(
ColorScheme? lightDynamic,
ColorScheme? darkDynamic,
) {
systemColorSchemes = SystemColorSchemes(
lightColorScheme: lightDynamic,
darkColorScheme: darkDynamic,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateSystemColorSchemes(systemColorSchemes);
});
}
@override
Widget build(context) {
return _buildPlatformState(
_buildState(
Consumer(
builder: (_, ref, child) {
final locale =
ref.watch(appSettingProvider.select((state) => state.locale));
final themeProps = ref.watch(themeSettingProvider);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return AppEnvManager(
child: _buildPlatformApp(
_buildApp(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(),
return _buildApp(
AppStateManager(
child: ClashManager(
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.appSetting.locale,
themeMode: config.themeProps.themeMode,
primaryColor: config.themeProps.primaryColor,
prueBlack: config.themeProps.prueBlack,
fontFamily: config.themeProps.fontFamily,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return _buildPage(child!);
},
);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales:
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
fontFamily: state.fontFamily.value,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
).toPrueBlack(state.prueBlack),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
),
);
@@ -187,11 +244,8 @@ class ApplicationState extends ConsumerState<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel();
await clashCore.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();
super.dispose();
_cancelTimer();
}
}

View File

@@ -1,4 +1,3 @@
export 'core.dart';
export 'lib.dart';
export 'message.dart';
export 'service.dart';
export 'message.dart';

View File

@@ -1,27 +1,41 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'generated/clash_ffi.dart';
class ClashCore {
static ClashCore? _instance;
late ClashHandlerInterface clashInterface;
static final receiver = ReceivePort();
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
DynamicLibrary _getClashLib() {
if (Platform.isWindows) {
return DynamicLibrary.open("libclash.dll");
}
if (Platform.isMacOS) {
return DynamicLibrary.open("libclash.dylib");
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open("libclash.so");
}
throw "Platform is not supported";
}
ClashCore._internal() {
if (Platform.isAndroid) {
clashInterface = clashLib!;
} else {
clashInterface = clashService!;
}
lib = _getClashLib();
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashCore() {
@@ -29,72 +43,67 @@ class ClashCore {
return _instance!;
}
Future<bool> preload() {
return clashInterface.preload();
bool init(String homeDir) {
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
}
static Future<void> initGeo() async {
final homePath = await appPath.homeDirPath;
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
shutdown() {
clashFFI.shutdownClash();
lib.close();
}
bool get isInit => clashFFI.getIsInit() == 1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
} catch (e) {
exit(0);
}
});
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
}
Future<bool> init() async {
await initGeo();
final homeDirPath = await appPath.homeDirPath;
return await clashInterface.init(
InitParams(
homeDir: homeDirPath,
version: globalState.appState.version,
),
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.updateConfig(
paramsChar,
receiver.sendPort.nativePort,
);
malloc.free(paramsChar);
return completer.future;
}
initMessage() {
clashFFI.initMessage(
receiver.sendPort.nativePort,
);
}
Future<bool> setState(CoreState state) async {
return await clashInterface.setState(state);
}
shutdown() async {
await clashInterface.shutdown();
}
FutureOr<bool> get isInit => clashInterface.isInit;
FutureOr<String> validateConfig(String data) {
return clashInterface.validateConfig(data);
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies();
Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(proxiesRaw);
return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
@@ -124,149 +133,256 @@ class ClashCore {
});
}
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
return await clashInterface.changeProxy(changeProxyParams);
}
Future<List<Connection>> getConnections() async {
final res = await clashInterface.getConnections();
final connectionsData = json.decode(res) as Map;
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
clashInterface.closeConnection(id);
}
closeConnections() {
clashInterface.closeConnections();
}
Future<List<ExternalProvider>> getExternalProviders() async {
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
await clashInterface.getExternalProviders();
if (externalProvidersRawString.isEmpty) {
return [];
}
return Isolate.run<List<ExternalProvider>>(
() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
},
externalProvidersRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProvidersRaw);
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
ExternalProvider? getExternalProvider(String externalProviderName) {
final externalProviderNameChar =
externalProviderName.toNativeUtf8().cast<Char>();
final externalProviderRaw =
clashFFI.getExternalProvider(externalProviderNameChar);
malloc.free(externalProviderNameChar);
final externalProviderRawString =
externalProviderRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(externalProviderRaw);
if (externalProviderRawString.isEmpty) return null;
return ExternalProvider.fromJson(json.decode(externalProviderRawString));
}
Future<String> updateGeoData({
required String geoType,
required String geoName,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final geoTypeChar = geoType.toNativeUtf8().cast<Char>();
final geoNameChar = geoName.toNativeUtf8().cast<Char>();
clashFFI.updateGeoData(
geoTypeChar,
geoNameChar,
receiver.sendPort.nativePort,
);
}
Future<ExternalProvider?> getExternalProvider(
String externalProviderName) async {
final externalProvidersRawString =
await clashInterface.getExternalProvider(externalProviderName);
if (externalProvidersRawString.isEmpty) {
return null;
}
if (externalProvidersRawString.isEmpty) {
return null;
}
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
}
Future<String> updateGeoData(UpdateGeoDataParams params) {
return clashInterface.updateGeoData(params);
malloc.free(geoTypeChar);
malloc.free(geoNameChar);
return completer.future;
}
Future<String> sideLoadExternalProvider({
required String providerName,
required String data,
}) {
return clashInterface.sideLoadExternalProvider(
providerName: providerName, data: data);
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
final dataChar = data.toNativeUtf8().cast<Char>();
clashFFI.sideLoadExternalProvider(
providerNameChar,
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
malloc.free(dataChar);
return completer.future;
}
Future<String> updateExternalProvider({
required String providerName,
}) async {
return clashInterface.updateExternalProvider(providerName);
}
startListener() async {
await clashInterface.startListener();
}
stopListener() async {
await clashInterface.stopListener();
}
Future<Delay> getDelay(String url, String proxyName) async {
final data = await clashInterface.asyncTestDelay(url, proxyName);
return Delay.fromJson(json.decode(data));
}
Future<Traffic> getTraffic() async {
final trafficString = await clashInterface.getTraffic();
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString));
}
Future<IpInfo?> getCountryCode(String ip) async {
final countryCode = await clashInterface.getCountryCode(ip);
if (countryCode.isEmpty) {
return null;
}
return IpInfo(
ip: ip,
countryCode: countryCode,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
clashFFI.updateExternalProvider(
providerNameChar,
receiver.sendPort.nativePort,
);
malloc.free(providerNameChar);
return completer.future;
}
Future<Traffic> getTotalTraffic() async {
final totalTrafficString = await clashInterface.getTotalTraffic();
if (totalTrafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(totalTrafficString));
changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
final paramsChar = params.toNativeUtf8().cast<Char>();
clashFFI.changeProxy(paramsChar);
malloc.free(paramsChar);
}
Future<int> getMemory() async {
final value = await clashInterface.getMemory();
if (value.isEmpty) {
return 0;
}
return int.parse(value);
start() {
clashFFI.start();
}
Future<ClashConfigSnippet?> getProfile(String id) async {
final res = await clashInterface.getProfile(id);
if (res.isEmpty) {
return null;
}
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
stop() {
clashFFI.stop();
}
resetTraffic() {
clashInterface.resetTraffic();
Future<Delay> getDelay(String proxyName) {
final delayParams = {
"proxy-name": proxyName,
"timeout": httpTimeoutDuration.inMilliseconds,
};
final completer = Completer<Delay>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(Delay.fromJson(json.decode(message)));
receiver.close();
}
});
final delayParamsChar =
json.encode(delayParams).toNativeUtf8().cast<Char>();
clashFFI.asyncTestDelay(
delayParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(delayParamsChar);
return completer.future;
}
startLog() {
clashInterface.startLog();
clearEffect(String profileId) {
final profileIdChar = profileId.toNativeUtf8().cast<Char>();
clashFFI.clearEffect(profileIdChar);
malloc.free(profileIdChar);
}
VersionInfo getVersionInfo() {
final versionInfoRaw = clashFFI.getVersionInfo();
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(versionInfoRaw);
return VersionInfo.fromJson(versionInfo);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
Traffic getTotalTraffic() {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficMap = json.decode(trafficRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(trafficRaw);
return Traffic.fromMap(trafficMap);
}
void resetTraffic() {
clashFFI.resetTraffic();
}
void startLog() {
clashFFI.startLog();
}
stopLog() {
clashInterface.stopLog();
clashFFI.stopLog();
}
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
}
updateDns(String dns) {
if (!Platform.isAndroid) return;
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
requestGc() {
clashInterface.forceGc();
clashFFI.forceGc();
}
destroy() async {
await clashInterface.destroy();
void stopTun() {
clashFFI.stopTun();
}
void setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
void setFdMap(int fd) {
clashFFI.setFdMap(fd);
}
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));
}
List<Connection> getConnections() {
final connectionsDataRaw = clashFFI.getConnections();
final connectionsData =
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
clashFFI.freeCString(connectionsDataRaw);
final connectionsRaw = connectionsData['connections'] as List? ?? [];
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
}
closeConnections() {
clashFFI.closeConnections();
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,311 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:ffi/ffi.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/service.dart';
import 'package:fl_clash/state.dart';
import 'generated/clash_ffi.dart';
import 'interface.dart';
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
static ClashLib? _instance;
Completer<bool> _canSendCompleter = Completer();
SendPort? sendPort;
final receiverPort = ReceivePort();
ClashLib._internal() {
_initService();
}
@override
preload() {
return _canSendCompleter.future;
}
_initService() async {
await service?.destroy();
_registerMainPort(receiverPort.sendPort);
receiverPort.listen((message) {
if (message is SendPort) {
if (_canSendCompleter.isCompleted) {
sendPort = null;
_canSendCompleter = Completer();
}
sendPort = message;
_canSendCompleter.complete(true);
} else {
handleResult(
ActionResult.fromJson(json.decode(
message,
)),
);
}
});
await service?.init();
}
_registerMainPort(SendPort sendPort) {
IsolateNameServer.removePortNameMapping(mainIsolate);
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
}
factory ClashLib() {
_instance ??= ClashLib._internal();
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
destroy() async {
await service?.destroy();
return true;
}
@override
reStart() {
_initService();
}
@override
Future<bool> shutdown() async {
await super.shutdown();
destroy();
return true;
}
@override
sendMessage(String message) async {
await _canSendCompleter.future;
sendPort?.send(message);
}
@override
Future<bool> setFdMap(int fd) {
return invoke<bool>(
method: ActionMethod.setFdMap,
data: json.encode(fd),
);
}
@override
Future<bool> setProcessMap(item) {
return invoke<bool>(
method: ActionMethod.setProcessMap,
data: item,
);
}
// @override
// Future<bool> stopTun() {
// return invoke<bool>(
// method: ActionMethod.stopTun,
// );
// }
@override
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
final res = await invoke<String>(
method: ActionMethod.getAndroidVpnOptions,
);
if (res.isEmpty) {
return null;
}
return AndroidVpnOptions.fromJson(json.decode(res));
}
@override
Future<bool> updateDns(String value) {
return invoke<bool>(
method: ActionMethod.updateDns,
data: value,
);
}
@override
Future<DateTime?> getRunTime() async {
final runTimeString = await invoke<String>(
method: ActionMethod.getRunTime,
);
if (runTimeString.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
@override
Future<String> getCurrentProfileName() {
return invoke<String>(
method: ActionMethod.getCurrentProfileName,
);
}
}
class ClashLibHandler {
static ClashLibHandler? _instance;
late final ClashFFI clashFFI;
late final DynamicLibrary lib;
ClashLibHandler._internal() {
lib = DynamicLibrary.open("libclash.so");
clashFFI = ClashFFI(lib);
clashFFI.initNativeApiBridge(
NativeApi.initializeApiDLData,
);
}
factory ClashLibHandler() {
_instance ??= ClashLibHandler._internal();
return _instance!;
}
Future<String> invokeAction(String actionParams) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
clashFFI.invokeAction(
actionParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(actionParamsChar);
return completer.future;
}
attachMessagePort(int messagePort) {
clashFFI.attachMessagePort(
messagePort,
);
}
updateDns(String dns) {
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
clashFFI.setState(stateChar);
malloc.free(stateChar);
}
String getCurrentProfileName() {
final currentProfileRaw = clashFFI.getCurrentProfileName();
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(currentProfileRaw);
return currentProfile;
}
AndroidVpnOptions getAndroidVpnOptions() {
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
clashFFI.freeCString(vpnOptionsRaw);
return AndroidVpnOptions.fromJson(vpnOptions);
}
Traffic getTraffic() {
final trafficRaw = clashFFI.getTraffic();
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString));
}
Traffic getTotalTraffic(bool value) {
final trafficRaw = clashFFI.getTotalTraffic();
final trafficString = trafficRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(trafficRaw);
if (trafficString.isEmpty) {
return Traffic();
}
return Traffic.fromMap(json.decode(trafficString));
}
startListener() async {
clashFFI.startListener();
return true;
}
stopListener() async {
clashFFI.stopListener();
return true;
}
Future<String> quickStart(
InitParams initParams,
UpdateConfigParams updateConfigParams,
CoreState state,
) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final params = json.encode(updateConfigParams);
final initValue = json.encode(initParams);
final stateParams = json.encode(state);
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart(
initParamsChar,
paramsChar,
stateParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(initParamsChar);
malloc.free(paramsChar);
malloc.free(stateParamsChar);
return completer.future;
}
DateTime? getRunTime() {
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 =>
Platform.isAndroid && !globalState.isService ? ClashLib() : null;

View File

@@ -5,34 +5,38 @@ import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart';
import 'core.dart';
class ClashMessage {
final controller = StreamController<String>();
StreamSubscription? subscription;
ClashMessage._() {
controller.stream.listen(
(message) {
if(message.isEmpty){
return;
if (subscription != null) {
subscription!.cancel();
subscription = null;
}
subscription = ClashCore.receiver.listen((message) {
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.started:
listener.onStarted(m.data);
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
final m = AppMessage.fromJson(json.decode(message));
for (final AppMessageListener listener in _listeners) {
switch (m.type) {
case AppMessageType.log:
listener.onLog(Log.fromJson(m.data));
break;
case AppMessageType.delay:
listener.onDelay(Delay.fromJson(m.data));
break;
case AppMessageType.request:
listener.onRequest(Connection.fromJson(m.data));
break;
case AppMessageType.loaded:
listener.onLoaded(m.data);
break;
}
}
},
);
}
});
}
static final ClashMessage instance = ClashMessage._();

View File

@@ -1,161 +1,54 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/core.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
class ClashService extends ClashHandlerInterface {
static ClashService? _instance;
import 'core.dart';
Completer<ServerSocket> serverCompleter = Completer();
Completer<Socket> socketCompleter = Completer();
bool isStarting = false;
Process? process;
factory ClashService() {
_instance ??= ClashService._internal();
return _instance!;
}
ClashService._internal() {
_initServer();
reStart();
}
_initServer() async {
runZonedGuarded(() async {
final address = !Platform.isWindows
? InternetAddress(
unixSocketPath,
type: InternetAddressType.unix,
)
: InternetAddress(
localhost,
type: InternetAddressType.IPv4,
);
await _deleteSocketFile();
final server = await ServerSocket.bind(
address,
0,
shared: true,
);
serverCompleter.complete(server);
await for (final socket in server) {
await _destroySocket();
socketCompleter.complete(socket);
socket
.transform(
StreamTransformer<Uint8List, String>.fromHandlers(
handleData: (Uint8List data, EventSink<String> sink) {
sink.add(utf8.decode(data, allowMalformed: true));
},
),
)
.transform(LineSplitter())
.listen(
(data) {
handleResult(
ActionResult.fromJson(
json.decode(data.trim()),
),
);
},
);
}
}, (error, stack) {
commonPrint.log(error.toString());
if (error is SocketException) {
globalState.showNotifier(error.toString());
// globalState.appController.restartCore();
}
});
}
@override
reStart() async {
if (isStarting == true) {
return;
}
isStarting = true;
socketCompleter = Completer();
if (process != null) {
await shutdown();
}
final serverSocket = await serverCompleter.future;
final arg = Platform.isWindows
? "${serverSocket.port}"
: serverSocket.address.address;
bool isSuccess = false;
if (Platform.isWindows && await system.checkIsAdmin()) {
isSuccess = await request.startCoreByHelper(arg);
}
if (isSuccess) {
return;
}
process = await Process.start(
appPath.corePath,
[
arg,
],
);
process!.stdout.listen((_) {});
isStarting = false;
}
@override
destroy() async {
final server = await serverCompleter.future;
await server.close();
await _deleteSocketFile();
return true;
}
@override
sendMessage(String message) async {
final socket = await socketCompleter.future;
socket.writeln(message);
}
_deleteSocketFile() async {
if (!Platform.isWindows) {
final file = File(unixSocketPath);
if (await file.exists()) {
await file.delete();
class ClashService {
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoIpFileName,
geoSiteFileName,
asnFileName,
];
try {
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
} catch (e) {
debugPrint("$e");
exit(0);
}
}
_destroySocket() async {
if (socketCompleter.isCompleted) {
final lastSocket = await socketCompleter.future;
await lastSocket.close();
socketCompleter = Completer();
}
}
@override
shutdown() async {
if (Platform.isWindows) {
await request.stopCoreByHelper();
}
await _destroySocket();
process?.kill();
process = null;
return true;
}
@override
Future<bool> preload() async {
await serverCompleter.future;
return true;
Future<bool> init({
required ClashConfig clashConfig,
required Config config,
}) async {
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;
}
}
final clashService = system.isDesktop ? ClashService() : null;
final clashService = ClashService();

View File

@@ -1,126 +1,36 @@
import 'dart:math';
import 'package:flutter/material.dart';
extension ColorExtension on Color {
Color get opacity80 {
return withAlpha(204);
toLight() {
return withOpacity(0.6);
}
Color get opacity60 {
return withAlpha(153);
toLighter() {
return withOpacity(0.4);
}
Color get opacity50 {
return withAlpha(128);
toSoft() {
return withOpacity(0.12);
}
Color get opacity38 {
return withAlpha(97);
toLittle() {
return withOpacity(0.03);
}
Color get opacity30 {
return withAlpha(77);
}
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(
BuildContext context, {
double factor = 0.1,
}) {
final brightness = Theme.of(context).brightness;
return Color.lerp(
this,
brightness == Brightness.dark ? Colors.white : Colors.black,
factor,
)!;
}
Color blendLighten(
BuildContext context, {
double factor = 0.1,
}) {
final brightness = Theme.of(context).brightness;
return Color.lerp(
this,
brightness == Brightness.dark ? Colors.black : Colors.white,
factor,
)!;
Color darken([double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(this);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
}
extension ColorSchemeExtension on ColorScheme {
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith(
surface: Colors.black,
surfaceContainer: surfaceContainer.darken(
5,
),
background: Colors.black,
surfaceContainer: surfaceContainer.darken(0.05),
)
: this;
}

View File

@@ -1,39 +1,33 @@
export 'android.dart';
export 'app_localizations.dart';
export 'color.dart';
export 'constant.dart';
export 'context.dart';
export 'datetime.dart';
export 'function.dart';
export 'future.dart';
export 'http.dart';
export 'icons.dart';
export 'iterable.dart';
export 'keyboard.dart';
export 'launch.dart';
export 'link.dart';
export 'list.dart';
export 'lock.dart';
export 'measure.dart';
export 'navigation.dart';
export 'navigator.dart';
export 'network.dart';
export 'num.dart';
export 'utils.dart';
export 'package.dart';
export 'path.dart';
export 'picker.dart';
export 'preferences.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'request.dart';
export 'scroll.dart';
export 'string.dart';
export 'system.dart';
export 'text.dart';
export 'tray.dart';
export 'preferences.dart';
export 'constant.dart';
export 'proxy.dart';
export 'other.dart';
export 'num.dart';
export 'navigation.dart';
export 'window.dart';
export 'system.dart';
export 'picker.dart';
export 'android.dart';
export 'launch.dart';
export 'protocol.dart';
export 'datetime.dart';
export 'context.dart';
export 'link.dart';
export 'text.dart';
export 'color.dart';
export 'list.dart';
export 'string.dart';
export 'app_localizations.dart';
export 'function.dart';
export 'package.dart';
export 'measure.dart';
export 'windows.dart';
export 'render.dart';
export 'mixin.dart';
export 'print.dart';
export 'iterable.dart';
export 'scroll.dart';
export 'icons.dart';
export 'http.dart';
export 'keyboard.dart';
export 'network.dart';
export 'navigator.dart';

View File

@@ -1,33 +1,18 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta";
const browserUa =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const maxTextScale = 1.4;
const minTextScale = 0.8;
final baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16.ap,
horizontal: 16.ap,
);
final defaultTextScaleFactor = WidgetsBinding.instance.platformDispatcher.textScaleFactor;
const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100);
const commonDuration = Duration(milliseconds: 300);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
@@ -36,19 +21,32 @@ const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop
? !Platform.isMacOS
? 40
: 28
: 26
: 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat",
"geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
};
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";
const configKey = "config";
const listItemPadding = EdgeInsets.symmetric(horizontal: 16);
const double dialogCommonWidth = 300;
const repository = "chen08209/FlClash";
const defaultExternalController = "127.0.0.1:9090";
const maxMobileWidth = 600;
const maxLaptopWidth = 840;
const geodataLoaderMemconservative = "memconservative";
const geodataLoaderStandard = "standard";
const defaultTestUrl = "https://www.gstatic.com/generate_204";
final commonFilter = ImageFilter.blur(
final filter = ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
tileMode: TileMode.mirror,
@@ -57,7 +55,6 @@ final commonFilter = ImageFilter.blur(
const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>();
const intListEquality = ListEquality<int>();
const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>();
@@ -66,7 +63,7 @@ const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>();
const delayMapEquality = MapEquality<String, Map<String, int?>>();
const stringAndIntQMapEquality = MapEquality<String, int?>();
const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
@@ -76,22 +73,4 @@ const viewModeColumnsMap = {
ViewMode.desktop: [4, 3],
};
const defaultPrimaryColor = 0xFF795548;
double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0).ap;
}
final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate";
const defaultPrimaryColors = [
defaultPrimaryColor,
0xFF03A9F4,
0xFFFFFF00,
0XFFBBC9CC,
0XFFABD397,
0XFFD8C0C3,
0XFF665390,
];
const defaultPrimaryColor = Colors.brown;

View File

@@ -1,47 +1,13 @@
import 'package:fl_clash/manager/message_manager.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
extension BuildContextExtension on BuildContext {
CommonScaffoldState? get commonScaffoldState {
return findAncestorStateOfType<CommonScaffoldState>();
}
showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
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;
}
@@ -52,23 +18,4 @@ extension BuildContextExtension on BuildContext {
ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme;
T? findLastStateOfType<T extends State>() {
T? state;
visitor(Element element) {
if (!element.mounted) {
return;
}
if (element is StatefulElement) {
if (element.state is T) {
state = element.state as T;
}
}
element.visitChildren(visitor);
}
visitor(this as Element);
return state;
}
}

View File

@@ -24,8 +24,8 @@ class DAVClient {
},
);
client.setConnectTimeout(8000);
client.setSendTimeout(60000);
client.setReceiveTimeout(60000);
client.setSendTimeout(8000);
client.setReceiveTimeout(8000);
pingCompleter.complete(_ping());
}

View File

@@ -1,87 +1,26 @@
import 'dart:async';
class Debouncer {
final Map<dynamic, Timer?> _operations = {};
final Duration delay;
Timer? _timer;
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
Debouncer({required this.delay});
void call(Function action, List<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
_timer?.cancel();
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
}
}
Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
Timer? timer;
return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
if (timer != null) {
timer.cancel();
timer!.cancel();
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
class Throttler {
final Map<dynamic, Timer?> _operations = {};
call(
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
return true;
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
_operations[tag] = null;
}
}
Future<T> retry<T>({
required Future<T> Function() task,
int maxAttempts = 3,
required bool Function(T res) retryIf,
Duration delay = Duration.zero,
}) async {
int attempts = 0;
while (attempts < maxAttempts) {
final res = await task();
if (!retryIf(res) || attempts >= maxAttempts) {
return res;
}
attempts++;
}
throw "unknown error";
}
final debouncer = Debouncer();
final throttler = Throttler();
timer = Timer(Duration(milliseconds: milliseconds), () async {
await Function.apply(func, args ?? [], namedArgs);
});
};
}

View File

@@ -1,44 +0,0 @@
import 'dart:async';
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
extension CompleterExt<T> on Completer<T> {
safeFuture({
Duration? timeout,
VoidCallback? onLast,
FutureOr<T> Function()? onTimeout,
required String functionName,
}) {
final realTimeout = timeout ?? const Duration(seconds: 30);
Timer(realTimeout + commonDuration, () {
if (onLast != null) {
onLast();
}
});
return future.withTimeout(
timeout: realTimeout,
functionName: functionName,
onTimeout: onTimeout,
);
}
}
extension FutureExt<T> on Future<T> {
Future<T> withTimeout({
required Duration timeout,
required String functionName,
FutureOr<T> Function()? onTimeout,
}) {
return this.timeout(
timeout,
onTimeout: () async {
if (onTimeout != null) {
return onTimeout();
} else {
throw TimeoutException('$functionName timeout');
}
},
);
}
}

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart';
import 'system.dart';
import 'windows.dart';
class AutoLaunch {
static AutoLaunch? _instance;
@@ -26,16 +26,58 @@ class AutoLaunch {
return await launchAtStartup.isEnabled();
}
Future<bool> get windowsIsEnable async {
final res = await Process.run(
'schtasks',
['/Query', '/TN', appName, '/V', "/FO", "LIST"],
runInShell: true,
);
return res.stdout.toString().contains(Platform.resolvedExecutable);
}
Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable();
}
windowsDisable() async {
final res = await Process.run(
'schtasks',
[
'/Delete',
'/TN',
appName,
'/F',
],
runInShell: true,
);
return res.exitCode == 0;
}
Future<bool> windowsEnable() async {
await disable();
return await windows?.registerTask(appName) ?? false;
}
Future<bool> disable() async {
return await launchAtStartup.disable();
}
updateStatus(bool isAutoLaunch) async {
if(kDebugMode){
updateStatus(AutoLaunchState state) async {
final isAdminAutoLaunch = state.isAdminAutoLaunch;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isAdminAutoLaunch) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return;

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
class SingleInstanceLock {
static SingleInstanceLock? _instance;
RandomAccessFile? _accessFile;
SingleInstanceLock._internal();
factory SingleInstanceLock() {
_instance ??= SingleInstanceLock._internal();
return _instance!;
}
Future<bool> acquire() async {
try {
final lockFilePath = await appPath.lockFilePath;
final lockFile = File(lockFilePath);
await lockFile.create();
_accessFile = await lockFile.open(mode: FileMode.write);
await _accessFile?.lock();
return true;
} catch (_) {
return false;
}
}
}
final singleInstanceLock = SingleInstanceLock();

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