Compare commits

..

17 Commits

Author SHA1 Message Date
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
317 changed files with 52036 additions and 74113 deletions

View File

@@ -1,58 +0,0 @@
<div align=center>
[![Release Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/total?style=flat-square&logo=github)](https://img.shields.io/github/downloads/chen08209/FlClash/vVERSION/)
</div>
**Download based on your OS:**
<div align=left>
<table>
<thead align=left>
<tr>
<th>OS</th>
<th>Download</th>
</tr>
</thead>
<tbody align=left>
<tr>
<td>Android</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-arm64-v8a.apk"><img src="https://img.shields.io/badge/APK-ARMv8-168039.svg?logo=android"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-armeabi-v7a.apk"><img src="https://img.shields.io/badge/APK-ARMv7-45bf55.svg?logo=android"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-android-x86_64.apk"><img src="https://img.shields.io/badge/APK-x64-96ed89.svg?logo=android"></a>
</td>
</tr>
<tr>
<td>Windows</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64-setup.exe"><img src="https://img.shields.io/badge/Setup-x64-2d7d9a.svg?logo=windows"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-windows-amd64.zip"><img src="https://img.shields.io/badge/Portable-x64-67b7d1.svg?logo=windows"></a>
</td>
</tr>
<tr>
<td>macOS</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>
</td>
</tr>
<tr>
<td>Linux</td>
<td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.AppImage"><img src="https://img.shields.io/badge/AppImage-x64-f84e29.svg?logo=linux"> </a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/DebPackage-x64-FF9966.svg?logo=debian"> </a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-linux-amd64.deb"><img src="https://img.shields.io/badge/RpmPackage-x64-F1B42F.svg?logo=redhat"> </a>
</td>
</tr>
</tbody>
</table>
</div>
<div dir="ltr">
**List of all changes:** [ChangeLog](https://github.com/chen08209/FlClash/blob/main/CHANGELOG.md)
</div>

View File

@@ -1,245 +0,0 @@
name: build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- platform: android
os: ubuntu-latest
- platform: windows
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-latest
arch: amd64
- platform: macos
os: macos-13
arch: amd64
- platform: macos
os: macos-latest
arch: arm64
steps:
- 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: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
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: 'stable'
cache-dependency-path: |
core/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 3.24.5
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) }}
- name: Upload
uses: actions/upload-artifact@v4
with:
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
path: ./dist
overwrite: true
changelog:
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ !contains(github.ref, '+') }}
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: ${{ !contains(github.ref, '+') }}
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
needs: [ build ]
runs-on: ubuntu-latest
services:
telegram-bot-api:
image: aiogram/telegram-bot-api:latest
env:
TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
ports:
- 8081:8081
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download
uses: actions/download-artifact@v4
with:
path: ./dist/
pattern: artifact-*
merge-multiple: true
- name: Generate release.md
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
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
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> release.md
fi
echo "" >> release.md
fi
currentTag=$tag
done
- name: Push to telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
python -m pip install --upgrade pip
pip install requests
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: Release
if: ${{ !contains(github.ref, '+') }}
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
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: ${{ !contains(github.ref, '+') }}
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: ./tmp/
destination-github-username: chen08209
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/

164
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,164 @@
name: build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- platform: android
os: ubuntu-latest
- platform: windows
os: windows-latest
arch: amd64
- platform: linux
os: ubuntu-latest
arch: amd64
- platform: macos
os: macos-13
arch: amd64
- platform: macos
os: macos-latest
arch: arm64
steps:
- 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: |
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: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/local.properties
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-file: 'core/go.mod'
cache-dependency-path: |
core/go.sum
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: 3.22.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) }}
- name: Upload
uses: actions/upload-artifact@v4
with:
name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }}
path: ./dist
retention-days: 1
overwrite: true
upload-release:
if: ${{ !contains(github.ref, '+') }}
permissions: write-all
needs: [ build ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download
uses: actions/download-artifact@v4
with:
path: ./dist/
pattern: artifact-*
merge-multiple: true
- name: Pre Release
run: |
pip install gitchangelog pystache mustache markdown
pre=$(curl --silent "https://api.github.com/repos/chen08209/FlClash/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")' || echo "")
if [ -z "pre" ]; then
echo "init" > release.md
else
current="${{ github.ref_name }}"
echo -e "\n\n<details markdown=1><summary>All changes from $current to the latest commit:</summary>\n\n" >> release.md
gitchangelog "${pre}.." >> release.md 2>&1 || echo "Error in gitchangelog"
echo -e "\n\n</details>" >> release.md
fi
- name: Release
uses: softprops/action-gh-release@v2
with:
files: ./dist/*
body_path: './release.md'
- name: Create Fdroid Source Dir
run: |
mkdir -p ./tmp
cp ./dist/*android-arm64-v8a* ./tmp/ || true
echo "Files copied successfully"
- name: Push to fdroid repo
uses: cpina/github-action-push-to-another-repository@v1.7.2
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: ./tmp/
destination-github-username: chen08209
destination-repository-name: FlClash-fdroid-repo
user-name: 'github-actions[bot]'
user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr
commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/

2
.gitignore vendored
View File

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

2
.gitmodules vendored
View File

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

View File

@@ -1,750 +0,0 @@
## 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
- Fix windows admin auto launch issues
- Add android vpn options
- Support proxies icon configuration
- Optimize android immersion display
- Fix some issues
- Optimize ip detection
- Support android vpn ipv6 inbound switch
- Support log export
- Optimize more details
- Fix android system dns issues
- Optimize dns default option
- Fix some issues
- Update readme
## v0.8.60
- Fix build error2
- Fix build error
- Support desktop hotkey
- Support android ipv6 inbound
- Support android system dns
- fix some bugs
## v0.8.59
- Fix delete profile error
## v0.8.58
- Fix submit error 2
- Fix submit error
- Optimize DNS strategy
- Fix the problem that the tray is not displayed in some cases
- Optimize tray
- Update core
- Fix some error
## v0.8.57
- Fix tun update issues
- Add DNS override
- Fixed some bugs
- Optimize more detail
- Add Hosts override
## v0.8.56
- fix android tip error
- fix windows auto launch error
## v0.8.55
- Fix windows tray issues
- Optimize windows logic
- Optimize app logic
- Support windows administrator auto launch
- Support android close vpn
## v0.8.53
- Change flutter version
- Support profiles sort
- Support windows country flags display
- Optimize proxies page and profiles page columns
## v0.8.52
- Update flutter version
- Update version
- Update timeout time
- Update access control page
- Fix bug
## v0.8.51
- Optimize provider page
- Optimize delay test
- Support local backup and recovery
- Fix android tile service issues
## v0.8.49
- Fix linux core build error
- Add proxy-only traffic statistics
- Update core
- Optimize more details
- Merge pull request #140 from txyyh/main
- 添加自建 F-Droid 仓库相关 workflow
- Rename readme fingerprint
- Rename workflow deploy repo name
- Add download guide to README
- Add push release files to fdroid-repo
## v0.8.48
- Optimize proxies page
- Fix ua issues
- Optimize more details
## v0.8.47
- Fix windows build error
## v0.8.46
- Update app icon
- Fix desktop backup error
- Optimize request ua
- Change android icon
- Optimize dashboard
## v0.8.44
- Remove request validate certificate
- Sync core
## v0.8.43
- Fix windows error
## v0.8.42
- Fix setup.dart error
- Fix android system proxy not effective
- Add macos arm64
## v0.8.41
- Optimize proxies page
- Support mouse drag scroll
- Adjust desktop ui
- Revert "Fix android vpn issues"
- This reverts commit 891977408e6938e2acd74e9b9adb959c48c79988.
## v0.8.40
- Fix android vpn issues
- Fix android vpn issues
- Rollback partial modification
## v0.8.39
- Fix the problem that ui can't be synchronized when android vpn is occupied by an external
- Override default socksPort,port
## v0.8.38
- Fix fab issues
## v0.8.37
- Update version
- Fix the problem that vpn cannot be started in some cases
- Fix the problem that geodata url does not take effect
## v0.8.36
- Update ua
- Fix change outbound mode without check ip issues
- Separate android ui and vpn
- Fix url validate issues 2
- Add android hidden from the recent task
- Add geoip file
- Support modify geoData URL
## v0.8.35
- Fix url validate issues
- Fix check ip performance problem
- Optimize resources page
## v0.8.34
- Add ua selector
- Support modify test url
- Optimize android proxy
- Fix the error that async proxy provider could not selected the proxy
## v0.8.33
- Fix android proxy error
- Fix submit error
- Add windows tun
- Optimize android proxy
- Optimize change profile
- Update application ua
- Optimize delay test
## v0.8.32
- Fix android repeated request notification issues
## v0.8.31
- Fix memory overflow issues
## v0.8.30
- Optimize proxies expansion panel 2
- Fix android scan qrcode error
## v0.8.29
- Optimize proxies expansion panel
- Fix text error
## v0.8.28
- Optimize proxy
- Optimize delayed sorting performance
- Add expansion panel proxies page
- Support to adjust the proxy card size
- Support to adjust proxies columns number
- Fix autoRun show issues
- Fix Android 10 issues
- Optimize ip show
## v0.8.26
- Add intranet IP display
- Add connections page
- Add search in connections, requests
- Add keyword search in connections, requests, logs
- Add basic viewing editing capabilities
- Optimize update profile
## v0.8.25
- Update version
- Fix the problem of excessive memory usage in traffic usage.
- Add lightBlue theme color
- Fix start unable to update profile issues
- Fix flashback caused by process
## v0.8.23
- Add build version
- Optimize quick start
- Update system default option
## v0.8.22
- Update build.yml
- Fix android vpn close issues
- Add requests page
- Fix checkUpdate dark mode style error
- Fix quickStart error open app
- Add memory proxies tab index
- Support hidden group
- Optimize logs
- Fix externalController hot load error
## v0.8.21
- Add tcp concurrent switch
- Add system proxy switch
- Add geodata loader switch
- Add external controller switch
- Add auto gc on trim memory
- Fix android notification error
## v0.8.20
- Fix ipv6 error
- Fix android udp direct error
- Add ipv6 switch
- Add access all selected button
- Remove android low version splash
## v0.8.19
- Update version
- Add allowBypass
- Fix Android only pick .text file issues
## v0.8.18
- Fix search issues
## v0.8.17
- Fix LoadBalance, Relay load error
- Fix build.yml4
- Fix build.yml3
- Fix build.yml2
- Fix build.yml
- Add search function at access control
- Fix the issues with the profile add button to cover the edit button
- Adapt LoadBalance and Relay
- Add arm
- Fix android notification icon error
## v0.8.16
- Add one-click update all profiles
- Add expire show
## v0.8.15
- Temp remove tun mode
- Remove macos in workflow
- Change go version
## v0.8.14
- Update Version
- Fix tun unable to open
## v0.8.13
- Optimize delay test2
- Optimize delay test
- Add check ip
- add check ip request
## 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 edit profile error
- Fix quickStart change proxy error
- Fix core version
## v0.8.10
- Fix core version
## v0.8.9
- Update file_picker
- Add resources page
- Optimize more detail
- Add access selected sorted
- Fix notification duplicate creation issue
- Fix AccessControl click issue
## v0.8.7
- Fix Workflow
- Fix Linux unable to open
- Update README.md 3
- Create LICENSE
- Update README.md 2
- Update README.md
- Optimize workFlow
## v0.8.6
- optimize checkUpdate
## v0.8.5
- Fix submit error
## v0.8.4
- add WebDAV
- add Auto check updates
- Optimize more details
- optimize delayTest
## v0.8.2
- upgrade flutter version
## v0.8.1
- Update kernel
- Add import profile via QR code image
## v0.8.0
- Add compatibility mode and adapt clash scheme.
## v0.7.14
- update Version
- Reconstruction application proxy logic
## v0.7.13
- Fix Tab destroy error
## v0.7.12
- Optimize repeat healthcheck
## v0.7.11
- Optimize Direct mode ui
## v0.7.10
- Optimize Healthcheck
- Remove proxies position animation, improve performance
- Add Telegram Link
- Update healthcheck policy
- New Check URLTest
- Fix the problem of invalid auto-selection
## v0.7.8
- New Async UpdateConfig
- add changeProfileDebounce
- Update Workflow
- Fix ChangeProfile block
- Fix Release Message Error
## v0.7.7
- Update Selector 2
## v0.7.6
- Update Version
- Fix Proxies Select Error
## v0.7.5
- Fix the problem that the proxy group is empty in global mode.
- Fix the problem that the proxy group is empty in global mode.
## v0.7.4
- Add ProxyProvider2
## v0.7.3
- Add ProxyProvider
- Update Version
- Update ProxyGroup Sort
- Fix Android quickStart VpnService some problems
## v0.7.1
- Update version
- Set Android notification low importance
- Fix the issue that VpnService can't be closed correctly in special cases
- Fix the problem that TileService is not destroyed correctly in some cases
- Adjust tab animation defaults
- Add Telegram in README_zh_CN.md
- Add Telegram
## v0.7.0
- update mobile_scanner
- Initial commit

View File

@@ -6,9 +6,13 @@
## FlClash ## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE) <p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash) <img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
@@ -34,33 +38,14 @@ on Mobile:
✨ Support subscription link, Dark mode ✨ 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 ## 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> <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>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build ## Build
1. Update submodules 1. Update submodules
@@ -93,7 +78,7 @@ Support the following actions
3. Run build script 3. Run build script
```bash ```bash
dart .\setup.dart windows --arch <arm64 | amd64> dart .\setup.dart
``` ```
- linux - linux
@@ -103,7 +88,7 @@ Support the following actions
2. Run build script 2. Run build script
```bash ```bash
dart .\setup.dart linux --arch <arm64 | amd64> dart .\setup.dart
``` ```
- macOS - macOS
@@ -113,8 +98,11 @@ Support the following actions
2. Run build script 2. Run build script
```bash ```bash
dart .\setup.dart macos --arch <arm64 | amd64> dart .\setup.dart
``` ```
## Star ## Star

View File

@@ -6,9 +6,13 @@
## FlClash ## FlClash
[![Downloads](https://img.shields.io/github/downloads/chen08209/FlClash/total?style=flat-square&logo=github)](https://github.com/chen08209/FlClash/releases/)[![Last Version](https://img.shields.io/github/release/chen08209/FlClash/all.svg?style=flat-square)](https://github.com/chen08209/FlClash/releases/)[![License](https://img.shields.io/github/license/chen08209/FlClash?style=flat-square)](LICENSE) <p style="text-align: left;">
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash) <img alt="downloads" src="https://img.shields.io/github/downloads/chen08209/FlClash/total"/>
<a href="LICENSE">
<img alt="license" src="https://img.shields.io/github/license/chen08209/FlClash"/>
</a>
</p>
基于ClashMeta的多平台代理客户端简单易用开源无广告。 基于ClashMeta的多平台代理客户端简单易用开源无广告。
@@ -34,33 +38,15 @@ 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 ## 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> <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>
## Contact
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
## Build ## Build
1. 更新 submodules 1. 更新 submodules
@@ -93,7 +79,7 @@ on Mobile:
3. 运行构建脚本 3. 运行构建脚本
```bash ```bash
dart .\setup.dart windows --arch <arm64 | amd64> dart .\setup.dart
``` ```
- linux - linux
@@ -103,7 +89,7 @@ on Mobile:
2. 运行构建脚本 2. 运行构建脚本
```bash ```bash
dart .\setup.dart linux --arch <arm64 | amd64> dart .\setup.dart
``` ```
- macOS - macOS
@@ -113,7 +99,7 @@ on Mobile:
2. 运行构建脚本 2. 运行构建脚本
```bash ```bash
dart .\setup.dart macos --arch <arm64 | amd64> dart .\setup.dart
``` ```
## Star History ## Star History

View File

@@ -34,22 +34,22 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
android { android {
namespace "com.follow.clash" namespace "com.follow.clash"
compileSdkVersion 34 compileSdkVersion 34
ndkVersion "27.1.12297006" ndkVersion "25.1.8937393"
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = '17' jvmTarget = '1.8'
} }
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/kotlin'
} }
signingConfigs { signingConfigs {
if (isRelease) { if (isRelease){
release { release {
storeFile defStoreFile storeFile defStoreFile
storePassword defStorePassword storePassword defStorePassword
@@ -74,9 +74,10 @@ android {
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
} }
release { release {
if (isRelease) { minifyEnabled true
if(isRelease){
signingConfig signingConfigs.release signingConfig signingConfigs.release
} else { }else{
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"

View File

@@ -10,23 +10,25 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name=".FlClashApplication" android:name="${applicationName}"
android:hardwareAccelerated="true" android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="FlClash"> android:label="FlClash"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="tiramisu">
<activity <activity
android:name="com.follow.clash.MainActivity" android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -64,25 +66,13 @@
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" /> <!-- <meta-data-->
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
<!-- android:value="true" />-->
<activity <activity
android:name=".TempActivity" android:name=".TempActivity"
android:exported="true" android:theme="@style/TransparentTheme" />
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" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.CHANGE" />
</intent-filter>
</activity>
<service <service
android:name=".services.FlClashTileService" android:name=".services.FlClashTileService"
@@ -90,8 +80,7 @@
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name" android:icon="@drawable/ic_stat_name"
android:label="FlClash" android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
tools:targetApi="n">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
@@ -130,19 +119,12 @@
<intent-filter> <intent-filter>
<action android:name="android.net.VpnService" /> <action android:name="android.net.VpnService" />
</intent-filter> </intent-filter>
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="vpn" />
</service> </service>
<service <service
android:name=".services.FlClashService" android:name=".services.FlClashService"
android:exported="false" android:exported="false"
android:foregroundServiceType="specialUse"> android:foregroundServiceType="specialUse" />
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="service" />
</service>
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"

View File

@@ -0,0 +1,9 @@
package com.follow.clash
import com.follow.clash.models.Props
interface BaseServiceInterface {
fun start(port: Int, props: Props?): 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

@@ -4,8 +4,8 @@ import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
@@ -20,6 +20,8 @@ enum class RunState {
object GlobalState { object GlobalState {
private val lock = ReentrantLock()
val runLock = ReentrantLock() val runLock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
@@ -31,77 +33,36 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin? return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
} }
suspend fun getText(text: String): String { fun getCurrentTitlePlugin(): TilePlugin? {
return getCurrentAppPlugin()?.getText(text) ?: ""
}
fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin? return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
} }
fun getCurrentVPNPlugin(): VpnPlugin? { fun getCurrentVPNPlugin(): VpnPlugin? {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin? val currentEngine = if (serviceEngine != null) serviceEngine else flutterEngine
} return currentEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
fun handleToggle() {
val starting = handleStart()
if (!starting) {
handleStop()
}
}
fun handleStart(): Boolean {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
runLock.lock()
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine()
}
return true
}
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() { fun destroyServiceEngine() {
runLock.withLock { serviceEngine?.destroy()
serviceEngine?.destroy() serviceEngine = null
serviceEngine = null
}
} }
fun initServiceEngine() { fun initServiceEngine(context: Context) {
if (serviceEngine != null) return if (serviceEngine != null) return
destroyServiceEngine() lock.withLock {
runLock.withLock { destroyServiceEngine()
serviceEngine = FlutterEngine(FlClashApplication.getAppContext()) serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(VpnPlugin) serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())
serviceEngine?.plugins?.add(TilePlugin()) serviceEngine?.plugins?.add(TilePlugin())
serviceEngine?.plugins?.add(ServicePlugin())
val vpnService = DartExecutor.DartEntrypoint( val vpnService = DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), FlutterInjector.instance().flutterLoader().findAppBundlePath(),
"_service" "vpnService"
) )
serviceEngine?.dartExecutor?.executeDartEntrypoint( serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService, vpnService,
if (flutterEngine == null) listOf("quick") else null
) )
} }
} }

View File

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

View File

@@ -2,24 +2,10 @@ package com.follow.clash
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import com.follow.clash.extensions.wrapAction
class TempActivity : Activity() { class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
when (intent.action) {
wrapAction("START") -> {
GlobalState.handleStart()
}
wrapAction("STOP") -> {
GlobalState.handleStop()
}
wrapAction("CHANGE") -> {
GlobalState.handleToggle()
}
}
finishAndRemoveTask() finishAndRemoveTask()
} }
} }

View File

@@ -1,31 +1,28 @@
package com.follow.clash.extensions package com.follow.clash.extensions
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.PendingIntent
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build import android.os.Build
import android.system.OsConstants.IPPROTO_TCP import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64 import android.util.Base64
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.TempActivity import com.follow.clash.MainActivity
import com.follow.clash.models.CIDR import com.follow.clash.R
import com.follow.clash.models.Metadata 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream 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 { suspend fun Drawable.getBase64(): String {
@@ -44,153 +41,6 @@ fun Metadata.getProtocol(): Int? {
return null return null
} }
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> { private val CHANNEL = "FlClash"
return routeAddress.filter {
it.isIpv4()
}.map {
it.toCIDR()
}
}
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> { private val notificationId: Int = 1
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) {
throw IllegalArgumentException("Invalid CIDR format")
}
val ipAddress = parts[0]
val prefixLength = parts[1].toIntOrNull()
?: throw IllegalArgumentException("Invalid prefix length")
val address = InetAddress.getByName(ipAddress)
val maxPrefix = if (address.address.size == 4) 32 else 128
if (prefixLength < 0 || prefixLength > maxPrefix) {
throw IllegalArgumentException("Invalid prefix length for IP version")
}
return CIDR(address, prefixLength)
}
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
val properties = getLinkProperties(network) ?: return listOf()
return properties.dnsServers.map { it.asSocketAddressText(53) }
}
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port"
is Inet4Address ->
"${this.hostAddress}:$port"
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
}
}
fun Context.wrapAction(action: String): String {
return "${this.packageName}.action.$action"
}
fun Context.getActionIntent(action: String): Intent {
val actionIntent = Intent(this, TempActivity::class.java)
actionIntent.action = wrapAction(action)
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
fun Context.getActionPendingIntent(action: String): PendingIntent {
return if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
for (i in 0 until 8) {
sb.append(
Integer.toHexString(
src[i shl 1].toInt() shl 8 and 0xff00
or (src[(i shl 1) + 1].toInt() and 0xff)
)
)
if (i < 7) {
sb.append(":")
}
}
return sb.toString()
}
suspend fun <T> MethodChannel.awaitResult(
method: String,
arguments: Any? = null
): T? = withContext(Dispatchers.Main) { // 切换到主线程
suspendCoroutine { continuation ->
invokeMethod(method, arguments, object : MethodChannel.Result {
override fun success(result: Any?) {
@Suppress("UNCHECKED_CAST")
continuation.resume(result as T)
}
override fun error(code: String, message: String?, details: Any?) {
continuation.resume(null)
}
override fun notImplemented() {
continuation.resume(null)
}
})
}
}
fun ReentrantLock.safeLock() {
if (this.isLocked) {
return
}
this.lock()
}
fun ReentrantLock.safeUnlock() {
if (!this.isLocked) {
return
}
this.unlock()
}

View File

@@ -1,8 +1,10 @@
package com.follow.clash.models package com.follow.clash.models
import java.util.Date
data class Package( data class Package(
val packageName: String, val packageName: String,
val label: String, val label: String,
val isSystem: Boolean, val isSystem: Boolean,
val lastUpdateTime: Long, val firstInstallTime: Long,
) )

View File

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

View File

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

View File

@@ -3,28 +3,26 @@ package com.follow.clash.plugins
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo import android.content.pm.ComponentInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.VpnService import android.net.VpnService
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
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.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import com.follow.clash.FlClashApplication import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.R
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getActionIntent
import com.follow.clash.extensions.getBase64 import com.follow.clash.extensions.getBase64
import com.follow.clash.extensions.getProtocol
import com.follow.clash.models.Package import com.follow.clash.models.Package
import com.follow.clash.models.Process
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -39,21 +37,26 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.net.InetSocketAddress
import java.util.zip.ZipFile import java.util.zip.ZipFile
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { 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 private lateinit var channel: MethodChannel
private lateinit var scope: CoroutineScope private lateinit var scope: CoroutineScope
private var connectivity: ConnectivityManager? = null
private var vpnCallBack: (() -> Unit)? = null private var vpnCallBack: (() -> Unit)? = null
private val iconMap = mutableMapOf<String, String?>() private val iconMap = mutableMapOf<String, String?>()
private val packages = mutableListOf<Package>() private val packages = mutableListOf<Package>()
private val skipPrefixList = listOf( private val skipPrefixList = listOf(
@@ -111,6 +114,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex() ("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
} }
val VPN_PERMISSION_REQUEST_CODE = 1001 val VPN_PERMISSION_REQUEST_CODE = 1001
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002 val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
@@ -119,27 +123,11 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this) channel.setMethodCallHandler(this)
} }
private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
.setShortLabel(label)
.setIcon(
IconCompat.createWithResource(
FlClashApplication.getAppContext(),
R.mipmap.ic_launcher_round
)
)
.setIntent(FlClashApplication.getAppContext().getActionIntent("CHANGE"))
.build()
ShortcutManagerCompat.setDynamicShortcuts(
FlClashApplication.getAppContext(),
listOf(shortcut)
)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
scope.cancel() scope.cancel()
@@ -147,26 +135,25 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun tip(message: String?) { private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) { if (GlobalState.flutterEngine == null) {
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show() if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
} }
} }
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"moveTaskToBack" -> { "moveTaskToBack" -> {
activityRef?.get()?.moveTaskToBack(true) activity?.moveTaskToBack(true)
result.success(true) result.success(true);
} }
"updateExcludeFromRecents" -> { "updateExcludeFromRecents" -> {
val value = call.argument<Boolean>("value") val value = call.argument<Boolean>("value")
updateExcludeFromRecents(value) updateExcludeFromRecents(value)
result.success(true) result.success(true);
}
"initShortcuts" -> {
initShortcuts(call.arguments as String)
result.success(true)
} }
"getPackages" -> { "getPackages" -> {
@@ -196,7 +183,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
if (iconMap["default"] == null) { if (iconMap["default"] == null) {
iconMap["default"] = iconMap["default"] =
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64() context.packageManager?.defaultActivityIcon?.getBase64()
} }
result.success(iconMap["default"]) result.success(iconMap["default"])
return@launch return@launch
@@ -204,6 +191,48 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
} }
"resolverProcess" -> {
val data = call.argument<String>("data")
val process =
if (data != null) Gson().fromJson(
data,
Process::class.java
) else null
val metadata = process?.metadata
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
}
if (connectivity == null) {
connectivity = context.getSystemService<ConnectivityManager>()
}
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())
}
}
}
"tip" -> { "tip" -> {
val message = call.argument<String>("message") val message = call.argument<String>("message")
tip(message) tip(message)
@@ -217,7 +246,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
else -> { else -> {
result.notImplemented() result.notImplemented();
} }
} }
} }
@@ -225,8 +254,8 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private fun openFile(path: String) { private fun openFile(path: String) {
val file = File(path) val file = File(path)
val uri = FileProvider.getUriForFile( val uri = FileProvider.getUriForFile(
FlClashApplication.getAppContext(), context,
"${FlClashApplication.getAppContext().packageName}.fileProvider", "${context.packageName}.fileProvider",
file file
) )
@@ -238,13 +267,13 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val flags = val flags =
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities( val resInfoList = context.packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY intent, PackageManager.MATCH_DEFAULT_ONLY
) )
for (resolveInfo in resInfoList) { for (resolveInfo in resInfoList) {
val packageName = resolveInfo.activityInfo.packageName val packageName = resolveInfo.activityInfo.packageName
FlClashApplication.getAppContext().grantUriPermission( context.grantUriPermission(
packageName, packageName,
uri, uri,
flags flags
@@ -252,19 +281,19 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
try { try {
activityRef?.get()?.startActivity(intent) activity?.startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
println(e) println(e)
} }
} }
private fun updateExcludeFromRecents(value: Boolean?) { private fun updateExcludeFromRecents(value: Boolean?) {
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java) val am = getSystemService(context, ActivityManager::class.java)
val task = am?.appTasks?.firstOrNull { val task = am?.appTasks?.firstOrNull {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.taskInfo.taskId == activityRef?.get()?.taskId it.taskInfo.taskId == activity?.taskId
} else { } else {
it.taskInfo.id == activityRef?.get()?.taskId it.taskInfo.id == activity?.taskId
} }
} }
@@ -276,7 +305,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private suspend fun getPackageIcon(packageName: String): String? { private suspend fun getPackageIcon(packageName: String): String? {
val packageManager = FlClashApplication.getAppContext().packageManager val packageManager = context.packageManager
if (iconMap[packageName] == null) { if (iconMap[packageName] == null) {
iconMap[packageName] = try { iconMap[packageName] = try {
packageManager?.getApplicationIcon(packageName)?.getBase64() packageManager?.getApplicationIcon(packageName)?.getBase64()
@@ -289,10 +318,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
private fun getPackages(): List<Package> { private fun getPackages(): List<Package> {
val packageManager = FlClashApplication.getAppContext().packageManager val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages if (packages.isNotEmpty()) return packages;
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter { packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != FlClashApplication.getAppContext().packageName it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true || it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|| it.packageName == "android" || it.packageName == "android"
@@ -301,10 +330,10 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
packageName = it.packageName, packageName = it.packageName,
label = it.applicationInfo.loadLabel(packageManager).toString(), label = it.applicationInfo.loadLabel(packageManager).toString(),
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1, isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1,
lastUpdateTime = it.lastUpdateTime firstInstallTime = it.firstInstallTime
) )
}?.let { packages.addAll(it) } }?.let { packages.addAll(it) }
return packages return packages;
} }
private suspend fun getPackagesToJson(): String { private suspend fun getPackagesToJson(): String {
@@ -321,45 +350,38 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
} }
fun requestVpnPermission(callBack: () -> Unit) { fun requestVpnPermission(context: Context, callBack: () -> Unit) {
vpnCallBack = callBack vpnCallBack = callBack
val intent = VpnService.prepare(FlClashApplication.getAppContext()) val intent = VpnService.prepare(context)
if (intent != null) { if (intent != null) {
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE) activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return return;
} }
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
fun requestNotificationsPermission() { fun requestNotificationsPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permission = ContextCompat.checkSelfPermission( val permission = ContextCompat.checkSelfPermission(
FlClashApplication.getAppContext(), context,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) )
if (permission != PackageManager.PERMISSION_GRANTED) { if (permission != PackageManager.PERMISSION_GRANTED) {
if (isBlockNotification) return if (isBlockNotification) return
if (activityRef?.get() == null) return if (activity == null) return
activityRef?.get()?.let { ActivityCompat.requestPermissions(
ActivityCompat.requestPermissions( activity!!,
it, arrayOf(Manifest.permission.POST_NOTIFICATIONS),
arrayOf(Manifest.permission.POST_NOTIFICATIONS), NOTIFICATION_PERMISSION_REQUEST_CODE
NOTIFICATION_PERMISSION_REQUEST_CODE )
) return
return
}
} }
} }
} }
suspend fun getText(text: String): String? {
return withContext(Dispatchers.Default){
channel.awaitResult<String>("getText", text)
}
}
private fun isChinaPackage(packageName: String): Boolean { private fun isChinaPackage(packageName: String): Boolean {
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false val packageManager = context.packageManager ?: return false
skipPrefixList.forEach { skipPrefixList.forEach {
if (packageName == it || packageName.startsWith("$it.")) return false if (packageName == it || packageName.startsWith("$it.")) return false
} }
@@ -379,7 +401,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong()) PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
) )
} else { } else {
packageManager.getPackageInfo( @Suppress("DEPRECATION") packageManager.getPackageInfo(
packageName, packageManagerFlags packageName, packageManagerFlags
) )
} }
@@ -425,29 +447,33 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
return false return false
} }
fun requestGc() {
channel.invokeMethod("gc", null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityRef = WeakReference(binding.activity) activity = binding.activity;
binding.addActivityResultListener(::onActivityResult) binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener) binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activityRef = null activity = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activityRef = WeakReference(binding.activity) activity = binding.activity;
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
channel.invokeMethod("exit", null) channel.invokeMethod("exit", null)
activityRef = null activity = null
} }
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == VPN_PERMISSION_REQUEST_CODE) { if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
if (resultCode == FlutterActivity.RESULT_OK) { if (resultCode == FlutterActivity.RESULT_OK) {
GlobalState.initServiceEngine() GlobalState.initServiceEngine(context)
vpnCallBack?.invoke() vpnCallBack?.invoke()
} }
} }
@@ -464,4 +490,4 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
} }
return true return true
} }
} }

View File

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

View File

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

View File

@@ -1,53 +1,31 @@
package com.follow.clash.plugins package com.follow.clash.plugins
import android.annotation.SuppressLint
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.content.getSystemService import android.util.Log
import com.follow.clash.FlClashApplication import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.extensions.awaitResult import com.follow.clash.models.Props
import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process
import com.follow.clash.models.StartForegroundParams
import com.follow.clash.models.VpnOptions
import com.follow.clash.services.BaseServiceInterface
import com.follow.clash.services.FlClashService import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson import com.google.gson.Gson
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
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 kotlin.concurrent.withLock
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var flutterMethodChannel: MethodChannel
private var flClashService: BaseServiceInterface? = null
private lateinit var options: VpnOptions
private lateinit var scope: CoroutineScope
private var lastStartForegroundParams: StartForegroundParams? = null
private var timerJob: Job? = null
private val connectivity by lazy { class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>() private lateinit var flutterMethodChannel: MethodChannel
} private lateinit var context: Context
private var flClashService: BaseServiceInterface? = null
private var port: Int = 7890
private var props: Props? = null
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
@@ -56,7 +34,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
is FlClashService.LocalBinder -> service.getService() is FlClashService.LocalBinder -> service.getService()
else -> throw Exception("invalid binder") else -> throw Exception("invalid binder")
} }
handleStartService() start()
} }
override fun onServiceDisconnected(arg: ComponentName) { override fun onServiceDisconnected(arg: ComponentName) {
@@ -65,188 +43,74 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
} }
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default) context = flutterPluginBinding.applicationContext
scope.launch {
registerNetworkCallback()
}
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn") flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
flutterMethodChannel.setMethodCallHandler(this) flutterMethodChannel.setMethodCallHandler(this)
} }
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
unRegisterNetworkCallback()
flutterMethodChannel.setMethodCallHandler(null) flutterMethodChannel.setMethodCallHandler(null)
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
when (call.method) {
"start" -> {
val data = call.argument<String>("data")
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
}
"stop" -> { "start" -> {
handleStop() port = call.argument<Int>("port")!!
val args = call.argument<String>("args")
props =
if (args != null) Gson().fromJson(args, Props::class.java) else null
when (props?.enable == true) {
true -> handleStartVpn()
false -> start()
}
result.success(true)
}
"stop" -> {
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) result.success(true)
} else {
result.success(false)
} }
}
"setProtect" -> { "startForeground" -> {
val fd = call.argument<Int>("fd") val title = call.argument<String>("title") as String
if (fd != null) { val content = call.argument<String>("content") as String
if (flClashService is FlClashVpnService) { startForeground(title, content)
(flClashService as FlClashVpnService).protect(fd) result.success(true)
} }
result.success(true)
} else {
result.success(false)
}
}
"resolverProcess" -> { else -> {
val data = call.argument<String>("data") result.notImplemented()
val process = if (data != null) Gson().fromJson(
data, Process::class.java
) else null
val metadata = process?.metadata
if (metadata == null) {
result.success(null)
return
}
val protocol = metadata.getProtocol()
if (protocol == null) {
result.success(null)
return
}
scope.launch {
withContext(Dispatchers.Default) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
result.success(null)
return@withContext
}
val src = InetSocketAddress(metadata.sourceIP, metadata.sourcePort)
val dst = InetSocketAddress(
metadata.destinationIP.ifEmpty { metadata.host },
metadata.destinationPort
)
val uid = try {
connectivity?.getConnectionOwnerUid(protocol, src, dst)
} catch (_: Exception) {
null
}
if (uid == null || uid == -1) {
result.success(null)
return@withContext
}
val packages =
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(uid)
result.success(packages?.first())
}
}
}
else -> {
result.notImplemented()
}
} }
} }
fun handleStart(options: VpnOptions): Boolean { @SuppressLint("ForegroundServiceType")
this.options = options fun handleStartVpn() {
when (options.enable) { GlobalState.getCurrentAppPlugin()?.requestVpnPermission(context) {
true -> handleStartVpn() start()
false -> handleStartService()
}
return true
}
private fun handleStartVpn() {
GlobalState.getCurrentAppPlugin()
?.requestVpnPermission {
handleStartService()
}
}
fun requestGc() {
flutterMethodChannel.invokeMethod("gc", null)
}
val networks = mutableSetOf<Network>()
fun onUpdateNetwork() {
val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList()
}.toSet().joinToString(",")
scope.launch {
withContext(Dispatchers.Main) {
flutterMethodChannel.invokeMethod("dnsChanged", dns)
}
} }
} }
private val callback = object : ConnectivityManager.NetworkCallback() { @SuppressLint("ForegroundServiceType")
override fun onAvailable(network: Network) { private fun startForeground(title: String, content: String) {
networks.add(network) GlobalState.runLock.withLock {
onUpdateNetwork()
}
override fun onLost(network: Network) {
networks.remove(network)
onUpdateNetwork()
}
}
private val request = NetworkRequest.Builder().apply {
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
}.build()
private fun registerNetworkCallback() {
networks.clear()
connectivity?.registerNetworkCallback(request, callback)
}
private fun unRegisterNetworkCallback() {
connectivity?.unregisterNetworkCallback(callback)
networks.clear()
onUpdateNetwork()
}
private suspend fun startForeground() {
GlobalState.runLock.lock()
try {
if (GlobalState.runState.value != RunState.START) return if (GlobalState.runState.value != RunState.START) return
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams") flClashService?.startForeground(title, content)
val startForegroundParams = Gson().fromJson(
data, StartForegroundParams::class.java
)
if (lastStartForegroundParams != startForegroundParams) {
lastStartForegroundParams = startForegroundParams
flClashService?.startForeground(
startForegroundParams.title,
startForegroundParams.content,
)
}
} finally {
GlobalState.runLock.unlock()
} }
} }
private fun startForegroundJob() { private fun start() {
timerJob = CoroutineScope(Dispatchers.Main).launch {
while (isActive) {
startForeground()
delay(1000)
}
}
}
private fun stopForegroundJob() {
timerJob?.cancel()
timerJob = null
}
private fun handleStartService() {
if (flClashService == null) { if (flClashService == null) {
bindService() bindService()
return return
@@ -254,29 +118,26 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.START) return if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options) val fd = flClashService?.start(port, props)
flutterMethodChannel.invokeMethod( flutterMethodChannel.invokeMethod("started", fd)
"started", fd
)
startForegroundJob();
} }
} }
fun handleStop() { fun stop() {
GlobalState.runLock.withLock { GlobalState.runLock.withLock {
if (GlobalState.runState.value == RunState.STOP) return if (GlobalState.runState.value == RunState.STOP) return
GlobalState.runState.value = RunState.STOP GlobalState.runState.value = RunState.STOP
stopForegroundJob()
flClashService?.stop() flClashService?.stop()
GlobalState.handleTryDestroy()
} }
GlobalState.destroyServiceEngine()
} }
private fun bindService() { private fun bindService() {
val intent = when (options.enable) { val intent = when (props?.enable == true) {
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java) true -> Intent(context, FlClashVpnService::class.java)
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java) false -> Intent(context, FlClashService::class.java)
} }
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE) context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
} }

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
package com.follow.clash.services package com.follow.clash.services
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.IBinder
import android.service.quicksettings.Tile import android.service.quicksettings.Tile
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -37,7 +37,6 @@ class FlClashTileService : TileService() {
GlobalState.runState.observeForever(observer) GlobalState.runState.observeForever(observer)
} }
@SuppressLint("StartActivityAndCollapseDeprecated")
private fun activityTransfer() { private fun activityTransfer() {
val intent = Intent(this, TempActivity::class.java) val intent = Intent(this, TempActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
@@ -66,7 +65,19 @@ class FlClashTileService : TileService() {
override fun onClick() { override fun onClick() {
super.onClick() super.onClick()
activityTransfer() activityTransfer()
GlobalState.handleToggle() if (GlobalState.runState.value == RunState.STOP) {
GlobalState.runState.value = RunState.PENDING
val titlePlugin = GlobalState.getCurrentTitlePlugin()
if (titlePlugin != null) {
titlePlugin.handleStart()
} else {
GlobalState.initServiceEngine(applicationContext)
}
} else if (GlobalState.runState.value == RunState.START) {
GlobalState.runState.value = RunState.PENDING
GlobalState.getCurrentTitlePlugin()?.handleStop()
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -16,59 +16,51 @@ import android.os.Parcel
import android.os.RemoteException import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState import com.follow.clash.GlobalState
import com.follow.clash.MainActivity import com.follow.clash.MainActivity
import com.follow.clash.R import com.follow.clash.R
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.extensions.getIpv4RouteAddress
import com.follow.clash.extensions.getIpv6RouteAddress
import com.follow.clash.extensions.toCIDR
import com.follow.clash.models.AccessControlMode import com.follow.clash.models.AccessControlMode
import com.follow.clash.models.VpnOptions import com.follow.clash.models.Props
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@SuppressLint("WrongConstant")
class FlClashVpnService : VpnService(), BaseServiceInterface { class FlClashVpnService : VpnService(), BaseServiceInterface {
private val passList = listOf(
"*zhihu.com",
"*zhimg.com",
"*jd.com",
"100ime-iat-api.xfyun.cn",
"*360buyimg.com",
"localhost",
"*.local",
"127.*",
"10.*",
"172.16.*",
"172.17.*",
"172.18.*",
"172.19.*",
"172.2*",
"172.30.*",
"172.31.*",
"192.168.*"
)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
GlobalState.initServiceEngine() GlobalState.initServiceEngine(applicationContext)
} }
override fun start(options: VpnOptions): Int { override fun start(port: Int, props: Props?): Int? {
return with(Builder()) { return with(Builder()) {
if (options.ipv4Address.isNotEmpty()) { addAddress("172.16.0.1", 30)
val cidr = options.ipv4Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv4RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute4", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("0.0.0.0", 0)
}
}
if (options.ipv6Address.isNotEmpty()) {
val cidr = options.ipv6Address.toCIDR()
addAddress(cidr.address, cidr.prefixLength)
val routeAddress = options.getIpv6RouteAddress()
if (routeAddress.isNotEmpty()) {
routeAddress.forEach { i ->
Log.d("addRoute6", "address: ${i.address} prefixLength:${i.prefixLength}")
addRoute(i.address, i.prefixLength)
}
} else {
addRoute("::", 0)
}
}
addDnsServer(options.dnsServerAddress)
setMtu(9000) setMtu(9000)
options.accessControl?.let { accessControl -> addRoute("0.0.0.0", 0)
props?.accessControl?.let { accessControl ->
when (accessControl.mode) { when (accessControl.mode) {
AccessControlMode.acceptSelected -> { AccessControlMode.acceptSelected -> {
(accessControl.acceptList + packageName).forEach { (accessControl.acceptList + packageName).forEach {
@@ -83,28 +75,29 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
} }
addDnsServer("172.16.0.2")
setSession("FlClash") setSession("FlClash")
setBlocking(false) setBlocking(false)
if (Build.VERSION.SDK_INT >= 29) { if (Build.VERSION.SDK_INT >= 29) {
setMetered(false) setMetered(false)
} }
if (options.allowBypass) { if (props?.allowBypass == true) {
allowBypass() allowBypass()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
setHttpProxy( setHttpProxy(
ProxyInfo.buildDirectProxy( ProxyInfo.buildDirectProxy(
"127.0.0.1", "127.0.0.1",
options.port, port,
options.bypassDomain passList
) )
) )
} }
establish()?.detachFd() establish()?.detachFd()
?: throw NullPointerException("Establish VPN rejected by system")
} }
} }
override fun stop() { override fun stop() {
stopSelf() stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -116,55 +109,42 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
private val notificationId: Int = 1 private val notificationId: Int = 1
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy { private val notificationBuilder: NotificationCompat.Builder by lazy {
CoroutineScope(Dispatchers.Main).async { val intent = Intent(this, MainActivity::class.java)
val stopText = GlobalState.getText("stop")
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) { val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity( PendingIntent.getActivity(
this@FlClashVpnService, this,
0, 0,
intent, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
} else { } else {
PendingIntent.getActivity( PendingIntent.getActivity(
this@FlClashVpnService, this,
0, 0,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
) )
} }
with(NotificationCompat.Builder(this, CHANNEL)) {
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) { setSmallIcon(R.drawable.ic_stat_name)
setSmallIcon(R.drawable.ic_stat_name) setContentTitle("FlClash")
setContentTitle("FlClash") setContentIntent(pendingIntent)
setContentIntent(pendingIntent) setCategory(NotificationCompat.CATEGORY_SERVICE)
setCategory(NotificationCompat.CATEGORY_SERVICE) priority = NotificationCompat.PRIORITY_MIN
priority = NotificationCompat.PRIORITY_MIN if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
setOngoing(true)
addAction(
0,
stopText,
getActionPendingIntent("STOP")
)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
setAutoCancel(true)
} }
} }
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
return notificationBuilderDeferred.await()
}
@SuppressLint("ForegroundServiceType", "WrongConstant") @SuppressLint("ForegroundServiceType", "WrongConstant")
override suspend fun startForeground(title: String, content: String) { override fun startForeground(title: String, content: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java) val manager = getSystemService(NotificationManager::class.java)
var channel = manager?.getNotificationChannel(CHANNEL) var channel = manager?.getNotificationChannel(CHANNEL)
@@ -175,10 +155,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
} }
} }
val notification = val notification =
getNotificationBuilder() notificationBuilder.setContentTitle(title).setContentText(content).build()
.setContentTitle(title)
.setContentText(content)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else { } else {
@@ -188,7 +165,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
GlobalState.getCurrentVPNPlugin()?.requestGc() GlobalState.getCurrentAppPlugin()?.requestGc()
} }
private val binder = LocalBinder() private val binder = LocalBinder()
@@ -201,7 +178,7 @@ class FlClashVpnService : VpnService(), BaseServiceInterface {
val isSuccess = super.onTransact(code, data, reply, flags) val isSuccess = super.onTransact(code, data, reply, flags)
if (!isSuccess) { if (!isSuccess) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
GlobalState.getCurrentTilePlugin()?.handleStop() GlobalState.getCurrentTitlePlugin()?.handleStop()
} }
} }
return isSuccess return isSuccess

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,167 +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) wrapMessage(data interface{}) []byte {
sendAction := ActionResult{
Id: action.Id,
Method: action.Method,
Data: data,
}
res, _ := sendAction.Json()
return res
}
func handleAction(action *Action, send func([]byte)) {
switch action.Method {
case initClashMethod:
data := action.Data.(string)
send(action.wrapMessage(handleInitClash(data)))
return
case getIsInitMethod:
send(action.wrapMessage(handleGetIsInit()))
return
case forceGcMethod:
handleForceGc()
send(action.wrapMessage(true))
return
case shutdownMethod:
send(action.wrapMessage(handleShutdown()))
return
case validateConfigMethod:
data := []byte(action.Data.(string))
send(action.wrapMessage(handleValidateConfig(data)))
return
case updateConfigMethod:
data := []byte(action.Data.(string))
send(action.wrapMessage(handleUpdateConfig(data)))
return
case getProxiesMethod:
send(action.wrapMessage(handleGetProxies()))
return
case changeProxyMethod:
data := action.Data.(string)
handleChangeProxy(data, func(value string) {
send(action.wrapMessage(value))
})
return
case getTrafficMethod:
send(action.wrapMessage(handleGetTraffic()))
return
case getTotalTrafficMethod:
send(action.wrapMessage(handleGetTotalTraffic()))
return
case resetTrafficMethod:
handleResetTraffic()
send(action.wrapMessage(true))
return
case asyncTestDelayMethod:
data := action.Data.(string)
handleAsyncTestDelay(data, func(value string) {
send(action.wrapMessage(value))
})
return
case getConnectionsMethod:
send(action.wrapMessage(handleGetConnections()))
return
case closeConnectionsMethod:
send(action.wrapMessage(handleCloseConnections()))
return
case closeConnectionMethod:
id := action.Data.(string)
send(action.wrapMessage(handleCloseConnection(id)))
return
case getExternalProvidersMethod:
send(action.wrapMessage(handleGetExternalProviders()))
return
case getExternalProviderMethod:
externalProviderName := action.Data.(string)
send(action.wrapMessage(handleGetExternalProvider(externalProviderName)))
case updateGeoDataMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
return
}
geoType := params["geo-type"]
geoName := params["geo-name"]
handleUpdateGeoData(geoType, geoName, func(value string) {
send(action.wrapMessage(value))
})
return
case updateExternalProviderMethod:
providerName := action.Data.(string)
handleUpdateExternalProvider(providerName, func(value string) {
send(action.wrapMessage(value))
})
return
case sideLoadExternalProviderMethod:
paramsString := action.Data.(string)
var params = map[string]string{}
err := json.Unmarshal([]byte(paramsString), &params)
if err != nil {
send(action.wrapMessage(err.Error()))
return
}
providerName := params["providerName"]
data := params["data"]
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
send(action.wrapMessage(value))
})
return
case startLogMethod:
handleStartLog()
send(action.wrapMessage(true))
return
case stopLogMethod:
handleStopLog()
send(action.wrapMessage(true))
return
case startListenerMethod:
send(action.wrapMessage(handleStartListener()))
return
case stopListenerMethod:
send(action.wrapMessage(handleStopListener()))
return
case getCountryCodeMethod:
ip := action.Data.(string)
handleGetCountryCode(ip, func(value string) {
send(action.wrapMessage(value))
})
return
case getMemoryMethod:
handleGetMemory(func(value string) {
send(action.wrapMessage(value))
})
return
default:
handle := nextHandle(action, send)
if handle {
return
} else {
send(action.wrapMessage(action.DefaultValue))
}
}
}

View File

@@ -1,9 +1,19 @@
package main package main
import "C"
import ( import (
"context" "context"
"errors" "errors"
"fmt" "math"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -11,29 +21,55 @@ import (
"github.com/metacubex/mihomo/common/batch" "github.com/metacubex/mihomo/common/batch"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/sniffer"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub" "github.com/metacubex/mihomo/hub"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/hub/route" "github.com/metacubex/mihomo/hub/route"
"github.com/metacubex/mihomo/listener" "github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider" rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/samber/lo"
"os"
"path/filepath"
"strings"
"sync"
) )
var ( type ConfigExtendedParams struct {
isRunning = false IsPatch bool `json:"is-patch"`
runLock sync.Mutex IsCompatible bool `json:"is-compatible"`
ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} SelectedMap map[string]string `json:"selected-map"`
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) TestURL *string `json:"test-url"`
) }
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 type ExternalProviders []ExternalProvider
@@ -41,6 +77,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) Less(i, j int) bool { return a[i].Name < a[j].Name }
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
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) { func readFile(path string) ([]byte, error) {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err return nil, err
@@ -53,6 +115,19 @@ func readFile(path string) ([]byte, error) {
return data, err 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 { func getProfilePath(id string) string {
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml") return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
} }
@@ -110,13 +185,12 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
case *provider.ProxySetProvider: case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider) psp := p.(*provider.ProxySetProvider)
return &ExternalProvider{ return &ExternalProvider{
Name: psp.Name(), Name: psp.Name(),
Type: psp.Type().String(), Type: psp.Type().String(),
VehicleType: psp.VehicleType().String(), VehicleType: psp.VehicleType().String(),
Count: psp.Count(), Count: psp.Count(),
UpdateAt: psp.UpdatedAt(), Path: psp.Vehicle().Path(),
Path: psp.Vehicle().Path(), UpdateAt: psp.UpdatedAt,
SubscriptionInfo: psp.GetSubscriptionInfo(),
}, nil }, nil
case *rp.RuleSetProvider: case *rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider) rsp := p.(*rp.RuleSetProvider)
@@ -125,8 +199,8 @@ func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
Type: rsp.Type().String(), Type: rsp.Type().String(),
VehicleType: rsp.VehicleType().String(), VehicleType: rsp.VehicleType().String(),
Count: rsp.Count(), Count: rsp.Count(),
UpdateAt: rsp.UpdatedAt(),
Path: rsp.Vehicle().Path(), Path: rsp.Vehicle().Path(),
UpdateAt: rsp.UpdatedAt,
}, nil }, nil
default: default:
return nil, errors.New("not external provider") return nil, errors.New("not external provider")
@@ -137,16 +211,16 @@ func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
switch p.(type) { switch p.(type) {
case *provider.ProxySetProvider: case *provider.ProxySetProvider:
psp := p.(*provider.ProxySetProvider) psp := p.(*provider.ProxySetProvider)
_, _, err := psp.SideUpdate(bytes) elm, same, err := psp.SideUpdate(bytes)
if err == nil { if err == nil && !same {
return err psp.OnUpdate(elm)
} }
return nil return nil
case rp.RuleSetProvider: case rp.RuleSetProvider:
rsp := p.(*rp.RuleSetProvider) rsp := p.(*rp.RuleSetProvider)
_, _, err := rsp.SideUpdate(bytes) elm, same, err := rsp.SideUpdate(bytes)
if err == nil { if err == nil && !same {
return err rsp.OnUpdate(elm)
} }
return nil return nil
default: default:
@@ -160,39 +234,150 @@ func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig
return prof return prof
} }
func genHosts(hosts, patchHosts map[string]any) { func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
for k, v := range patchHosts { for _, v := range s {
hosts[k] = v initVal = f(initVal, v)
} }
return initVal
} }
func trimArr(arr []string) (r []string) { func Map[T, U any](slice []T, fn func(T) U) []U {
for _, e := range arr { result := make([]U, len(slice))
r = append(r, strings.Trim(e, " ")) for i, v := range slice {
result[i] = fn(v)
} }
return return result
} }
func overrideRules(rules *[]string) { func replaceFromMap(s string, m map[string]string) string {
var target = "" for k, v := range m {
for _, line := range *rules { s = strings.ReplaceAll(s, k, v)
rule := trimArr(strings.Split(line, ",")) }
l := len(rule) return s
if l != 2 { }
return
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{}{}
} }
if strings.ToUpper(rule[0]) == "MATCH" { }
target = rule[1] return result
}
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 break
} }
} }
if target == "" {
return ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
} if cur["proxies"] == nil {
var rulesExt = lo.Map(ips, func(ip string, index int) string { return res
return fmt.Sprintf("DOMAIN %s %s", ip, target) }
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
}) })
*rules = append(rulesExt, *rules...)
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 overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
@@ -202,6 +387,7 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.ExternalUIURL = "" targetConfig.ExternalUIURL = ""
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
//targetConfig.GeodataMode = false
targetConfig.IPv6 = patchConfig.IPv6 targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0 targetConfig.Port = 0
@@ -219,29 +405,27 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.Profile.StoreSelected = false targetConfig.Profile.StoreSelected = false
targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GeoXUrl = patchConfig.GeoXUrl
targetConfig.GlobalUA = patchConfig.GlobalUA targetConfig.GlobalUA = patchConfig.GlobalUA
if configParams.TestURL != nil { if targetConfig.DNS.Enable == false {
constant.DefaultTestURL = *configParams.TestURL
}
for idx := range targetConfig.ProxyGroup {
targetConfig.ProxyGroup[idx]["url"] = ""
}
genHosts(targetConfig.Hosts, patchConfig.Hosts)
if configParams.OverrideDns {
targetConfig.DNS = patchConfig.DNS targetConfig.DNS = patchConfig.DNS
} else {
if targetConfig.DNS.Enable == false {
targetConfig.DNS.Enable = true
}
} }
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) {
log.Infoln("[Apply] patch") log.Infoln("[Apply] patch")
general := currentConfig.General route.ReStartServer(general.ExternalController)
controller := currentConfig.Controller if sniffer.Dispatcher != nil {
tls := currentConfig.TLS tunnel.SetSniffing(general.Sniffing)
tunnel.SetSniffing(general.Sniffing) }
tunnel.SetFindProcessMode(general.FindProcessMode) tunnel.SetFindProcessMode(general.FindProcessMode)
dialer.SetTcpConcurrent(general.TCPConcurrent) dialer.SetTcpConcurrent(general.TCPConcurrent)
dialer.DefaultInterface.Store(general.Interface) dialer.DefaultInterface.Store(general.Interface)
@@ -249,33 +433,13 @@ func patchConfig() {
tunnel.SetMode(general.Mode) tunnel.SetMode(general.Mode)
log.SetLevel(general.LogLevel) log.SetLevel(general.LogLevel)
resolver.DisableIPv6 = !general.IPv6 resolver.DisableIPv6 = !general.IPv6
route.ReCreateServer(&route.Config{
Addr: controller.ExternalController,
TLSAddr: controller.ExternalControllerTLS,
UnixAddr: controller.ExternalControllerUnix,
PipeAddr: controller.ExternalControllerPipe,
Secret: controller.Secret,
Certificate: tls.Certificate,
PrivateKey: tls.PrivateKey,
DohServer: controller.ExternalDohServer,
IsDebug: false,
Cors: route.Cors{
AllowOrigins: controller.Cors.AllowOrigins,
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
},
})
} }
func updateListeners(force bool) { var isRunning = false
if !isRunning {
return var runLock sync.Mutex
}
general := currentConfig.General func updateListeners(general *config.General, listeners map[string]constant.InboundListener) {
listeners := currentConfig.Listeners
if force == true {
stopListeners()
}
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
listener.SetAllowLan(general.AllowLan) listener.SetAllowLan(general.AllowLan)
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
@@ -285,20 +449,41 @@ func updateListeners(force bool) {
listener.ReCreateHTTP(general.Port, tunnel.Tunnel) listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel) listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel) listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel) listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel) listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel) listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel) listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel) listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
if !features.Android { listener.ReCreateTun(general.Tun, tunnel.Tunnel)
listener.ReCreateTun(general.Tun, tunnel.Tunnel) listener.ReCreateRedirToTun(general.EBpf.RedirectToTun)
}
} }
func stopListeners() { func stopListeners() {
listener.StopListener() listener.StopListener()
} }
func hcCompatibleProvider(proxyProviders map[string]cp.ProxyProvider) {
wg := sync.WaitGroup{}
ch := make(chan struct{}, math.MaxInt)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == cp.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
}
}()
}
}
}
func patchSelectGroup() { func patchSelectGroup() {
mapping := configParams.SelectedMap mapping := configParams.SelectedMap
if mapping == nil { if mapping == nil {
@@ -324,20 +509,26 @@ func patchSelectGroup() {
} }
} }
func applyConfig(rawConfig *config.RawConfig) error { func applyConfig() error {
runLock.Lock() cfg, err := config.ParseRawConfig(currentRawConfig)
defer runLock.Unlock()
var err error
currentConfig, err = config.ParseRawConfig(rawConfig)
if err != nil { if err != nil {
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig()) cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
}
if configParams.TestURL != nil {
constant.DefaultTestURL = *configParams.TestURL
} }
if configParams.IsPatch { if configParams.IsPatch {
patchConfig() patchConfig(cfg.General)
} else { } else {
hub.ApplyConfig(currentConfig) closeConnections()
runtime.GC()
hub.UltraApplyConfig(cfg)
patchSelectGroup() patchSelectGroup()
} }
updateListeners(false) if isRunning {
updateListeners(cfg.General, cfg.Listeners)
hcCompatibleProvider(cfg.Providers)
}
externalProviders = getExternalProvidersRaw()
return err return err
} }

View File

@@ -1,127 +0,0 @@
package main
import (
"encoding/json"
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/config"
"time"
)
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"`
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
}
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"
startTunMethod Method = "startTun"
stopTunMethod Method = "stopTun"
updateDnsMethod Method = "updateDns"
setProcessMapMethod Method = "setProcessMap"
setFdMapMethod Method = "setFdMap"
setStateMethod Method = "setState"
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
getRunTimeMethod Method = "getRunTime"
getCurrentProfileNameMethod Method = "getCurrentProfileName"
)
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
}
type InvokeMessage struct {
Type InvokeType `json:"type"`
Data interface{} `json:"data"`
}
type InvokeType string
const (
ProtectInvoke InvokeType = "protect"
ProcessInvoke InvokeType = "process"
)
func (message *InvokeMessage) Json() string {
data, _ := json.Marshal(message)
return string(data)
}

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
module core module core
go 1.20 go 1.21.0
replace github.com/metacubex/mihomo => ./Clash.Meta replace github.com/metacubex/mihomo => ./Clash.Meta
require ( require (
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000 github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
github.com/samber/lo v1.47.0 github.com/metacubex/mihomo v1.17.1
github.com/miekg/dns v1.1.61
golang.org/x/net v0.26.0
golang.org/x/sync v0.7.0
) )
require ( require (
@@ -17,55 +20,51 @@ require (
github.com/andybalholm/brotli v1.0.6 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cilium/ebpf v0.12.3 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect github.com/coreos/go-iptables v0.7.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/enfein/mieru/v3 v3.10.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-chi/chi/v5 v5.2.0 // indirect github.com/go-chi/chi/v5 v5.0.14 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.3 // indirect github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect github.com/gofrs/uuid/v5 v5.2.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d // indirect github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
github.com/metacubex/chacha v0.1.0 // indirect github.com/metacubex/chacha v0.1.0 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a // indirect github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da // indirect github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
github.com/metacubex/randv2 v0.2.0 // indirect github.com/metacubex/randv2 v0.2.0 // indirect
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 // indirect github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect github.com/metacubex/sing-shadowsocks v0.2.7 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect github.com/metacubex/sing-shadowsocks2 v0.2.1 // indirect
github.com/metacubex/sing-tun v0.4.5 // indirect github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 // indirect
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
github.com/metacubex/utls v1.6.6 // indirect github.com/metacubex/utls v1.6.6 // indirect
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -73,18 +72,21 @@ require (
github.com/oschwald/maxminddb-golang v1.12.0 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/sagernet/sing v0.5.1 // indirect github.com/sagernet/sing v0.5.0-alpha.13 // indirect
github.com/sagernet/sing-mux v0.2.1 // indirect github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.5 // indirect github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/shirou/gopsutil/v4 v4.24.11 // indirect github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
@@ -93,23 +95,19 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.20.0 // indirect golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/text v0.16.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.22.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect

View File

@@ -1,5 +1,7 @@
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08= github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY= github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34 h1:USCTqih5d1bUXUxWNS9ZD5Tx/lb0jXHEtRIIx/F9dMc=
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34/go.mod h1:YR9wK13TgI5ww8iKWm91MHiSoHC7Oz0U4beCCmtXqLw=
github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A= github.com/RyuaNerin/elliptic2 v1.0.0/go.mod h1:wWB8fWrJI/6EPJkyV/r1Rj0hxUgrusmqSj8JN6yNf/A=
github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go= github.com/RyuaNerin/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8= github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
@@ -17,37 +19,41 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ=
github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg= github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4= github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok= github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0= github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -59,31 +65,37 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gofrs/uuid/v5 v5.2.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.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk= github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
@@ -94,40 +106,34 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
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.0 h1:tg9RSJ18NvL38cCWNyYH1eiG6qDCyyXIaTLQthon0sc= 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/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 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a h1:cZ6oNVrsmsi3SNlnSnRio4zOgtQq+/XidwsaNgKICcg= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20241126021258-5b028898cc5a/go.mod h1:xBw/SYJPgUMPQ1tklV/brGn2nxhfr3BnvBzNlyi4Nic= github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da h1:Mq6cbHbPTLLTUfA9scrwBmOGkvl6y99E3WmtMIMqo30= github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da/go.mod h1:AiZ+UPgrkO1DTnmiAX4b+kRoV1Vfc65UkYD7RbFlIZA= github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs= github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.7 h1:9f3Dt2+71TNp0e202llA2ug5h/rkWs2EZxQ5IMpf+9g=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks v0.2.7/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.1 h1:XIZBXlazp8EEoPp1S0DViAhLkJakjQ2f+AOwwdKKNYg=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.1/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d h1:iYlepjRCYlPXtELupDL+pQjGqkCnQz4KQOfKImP9sog=
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-tun v0.2.7-0.20240719141246-19c49ac9589d/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 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I=
github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= 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/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
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 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
@@ -135,6 +141,7 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:U
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0= github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo= github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0= github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
@@ -149,14 +156,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
@@ -164,18 +173,24 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJ
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= github.com/sagernet/sing v0.5.0-alpha.13 h1:fpR4TFZfu/9V3LbHSAnnnwcaXGMF8ijmAAPoY2WHSKw=
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.5.0-alpha.13/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-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
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/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= 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 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
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 h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
@@ -188,7 +203,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -200,10 +216,6 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -218,21 +230,21 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -244,21 +256,23 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=

View File

@@ -1,76 +1,81 @@
package main package main
/*
#include <stdlib.h>
*/
import "C"
import ( import (
"context" bridge "core/dart-bridge"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"runtime"
"sort"
"sync"
"time"
"unsafe"
"github.com/metacubex/mihomo/adapter" "github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider" cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor" "github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
"net" "golang.org/x/net/context"
"runtime"
"sort"
"strconv"
"time"
) )
var ( var currentRawConfig = config.DefaultRawConfig()
isInit = false
configParams = ConfigExtendedParams{
OnlyStatisticsProxy: false,
}
externalProviders = map[string]cp.Provider{}
logSubscriber observable.Subscription[log.Event]
currentConfig *config.Config
)
func handleInitClash(homeDirStr string) bool { var configParams = ConfigExtendedParams{}
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()
defer runLock.Unlock()
isRunning = false
stopListeners()
}
//export initClash
func initClash(homeDirStr *C.char) bool {
if !isInit { if !isInit {
constant.SetHomeDir(homeDirStr) constant.SetHomeDir(C.GoString(homeDirStr))
isInit = true isInit = true
} }
return isInit return isInit
} }
func handleStartListener() bool { //export getIsInit
runLock.Lock() func getIsInit() bool {
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 {
return isInit return isInit
} }
func handleForceGc() { //export restartClash
go func() { func restartClash() bool {
log.Infoln("[APP] request force GC") execPath, _ := os.Executable()
runtime.GC() go restartExecutable(execPath)
}() return true
} }
func handleShutdown() bool { //export shutdownClash
func shutdownClash() bool {
stopListeners() stopListeners()
executor.Shutdown() executor.Shutdown()
runtime.GC() runtime.GC()
@@ -78,81 +83,106 @@ func handleShutdown() bool {
return true return true
} }
func handleValidateConfig(bytes []byte) string { //export forceGc
_, err := config.UnmarshalRawConfig(bytes) func forceGc() {
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()
go func() { go func() {
defer runLock.Unlock() log.Infoln("[APP] request force GC")
var params = &ChangeProxyParams{} runtime.GC()
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
}() }()
} }
func handleGetTraffic() string { //export validateConfig
up, down := statistic.DefaultManager.Current(configParams.OnlyStatisticsProxy) 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)
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.OnlyProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -160,13 +190,14 @@ func handleGetTraffic() string {
data, err := json.Marshal(traffic) data, err := json.Marshal(traffic)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return "" return C.CString("")
} }
return string(data) return C.CString(string(data))
} }
func handleGetTotalTraffic() string { //export getTotalTraffic
up, down := statistic.DefaultManager.Total(configParams.OnlyStatisticsProxy) func getTotalTraffic() *C.char {
up, down := statistic.DefaultManager.Total(state.OnlyProxy)
traffic := map[string]int64{ traffic := map[string]int64{
"up": up, "up": up,
"down": down, "down": down,
@@ -174,27 +205,29 @@ func handleGetTotalTraffic() string {
data, err := json.Marshal(traffic) data, err := json.Marshal(traffic)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return "" return C.CString("")
} }
return string(data) return C.CString(string(data))
} }
func handleResetTraffic() { //export resetTraffic
func resetTraffic() {
statistic.DefaultManager.ResetStatistic() 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) { b.Go(paramsString, func() (bool, error) {
var params = &TestDelayParams{} var params = &TestDelayParams{}
err := json.Unmarshal([]byte(paramsString), params) err := json.Unmarshal([]byte(paramsString), params)
if err != nil { if err != nil {
fn("")
return false, nil return false, nil
} }
expectedStatus, err := utils.NewUnsignedRanges[uint16]("") expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
if err != nil { if err != nil {
fn("")
return false, nil return false, nil
} }
@@ -211,46 +244,52 @@ func handleAsyncTestDelay(paramsString string, fn func(string)) {
if proxy == nil { if proxy == nil {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
fn(string(data)) bridge.SendToPort(i, string(data))
return false, nil return false, nil
} }
testUrl := constant.DefaultTestURL delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
if params.TestUrl != "" {
testUrl = params.TestUrl
}
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
if err != nil || delay == 0 { if err != nil || delay == 0 {
delayData.Value = -1 delayData.Value = -1
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
fn(string(data)) bridge.SendToPort(i, string(data))
return false, nil return false, nil
} }
delayData.Value = int32(delay) delayData.Value = int32(delay)
data, _ := json.Marshal(delayData) data, _ := json.Marshal(delayData)
fn(string(data)) bridge.SendToPort(i, string(data))
return false, nil return false, nil
}) })
} }
func handleGetConnections() string { //export getVersionInfo
runLock.Lock() func getVersionInfo() *C.char {
defer runLock.Unlock() 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() snapshot := statistic.DefaultManager.Snapshot()
data, err := json.Marshal(snapshot) data, err := json.Marshal(snapshot)
if err != nil { if err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
return "" return C.CString("")
} }
return string(data) return C.CString(string(data))
} }
func handleCloseConnections() bool { //export closeConnections
runLock.Lock() func closeConnections() {
defer runLock.Unlock()
statistic.DefaultManager.Range(func(c statistic.Tracker) bool { statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close() err := c.Close()
if err != nil { if err != nil {
@@ -258,24 +297,43 @@ func handleCloseConnections() bool {
} }
return true return true
}) })
return true
} }
func handleCloseConnection(connectionId string) bool { //export closeConnection
runLock.Lock() func closeConnection(id *C.char) {
defer runLock.Unlock() connectionId := C.GoString(id)
c := statistic.DefaultManager.Get(connectionId) c := statistic.DefaultManager.Get(connectionId)
if c == nil { if c == nil {
return false return
} }
_ = c.Close() _ = c.Close()
return true
} }
func handleGetExternalProviders() string { //export getProviders
runLock.Lock() func getProviders() *C.char {
defer runLock.Unlock() data, err := json.Marshal(tunnel.Providers())
externalProviders = getExternalProvidersRaw() 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) eps := make([]ExternalProvider, 0)
for _, p := range externalProviders { for _, p := range externalProviders {
externalProvider, err := toExternalProvider(p) externalProvider, err := toExternalProvider(p)
@@ -287,146 +345,123 @@ func handleGetExternalProviders() string {
sort.Sort(ExternalProviders(eps)) sort.Sort(ExternalProviders(eps))
data, err := json.Marshal(eps) data, err := json.Marshal(eps)
if err != nil { if err != nil {
return "" return C.CString("")
} }
return string(data) return C.CString(string(data))
} }
func handleGetExternalProvider(externalProviderName string) string { //export getExternalProvider
runLock.Lock() func getExternalProvider(name *C.char) *C.char {
defer runLock.Unlock() externalProviderName := C.GoString(name)
externalProvider, exist := externalProviders[externalProviderName] externalProvider, exist := externalProviders[externalProviderName]
if !exist { if !exist {
return "" return C.CString("")
} }
e, err := toExternalProvider(externalProvider) e, err := toExternalProvider(externalProvider)
if err != nil { if err != nil {
return "" return C.CString("")
} }
data, err := json.Marshal(e) data, err := json.Marshal(e)
if err != nil { 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() { go func() {
path := constant.Path.Resolve(geoName) switch geoTypeString {
switch geoType {
case "MMDB": case "MMDB":
err := updater.UpdateMMDBWithPath(path) err := updater.UpdateMMDB(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "ASN": case "ASN":
err := updater.UpdateASNWithPath(path) err := updater.UpdateASN(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoIp": case "GeoIp":
err := updater.UpdateGeoIpWithPath(path) err := updater.UpdateGeoIp(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
case "GeoSite": case "GeoSite":
err := updater.UpdateGeoSiteWithPath(path) err := updater.UpdateGeoSite(constant.Path.Resolve(geoNameString))
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return 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() { go func() {
externalProvider, exist := externalProviders[providerName] externalProvider, exist := externalProviders[providerNameString]
if !exist { if !exist {
fn("external provider is not exist") bridge.SendToPort(i, "external provider is not exist")
return return
} }
err := externalProvider.Update() err := externalProvider.Update()
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return 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() { go func() {
runLock.Lock() externalProvider, exist := externalProviders[providerNameString]
defer runLock.Unlock()
externalProvider, exist := externalProviders[providerName]
if !exist { if !exist {
fn("external provider is not exist") bridge.SendToPort(i, "external provider is not exist")
return return
} }
err := sideUpdateExternalProvider(externalProvider, data) err := sideUpdateExternalProvider(externalProvider, bytes)
if err != nil { if err != nil {
fn(err.Error()) bridge.SendToPort(i, err.Error())
return return
} }
fn("") bridge.SendToPort(i, "")
}() }()
} }
func handleStartLog() { //export initNativeApiBridge
if logSubscriber != nil { func initNativeApiBridge(api unsafe.Pointer) {
log.UnSubscribe(logSubscriber) bridge.InitDartApi(api)
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)
}
}()
} }
func handleStopLog() { //export initMessage
if logSubscriber != nil { func initMessage(port C.longlong) {
log.UnSubscribe(logSubscriber) i := int64(port)
logSubscriber = nil Port = i
}
} }
func handleGetCountryCode(ip string, fn func(value string)) { //export freeCString
go func() { func freeCString(s *C.char) {
runLock.Lock() C.free(unsafe.Pointer(s))
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 init() { func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) { provider.HealthcheckHook = func(name string, delay uint16) {
delayData := &Delay{ delayData := &Delay{
Url: url,
Name: name, Name: name,
} }
if delay == 0 { if delay == 0 {
@@ -434,19 +469,19 @@ func init() {
} else { } else {
delayData.Value = int32(delay) delayData.Value = int32(delay)
} }
sendMessage(Message{ SendMessage(Message{
Type: DelayMessage, Type: DelayMessage,
Data: delayData, Data: delayData,
}) })
} }
statistic.DefaultRequestNotify = func(c statistic.Tracker) { statistic.DefaultRequestNotify = func(c statistic.Tracker) {
sendMessage(Message{ SendMessage(Message{
Type: RequestMessage, Type: RequestMessage,
Data: c, Data: c,
}) })
} }
executor.DefaultProviderLoadedHook = func(providerName string) { executor.DefaultProviderLoadedHook = func(providerName string) {
sendMessage(Message{ SendMessage(Message{
Type: LoadedMessage, Type: LoadedMessage,
Data: providerName, Data: providerName,
}) })

View File

@@ -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(bytes []byte) {
bridge.SendToPort(i, string(bytes))
})
}
func sendMessage(message Message) {
if messagePort == -1 {
return
}
res, err := message.Json()
if err != nil {
return
}
bridge.SendToPort(messagePort, string(Action{
Method: messageMethod,
}.wrapMessage(res)))
}
//export startListener
func startListener() {
handleStartListener()
}
//export stopListener
func stopListener() {
handleStopListener()
}

View File

@@ -1,378 +0,0 @@
//go:build android && cgo
package main
import "C"
import (
bridge "core/dart-bridge"
"core/platform"
"core/state"
t "core/tun"
"encoding/json"
"errors"
"fmt"
"github.com/metacubex/mihomo/common/utils"
"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"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
type Fd struct {
Id string `json:"id"`
Value int64 `json:"value"`
}
type Process struct {
Id string `json:"id"`
Metadata *constant.Metadata `json:"metadata"`
}
type ProcessMapItem struct {
Id string `json:"id"`
Value string `json:"value"`
}
type InvokeManager struct {
invokeMap sync.Map
chanMap map[string]chan struct{}
chanLock sync.Mutex
}
func NewInvokeManager() *InvokeManager {
return &InvokeManager{
chanMap: make(map[string]chan struct{}),
}
}
func (m *InvokeManager) load(id string) string {
res, ok := m.invokeMap.Load(id)
if ok {
return res.(string)
}
return ""
}
func (m *InvokeManager) delete(id string) {
m.invokeMap.Delete(id)
}
func (m *InvokeManager) completer(id string, value string) {
m.invokeMap.Store(id, value)
m.chanLock.Lock()
if ch, ok := m.chanMap[id]; ok {
close(ch)
delete(m.chanMap, id)
}
m.chanLock.Unlock()
}
func (m *InvokeManager) await(id string) {
m.chanLock.Lock()
if _, ok := m.chanMap[id]; !ok {
m.chanMap[id] = make(chan struct{})
}
ch := m.chanMap[id]
m.chanLock.Unlock()
timeout := time.After(500 * time.Millisecond)
select {
case <-ch:
return
case <-timeout:
m.completer(id, "")
return
}
}
var (
invokePort int64 = -1
tunListener *sing_tun.Listener
fdInvokeMap = NewInvokeManager()
processInvokeMap = NewInvokeManager()
tunLock sync.Mutex
runTime *time.Time
errBlocked = errors.New("blocked")
)
func handleStartTun(fd int) string {
handleStopTun()
tunLock.Lock()
defer tunLock.Unlock()
if fd == 0 {
now := time.Now()
runTime = &now
} else {
initSocketHook()
tunListener, _ = t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
}
now := time.Now()
runTime = &now
}
return handleGetRunTime()
}
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
removeSocketHook()
runTime = nil
if tunListener != nil {
log.Infoln("TUN close")
_ = tunListener.Close()
}
}
func handleGetRunTime() string {
if runTime == nil {
return ""
}
return strconv.FormatInt(runTime.UnixMilli(), 10)
}
func handleSetProcessMap(params string) {
var processMapItem = &ProcessMapItem{}
err := json.Unmarshal([]byte(params), processMapItem)
if err == nil {
processInvokeMap.completer(processMapItem.Id, processMapItem.Value)
}
}
//export attachInvokePort
func attachInvokePort(mPort C.longlong) {
invokePort = int64(mPort)
}
func sendInvokeMessage(message InvokeMessage) {
if invokePort == -1 {
return
}
bridge.SendToPort(invokePort, message.Json())
}
func handleMarkSocket(fd Fd) {
sendInvokeMessage(InvokeMessage{
Type: ProtectInvoke,
Data: fd,
})
}
func handleParseProcess(process Process) {
sendInvokeMessage(InvokeMessage{
Type: ProcessInvoke,
Data: process,
})
}
func handleSetFdMap(id string) {
go func() {
fdInvokeMap.completer(id, "")
}()
}
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
fdInt := int64(fd)
id := utils.NewUUIDV1().String()
handleMarkSocket(Fd{
Id: id,
Value: fdInt,
})
fdInvokeMap.await(id)
fdInvokeMap.delete(id)
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}
func init() {
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
if metadata == nil {
return "", process.ErrInvalidNetwork
}
id := utils.NewUUIDV1().String()
handleParseProcess(Process{
Id: id,
Metadata: metadata,
})
processInvokeMap.await(id)
res := processInvokeMap.load(id)
processInvokeMap.delete(id)
return res, nil
}
}
func handleGetAndroidVpnOptions() string {
tunLock.Lock()
defer tunLock.Unlock()
options := state.AndroidVpnOptions{
Enable: state.CurrentState.Enable,
Port: currentConfig.General.MixedPort,
Ipv4Address: state.DefaultIpv4Address,
Ipv6Address: state.GetIpv6Address(),
AccessControl: state.CurrentState.AccessControl,
SystemProxy: state.CurrentState.SystemProxy,
AllowBypass: state.CurrentState.AllowBypass,
RouteAddress: state.CurrentState.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 handleSetState(params string) {
_ = json.Unmarshal([]byte(params), state.CurrentState)
}
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, send func([]byte)) bool {
switch action.Method {
case startTunMethod:
data := action.Data.(string)
var fd int
_ = json.Unmarshal([]byte(data), &fd)
send(action.wrapMessage(handleStartTun(fd)))
return true
case stopTunMethod:
handleStopTun()
send(action.wrapMessage(true))
return true
case setStateMethod:
data := action.Data.(string)
handleSetState(data)
send(action.wrapMessage(true))
return true
case getAndroidVpnOptionsMethod:
send(action.wrapMessage(handleGetAndroidVpnOptions()))
return true
case updateDnsMethod:
data := action.Data.(string)
handleUpdateDns(data)
send(action.wrapMessage(true))
return true
case setFdMapMethod:
fdId := action.Data.(string)
handleSetFdMap(fdId)
send(action.wrapMessage(true))
return true
case setProcessMapMethod:
data := action.Data.(string)
handleSetProcessMap(data)
send(action.wrapMessage(true))
return true
case getRunTimeMethod:
send(action.wrapMessage(handleGetRunTime()))
return true
case getCurrentProfileNameMethod:
send(action.wrapMessage(handleGetCurrentProfileName()))
return true
}
return false
}
//export quickStart
func quickStart(dirChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
i := int64(port)
dir := C.GoString(dirChar)
bytes := []byte(C.GoString(paramsChar))
stateParams := C.GoString(stateParamsChar)
go func() {
res := handleInitClash(dir)
if res == false {
bridge.SendToPort(i, "init error")
}
handleSetState(stateParams)
bridge.SendToPort(i, handleUpdateConfig(bytes))
}()
}
//export startTUN
func startTUN(fd C.int) *C.char {
f := int(fd)
return C.CString(handleStartTun(f))
}
//export getRunTime
func getRunTime() *C.char {
return C.CString(handleGetRunTime())
}
//export stopTun
func stopTun() {
handleStopTun()
}
//export setFdMap
func setFdMap(fdIdChar *C.char) {
fdId := C.GoString(fdIdChar)
handleSetFdMap(fdId)
}
//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)
}
//export setProcessMap
func setProcessMap(s *C.char) {
if s == nil {
return
}
paramsString := C.GoString(s)
handleSetProcessMap(paramsString)
}

View File

@@ -1,11 +0,0 @@
//go:build !android && cgo
package main
func nextHandle(action *Action) {
return action
}
func nextHandle(action *Action, send func([]byte)) 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 package main
import "C"
import ( import (
"fmt" "fmt"
"os"
) )
func main() { func main() {
args := os.Args fmt.Println("init clash")
if len(args) <= 1 {
fmt.Println("Arguments error")
os.Exit(1)
}
startServer(args[1])
} }

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 package platform

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,
}.wrapMessage(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(bytes []byte) {
send(bytes)
})
}
}
func nextHandle(action *Action, send func([]byte)) bool {
return false
}

49
core/state.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import "C"
import (
"encoding/json"
"fmt"
)
type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidProps struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
}
type State struct {
AndroidProps
CurrentProfileName string `json:"currentProfileName"`
MixedPort int `json:"mixedPort"`
OnlyProxy bool `json:"onlyProxy"`
}
var state State
//export getState
func getState() *C.char {
data, err := json.Marshal(state)
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)
if err != nil {
return
}
}

View File

@@ -1,56 +0,0 @@
//go:build android && cgo
package state
var DefaultIpv4Address = "172.19.0.1/30"
var DefaultDnsAddress = "172.19.0.2"
var DefaultIpv6Address = "fdfe:dcba:9876::1/126"
type AndroidVpnOptions struct {
Enable bool `json:"enable"`
Port int `json:"port"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
BypassDomain []string `json:"bypassDomain"`
RouteAddress []string `json:"routeAddress"`
Ipv4Address string `json:"ipv4Address"`
Ipv6Address string `json:"ipv6Address"`
DnsServerAddress string `json:"dnsServerAddress"`
}
type AccessControl struct {
Mode string `json:"mode"`
AcceptList []string `json:"acceptList"`
RejectList []string `json:"rejectList"`
IsFilterSystemApp bool `json:"isFilterSystemApp"`
}
type AndroidVpnRawOptions struct {
Enable bool `json:"enable"`
AccessControl *AccessControl `json:"accessControl"`
AllowBypass bool `json:"allowBypass"`
SystemProxy bool `json:"systemProxy"`
RouteAddress []string `json:"routeAddress"`
Ipv6 bool `json:"ipv6"`
BypassDomain []string `json:"bypassDomain"`
}
type State struct {
AndroidVpnRawOptions
CurrentProfileName string `json:"currentProfileName"`
}
var CurrentState = &State{}
func GetIpv6Address() string {
if CurrentState.Ipv6 {
return DefaultIpv6Address
} else {
return ""
}
}
func GetDnsServerAddress() string {
return DefaultDnsAddress
}

180
core/tun.go Normal file
View File

@@ -0,0 +1,180 @@
//go:build android
package main
import "C"
import (
"core/platform"
t "core/tun"
"errors"
"strconv"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/log"
"golang.org/x/sync/semaphore"
)
var tunLock sync.Mutex
var tun *t.Tun
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 fdMap FdMap
//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()
if tun != nil {
tun.Close()
tun = nil
}
f := int(fd)
gateway := "172.16.0.1/30"
portal := "172.16.0.2"
dns := "0.0.0.0"
tempTun := &t.Tun{Closed: false, Limit: semaphore.NewWeighted(4)}
closer, err := t.Start(f, gateway, portal, dns)
if err != nil {
log.Errorln("startTUN error: %v", err)
tempTun.Close()
}
tempTun.Closer = closer
tun = tempTun
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 tun != nil {
log.Errorln("[Tun] stopTun")
tun.Close()
tun = nil
}
}()
}
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,
})
}
var fdCounter int64 = 0
func initSocketHook() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return conn.Control(func(fd uintptr) {
if tun == nil {
return
}
fdInt := int64(fd)
timeout := time.After(100 * 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(10 * time.Millisecond)
}
}
})
}
}
func removeSocketHook() {
dialer.DefaultSocketHook = nil
}

33
core/tun/dns.go Normal file
View File

@@ -0,0 +1,33 @@
//go:build android
package tun
import (
"github.com/metacubex/mihomo/dns"
D "github.com/miekg/dns"
"net"
)
func shouldHijackDns(dns net.IP, target net.IP, targetPort int) bool {
if targetPort != 53 {
return false
}
return net.IPv4zero.Equal(dns) || target.Equal(dns)
}
func relayDns(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
}
r, err := dns.ServeDNSWithDefaultServer(msg)
if err != nil {
return nil, err
}
r.SetRcode(msg, r.Rcode)
return r.Pack()
}

20
core/tun/tcp.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build android
package tun
import (
"github.com/metacubex/mihomo/constant"
"net"
)
func createMetadata(lAddr, rAddr *net.TCPAddr) *constant.Metadata {
return &constant.Metadata{
NetWork: constant.TCP,
Type: constant.SOCKS5,
SrcIP: lAddr.AddrPort().Addr(),
DstIP: rAddr.AddrPort().Addr(),
SrcPort: uint16(lAddr.Port),
DstPort: uint16(rAddr.Port),
Host: "",
}
}

View File

@@ -1,69 +1,186 @@
//go:build android && cgo //go:build android
package tun package tun
import "C" import "C"
import ( import (
"core/state" "context"
"encoding/binary"
"github.com/Kr328/tun2socket"
"github.com/Kr328/tun2socket/nat"
"github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"golang.org/x/sync/semaphore"
"io"
"net" "net"
"net/netip" "os"
"time"
) )
type Props struct { type Tun struct {
Fd int `json:"fd"` Closer io.Closer
Gateway string `json:"gateway"`
Gateway6 string `json:"gateway6"` Closed bool
Portal string `json:"portal"` Limit *semaphore.Weighted
Portal6 string `json:"portal6"`
Dns string `json:"dns"`
Dns6 string `json:"dns6"`
} }
func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener, error) { func (t *Tun) Close() {
var prefix4 []netip.Prefix _ = t.Limit.Acquire(context.TODO(), 4)
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address) defer t.Limit.Release(4)
t.Closed = true
if t.Closer != nil {
_ = t.Closer.Close()
}
}
var _, ipv4LoopBack, _ = net.ParseCIDR("127.0.0.0/8")
func Start(fd int, gateway, portal, dns string) (io.Closer, error) {
device := os.NewFile(uintptr(fd), "/dev/tun")
ip, network, err := net.ParseCIDR(gateway)
if err != nil { if err != nil {
log.Errorln("startTUN error:", err) panic(err.Error())
} else {
network.IP = ip
}
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
if err != nil {
_ = device.Close()
return nil, err return nil, err
} }
prefix4 = append(prefix4, tempPrefix4)
var prefix6 []netip.Prefix dnsAddr := net.ParseIP(dns)
if state.CurrentState.Ipv6 {
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address) tcp := func() {
if err != nil { defer func(tcp *nat.TCP) {
log.Errorln("startTUN error:", err) _ = tcp.Close()
return nil, err }(stack.TCP())
defer log.Debugln("TCP: closed")
for stack.TCP().SetDeadline(time.Time{}) == nil {
conn, err := stack.TCP().Accept()
if err != nil {
log.Errorln("Accept connection: %v", err)
continue
}
lAddr := conn.LocalAddr().(*net.TCPAddr)
rAddr := conn.RemoteAddr().(*net.TCPAddr)
if ipv4LoopBack.Contains(rAddr.IP) {
_ = conn.Close()
continue
}
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
go func() {
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
buf := pool.Get(pool.UDPBufferSize)
defer func(buf []byte) {
_ = pool.Put(buf)
}(buf)
for {
_ = conn.SetReadDeadline(time.Now().Add(constant.DefaultTCPTimeout))
length := uint16(0)
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
return
}
if int(length) > len(buf) {
return
}
n, err := conn.Read(buf[:length])
if err != nil {
return
}
msg, err := relayDns(buf[:n])
if err != nil {
return
}
_, _ = conn.Write(msg)
}
}()
continue
}
go tunnel.Tunnel.HandleTCPConn(conn, createMetadata(lAddr, rAddr))
} }
prefix6 = append(prefix6, tempPrefix6)
} }
var dnsHijack []string udp := func() {
dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53")) defer func(udp *nat.UDP) {
_ = udp.Close()
}(stack.UDP())
defer log.Debugln("UDP: closed")
options := LC.Tun{ for {
Enable: true, buf := pool.Get(pool.UDPBufferSize)
Device: device,
Stack: stack, n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf)
DNSHijack: dnsHijack, if err != nil {
AutoRoute: false, return
AutoDetectInterface: false, }
Inet4Address: prefix4,
Inet6Address: prefix6, raw := buf[:n]
MTU: 9000, lAddr := lRAddr.(*net.UDPAddr)
FileDescriptor: fd, rAddr := rRAddr.(*net.UDPAddr)
if ipv4LoopBack.Contains(rAddr.IP) {
_ = pool.Put(buf)
continue
}
if shouldHijackDns(dnsAddr, rAddr.IP, rAddr.Port) {
go func() {
defer func(buf []byte) {
_ = pool.Put(buf)
}(buf)
msg, err := relayDns(raw)
if err != nil {
return
}
_, _ = stack.UDP().WriteTo(msg, rAddr, lAddr)
}()
continue
}
pkt := &packet{
local: lAddr,
data: raw,
writeBack: func(b []byte, addr net.Addr) (int, error) {
return stack.UDP().WriteTo(b, addr, lAddr)
},
drop: func() {
_ = pool.Put(buf)
},
}
tunnel.Tunnel.HandleUDPPacket(inbound.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, constant.SOCKS5))
}
} }
listener, err := sing_tun.New(options, tunnel.Tunnel) go tcp()
go udp()
if err != nil { return stack, nil
log.Errorln("startTUN error:", err)
return nil, err
}
return listener, nil
} }

28
core/tun/udp.go Normal file
View File

@@ -0,0 +1,28 @@
//go:build android
package tun
import "net"
type packet struct {
local *net.UDPAddr
data []byte
writeBack func(b []byte, addr net.Addr) (int, error)
drop func()
}
func (pkt *packet) Data() []byte {
return pkt.data
}
func (pkt *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return pkt.writeBack(b, addr)
}
func (pkt *packet) Drop() {
pkt.drop()
}
func (pkt *packet) LocalAddr() net.Addr {
return pkt.local
}

View File

@@ -1,13 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/manager/hotkey_manager.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/proxy_container.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -20,7 +18,6 @@ runAppWithPreferences(
Widget child, { Widget child, {
required AppState appState, required AppState appState,
required Config config, required Config config,
required AppFlowingState appFlowingState,
required ClashConfig clashConfig, required ClashConfig clashConfig,
}) { }) {
runApp(MultiProvider( runApp(MultiProvider(
@@ -31,13 +28,11 @@ runAppWithPreferences(
ChangeNotifierProvider<Config>( ChangeNotifierProvider<Config>(
create: (_) => config, create: (_) => config,
), ),
ChangeNotifierProvider<AppFlowingState>(
create: (_) => appFlowingState,
),
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>( ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
create: (_) => appState, create: (_) => appState,
update: (_, config, clashConfig, appState) { update: (_, config, clashConfig, appState) {
appState?.mode = clashConfig.mode; appState?.mode = clashConfig.mode;
appState?.isCompatible = config.isCompatible;
appState?.selectedMap = config.currentSelectedMap; appState?.selectedMap = config.currentSelectedMap;
return appState!; return appState!;
}, },
@@ -58,15 +53,13 @@ class Application extends StatefulWidget {
class ApplicationState extends State<Application> { class ApplicationState extends State<Application> {
late SystemColorSchemes systemColorSchemes; late SystemColorSchemes systemColorSchemes;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
final _pageTransitionsTheme = const PageTransitionsTheme( final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{ builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: CommonPageTransitionsBuilder(), TargetPlatform.android: CupertinoPageTransitionsBuilder(),
TargetPlatform.windows: CommonPageTransitionsBuilder(), TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: CommonPageTransitionsBuilder(), TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: CommonPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
}, },
); );
@@ -88,10 +81,7 @@ class ApplicationState extends State<Application> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_autoUpdateGroupTask();
_autoUpdateProfilesTask();
globalState.appController = AppController(context); globalState.appController = AppController(context);
globalState.measure = Measure.of(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
final currentContext = globalState.navigatorKey.currentContext; final currentContext = globalState.navigatorKey.currentContext;
if (currentContext != null) { if (currentContext != null) {
@@ -99,66 +89,22 @@ class ApplicationState extends State<Application> {
} }
await globalState.appController.init(); await globalState.appController.init();
globalState.appController.initLink(); globalState.appController.initLink();
app?.initShortcuts();
}); });
} }
_autoUpdateGroupTask() { _buildApp(Widget app) {
_autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.updateGroupsDebounce();
_autoUpdateGroupTask();
});
});
}
_autoUpdateProfilesTask() {
_autoUpdateProfilesTaskTimer = Timer(const Duration(minutes: 20), () async {
await globalState.appController.autoUpdateProfiles();
_autoUpdateProfilesTask();
});
}
_buildPlatformWrap(Widget child) {
if (system.isDesktop) { if (system.isDesktop) {
return WindowManager( return WindowContainer(
child: TrayManager( child: TrayContainer(
child: HotKeyManager( child: ProxyContainer(
child: ProxyManager( child: app,
child: child,
),
), ),
), ),
); );
} }
return AndroidManager( return AndroidContainer(
child: TileManager( child: TileContainer(
child: child, child: app,
),
);
}
_buildPage(Widget page) {
if (system.isDesktop) {
return WindowHeaderContainer(
child: page,
);
}
return VpnManager(
child: page,
);
}
_buildWrap(Widget child) {
return AppStateManager(
child: ClashManager(
child: ConnectivityManager(
onConnectivityChanged: () {
globalState.appController.updateLocalIp();
globalState.appController.addCheckIpNumDebounce();
},
child: child,
),
), ),
); );
} }
@@ -178,73 +124,65 @@ class ApplicationState extends State<Application> {
@override @override
Widget build(context) { Widget build(context) {
return _buildPlatformWrap( return _buildApp(
_buildWrap( AppStateContainer(
Selector2<AppState, Config, ApplicationSelectorState>( child: ClashContainer(
selector: (_, appState, config) => ApplicationSelectorState( child: Selector2<AppState, Config, ApplicationSelectorState>(
locale: config.appSetting.locale, selector: (_, appState, config) => ApplicationSelectorState(
themeMode: config.themeProps.themeMode, locale: config.locale,
primaryColor: config.themeProps.primaryColor, themeMode: config.themeMode,
prueBlack: config.themeProps.prueBlack, primaryColor: config.primaryColor,
fontFamily: config.themeProps.fontFamily, prueBlack: config.prueBlack,
), ),
builder: (_, state, child) { builder: (_, state, child) {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic); _updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp( return MaterialApp(
navigatorKey: globalState.navigatorKey, navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate GlobalWidgetsLocalizations.delegate
], ],
builder: (_, child) { builder: (_, child) {
return MessageManager( if (system.isDesktop) {
child: LayoutBuilder( return WindowHeaderContainer(child: child!);
builder: (_, container) { }
final appController = globalState.appController; return child!;
final maxWidth = container.maxWidth; },
if (appController.appState.viewWidth != maxWidth) { scrollBehavior: BaseScrollBehavior(),
globalState.appController.updateViewWidth(maxWidth); title: appName,
} locale: other.getLocaleForString(state.locale),
return _buildPage(child!); supportedLocales:
}, AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
), ),
);
},
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(
darkTheme: ThemeData( useMaterial3: true,
useMaterial3: true, pageTransitionsTheme: _pageTransitionsTheme,
fontFamily: state.fontFamily.value, colorScheme: _getAppColorScheme(
pageTransitionsTheme: _pageTransitionsTheme, brightness: Brightness.dark,
colorScheme: _getAppColorScheme( systemColorSchemes: systemColorSchemes,
brightness: Brightness.dark, primaryColor: state.primaryColor,
systemColorSchemes: systemColorSchemes, ).toPrueBlack(state.prueBlack),
primaryColor: state.primaryColor, ),
).toPrueBlack(state.prueBlack), home: child,
), );
home: child, },
); );
}, },
); child: const HomePage(),
}, ),
child: const HomePage(),
), ),
), ),
); );
@@ -253,11 +191,7 @@ class ApplicationState extends State<Application> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
linkManager.destroy(); linkManager.destroy();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel();
await clashCore.destroy();
await globalState.appController.savePreferences(); await globalState.appController.savePreferences();
await globalState.appController.handleExit();
super.dispose(); super.dispose();
} }
} }

View File

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

View File

@@ -1,26 +1,41 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:fl_clash/clash/clash.dart'; import 'package:ffi/ffi.dart';
import 'package:fl_clash/clash/interface.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/models.dart';
import 'package:flutter/services.dart'; import 'generated/clash_ffi.dart';
import 'package:path/path.dart';
class ClashCore { class ClashCore {
static ClashCore? _instance; 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() { ClashCore._internal() {
if (Platform.isAndroid) { lib = _getClashLib();
clashInterface = clashLib!; clashFFI = ClashFFI(lib);
} else { clashFFI.initNativeApiBridge(
clashInterface = clashService!; NativeApi.initializeApiDLData,
} );
} }
factory ClashCore() { factory ClashCore() {
@@ -28,66 +43,67 @@ class ClashCore {
return _instance!; return _instance!;
} }
Future<bool> preload() { bool init(String homeDir) {
return clashInterface.preload(); final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
final isInit = clashFFI.initClash(homeDirChar) == 1;
malloc.free(homeDirChar);
return isInit;
} }
static Future<void> initGeo() async { shutdown() {
final homePath = await appPath.homeDirPath; clashFFI.shutdownClash();
final homeDir = Directory(homePath); lib.close();
final isExists = await homeDir.exists(); }
if (!isExists) {
await homeDir.create(recursive: true); bool get isInit => clashFFI.getIsInit() == 1;
}
const geoFileNameList = [ Future<String> validateConfig(String data) {
mmdbFileName, final completer = Completer<String>();
geoIpFileName, final receiver = ReceivePort();
geoSiteFileName, receiver.listen((message) {
asnFileName, if (!completer.isCompleted) {
]; completer.complete(message);
try { receiver.close();
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) { });
exit(0); final dataChar = data.toNativeUtf8().cast<Char>();
} clashFFI.validateConfig(
dataChar,
receiver.sendPort.nativePort,
);
malloc.free(dataChar);
return completer.future;
} }
Future<bool> init({ Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
required ClashConfig clashConfig, final completer = Completer<String>();
required Config config, final receiver = ReceivePort();
}) async { receiver.listen((message) {
await initGeo(); if (!completer.isCompleted) {
final homeDirPath = await appPath.homeDirPath; completer.complete(message);
return await clashInterface.init(homeDirPath); 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;
} }
shutdown() async { initMessage() {
await clashInterface.shutdown(); clashFFI.initMessage(
receiver.sendPort.nativePort,
);
} }
FutureOr<bool> get isInit => clashInterface.isInit; Future<List<Group>> getProxiesGroups() {
final proxiesRaw = clashFFI.getProxies();
FutureOr<String> validateConfig(String data) { final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
return clashInterface.validateConfig(data); clashFFI.freeCString(proxiesRaw);
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
return await clashInterface.updateConfig(updateConfigParams);
}
Future<List<Group>> getProxiesGroups() async {
final proxiesRawString = await clashInterface.getProxies();
return Isolate.run<List<Group>>(() { return Isolate.run<List<Group>>(() {
if (proxiesRawString.isEmpty) return []; if (proxiesRawString.isEmpty) return [];
final proxies = (json.decode(proxiesRawString) ?? {}) as Map; final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
@@ -117,141 +133,244 @@ class ClashCore {
}); });
} }
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async { Future<List<ExternalProvider>> getExternalProviders() {
return await clashInterface.changeProxy(changeProxyParams); final externalProvidersRaw = clashFFI.getExternalProviders();
}
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 {
final externalProvidersRawString = final externalProvidersRawString =
await clashInterface.getExternalProviders(); externalProvidersRaw.cast<Utf8>().toDartString();
if (externalProvidersRawString.isEmpty) { clashFFI.freeCString(externalProvidersRaw);
return []; return Isolate.run<List<ExternalProvider>>(() {
} final externalProviders =
return Isolate.run<List<ExternalProvider>>( (json.decode(externalProvidersRawString) as List<dynamic>)
() { .map(
final externalProviders = (item) => ExternalProvider.fromJson(item),
(json.decode(externalProvidersRawString) as List<dynamic>) )
.map( .toList();
(item) => ExternalProvider.fromJson(item), return externalProviders;
) });
.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,
); );
} malloc.free(geoTypeChar);
malloc.free(geoNameChar);
Future<ExternalProvider?> getExternalProvider( return completer.future;
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);
} }
Future<String> sideLoadExternalProvider({ Future<String> sideLoadExternalProvider({
required String providerName, required String providerName,
required String data, required String data,
}) { }) {
return clashInterface.sideLoadExternalProvider( final completer = Completer<String>();
providerName: providerName, data: data); 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({ Future<String> updateExternalProvider({
required String providerName, required String providerName,
}) async { }) {
return clashInterface.updateExternalProvider(providerName); final completer = Completer<String>();
} final receiver = ReceivePort();
receiver.listen((message) {
startListener() async { if (!completer.isCompleted) {
await clashInterface.startListener(); completer.complete(message);
} receiver.close();
}
stopListener() async { });
await clashInterface.stopListener(); final providerNameChar = providerName.toNativeUtf8().cast<Char>();
} clashFFI.updateExternalProvider(
providerNameChar,
Future<Delay> getDelay(String url, String proxyName) async { receiver.sendPort.nativePort,
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,
); );
malloc.free(providerNameChar);
return completer.future;
} }
Future<Traffic> getTotalTraffic() async { changeProxy(ChangeProxyParams changeProxyParams) {
final totalTrafficString = await clashInterface.getTotalTraffic(); final params = json.encode(changeProxyParams);
if (totalTrafficString.isEmpty) { final paramsChar = params.toNativeUtf8().cast<Char>();
return Traffic(); clashFFI.changeProxy(paramsChar);
} malloc.free(paramsChar);
return Traffic.fromMap(json.decode(totalTrafficString));
} }
Future<int> getMemory() async { start() {
final value = await clashInterface.getMemory(); clashFFI.start();
if (value.isEmpty) {
return 0;
}
return int.parse(value);
} }
resetTraffic() { stop() {
clashInterface.resetTraffic(); clashFFI.stop();
} }
startLog() { Future<Delay> getDelay(String proxyName) {
clashInterface.startLog(); 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;
}
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);
}
CoreState getState() {
final stateRaw = clashFFI.getState();
final state = json.decode(
stateRaw.cast<Utf8>().toDartString(),
);
clashFFI.freeCString(stateRaw);
return CoreState.fromJson(state);
}
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() { stopLog() {
clashInterface.stopLog(); clashFFI.stopLog();
}
startTun(int fd, int port) {
if (!Platform.isAndroid) return;
clashFFI.startTUN(fd, port);
} }
requestGc() { requestGc() {
clashInterface.forceGc(); clashFFI.forceGc();
} }
destroy() async { void stopTun() {
await clashInterface.destroy(); 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,405 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:fl_clash/clash/message.dart';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/common/future.dart';
import 'package:fl_clash/common/other.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart' hide Action;
mixin ClashInterface {
Future<bool> init(String homeDir);
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();
FutureOr<String> getConnections();
FutureOr<bool> closeConnection(String id);
FutureOr<bool> closeConnections();
}
mixin AndroidClashInterface {
Future<bool> setFdMap(int fd);
Future<bool> setProcessMap(ProcessMapItem item);
Future<bool> setState(CoreState state);
Future<bool> stopTun();
Future<bool> updateDns(String value);
Future<DateTime?> startTun(int fd);
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:
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 (_) {
debugPrint(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}#${other.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(String homeDir) {
return invoke<bool>(
method: ActionMethod.initClash,
data: homeDir,
);
}
@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),
);
}
@override
Future<String> getProxies() {
return invoke<String>(
method: ActionMethod.getProxies,
);
}
@override
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
return invoke<String>(
method: ActionMethod.changeProxy,
data: json.encode(changeProxyParams),
);
}
@override
FutureOr<String> getExternalProviders() {
return invoke<String>(
method: ActionMethod.getExternalProviders,
);
}
@override
FutureOr<String> getExternalProvider(String externalProviderName) {
return invoke<String>(
method: ActionMethod.getExternalProvider,
data: externalProviderName,
);
}
@override
Future<String> updateGeoData(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
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,363 +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.setState:
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> setState(CoreState state) {
return invoke<bool>(
method: ActionMethod.setState,
data: json.encode(state),
);
}
@override
Future<DateTime?> startTun(int fd) async {
final res = await invoke<String>(
method: ActionMethod.startTun,
data: json.encode(fd),
);
if (res.isEmpty) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(int.parse(res));
}
@override
Future<bool> stopTun() {
return invoke<bool>(
method: ActionMethod.stopTun,
);
}
@override
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,
);
}
attachInvokePort(int invokePort) {
clashFFI.attachInvokePort(
invokePort,
);
}
DateTime? startTun(int fd) {
final runTimeRaw = clashFFI.startTUN(fd);
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
clashFFI.freeCString(runTimeRaw);
if (runTimeString.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
}
stopTun() {
clashFFI.stopTun();
}
updateDns(String dns) {
final dnsChar = dns.toNativeUtf8().cast<Char>();
clashFFI.updateDns(dnsChar);
malloc.free(dnsChar);
}
setProcessMap(ProcessMapItem processMapItem) {
final processMapItemChar =
json.encode(processMapItem).toNativeUtf8().cast<Char>();
clashFFI.setProcessMap(processMapItemChar);
malloc.free(processMapItemChar);
}
setState(CoreState state) {
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
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;
}
setFdMap(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.setFdMap(idChar);
malloc.free(idChar);
}
Future<String> quickStart(
String homeDir,
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 stateParams = json.encode(state);
final homeChar = homeDir.toNativeUtf8().cast<Char>();
final paramsChar = params.toNativeUtf8().cast<Char>();
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
clashFFI.quickStart(
homeChar,
paramsChar,
stateParamsChar,
receiver.sendPort.nativePort,
);
malloc.free(homeChar);
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:fl_clash/models/models.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'core.dart';
class ClashMessage { class ClashMessage {
final controller = StreamController<String>(); StreamSubscription? subscription;
ClashMessage._() { ClashMessage._() {
controller.stream.listen( if (subscription != null) {
(message) { subscription!.cancel();
if(message.isEmpty){ subscription = null;
return; }
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._(); static final ClashMessage instance = ClashMessage._();

View File

@@ -1,145 +1,54 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io'; 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/common/common.dart';
import 'package:fl_clash/models/core.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 { import 'core.dart';
static ClashService? _instance;
Completer<ServerSocket> serverCompleter = Completer(); class ClashService {
Future<void> initGeo() async {
Completer<Socket> socketCompleter = Completer(); final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
Process? process; final isExists = await homeDir.exists();
if (!isExists) {
factory ClashService() { await homeDir.create(recursive: true);
_instance ??= ClashService._internal();
return _instance!;
}
ClashService._internal() {
_initServer();
reStart();
}
_initServer() 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()),
),
);
},
);
} }
} const geoFileNameList = [
mmdbFileName,
@override geoIpFileName,
reStart() async { geoSiteFileName,
if (process != null) { asnFileName,
await shutdown(); ];
} try {
final serverSocket = await serverCompleter.future; for (final geoFileName in geoFileNameList) {
final arg = Platform.isWindows final geoFile = File(
? "${serverSocket.port}" join(homePath, geoFileName),
: serverSocket.address.address; );
bool isSuccess = false; final isExists = await geoFile.exists();
if (Platform.isWindows && await system.checkIsAdmin()) { if (isExists) {
isSuccess = await request.startCoreByHelper(arg); continue;
} }
if (isSuccess) { final data = await rootBundle.load('assets/data/$geoFileName');
return; List<int> bytes = data.buffer.asUint8List();
} await geoFile.writeAsBytes(bytes, flush: true);
process = await Process.start(
appPath.corePath,
[
arg,
],
);
process!.stdout.listen((_) {});
}
@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();
} }
} catch (e) {
debugPrint("$e");
exit(0);
} }
} }
_destroySocket() async { Future<bool> init({
if (socketCompleter.isCompleted) { required ClashConfig clashConfig,
final lastSocket = await socketCompleter.future; required Config config,
await lastSocket.close(); }) async {
socketCompleter = Completer(); await initGeo();
} final homeDirPath = await appPath.getHomeDirPath();
} final isInit = clashCore.init(homeDirPath);
return isInit;
@override
shutdown() async {
await super.shutdown();
if (Platform.isWindows) {
await request.stopCoreByHelper();
}
await _destroySocket();
process?.kill();
process = null;
return true;
}
@override
Future<bool> preload() async {
await serverCompleter.future;
return true;
} }
} }
final clashService = system.isDesktop ? ClashService() : null; final clashService = ClashService();

View File

@@ -1,13 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/app.dart';
import 'package:fl_clash/state.dart';
class Android { class Android {
init() async { init() async {
app?.onExit = () async { app?.onExit = () {};
await globalState.appController.savePreferences();
};
} }
} }

View File

@@ -1,20 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension ColorExtension on Color { extension ColorExtension on Color {
toLight() {
Color get toLight {
return withOpacity(0.8);
}
Color get toLighter {
return withOpacity(0.6); return withOpacity(0.6);
} }
Color get toSoft { toLighter() {
return withOpacity(0.4);
}
toSoft() {
return withOpacity(0.12); return withOpacity(0.12);
} }
Color get toLittle { toLittle() {
return withOpacity(0.03); return withOpacity(0.03);
} }
@@ -24,39 +23,14 @@ extension ColorExtension on Color {
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor(); return hslDark.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,
)!;
}
} }
extension ColorSchemeExtension on ColorScheme { extension ColorSchemeExtension on ColorScheme {
ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack
? copyWith( ? copyWith(
surface: Colors.black, surface: Colors.black,
surfaceContainer: surfaceContainer.darken( background: Colors.black,
0.05, surfaceContainer: surfaceContainer.darken(0.05),
),
) )
: this; : this;
} }

View File

@@ -1,38 +1,28 @@
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 'other.dart';
export 'package.dart';
export 'path.dart'; export 'path.dart';
export 'picker.dart';
export 'preferences.dart';
export 'protocol.dart';
export 'proxy.dart';
export 'request.dart'; export 'request.dart';
export 'scroll.dart'; export 'preferences.dart';
export 'string.dart'; export 'constant.dart';
export 'system.dart'; export 'proxy.dart';
export 'text.dart'; export 'other.dart';
export 'tray.dart'; export 'num.dart';
export 'navigation.dart';
export 'window.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 'windows.dart';
export 'render.dart'; export 'iterable.dart';
export 'view.dart'; export 'scroll.dart';

View File

@@ -1,45 +1,29 @@
import 'dart:io';
import 'dart:math';
import 'dart:ui'; 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/enum/enum.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash"; const appName = "FlClash";
const appHelperService = "FlClashHelperService";
const coreName = "clash.meta"; const coreName = "clash.meta";
const packageName = "com.follow.clash"; const packageName = "com.follow.clash";
final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock";
const helperPort = 47890;
const helperTag = "2024125";
const baseInfoEdgeInsets = EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
);
const httpTimeoutDuration = Duration(milliseconds: 5000); const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100); const moreDuration = Duration(milliseconds: 100);
const animateDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100);
const commonDuration = Duration(milliseconds: 300);
const defaultUpdateDuration = Duration(days: 1); const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb"; const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb"; const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat"; const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat"; const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop final double kHeaderHeight = system.isDesktop ? 40 : 0;
? !Platform.isMacOS
? 40
: 28
: 0;
const GeoXMap defaultGeoXMap = { const GeoXMap defaultGeoXMap = {
"mmdb": "mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",
"asn": "asn":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb", "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb",
"geoip": "geoip":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoIP.dat",
"geosite": "geosite":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat" "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
}; };
@@ -62,21 +46,6 @@ final filter = ImageFilter.blur(
tileMode: TileMode.mirror, tileMode: TileMode.mirror,
); );
const navigationItemListEquality = ListEquality<NavigationItem>();
const connectionListEquality = ListEquality<Connection>();
const stringListEquality = ListEquality<String>();
const logListEquality = ListEquality<Log>();
const groupListEquality = ListEquality<Group>();
const externalProviderListEquality = ListEquality<ExternalProvider>();
const packageListEquality = ListEquality<Package>();
const hotKeyActionListEquality = ListEquality<HotKeyAction>();
const stringAndStringMapEquality = MapEquality<String, String>();
const stringAndStringMapEntryIterableEquality =
IterableEquality<MapEntry<String, String>>();
const delayMapEquality = MapEquality<String, Map<String, int?>>();
const stringSetEquality = SetEquality<String>();
const keyboardModifierListEquality = SetEquality<KeyboardModifier>();
const viewModeColumnsMap = { const viewModeColumnsMap = {
ViewMode.mobile: [2, 1], ViewMode.mobile: [2, 1],
ViewMode.laptop: [3, 2], ViewMode.laptop: [3, 2],
@@ -84,11 +53,3 @@ const viewModeColumnsMap = {
}; };
const defaultPrimaryColor = Colors.brown; const defaultPrimaryColor = Colors.brown;
double getWidgetHeight(num lines) {
return max(lines * 84 + (lines - 1) * 16, 0);
}
final mainIsolate = "FlClashMainIsolate";
final serviceIsolate = "FlClashServiceIsolate";

View File

@@ -1,44 +1,21 @@
import 'package:fl_clash/manager/manager.dart';
import 'package:fl_clash/widgets/scaffold.dart'; import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension BuildContextExtension on BuildContext { extension BuildContextExtension on BuildContext {
CommonScaffoldState? get commonScaffoldState { CommonScaffoldState? get commonScaffoldState {
return findAncestorStateOfType<CommonScaffoldState>(); return findAncestorStateOfType<CommonScaffoldState>();
} }
showNotifier(String text) { Size get appSize{
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
Size get appSize {
return MediaQuery.of(this).size; return MediaQuery.of(this).size;
} }
double get viewWidth { double get width {
return appSize.width; return appSize.width;
} }
ColorScheme get colorScheme => Theme.of(this).colorScheme; ColorScheme get colorScheme => Theme.of(this).colorScheme;
TextTheme get textTheme => Theme.of(this).textTheme; TextTheme get textTheme => Theme.of(this).textTheme;
T? findLastStateOfType<T extends State>() {
T? state;
visitor(Element element) {
if(!element.mounted){
return;
}
if(element is StatefulElement){
if (element.state is T) {
state = element.state as T;
}
}
element.visitChildren(visitor);
}
visitor(this as Element);
return state;
}
} }

View File

@@ -8,7 +8,6 @@ import 'package:webdav_client/webdav_client.dart';
class DAVClient { class DAVClient {
late Client client; late Client client;
Completer<bool> pingCompleter = Completer(); Completer<bool> pingCompleter = Completer();
late String fileName;
DAVClient(DAV dav) { DAVClient(DAV dav) {
client = newClient( client = newClient(
@@ -16,7 +15,6 @@ class DAVClient {
user: dav.user, user: dav.user,
password: dav.password, password: dav.password,
); );
fileName = dav.fileName;
client.setHeaders( client.setHeaders(
{ {
'accept-charset': 'utf-8', 'accept-charset': 'utf-8',
@@ -24,8 +22,8 @@ class DAVClient {
}, },
); );
client.setConnectTimeout(8000); client.setConnectTimeout(8000);
client.setSendTimeout(60000); client.setSendTimeout(8000);
client.setReceiveTimeout(60000); client.setReceiveTimeout(8000);
pingCompleter.complete(_ping()); pingCompleter.complete(_ping());
} }
@@ -40,7 +38,7 @@ class DAVClient {
get root => "/$appName"; get root => "/$appName";
get backupFile => "$root/$fileName"; get backupFile => "$root/backup.zip";
backup(Uint8List data) async { backup(Uint8List data) async {
await client.mkdir("$root"); await client.mkdir("$root");

View File

@@ -1,69 +1,26 @@
import 'dart:async'; import 'dart:async';
class Debouncer { class Debouncer {
final Map<dynamic, Timer> _operations = {}; final Duration delay;
Timer? _timer;
call( Debouncer({required this.delay});
dynamic tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) {
timer.cancel();
}
_operations[tag] = Timer(
duration,
() {
_operations[tag]?.cancel();
_operations.remove(tag);
Function.apply(
func,
args,
);
},
);
}
cancel(dynamic tag) { void call(Function action, List<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
_operations[tag]?.cancel(); _timer?.cancel();
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
} }
} }
class Throttler { Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
final Map<dynamic, Timer> _operations = {}; Timer? timer;
call( return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
String tag,
Function func, {
List<dynamic>? args,
Duration duration = const Duration(milliseconds: 600),
}) {
final timer = _operations[tag];
if (timer != null) { if (timer != null) {
return true; timer!.cancel();
} }
_operations[tag] = Timer( timer = Timer(Duration(milliseconds: milliseconds), () async {
duration, await Function.apply(func, args ?? [], namedArgs);
() { });
_operations[tag]?.cancel(); };
_operations.remove(tag); }
Function.apply(
func,
args,
);
},
);
return false;
}
cancel(dynamic tag) {
_operations[tag]?.cancel();
}
}
final debouncer = Debouncer();
final throttler = Throttler();

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: 1);
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,26 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import '../state.dart';
import 'constant.dart';
class FlClashHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
final client = super.createHttpClient(context);
client.badCertificateCallback = (_, __, ___) => true;
client.findProxy = (url) {
if ([localhost].contains(url.host)) {
return "DIRECT";
}
final appController = globalState.appController;
final port = appController.clashConfig.mixedPort;
final isStart = appController.appFlowingState.isStart;
debugPrint("find $url proxy:$isStart");
if (!isStart) return "DIRECT";
return "PROXY localhost:$port";
};
return client;
}
}

View File

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

View File

@@ -62,6 +62,6 @@ extension DoubleListExt on List<double> {
} }
} }
return -1; return -1; // 这行理论上不会执行到,但为了完整性保留
} }
} }

View File

@@ -1,106 +0,0 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:uni_platform/uni_platform.dart';
final Map<PhysicalKeyboardKey, String> _knownKeyLabels =
<PhysicalKeyboardKey, String>{
PhysicalKeyboardKey.keyA: 'A',
PhysicalKeyboardKey.keyB: 'B',
PhysicalKeyboardKey.keyC: 'C',
PhysicalKeyboardKey.keyD: 'D',
PhysicalKeyboardKey.keyE: 'E',
PhysicalKeyboardKey.keyF: 'F',
PhysicalKeyboardKey.keyG: 'G',
PhysicalKeyboardKey.keyH: 'H',
PhysicalKeyboardKey.keyI: 'I',
PhysicalKeyboardKey.keyJ: 'J',
PhysicalKeyboardKey.keyK: 'K',
PhysicalKeyboardKey.keyL: 'L',
PhysicalKeyboardKey.keyM: 'M',
PhysicalKeyboardKey.keyN: 'N',
PhysicalKeyboardKey.keyO: 'O',
PhysicalKeyboardKey.keyP: 'P',
PhysicalKeyboardKey.keyQ: 'Q',
PhysicalKeyboardKey.keyR: 'R',
PhysicalKeyboardKey.keyS: 'S',
PhysicalKeyboardKey.keyT: 'T',
PhysicalKeyboardKey.keyU: 'U',
PhysicalKeyboardKey.keyV: 'V',
PhysicalKeyboardKey.keyW: 'W',
PhysicalKeyboardKey.keyX: 'X',
PhysicalKeyboardKey.keyY: 'Y',
PhysicalKeyboardKey.keyZ: 'Z',
PhysicalKeyboardKey.digit1: '1',
PhysicalKeyboardKey.digit2: '2',
PhysicalKeyboardKey.digit3: '3',
PhysicalKeyboardKey.digit4: '4',
PhysicalKeyboardKey.digit5: '5',
PhysicalKeyboardKey.digit6: '6',
PhysicalKeyboardKey.digit7: '7',
PhysicalKeyboardKey.digit8: '8',
PhysicalKeyboardKey.digit9: '9',
PhysicalKeyboardKey.digit0: '0',
PhysicalKeyboardKey.enter: 'ENTER',
PhysicalKeyboardKey.escape: 'ESCAPE',
PhysicalKeyboardKey.backspace: 'BACKSPACE',
PhysicalKeyboardKey.tab: 'TAB',
PhysicalKeyboardKey.space: 'SPACE',
PhysicalKeyboardKey.minus: '-',
PhysicalKeyboardKey.equal: '=',
PhysicalKeyboardKey.bracketLeft: '[',
PhysicalKeyboardKey.bracketRight: ']',
PhysicalKeyboardKey.backslash: '\\',
PhysicalKeyboardKey.semicolon: ';',
PhysicalKeyboardKey.quote: '"',
PhysicalKeyboardKey.backquote: '`',
PhysicalKeyboardKey.comma: ',',
PhysicalKeyboardKey.period: '.',
PhysicalKeyboardKey.slash: '/',
PhysicalKeyboardKey.capsLock: 'CAPSLOCK',
PhysicalKeyboardKey.f1: 'F1',
PhysicalKeyboardKey.f2: 'F2',
PhysicalKeyboardKey.f3: 'F3',
PhysicalKeyboardKey.f4: 'F4',
PhysicalKeyboardKey.f5: 'F5',
PhysicalKeyboardKey.f6: 'F6',
PhysicalKeyboardKey.f7: 'F7',
PhysicalKeyboardKey.f8: 'F8',
PhysicalKeyboardKey.f9: 'F9',
PhysicalKeyboardKey.f10: 'F10',
PhysicalKeyboardKey.f11: 'F11',
PhysicalKeyboardKey.f12: 'F12',
PhysicalKeyboardKey.home: 'HOME',
PhysicalKeyboardKey.pageUp: 'PAGEUP',
PhysicalKeyboardKey.delete: 'DELETE',
PhysicalKeyboardKey.end: 'END',
PhysicalKeyboardKey.pageDown: 'PAGEDOWN',
PhysicalKeyboardKey.arrowRight: '',
PhysicalKeyboardKey.arrowLeft: '',
PhysicalKeyboardKey.arrowDown: '',
PhysicalKeyboardKey.arrowUp: '',
PhysicalKeyboardKey.controlLeft: "CTRL",
PhysicalKeyboardKey.shiftLeft: 'SHIFT',
PhysicalKeyboardKey.altLeft: "ALT",
PhysicalKeyboardKey.metaLeft: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.controlRight: "CTRL",
PhysicalKeyboardKey.shiftRight: 'SHIFT',
PhysicalKeyboardKey.altRight: "ALT",
PhysicalKeyboardKey.metaRight: Platform.isMacOS ? '' : 'WIN',
PhysicalKeyboardKey.fn: 'FN',
};
extension KeyboardKeyExt on KeyboardKey {
String get label {
PhysicalKeyboardKey? physicalKey;
if (this is LogicalKeyboardKey) {
physicalKey = (this as LogicalKeyboardKey).physicalKey;
} else if (this is PhysicalKeyboardKey) {
physicalKey = this as PhysicalKeyboardKey;
}
return _knownKeyLabels[physicalKey] ?? physicalKey?.debugName ?? 'Unknown';
}
}

View File

@@ -1,10 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fl_clash/models/models.dart' hide Process;
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'constant.dart'; import 'constant.dart';
import 'system.dart'; import 'system.dart';
import 'windows.dart';
class AutoLaunch { class AutoLaunch {
static AutoLaunch? _instance; static AutoLaunch? _instance;
@@ -25,15 +26,75 @@ class AutoLaunch {
return await launchAtStartup.isEnabled(); 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 { Future<bool> enable() async {
if (Platform.isWindows) {
await windowsDisable();
}
return await launchAtStartup.enable(); 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 windows?.runas(
'schtasks',
[
'/Create',
'/SC',
'ONLOGON',
'/TN',
appName,
'/TR',
Platform.resolvedExecutable,
'/RL',
'HIGHEST',
'/F'
].join(" "),
) ??
false;
}
Future<bool> disable() async { Future<bool> disable() async {
return await launchAtStartup.disable(); return await launchAtStartup.disable();
} }
updateStatus(bool isAutoLaunch) async { updateStatus(AutoLaunchState state) async {
final isOpenTun = state.isOpenTun;
final isAutoLaunch = state.isAutoLaunch;
if (Platform.isWindows && isOpenTun) {
if (await windowsIsEnable == isAutoLaunch) return;
if (isAutoLaunch) {
final isEnable = await windowsEnable();
if (!isEnable) {
enable();
}
} else {
windowsDisable();
}
return;
}
if (await isEnable == isAutoLaunch) return; if (await isEnable == isAutoLaunch) return;
if (isAutoLaunch == true) { if (isAutoLaunch == true) {
enable(); enable();

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart';
typedef InstallConfigCallBack = void Function(String url); typedef InstallConfigCallBack = void Function(String url);
@@ -17,7 +17,7 @@ class LinkManager {
initAppLinksListen(installConfigCallBack) async { initAppLinksListen(installConfigCallBack) async {
debugPrint("initAppLinksListen"); debugPrint("initAppLinksListen");
destroy(); destroy();
subscription = _appLinks.uriLinkStream.listen( subscription = _appLinks.allUriLinkStream.listen(
(uri) { (uri) {
debugPrint('onAppLink: $uri'); debugPrint('onAppLink: $uri');
if (uri.host == 'install-config') { if (uri.host == 'install-config') {
@@ -31,7 +31,8 @@ class LinkManager {
); );
} }
destroy() {
destroy(){
if (subscription != null) { if (subscription != null) {
subscription?.cancel(); subscription?.cancel();
subscription = null; subscription = null;

View File

@@ -1,76 +1,5 @@
import 'dart:collection';
class FixedList<T> {
final int maxLength;
final List<T> _list = [];
FixedList(this.maxLength);
add(T item) {
if (_list.length == maxLength) {
_list.removeAt(0);
}
_list.add(item);
}
List<T> get list => List.unmodifiable(_list);
int get length => _list.length;
T operator [](int index) => _list[index];
}
class FixedMap<K, V> {
final int maxSize;
final Map<K, V> _map = {};
final Queue<K> _queue = Queue<K>();
FixedMap(this.maxSize);
put(K key, V value) {
if (_map.length == maxSize) {
final oldestKey = _queue.removeFirst();
_map.remove(oldestKey);
}
_map[key] = value;
_queue.add(key);
}
clear(){
_map.clear();
_queue.clear();
}
V? get(K key) => _map[key];
bool containsKey(K key) => _map.containsKey(key);
int get length => _map.length;
Map<K, V> get map => Map.unmodifiable(_map);
}
extension ListExtension<T> on List<T> { extension ListExtension<T> on List<T> {
List<T> intersection(List<T> list) { List<T> intersection(List<T> list) {
return where((item) => list.contains(item)).toList(); return where((item) => list.contains(item)).toList();
} }
}
List<List<T>> batch(int maxConcurrent) {
final batches = (length / maxConcurrent).ceil();
final List<List<T>> res = [];
for (int i = 0; i < batches; i++) {
if (i != batches - 1) {
res.add(sublist(i * maxConcurrent, maxConcurrent * (i + 1)));
} else {
res.add(sublist(i * maxConcurrent, length));
}
}
return res;
}
List<T> safeSublist(int start) {
if (start <= 0) return this;
if (start > length) return [];
return sublist(start);
}
}

View File

@@ -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();

View File

@@ -3,31 +3,24 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class Measure { class Measure {
final TextScaler _textScale; Measure.of(this.context);
late BuildContext context;
Measure.of(this.context) final _textScaleFactor =
: _textScale = TextScaler.linear( WidgetsBinding.instance.platformDispatcher.textScaleFactor;
WidgetsBinding.instance.platformDispatcher.textScaleFactor,
);
Size computeTextSize( Size computeTextSize(Text text) {
Text text, {
double maxWidth = double.infinity,
}) {
final textPainter = TextPainter( final textPainter = TextPainter(
text: TextSpan(text: text.data, style: text.style), text: TextSpan(text: text.data, style: text.style),
maxLines: text.maxLines, maxLines: text.maxLines,
textScaler: _textScale, textScaler: TextScaler.linear(_textScaleFactor),
textDirection: text.textDirection ?? TextDirection.ltr, textDirection: text.textDirection ?? TextDirection.ltr,
)..layout( )..layout();
maxWidth: maxWidth,
);
return textPainter.size; return textPainter.size;
} }
late BuildContext context;
double? _bodyMediumHeight; double? _bodyMediumHeight;
Size? _bodyLargeSize;
double? _bodySmallHeight; double? _bodySmallHeight;
double? _labelSmallHeight; double? _labelSmallHeight;
double? _labelMediumHeight; double? _labelMediumHeight;
@@ -37,27 +30,17 @@ class Measure {
double get bodyMediumHeight { double get bodyMediumHeight {
_bodyMediumHeight ??= computeTextSize( _bodyMediumHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.bodyMedium, style: context.textTheme.bodyMedium,
), ),
).height; ).height;
return _bodyMediumHeight!; return _bodyMediumHeight!;
} }
Size get bodyLargeSize {
_bodyLargeSize ??= computeTextSize(
Text(
"X",
style: context.textTheme.bodyLarge,
),
);
return _bodyLargeSize!;
}
double get bodySmallHeight { double get bodySmallHeight {
_bodySmallHeight ??= computeTextSize( _bodySmallHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.bodySmall, style: context.textTheme.bodySmall,
), ),
).height; ).height;
@@ -67,7 +50,7 @@ class Measure {
double get labelSmallHeight { double get labelSmallHeight {
_labelSmallHeight ??= computeTextSize( _labelSmallHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.labelSmall, style: context.textTheme.labelSmall,
), ),
).height; ).height;
@@ -77,7 +60,7 @@ class Measure {
double get labelMediumHeight { double get labelMediumHeight {
_labelMediumHeight ??= computeTextSize( _labelMediumHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.labelMedium, style: context.textTheme.labelMedium,
), ),
).height; ).height;
@@ -87,7 +70,7 @@ class Measure {
double get titleLargeHeight { double get titleLargeHeight {
_titleLargeHeight ??= computeTextSize( _titleLargeHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.titleLarge, style: context.textTheme.titleLarge,
), ),
).height; ).height;
@@ -97,7 +80,7 @@ class Measure {
double get titleMediumHeight { double get titleMediumHeight {
_titleMediumHeight ??= computeTextSize( _titleMediumHeight ??= computeTextSize(
Text( Text(
"X", "",
style: context.textTheme.titleMedium, style: context.textTheme.titleMedium,
), ),
).height; ).height;

View File

@@ -30,16 +30,16 @@ class Navigation {
fragment: ProfilesFragment(), fragment: ProfilesFragment(),
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.view_timeline), icon: Icon(Icons.view_timeline),
label: "requests", label: "requests",
fragment: RequestsFragment(), fragment: RequestsFragment(),
description: "requestsDesc", description: "requestsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
const NavigationItem( const NavigationItem(
icon: Icon(Icons.ballot), icon: Icon(Icons.ballot),
label: "connections", label: "connections",
fragment: ConnectionsFragment(), fragment: ConnectionsFragment(),
description: "connectionsDesc", description: "connectionsDesc",
modes: [NavigationItemMode.desktop, NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
@@ -49,7 +49,7 @@ class Navigation {
description: "resourcesDesc", description: "resourcesDesc",
keep: false, keep: false,
fragment: Resources(), fragment: Resources(),
modes: [NavigationItemMode.more], modes: [NavigationItemMode.desktop, NavigationItemMode.more],
), ),
NavigationItem( NavigationItem(
icon: const Icon(Icons.adb), icon: const Icon(Icons.adb),

View File

@@ -1,299 +0,0 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
if (!globalState.appController.isMobileView) {
return await Navigator.of(context).push<T>(
CommonDesktopRoute(
builder: (context) => child,
),
);
}
return await Navigator.of(context).push<T>(
CommonRoute(
builder: (context) => child,
),
);
}
}
class CommonDesktopRoute<T> extends PageRoute<T> {
final Widget Function(BuildContext context) builder;
CommonDesktopRoute({
required this.builder,
});
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
final Widget result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: FadeTransition(
opacity: animation,
child: result,
),
);
}
@override
bool get maintainState => true;
@override
Duration get transitionDuration => Duration(milliseconds: 200);
@override
Duration get reverseTransitionDuration => Duration(milliseconds: 200);
}
class CommonRoute<T> extends MaterialPageRoute<T> {
CommonRoute({
required super.builder,
});
@override
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
}
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
);
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0 / 3.0, 0.0),
);
class CommonPageTransitionsBuilder extends PageTransitionsBuilder {
const CommonPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return CommonPageTransition(
context: context,
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: false,
child: child,
);
}
}
class CommonPageTransition extends StatefulWidget {
const CommonPageTransition({
super.key,
required this.context,
required this.primaryRouteAnimation,
required this.secondaryRouteAnimation,
required this.child,
required this.linearTransition,
});
final Widget child;
final Animation<double> primaryRouteAnimation;
final Animation<double> secondaryRouteAnimation;
final BuildContext context;
final bool linearTransition;
static Widget? delegatedTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
bool allowSnapshotting,
Widget? child) {
final Animation<Offset> delegatedPositionAnimation = CurvedAnimation(
parent: secondaryAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
).drive(_kMiddleLeftTween);
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: delegatedPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: child,
);
}
@override
State<CommonPageTransition> createState() => _CommonPageTransitionState();
}
class _CommonPageTransitionState extends State<CommonPageTransition> {
late Animation<Offset> _primaryPositionAnimation;
late Animation<Offset> _secondaryPositionAnimation;
late Animation<Decoration> _primaryShadowAnimation;
CurvedAnimation? _primaryPositionCurve;
CurvedAnimation? _secondaryPositionCurve;
CurvedAnimation? _primaryShadowCurve;
@override
void initState() {
super.initState();
_setupAnimation();
}
@override
void didUpdateWidget(covariant CommonPageTransition oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation ||
oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation ||
oldWidget.linearTransition != widget.linearTransition) {
_disposeCurve();
_setupAnimation();
}
}
@override
void dispose() {
_disposeCurve();
super.dispose();
}
void _disposeCurve() {
_primaryPositionCurve?.dispose();
_secondaryPositionCurve?.dispose();
_primaryShadowCurve?.dispose();
_primaryPositionCurve = null;
_secondaryPositionCurve = null;
_primaryShadowCurve = null;
}
void _setupAnimation() {
if (!widget.linearTransition) {
_primaryPositionCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.fastEaseInToSlowEaseOut,
reverseCurve: Curves.easeInOut,
);
_secondaryPositionCurve = CurvedAnimation(
parent: widget.secondaryRouteAnimation,
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
);
_primaryShadowCurve = CurvedAnimation(
parent: widget.primaryRouteAnimation,
curve: Curves.linearToEaseOut,
);
}
_primaryPositionAnimation =
(_primaryPositionCurve ?? widget.primaryRouteAnimation)
.drive(_kRightMiddleTween);
_secondaryPositionAnimation =
(_secondaryPositionCurve ?? widget.secondaryRouteAnimation)
.drive(_kMiddleLeftTween);
_primaryShadowAnimation =
(_primaryShadowCurve ?? widget.primaryRouteAnimation).drive(
DecorationTween(
begin: const _CommonEdgeShadowDecoration(),
end: _CommonEdgeShadowDecoration(
<Color>[
widget.context.colorScheme.inverseSurface.withOpacity(
0.06,
),
Colors.transparent,
],
),
),
);
}
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
return SlideTransition(
position: _secondaryPositionAnimation,
textDirection: textDirection,
transformHitTests: false,
child: SlideTransition(
position: _primaryPositionAnimation,
textDirection: textDirection,
child: DecoratedBoxTransition(
decoration: _primaryShadowAnimation,
child: widget.child,
),
),
);
}
}
class _CommonEdgeShadowDecoration extends Decoration {
final List<Color>? _colors;
const _CommonEdgeShadowDecoration([this._colors]);
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _CommonEdgeShadowPainter(this, onChanged);
}
}
class _CommonEdgeShadowPainter extends BoxPainter {
_CommonEdgeShadowPainter(
this._decoration,
super.onChanged,
) : assert(_decoration._colors == null || _decoration._colors!.length > 1);
final _CommonEdgeShadowDecoration _decoration;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
final List<Color>? colors = _decoration._colors;
if (colors == null) {
return;
}
final double shadowWidth = 0.05 * configuration.size!.width;
final double shadowHeight = configuration.size!.height;
final double bandWidth = shadowWidth / (colors.length - 1);
final TextDirection? textDirection = configuration.textDirection;
assert(textDirection != null);
final (double shadowDirection, double start) = switch (textDirection!) {
TextDirection.rtl => (1, offset.dx + configuration.size!.width),
TextDirection.ltr => (-1, offset.dx),
};
int bandColorIndex = 0;
for (int dx = 0; dx < shadowWidth; dx += 1) {
if (dx ~/ bandWidth != bandColorIndex) {
bandColorIndex += 1;
}
final Paint paint = Paint()
..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1],
(dx % bandWidth) / bandWidth)!;
final double x = start + shadowDirection * dx;
canvas.drawRect(
Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint);
}
}
}

View File

@@ -1,25 +0,0 @@
import 'dart:io';
extension NetworkInterfaceExt on NetworkInterface {
bool get isWifi {
final nameLowCase = name.toLowerCase();
if (nameLowCase.contains('wlan') ||
nameLowCase.contains('wi-fi') ||
nameLowCase == 'en0' ||
nameLowCase == 'eth0') {
return true;
}
return false;
}
bool get includesIPv4 {
return addresses.any((addr) => addr.isIPv4);
}
}
extension InternetAddressExt on InternetAddress {
bool get isIPv4 {
return type == InternetAddressType.IPv4;
}
}

View File

@@ -1,43 +1,5 @@
import 'package:flutter/foundation.dart'; extension NumExtension on num {
import 'package:flutter/material.dart';
extension NumExt on num {
String fixed({digit = 2}) { String fixed({digit = 2}) {
return toStringAsFixed(truncateToDouble() == this ? 0 : digit); return toStringAsFixed(truncateToDouble() == this ? 0 : digit);
} }
} }
extension DoubleExt on double {
moreOrEqual(double value) {
return this > value || (value - this).abs() < precisionErrorTolerance + 1;
}
}
extension OffsetExt on Offset {
double getCrossAxisOffset(Axis direction) {
return direction == Axis.vertical ? dx : dy;
}
double getMainAxisOffset(Axis direction) {
return direction == Axis.vertical ? dy : dx;
}
bool less(Offset offset) {
if (dy < offset.dy) {
return true;
}
if (dy == offset.dy && dx < offset.dx) {
return true;
}
return false;
}
}
extension RectExt on Rect {
doRectIntersect(Rect rect) {
return left < rect.right &&
right > rect.left &&
top < rect.bottom &&
bottom > rect.top;
}
}

View File

@@ -1,11 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:typed_data';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lpinyin/lpinyin.dart'; import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
class Other { class Other {
Color? getDelayColor(int? delay) { Color? getDelayColor(int? delay) {
@@ -15,14 +17,6 @@ class Other {
return const Color(0xFFC57F0A); return const Color(0xFFC57F0A);
} }
String get id {
final timestamp = DateTime.now().microsecondsSinceEpoch;
final random = Random();
final randomStr =
String.fromCharCodes(List.generate(8, (_) => random.nextInt(26) + 97));
return "$timestamp$randomStr";
}
String getDateStringLast2(int value) { String getDateStringLast2(int value) {
var valueRaw = "0$value"; var valueRaw = "0$value";
return valueRaw.substring( return valueRaw.substring(
@@ -30,39 +24,6 @@ class Other {
); );
} }
String generateRandomString({int minLength = 10, int maxLength = 100}) {
const latinChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
final random = Random();
int length = minLength + random.nextInt(maxLength - minLength + 1);
String result = '';
for (int i = 0; i < length; i++) {
if (random.nextBool()) {
result +=
String.fromCharCode(0x4E00 + random.nextInt(0x9FA5 - 0x4E00 + 1));
} else {
result += latinChars[random.nextInt(latinChars.length)];
}
}
return result;
}
String get uuidV4 {
final Random random = Random();
final bytes = List.generate(16, (_) => random.nextInt(256));
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
final hex =
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}';
}
String getTimeDifference(DateTime dateTime) { String getTimeDifference(DateTime dateTime) {
var currentDateTime = DateTime.now(); var currentDateTime = DateTime.now();
var difference = currentDateTime.difference(dateTime); var difference = currentDateTime.difference(dateTime);
@@ -138,18 +99,12 @@ class Other {
} }
} }
String getTrayIconPath({ String getTrayIconPath() {
required Brightness brightness, if (Platform.isWindows) {
}) { return "assets/images/icon.ico";
if (Platform.isMacOS) { } else {
return "assets/images/icon_white.png"; return "assets/images/icon_monochrome.png";
} }
final suffix = Platform.isWindows ? "ico" : "png";
return "assets/images/icon.$suffix";
// return switch (brightness) {
// Brightness.dark => "assets/images/icon_white.$suffix",
// Brightness.light => "assets/images/icon_black.$suffix",
// };
} }
int compareVersions(String version1, String version2) { int compareVersions(String version1, String version2) {
@@ -175,37 +130,53 @@ class Other {
return build1.compareTo(build2); return build1.compareTo(build2);
} }
String getPinyin(String value) { Future<String?> parseQRCode(Uint8List? bytes) {
return value.isNotEmpty return Isolate.run<String?>(() {
? PinyinHelper.getFirstWordPinyin(value.substring(0, 1)) if (bytes == null) return null;
: ""; img.Image? image = img.decodeImage(bytes);
LuminanceSource source = RGBLuminanceSource(
image!.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final bitmap = BinaryBitmap(GlobalHistogramBinarizer(source));
final reader = QRCodeReader();
try {
final result = reader.decode(bitmap);
return result.text;
} catch (_) {
return null;
}
});
} }
String? getFileNameForDisposition(String? disposition) { String? getFileNameForDisposition(String? disposition) {
if (disposition == null) return null; if (disposition == null) return null;
final parseValue = HeaderValue.parse(disposition); final parseValue = HeaderValue.parse(disposition);
final parameters = parseValue.parameters; final parameters = parseValue.parameters;
final fileNamePointKey = parameters.keys final key = parameters.keys
.firstWhere((key) => key == "filename*", orElse: () => ""); .firstWhere((key) => key.startsWith("filename"), orElse: () => '');
if (fileNamePointKey.isNotEmpty) { if (key.isEmpty) return null;
final res = parameters[fileNamePointKey]?.split("''") ?? []; if (key == "filename*") {
if (res.length >= 2) { return Uri.decodeComponent((parameters[key] ?? "").split("'").last);
return Uri.decodeComponent(res[1]); } else {
} return parameters[key];
} }
final fileNameKey = parameters.keys
.firstWhere((key) => key == "filename", orElse: () => "");
if (fileNameKey.isEmpty) return null;
return parameters[fileNameKey];
} }
FlutterView getScreen() { double getViewWidth() {
return WidgetsBinding.instance.platformDispatcher.views.first; final view = WidgetsBinding.instance.platformDispatcher.views.first;
final size = view.physicalSize / view.devicePixelRatio;
return size.width;
} }
List<String> parseReleaseBody(String? body) { List<String> parseReleaseBody(String? body) {
if (body == null) return []; if (body == null) return [];
const pattern = r'- \s*(.*)'; const pattern = r'- (.+?)\. \[.+?\]';
final regex = RegExp(pattern); final regex = RegExp(pattern);
return regex return regex
.allMatches(body) .allMatches(body)
@@ -223,54 +194,19 @@ class Other {
int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) { int getProxiesColumns(double viewWidth, ProxiesLayout proxiesLayout) {
final columns = max((viewWidth / 300).ceil(), 2); final columns = max((viewWidth / 300).ceil(), 2);
return switch (proxiesLayout) { return switch (proxiesLayout) {
ProxiesLayout.tight => columns + 1, ProxiesLayout.tight => columns - 1,
ProxiesLayout.standard => columns, ProxiesLayout.standard => columns,
ProxiesLayout.loose => columns - 1, ProxiesLayout.loose => columns + 1,
}; };
} }
int getProfilesColumns(double viewWidth) { int getProfilesColumns(double viewWidth) {
return max((viewWidth / 350).floor(), 1); return max((viewWidth / 400).floor(), 1);
} }
String getBackupFileName() { String getBackupFileName() {
return "${appName}_backup_${DateTime.now().show}.zip"; return "${appName}_backup_${DateTime.now().show}.zip";
} }
String get logFile {
return "${appName}_${DateTime.now().show}.log";
}
Size getScreenSize() {
final view = WidgetsBinding.instance.platformDispatcher.views.first;
return view.physicalSize / view.devicePixelRatio;
}
Future<String?> getLocalIpAddress() async {
List<NetworkInterface> interfaces = await NetworkInterface.list(
includeLoopback: false,
)
..sort((a, b) {
if (a.isWifi && !b.isWifi) return -1;
if (!a.isWifi && b.isWifi) return 1;
if (a.includesIPv4 && !b.includesIPv4) return -1;
if (!a.includesIPv4 && b.includesIPv4) return 1;
return 0;
});
for (final interface in interfaces) {
final addresses = interface.addresses;
if (addresses.isEmpty) {
continue;
}
addresses.sort((a, b) {
if (a.isIPv4 && !b.isIPv4) return -1;
if (!a.isIPv4 && b.isIPv4) return 1;
return 0;
});
return addresses.first.address;
}
return "";
}
} }
final other = Other(); final other = Other();

View File

@@ -8,22 +8,34 @@ import 'constant.dart';
class AppPath { class AppPath {
static AppPath? _instance; static AppPath? _instance;
Completer<Directory> dataDir = Completer(); Completer<Directory> cacheDir = Completer();
Completer<Directory> downloadDir = Completer(); Completer<Directory> downloadDir = Completer();
Completer<Directory> tempDir = Completer();
late String appDirPath; // Future<Directory> _createDesktopCacheDir() async {
// final path = join(dirname(Platform.resolvedExecutable), 'cache');
// final dir = Directory(path);
// if (await dir.exists()) {
// await dir.create(recursive: true);
// }
// return dir;
// }
AppPath._internal() { AppPath._internal() {
appDirPath = join(dirname(Platform.resolvedExecutable));
getApplicationSupportDirectory().then((value) { getApplicationSupportDirectory().then((value) {
dataDir.complete(value); cacheDir.complete(value);
});
getTemporaryDirectory().then((value) {
tempDir.complete(value);
}); });
getDownloadsDirectory().then((value) { getDownloadsDirectory().then((value) {
downloadDir.complete(value); downloadDir.complete(value);
}); });
// if (Platform.isAndroid) {
// getApplicationSupportDirectory().then((value) {
// cacheDir.complete(value);
// });
// } else {
// _createDesktopCacheDir().then((value) {
// cacheDir.complete(value);
// });
// }
} }
factory AppPath() { factory AppPath() {
@@ -31,64 +43,26 @@ class AppPath {
return _instance!; return _instance!;
} }
String get executableExtension { Future<String> getDownloadDirPath() async {
return Platform.isWindows ? ".exe" : "";
}
String get executableDirPath {
final currentExecutablePath = Platform.resolvedExecutable;
return dirname(currentExecutablePath);
}
String get corePath {
return join(executableDirPath, "FlClashCore$executableExtension");
}
String get helperPath {
return join(executableDirPath, "$appHelperService$executableExtension");
}
Future<String> get downloadDirPath async {
final directory = await downloadDir.future; final directory = await downloadDir.future;
return directory.path; return directory.path;
} }
Future<String> get homeDirPath async { Future<String> getHomeDirPath() async {
final directory = await dataDir.future; final directory = await cacheDir.future;
return directory.path; return directory.path;
} }
Future<String> get lockFilePath async { Future<String> getProfilesPath() async {
final directory = await dataDir.future; final directory = await cacheDir.future;
return join(directory.path, "FlClash.lock");
}
Future<String> get sharedPreferencesPath async {
final directory = await dataDir.future;
return join(directory.path, "shared_preferences.json");
}
Future<String> get profilesPath async {
final directory = await dataDir.future;
return join(directory.path, profilesDirectoryName); return join(directory.path, profilesDirectoryName);
} }
Future<String?> getProfilePath(String? id) async { Future<String?> getProfilePath(String? id) async {
if (id == null) return null; if (id == null) return null;
final directory = await profilesPath; final directory = await getProfilesPath();
return join(directory, "$id.yaml"); return join(directory, "$id.yaml");
} }
Future<String?> getProvidersPath(String? id) async {
if (id == null) return null;
final directory = await profilesPath;
return join(directory, "providers", id);
}
Future<String> get tempPath async {
final directory = await tempDir.future;
return directory.path;
}
} }
final appPath = AppPath(); final appPath = AppPath();

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,50 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:math';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:fl_clash/clash/clash.dart'; import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/models/ip.dart';
import 'package:fl_clash/state.dart'; import 'package:fl_clash/state.dart';
import 'package:flutter/cupertino.dart';
class Request { class Request {
late final Dio _dio; late final Dio _dio;
String? userAgent; int? _port;
bool _isStart = false;
Request() { Request() {
_dio = Dio(); _dio = Dio();
_dio.interceptors.add( _dio.interceptors.add(
InterceptorsWrapper( InterceptorsWrapper(
onRequest: (options, handler) { onRequest: (options, handler) {
_updateAdapter();
return handler.next(options); // 继续请求 return handler.next(options); // 继续请求
}, },
), ),
); );
} }
_updateAdapter() {
final port = globalState.appController.clashConfig.mixedPort;
final isStart = globalState.appController.appState.isStart;
if (_port != port || isStart != _isStart) {
_port = port;
_isStart = isStart;
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.userAgent = globalState.appController.clashConfig.globalUa;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
return client;
},
validateCertificate: (_, __, ___) => true,
);
}
}
Future<Response> getFileResponseForUrl(String url) async { Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio final response = await _dio
.get( .get(
@@ -41,19 +62,6 @@ class Request {
return response; return response;
} }
Future<MemoryImage?> getImage(String url) async {
if (url.isEmpty) return null;
final response = await _dio.get<Uint8List>(
url,
options: Options(
responseType: ResponseType.bytes,
),
);
final data = response.data;
if (data == null) return null;
return MemoryImage(data);
}
Future<Map<String, dynamic>?> checkForUpdate() async { Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get( final response = await _dio.get(
"https://api.github.com/repos/$repository/releases/latest", "https://api.github.com/repos/$repository/releases/latest",
@@ -71,113 +79,36 @@ class Request {
return data; return data;
} }
final List<String> _ipInfoSources = [ final Map<String, IpInfo Function(Map<String, dynamic>)> _ipInfoSources = {
"https://ipwho.is/?fields=ip&output=csv", "https://ipwho.is/": IpInfo.fromIpwhoIsJson,
"https://ipinfo.io/ip", "https://api.ip.sb/geoip/": IpInfo.fromIpSbJson,
"https://ifconfig.me/ip/", "https://ipapi.co/json/": IpInfo.fromIpApiCoJson,
]; "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson,
};
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async { Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources) { for (final source in _ipInfoSources.entries.toList()..shuffle(Random())) {
try { try {
final response = await _dio final response = await _dio
.get<String>( .get<Map<String, dynamic>>(
source, source.key,
cancelToken: cancelToken, cancelToken: cancelToken,
) )
.timeout(httpTimeoutDuration); .timeout(
if (response.statusCode != 200 || response.data == null) { httpTimeoutDuration,
continue; );
if (response.statusCode == 200 && response.data != null) {
return source.value(response.data!);
} }
final ipInfo = await clashCore.getCountryCode(response.data!);
if (ipInfo == null && source != _ipInfoSources.last) {
continue;
}
return ipInfo;
} catch (e) { } catch (e) {
debugPrint("checkIp error ===> $e"); if(cancelToken?.isCancelled == true){
if (e is DioException && e.type == DioExceptionType.cancel) {
throw "cancelled"; throw "cancelled";
} }
continue;
} }
} }
return null; return null;
} }
Future<bool> pingHelper() async {
try {
final response = await _dio
.get(
"http://$localhost:$helperPort/ping",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
return (response.data as String) == helperTag;
} catch (_) {
return false;
}
}
Future<bool> startCoreByHelper(String arg) async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/start",
data: json.encode({
"path": appPath.corePath,
"arg": arg,
}),
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
Future<bool> stopCoreByHelper() async {
try {
final response = await _dio
.post(
"http://$localhost:$helperPort/stop",
options: Options(
responseType: ResponseType.plain,
),
)
.timeout(
const Duration(
milliseconds: 2000,
),
);
if (response.statusCode != HttpStatus.ok) {
return false;
}
final data = response.data as String;
return data.isEmpty;
} catch (_) {
return false;
}
}
} }
final request = Request(); final request = Request();

View File

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

View File

@@ -1,8 +1,3 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
extension StringExtension on String { extension StringExtension on String {
bool get isUrl { bool get isUrl {
return RegExp(r'^(http|https|ftp)://').hasMatch(this); return RegExp(r'^(http|https|ftp)://').hasMatch(this);
@@ -13,38 +8,4 @@ extension StringExtension on String {
other.toLowerCase(), other.toLowerCase(),
); );
} }
List<int> get encodeUtf16LeWithBom {
final byteData = ByteData(length * 2);
final bom = [0xFF, 0xFE];
for (int i = 0; i < length; i++) {
int charCode = codeUnitAt(i);
byteData.setUint16(i * 2, charCode, Endian.little);
}
return bom + byteData.buffer.asUint8List();
}
Uint8List? get getBase64 {
final regExp = RegExp(r'base64,(.*)');
final match = regExp.firstMatch(this);
final realValue = match?.group(1) ?? '';
if (realValue.isEmpty) {
return null;
}
try {
return base64.decode(realValue);
} catch (e) {
return null;
}
}
bool get isRegex {
try {
RegExp(this);
return true;
} catch (e) {
debugPrint(e.toString());
return false;
}
}
} }

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