Compare commits
68 Commits
v0.8.58
...
v0.8.82-pre.2
| Author | SHA1 | Date | |
|---|---|---|---|
| c2f3b13deb | |||
| edc4714488 | |||
| aec325a19a | |||
| a77b3a35e8 | |||
| 2d2708d7bd | |||
| ef5f6dbd59 | |||
| b6c7b15e3e | |||
| de9c5ba9cc | |||
| 2aae00cf68 | |||
| 68be2d34a1 | |||
| 7895ccf720 | |||
| e92900dbbd | |||
| eada271c49 | |||
| 5dda2854be | |||
| 5184ed6fc7 | |||
| 4e679f776e | |||
| 96328f66e9 | |||
| 3eb14ab8a1 | |||
| c6266b7917 | |||
| 6c27f2e2f1 | |||
| e04a0094b1 | |||
| 683e6a58ea | |||
| b340feeb49 | |||
| 6a39b7ef5a | |||
| 35f89fea90 | |||
| 58acd9c1ab | |||
| ef97ef40a1 | |||
| 9cb75f4814 | |||
| 375c4e0884 | |||
| ece8a48181 | |||
| 4b32a096dd | |||
| 04d6a928eb | |||
| 22e71ec6e1 | |||
| 6f1b07e26b | |||
| 1e3ab1c717 | |||
| 3a43dc2fe9 | |||
| c94f64cf78 | |||
| 25764fc4d3 | |||
| 649dbb9886 | |||
| e1dd616d74 | |||
| c91d78b63f | |||
| 82767325e5 | |||
| 3f0f7f051b | |||
| 15c64327db | |||
| 09e35eb5dd | |||
| 77c4935106 | |||
| e6da643186 | |||
| 61bd4e4549 | |||
| a5f4d12748 | |||
| 9dc67f31a3 | |||
| 846ec9728f | |||
| 6e5f1b8e5f | |||
| aca4a3e979 | |||
| 3783c3c650 | |||
| 01aed99002 | |||
| 9aec1e75e4 | |||
| 5dfb95a22d | |||
| 0f8cfa20b2 | |||
| 3933fd9d9e | |||
| b7400f2ce8 | |||
| d0ec580932 | |||
| 6bc0be9876 | |||
| 38221bcd10 | |||
| bee2f8aa4f | |||
| 6cfcaa4edc | |||
| 3fdc69edd5 | |||
| ed7ade0d72 | |||
| 621ddefc65 |
@@ -0,0 +1,58 @@
|
||||
<div align=center>
|
||||
|
||||
[](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>
|
||||
@@ -0,0 +1,245 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
env:
|
||||
IS_STABLE: ${{ !contains(github.ref, '-') }}
|
||||
|
||||
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: r28
|
||||
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:
|
||||
channel: stable
|
||||
cache: true
|
||||
|
||||
- name: Get Flutter Dependency
|
||||
run: flutter pub get
|
||||
|
||||
- name: Setup
|
||||
run: dart setup.dart ${{ matrix.platform }} ${{ matrix.arch && format('--arch {0}', matrix.arch) }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }}
|
||||
|
||||
- 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
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: refs/heads/main
|
||||
- name: Generate
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
|
||||
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
|
||||
currentTag=""
|
||||
for ((i = 0; i <= ${#tags[@]}; i++)); do
|
||||
if (( i < ${#tags[@]} )); then
|
||||
tag=${tags[$i]}
|
||||
else
|
||||
tag=""
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
if [ -n "$currentTag" ]; then
|
||||
echo "## $currentTag" >> NEW_CHANGELOG.md
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
if [ -n "$tag" ]; then
|
||||
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
else
|
||||
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
|
||||
fi
|
||||
echo "" >> NEW_CHANGELOG.md
|
||||
fi
|
||||
currentTag=$tag
|
||||
done
|
||||
cat CHANGELOG.md >> NEW_CHANGELOG.md
|
||||
cat NEW_CHANGELOG.md > CHANGELOG.md
|
||||
|
||||
- name: Commit
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
git add CHANGELOG.md
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "Commit pushing"
|
||||
git config --local user.email "chen08209@gmail.com"
|
||||
git config --local user.name "chen08209"
|
||||
git commit -m "Update changelog"
|
||||
git push
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Push succeeded"
|
||||
else
|
||||
echo "Push failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload:
|
||||
permissions: write-all
|
||||
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_telegram.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: ${{ env.IS_STABLE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ./dist/*
|
||||
body_path: './release.md'
|
||||
|
||||
- name: Create Fdroid Source Dir
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
run: |
|
||||
mkdir -p ./tmp
|
||||
cp ./dist/*android-arm64-v8a* ./tmp/ || true
|
||||
echo "Files copied successfully"
|
||||
|
||||
- name: Push to fdroid repo
|
||||
if: ${{ env.IS_STABLE == 'true' }}
|
||||
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: main
|
||||
commit-message: Update from ${{ github.ref_name }}
|
||||
target-directory: /tmp/
|
||||
|
||||
@@ -1,143 +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: 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.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'
|
||||
@@ -5,9 +5,11 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
path = plugins/flutter_distributor
|
||||
url = git@github.com:chen08209/flutter_distributor.git
|
||||
branch = FlClash
|
||||
|
||||
|
||||
|
||||
+824
@@ -0,0 +1,824 @@
|
||||
## v0.8.81
|
||||
|
||||
- Add rule override
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.80
|
||||
|
||||
- Optimize dashboard performance
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix unselected proxy group delay issues
|
||||
|
||||
- Fix asn url issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.79
|
||||
|
||||
- Fix tab delay view issues
|
||||
|
||||
- Fix tray action issues
|
||||
|
||||
- Fix get profile redirect client ua issues
|
||||
|
||||
- Fix proxy card delay view issues
|
||||
|
||||
- Add Russian, Japanese adaptation
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.78
|
||||
|
||||
- Fix list form input view issues
|
||||
|
||||
- Fix traffic view issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.77
|
||||
|
||||
- Optimize performance
|
||||
|
||||
- Update core
|
||||
|
||||
- Optimize core stability
|
||||
|
||||
- Fix linux tun authority check error
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Fix scroll physics error
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.75
|
||||
|
||||
- Add windows storage corruption detection
|
||||
|
||||
- Fix core crash caused by windows resource manager restart
|
||||
|
||||
- Optimize logs, requests, access to pages
|
||||
|
||||
- Fix macos bypass domain issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.74
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.73
|
||||
|
||||
- Update popup menu
|
||||
|
||||
- Add file editor
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Optimize desktop background performance
|
||||
|
||||
- Optimize android main process performance
|
||||
|
||||
- Optimize delay test
|
||||
|
||||
- Optimize vpn protect
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.72
|
||||
|
||||
- Update core
|
||||
|
||||
- Fix some issues
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.71
|
||||
|
||||
- Remake dashboard
|
||||
|
||||
- Optimize theme
|
||||
|
||||
- Optimize more details
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Update changelog
|
||||
|
||||
## v0.8.70
|
||||
|
||||
- Support better window position memory
|
||||
|
||||
- Add windows arm64 and linux arm64 build script
|
||||
|
||||
- Optimize some details
|
||||
|
||||
## v0.8.69
|
||||
|
||||
- Remake desktop
|
||||
|
||||
- Optimize change proxy
|
||||
|
||||
- Optimize network check
|
||||
|
||||
- Fix fallback issues
|
||||
|
||||
- Optimize lots of details
|
||||
|
||||
- Update change.yaml
|
||||
|
||||
- Fix android tile issues
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Support setting bypassDomain
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Fix android service issues
|
||||
|
||||
- Fix macos dock exit button issues
|
||||
|
||||
- Add route address setting
|
||||
|
||||
- Optimize provider view
|
||||
|
||||
- Update changelog
|
||||
|
||||
- Update CHANGELOG.md
|
||||
|
||||
## v0.8.67
|
||||
|
||||
- Add android shortcuts
|
||||
|
||||
- Fix init params issues
|
||||
|
||||
- Fix dynamic color issues
|
||||
|
||||
- Optimize navigator animate
|
||||
|
||||
- Optimize window init
|
||||
|
||||
- Optimize fab
|
||||
|
||||
- Optimize save
|
||||
|
||||
## v0.8.66
|
||||
|
||||
- Fix the collapse issues
|
||||
|
||||
- Add fontFamily options
|
||||
|
||||
## v0.8.65
|
||||
|
||||
- Update core version
|
||||
|
||||
- Update flutter version
|
||||
|
||||
- Optimize ip check
|
||||
|
||||
- Optimize url-test
|
||||
|
||||
## v0.8.64
|
||||
|
||||
- Update release message
|
||||
|
||||
- Init auto gen changelog
|
||||
|
||||
- Fix windows tray issues
|
||||
|
||||
- Fix urltest issues
|
||||
|
||||
- Add auto changelog
|
||||
|
||||
- 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
|
||||
@@ -0,0 +1,8 @@
|
||||
android_arm64:
|
||||
dart ./setup.dart android --arch arm64
|
||||
macos_arm64:
|
||||
dart ./setup.dart macos --arch arm64
|
||||
android_arm64_core:
|
||||
dart ./setup.dart android --arch arm64 --out core
|
||||
macos_arm64_core:
|
||||
dart ./setup.dart macos --arch arm64 --out core
|
||||
@@ -6,13 +6,9 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<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>
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
|
||||
A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
|
||||
|
||||
@@ -38,9 +34,32 @@ on Mobile:
|
||||
|
||||
✨ Support subscription link, Dark mode
|
||||
|
||||
## Contact
|
||||
## Use
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
### Linux
|
||||
|
||||
⚠️ Make sure to install the following dependencies before using them
|
||||
|
||||
```bash
|
||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
Support the following actions
|
||||
|
||||
```bash
|
||||
com.follow.clash.action.START
|
||||
|
||||
com.follow.clash.action.STOP
|
||||
|
||||
com.follow.clash.action.CHANGE
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
## Build
|
||||
|
||||
@@ -74,7 +93,7 @@ on Mobile:
|
||||
3. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
- linux
|
||||
@@ -84,7 +103,7 @@ on Mobile:
|
||||
2. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
- macOS
|
||||
@@ -94,11 +113,8 @@ on Mobile:
|
||||
2. Run build script
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Star
|
||||
|
||||
|
||||
+31
-12
@@ -6,13 +6,9 @@
|
||||
|
||||
## FlClash
|
||||
|
||||
<p style="text-align: left;">
|
||||
<img alt="stars" src="https://img.shields.io/github/stars/chen08209/FlClash?style=flat-square&logo=github"/>
|
||||
<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>
|
||||
[](https://github.com/chen08209/FlClash/releases/)[](https://github.com/chen08209/FlClash/releases/)[](LICENSE)
|
||||
|
||||
[](https://t.me/FlClash)
|
||||
|
||||
基于ClashMeta的多平台代理客户端,简单易用,开源无广告。
|
||||
|
||||
@@ -38,9 +34,32 @@ on Mobile:
|
||||
|
||||
✨ 支持一键导入订阅, 深色模式
|
||||
|
||||
## Contact
|
||||
## Use
|
||||
|
||||
[Telegram](https://t.me/+G-veVtwBOl4wODc1)
|
||||
### Linux
|
||||
|
||||
⚠️ 使用前请确保安装以下依赖
|
||||
|
||||
```bash
|
||||
sudo apt-get install appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
支持下列操作
|
||||
|
||||
```bash
|
||||
com.follow.clash.action.START
|
||||
|
||||
com.follow.clash.action.STOP
|
||||
|
||||
com.follow.clash.action.CHANGE
|
||||
```
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
|
||||
|
||||
## Build
|
||||
|
||||
@@ -74,7 +93,7 @@ on Mobile:
|
||||
3. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart windows --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
- linux
|
||||
@@ -84,7 +103,7 @@ on Mobile:
|
||||
2. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart linux --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
- macOS
|
||||
@@ -94,7 +113,7 @@ on Mobile:
|
||||
2. 运行构建脚本
|
||||
|
||||
```bash
|
||||
dart .\setup.dart
|
||||
dart .\setup.dart macos --arch <arm64 | amd64>
|
||||
```
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -1,29 +1 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at
|
||||
# https://dart-lang.github.io/linter/lints/index.html.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
+15
-31
@@ -1,5 +1,3 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
@@ -33,23 +31,23 @@ def isRelease = defStoreFile.exists() && defStorePassword != null && defKeyAlias
|
||||
|
||||
android {
|
||||
namespace "com.follow.clash"
|
||||
compileSdkVersion 34
|
||||
ndkVersion "25.1.8937393"
|
||||
compileSdkVersion 35
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (isRelease){
|
||||
if (isRelease) {
|
||||
release {
|
||||
storeFile defStoreFile
|
||||
storePassword defStorePassword
|
||||
@@ -63,7 +61,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.follow.clash"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
@@ -74,10 +72,9 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
if(isRelease){
|
||||
if (isRelease) {
|
||||
signingConfig signingConfigs.release
|
||||
}else{
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
||||
@@ -85,28 +82,15 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyNativeLibs', Copy) {
|
||||
delete('src/main/jniLibs')
|
||||
from('../../libclash/android')
|
||||
into('src/main/jniLibs')
|
||||
}
|
||||
|
||||
tasks.withType(MergeSourceSetFolders).configureEach {
|
||||
dependsOn copyNativeLibs
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
}
|
||||
|
||||
|
||||
afterEvaluate {
|
||||
assembleDebug.dependsOn copyNativeLibs
|
||||
|
||||
assembleRelease.dependsOn copyNativeLibs
|
||||
}
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation("com.android.tools.smali:smali-dexlib2:3.0.9") {
|
||||
exclude group: "com.google.guava", module: "guava"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<application android:label="FlClash Debug" tools:replace="android:label">
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label">
|
||||
</service>
|
||||
android:name=".services.FlClashTileService"
|
||||
android:label="FlClash Debug"
|
||||
tools:replace="android:label"
|
||||
tools:targetApi="24" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@@ -14,19 +14,17 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:name=".FlClashApplication"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:extractNativeLibs="true"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:label="FlClash"
|
||||
tools:targetApi="tiramisu">
|
||||
android:label="FlClash">
|
||||
<activity
|
||||
android:name="com.follow.clash.MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
@@ -56,29 +54,43 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="clash"/>
|
||||
<data android:scheme="clashmeta"/>
|
||||
<data android:scheme="flclash"/>
|
||||
<data android:scheme="clash" />
|
||||
<data android:scheme="clashmeta" />
|
||||
<data android:scheme="flclash" />
|
||||
|
||||
<data android:host="install-config"/>
|
||||
<data android:host="install-config" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- <meta-data-->
|
||||
<!-- android:name="io.flutter.embedding.android.EnableImpeller"-->
|
||||
<!-- android:value="true" />-->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
|
||||
<activity
|
||||
android:name=".TempActivity"
|
||||
android:theme="@style/TransparentTheme" />
|
||||
android:exported="true"
|
||||
android:theme="@style/TransparentTheme">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.START" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.STOP" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="${applicationId}.action.CHANGE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_stat_name"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="FlClash"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:targetApi="n">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
@@ -86,14 +98,49 @@
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name=".FilesProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:process=":background">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashVpnService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.VpnService" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="vpn" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".services.FlClashService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="service" />
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.provider.DocumentsContract.Root
|
||||
import android.provider.DocumentsProvider
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class FilesProvider : DocumentsProvider() {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ROOT_ID = "0"
|
||||
|
||||
private val DEFAULT_DOCUMENT_COLUMNS = arrayOf(
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_SIZE,
|
||||
)
|
||||
private val DEFAULT_ROOT_COLUMNS = arrayOf(
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_SUMMARY,
|
||||
Root.COLUMN_DOCUMENT_ID
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun queryRoots(projection: Array<String>?): Cursor {
|
||||
return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply {
|
||||
newRow().apply {
|
||||
add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID)
|
||||
add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY)
|
||||
add(Root.COLUMN_ICON, R.mipmap.ic_launcher)
|
||||
add(Root.COLUMN_TITLE, context!!.getString(R.string.fl_clash))
|
||||
add(Root.COLUMN_SUMMARY, "Data")
|
||||
add(Root.COLUMN_DOCUMENT_ID, "/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun queryChildDocuments(
|
||||
parentDocumentId: String,
|
||||
projection: Array<String>?,
|
||||
sortOrder: String?
|
||||
): Cursor {
|
||||
val result = MatrixCursor(resolveDocumentProjection(projection))
|
||||
val parentFile = if (parentDocumentId == "/") {
|
||||
context?.filesDir
|
||||
} else {
|
||||
File(parentDocumentId)
|
||||
} ?: throw FileNotFoundException("Parent directory not found")
|
||||
parentFile.listFiles()?.forEach { file ->
|
||||
includeFile(result, file)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
|
||||
val result = MatrixCursor(resolveDocumentProjection(projection))
|
||||
val file = File(documentId)
|
||||
includeFile(result, file)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun openDocument(
|
||||
documentId: String,
|
||||
mode: String,
|
||||
signal: CancellationSignal?
|
||||
): ParcelFileDescriptor {
|
||||
val file = File(documentId)
|
||||
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||
return ParcelFileDescriptor.open(file, accessMode)
|
||||
}
|
||||
|
||||
private fun includeFile(result: MatrixCursor, file: File) {
|
||||
result.newRow().apply {
|
||||
add(Document.COLUMN_DOCUMENT_ID, file.absolutePath)
|
||||
add(Document.COLUMN_DISPLAY_NAME, file.name)
|
||||
add(Document.COLUMN_SIZE, file.length())
|
||||
add(
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.FLAG_SUPPORTS_WRITE or Document.FLAG_SUPPORTS_DELETE
|
||||
)
|
||||
add(Document.COLUMN_MIME_TYPE, getDocumentType(file))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDocumentType(file: File): String {
|
||||
return if (file.isDirectory) {
|
||||
Document.MIME_TYPE_DIR
|
||||
} else {
|
||||
"application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDocumentProjection(projection: Array<String>?): Array<String> {
|
||||
return projection ?: DEFAULT_DOCUMENT_COLUMNS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.follow.clash;
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
|
||||
class FlClashApplication : Application() {
|
||||
companion object {
|
||||
private lateinit var instance: FlClashApplication
|
||||
fun getAppContext(): Context {
|
||||
return instance.applicationContext
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package com.follow.clash
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ProxyPlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import com.follow.clash.plugins.VpnPlugin
|
||||
import io.flutter.FlutterInjector
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
@@ -20,8 +18,7 @@ enum class RunState {
|
||||
|
||||
|
||||
object GlobalState {
|
||||
|
||||
private val lock = ReentrantLock()
|
||||
val runLock = ReentrantLock()
|
||||
|
||||
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
|
||||
var flutterEngine: FlutterEngine? = null
|
||||
@@ -32,33 +29,78 @@ object GlobalState {
|
||||
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
|
||||
}
|
||||
|
||||
fun getCurrentTitlePlugin(): TilePlugin? {
|
||||
suspend fun getText(text: String): String {
|
||||
return getCurrentAppPlugin()?.getText(text) ?: ""
|
||||
}
|
||||
|
||||
fun getCurrentTilePlugin(): TilePlugin? {
|
||||
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
|
||||
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
serviceEngine?.destroy()
|
||||
serviceEngine = null
|
||||
fun getCurrentVPNPlugin(): VpnPlugin? {
|
||||
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
|
||||
}
|
||||
|
||||
fun initServiceEngine(context: Context) {
|
||||
if (serviceEngine != null) return
|
||||
lock.withLock {
|
||||
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()
|
||||
serviceEngine = FlutterEngine(context)
|
||||
serviceEngine?.plugins?.add(ProxyPlugin())
|
||||
}
|
||||
}
|
||||
|
||||
fun destroyServiceEngine() {
|
||||
runLock.withLock {
|
||||
serviceEngine?.destroy()
|
||||
serviceEngine = null
|
||||
}
|
||||
}
|
||||
|
||||
fun initServiceEngine() {
|
||||
if (serviceEngine != null) return
|
||||
destroyServiceEngine()
|
||||
runLock.withLock {
|
||||
serviceEngine = FlutterEngine(FlClashApplication.getAppContext())
|
||||
serviceEngine?.plugins?.add(VpnPlugin)
|
||||
serviceEngine?.plugins?.add(AppPlugin())
|
||||
serviceEngine?.plugins?.add(TilePlugin())
|
||||
val vpnService = DartExecutor.DartEntrypoint(
|
||||
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
|
||||
"vpnService"
|
||||
"_service"
|
||||
)
|
||||
serviceEngine?.dartExecutor?.executeDartEntrypoint(
|
||||
vpnService,
|
||||
if (flutterEngine == null) listOf("quick") else null
|
||||
)
|
||||
|
||||
Log.e("FlClashVpnService", "initServiceEngine ===>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package com.follow.clash
|
||||
|
||||
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.plugins.AppPlugin
|
||||
import com.follow.clash.plugins.ProxyPlugin
|
||||
import com.follow.clash.plugins.ServicePlugin
|
||||
import com.follow.clash.plugins.TilePlugin
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(AppPlugin())
|
||||
flutterEngine.plugins.add(ProxyPlugin())
|
||||
flutterEngine.plugins.add(ServicePlugin)
|
||||
flutterEngine.plugins.add(TilePlugin())
|
||||
GlobalState.flutterEngine = flutterEngine
|
||||
}
|
||||
|
||||
@@ -2,10 +2,24 @@ package com.follow.clash
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import com.follow.clash.extensions.wrapAction
|
||||
|
||||
class TempActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
when (intent.action) {
|
||||
wrapAction("START") -> {
|
||||
GlobalState.handleStart()
|
||||
}
|
||||
|
||||
wrapAction("STOP") -> {
|
||||
GlobalState.handleStop()
|
||||
}
|
||||
|
||||
wrapAction("CHANGE") -> {
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
}
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.follow.clash.extensions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
import java.net.URL
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.models.Metadata
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
fun Metadata.getProtocol(): Int? {
|
||||
if (network.startsWith("tcp")) return IPPROTO_TCP
|
||||
if (network.startsWith("udp")) return IPPROTO_UDP
|
||||
return null
|
||||
}
|
||||
|
||||
fun String.getInetSocketAddress(): InetSocketAddress {
|
||||
val url = URL("https://$this")
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.follow.clash.extensions
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.system.OsConstants.IPPROTO_TCP
|
||||
import android.system.OsConstants.IPPROTO_UDP
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.follow.clash.TempActivity
|
||||
import com.follow.clash.models.CIDR
|
||||
import com.follow.clash.models.Metadata
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
suspend fun Drawable.getBase64(): String {
|
||||
val drawable = this
|
||||
return withContext(Dispatchers.IO) {
|
||||
val bitmap = drawable.toBitmap()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
|
||||
Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
fun Metadata.getProtocol(): Int? {
|
||||
if (network.startsWith("tcp")) return IPPROTO_TCP
|
||||
if (network.startsWith("udp")) return IPPROTO_UDP
|
||||
return null
|
||||
}
|
||||
|
||||
fun VpnOptions.getIpv4RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv4()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun VpnOptions.getIpv6RouteAddress(): List<CIDR> {
|
||||
return routeAddress.filter {
|
||||
it.isIpv6()
|
||||
}.map {
|
||||
it.toCIDR()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isIpv4(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 4
|
||||
}
|
||||
|
||||
fun String.isIpv6(): Boolean {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
throw IllegalArgumentException("Invalid CIDR format")
|
||||
}
|
||||
val address = InetAddress.getByName(parts[0])
|
||||
return address.address.size == 16
|
||||
}
|
||||
|
||||
fun String.toCIDR(): CIDR {
|
||||
val parts = split("/")
|
||||
if (parts.size != 2) {
|
||||
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()
|
||||
}
|
||||
@@ -3,5 +3,6 @@ package com.follow.clash.models
|
||||
data class Package(
|
||||
val packageName: String,
|
||||
val label: String,
|
||||
val isSystem:Boolean
|
||||
val isSystem: Boolean,
|
||||
val lastUpdateTime: Long,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
data class Process(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val metadata: Metadata,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
package com.follow.clash.models
|
||||
|
||||
import java.net.InetAddress
|
||||
|
||||
enum class AccessControlMode {
|
||||
acceptSelected,
|
||||
rejectSelected,
|
||||
acceptSelected, rejectSelected,
|
||||
}
|
||||
|
||||
data class AccessControl(
|
||||
val enable: Boolean,
|
||||
val mode: AccessControlMode,
|
||||
val acceptList: List<String>,
|
||||
val rejectList: List<String>,
|
||||
)
|
||||
|
||||
data class Props(
|
||||
val accessControl: AccessControl?,
|
||||
val allowBypass: Boolean?,
|
||||
val systemProxy: Boolean?,
|
||||
data class CIDR(val address: InetAddress, val prefixLength: Int)
|
||||
|
||||
data class VpnOptions(
|
||||
val enable: Boolean,
|
||||
val port: Int,
|
||||
val accessControl: AccessControl,
|
||||
val allowBypass: Boolean,
|
||||
val systemProxy: Boolean,
|
||||
val bypassDomain: List<String>,
|
||||
val routeAddress: List<String>,
|
||||
val ipv4Address: String,
|
||||
val ipv6Address: String,
|
||||
val dnsServerAddress: String,
|
||||
)
|
||||
|
||||
data class StartForegroundParams(
|
||||
val title: String,
|
||||
val content: String,
|
||||
)
|
||||
@@ -3,20 +3,30 @@ package com.follow.clash.plugins
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.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.follow.clash.FlClashApplication
|
||||
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.getProtocol
|
||||
import com.follow.clash.models.Package
|
||||
import com.follow.clash.models.Process
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
@@ -28,31 +38,106 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private var activity: Activity? = null
|
||||
|
||||
private var toast: Toast? = null
|
||||
|
||||
private var context: Context? = null
|
||||
private var activityRef: WeakReference<Activity>? = null
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private var connectivity: ConnectivityManager? = null
|
||||
private var vpnCallBack: (() -> Unit)? = null
|
||||
|
||||
private val iconMap = mutableMapOf<String, String?>()
|
||||
|
||||
private val packages = mutableListOf<Package>()
|
||||
|
||||
private val skipPrefixList = listOf(
|
||||
"com.google",
|
||||
"com.android.chrome",
|
||||
"com.android.vending",
|
||||
"com.microsoft",
|
||||
"com.apple",
|
||||
"com.zhiliaoapp.musically", // Banned by China
|
||||
)
|
||||
|
||||
private val chinaAppPrefixList = listOf(
|
||||
"com.tencent",
|
||||
"com.alibaba",
|
||||
"com.umeng",
|
||||
"com.qihoo",
|
||||
"com.ali",
|
||||
"com.alipay",
|
||||
"com.amap",
|
||||
"com.sina",
|
||||
"com.weibo",
|
||||
"com.vivo",
|
||||
"com.xiaomi",
|
||||
"com.huawei",
|
||||
"com.taobao",
|
||||
"com.secneo",
|
||||
"s.h.e.l.l",
|
||||
"com.stub",
|
||||
"com.kiwisec",
|
||||
"com.secshell",
|
||||
"com.wrapper",
|
||||
"cn.securitystack",
|
||||
"com.mogosec",
|
||||
"com.secoen",
|
||||
"com.netease",
|
||||
"com.mx",
|
||||
"com.qq.e",
|
||||
"com.baidu",
|
||||
"com.bytedance",
|
||||
"com.bugly",
|
||||
"com.miui",
|
||||
"com.oppo",
|
||||
"com.coloros",
|
||||
"com.iqoo",
|
||||
"com.meizu",
|
||||
"com.gionee",
|
||||
"cn.nubia",
|
||||
"com.oplus",
|
||||
"andes.oplus",
|
||||
"com.unionpay",
|
||||
"cn.wps"
|
||||
)
|
||||
|
||||
private val chinaAppRegex by lazy {
|
||||
("(" + chinaAppPrefixList.joinToString("|").replace(".", "\\.") + ").*").toRegex()
|
||||
}
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var isBlockNotification: Boolean = false
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
context = flutterPluginBinding.applicationContext;
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
|
||||
channel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
private fun initShortcuts(label: String) {
|
||||
val shortcut = ShortcutInfoCompat.Builder(FlClashApplication.getAppContext(), "toggle")
|
||||
.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) {
|
||||
@@ -61,31 +146,38 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private fun tip(message: String?) {
|
||||
if(GlobalState.flutterEngine == null){
|
||||
if (toast != null) {
|
||||
toast!!.cancel()
|
||||
}
|
||||
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
|
||||
toast!!.show()
|
||||
if (GlobalState.flutterEngine == null) {
|
||||
Toast.makeText(FlClashApplication.getAppContext(), message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
when (call.method) {
|
||||
"moveTaskToBack" -> {
|
||||
activity?.moveTaskToBack(true)
|
||||
result.success(true);
|
||||
activityRef?.get()?.moveTaskToBack(true)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"updateExcludeFromRecents" -> {
|
||||
val value = call.argument<Boolean>("value")
|
||||
updateExcludeFromRecents(value)
|
||||
result.success(true);
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"initShortcuts" -> {
|
||||
initShortcuts(call.arguments as String)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"getPackages" -> {
|
||||
scope.launch {
|
||||
result.success(getPackages())
|
||||
result.success(getPackagesToJson())
|
||||
}
|
||||
}
|
||||
|
||||
"getChinaPackageNames" -> {
|
||||
scope.launch {
|
||||
result.success(getChinaPackageNames())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +196,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
if (iconMap["default"] == null) {
|
||||
iconMap["default"] =
|
||||
context?.packageManager?.defaultActivityIcon?.getBase64()
|
||||
FlClashApplication.getAppContext().packageManager?.defaultActivityIcon?.getBase64()
|
||||
}
|
||||
result.success(iconMap["default"])
|
||||
return@launch
|
||||
@@ -112,72 +204,67 @@ 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 (context == null) {
|
||||
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" -> {
|
||||
val message = call.argument<String>("message")
|
||||
tip(message)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"openFile" -> {
|
||||
val path = call.argument<String>("path")!!
|
||||
openFile(path)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented();
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFile(path: String) {
|
||||
val file = File(path)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
FlClashApplication.getAppContext(),
|
||||
"${FlClashApplication.getAppContext().packageName}.fileProvider",
|
||||
file
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(
|
||||
uri,
|
||||
"text/plain"
|
||||
)
|
||||
|
||||
val flags =
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
val resInfoList = FlClashApplication.getAppContext().packageManager.queryIntentActivities(
|
||||
intent, PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
|
||||
for (resolveInfo in resInfoList) {
|
||||
val packageName = resolveInfo.activityInfo.packageName
|
||||
FlClashApplication.getAppContext().grantUriPermission(
|
||||
packageName,
|
||||
uri,
|
||||
flags
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
activityRef?.get()?.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateExcludeFromRecents(value: Boolean?) {
|
||||
if (context == null) return
|
||||
val am = getSystemService(context!!, ActivityManager::class.java)
|
||||
val am = getSystemService(FlClashApplication.getAppContext(), ActivityManager::class.java)
|
||||
val task = am?.appTasks?.firstOrNull {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
it.taskInfo.taskId == activity?.taskId
|
||||
it.taskInfo.taskId == activityRef?.get()?.taskId
|
||||
} else {
|
||||
it.taskInfo.id == activity?.taskId
|
||||
it.taskInfo.id == activityRef?.get()?.taskId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +276,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
}
|
||||
|
||||
private suspend fun getPackageIcon(packageName: String): String? {
|
||||
val packageManager = context?.packageManager
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (iconMap[packageName] == null) {
|
||||
iconMap[packageName] = try {
|
||||
packageManager?.getApplicationIcon(packageName)?.getBase64()
|
||||
@@ -201,44 +288,184 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
|
||||
return iconMap[packageName]
|
||||
}
|
||||
|
||||
private suspend fun getPackages(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val packageManager = context?.packageManager
|
||||
val packages: List<Package>? =
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
|
||||
it.packageName != context?.packageName
|
||||
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
private fun getPackages(): List<Package> {
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager
|
||||
if (packages.isNotEmpty()) return packages
|
||||
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA or PackageManager.GET_PERMISSIONS)
|
||||
?.filter {
|
||||
it.packageName != FlClashApplication.getAppContext().packageName && (
|
||||
it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
|
||||
|| it.packageName == "android"
|
||||
)
|
||||
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 1
|
||||
)
|
||||
}
|
||||
}?.map {
|
||||
Package(
|
||||
packageName = it.packageName,
|
||||
label = it.applicationInfo?.loadLabel(packageManager).toString(),
|
||||
isSystem = (it.applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM)) == 1,
|
||||
lastUpdateTime = it.lastUpdateTime
|
||||
)
|
||||
}?.let { packages.addAll(it) }
|
||||
return packages
|
||||
}
|
||||
|
||||
private suspend fun getPackagesToJson(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
Gson().toJson(getPackages())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getChinaPackageNames(): String {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val packages: List<String> =
|
||||
getPackages().map { it.packageName }.filter { isChinaPackage(it) }
|
||||
Gson().toJson(packages)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGc() {
|
||||
channel.invokeMethod("gc", null)
|
||||
fun requestVpnPermission(callBack: () -> Unit) {
|
||||
vpnCallBack = callBack
|
||||
val intent = VpnService.prepare(FlClashApplication.getAppContext())
|
||||
if (intent != null) {
|
||||
activityRef?.get()?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
return
|
||||
}
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
|
||||
fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = ContextCompat.checkSelfPermission(
|
||||
FlClashApplication.getAppContext(),
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activityRef?.get() == null) return
|
||||
activityRef?.get()?.let {
|
||||
ActivityCompat.requestPermissions(
|
||||
it,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getText(text: String): String? {
|
||||
return withContext(Dispatchers.Default) {
|
||||
channel.awaitResult<String>("getText", text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isChinaPackage(packageName: String): Boolean {
|
||||
val packageManager = FlClashApplication.getAppContext().packageManager ?: return false
|
||||
skipPrefixList.forEach {
|
||||
if (packageName == it || packageName.startsWith("$it.")) return false
|
||||
}
|
||||
val packageManagerFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES or PackageManager.GET_RECEIVERS or PackageManager.GET_PROVIDERS
|
||||
}
|
||||
if (packageName.matches(chinaAppRegex)) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getPackageInfo(
|
||||
packageName,
|
||||
PackageManager.PackageInfoFlags.of(packageManagerFlags.toLong())
|
||||
)
|
||||
} else {
|
||||
packageManager.getPackageInfo(
|
||||
packageName, packageManagerFlags
|
||||
)
|
||||
}
|
||||
mutableListOf<ComponentInfo>().apply {
|
||||
packageInfo.services?.let { addAll(it) }
|
||||
packageInfo.activities?.let { addAll(it) }
|
||||
packageInfo.receivers?.let { addAll(it) }
|
||||
packageInfo.providers?.let { addAll(it) }
|
||||
}.forEach {
|
||||
if (it.name.matches(chinaAppRegex)) return true
|
||||
}
|
||||
packageInfo.applicationInfo?.publicSourceDir?.let {
|
||||
ZipFile(File(it)).use {
|
||||
for (packageEntry in it.entries()) {
|
||||
if (packageEntry.name.startsWith("firebase-")) return false
|
||||
}
|
||||
for (packageEntry in it.entries()) {
|
||||
if (!(packageEntry.name.startsWith("classes") && packageEntry.name.endsWith(
|
||||
".dex"
|
||||
))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (packageEntry.size > 15000000) {
|
||||
return true
|
||||
}
|
||||
val input = it.getInputStream(packageEntry).buffered()
|
||||
val dexFile = try {
|
||||
DexBackedDexFile.fromInputStream(null, input)
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
for (clazz in dexFile.classes) {
|
||||
val clazzName =
|
||||
clazz.type.substring(1, clazz.type.length - 1).replace("/", ".")
|
||||
.replace("$", ".")
|
||||
if (clazzName.matches(chinaAppRegex)) return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity;
|
||||
activityRef = WeakReference(binding.activity)
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
activityRef = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity;
|
||||
activityRef = WeakReference(binding.activity)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
channel.invokeMethod("exit", null)
|
||||
activity = null
|
||||
activityRef = null
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
GlobalState.initServiceEngine()
|
||||
vpnCallBack?.invoke()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.models.Props
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
val VPN_PERMISSION_REQUEST_CODE = 1001
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 1002
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var context: Context? = null
|
||||
private var flClashVpnService: FlClashVpnService? = null
|
||||
private var port: Int = 7890
|
||||
private var props: Props? = null
|
||||
private var isBlockNotification: Boolean = false
|
||||
private var isStart: Boolean = false
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as FlClashVpnService.LocalBinder
|
||||
flClashVpnService = binder.getService()
|
||||
if (isStart) {
|
||||
startVpn()
|
||||
} else {
|
||||
flClashVpnService?.initServiceEngine()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
flClashVpnService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "proxy")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"initService" -> {
|
||||
isStart = false
|
||||
initService()
|
||||
requestNotificationsPermission()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"startProxy" -> {
|
||||
isStart = true
|
||||
port = call.argument<Int>("port")!!
|
||||
val args = call.argument<String>("args")
|
||||
props =
|
||||
if (args != null) Gson().fromJson(args, Props::class.java) else null
|
||||
startVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopProxy" -> {
|
||||
stopVpn()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"setProtect" -> {
|
||||
val fd = call.argument<Int>("fd")
|
||||
if (fd != null) {
|
||||
flClashVpnService?.protect(fd)
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
|
||||
"startForeground" -> {
|
||||
val title = call.argument<String>("title") as String
|
||||
val content = call.argument<String>("content") as String
|
||||
startForeground(title, content)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initService() {
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
if (flClashVpnService != null) {
|
||||
flClashVpnService!!.initServiceEngine()
|
||||
} else {
|
||||
bindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpn() {
|
||||
if (flClashVpnService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val intent = VpnService.prepare(context)
|
||||
if (intent != null) {
|
||||
stopVpn()
|
||||
return
|
||||
}
|
||||
val fd = flClashVpnService?.start(port, props)
|
||||
flutterMethodChannel.invokeMethod("started", fd)
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
flClashVpnService?.stop()
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
|
||||
private fun startForeground(title: String, content: String) {
|
||||
if (GlobalState.runState.value != RunState.START) return
|
||||
flClashVpnService?.startForeground(title, content)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
binding.addActivityResultListener(::onActivityResult)
|
||||
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == VPN_PERMISSION_REQUEST_CODE) {
|
||||
if (resultCode == FlutterActivity.RESULT_OK) {
|
||||
bindService()
|
||||
} else {
|
||||
stopVpn()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onRequestPermissionsResultListener(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
): Boolean {
|
||||
if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) {
|
||||
isBlockNotification = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun requestNotificationsPermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val permission = context?.let {
|
||||
ContextCompat.checkSelfPermission(
|
||||
it,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
if (isBlockNotification) return
|
||||
if (activity == null) return
|
||||
ActivityCompat.requestPermissions(
|
||||
activity!!,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
val intent = Intent(context, FlClashVpnService::class.java)
|
||||
context?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
||||
data object ServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "service")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) = when (call.method) {
|
||||
"startVpn" -> {
|
||||
val data = call.argument<String>("data")
|
||||
val options = Gson().fromJson(data, VpnOptions::class.java)
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStart(options)
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"stopVpn" -> {
|
||||
GlobalState.getCurrentVPNPlugin()?.handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"init" -> {
|
||||
GlobalState.getCurrentAppPlugin()
|
||||
?.requestNotificationsPermission()
|
||||
GlobalState.initServiceEngine()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"destroy" -> {
|
||||
handleDestroy()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDestroy() {
|
||||
GlobalState.destroyServiceEngine()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop: (() -> Unit)? = null) : FlutterPlugin,
|
||||
MethodChannel.MethodCallHandler {
|
||||
class TilePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "tile")
|
||||
channel.setMethodCallHandler(this)
|
||||
@@ -20,13 +19,11 @@ class TilePlugin(private val onStart: (() -> Unit)? = null, private val onStop:
|
||||
}
|
||||
|
||||
fun handleStart() {
|
||||
onStart?.let { it() }
|
||||
channel.invokeMethod("start", null)
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
channel.invokeMethod("stop", null)
|
||||
onStop?.let { it() }
|
||||
}
|
||||
|
||||
private fun handleDetached() {
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.follow.clash.plugins
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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 androidx.core.content.getSystemService
|
||||
import com.follow.clash.FlClashApplication
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.RunState
|
||||
import com.follow.clash.core.Core
|
||||
import com.follow.clash.extensions.awaitResult
|
||||
import com.follow.clash.extensions.resolveDns
|
||||
import com.follow.clash.models.StartForegroundParams
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import com.follow.clash.services.BaseServiceInterface
|
||||
import com.follow.clash.services.FlClashService
|
||||
import com.follow.clash.services.FlClashVpnService
|
||||
import com.google.gson.Gson
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.InetSocketAddress
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private lateinit var flutterMethodChannel: MethodChannel
|
||||
private var flClashService: BaseServiceInterface? = null
|
||||
private var options: VpnOptions? = null
|
||||
private var isBind: Boolean = false
|
||||
private lateinit var scope: CoroutineScope
|
||||
private var lastStartForegroundParams: StartForegroundParams? = null
|
||||
private var timerJob: Job? = null
|
||||
private val uidPageNameMap = mutableMapOf<Int, String>()
|
||||
|
||||
private val connectivity by lazy {
|
||||
FlClashApplication.getAppContext().getSystemService<ConnectivityManager>()
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
isBind = true
|
||||
flClashService = when (service) {
|
||||
is FlClashVpnService.LocalBinder -> service.getService()
|
||||
is FlClashService.LocalBinder -> service.getService()
|
||||
else -> throw Exception("invalid binder")
|
||||
}
|
||||
handleStartService()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg: ComponentName) {
|
||||
isBind = false
|
||||
flClashService = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
scope.launch {
|
||||
registerNetworkCallback()
|
||||
}
|
||||
flutterMethodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "vpn")
|
||||
flutterMethodChannel.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
unRegisterNetworkCallback()
|
||||
flutterMethodChannel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"start" -> {
|
||||
val data = call.argument<String>("data")
|
||||
result.success(handleStart(Gson().fromJson(data, VpnOptions::class.java)))
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
handleStop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleStart(options: VpnOptions): Boolean {
|
||||
if (options.enable != this.options?.enable) {
|
||||
this.flClashService = null
|
||||
}
|
||||
this.options = options
|
||||
when (options.enable) {
|
||||
true -> handleStartVpn()
|
||||
false -> handleStartService()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleStartVpn() {
|
||||
GlobalState.getCurrentAppPlugin()?.requestVpnPermission {
|
||||
handleStartService()
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
override fun onAvailable(network: Network) {
|
||||
networks.add(network)
|
||||
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
|
||||
val data = flutterMethodChannel.awaitResult<String>("getStartForegroundParams")
|
||||
val startForegroundParams = Gson().fromJson(
|
||||
data, StartForegroundParams::class.java
|
||||
)
|
||||
if (lastStartForegroundParams != startForegroundParams) {
|
||||
lastStartForegroundParams = startForegroundParams
|
||||
flClashService?.startForeground(
|
||||
startForegroundParams.title,
|
||||
startForegroundParams.content,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
GlobalState.runLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundJob() {
|
||||
stopForegroundJob()
|
||||
timerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
startForeground()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundJob() {
|
||||
timerJob?.cancel()
|
||||
timerJob = null
|
||||
}
|
||||
|
||||
private fun handleStartService() {
|
||||
if (flClashService == null) {
|
||||
bindService()
|
||||
return
|
||||
}
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.START) return
|
||||
GlobalState.runState.value = RunState.START
|
||||
val fd = flClashService?.start(options!!)
|
||||
Core.startTun(
|
||||
fd = fd ?: 0,
|
||||
protect = this::protect,
|
||||
resolverProcess = this::resolverProcess,
|
||||
)
|
||||
startForegroundJob()
|
||||
}
|
||||
}
|
||||
|
||||
private fun protect(fd: Int): Boolean {
|
||||
return (flClashService as? FlClashVpnService)?.protect(fd) == true
|
||||
}
|
||||
|
||||
private fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: InetSocketAddress,
|
||||
target: InetSocketAddress,
|
||||
uid: Int,
|
||||
): String {
|
||||
val nextUid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
connectivity?.getConnectionOwnerUid(protocol, source, target) ?: -1
|
||||
} else {
|
||||
uid
|
||||
}
|
||||
if (nextUid == -1) {
|
||||
return ""
|
||||
}
|
||||
if (!uidPageNameMap.containsKey(nextUid)) {
|
||||
uidPageNameMap[nextUid] =
|
||||
FlClashApplication.getAppContext().packageManager?.getPackagesForUid(nextUid)
|
||||
?.first() ?: ""
|
||||
}
|
||||
return uidPageNameMap[nextUid] ?: ""
|
||||
}
|
||||
|
||||
fun handleStop() {
|
||||
GlobalState.runLock.withLock {
|
||||
if (GlobalState.runState.value == RunState.STOP) return
|
||||
GlobalState.runState.value = RunState.STOP
|
||||
stopForegroundJob()
|
||||
Core.stopTun()
|
||||
flClashService?.stop()
|
||||
GlobalState.handleTryDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
if (isBind) {
|
||||
FlClashApplication.getAppContext().unbindService(connection)
|
||||
}
|
||||
val intent = when (options?.enable == true) {
|
||||
true -> Intent(FlClashApplication.getAppContext(), FlClashVpnService::class.java)
|
||||
false -> Intent(FlClashApplication.getAppContext(), FlClashService::class.java)
|
||||
}
|
||||
FlClashApplication.getAppContext().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
|
||||
class FlClashService : Service(), BaseServiceInterface {
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): FlClashService = this@FlClashService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
|
||||
val intent = Intent(
|
||||
this@FlClashService, MainActivity::class.java
|
||||
)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashService, CHANNEL)) {
|
||||
setSmallIcon(com.follow.clash.R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
addAction(
|
||||
0,
|
||||
stopText, // 使用 suspend 函数获取的文本
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun start(options: VpnOptions) = 0
|
||||
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
if (channel == null) {
|
||||
channel =
|
||||
NotificationChannel(CHANNEL, "FlClash", NotificationManager.IMPORTANCE_LOW)
|
||||
manager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} catch (_: Exception) {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -37,6 +37,7 @@ class FlClashTileService : TileService() {
|
||||
GlobalState.runState.observeForever(observer)
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
private fun activityTransfer() {
|
||||
val intent = Intent(this, TempActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
@@ -65,19 +66,7 @@ class FlClashTileService : TileService() {
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
activityTransfer()
|
||||
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()
|
||||
}
|
||||
|
||||
GlobalState.handleToggle()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.follow.clash.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification.FOREGROUND_SERVICE_IMMEDIATE
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
@@ -18,134 +19,168 @@ import androidx.core.app.NotificationCompat
|
||||
import com.follow.clash.GlobalState
|
||||
import com.follow.clash.MainActivity
|
||||
import com.follow.clash.R
|
||||
import com.follow.clash.extensions.getActionPendingIntent
|
||||
import com.follow.clash.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.Props
|
||||
import com.follow.clash.models.VpnOptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class FlClashVpnService : VpnService() {
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
private val notificationId: Int = 1
|
||||
|
||||
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.*"
|
||||
)
|
||||
|
||||
class FlClashVpnService : VpnService(), BaseServiceInterface {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initServiceEngine()
|
||||
GlobalState.initServiceEngine()
|
||||
}
|
||||
|
||||
fun start(port: Int, props: Props?): Int? {
|
||||
override fun start(options: VpnOptions): Int {
|
||||
return with(Builder()) {
|
||||
addAddress("172.16.0.1", 30)
|
||||
setMtu(9000)
|
||||
addRoute("0.0.0.0", 0)
|
||||
props?.accessControl?.let { accessControl ->
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
if (options.ipv4Address.isNotEmpty()) {
|
||||
val cidr = options.ipv4Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv4RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute4",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("0.0.0.0", 0)
|
||||
}
|
||||
}
|
||||
if (options.ipv6Address.isNotEmpty()) {
|
||||
val cidr = options.ipv6Address.toCIDR()
|
||||
addAddress(cidr.address, cidr.prefixLength)
|
||||
val routeAddress = options.getIpv6RouteAddress()
|
||||
if (routeAddress.isNotEmpty()) {
|
||||
try {
|
||||
routeAddress.forEach { i ->
|
||||
Log.d(
|
||||
"addRoute6",
|
||||
"address: ${i.address} prefixLength:${i.prefixLength}"
|
||||
)
|
||||
addRoute(i.address, i.prefixLength)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
} else {
|
||||
addRoute("::", 0)
|
||||
}
|
||||
}
|
||||
addDnsServer(options.dnsServerAddress)
|
||||
setMtu(9000)
|
||||
options.accessControl.let { accessControl ->
|
||||
if (accessControl.enable) {
|
||||
when (accessControl.mode) {
|
||||
AccessControlMode.acceptSelected -> {
|
||||
(accessControl.acceptList + packageName).forEach {
|
||||
addAllowedApplication(it)
|
||||
}
|
||||
}
|
||||
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
AccessControlMode.rejectSelected -> {
|
||||
(accessControl.rejectList - packageName).forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
addDnsServer("172.16.0.2")
|
||||
setSession("FlClash")
|
||||
setBlocking(false)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
setMetered(false)
|
||||
}
|
||||
if (props?.allowBypass == true) {
|
||||
if (options.allowBypass) {
|
||||
allowBypass()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && props?.systemProxy == true) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.systemProxy) {
|
||||
setHttpProxy(
|
||||
ProxyInfo.buildDirectProxy(
|
||||
"127.0.0.1",
|
||||
port,
|
||||
passList
|
||||
options.port,
|
||||
options.bypassDomain
|
||||
)
|
||||
)
|
||||
}
|
||||
establish()?.detachFd()
|
||||
?: throw NullPointerException("Establish VPN rejected by system")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
override fun stop() {
|
||||
stopSelf()
|
||||
stopForeground()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
private val CHANNEL = "FlClash"
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
with(NotificationCompat.Builder(this, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
private val notificationId: Int = 1
|
||||
|
||||
private val notificationBuilderDeferred: Deferred<NotificationCompat.Builder> by lazy {
|
||||
CoroutineScope(Dispatchers.Main).async {
|
||||
val stopText = GlobalState.getText("stop")
|
||||
val intent = Intent(this@FlClashVpnService, MainActivity::class.java)
|
||||
|
||||
val pendingIntent = if (Build.VERSION.SDK_INT >= 31) {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
} else {
|
||||
PendingIntent.getActivity(
|
||||
this@FlClashVpnService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
|
||||
with(NotificationCompat.Builder(this@FlClashVpnService, CHANNEL)) {
|
||||
setSmallIcon(R.drawable.ic_stat_name)
|
||||
setContentTitle("FlClash")
|
||||
setContentIntent(pendingIntent)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
priority = NotificationCompat.PRIORITY_MIN
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
|
||||
}
|
||||
setOngoing(true)
|
||||
addAction(
|
||||
0,
|
||||
stopText,
|
||||
getActionPendingIntent("STOP")
|
||||
)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
setOngoing(true)
|
||||
setShowWhen(false)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun initServiceEngine() {
|
||||
GlobalState.initServiceEngine(applicationContext)
|
||||
private suspend fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return notificationBuilderDeferred.await()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentAppPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
fun startForeground(title: String, content: String) {
|
||||
@SuppressLint("ForegroundServiceType")
|
||||
override suspend fun startForeground(title: String, content: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var channel = manager?.getNotificationChannel(CHANNEL)
|
||||
@@ -156,18 +191,24 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
val notification =
|
||||
notificationBuilder.setContentTitle(title).setContentText(content).build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
getNotificationBuilder()
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
try {
|
||||
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} catch (_: Exception) {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
} else {
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForeground() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
GlobalState.getCurrentVPNPlugin()?.requestGc()
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
@@ -180,7 +221,7 @@ class FlClashVpnService : VpnService() {
|
||||
val isSuccess = super.onTransact(code, data, reply, flags)
|
||||
if (!isSuccess) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
GlobalState.getCurrentTitlePlugin()?.handleStop()
|
||||
GlobalState.getCurrentTilePlugin()?.handleStop()
|
||||
}
|
||||
}
|
||||
return isSuccess
|
||||
@@ -190,7 +231,6 @@ class FlClashVpnService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<paths>
|
||||
<files-path
|
||||
name="files"
|
||||
path="."/>
|
||||
</paths>
|
||||
|
||||
@@ -24,6 +24,7 @@ subprojects {
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
project.evaluationDependsOn(':core')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,72 @@
|
||||
import com.android.build.gradle.tasks.MergeSourceSetFolders
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.follow.clash.core"
|
||||
compileSdk = 35
|
||||
ndkVersion = "28.0.13004108"
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
jniLibs.srcDirs("src/main/jniLibs")
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyNativeLibs") {
|
||||
doFirst {
|
||||
delete("src/main/jniLibs")
|
||||
}
|
||||
from("../../libclash/android")
|
||||
into("src/main/jniLibs")
|
||||
}
|
||||
|
||||
tasks.withType<MergeSourceSetFolders>().configureEach {
|
||||
dependsOn("copyNativeLibs")
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.named("assembleDebug").configure {
|
||||
dependsOn("copyNativeLibs")
|
||||
}
|
||||
tasks.named("assembleRelease").configure {
|
||||
dependsOn("copyNativeLibs")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.16.0")
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,41 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
|
||||
|
||||
project("core")
|
||||
|
||||
|
||||
if (NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
add_compile_options(-O3)
|
||||
|
||||
add_compile_options(-flto)
|
||||
|
||||
add_compile_options(-g0)
|
||||
|
||||
add_compile_options(-ffunction-sections -fdata-sections)
|
||||
|
||||
add_compile_options(-fno-exceptions -fno-rtti)
|
||||
|
||||
add_link_options(
|
||||
-flto
|
||||
-Wl,--gc-sections
|
||||
-Wl,--strip-all
|
||||
-Wl,--exclude-libs=ALL
|
||||
)
|
||||
endif ()
|
||||
set(LIB_CLASH_PATH "${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libclash.so")
|
||||
if (EXISTS ${LIB_CLASH_PATH})
|
||||
message(STATUS "Found libclash.so for ABI ${ANDROID_ABI}")
|
||||
add_definitions(-D_LIBCLASH)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
link_directories(${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
clash)
|
||||
else ()
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_helper.cpp
|
||||
core.cpp)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME})
|
||||
endif ()
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <jni.h>
|
||||
|
||||
#ifdef _LIBCLASH
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include "jni_helper.h"
|
||||
#include "libclash.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
|
||||
auto interface = new_global(cb);
|
||||
startTUN(fd, interface);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
stopTun();
|
||||
}
|
||||
|
||||
|
||||
static jmethodID m_tun_interface_protect;
|
||||
static jmethodID m_tun_interface_resolve_process;
|
||||
|
||||
|
||||
static void release_jni_object_impl(void *obj) {
|
||||
ATTACH_JNI();
|
||||
del_global((jobject) obj);
|
||||
}
|
||||
|
||||
static void call_tun_interface_protect_impl(void *tun_interface, int fd) {
|
||||
ATTACH_JNI();
|
||||
env->CallVoidMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_protect,
|
||||
(jint) fd);
|
||||
}
|
||||
|
||||
static const char*
|
||||
call_tun_interface_resolve_process_impl(void *tun_interface, int protocol,
|
||||
const char *source,
|
||||
const char *target,
|
||||
int uid) {
|
||||
ATTACH_JNI();
|
||||
jstring packageName = (jstring)env->CallObjectMethod((jobject) tun_interface,
|
||||
(jmethodID) m_tun_interface_resolve_process,
|
||||
(jint) protocol,
|
||||
(jstring) new_string(source),
|
||||
(jstring) new_string(target),
|
||||
(jint) uid);
|
||||
return get_string(packageName);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
JNIEnv *env = nullptr;
|
||||
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initialize_jni(vm, env);
|
||||
|
||||
jclass c_tun_interface = find_class("com/follow/clash/core/TunInterface");
|
||||
|
||||
m_tun_interface_protect = find_method(c_tun_interface, "protect", "(I)V");
|
||||
m_tun_interface_resolve_process = find_method(c_tun_interface, "resolverProcess",
|
||||
"(ILjava/lang/String;Ljava/lang/String;I)Ljava/lang/String;");
|
||||
|
||||
registerCallbacks(&call_tun_interface_protect_impl,
|
||||
&call_tun_interface_resolve_process_impl,
|
||||
&release_jni_object_impl);
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_stopTun(JNIEnv *env, jobject thiz) {
|
||||
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_follow_clash_core_Core_startTun(JNIEnv *env, jobject thiz, jint fd, jobject cb) {
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "jni_helper.h"
|
||||
|
||||
#include <malloc.h>
|
||||
#include <cstring>
|
||||
|
||||
static JavaVM *global_vm;
|
||||
|
||||
static jclass c_string;
|
||||
static jmethodID m_new_string;
|
||||
static jmethodID m_get_bytes;
|
||||
|
||||
void initialize_jni(JavaVM *vm, JNIEnv *env) {
|
||||
global_vm = vm;
|
||||
|
||||
c_string = (jclass) new_global(find_class("java/lang/String"));
|
||||
m_new_string = find_method(c_string, "<init>", "([B)V");
|
||||
m_get_bytes = find_method(c_string, "getBytes", "()[B");
|
||||
}
|
||||
|
||||
JavaVM *global_java_vm() {
|
||||
return global_vm;
|
||||
}
|
||||
|
||||
char *jni_get_string(JNIEnv *env, jstring str) {
|
||||
auto array = (jbyteArray) env->CallObjectMethod(str, m_get_bytes);
|
||||
int length = env->GetArrayLength(array);
|
||||
char *content = (char *) malloc(length + 1);
|
||||
env->GetByteArrayRegion(array, 0, length, (jbyte *) content);
|
||||
content[length] = 0;
|
||||
return content;
|
||||
}
|
||||
|
||||
jstring jni_new_string(JNIEnv *env, const char *str) {
|
||||
auto length = (int) strlen(str);
|
||||
jbyteArray array = env->NewByteArray(length);
|
||||
env->SetByteArrayRegion(array, 0, length, (const jbyte *) str);
|
||||
return (jstring) env->NewObject(c_string, m_new_string, array);
|
||||
}
|
||||
|
||||
int jni_catch_exception(JNIEnv *env) {
|
||||
int result = env->ExceptionCheck();
|
||||
if (result) {
|
||||
env->ExceptionDescribe();
|
||||
env->ExceptionClear();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void jni_attach_thread(struct scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (vm->GetEnv((void **) &jni->env, JNI_VERSION_1_6) == JNI_OK) {
|
||||
jni->require_release = 0;
|
||||
return;
|
||||
}
|
||||
if (vm->AttachCurrentThread(&jni->env, nullptr) != JNI_OK) {
|
||||
abort();
|
||||
}
|
||||
jni->require_release = 1;
|
||||
}
|
||||
|
||||
void jni_detach_thread(struct scoped_jni *jni) {
|
||||
JavaVM *vm = global_java_vm();
|
||||
if (jni->require_release) {
|
||||
vm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
|
||||
void release_string(char **str) {
|
||||
free(*str);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <malloc.h>
|
||||
|
||||
struct scoped_jni {
|
||||
JNIEnv *env;
|
||||
int require_release;
|
||||
};
|
||||
|
||||
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
|
||||
|
||||
extern jstring jni_new_string(JNIEnv *env, const char *str);
|
||||
|
||||
extern char *jni_get_string(JNIEnv *env, jstring str);
|
||||
|
||||
extern int jni_catch_exception(JNIEnv *env);
|
||||
|
||||
extern void jni_attach_thread(struct scoped_jni *jni);
|
||||
|
||||
extern void jni_detach_thread(struct scoped_jni *env);
|
||||
|
||||
extern void release_string(char **str);
|
||||
|
||||
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) \
|
||||
struct scoped_jni _jni; \
|
||||
jni_attach_thread(&_jni); \
|
||||
JNIEnv *env = _jni.env
|
||||
|
||||
#define scoped_string __attribute__((cleanup(release_string))) char*
|
||||
|
||||
#define find_class(name) env->FindClass(name)
|
||||
#define find_method(cls, name, signature) env->GetMethodID(cls, name, signature)
|
||||
#define new_global(obj) env->NewGlobalRef(obj)
|
||||
#define del_global(obj) env->DeleteGlobalRef(obj)
|
||||
#define get_string(jstr) jni_get_string(env, jstr)
|
||||
#define new_string(cstr) jni_new_string(env, cstr)
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.URL
|
||||
|
||||
data object Core {
|
||||
private external fun startTun(
|
||||
fd: Int,
|
||||
cb: TunInterface
|
||||
)
|
||||
|
||||
private fun parseInetSocketAddress(address: String): InetSocketAddress {
|
||||
val url = URL("https://$address")
|
||||
|
||||
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
|
||||
}
|
||||
|
||||
fun startTun(
|
||||
fd: Int,
|
||||
protect: (Int) -> Boolean,
|
||||
resolverProcess: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress, uid: Int) -> String
|
||||
) {
|
||||
startTun(fd, object : TunInterface {
|
||||
override fun protect(fd: Int) {
|
||||
protect(fd)
|
||||
}
|
||||
|
||||
override fun resolverProcess(
|
||||
protocol: Int,
|
||||
source: String,
|
||||
target: String,
|
||||
uid: Int
|
||||
): String {
|
||||
return resolverProcess(
|
||||
protocol,
|
||||
parseInetSocketAddress(source),
|
||||
parseInetSocketAddress(target),
|
||||
uid,
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
external fun stopTun()
|
||||
|
||||
init {
|
||||
System.loadLibrary("core")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.follow.clash.core
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
interface TunInterface {
|
||||
fun protect(fd: Int)
|
||||
fun resolverProcess(protocol: Int, source: String, target: String, uid: Int): String
|
||||
}
|
||||
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx4G
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
kotlin_version=1.9.22
|
||||
agp_version=8.2.1
|
||||
agp_version=8.9.1
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
|
||||
@@ -24,3 +24,4 @@ plugins {
|
||||
}
|
||||
|
||||
include ":app"
|
||||
include ':core'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+34314
-29943
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.
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -5,6 +5,7 @@ targets:
|
||||
options:
|
||||
build_extensions:
|
||||
'^lib/models/{{}}.dart': 'lib/models/generated/{{}}.g.dart'
|
||||
'^lib/providers/{{}}.dart': 'lib/providers/generated/{{}}.g.dart'
|
||||
freezed:
|
||||
options:
|
||||
build_extensions:
|
||||
|
||||
+1
-1
Submodule core/Clash.Meta updated: 0292a65f16...f19dad529f
+177
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
DefaultValue interface{} `json:"default-value"`
|
||||
}
|
||||
|
||||
type ActionResult struct {
|
||||
Id string `json:"id"`
|
||||
Method Method `json:"method"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (result ActionResult) Json() ([]byte, error) {
|
||||
data, err := json.Marshal(result)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (action Action) getResult(data interface{}) []byte {
|
||||
resultAction := ActionResult{
|
||||
Id: action.Id,
|
||||
Method: action.Method,
|
||||
Data: data,
|
||||
}
|
||||
res, _ := resultAction.Json()
|
||||
return res
|
||||
}
|
||||
|
||||
func handleAction(action *Action, result func(data interface{})) {
|
||||
switch action.Method {
|
||||
case initClashMethod:
|
||||
paramsString := action.Data.(string)
|
||||
result(handleInitClash(paramsString))
|
||||
return
|
||||
case getIsInitMethod:
|
||||
result(handleGetIsInit())
|
||||
return
|
||||
case forceGcMethod:
|
||||
handleForceGc()
|
||||
result(true)
|
||||
return
|
||||
case shutdownMethod:
|
||||
result(handleShutdown())
|
||||
return
|
||||
case validateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
result(handleValidateConfig(data))
|
||||
return
|
||||
case updateConfigMethod:
|
||||
data := []byte(action.Data.(string))
|
||||
result(handleUpdateConfig(data))
|
||||
return
|
||||
case getProxiesMethod:
|
||||
result(handleGetProxies())
|
||||
return
|
||||
case changeProxyMethod:
|
||||
data := action.Data.(string)
|
||||
handleChangeProxy(data, func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getTrafficMethod:
|
||||
result(handleGetTraffic())
|
||||
return
|
||||
case getTotalTrafficMethod:
|
||||
result(handleGetTotalTraffic())
|
||||
return
|
||||
case resetTrafficMethod:
|
||||
handleResetTraffic()
|
||||
result(true)
|
||||
return
|
||||
case asyncTestDelayMethod:
|
||||
data := action.Data.(string)
|
||||
handleAsyncTestDelay(data, func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getConnectionsMethod:
|
||||
result(handleGetConnections())
|
||||
return
|
||||
case closeConnectionsMethod:
|
||||
result(handleCloseConnections())
|
||||
return
|
||||
case closeConnectionMethod:
|
||||
id := action.Data.(string)
|
||||
result(handleCloseConnection(id))
|
||||
return
|
||||
case getExternalProvidersMethod:
|
||||
result(handleGetExternalProviders())
|
||||
return
|
||||
case getExternalProviderMethod:
|
||||
externalProviderName := action.Data.(string)
|
||||
result(handleGetExternalProvider(externalProviderName))
|
||||
case updateGeoDataMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
geoType := params["geo-type"]
|
||||
geoName := params["geo-name"]
|
||||
handleUpdateGeoData(geoType, geoName, func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case updateExternalProviderMethod:
|
||||
providerName := action.Data.(string)
|
||||
handleUpdateExternalProvider(providerName, func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case sideLoadExternalProviderMethod:
|
||||
paramsString := action.Data.(string)
|
||||
var params = map[string]string{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
result(err.Error())
|
||||
return
|
||||
}
|
||||
providerName := params["providerName"]
|
||||
data := params["data"]
|
||||
handleSideLoadExternalProvider(providerName, []byte(data), func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case startLogMethod:
|
||||
handleStartLog()
|
||||
result(true)
|
||||
return
|
||||
case stopLogMethod:
|
||||
handleStopLog()
|
||||
result(true)
|
||||
return
|
||||
case startListenerMethod:
|
||||
result(handleStartListener())
|
||||
return
|
||||
case stopListenerMethod:
|
||||
result(handleStopListener())
|
||||
return
|
||||
case getCountryCodeMethod:
|
||||
ip := action.Data.(string)
|
||||
handleGetCountryCode(ip, func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getMemoryMethod:
|
||||
handleGetMemory(func(value string) {
|
||||
result(value)
|
||||
})
|
||||
return
|
||||
case getProfileMethod:
|
||||
profileId := action.Data.(string)
|
||||
handleGetMemory(func(value string) {
|
||||
result(handleGetProfile(profileId))
|
||||
})
|
||||
return
|
||||
case setStateMethod:
|
||||
data := action.Data.(string)
|
||||
handleSetState(data)
|
||||
result(true)
|
||||
default:
|
||||
handle := nextHandle(action, result)
|
||||
if handle {
|
||||
return
|
||||
} else {
|
||||
result(action.DefaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef void (*release_object_func)(void *obj);
|
||||
|
||||
typedef void (*protect_func)(void *tun_interface, int fd);
|
||||
|
||||
typedef const char* (*resolve_process_func)(void *tun_interface, int protocol, const char *source, const char *target, int uid);
|
||||
|
||||
static void protect(protect_func fn, void *tun_interface, int fd) {
|
||||
if (fn) {
|
||||
fn(tun_interface, fd);
|
||||
}
|
||||
}
|
||||
|
||||
static const char* resolve_process(resolve_process_func fn, void *tun_interface, int protocol, const char *source, const char *target, int uid) {
|
||||
if (fn) {
|
||||
return fn(tun_interface, protocol, source, target, uid);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void release_object(release_object_func fn, void *obj) {
|
||||
if (fn) {
|
||||
return fn(obj);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
globalCallbacks struct {
|
||||
releaseObjectFunc C.release_object_func
|
||||
protectFunc C.protect_func
|
||||
resolveProcessFunc C.resolve_process_func
|
||||
}
|
||||
)
|
||||
|
||||
func protect(callback unsafe.Pointer, fd int) {
|
||||
if globalCallbacks.protectFunc != nil {
|
||||
C.protect(globalCallbacks.protectFunc, callback, C.int(fd))
|
||||
}
|
||||
}
|
||||
|
||||
func resolveProcess(callback unsafe.Pointer, protocol int, source, target string, uid int) string {
|
||||
if globalCallbacks.resolveProcessFunc == nil {
|
||||
return ""
|
||||
}
|
||||
s := C.CString(source)
|
||||
defer C.free(unsafe.Pointer(s))
|
||||
t := C.CString(target)
|
||||
defer C.free(unsafe.Pointer(t))
|
||||
res := C.resolve_process(globalCallbacks.resolveProcessFunc, callback, C.int(protocol), s, t, C.int(uid))
|
||||
defer C.free(unsafe.Pointer(res))
|
||||
return C.GoString(res)
|
||||
}
|
||||
|
||||
func releaseObject(callback unsafe.Pointer) {
|
||||
if globalCallbacks.releaseObjectFunc == nil {
|
||||
return
|
||||
}
|
||||
C.release_object(globalCallbacks.releaseObjectFunc, callback)
|
||||
}
|
||||
|
||||
//export registerCallbacks
|
||||
func registerCallbacks(markSocketFunc C.protect_func, resolveProcessFunc C.resolve_process_func, releaseObjectFunc C.release_object_func) {
|
||||
globalCallbacks.protectFunc = markSocketFunc
|
||||
globalCallbacks.resolveProcessFunc = resolveProcessFunc
|
||||
globalCallbacks.releaseObjectFunc = releaseObjectFunc
|
||||
}
|
||||
+234
-294
@@ -1,125 +1,59 @@
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
ap "github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/batch"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/constant/features"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/hub/route"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type healthCheckSchema struct {
|
||||
Enable bool `provider:"enable"`
|
||||
URL string `provider:"url"`
|
||||
Interval int `provider:"interval"`
|
||||
TestTimeout int `provider:"timeout,omitempty"`
|
||||
Lazy bool `provider:"lazy,omitempty"`
|
||||
ExpectedStatus string `provider:"expected-status,omitempty"`
|
||||
}
|
||||
|
||||
type proxyProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
Filter string `provider:"filter,omitempty"`
|
||||
ExcludeFilter string `provider:"exclude-filter,omitempty"`
|
||||
ExcludeType string `provider:"exclude-type,omitempty"`
|
||||
DialerProxy string `provider:"dialer-proxy,omitempty"`
|
||||
|
||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||
Override ap.OverrideSchema `provider:"override,omitempty"`
|
||||
Header map[string][]string `provider:"header,omitempty"`
|
||||
}
|
||||
|
||||
type ruleProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Behavior string `provider:"behavior"`
|
||||
Path string `provider:"path,omitempty"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Proxy string `provider:"proxy,omitempty"`
|
||||
Format string `provider:"format,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
ProfilePath *string `json:"profile-path"`
|
||||
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"`
|
||||
UpdateAt time.Time `json:"update-at"`
|
||||
}
|
||||
|
||||
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)
|
||||
func splitByMultipleSeparators(s string) interface{} {
|
||||
isSeparator := func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == ';'
|
||||
}
|
||||
|
||||
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)
|
||||
parts := strings.FieldsFunc(s, isSeparator)
|
||||
if len(parts) > 1 {
|
||||
return parts
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var (
|
||||
version = 0
|
||||
isRunning = false
|
||||
runLock sync.Mutex
|
||||
ips = []string{"ipwho.is", "api.ip.sb", "ipapi.co", "ipinfo.io"}
|
||||
b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50))
|
||||
)
|
||||
|
||||
type ExternalProviders []ExternalProvider
|
||||
|
||||
func (a ExternalProviders) Len() int { return len(a) }
|
||||
func (a ExternalProviders) Less(i, j int) bool { return a[i].Name < a[j].Name }
|
||||
func (a ExternalProviders) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func readFile(path string) ([]byte, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
@@ -132,187 +66,155 @@ func readFile(path string) ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func removeFile(path string) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Remove(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func getProfilePath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "profiles", id+".yaml")
|
||||
}
|
||||
|
||||
func getRawConfigWithPath(path *string) *config.RawConfig {
|
||||
if path == nil {
|
||||
func getProfileProvidersPath(id string) string {
|
||||
return filepath.Join(constant.Path.HomeDir(), "providers", id)
|
||||
}
|
||||
|
||||
func getRawConfigWithId(id string) *config.RawConfig {
|
||||
path := getProfilePath(id)
|
||||
bytes, err := readFile(path)
|
||||
if err != nil {
|
||||
log.Errorln("profile is not exist")
|
||||
return config.DefaultRawConfig()
|
||||
} else {
|
||||
bytes, err := readFile(*path)
|
||||
if err != nil {
|
||||
log.Errorln("getProfile readFile error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
log.Errorln("unmarshalRawConfig error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
}
|
||||
for _, mapping := range prof.ProxyProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
prof, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
log.Errorln("getProfile UnmarshalRawConfig error %v", err)
|
||||
return config.DefaultRawConfig()
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
}
|
||||
for _, mapping := range prof.RuleProvider {
|
||||
value, exist := mapping["path"].(string)
|
||||
if !exist {
|
||||
continue
|
||||
}
|
||||
return prof
|
||||
mapping["path"] = filepath.Join(getProfileProvidersPath(id), value)
|
||||
}
|
||||
return prof
|
||||
}
|
||||
|
||||
func getExternalProvidersRaw() map[string]cp.Provider {
|
||||
eps := make(map[string]cp.Provider)
|
||||
for n, p := range tunnel.Providers() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
eps[n] = p
|
||||
}
|
||||
}
|
||||
for n, p := range tunnel.RuleProviders() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
eps[n] = p
|
||||
}
|
||||
}
|
||||
return eps
|
||||
}
|
||||
|
||||
func toExternalProvider(p cp.Provider) (*ExternalProvider, error) {
|
||||
switch p.(type) {
|
||||
case *provider.ProxySetProvider:
|
||||
psp := p.(*provider.ProxySetProvider)
|
||||
return &ExternalProvider{
|
||||
Name: psp.Name(),
|
||||
Type: psp.Type().String(),
|
||||
VehicleType: psp.VehicleType().String(),
|
||||
Count: psp.Count(),
|
||||
UpdateAt: psp.UpdatedAt(),
|
||||
Path: psp.Vehicle().Path(),
|
||||
SubscriptionInfo: psp.GetSubscriptionInfo(),
|
||||
}, nil
|
||||
case *rp.RuleSetProvider:
|
||||
rsp := p.(*rp.RuleSetProvider)
|
||||
return &ExternalProvider{
|
||||
Name: rsp.Name(),
|
||||
Type: rsp.Type().String(),
|
||||
VehicleType: rsp.VehicleType().String(),
|
||||
Count: rsp.Count(),
|
||||
UpdateAt: rsp.UpdatedAt(),
|
||||
Path: rsp.Vehicle().Path(),
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.New("not external provider")
|
||||
}
|
||||
}
|
||||
|
||||
func decorationConfig(profilePath *string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithPath(profilePath)
|
||||
func sideUpdateExternalProvider(p cp.Provider, bytes []byte) error {
|
||||
switch p.(type) {
|
||||
case *provider.ProxySetProvider:
|
||||
psp := p.(*provider.ProxySetProvider)
|
||||
_, _, err := psp.SideUpdate(bytes)
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case rp.RuleSetProvider:
|
||||
rsp := p.(*rp.RuleSetProvider)
|
||||
_, _, err := rsp.SideUpdate(bytes)
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("not external provider")
|
||||
}
|
||||
}
|
||||
|
||||
func decorationConfig(profileId string, cfg config.RawConfig) *config.RawConfig {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
overwriteConfig(prof, cfg)
|
||||
return prof
|
||||
}
|
||||
|
||||
func Reduce[T any, U any](s []T, initVal U, f func(U, T) U) U {
|
||||
for _, v := range s {
|
||||
initVal = f(initVal, v)
|
||||
}
|
||||
return initVal
|
||||
}
|
||||
|
||||
func Map[T, U any](slice []T, fn func(T) U) []U {
|
||||
result := make([]U, len(slice))
|
||||
for i, v := range slice {
|
||||
result[i] = fn(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func replaceFromMap(s string, m map[string]string) string {
|
||||
for k, v := range m {
|
||||
s = strings.ReplaceAll(s, k, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func removeDuplicateFromSlice[T any](slice []T) []T {
|
||||
result := make([]T, 0)
|
||||
seen := make(map[any]struct{})
|
||||
for _, value := range slice {
|
||||
if _, ok := seen[value]; !ok {
|
||||
result = append(result, value)
|
||||
seen[value] = struct{}{}
|
||||
func attachHosts(hosts, patchHosts map[string]any) {
|
||||
for k, v := range patchHosts {
|
||||
if str, ok := v.(string); ok {
|
||||
hosts[k] = splitByMultipleSeparators(str)
|
||||
}
|
||||
}
|
||||
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:
|
||||
func updatePatchDns(dns config.RawDNS) {
|
||||
for pair := dns.NameServerPolicy.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if str, ok := pair.Value.(string); ok {
|
||||
dns.NameServerPolicy.Set(pair.Key, splitByMultipleSeparators(str))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func overrideRules(rules, patchRules []string) []string {
|
||||
target := ""
|
||||
for _, line := range rules {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
if len(rule) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(rule[0], "MATCH") {
|
||||
target = rule[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ProxyProxies := Reduce(selectArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||
if cur["proxies"] == nil {
|
||||
return res
|
||||
}
|
||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||
if str, ok := proxyName.(string); ok {
|
||||
str = replaceFromMap(str, replacements)
|
||||
if str != "Proxy" {
|
||||
res = append(res, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
ProxyProxies = removeDuplicateFromSlice(ProxyProxies)
|
||||
|
||||
AutoProxies := Reduce(urlTestArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||
if cur["proxies"] == nil {
|
||||
return res
|
||||
}
|
||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||
if str, ok := proxyName.(string); ok {
|
||||
str = replaceFromMap(str, replacements)
|
||||
if str != "Auto" {
|
||||
res = append(res, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
AutoProxies = removeDuplicateFromSlice(AutoProxies)
|
||||
|
||||
FallbackProxies := Reduce(fallbackArr, []string{}, func(res []string, cur map[string]any) []string {
|
||||
if cur["proxies"] == nil {
|
||||
return res
|
||||
}
|
||||
for _, proxyName := range cur["proxies"].([]interface{}) {
|
||||
if str, ok := proxyName.(string); ok {
|
||||
str = replaceFromMap(str, replacements)
|
||||
if str != "Fallback" {
|
||||
res = append(res, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
FallbackProxies = removeDuplicateFromSlice(FallbackProxies)
|
||||
|
||||
var computedProxyGroup []map[string]any
|
||||
|
||||
if len(ProxyProxies) > 0 {
|
||||
computedProxyGroup = append(computedProxyGroup,
|
||||
map[string]any{
|
||||
"name": "Proxy",
|
||||
"type": "select",
|
||||
"proxies": ProxyProxies,
|
||||
})
|
||||
if target == "" {
|
||||
return rules
|
||||
}
|
||||
|
||||
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)
|
||||
rulesExt := lo.Map(ips, func(ip string, _ int) string {
|
||||
return fmt.Sprintf("DOMAIN,%s,%s", ip, target)
|
||||
})
|
||||
|
||||
*proxyGroup = computedProxyGroup
|
||||
*rule = computedRule
|
||||
return append(append(rulesExt, patchRules...), rules...)
|
||||
}
|
||||
|
||||
func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) {
|
||||
@@ -322,11 +224,11 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.ExternalUIURL = ""
|
||||
targetConfig.TCPConcurrent = patchConfig.TCPConcurrent
|
||||
targetConfig.UnifiedDelay = patchConfig.UnifiedDelay
|
||||
//targetConfig.GeodataMode = false
|
||||
targetConfig.IPv6 = patchConfig.IPv6
|
||||
targetConfig.LogLevel = patchConfig.LogLevel
|
||||
targetConfig.Port = 0
|
||||
targetConfig.SocksPort = 0
|
||||
targetConfig.KeepAliveInterval = patchConfig.KeepAliveInterval
|
||||
targetConfig.MixedPort = patchConfig.MixedPort
|
||||
targetConfig.FindProcessMode = patchConfig.FindProcessMode
|
||||
targetConfig.AllowLan = patchConfig.AllowLan
|
||||
@@ -335,52 +237,94 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
|
||||
targetConfig.Tun.Device = patchConfig.Tun.Device
|
||||
targetConfig.Tun.DNSHijack = patchConfig.Tun.DNSHijack
|
||||
targetConfig.Tun.Stack = patchConfig.Tun.Stack
|
||||
targetConfig.Tun.RouteAddress = patchConfig.Tun.RouteAddress
|
||||
targetConfig.GeodataLoader = patchConfig.GeodataLoader
|
||||
targetConfig.Profile.StoreSelected = false
|
||||
targetConfig.GeoXUrl = patchConfig.GeoXUrl
|
||||
targetConfig.GlobalUA = patchConfig.GlobalUA
|
||||
if targetConfig.DNS.Enable == false {
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
if configParams.TestURL != nil {
|
||||
constant.DefaultTestURL = *configParams.TestURL
|
||||
}
|
||||
//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)
|
||||
for idx := range targetConfig.ProxyGroup {
|
||||
targetConfig.ProxyGroup[idx]["url"] = ""
|
||||
}
|
||||
attachHosts(targetConfig.Hosts, patchConfig.Hosts)
|
||||
if configParams.OverrideDns {
|
||||
updatePatchDns(patchConfig.DNS)
|
||||
targetConfig.DNS = patchConfig.DNS
|
||||
} else {
|
||||
if targetConfig.DNS.Enable == false {
|
||||
targetConfig.DNS.Enable = true
|
||||
}
|
||||
}
|
||||
if configParams.OverrideRule {
|
||||
targetConfig.Rule = overrideRules(patchConfig.Rule, []string{})
|
||||
} else {
|
||||
targetConfig.Rule = overrideRules(targetConfig.Rule, patchConfig.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
func patchConfig(general *config.General) {
|
||||
func patchConfig() {
|
||||
log.Infoln("[Apply] patch")
|
||||
route.ReStartServer(general.ExternalController)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
inbound.SetAllowedIPs(general.LanAllowedIPs)
|
||||
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
|
||||
listener.SetBindAddress(general.BindAddress)
|
||||
general := currentConfig.General
|
||||
controller := currentConfig.Controller
|
||||
tls := currentConfig.TLS
|
||||
tunnel.SetSniffing(general.Sniffing)
|
||||
tunnel.SetFindProcessMode(general.FindProcessMode)
|
||||
dialer.SetTcpConcurrent(general.TCPConcurrent)
|
||||
dialer.DefaultInterface.Store(general.Interface)
|
||||
adapter.UnifiedDelay.Store(general.UnifiedDelay)
|
||||
tunnel.SetMode(general.Mode)
|
||||
log.SetLevel(general.LogLevel)
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
|
||||
route.ReCreateServer(&route.Config{
|
||||
Addr: controller.ExternalController,
|
||||
TLSAddr: controller.ExternalControllerTLS,
|
||||
UnixAddr: controller.ExternalControllerUnix,
|
||||
PipeAddr: controller.ExternalControllerPipe,
|
||||
Secret: controller.Secret,
|
||||
Certificate: tls.Certificate,
|
||||
PrivateKey: tls.PrivateKey,
|
||||
DohServer: controller.ExternalDohServer,
|
||||
IsDebug: false,
|
||||
Cors: route.Cors{
|
||||
AllowOrigins: controller.Cors.AllowOrigins,
|
||||
AllowPrivateNetwork: controller.Cors.AllowPrivateNetwork,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func updateListeners(force bool) {
|
||||
if !isRunning {
|
||||
return
|
||||
}
|
||||
general := currentConfig.General
|
||||
listeners := currentConfig.Listeners
|
||||
if force == true {
|
||||
stopListeners()
|
||||
}
|
||||
listener.PatchInboundListeners(listeners, tunnel.Tunnel, true)
|
||||
listener.SetAllowLan(general.AllowLan)
|
||||
inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes)
|
||||
inbound.SetAllowedIPs(general.LanAllowedIPs)
|
||||
inbound.SetDisAllowedIPs(general.LanDisAllowedIPs)
|
||||
listener.SetBindAddress(general.BindAddress)
|
||||
listener.ReCreateHTTP(general.Port, tunnel.Tunnel)
|
||||
listener.ReCreateSocks(general.SocksPort, tunnel.Tunnel)
|
||||
listener.ReCreateRedir(general.RedirPort, tunnel.Tunnel)
|
||||
listener.ReCreateAutoRedir(general.EBpf.AutoRedir, tunnel.Tunnel)
|
||||
listener.ReCreateTProxy(general.TProxyPort, tunnel.Tunnel)
|
||||
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
||||
listener.ReCreateMixed(general.MixedPort, tunnel.Tunnel)
|
||||
listener.ReCreateShadowSocks(general.ShadowSocksConfig, tunnel.Tunnel)
|
||||
listener.ReCreateVmess(general.VmessConfig, tunnel.Tunnel)
|
||||
listener.ReCreateTuic(general.TuicServer, tunnel.Tunnel)
|
||||
tunnel.SetMode(general.Mode)
|
||||
log.SetLevel(general.LogLevel)
|
||||
if !features.Android {
|
||||
listener.ReCreateTun(general.Tun, tunnel.Tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
resolver.DisableIPv6 = !general.IPv6
|
||||
func stopListeners() {
|
||||
listener.StopListener()
|
||||
}
|
||||
|
||||
func patchSelectGroup() {
|
||||
@@ -408,24 +352,20 @@ func patchSelectGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
var applyLock sync.Mutex
|
||||
|
||||
func applyConfig() {
|
||||
applyLock.Lock()
|
||||
defer applyLock.Unlock()
|
||||
cfg, err := config.ParseRawConfig(currentConfig)
|
||||
func applyConfig(rawConfig *config.RawConfig) error {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
var err error
|
||||
currentConfig, err = config.ParseRawConfig(rawConfig)
|
||||
if err != nil {
|
||||
cfg, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
if configParams.TestURL != nil {
|
||||
constant.DefaultTestURL = *configParams.TestURL
|
||||
currentConfig, _ = config.ParseRawConfig(config.DefaultRawConfig())
|
||||
}
|
||||
if configParams.IsPatch {
|
||||
patchConfig(cfg.General)
|
||||
patchConfig()
|
||||
} else {
|
||||
closeConnections()
|
||||
runtime.GC()
|
||||
hub.UltraApplyConfig(cfg, true)
|
||||
hub.ApplyConfig(currentConfig)
|
||||
patchSelectGroup()
|
||||
}
|
||||
updateListeners(false)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InitParams struct {
|
||||
HomeDir string `json:"home-dir"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type ConfigExtendedParams struct {
|
||||
IsPatch bool `json:"is-patch"`
|
||||
IsCompatible bool `json:"is-compatible"`
|
||||
SelectedMap map[string]string `json:"selected-map"`
|
||||
TestURL *string `json:"test-url"`
|
||||
OverrideDns bool `json:"override-dns"`
|
||||
OverrideRule bool `json:"override-rule"`
|
||||
}
|
||||
|
||||
type GenerateConfigParams struct {
|
||||
ProfileId string `json:"profile-id"`
|
||||
Config config.RawConfig `json:"config" `
|
||||
Params ConfigExtendedParams `json:"params"`
|
||||
}
|
||||
|
||||
type ChangeProxyParams struct {
|
||||
GroupName *string `json:"group-name"`
|
||||
ProxyName *string `json:"proxy-name"`
|
||||
}
|
||||
|
||||
type TestDelayParams struct {
|
||||
ProxyName string `json:"proxy-name"`
|
||||
TestUrl string `json:"test-url"`
|
||||
Timeout int64 `json:"timeout"`
|
||||
}
|
||||
|
||||
type ExternalProvider struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
VehicleType string `json:"vehicle-type"`
|
||||
Count int `json:"count"`
|
||||
Path string `json:"path"`
|
||||
UpdateAt time.Time `json:"update-at"`
|
||||
SubscriptionInfo *provider.SubscriptionInfo `json:"subscription-info"`
|
||||
}
|
||||
|
||||
const (
|
||||
messageMethod Method = "message"
|
||||
initClashMethod Method = "initClash"
|
||||
getIsInitMethod Method = "getIsInit"
|
||||
forceGcMethod Method = "forceGc"
|
||||
shutdownMethod Method = "shutdown"
|
||||
validateConfigMethod Method = "validateConfig"
|
||||
updateConfigMethod Method = "updateConfig"
|
||||
getProxiesMethod Method = "getProxies"
|
||||
changeProxyMethod Method = "changeProxy"
|
||||
getTrafficMethod Method = "getTraffic"
|
||||
getTotalTrafficMethod Method = "getTotalTraffic"
|
||||
resetTrafficMethod Method = "resetTraffic"
|
||||
asyncTestDelayMethod Method = "asyncTestDelay"
|
||||
getConnectionsMethod Method = "getConnections"
|
||||
closeConnectionsMethod Method = "closeConnections"
|
||||
closeConnectionMethod Method = "closeConnection"
|
||||
getExternalProvidersMethod Method = "getExternalProviders"
|
||||
getExternalProviderMethod Method = "getExternalProvider"
|
||||
getCountryCodeMethod Method = "getCountryCode"
|
||||
getMemoryMethod Method = "getMemory"
|
||||
updateGeoDataMethod Method = "updateGeoData"
|
||||
updateExternalProviderMethod Method = "updateExternalProvider"
|
||||
sideLoadExternalProviderMethod Method = "sideLoadExternalProvider"
|
||||
startLogMethod Method = "startLog"
|
||||
stopLogMethod Method = "stopLog"
|
||||
startListenerMethod Method = "startListener"
|
||||
stopListenerMethod Method = "stopListener"
|
||||
updateDnsMethod Method = "updateDns"
|
||||
setStateMethod Method = "setState"
|
||||
getAndroidVpnOptionsMethod Method = "getAndroidVpnOptions"
|
||||
getRunTimeMethod Method = "getRunTime"
|
||||
getCurrentProfileNameMethod Method = "getCurrentProfileName"
|
||||
getProfileMethod Method = "getProfile"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build cgo
|
||||
|
||||
package dart_bridge
|
||||
|
||||
/*
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !cgo
|
||||
|
||||
package dart_bridge
|
||||
|
||||
func SendToPort(port int64, msg string) bool {
|
||||
return false
|
||||
}
|
||||
+50
-44
@@ -1,70 +1,73 @@
|
||||
module core
|
||||
|
||||
go 1.21.0
|
||||
go 1.20
|
||||
|
||||
replace github.com/metacubex/mihomo => ./Clash.Meta
|
||||
|
||||
require (
|
||||
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
|
||||
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
|
||||
github.com/metacubex/mihomo v0.0.0-00010101000000-000000000000
|
||||
github.com/samber/lo v1.49.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.2.4 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // 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/coreos/go-iptables v0.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.13.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.14 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.1 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.1 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
|
||||
github.com/metacubex/utls v1.6.6 // indirect
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
github.com/metacubex/utls v1.6.8-alpha.4 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
@@ -72,20 +75,18 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
github.com/sagernet/fswatch v0.1.1 // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/sing v0.5.0-alpha.10 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
|
||||
github.com/sagernet/sing v0.5.2 // indirect
|
||||
github.com/sagernet/sing-mux v0.2.1 // indirect
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/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/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
@@ -94,18 +95,23 @@ require (
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
|
||||
+100
-104
@@ -1,14 +1,10 @@
|
||||
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/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/go-krypto v1.2.4 h1:mXuNdK6M317aPV0llW6Xpjbo4moOlPF7Yxz4tb4b4Go=
|
||||
github.com/RyuaNerin/go-krypto v1.2.4/go.mod h1:QqCYkoutU3yInyD9INt2PGolVRsc3W4oraQadVGXJ/8=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
@@ -21,41 +17,37 @@ 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/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/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/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
github.com/coreos/go-iptables v0.8.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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
|
||||
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/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
|
||||
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/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -67,37 +59,32 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
|
||||
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.1 h1:aPx49MwJbekCzOyhZDjJVb0hx3A0KLjlbLx6p2gY0p0=
|
||||
github.com/gofrs/uuid/v5 v5.3.1/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/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/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6 h1:dh8D8FksyMhD64mRMbUhZHWYJfNoNMCxfVq6eexleMw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20240529192340-51bc6136a0a6/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/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/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
@@ -108,40 +95,52 @@ 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/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
|
||||
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e h1:bLYn3GuRvWDcBDAkIv5kUYIhzHwafDVq635BuybnKqI=
|
||||
github.com/metacubex/quic-go v0.45.1-0.20240610004319-163fee60637e/go.mod h1:Yza2H7Ax1rxWPUcJx0vW+oAt9EsPuSiyQFhFabUPzwU=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/jBDS/kCYjz/x+0BCOKfd2VODYevyeIt+Ds=
|
||||
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
|
||||
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e h1:o+zohxPRo45P35fS9u1zfdBgr+L/7S0ObGU6YjbVBIc=
|
||||
github.com/metacubex/sing-tun v0.2.7-0.20240627012306-9d1f5fc0b45e/go.mod h1:WwJGbCx7bQcBzuQXiDOJvZH27R0kIjKNNlISIWsL6kM=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ=
|
||||
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a h1:NpSGclHJUYndUwBmyIpFBSoBVg8PoVX7QQKhYg0DjM0=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20240618022557-a6efaa37127a/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
|
||||
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
|
||||
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/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
|
||||
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI=
|
||||
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
|
||||
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
|
||||
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
|
||||
@@ -156,39 +155,33 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
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/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
|
||||
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
|
||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.5.0-alpha.10 h1:kuHl10gpjbKQAdQfyogQU3u0CVnpqC3wrAHe/+BFaXc=
|
||||
github.com/sagernet/sing v0.5.0-alpha.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 h1:5bCAkvDDzSMITiHFjolBwpdqYsvycdTu71FsMEFXQ14=
|
||||
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
|
||||
github.com/sagernet/sing v0.5.2 h1:2OZQJNKGtji/66QLxbf/T/dqtK/3+fF/zuHH9tsGK7M=
|
||||
github.com/sagernet/sing v0.5.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
|
||||
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
|
||||
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
@@ -201,8 +194,7 @@ 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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
@@ -214,10 +206,16 @@ 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.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
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/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
@@ -226,21 +224,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=
|
||||
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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -252,29 +250,27 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
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=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+273
-271
@@ -1,198 +1,163 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"context"
|
||||
"core/state"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/adapter"
|
||||
"github.com/metacubex/mihomo/adapter/outboundgroup"
|
||||
"github.com/metacubex/mihomo/adapter/provider"
|
||||
"github.com/metacubex/mihomo/common/structure"
|
||||
"github.com/metacubex/mihomo/common/observable"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/geodata"
|
||||
"github.com/metacubex/mihomo/component/mmdb"
|
||||
"github.com/metacubex/mihomo/component/updater"
|
||||
"github.com/metacubex/mihomo/config"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
cp "github.com/metacubex/mihomo/constant/provider"
|
||||
"github.com/metacubex/mihomo/hub/executor"
|
||||
"github.com/metacubex/mihomo/listener"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
rp "github.com/metacubex/mihomo/rules/provider"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"github.com/metacubex/mihomo/tunnel/statistic"
|
||||
"golang.org/x/net/context"
|
||||
"os"
|
||||
"net"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var currentConfig = config.DefaultRawConfig()
|
||||
var (
|
||||
isInit = false
|
||||
configParams = ConfigExtendedParams{}
|
||||
externalProviders = map[string]cp.Provider{}
|
||||
logSubscriber observable.Subscription[log.Event]
|
||||
currentConfig *config.Config
|
||||
)
|
||||
|
||||
var configParams = ConfigExtendedParams{}
|
||||
|
||||
var isInit = false
|
||||
|
||||
var currentProfileName = ""
|
||||
|
||||
//export initClash
|
||||
func initClash(homeDirStr *C.char) bool {
|
||||
func handleInitClash(paramsString string) bool {
|
||||
var params = InitParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), ¶ms)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
version = params.Version
|
||||
if !isInit {
|
||||
constant.SetHomeDir(C.GoString(homeDirStr))
|
||||
constant.SetHomeDir(params.HomeDir)
|
||||
isInit = true
|
||||
}
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export getIsInit
|
||||
func getIsInit() bool {
|
||||
func handleStartListener() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = true
|
||||
updateListeners(true)
|
||||
return true
|
||||
}
|
||||
|
||||
func handleStopListener() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
isRunning = false
|
||||
listener.StopListener()
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetIsInit() bool {
|
||||
return isInit
|
||||
}
|
||||
|
||||
//export restartClash
|
||||
func restartClash() bool {
|
||||
execPath, _ := os.Executable()
|
||||
go restartExecutable(execPath)
|
||||
return true
|
||||
}
|
||||
|
||||
//export shutdownClash
|
||||
func shutdownClash() bool {
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
isInit = false
|
||||
currentConfig = nil
|
||||
return true
|
||||
}
|
||||
|
||||
//export forceGc
|
||||
func forceGc() {
|
||||
func handleForceGc() {
|
||||
go func() {
|
||||
log.Infoln("[APP] request force GC")
|
||||
runtime.GC()
|
||||
}()
|
||||
}
|
||||
|
||||
//export setCurrentProfileName
|
||||
func setCurrentProfileName(s *C.char) {
|
||||
currentProfileName = C.GoString(s)
|
||||
func handleShutdown() bool {
|
||||
stopListeners()
|
||||
executor.Shutdown()
|
||||
runtime.GC()
|
||||
isInit = false
|
||||
return true
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(currentProfileName)
|
||||
func handleValidateConfig(bytes []byte) string {
|
||||
_, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
//export validateConfig
|
||||
func validateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
bytes := []byte(C.GoString(s))
|
||||
go func() {
|
||||
_, err := config.UnmarshalRawConfig(bytes)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
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 ""
|
||||
}
|
||||
|
||||
//export updateConfig
|
||||
func updateConfig(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
var params = &GenerateConfigParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
configParams = params.Params
|
||||
prof := decorationConfig(params.ProfilePath, params.Config)
|
||||
currentConfig = prof
|
||||
applyConfig()
|
||||
bridge.SendToPort(i, "")
|
||||
}()
|
||||
}
|
||||
|
||||
//export clearEffect
|
||||
func clearEffect(s *C.char) {
|
||||
path := C.GoString(s)
|
||||
go func() {
|
||||
rawCfg := getRawConfigWithPath(&path)
|
||||
for _, mapping := range rawCfg.RuleProvider {
|
||||
schema := &ruleProviderSchema{}
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return
|
||||
}
|
||||
if schema.Type == "http" {
|
||||
_ = removeFile(constant.Path.Resolve(schema.Path))
|
||||
}
|
||||
}
|
||||
for _, mapping := range rawCfg.ProxyProvider {
|
||||
schema := &proxyProviderSchema{
|
||||
HealthCheck: healthCheckSchema{
|
||||
Lazy: true,
|
||||
},
|
||||
}
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return
|
||||
}
|
||||
if schema.Type == "http" {
|
||||
_ = removeFile(constant.Path.Resolve(schema.Path))
|
||||
}
|
||||
}
|
||||
_ = removeFile(path)
|
||||
}()
|
||||
}
|
||||
|
||||
//export getProxies
|
||||
func getProxies() *C.char {
|
||||
func handleGetProxies() string {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
data, err := json.Marshal(tunnel.ProxiesWithProviders())
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export changeProxy
|
||||
func changeProxy(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
func handleChangeProxy(data string, fn func(string string)) {
|
||||
runLock.Lock()
|
||||
go func() {
|
||||
defer runLock.Unlock()
|
||||
var params = &ChangeProxyParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
err := json.Unmarshal([]byte(data), params)
|
||||
if err != nil {
|
||||
log.Infoln("Unmarshal ChangeProxyParams %v", err)
|
||||
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.Selector)
|
||||
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
|
||||
}
|
||||
|
||||
err = selector.Set(proxyName)
|
||||
if err == nil {
|
||||
log.Infoln("[Selector] %s selected %s", groupName, proxyName)
|
||||
}
|
||||
fn("")
|
||||
return
|
||||
}()
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Now()
|
||||
func handleGetTraffic() string {
|
||||
up, down := statistic.DefaultManager.Current(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -200,14 +165,13 @@ func getTraffic() *C.char {
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
up, down := statistic.DefaultManager.Total()
|
||||
func handleGetTotalTraffic() string {
|
||||
up, down := statistic.DefaultManager.Total(state.CurrentState.OnlyStatisticsProxy)
|
||||
traffic := map[string]int64{
|
||||
"up": up,
|
||||
"down": down,
|
||||
@@ -215,30 +179,37 @@ func getTotalTraffic() *C.char {
|
||||
data, err := json.Marshal(traffic)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export resetTraffic
|
||||
func resetTraffic() {
|
||||
func handleGetProfile(profileId string) string {
|
||||
prof := getRawConfigWithId(profileId)
|
||||
data, err := json.Marshal(prof)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleResetTraffic() {
|
||||
statistic.DefaultManager.ResetStatistic()
|
||||
}
|
||||
|
||||
//export asyncTestDelay
|
||||
func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(s)
|
||||
go func() {
|
||||
func handleAsyncTestDelay(paramsString string, fn func(string)) {
|
||||
b.Go(paramsString, func() (bool, error) {
|
||||
var params = &TestDelayParams{}
|
||||
err := json.Unmarshal([]byte(paramsString), params)
|
||||
if err != nil {
|
||||
return
|
||||
fn("")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
expectedStatus, err := utils.NewUnsignedRanges[uint16]("")
|
||||
if err != nil {
|
||||
return
|
||||
fn("")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(params.Timeout))
|
||||
@@ -254,52 +225,47 @@ func asyncTestDelay(s *C.char, port C.longlong) {
|
||||
if proxy == nil {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
fn(string(data))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delay, err := proxy.URLTest(ctx, constant.DefaultTestURL, expectedStatus)
|
||||
testUrl := constant.DefaultTestURL
|
||||
|
||||
if params.TestUrl != "" {
|
||||
testUrl = params.TestUrl
|
||||
}
|
||||
delayData.Url = testUrl
|
||||
|
||||
delay, err := proxy.URLTest(ctx, testUrl, expectedStatus)
|
||||
if err != nil || delay == 0 {
|
||||
delayData.Value = -1
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
fn(string(data))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
delayData.Value = int32(delay)
|
||||
data, _ := json.Marshal(delayData)
|
||||
bridge.SendToPort(i, string(data))
|
||||
return
|
||||
}()
|
||||
fn(string(data))
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
//export getVersionInfo
|
||||
func getVersionInfo() *C.char {
|
||||
versionInfo := map[string]string{
|
||||
"clashName": constant.Name,
|
||||
"version": "1.18.5",
|
||||
}
|
||||
data, err := json.Marshal(versionInfo)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getConnections
|
||||
func getConnections() *C.char {
|
||||
func handleGetConnections() string {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
snapshot := statistic.DefaultManager.Snapshot()
|
||||
data, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export closeConnections
|
||||
func closeConnections() {
|
||||
func handleCloseConnections() bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
@@ -307,143 +273,179 @@ func closeConnections() {
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
//export closeConnection
|
||||
func closeConnection(id *C.char) {
|
||||
connectionId := C.GoString(id)
|
||||
func handleCloseConnection(connectionId string) bool {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
c := statistic.DefaultManager.Get(connectionId)
|
||||
if c == nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
_ = c.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
//export getProviders
|
||||
func getProviders() *C.char {
|
||||
data, err := json.Marshal(tunnel.Providers())
|
||||
var msg *C.char
|
||||
if err != nil {
|
||||
msg = C.CString("")
|
||||
return msg
|
||||
}
|
||||
msg = C.CString(string(data))
|
||||
return msg
|
||||
}
|
||||
|
||||
//export getProvider
|
||||
func getProvider(name *C.char) *C.char {
|
||||
providerName := C.GoString(name)
|
||||
providers := tunnel.Providers()
|
||||
data, err := json.Marshal(providers[providerName])
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export getExternalProviders
|
||||
func getExternalProviders() *C.char {
|
||||
externalProviders := make([]ExternalProvider, 0)
|
||||
providers := tunnel.Providers()
|
||||
for n, p := range providers {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
p := p.(*provider.ProxySetProvider)
|
||||
externalProviders = append(externalProviders, ExternalProvider{
|
||||
Name: n,
|
||||
Type: p.Type().String(),
|
||||
VehicleType: p.VehicleType().String(),
|
||||
UpdateAt: p.UpdatedAt,
|
||||
})
|
||||
func handleGetExternalProviders() string {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
externalProviders = getExternalProvidersRaw()
|
||||
eps := make([]ExternalProvider, 0)
|
||||
for _, p := range externalProviders {
|
||||
externalProvider, err := toExternalProvider(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eps = append(eps, *externalProvider)
|
||||
}
|
||||
for n, p := range tunnel.RuleProviders() {
|
||||
if p.VehicleType() != cp.Compatible {
|
||||
p := p.(*rp.RuleSetProvider)
|
||||
externalProviders = append(externalProviders, ExternalProvider{
|
||||
Name: n,
|
||||
Type: p.Type().String(),
|
||||
VehicleType: p.VehicleType().String(),
|
||||
UpdateAt: p.UpdatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(externalProviders)
|
||||
sort.Sort(ExternalProviders(eps))
|
||||
data, err := json.Marshal(eps)
|
||||
if err != nil {
|
||||
return C.CString("")
|
||||
return ""
|
||||
}
|
||||
return C.CString(string(data))
|
||||
return string(data)
|
||||
}
|
||||
|
||||
//export updateExternalProvider
|
||||
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
providerNameString := C.GoString(providerName)
|
||||
providerTypeString := C.GoString(providerType)
|
||||
func handleGetExternalProvider(externalProviderName string) string {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
externalProvider, exist := externalProviders[externalProviderName]
|
||||
if !exist {
|
||||
return ""
|
||||
}
|
||||
e, err := toExternalProvider(externalProvider)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
data, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleUpdateGeoData(geoType string, geoName string, fn func(value string)) {
|
||||
go func() {
|
||||
switch providerTypeString {
|
||||
case "Proxy":
|
||||
providers := tunnel.Providers()
|
||||
err := providers[providerNameString].Update()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
case "Rule":
|
||||
providers := tunnel.RuleProviders()
|
||||
err := providers[providerNameString].Update()
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
path := constant.Path.Resolve(geoName)
|
||||
switch geoType {
|
||||
case "MMDB":
|
||||
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateMMDBWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
case "ASN":
|
||||
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateASNWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoIp":
|
||||
err := geodata.DownloadGeoIP(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateGeoIpWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
case "GeoSite":
|
||||
err := geodata.DownloadGeoSite(constant.Path.Resolve(providerNameString))
|
||||
err := updater.UpdateGeoSiteWithPath(path)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
bridge.SendToPort(i, "")
|
||||
fn("")
|
||||
}()
|
||||
}
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
func handleUpdateExternalProvider(providerName string, fn func(value string)) {
|
||||
go func() {
|
||||
externalProvider, exist := externalProviders[providerName]
|
||||
if !exist {
|
||||
fn("external provider is not exist")
|
||||
return
|
||||
}
|
||||
err := externalProvider.Update()
|
||||
if err != nil {
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
fn("")
|
||||
}()
|
||||
}
|
||||
|
||||
//export initMessage
|
||||
func initMessage(port C.longlong) {
|
||||
i := int64(port)
|
||||
Port = i
|
||||
func handleSideLoadExternalProvider(providerName string, data []byte, fn func(value string)) {
|
||||
go func() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
externalProvider, exist := externalProviders[providerName]
|
||||
if !exist {
|
||||
fn("external provider is not exist")
|
||||
return
|
||||
}
|
||||
err := sideUpdateExternalProvider(externalProvider, data)
|
||||
if err != nil {
|
||||
fn(err.Error())
|
||||
return
|
||||
}
|
||||
fn("")
|
||||
}()
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
func handleStartLog() {
|
||||
if logSubscriber != nil {
|
||||
log.UnSubscribe(logSubscriber)
|
||||
logSubscriber = nil
|
||||
}
|
||||
logSubscriber = log.Subscribe()
|
||||
go func() {
|
||||
for logData := range logSubscriber {
|
||||
if logData.LogLevel < log.Level() {
|
||||
continue
|
||||
}
|
||||
message := &Message{
|
||||
Type: LogMessage,
|
||||
Data: logData,
|
||||
}
|
||||
sendMessage(*message)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func handleStopLog() {
|
||||
if logSubscriber != nil {
|
||||
log.UnSubscribe(logSubscriber)
|
||||
logSubscriber = nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetCountryCode(ip string, fn func(value string)) {
|
||||
go func() {
|
||||
runLock.Lock()
|
||||
defer runLock.Unlock()
|
||||
codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip))
|
||||
if len(codes) == 0 {
|
||||
fn("")
|
||||
return
|
||||
}
|
||||
fn(codes[0])
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetMemory(fn func(value string)) {
|
||||
go func() {
|
||||
fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10))
|
||||
}()
|
||||
}
|
||||
|
||||
func handleSetState(params string) {
|
||||
_ = json.Unmarshal([]byte(params), state.CurrentState)
|
||||
}
|
||||
|
||||
func init() {
|
||||
provider.HealthcheckHook = func(name string, delay uint16) {
|
||||
adapter.UrlTestHook = func(url string, name string, delay uint16) {
|
||||
delayData := &Delay{
|
||||
Url: url,
|
||||
Name: name,
|
||||
}
|
||||
if delay == 0 {
|
||||
@@ -451,19 +453,19 @@ func init() {
|
||||
} else {
|
||||
delayData.Value = int32(delay)
|
||||
}
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: DelayMessage,
|
||||
Data: delayData,
|
||||
})
|
||||
}
|
||||
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
|
||||
SendMessage(Message{
|
||||
sendMessage(Message{
|
||||
Type: RequestMessage,
|
||||
Data: c,
|
||||
})
|
||||
}
|
||||
executor.DefaultProxyProviderLoadedHook = func(providerName string) {
|
||||
SendMessage(Message{
|
||||
executor.DefaultProviderLoadedHook = func(providerName string) {
|
||||
sendMessage(Message{
|
||||
Type: LoadedMessage,
|
||||
Data: providerName,
|
||||
})
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
//go:build cgo
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
bridge "core/dart-bridge"
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var messagePort int64 = -1
|
||||
|
||||
//export initNativeApiBridge
|
||||
func initNativeApiBridge(api unsafe.Pointer) {
|
||||
bridge.InitDartApi(api)
|
||||
}
|
||||
|
||||
//export attachMessagePort
|
||||
func attachMessagePort(mPort C.longlong) {
|
||||
messagePort = int64(mPort)
|
||||
}
|
||||
|
||||
//export getTraffic
|
||||
func getTraffic() *C.char {
|
||||
return C.CString(handleGetTraffic())
|
||||
}
|
||||
|
||||
//export getTotalTraffic
|
||||
func getTotalTraffic() *C.char {
|
||||
return C.CString(handleGetTotalTraffic())
|
||||
}
|
||||
|
||||
//export freeCString
|
||||
func freeCString(s *C.char) {
|
||||
C.free(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
//export invokeAction
|
||||
func invokeAction(paramsChar *C.char, port C.longlong) {
|
||||
params := C.GoString(paramsChar)
|
||||
i := int64(port)
|
||||
var action = &Action{}
|
||||
err := json.Unmarshal([]byte(params), action)
|
||||
if err != nil {
|
||||
bridge.SendToPort(i, err.Error())
|
||||
return
|
||||
}
|
||||
go handleAction(action, func(data interface{}) {
|
||||
bridge.SendToPort(i, string(action.getResult(data)))
|
||||
})
|
||||
}
|
||||
|
||||
func sendMessage(message Message) {
|
||||
if messagePort == -1 {
|
||||
return
|
||||
}
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bridge.SendToPort(messagePort, string(Action{
|
||||
Method: messageMethod,
|
||||
}.getResult(res)))
|
||||
}
|
||||
|
||||
//export startListener
|
||||
func startListener() {
|
||||
handleStartListener()
|
||||
}
|
||||
|
||||
//export stopListener
|
||||
func stopListener() {
|
||||
handleStopListener()
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
//go:build android && cgo
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
bridge "core/dart-bridge"
|
||||
"core/platform"
|
||||
"core/state"
|
||||
t "core/tun"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/process"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/dns"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type TunHandler struct {
|
||||
listener *sing_tun.Listener
|
||||
callback unsafe.Pointer
|
||||
|
||||
limit *semaphore.Weighted
|
||||
}
|
||||
|
||||
func (t *TunHandler) close() {
|
||||
_ = t.limit.Acquire(context.TODO(), 4)
|
||||
defer t.limit.Release(4)
|
||||
removeTunHook()
|
||||
if t.listener != nil {
|
||||
_ = t.listener.Close()
|
||||
}
|
||||
|
||||
if t.callback != nil {
|
||||
releaseObject(t.callback)
|
||||
}
|
||||
t.callback = nil
|
||||
t.listener = nil
|
||||
}
|
||||
|
||||
func (t *TunHandler) handleProtect(fd int) {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
if t.listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
protect(t.callback, fd)
|
||||
}
|
||||
|
||||
func (t *TunHandler) handleResolveProcess(source, target net.Addr) string {
|
||||
_ = t.limit.Acquire(context.Background(), 1)
|
||||
defer t.limit.Release(1)
|
||||
|
||||
if t.listener == nil {
|
||||
return ""
|
||||
}
|
||||
var protocol int
|
||||
uid := -1
|
||||
switch source.Network() {
|
||||
case "udp", "udp4", "udp6":
|
||||
protocol = syscall.IPPROTO_UDP
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
}
|
||||
if version < 29 {
|
||||
uid = platform.QuerySocketUidFromProcFs(source, target)
|
||||
}
|
||||
return resolveProcess(t.callback, protocol, source.String(), target.String(), uid)
|
||||
}
|
||||
|
||||
var (
|
||||
tunLock sync.Mutex
|
||||
runTime *time.Time
|
||||
errBlocked = errors.New("blocked")
|
||||
tunHandler *TunHandler
|
||||
)
|
||||
|
||||
func handleStopTun() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
runTime = nil
|
||||
if tunHandler != nil {
|
||||
tunHandler.close()
|
||||
}
|
||||
}
|
||||
|
||||
func handleStartTun(fd int, callback unsafe.Pointer) bool {
|
||||
handleStopTun()
|
||||
now := time.Now()
|
||||
runTime = &now
|
||||
if fd != 0 {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
tunHandler = &TunHandler{
|
||||
callback: callback,
|
||||
limit: semaphore.NewWeighted(4),
|
||||
}
|
||||
initTunHook()
|
||||
tunListener, _ := t.Start(fd, currentConfig.General.Tun.Device, currentConfig.General.Tun.Stack)
|
||||
if tunListener != nil {
|
||||
log.Infoln("TUN address: %v", tunListener.Address())
|
||||
} else {
|
||||
removeTunHook()
|
||||
return false
|
||||
}
|
||||
tunHandler.listener = tunListener
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func handleGetRunTime() string {
|
||||
if runTime == nil {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(runTime.UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func initTunHook() {
|
||||
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
|
||||
if platform.ShouldBlockConnection() {
|
||||
return errBlocked
|
||||
}
|
||||
return conn.Control(func(fd uintptr) {
|
||||
tunHandler.handleProtect(int(fd))
|
||||
})
|
||||
}
|
||||
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
|
||||
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
|
||||
if src == nil || dst == nil {
|
||||
return "", process.ErrInvalidNetwork
|
||||
}
|
||||
return tunHandler.handleResolveProcess(src, dst), nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeTunHook() {
|
||||
dialer.DefaultSocketHook = nil
|
||||
process.DefaultPackageNameResolver = nil
|
||||
}
|
||||
|
||||
func handleGetAndroidVpnOptions() string {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
options := state.AndroidVpnOptions{
|
||||
Enable: state.CurrentState.VpnProps.Enable,
|
||||
Port: currentConfig.General.MixedPort,
|
||||
Ipv4Address: state.DefaultIpv4Address,
|
||||
Ipv6Address: state.GetIpv6Address(),
|
||||
AccessControl: state.CurrentState.VpnProps.AccessControl,
|
||||
SystemProxy: state.CurrentState.VpnProps.SystemProxy,
|
||||
AllowBypass: state.CurrentState.VpnProps.AllowBypass,
|
||||
RouteAddress: currentConfig.General.Tun.RouteAddress,
|
||||
BypassDomain: state.CurrentState.BypassDomain,
|
||||
DnsServerAddress: state.GetDnsServerAddress(),
|
||||
}
|
||||
data, err := json.Marshal(options)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func handleUpdateDns(value string) {
|
||||
go func() {
|
||||
log.Infoln("[DNS] updateDns %s", value)
|
||||
dns.UpdateSystemDNS(strings.Split(value, ","))
|
||||
dns.FlushCacheWithDefaultResolver()
|
||||
}()
|
||||
}
|
||||
|
||||
func handleGetCurrentProfileName() string {
|
||||
if state.CurrentState == nil {
|
||||
return ""
|
||||
}
|
||||
return state.CurrentState.CurrentProfileName
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
switch action.Method {
|
||||
case getAndroidVpnOptionsMethod:
|
||||
result(handleGetAndroidVpnOptions())
|
||||
return true
|
||||
case updateDnsMethod:
|
||||
data := action.Data.(string)
|
||||
handleUpdateDns(data)
|
||||
result(true)
|
||||
return true
|
||||
case getRunTimeMethod:
|
||||
result(handleGetRunTime())
|
||||
return true
|
||||
case getCurrentProfileNameMethod:
|
||||
result(handleGetCurrentProfileName())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//export quickStart
|
||||
func quickStart(initParamsChar *C.char, paramsChar *C.char, stateParamsChar *C.char, port C.longlong) {
|
||||
i := int64(port)
|
||||
paramsString := C.GoString(initParamsChar)
|
||||
bytes := []byte(C.GoString(paramsChar))
|
||||
stateParams := C.GoString(stateParamsChar)
|
||||
go func() {
|
||||
res := handleInitClash(paramsString)
|
||||
if res == false {
|
||||
bridge.SendToPort(i, "init error")
|
||||
}
|
||||
handleSetState(stateParams)
|
||||
bridge.SendToPort(i, handleUpdateConfig(bytes))
|
||||
}()
|
||||
}
|
||||
|
||||
//export startTUN
|
||||
func startTUN(fd C.int, callback unsafe.Pointer) bool {
|
||||
return handleStartTun(int(fd), callback)
|
||||
}
|
||||
|
||||
//export getRunTime
|
||||
func getRunTime() *C.char {
|
||||
return C.CString(handleGetRunTime())
|
||||
}
|
||||
|
||||
//export stopTun
|
||||
func stopTun() {
|
||||
handleStopTun()
|
||||
}
|
||||
|
||||
//export getCurrentProfileName
|
||||
func getCurrentProfileName() *C.char {
|
||||
return C.CString(handleGetCurrentProfileName())
|
||||
}
|
||||
|
||||
//export getAndroidVpnOptions
|
||||
func getAndroidVpnOptions() *C.char {
|
||||
return C.CString(handleGetAndroidVpnOptions())
|
||||
}
|
||||
|
||||
//export setState
|
||||
func setState(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
handleSetState(paramsString)
|
||||
}
|
||||
|
||||
//export updateDns
|
||||
func updateDns(s *C.char) {
|
||||
dnsList := C.GoString(s)
|
||||
handleUpdateDns(dnsList)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !android && cgo
|
||||
|
||||
package main
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -1,10 +1,17 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("init clash")
|
||||
args := os.Args
|
||||
if len(args) <= 1 {
|
||||
fmt.Println("Arguments error")
|
||||
os.Exit(1)
|
||||
}
|
||||
startServer(args[1])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//go:build cgo
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
|
||||
func main() {
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build android
|
||||
//go:build android && cgo
|
||||
|
||||
package platform
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var netIndexOfLocal = -1
|
||||
var netIndexOfUid = -1
|
||||
|
||||
var nativeEndian binary.ByteOrder
|
||||
|
||||
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
|
||||
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
network := source.Network()
|
||||
|
||||
if strings.HasSuffix(network, "4") || strings.HasSuffix(network, "6") {
|
||||
network = network[:len(network)-1]
|
||||
}
|
||||
|
||||
path := "/proc/net/" + network
|
||||
|
||||
var sIP net.IP
|
||||
var sPort int
|
||||
|
||||
switch s := source.(type) {
|
||||
case *net.TCPAddr:
|
||||
sIP = s.IP
|
||||
sPort = s.Port
|
||||
case *net.UDPAddr:
|
||||
sIP = s.IP
|
||||
sPort = s.Port
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
|
||||
sIP = sIP.To16()
|
||||
if sIP == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
uid := doQuery(path+"6", sIP, sPort)
|
||||
if uid == -1 {
|
||||
sIP = sIP.To4()
|
||||
if sIP == nil {
|
||||
return -1
|
||||
}
|
||||
uid = doQuery(path, sIP, sPort)
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
|
||||
func doQuery(path string, sIP net.IP, sPort int) int {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
var bytes [2]byte
|
||||
|
||||
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
|
||||
|
||||
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
|
||||
|
||||
for {
|
||||
row, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(row))
|
||||
|
||||
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(local, fields[netIndexOfLocal]) {
|
||||
uid, err := strconv.Atoi(fields[netIndexOfUid])
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return uid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nativeEndianIP(ip net.IP) []byte {
|
||||
result := make([]byte, len(ip))
|
||||
|
||||
for i := 0; i < len(ip); i += 4 {
|
||||
value := binary.BigEndian.Uint32(ip[i:])
|
||||
|
||||
nativeEndian.PutUint32(result[i:], value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
file, err := os.Open("/proc/net/tcp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func(file *os.File) {
|
||||
_ = file.Close()
|
||||
}(file)
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
header, _, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
columns := strings.Fields(string(header))
|
||||
|
||||
var txQueue, rxQueue, tr, tmWhen bool
|
||||
|
||||
for idx, col := range columns {
|
||||
offset := 0
|
||||
|
||||
if txQueue && rxQueue {
|
||||
offset--
|
||||
}
|
||||
|
||||
if tr && tmWhen {
|
||||
offset--
|
||||
}
|
||||
|
||||
switch col {
|
||||
case "tx_queue":
|
||||
txQueue = true
|
||||
case "rx_queue":
|
||||
rxQueue = true
|
||||
case "tr":
|
||||
tr = true
|
||||
case "tm->when":
|
||||
tmWhen = true
|
||||
case "local_address":
|
||||
netIndexOfLocal = idx + offset
|
||||
case "uid":
|
||||
netIndexOfUid = idx + offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
var x uint32 = 0x01020304
|
||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||
nativeEndian = binary.BigEndian
|
||||
} else {
|
||||
nativeEndian = binary.LittleEndian
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//go:build !cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var conn net.Conn
|
||||
|
||||
func sendMessage(message Message) {
|
||||
res, err := message.Json()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
send(Action{
|
||||
Method: messageMethod,
|
||||
}.getResult(res))
|
||||
}
|
||||
|
||||
func send(data []byte) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
_, _ = conn.Write(append(data, []byte("\n")...))
|
||||
}
|
||||
|
||||
func startServer(arg string) {
|
||||
|
||||
_, err := strconv.Atoi(arg)
|
||||
|
||||
if err != nil {
|
||||
conn, err = net.Dial("unix", arg)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", arg))
|
||||
}
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
defer func(conn net.Conn) {
|
||||
_ = conn.Close()
|
||||
}(conn)
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
for {
|
||||
data, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var action = &Action{}
|
||||
|
||||
err = json.Unmarshal([]byte(data), action)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go handleAction(action, func(data interface{}) {
|
||||
send(action.getResult(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nextHandle(action *Action, result func(data interface{})) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package state
|
||||
|
||||
import "net/netip"
|
||||
|
||||
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 []netip.Prefix `json:"routeAddress"`
|
||||
Ipv4Address string `json:"ipv4Address"`
|
||||
Ipv6Address string `json:"ipv6Address"`
|
||||
DnsServerAddress string `json:"dnsServerAddress"`
|
||||
}
|
||||
|
||||
type AccessControl struct {
|
||||
Enable bool `json:"enable"`
|
||||
Mode string `json:"mode"`
|
||||
AcceptList []string `json:"acceptList"`
|
||||
RejectList []string `json:"rejectList"`
|
||||
IsFilterSystemApp bool `json:"isFilterSystemApp"`
|
||||
}
|
||||
|
||||
type AndroidVpnRawOptions struct {
|
||||
Enable bool `json:"enable"`
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
Ipv6 bool `json:"ipv6"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
VpnProps AndroidVpnRawOptions `json:"vpn-props"`
|
||||
CurrentProfileName string `json:"current-profile-name"`
|
||||
OnlyStatisticsProxy bool `json:"only-statistics-proxy"`
|
||||
BypassDomain []string `json:"bypass-domain"`
|
||||
}
|
||||
|
||||
var CurrentState = &State{
|
||||
OnlyStatisticsProxy: false,
|
||||
CurrentProfileName: "",
|
||||
}
|
||||
|
||||
func GetIpv6Address() string {
|
||||
if CurrentState.VpnProps.Ipv6 {
|
||||
return DefaultIpv6Address
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func GetDnsServerAddress() string {
|
||||
return DefaultDnsAddress
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//go:build android
|
||||
|
||||
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 {
|
||||
AccessControl *AccessControl `json:"accessControl"`
|
||||
AllowBypass bool `json:"allowBypass"`
|
||||
SystemProxy bool `json:"systemProxy"`
|
||||
}
|
||||
|
||||
var androidProps AndroidProps
|
||||
|
||||
//export getAndroidProps
|
||||
func getAndroidProps() *C.char {
|
||||
data, err := json.Marshal(androidProps)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return C.CString("")
|
||||
}
|
||||
return C.CString(string(data))
|
||||
}
|
||||
|
||||
//export setAndroidProps
|
||||
func setAndroidProps(s *C.char) {
|
||||
paramsString := C.GoString(s)
|
||||
err := json.Unmarshal([]byte(paramsString), &androidProps)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
//go:build android
|
||||
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"core/platform"
|
||||
t "core/tun"
|
||||
"errors"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
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
|
||||
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() {
|
||||
go func() {
|
||||
tunLock.Lock()
|
||||
defer tunLock.Unlock()
|
||||
|
||||
runTime = nil
|
||||
|
||||
if tun != nil {
|
||||
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 init() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
//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()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//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: "",
|
||||
}
|
||||
}
|
||||
+48
-165
@@ -1,186 +1,69 @@
|
||||
//go:build android
|
||||
//go:build android && cgo
|
||||
|
||||
package tun
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"github.com/Kr328/tun2socket"
|
||||
"github.com/Kr328/tun2socket/nat"
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"core/state"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/listener/sing_tun"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
"github.com/metacubex/mihomo/tunnel"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Tun struct {
|
||||
Closer io.Closer
|
||||
|
||||
Closed bool
|
||||
Limit *semaphore.Weighted
|
||||
type Props struct {
|
||||
Fd int `json:"fd"`
|
||||
Gateway string `json:"gateway"`
|
||||
Gateway6 string `json:"gateway6"`
|
||||
Portal string `json:"portal"`
|
||||
Portal6 string `json:"portal6"`
|
||||
Dns string `json:"dns"`
|
||||
Dns6 string `json:"dns6"`
|
||||
}
|
||||
|
||||
func (t *Tun) Close() {
|
||||
_ = t.Limit.Acquire(context.TODO(), 4)
|
||||
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)
|
||||
func Start(fd int, device string, stack constant.TUNStack) (*sing_tun.Listener, error) {
|
||||
var prefix4 []netip.Prefix
|
||||
tempPrefix4, err := netip.ParsePrefix(state.DefaultIpv4Address)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
network.IP = ip
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
stack, err := tun2socket.StartTun2Socket(device, network, net.ParseIP(portal))
|
||||
prefix4 = append(prefix4, tempPrefix4)
|
||||
var prefix6 []netip.Prefix
|
||||
if state.CurrentState.VpnProps.Ipv6 {
|
||||
tempPrefix6, err := netip.ParsePrefix(state.DefaultIpv6Address)
|
||||
if err != nil {
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
prefix6 = append(prefix6, tempPrefix6)
|
||||
}
|
||||
|
||||
var dnsHijack []string
|
||||
dnsHijack = append(dnsHijack, net.JoinHostPort(state.GetDnsServerAddress(), "53"))
|
||||
|
||||
options := LC.Tun{
|
||||
Enable: true,
|
||||
Device: device,
|
||||
Stack: stack,
|
||||
DNSHijack: dnsHijack,
|
||||
AutoRoute: false,
|
||||
AutoDetectInterface: false,
|
||||
Inet4Address: prefix4,
|
||||
Inet6Address: prefix6,
|
||||
MTU: 9000,
|
||||
FileDescriptor: fd,
|
||||
}
|
||||
|
||||
listener, err := sing_tun.New(options, tunnel.Tunnel)
|
||||
|
||||
if err != nil {
|
||||
_ = device.Close()
|
||||
log.Errorln("startTUN error:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dnsAddr := net.ParseIP(dns)
|
||||
|
||||
tcp := func() {
|
||||
defer func(tcp *nat.TCP) {
|
||||
_ = tcp.Close()
|
||||
}(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))
|
||||
}
|
||||
}
|
||||
|
||||
udp := func() {
|
||||
defer func(udp *nat.UDP) {
|
||||
_ = udp.Close()
|
||||
}(stack.UDP())
|
||||
defer log.Debugln("UDP: closed")
|
||||
|
||||
for {
|
||||
buf := pool.Get(pool.UDPBufferSize)
|
||||
|
||||
n, lRAddr, rRAddr, err := stack.UDP().ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
raw := buf[:n]
|
||||
lAddr := lRAddr.(*net.UDPAddr)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
go tcp()
|
||||
go udp()
|
||||
|
||||
return stack, nil
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//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
|
||||
}
|
||||
+106
-80
@@ -1,71 +1,49 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_clash/l10n/l10n.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/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/manager/manager.dart';
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'models/models.dart';
|
||||
import 'pages/pages.dart';
|
||||
|
||||
runAppWithPreferences(
|
||||
Widget child, {
|
||||
required AppState appState,
|
||||
required Config config,
|
||||
required ClashConfig clashConfig,
|
||||
}) {
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ClashConfig>(
|
||||
create: (_) => clashConfig,
|
||||
),
|
||||
ChangeNotifierProvider<Config>(
|
||||
create: (_) => config,
|
||||
),
|
||||
ChangeNotifierProxyProvider2<Config, ClashConfig, AppState>(
|
||||
create: (_) => appState,
|
||||
update: (_, config, clashConfig, appState) {
|
||||
appState?.mode = clashConfig.mode;
|
||||
appState?.isCompatible = config.isCompatible;
|
||||
appState?.selectedMap = config.currentSelectedMap;
|
||||
return appState!;
|
||||
},
|
||||
)
|
||||
],
|
||||
child: child,
|
||||
));
|
||||
}
|
||||
|
||||
class Application extends StatefulWidget {
|
||||
class Application extends ConsumerStatefulWidget {
|
||||
const Application({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<Application> createState() => ApplicationState();
|
||||
ConsumerState<Application> createState() => ApplicationState();
|
||||
}
|
||||
|
||||
class ApplicationState extends State<Application> {
|
||||
late SystemColorSchemes systemColorSchemes;
|
||||
class ApplicationState extends ConsumerState<Application> {
|
||||
late ColorSchemes systemColorSchemes;
|
||||
Timer? _autoUpdateGroupTaskTimer;
|
||||
Timer? _autoUpdateProfilesTaskTimer;
|
||||
|
||||
final _pageTransitionsTheme = const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.android: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: CommonPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CommonPageTransitionsBuilder(),
|
||||
},
|
||||
);
|
||||
|
||||
ColorScheme _getAppColorScheme({
|
||||
required Brightness brightness,
|
||||
int? primaryColor,
|
||||
required SystemColorSchemes systemColorSchemes,
|
||||
required ColorSchemes systemColorSchemes,
|
||||
}) {
|
||||
if (primaryColor != null) {
|
||||
return ColorScheme.fromSeed(
|
||||
@@ -73,36 +51,91 @@ class ApplicationState extends State<Application> {
|
||||
brightness: brightness,
|
||||
);
|
||||
} else {
|
||||
return systemColorSchemes.getSystemColorSchemeForBrightness(brightness);
|
||||
return systemColorSchemes.getColorSchemeForBrightness(brightness);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
globalState.appController = AppController(context);
|
||||
_autoUpdateGroupTask();
|
||||
_autoUpdateProfilesTask();
|
||||
globalState.appController = AppController(context, ref);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final currentContext = globalState.navigatorKey.currentContext;
|
||||
if (currentContext != null) {
|
||||
globalState.appController = AppController(currentContext);
|
||||
globalState.appController = AppController(currentContext, ref);
|
||||
}
|
||||
await globalState.appController.init();
|
||||
globalState.appController.initLink();
|
||||
_updateGroups();
|
||||
app?.initShortcuts();
|
||||
});
|
||||
}
|
||||
|
||||
_buildApp(Widget app) {
|
||||
_autoUpdateGroupTask() {
|
||||
_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();
|
||||
});
|
||||
}
|
||||
|
||||
_buildPlatformState(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowContainer(
|
||||
child: TrayContainer(
|
||||
child: app,
|
||||
return WindowManager(
|
||||
child: TrayManager(
|
||||
child: HotKeyManager(
|
||||
child: ProxyManager(
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AndroidContainer(
|
||||
child: TileContainer(
|
||||
child: app,
|
||||
return AndroidManager(
|
||||
child: TileManager(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildState(Widget child) {
|
||||
return AppStateManager(
|
||||
child: ClashManager(
|
||||
child: ConnectivityManager(
|
||||
onConnectivityChanged: () {
|
||||
globalState.appController.updateLocalIp();
|
||||
globalState.appController.addCheckIpNumDebounce();
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildPlatformApp(Widget child) {
|
||||
if (system.isDesktop) {
|
||||
return WindowHeaderContainer(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return VpnManager(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_buildApp(Widget child) {
|
||||
return MessageManager(
|
||||
child: ThemeManager(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -111,7 +144,7 @@ class ApplicationState extends State<Application> {
|
||||
ColorScheme? lightDynamic,
|
||||
ColorScheme? darkDynamic,
|
||||
) {
|
||||
systemColorSchemes = SystemColorSchemes(
|
||||
systemColorSchemes = ColorSchemes(
|
||||
lightColorScheme: lightDynamic,
|
||||
darkColorScheme: darkDynamic,
|
||||
);
|
||||
@@ -120,30 +153,15 @@ class ApplicationState extends State<Application> {
|
||||
});
|
||||
}
|
||||
|
||||
_updateGroups() {
|
||||
if (globalState.groupsUpdateTimer != null) {
|
||||
globalState.groupsUpdateTimer?.cancel();
|
||||
globalState.groupsUpdateTimer = null;
|
||||
}
|
||||
globalState.groupsUpdateTimer ??= Timer.periodic(
|
||||
httpTimeoutDuration,
|
||||
(timer) async {
|
||||
await globalState.appController.updateGroups();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return AppStateContainer(
|
||||
child: ClashMessageContainer(
|
||||
child: Selector2<AppState, Config, ApplicationSelectorState>(
|
||||
selector: (_, appState, config) => ApplicationSelectorState(
|
||||
locale: config.locale,
|
||||
themeMode: config.themeMode,
|
||||
primaryColor: config.primaryColor,
|
||||
),
|
||||
builder: (_, state, child) {
|
||||
return _buildPlatformState(
|
||||
_buildState(
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final locale =
|
||||
ref.watch(appSettingProvider.select((state) => state.locale));
|
||||
final themeProps = ref.watch(themeSettingProvider);
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightDynamic, darkDynamic) {
|
||||
_updateSystemColorSchemes(lightDynamic, darkDynamic);
|
||||
@@ -157,20 +175,24 @@ class ApplicationState extends State<Application> {
|
||||
GlobalWidgetsLocalizations.delegate
|
||||
],
|
||||
builder: (_, child) {
|
||||
return _buildApp(child!);
|
||||
return AppEnvManager(
|
||||
child: _buildPlatformApp(
|
||||
_buildApp(child!),
|
||||
),
|
||||
);
|
||||
},
|
||||
scrollBehavior: BaseScrollBehavior(),
|
||||
title: appName,
|
||||
locale: other.getLocaleForString(state.locale),
|
||||
locale: other.getLocaleForString(locale),
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
themeMode: state.themeMode,
|
||||
themeMode: themeProps.themeMode,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: _pageTransitionsTheme,
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.light,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
primaryColor: themeProps.primaryColor,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
@@ -179,8 +201,8 @@ class ApplicationState extends State<Application> {
|
||||
colorScheme: _getAppColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
systemColorSchemes: systemColorSchemes,
|
||||
primaryColor: state.primaryColor,
|
||||
),
|
||||
primaryColor: themeProps.primaryColor,
|
||||
).toPureBlack(themeProps.pureBlack),
|
||||
),
|
||||
home: child,
|
||||
);
|
||||
@@ -196,7 +218,11 @@ class ApplicationState extends State<Application> {
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
linkManager.destroy();
|
||||
_autoUpdateGroupTaskTimer?.cancel();
|
||||
_autoUpdateProfilesTaskTimer?.cancel();
|
||||
await clashCore.destroy();
|
||||
await globalState.appController.savePreferences();
|
||||
await globalState.appController.handleExit();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'core.dart';
|
||||
export 'lib.dart';
|
||||
export 'message.dart';
|
||||
export 'service.dart';
|
||||
export 'message.dart';
|
||||
+204
-264
@@ -1,41 +1,27 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/clash/clash.dart';
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'generated/clash_ffi.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class ClashCore {
|
||||
static ClashCore? _instance;
|
||||
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";
|
||||
}
|
||||
late ClashHandlerInterface clashInterface;
|
||||
|
||||
ClashCore._internal() {
|
||||
lib = _getClashLib();
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
if (Platform.isAndroid) {
|
||||
clashInterface = clashLib!;
|
||||
} else {
|
||||
clashInterface = clashService!;
|
||||
}
|
||||
}
|
||||
|
||||
factory ClashCore() {
|
||||
@@ -43,83 +29,72 @@ class ClashCore {
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
bool init(String homeDir) {
|
||||
final homeDirChar = homeDir.toNativeUtf8().cast<Char>();
|
||||
final isInit = clashFFI.initClash(homeDirChar) == 1;
|
||||
malloc.free(homeDirChar);
|
||||
return isInit;
|
||||
Future<bool> preload() {
|
||||
return clashInterface.preload();
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
clashFFI.shutdownClash();
|
||||
lib.close();
|
||||
}
|
||||
|
||||
bool get isInit => clashFFI.getIsInit() == 1;
|
||||
|
||||
Future<String> validateConfig(String data) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
static Future<void> initGeo() async {
|
||||
final homePath = await appPath.homeDirPath;
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
if (!isExists) {
|
||||
await homeDir.create(recursive: true);
|
||||
}
|
||||
const geoFileNameList = [
|
||||
mmdbFileName,
|
||||
geoIpFileName,
|
||||
geoSiteFileName,
|
||||
asnFileName,
|
||||
];
|
||||
try {
|
||||
for (final geoFileName in geoFileNameList) {
|
||||
final geoFile = File(
|
||||
join(homePath, geoFileName),
|
||||
);
|
||||
final isExists = await geoFile.exists();
|
||||
if (isExists) {
|
||||
continue;
|
||||
}
|
||||
final data = await rootBundle.load('assets/data/$geoFileName');
|
||||
List<int> bytes = data.buffer.asUint8List();
|
||||
await geoFile.writeAsBytes(bytes, flush: true);
|
||||
}
|
||||
});
|
||||
final dataChar = data.toNativeUtf8().cast<Char>();
|
||||
clashFFI.validateConfig(
|
||||
dataChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(dataChar);
|
||||
return completer.future;
|
||||
} catch (e) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateConfig(
|
||||
paramsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(paramsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
initMessage() {
|
||||
clashFFI.initMessage(
|
||||
receiver.sendPort.nativePort,
|
||||
Future<bool> init() async {
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.homeDirPath;
|
||||
return await clashInterface.init(
|
||||
InitParams(
|
||||
homeDir: homeDirPath,
|
||||
version: globalState.appState.version,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setProfileName(String profileName) {
|
||||
final profileNameChar = profileName.toNativeUtf8().cast<Char>();
|
||||
clashFFI.setCurrentProfileName(
|
||||
profileNameChar,
|
||||
);
|
||||
malloc.free(profileNameChar);
|
||||
Future<bool> setState(CoreState state) async {
|
||||
return await clashInterface.setState(state);
|
||||
}
|
||||
|
||||
getProfileName() {
|
||||
final currentProfileNameRaw = clashFFI.getCurrentProfileName();
|
||||
final currentProfileName =
|
||||
currentProfileNameRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(currentProfileNameRaw);
|
||||
return currentProfileName;
|
||||
shutdown() async {
|
||||
await clashInterface.shutdown();
|
||||
}
|
||||
|
||||
Future<List<Group>> getProxiesGroups() {
|
||||
final proxiesRaw = clashFFI.getProxies();
|
||||
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(proxiesRaw);
|
||||
FutureOr<bool> get isInit => clashInterface.isInit;
|
||||
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return clashInterface.validateConfig(data);
|
||||
}
|
||||
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await clashInterface.updateConfig(updateConfigParams);
|
||||
}
|
||||
|
||||
Future<List<Group>> getProxiesGroups() async {
|
||||
final proxiesRawString = await clashInterface.getProxies();
|
||||
return Isolate.run<List<Group>>(() {
|
||||
if (proxiesRawString.isEmpty) return [];
|
||||
final proxies = (json.decode(proxiesRawString) ?? {}) as Map;
|
||||
@@ -128,8 +103,7 @@ class ClashCore {
|
||||
UsedProxy.GLOBAL.name,
|
||||
...(proxies[UsedProxy.GLOBAL.name]["all"] as List).where((e) {
|
||||
final proxy = proxies[e] ?? {};
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']) &&
|
||||
proxy['hidden'] != true;
|
||||
return GroupTypeExtension.valueList.contains(proxy['type']);
|
||||
})
|
||||
];
|
||||
final groupsRaw = groupNames.map((groupName) {
|
||||
@@ -142,191 +116,157 @@ class ClashCore {
|
||||
.toList();
|
||||
return group;
|
||||
}).toList();
|
||||
return groupsRaw.map((e) => Group.fromJson(e)).toList();
|
||||
return groupsRaw
|
||||
.map(
|
||||
(e) => Group.fromJson(e),
|
||||
)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<ExternalProvider>> getExternalProviders() {
|
||||
final externalProvidersRaw = clashFFI.getExternalProviders();
|
||||
final externalProvidersRawString =
|
||||
externalProvidersRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(externalProvidersRaw);
|
||||
return Isolate.run<List<ExternalProvider>>(() {
|
||||
final externalProviders =
|
||||
(json.decode(externalProvidersRawString) as List<dynamic>)
|
||||
.map(
|
||||
(item) => ExternalProvider.fromJson(item),
|
||||
)
|
||||
.toList();
|
||||
return externalProviders;
|
||||
});
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) async {
|
||||
return await clashInterface.changeProxy(changeProxyParams);
|
||||
}
|
||||
|
||||
Future<String> updateExternalProvider({
|
||||
required String providerName,
|
||||
required String providerType,
|
||||
}) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final providerNameChar = providerName.toNativeUtf8().cast<Char>();
|
||||
final providerTypeChar = providerType.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateExternalProvider(
|
||||
providerNameChar,
|
||||
providerTypeChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(providerNameChar);
|
||||
malloc.free(providerTypeChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
final params = json.encode(changeProxyParams);
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
clashFFI.changeProxy(paramsChar);
|
||||
malloc.free(paramsChar);
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
};
|
||||
final completer = Completer<Delay>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(Delay.fromJson(json.decode(message)));
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final delayParamsChar =
|
||||
json.encode(delayParams).toNativeUtf8().cast<Char>();
|
||||
clashFFI.asyncTestDelay(
|
||||
delayParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(delayParamsChar);
|
||||
Future.delayed(httpTimeoutDuration + moreDuration, () {
|
||||
receiver.close();
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(
|
||||
Delay(name: proxyName, value: -1),
|
||||
);
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
clearEffect(String path) {
|
||||
final pathChar = path.toNativeUtf8().cast<Char>();
|
||||
clashFFI.clearEffect(pathChar);
|
||||
malloc.free(pathChar);
|
||||
}
|
||||
|
||||
VersionInfo getVersionInfo() {
|
||||
final versionInfoRaw = clashFFI.getVersionInfo();
|
||||
final versionInfo = json.decode(versionInfoRaw.cast<Utf8>().toDartString());
|
||||
clashFFI.freeCString(versionInfoRaw);
|
||||
return VersionInfo.fromJson(versionInfo);
|
||||
}
|
||||
|
||||
setProps(Props props) {
|
||||
final propsChar = json.encode(props).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setAndroidProps(propsChar);
|
||||
malloc.free(propsChar);
|
||||
}
|
||||
|
||||
Props getProps() {
|
||||
final androidPropsRaw = clashFFI.getAndroidProps();
|
||||
final androidProps = json.decode(
|
||||
androidPropsRaw.cast<Utf8>().toDartString(),
|
||||
);
|
||||
clashFFI.freeCString(androidPropsRaw);
|
||||
return Props.fromJson(androidProps);
|
||||
}
|
||||
|
||||
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() {
|
||||
clashFFI.stopLog();
|
||||
}
|
||||
|
||||
startTun(int fd, int port) {
|
||||
if (!Platform.isAndroid) return;
|
||||
clashFFI.startTUN(fd, port);
|
||||
}
|
||||
|
||||
requestGc() {
|
||||
clashFFI.forceGc();
|
||||
}
|
||||
|
||||
void stopTun() {
|
||||
clashFFI.stopTun();
|
||||
}
|
||||
|
||||
void setProcessMap(ProcessMapItem processMapItem) {
|
||||
final processMapItemChar =
|
||||
json.encode(processMapItem).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setProcessMap(processMapItemChar);
|
||||
malloc.free(processMapItemChar);
|
||||
}
|
||||
|
||||
void setFdMap(int fd) {
|
||||
clashFFI.setFdMap(fd);
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
final runTimeRaw = clashFFI.getRunTime();
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
List<Connection> getConnections() {
|
||||
final connectionsDataRaw = clashFFI.getConnections();
|
||||
final connectionsData =
|
||||
json.decode(connectionsDataRaw.cast<Utf8>().toDartString()) as Map;
|
||||
clashFFI.freeCString(connectionsDataRaw);
|
||||
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) {
|
||||
final idChar = id.toNativeUtf8().cast<Char>();
|
||||
clashFFI.closeConnection(idChar);
|
||||
malloc.free(idChar);
|
||||
clashInterface.closeConnection(id);
|
||||
}
|
||||
|
||||
closeConnections() {
|
||||
clashFFI.closeConnections();
|
||||
clashInterface.closeConnections();
|
||||
}
|
||||
|
||||
Future<List<ExternalProvider>> getExternalProviders() async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProviders();
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
return Isolate.run<List<ExternalProvider>>(
|
||||
() {
|
||||
final externalProviders =
|
||||
(json.decode(externalProvidersRawString) as List<dynamic>)
|
||||
.map(
|
||||
(item) => ExternalProvider.fromJson(item),
|
||||
)
|
||||
.toList();
|
||||
return externalProviders;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ExternalProvider?> getExternalProvider(
|
||||
String externalProviderName) async {
|
||||
final externalProvidersRawString =
|
||||
await clashInterface.getExternalProvider(externalProviderName);
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (externalProvidersRawString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return ExternalProvider.fromJson(json.decode(externalProvidersRawString));
|
||||
}
|
||||
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return clashInterface.updateGeoData(params);
|
||||
}
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
return clashInterface.sideLoadExternalProvider(
|
||||
providerName: providerName, data: data);
|
||||
}
|
||||
|
||||
Future<String> updateExternalProvider({
|
||||
required String providerName,
|
||||
}) async {
|
||||
return clashInterface.updateExternalProvider(providerName);
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
await clashInterface.startListener();
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
await clashInterface.stopListener();
|
||||
}
|
||||
|
||||
Future<Delay> getDelay(String url, String proxyName) async {
|
||||
final data = await clashInterface.asyncTestDelay(url, proxyName);
|
||||
return Delay.fromJson(json.decode(data));
|
||||
}
|
||||
|
||||
Future<Traffic> getTraffic() async {
|
||||
final trafficString = await clashInterface.getTraffic();
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Future<IpInfo?> getCountryCode(String ip) async {
|
||||
final countryCode = await clashInterface.getCountryCode(ip);
|
||||
if (countryCode.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return IpInfo(
|
||||
ip: ip,
|
||||
countryCode: countryCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Traffic> getTotalTraffic() async {
|
||||
final totalTrafficString = await clashInterface.getTotalTraffic();
|
||||
if (totalTrafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(totalTrafficString));
|
||||
}
|
||||
|
||||
Future<int> getMemory() async {
|
||||
final value = await clashInterface.getMemory();
|
||||
if (value.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return int.parse(value);
|
||||
}
|
||||
|
||||
Future<ClashConfigSnippet?> getProfile(String id) async {
|
||||
final res = await clashInterface.getProfile(id);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Isolate.run(() => ClashConfigSnippet.fromJson(json.decode(res)));
|
||||
}
|
||||
|
||||
resetTraffic() {
|
||||
clashInterface.resetTraffic();
|
||||
}
|
||||
|
||||
startLog() {
|
||||
clashInterface.startLog();
|
||||
}
|
||||
|
||||
stopLog() {
|
||||
clashInterface.stopLog();
|
||||
}
|
||||
|
||||
requestGc() {
|
||||
clashInterface.forceGc();
|
||||
}
|
||||
|
||||
destroy() async {
|
||||
await clashInterface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4460
-5209
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:fl_clash/clash/message.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
|
||||
mixin ClashInterface {
|
||||
Future<bool> init(InitParams params);
|
||||
|
||||
Future<bool> preload();
|
||||
|
||||
Future<bool> shutdown();
|
||||
|
||||
Future<bool> get isInit;
|
||||
|
||||
Future<bool> forceGc();
|
||||
|
||||
FutureOr<String> validateConfig(String data);
|
||||
|
||||
Future<String> asyncTestDelay(String url, String proxyName);
|
||||
|
||||
FutureOr<String> updateConfig(UpdateConfigParams updateConfigParams);
|
||||
|
||||
FutureOr<String> getProxies();
|
||||
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams);
|
||||
|
||||
Future<bool> startListener();
|
||||
|
||||
Future<bool> stopListener();
|
||||
|
||||
FutureOr<String> getExternalProviders();
|
||||
|
||||
FutureOr<String>? getExternalProvider(String externalProviderName);
|
||||
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params);
|
||||
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
});
|
||||
|
||||
Future<String> updateExternalProvider(String providerName);
|
||||
|
||||
FutureOr<String> getTraffic();
|
||||
|
||||
FutureOr<String> getTotalTraffic();
|
||||
|
||||
FutureOr<String> getCountryCode(String ip);
|
||||
|
||||
FutureOr<String> getMemory();
|
||||
|
||||
resetTraffic();
|
||||
|
||||
startLog();
|
||||
|
||||
stopLog();
|
||||
|
||||
FutureOr<String> getConnections();
|
||||
|
||||
FutureOr<bool> closeConnection(String id);
|
||||
|
||||
FutureOr<bool> closeConnections();
|
||||
|
||||
FutureOr<String> getProfile(String id);
|
||||
|
||||
Future<bool> setState(CoreState state);
|
||||
}
|
||||
|
||||
mixin AndroidClashInterface {
|
||||
Future<bool> setFdMap(int fd);
|
||||
|
||||
Future<bool> setProcessMap(ProcessMapItem item);
|
||||
|
||||
// Future<bool> stopTun();
|
||||
|
||||
Future<bool> updateDns(String value);
|
||||
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions();
|
||||
|
||||
Future<String> getCurrentProfileName();
|
||||
|
||||
Future<DateTime?> getRunTime();
|
||||
}
|
||||
|
||||
abstract class ClashHandlerInterface with ClashInterface {
|
||||
Map<String, Completer> callbackCompleterMap = {};
|
||||
|
||||
Future<bool> nextHandleResult(ActionResult result, Completer? completer) =>
|
||||
Future.value(false);
|
||||
|
||||
handleResult(ActionResult result) async {
|
||||
final completer = callbackCompleterMap[result.id];
|
||||
try {
|
||||
switch (result.method) {
|
||||
case ActionMethod.initClash:
|
||||
case ActionMethod.shutdown:
|
||||
case ActionMethod.getIsInit:
|
||||
case ActionMethod.startListener:
|
||||
case ActionMethod.resetTraffic:
|
||||
case ActionMethod.closeConnections:
|
||||
case ActionMethod.closeConnection:
|
||||
case ActionMethod.stopListener:
|
||||
case ActionMethod.setState:
|
||||
completer?.complete(result.data as bool);
|
||||
return;
|
||||
case ActionMethod.changeProxy:
|
||||
case ActionMethod.getProxies:
|
||||
case ActionMethod.getTraffic:
|
||||
case ActionMethod.getTotalTraffic:
|
||||
case ActionMethod.asyncTestDelay:
|
||||
case ActionMethod.getConnections:
|
||||
case ActionMethod.getExternalProviders:
|
||||
case ActionMethod.getExternalProvider:
|
||||
case ActionMethod.validateConfig:
|
||||
case ActionMethod.updateConfig:
|
||||
case ActionMethod.updateGeoData:
|
||||
case ActionMethod.updateExternalProvider:
|
||||
case ActionMethod.sideLoadExternalProvider:
|
||||
case ActionMethod.getCountryCode:
|
||||
case ActionMethod.getMemory:
|
||||
completer?.complete(result.data as String);
|
||||
return;
|
||||
case ActionMethod.message:
|
||||
clashMessage.controller.add(result.data as String);
|
||||
completer?.complete(true);
|
||||
return;
|
||||
default:
|
||||
final isHandled = await nextHandleResult(result, completer);
|
||||
if (isHandled) {
|
||||
return;
|
||||
}
|
||||
completer?.complete(result.data);
|
||||
}
|
||||
} catch (_) {
|
||||
commonPrint.log(result.id);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(String message);
|
||||
|
||||
reStart();
|
||||
|
||||
FutureOr<bool> destroy();
|
||||
|
||||
Future<T> invoke<T>({
|
||||
required ActionMethod method,
|
||||
dynamic data,
|
||||
Duration? timeout,
|
||||
FutureOr<T> Function()? onTimeout,
|
||||
}) async {
|
||||
final id = "${method.name}#${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(InitParams params) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.initClash,
|
||||
data: json.encode(params),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setState(CoreState state) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setState,
|
||||
data: json.encode(state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
return await invoke<bool>(
|
||||
method: ActionMethod.shutdown,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> get isInit {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.getIsInit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> forceGc() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.forceGc,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> validateConfig(String data) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.validateConfig,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateConfig(UpdateConfigParams updateConfigParams) async {
|
||||
return await invoke<String>(
|
||||
method: ActionMethod.updateConfig,
|
||||
data: json.encode(updateConfigParams),
|
||||
timeout: Duration(minutes: 2),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProxies() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProxies,
|
||||
timeout: Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> changeProxy(ChangeProxyParams changeProxyParams) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.changeProxy,
|
||||
data: json.encode(changeProxyParams),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProviders() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProviders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getExternalProvider(String externalProviderName) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getExternalProvider,
|
||||
data: externalProviderName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateGeoData(UpdateGeoDataParams params) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateGeoData,
|
||||
data: json.encode(params),
|
||||
timeout: Duration(minutes: 1));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> sideLoadExternalProvider({
|
||||
required String providerName,
|
||||
required String data,
|
||||
}) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.sideLoadExternalProvider,
|
||||
data: json.encode({
|
||||
"providerName": providerName,
|
||||
"data": data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> updateExternalProvider(String providerName) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.updateExternalProvider,
|
||||
data: providerName,
|
||||
timeout: Duration(minutes: 1),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getConnections() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnections() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnections,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> closeConnection(String id) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.closeConnection,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getProfile(String id) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getProfile,
|
||||
data: id,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTotalTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTotalTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getTraffic() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getTraffic,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
resetTraffic() {
|
||||
invoke(method: ActionMethod.resetTraffic);
|
||||
}
|
||||
|
||||
@override
|
||||
startLog() {
|
||||
invoke(method: ActionMethod.startLog);
|
||||
}
|
||||
|
||||
@override
|
||||
stopLog() {
|
||||
invoke<bool>(
|
||||
method: ActionMethod.stopLog,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> startListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.startListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
stopListener() {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.stopListener,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> asyncTestDelay(String url, String proxyName) {
|
||||
final delayParams = {
|
||||
"proxy-name": proxyName,
|
||||
"timeout": httpTimeoutDuration.inMilliseconds,
|
||||
"test-url": url,
|
||||
};
|
||||
return invoke<String>(
|
||||
method: ActionMethod.asyncTestDelay,
|
||||
data: json.encode(delayParams),
|
||||
timeout: Duration(
|
||||
milliseconds: 6000,
|
||||
),
|
||||
onTimeout: () {
|
||||
return json.encode(
|
||||
Delay(
|
||||
name: proxyName,
|
||||
value: -1,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getCountryCode(String ip) {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCountryCode,
|
||||
data: ip,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<String> getMemory() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getMemory,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fl_clash/common/constant.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/plugins/service.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'generated/clash_ffi.dart';
|
||||
import 'interface.dart';
|
||||
|
||||
class ClashLib extends ClashHandlerInterface with AndroidClashInterface {
|
||||
static ClashLib? _instance;
|
||||
Completer<bool> _canSendCompleter = Completer();
|
||||
SendPort? sendPort;
|
||||
final receiverPort = ReceivePort();
|
||||
|
||||
ClashLib._internal() {
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
preload() {
|
||||
return _canSendCompleter.future;
|
||||
}
|
||||
|
||||
_initService() async {
|
||||
await service?.destroy();
|
||||
_registerMainPort(receiverPort.sendPort);
|
||||
receiverPort.listen((message) {
|
||||
if (message is SendPort) {
|
||||
if (_canSendCompleter.isCompleted) {
|
||||
sendPort = null;
|
||||
_canSendCompleter = Completer();
|
||||
}
|
||||
sendPort = message;
|
||||
_canSendCompleter.complete(true);
|
||||
} else {
|
||||
handleResult(
|
||||
ActionResult.fromJson(json.decode(
|
||||
message,
|
||||
)),
|
||||
);
|
||||
}
|
||||
});
|
||||
await service?.init();
|
||||
}
|
||||
|
||||
_registerMainPort(SendPort sendPort) {
|
||||
IsolateNameServer.removePortNameMapping(mainIsolate);
|
||||
IsolateNameServer.registerPortWithName(sendPort, mainIsolate);
|
||||
}
|
||||
|
||||
factory ClashLib() {
|
||||
_instance ??= ClashLib._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> nextHandleResult(result, completer) async {
|
||||
switch (result.method) {
|
||||
case ActionMethod.setFdMap:
|
||||
case ActionMethod.setProcessMap:
|
||||
case ActionMethod.stopTun:
|
||||
case ActionMethod.updateDns:
|
||||
completer?.complete(result.data as bool);
|
||||
return true;
|
||||
case ActionMethod.getRunTime:
|
||||
case ActionMethod.startTun:
|
||||
case ActionMethod.getAndroidVpnOptions:
|
||||
case ActionMethod.getCurrentProfileName:
|
||||
completer?.complete(result.data as String);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
await service?.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
reStart() {
|
||||
_initService();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> shutdown() async {
|
||||
await super.shutdown();
|
||||
destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
sendMessage(String message) async {
|
||||
await _canSendCompleter.future;
|
||||
sendPort?.send(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setFdMap(int fd) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setFdMap,
|
||||
data: json.encode(fd),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> setProcessMap(item) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.setProcessMap,
|
||||
data: item,
|
||||
);
|
||||
}
|
||||
|
||||
// @override
|
||||
// Future<bool> stopTun() {
|
||||
// return invoke<bool>(
|
||||
// method: ActionMethod.stopTun,
|
||||
// );
|
||||
// }
|
||||
|
||||
@override
|
||||
Future<AndroidVpnOptions?> getAndroidVpnOptions() async {
|
||||
final res = await invoke<String>(
|
||||
method: ActionMethod.getAndroidVpnOptions,
|
||||
);
|
||||
if (res.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return AndroidVpnOptions.fromJson(json.decode(res));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> updateDns(String value) {
|
||||
return invoke<bool>(
|
||||
method: ActionMethod.updateDns,
|
||||
data: value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DateTime?> getRunTime() async {
|
||||
final runTimeString = await invoke<String>(
|
||||
method: ActionMethod.getRunTime,
|
||||
);
|
||||
if (runTimeString.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> getCurrentProfileName() {
|
||||
return invoke<String>(
|
||||
method: ActionMethod.getCurrentProfileName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClashLibHandler {
|
||||
static ClashLibHandler? _instance;
|
||||
|
||||
late final ClashFFI clashFFI;
|
||||
|
||||
late final DynamicLibrary lib;
|
||||
|
||||
ClashLibHandler._internal() {
|
||||
lib = DynamicLibrary.open("libclash.so");
|
||||
clashFFI = ClashFFI(lib);
|
||||
clashFFI.initNativeApiBridge(
|
||||
NativeApi.initializeApiDLData,
|
||||
);
|
||||
}
|
||||
|
||||
factory ClashLibHandler() {
|
||||
_instance ??= ClashLibHandler._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<String> invokeAction(String actionParams) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final actionParamsChar = actionParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.invokeAction(
|
||||
actionParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(actionParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
attachMessagePort(int messagePort) {
|
||||
clashFFI.attachMessagePort(
|
||||
messagePort,
|
||||
);
|
||||
}
|
||||
|
||||
updateDns(String dns) {
|
||||
final dnsChar = dns.toNativeUtf8().cast<Char>();
|
||||
clashFFI.updateDns(dnsChar);
|
||||
malloc.free(dnsChar);
|
||||
}
|
||||
|
||||
setState(CoreState state) {
|
||||
final stateChar = json.encode(state).toNativeUtf8().cast<Char>();
|
||||
clashFFI.setState(stateChar);
|
||||
malloc.free(stateChar);
|
||||
}
|
||||
|
||||
String getCurrentProfileName() {
|
||||
final currentProfileRaw = clashFFI.getCurrentProfileName();
|
||||
final currentProfile = currentProfileRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(currentProfileRaw);
|
||||
return currentProfile;
|
||||
}
|
||||
|
||||
AndroidVpnOptions getAndroidVpnOptions() {
|
||||
final vpnOptionsRaw = clashFFI.getAndroidVpnOptions();
|
||||
final vpnOptions = json.decode(vpnOptionsRaw.cast<Utf8>().toDartString());
|
||||
clashFFI.freeCString(vpnOptionsRaw);
|
||||
return AndroidVpnOptions.fromJson(vpnOptions);
|
||||
}
|
||||
|
||||
Traffic getTraffic() {
|
||||
final trafficRaw = clashFFI.getTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
Traffic getTotalTraffic(bool value) {
|
||||
final trafficRaw = clashFFI.getTotalTraffic();
|
||||
final trafficString = trafficRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(trafficRaw);
|
||||
if (trafficString.isEmpty) {
|
||||
return Traffic();
|
||||
}
|
||||
return Traffic.fromMap(json.decode(trafficString));
|
||||
}
|
||||
|
||||
startListener() async {
|
||||
clashFFI.startListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
stopListener() async {
|
||||
clashFFI.stopListener();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> quickStart(
|
||||
InitParams initParams,
|
||||
UpdateConfigParams updateConfigParams,
|
||||
CoreState state,
|
||||
) {
|
||||
final completer = Completer<String>();
|
||||
final receiver = ReceivePort();
|
||||
receiver.listen((message) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(message);
|
||||
receiver.close();
|
||||
}
|
||||
});
|
||||
final params = json.encode(updateConfigParams);
|
||||
final initValue = json.encode(initParams);
|
||||
final stateParams = json.encode(state);
|
||||
final initParamsChar = initValue.toNativeUtf8().cast<Char>();
|
||||
final paramsChar = params.toNativeUtf8().cast<Char>();
|
||||
final stateParamsChar = stateParams.toNativeUtf8().cast<Char>();
|
||||
clashFFI.quickStart(
|
||||
initParamsChar,
|
||||
paramsChar,
|
||||
stateParamsChar,
|
||||
receiver.sendPort.nativePort,
|
||||
);
|
||||
malloc.free(initParamsChar);
|
||||
malloc.free(paramsChar);
|
||||
malloc.free(stateParamsChar);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
DateTime? getRunTime() {
|
||||
final runTimeRaw = clashFFI.getRunTime();
|
||||
final runTimeString = runTimeRaw.cast<Utf8>().toDartString();
|
||||
clashFFI.freeCString(runTimeRaw);
|
||||
if (runTimeString.isEmpty) return null;
|
||||
return DateTime.fromMillisecondsSinceEpoch(int.parse(runTimeString));
|
||||
}
|
||||
}
|
||||
|
||||
ClashLib? get clashLib =>
|
||||
Platform.isAndroid && !globalState.isService ? ClashLib() : null;
|
||||
+24
-28
@@ -5,38 +5,34 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'core.dart';
|
||||
|
||||
class ClashMessage {
|
||||
StreamSubscription? subscription;
|
||||
final controller = StreamController<String>();
|
||||
|
||||
ClashMessage._() {
|
||||
if (subscription != null) {
|
||||
subscription!.cancel();
|
||||
subscription = null;
|
||||
}
|
||||
subscription = ClashCore.receiver.listen((message) {
|
||||
final m = AppMessage.fromJson(json.decode(message));
|
||||
for (final AppMessageListener listener in _listeners) {
|
||||
switch (m.type) {
|
||||
case AppMessageType.log:
|
||||
listener.onLog(Log.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.delay:
|
||||
listener.onDelay(Delay.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.request:
|
||||
listener.onRequest(Connection.fromJson(m.data));
|
||||
break;
|
||||
case AppMessageType.started:
|
||||
listener.onStarted(m.data);
|
||||
break;
|
||||
case AppMessageType.loaded:
|
||||
listener.onLoaded(m.data);
|
||||
break;
|
||||
controller.stream.listen(
|
||||
(message) {
|
||||
if(message.isEmpty){
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
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._();
|
||||
|
||||
+150
-43
@@ -1,54 +1,161 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fl_clash/clash/interface.dart';
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
import 'core.dart';
|
||||
class ClashService extends ClashHandlerInterface {
|
||||
static ClashService? _instance;
|
||||
|
||||
class ClashService {
|
||||
Future<void> initGeo() async {
|
||||
final homePath = await appPath.getHomeDirPath();
|
||||
final homeDir = Directory(homePath);
|
||||
final isExists = await homeDir.exists();
|
||||
if (!isExists) {
|
||||
await homeDir.create(recursive: true);
|
||||
}
|
||||
const geoFileNameList = [
|
||||
mmdbFileName,
|
||||
geoIpFileName,
|
||||
geoSiteFileName,
|
||||
asnFileName,
|
||||
];
|
||||
try {
|
||||
for (final geoFileName in geoFileNameList) {
|
||||
final geoFile = File(
|
||||
join(homePath, geoFileName),
|
||||
);
|
||||
final isExists = await geoFile.exists();
|
||||
if (isExists) {
|
||||
continue;
|
||||
}
|
||||
final data = await rootBundle.load('assets/data/$geoFileName');
|
||||
List<int> bytes = data.buffer.asUint8List();
|
||||
await geoFile.writeAsBytes(bytes, flush: true);
|
||||
Completer<ServerSocket> serverCompleter = Completer();
|
||||
|
||||
Completer<Socket> socketCompleter = Completer();
|
||||
|
||||
bool isStarting = false;
|
||||
|
||||
Process? process;
|
||||
|
||||
factory ClashService() {
|
||||
_instance ??= ClashService._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
ClashService._internal() {
|
||||
_initServer();
|
||||
reStart();
|
||||
}
|
||||
|
||||
_initServer() async {
|
||||
runZonedGuarded(() async {
|
||||
final address = !Platform.isWindows
|
||||
? InternetAddress(
|
||||
unixSocketPath,
|
||||
type: InternetAddressType.unix,
|
||||
)
|
||||
: InternetAddress(
|
||||
localhost,
|
||||
type: InternetAddressType.IPv4,
|
||||
);
|
||||
await _deleteSocketFile();
|
||||
final server = await ServerSocket.bind(
|
||||
address,
|
||||
0,
|
||||
shared: true,
|
||||
);
|
||||
serverCompleter.complete(server);
|
||||
await for (final socket in server) {
|
||||
await _destroySocket();
|
||||
socketCompleter.complete(socket);
|
||||
socket
|
||||
.transform(
|
||||
StreamTransformer<Uint8List, String>.fromHandlers(
|
||||
handleData: (Uint8List data, EventSink<String> sink) {
|
||||
sink.add(utf8.decode(data, allowMalformed: true));
|
||||
},
|
||||
),
|
||||
)
|
||||
.transform(LineSplitter())
|
||||
.listen(
|
||||
(data) {
|
||||
handleResult(
|
||||
ActionResult.fromJson(
|
||||
json.decode(data.trim()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}, (error, stack) {
|
||||
commonPrint.log(error.toString());
|
||||
if(error is SocketException){
|
||||
globalState.showNotifier(error.toString());
|
||||
globalState.appController.restartCore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
reStart() async {
|
||||
if (isStarting == true) {
|
||||
return;
|
||||
}
|
||||
isStarting = true;
|
||||
socketCompleter = Completer();
|
||||
if (process != null) {
|
||||
await shutdown();
|
||||
}
|
||||
final serverSocket = await serverCompleter.future;
|
||||
final arg = Platform.isWindows
|
||||
? "${serverSocket.port}"
|
||||
: serverSocket.address.address;
|
||||
bool isSuccess = false;
|
||||
if (Platform.isWindows && await system.checkIsAdmin()) {
|
||||
isSuccess = await request.startCoreByHelper(arg);
|
||||
}
|
||||
if (isSuccess) {
|
||||
return;
|
||||
}
|
||||
process = await Process.start(
|
||||
appPath.corePath,
|
||||
[
|
||||
arg,
|
||||
],
|
||||
);
|
||||
process!.stdout.listen((_) {});
|
||||
isStarting = false;
|
||||
}
|
||||
|
||||
@override
|
||||
destroy() async {
|
||||
final server = await serverCompleter.future;
|
||||
await server.close();
|
||||
await _deleteSocketFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
sendMessage(String message) async {
|
||||
final socket = await socketCompleter.future;
|
||||
socket.writeln(message);
|
||||
}
|
||||
|
||||
_deleteSocketFile() async {
|
||||
if (!Platform.isWindows) {
|
||||
final file = File(unixSocketPath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("$e");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> init({
|
||||
required ClashConfig clashConfig,
|
||||
required Config config,
|
||||
}) async {
|
||||
await initGeo();
|
||||
final homeDirPath = await appPath.getHomeDirPath();
|
||||
final isInit = clashCore.init(homeDirPath);
|
||||
return isInit;
|
||||
_destroySocket() async {
|
||||
if (socketCompleter.isCompleted) {
|
||||
final lastSocket = await socketCompleter.future;
|
||||
await lastSocket.close();
|
||||
socketCompleter = Completer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
shutdown() async {
|
||||
if (Platform.isWindows) {
|
||||
await request.stopCoreByHelper();
|
||||
}
|
||||
await _destroySocket();
|
||||
process?.kill();
|
||||
process = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> preload() async {
|
||||
await serverCompleter.future;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final clashService = ClashService();
|
||||
final clashService = system.isDesktop ? ClashService() : null;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
|
||||
class Android {
|
||||
init() async {
|
||||
app?.onExit = () {};
|
||||
app?.onExit = () async {
|
||||
await globalState.appController.savePreferences();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
extension ArchiveExt on Archive {
|
||||
addDirectoryToArchive(String dirPath, String parentPath) {
|
||||
final dir = Directory(dirPath);
|
||||
final entities = dir.listSync(recursive: false);
|
||||
for (final entity in entities) {
|
||||
final relativePath = relative(entity.path, from: parentPath);
|
||||
if (entity is File) {
|
||||
final data = entity.readAsBytesSync();
|
||||
final archiveFile = ArchiveFile(relativePath, data.length, data);
|
||||
addFile(archiveFile);
|
||||
} else if (entity is Directory) {
|
||||
addDirectoryToArchive(entity.path, parentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add<T>(String name, T raw) {
|
||||
final data = json.encode(raw);
|
||||
addFile(
|
||||
ArchiveFile(name, data.length, data),
|
||||
);
|
||||
}
|
||||
}
|
||||
+71
-9
@@ -1,19 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ColorExtension on Color {
|
||||
toLight() {
|
||||
return withOpacity(0.6);
|
||||
Color get opacity80 {
|
||||
return withAlpha(204);
|
||||
}
|
||||
|
||||
toLighter() {
|
||||
return withOpacity(0.4);
|
||||
Color get opacity60 {
|
||||
return withAlpha(153);
|
||||
}
|
||||
|
||||
toSoft() {
|
||||
return withOpacity(0.12);
|
||||
Color get opacity50 {
|
||||
return withAlpha(128);
|
||||
}
|
||||
|
||||
toLittle() {
|
||||
return withOpacity(0.03);
|
||||
Color get opacity38 {
|
||||
return withAlpha(97);
|
||||
}
|
||||
}
|
||||
|
||||
Color get opacity30 {
|
||||
return withAlpha(77);
|
||||
}
|
||||
|
||||
Color get opacity15 {
|
||||
return withAlpha(38);
|
||||
}
|
||||
|
||||
Color get opacity10 {
|
||||
return withAlpha(15);
|
||||
}
|
||||
|
||||
Color get opacity3 {
|
||||
return withAlpha(76);
|
||||
}
|
||||
|
||||
Color get opacity0 {
|
||||
return withAlpha(0);
|
||||
}
|
||||
|
||||
Color darken([double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
final hsl = HSLColor.fromColor(this);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
return hslDark.toColor();
|
||||
}
|
||||
|
||||
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 {
|
||||
ColorScheme toPureBlack(bool isPrueBlack) => isPrueBlack
|
||||
? copyWith(
|
||||
surface: Colors.black,
|
||||
surfaceContainer: surfaceContainer.darken(
|
||||
0.05,
|
||||
),
|
||||
)
|
||||
: this;
|
||||
}
|
||||
|
||||
+35
-24
@@ -1,28 +1,39 @@
|
||||
export 'path.dart';
|
||||
export 'request.dart';
|
||||
export 'preferences.dart';
|
||||
export 'constant.dart';
|
||||
export 'proxy.dart';
|
||||
export 'other.dart';
|
||||
export 'num.dart';
|
||||
export 'navigation.dart';
|
||||
export 'window.dart';
|
||||
export 'system.dart';
|
||||
export 'picker.dart';
|
||||
export 'android.dart';
|
||||
export 'launch.dart';
|
||||
export 'protocol.dart';
|
||||
export 'datetime.dart';
|
||||
export 'context.dart';
|
||||
export 'link.dart';
|
||||
export 'text.dart';
|
||||
export 'color.dart';
|
||||
export 'list.dart';
|
||||
export 'string.dart';
|
||||
export 'app_localizations.dart';
|
||||
export 'color.dart';
|
||||
export 'constant.dart';
|
||||
export 'context.dart';
|
||||
export 'datetime.dart';
|
||||
export 'function.dart';
|
||||
export 'package.dart';
|
||||
export 'measure.dart';
|
||||
export 'service.dart';
|
||||
export 'future.dart';
|
||||
export 'http.dart';
|
||||
export 'icons.dart';
|
||||
export 'iterable.dart';
|
||||
export 'scroll.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 'picker.dart';
|
||||
export 'preferences.dart';
|
||||
export 'protocol.dart';
|
||||
export 'proxy.dart';
|
||||
export 'request.dart';
|
||||
export 'scroll.dart';
|
||||
export 'string.dart';
|
||||
export 'system.dart';
|
||||
export 'text.dart';
|
||||
export 'tray.dart';
|
||||
export 'window.dart';
|
||||
export 'windows.dart';
|
||||
export 'render.dart';
|
||||
export 'mixin.dart';
|
||||
export 'print.dart';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user