Android Support (#166)
1. Add vpnservice tauri plugin for android. 2. add workflow for android. 3. Easytier Core support android, allow set tun fd.
4
.github/workflows/gui.yml
vendored
|
@ -88,8 +88,8 @@ jobs:
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: |
|
run: |
|
||||||
cd easytier-gui
|
(cd easytier-gui; pnpm install)
|
||||||
pnpm install
|
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||||
|
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|
164
.github/workflows/mobile.yml
vendored
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
name: EasyTier Mobile
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["develop", "main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["develop", "main"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
# necessary for windows
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre_job:
|
||||||
|
# continue-on-error: true # Uncomment once integration is finished
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Map a step output to a job output
|
||||||
|
outputs:
|
||||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
|
steps:
|
||||||
|
- id: skip_check
|
||||||
|
uses: fkirc/skip-duplicate-actions@v5
|
||||||
|
with:
|
||||||
|
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||||
|
concurrent_skipping: 'never'
|
||||||
|
skip_after_successful_duplicate: 'true'
|
||||||
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml"]'
|
||||||
|
build-mobile:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- TARGET: android
|
||||||
|
OS: ubuntu-latest
|
||||||
|
runs-on: ${{ matrix.OS }}
|
||||||
|
env:
|
||||||
|
NAME: easytier
|
||||||
|
TARGET: ${{ matrix.TARGET }}
|
||||||
|
OS: ${{ matrix.OS }}
|
||||||
|
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||||
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'oracle'
|
||||||
|
java-version: '20'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
cmdline-tools-version: 11076708
|
||||||
|
packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
|
||||||
|
|
||||||
|
- name: Setup Android Environment
|
||||||
|
run: |
|
||||||
|
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
||||||
|
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||||
|
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 21
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: |
|
||||||
|
(cd easytier-gui; pnpm install)
|
||||||
|
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
|
||||||
|
|
||||||
|
- name: Cargo cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo
|
||||||
|
./target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install rust target
|
||||||
|
run: |
|
||||||
|
bash ./.github/workflows/install_rust.sh
|
||||||
|
rustup target add aarch64-linux-android
|
||||||
|
rustup target add armv7-linux-androideabi
|
||||||
|
rustup target add i686-linux-android
|
||||||
|
rustup target add x86_64-linux-android
|
||||||
|
|
||||||
|
- name: Setup protoc
|
||||||
|
uses: arduino/setup-protoc@v2
|
||||||
|
with:
|
||||||
|
# GitHub repo token to use to avoid rate limiter
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build Android
|
||||||
|
run: |
|
||||||
|
cd easytier-gui
|
||||||
|
pnpm tauri android build
|
||||||
|
|
||||||
|
- name: Compress
|
||||||
|
run: |
|
||||||
|
mkdir -p ./artifacts/objects/
|
||||||
|
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
|
||||||
|
|
||||||
|
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||||
|
TAG=$GITHUB_REF_NAME
|
||||||
|
else
|
||||||
|
TAG=$GITHUB_SHA
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -cvf ./artifacts/$NAME-$TARGET-$TAG.tar -C ./artifacts/objects/ .
|
||||||
|
rm -rf ./artifacts/objects/
|
||||||
|
|
||||||
|
- name: Archive artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: easytier-gui-${{ matrix.TARGET }}
|
||||||
|
path: |
|
||||||
|
./artifacts/*
|
||||||
|
|
||||||
|
- name: Upload OSS
|
||||||
|
if: ${{ env.OSS_BUCKET != '' }}
|
||||||
|
uses: Menci/upload-to-oss@main
|
||||||
|
with:
|
||||||
|
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
||||||
|
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
||||||
|
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||||
|
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||||
|
local-path: ./artifacts/
|
||||||
|
remote-path: /easytier-releases/${{ github.sha }}/mobile
|
||||||
|
no-delete-remote-files: true
|
||||||
|
retry: 5
|
||||||
|
mobile-result:
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- build-mobile
|
||||||
|
steps:
|
||||||
|
- name: Mark result as failed
|
||||||
|
if: needs.build-mobile.result != 'success'
|
||||||
|
run: exit 1
|
54
Cargo.lock
generated
|
@ -1448,9 +1448,11 @@ dependencies = [
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-positioner",
|
"tauri-plugin-positioner",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"tauri-plugin-vpnservice",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3569,6 +3571,17 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_info"
|
||||||
|
version = "3.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_pipe"
|
name = "os_pipe"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
|
@ -5311,6 +5324,15 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sys-locale"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -5618,6 +5640,24 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-os"
|
||||||
|
version = "2.0.0-beta.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2946635d31de19ed4191f1c556da20d1257f4667a1bac03852d9bb4be9fa8ffa"
|
||||||
|
dependencies = [
|
||||||
|
"gethostname",
|
||||||
|
"log",
|
||||||
|
"os_info",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serialize-to-javascript",
|
||||||
|
"sys-locale",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-positioner"
|
name = "tauri-plugin-positioner"
|
||||||
version = "2.0.0-beta.8"
|
version = "2.0.0-beta.8"
|
||||||
|
@ -5664,6 +5704,16 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-vpnservice"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.0.0-beta.19"
|
version = "2.0.0-beta.19"
|
||||||
|
@ -6280,9 +6330,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tun-easytier"
|
name = "tun-easytier"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6d01bd11265e1cb5ca22e9103daf57194afa43b1dc4c8cd49b950c969ffbe7c"
|
checksum = "060078376b7da6986f3050f23cdb123b05371f11802c1bdc5c0af919db9292bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
2
easytier-gui/.gitignore
vendored
|
@ -23,3 +23,5 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
vite.config.ts.timestamp*
|
||||||
|
|
|
@ -1,16 +1,46 @@
|
||||||
# Tauri + Vue 3 + TypeScript
|
# GUI for EasyTier
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
this is a GUI implementation for EasyTier, based on Tauri2.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Compile
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
### Install prerequisites
|
||||||
|
|
||||||
## Type Support For `.vue` Imports in TS
|
```
|
||||||
|
apt install npm
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
### For Desktop (Win/Mac/Linux)
|
||||||
|
|
||||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
```
|
||||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
pnpm install
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
|
|
||||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
### For Android
|
||||||
|
|
||||||
|
Need to install android SDK / emulator / NDK / Java (easy with android studio)
|
||||||
|
|
||||||
|
```
|
||||||
|
# For ArchLinux
|
||||||
|
sudo pacman -Sy sdkmanager
|
||||||
|
sudo sdkmanager --install platform-tools platforms\;android-34 ndk\;r26 build-tools
|
||||||
|
export PATH=/opt/android-sdk/platform-tools:$PATH
|
||||||
|
export ANDROID_HOME=/opt/android-sdk/
|
||||||
|
export NDK_HOME=/opt/android-sdk/ndk/26.0.10792818/
|
||||||
|
rustup target add aarch64-linux-android
|
||||||
|
|
||||||
|
install java 20
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts)
|
||||||
|
|
||||||
|
See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail .
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install
|
||||||
|
pnpm tauri android init
|
||||||
|
pnpm tauri android build
|
||||||
|
```
|
|
@ -21,7 +21,7 @@ config_network: Config Network
|
||||||
running: Running
|
running: Running
|
||||||
error_msg: Error Message
|
error_msg: Error Message
|
||||||
detail: Detail
|
detail: Detail
|
||||||
add_new_network: Add New Network
|
add_new_network: New Network
|
||||||
del_cur_network: Delete Current Network
|
del_cur_network: Delete Current Network
|
||||||
select_network: Select Network
|
select_network: Select Network
|
||||||
network_instances: Network Instances
|
network_instances: Network Instances
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.0.0",
|
"@primevue/themes": "^4.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.4",
|
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.4",
|
||||||
|
"@tauri-apps/plugin-os": "2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-beta.6",
|
"@tauri-apps/plugin-process": "2.0.0-beta.6",
|
||||||
"@tauri-apps/plugin-shell": "2.0.0-beta.7",
|
"@tauri-apps/plugin-shell": "2.0.0-beta.7",
|
||||||
"aura": "link:@primevue/themes/aura",
|
"aura": "link:@primevue/themes/aura",
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
"primeflex": "^3.3.1",
|
"primeflex": "^3.3.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.0.0",
|
"primevue": "^4.0.0",
|
||||||
|
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice/",
|
||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
|
|
|
@ -14,6 +14,9 @@ importers:
|
||||||
'@tauri-apps/plugin-clipboard-manager':
|
'@tauri-apps/plugin-clipboard-manager':
|
||||||
specifier: 2.1.0-beta.4
|
specifier: 2.1.0-beta.4
|
||||||
version: 2.1.0-beta.4
|
version: 2.1.0-beta.4
|
||||||
|
'@tauri-apps/plugin-os':
|
||||||
|
specifier: 2.0.0-beta.6
|
||||||
|
version: 2.0.0-beta.6
|
||||||
'@tauri-apps/plugin-process':
|
'@tauri-apps/plugin-process':
|
||||||
specifier: 2.0.0-beta.6
|
specifier: 2.0.0-beta.6
|
||||||
version: 2.0.0-beta.6
|
version: 2.0.0-beta.6
|
||||||
|
@ -35,6 +38,9 @@ importers:
|
||||||
primevue:
|
primevue:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0(@primeuix/utils@0.0.5)(vue@3.4.31(typescript@5.5.3))
|
version: 4.0.0(@primeuix/utils@0.0.5)(vue@3.4.31(typescript@5.5.3))
|
||||||
|
tauri-plugin-vpnservice-api:
|
||||||
|
specifier: link:../tauri-plugin-vpnservice/
|
||||||
|
version: link:../tauri-plugin-vpnservice
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.31
|
specifier: ^3.4.31
|
||||||
version: 3.4.31(typescript@5.5.3)
|
version: 3.4.31(typescript@5.5.3)
|
||||||
|
@ -860,6 +866,9 @@ packages:
|
||||||
'@tauri-apps/plugin-clipboard-manager@2.1.0-beta.4':
|
'@tauri-apps/plugin-clipboard-manager@2.1.0-beta.4':
|
||||||
resolution: {integrity: sha512-iu44K+WmNCbU3l8/sdQq6e0ZE8Uv/HKAj7J68K4Cx0RhOvX+mnjHWAX/A/ok5C4/OCAuYdEvWmi4+Lsdld+njQ==}
|
resolution: {integrity: sha512-iu44K+WmNCbU3l8/sdQq6e0ZE8Uv/HKAj7J68K4Cx0RhOvX+mnjHWAX/A/ok5C4/OCAuYdEvWmi4+Lsdld+njQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-os@2.0.0-beta.6':
|
||||||
|
resolution: {integrity: sha512-28Ts286o4YH3vZ+swptVblRMuMa1MLjLbgPpnR1wuPNzzR4p7J6+Hr3Euge71RIsFJhjAeP1XkNbHgpAFj4Mpg==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-process@2.0.0-beta.6':
|
'@tauri-apps/plugin-process@2.0.0-beta.6':
|
||||||
resolution: {integrity: sha512-Rem3r8lGe6ZSvncqIV9xpq2hOey7krMoPh5nu7WxbR73LOSkRBUDaYMvZjXu1DrJ3LEyXxo48sp76+9MW2Rp/w==}
|
resolution: {integrity: sha512-Rem3r8lGe6ZSvncqIV9xpq2hOey7krMoPh5nu7WxbR73LOSkRBUDaYMvZjXu1DrJ3LEyXxo48sp76+9MW2Rp/w==}
|
||||||
|
|
||||||
|
@ -3764,6 +3773,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.0-beta.14
|
'@tauri-apps/api': 2.0.0-beta.14
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-os@2.0.0-beta.6':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.0.0-beta.14
|
||||||
|
|
||||||
'@tauri-apps/plugin-process@2.0.0-beta.6':
|
'@tauri-apps/plugin-process@2.0.0-beta.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.0.0-beta.14
|
'@tauri-apps/api': 2.0.0-beta.14
|
||||||
|
|
4
easytier-gui/src-tauri/.cargo/config.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[build]
|
||||||
|
target = "x86_64-unknown-linux-gnu"
|
||||||
|
|
||||||
|
[target]
|
|
@ -7,6 +7,10 @@ edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "app_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||||
|
|
||||||
|
@ -34,6 +38,8 @@ tauri-plugin-shell = "2.0.0-beta.8"
|
||||||
tauri-plugin-process = "2.0.0-beta.7"
|
tauri-plugin-process = "2.0.0-beta.7"
|
||||||
tauri-plugin-clipboard-manager = "2.1.0-beta.5"
|
tauri-plugin-clipboard-manager = "2.1.0-beta.5"
|
||||||
tauri-plugin-positioner = { version = "2.0.0-beta", features = ["tray-icon"] }
|
tauri-plugin-positioner = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||||
|
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||||
|
tauri-plugin-os = "2.0.0-beta.7"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
|
|
|
@ -8,13 +8,11 @@
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
|
|
||||||
"window:default",
|
"window:default",
|
||||||
"window:allow-is-visible",
|
"window:allow-is-visible",
|
||||||
"window:allow-show",
|
"window:allow-show",
|
||||||
"window:allow-hide",
|
"window:allow-hide",
|
||||||
"window:allow-set-focus",
|
"window:allow-set-focus",
|
||||||
|
|
||||||
"app:default",
|
"app:default",
|
||||||
"resources:default",
|
"resources:default",
|
||||||
"menu:default",
|
"menu:default",
|
||||||
|
@ -26,7 +24,6 @@
|
||||||
"shell:default",
|
"shell:default",
|
||||||
"process:default",
|
"process:default",
|
||||||
"clipboard-manager:default",
|
"clipboard-manager:default",
|
||||||
|
|
||||||
"tray:default",
|
"tray:default",
|
||||||
"tray:allow-new",
|
"tray:allow-new",
|
||||||
"tray:allow-set-menu",
|
"tray:allow-set-menu",
|
||||||
|
@ -36,6 +33,17 @@
|
||||||
"tray:allow-set-icon",
|
"tray:allow-set-icon",
|
||||||
"tray:allow-set-icon-as-template",
|
"tray:allow-set-icon-as-template",
|
||||||
"tray:allow-set-show-menu-on-left-click",
|
"tray:allow-set-show-menu-on-left-click",
|
||||||
"tray:allow-set-tooltip"
|
"tray:allow-set-tooltip",
|
||||||
|
"vpnservice:allow-ping",
|
||||||
|
"vpnservice:allow-prepare-vpn",
|
||||||
|
"vpnservice:allow-start-vpn",
|
||||||
|
"vpnservice:allow-stop-vpn",
|
||||||
|
"vpnservice:allow-register-listener",
|
||||||
|
"os:default",
|
||||||
|
"os:allow-os-type",
|
||||||
|
"os:allow-arch",
|
||||||
|
"os:allow-hostname",
|
||||||
|
"os:allow-platform",
|
||||||
|
"os:allow-locale"
|
||||||
]
|
]
|
||||||
}
|
}
|
12
easytier-gui/src-tauri/gen/android/.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
19
easytier-gui/src-tauri/gen/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
||||||
|
key.properties
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/tauri.settings.gradle
|
6
easytier-gui/src-tauri/gen/android/app/.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/src/main/java/com/kkrainbow/easytier/generated
|
||||||
|
/src/main/jniLibs/**/*.so
|
||||||
|
/src/main/assets/tauri.conf.json
|
||||||
|
/tauri.build.gradle.kts
|
||||||
|
/proguard-tauri.pro
|
||||||
|
/tauri.properties
|
85
easytier-gui/src-tauri/gen/android/app/build.gradle.kts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("rust")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tauriProperties = Properties().apply {
|
||||||
|
val propFile = file("tauri.properties")
|
||||||
|
if (propFile.exists()) {
|
||||||
|
propFile.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdk = 34
|
||||||
|
namespace = "com.kkrainbow.easytier"
|
||||||
|
defaultConfig {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||||
|
applicationId = "com.kkrainbow.easytier"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 34
|
||||||
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
val keystoreProperties = Properties()
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
keyAlias = keystoreProperties["keyAlias"] as String
|
||||||
|
keyPassword = keystoreProperties["keyPassword"] as String
|
||||||
|
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||||
|
storePassword = keystoreProperties["storePassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
getByName("debug") {
|
||||||
|
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||||
|
isDebuggable = true
|
||||||
|
isJniDebuggable = true
|
||||||
|
isMinifyEnabled = false
|
||||||
|
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||||
|
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getByName("release") {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
*fileTree(".") { include("**/*.pro") }
|
||||||
|
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||||
|
.toList().toTypedArray()
|
||||||
|
)
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rust {
|
||||||
|
rootDirRel = "../../../"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("androidx.webkit:webkit:1.6.1")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
|
implementation("com.google.android.material:material:1.8.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = "tauri.build.gradle.kts")
|
21
easytier-gui/src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,44 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.easytier_gui"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<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="com.plugin.vpnservice.TauriVpnService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.VpnService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.kkrainbow.easytier
|
||||||
|
|
||||||
|
class MainActivity : TauriActivity()
|
|
@ -0,0 +1,30 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,170 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Hello World!"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.easytier_gui" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">easytier-gui</string>
|
||||||
|
<string name="main_activity_title">easytier-gui</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.easytier_gui" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
BIN
easytier-gui/src-tauri/gen/android/app/upload-keystore.jks
Normal file
22
easytier-gui/src-tauri/gen/android/build.gradle.kts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.3.2")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean").configure {
|
||||||
|
delete("build")
|
||||||
|
}
|
||||||
|
|
23
easytier-gui/src-tauri/gen/android/buildSrc/build.gradle.kts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
plugins {
|
||||||
|
`kotlin-dsl`
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
create("pluginsForCoolKids") {
|
||||||
|
id = "rust"
|
||||||
|
implementationClass = "RustPlugin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(gradleApi())
|
||||||
|
implementation("com.android.tools.build:gradle:8.3.2")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import java.io.File
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.GradleException
|
||||||
|
import org.gradle.api.logging.LogLevel
|
||||||
|
import org.gradle.api.tasks.Input
|
||||||
|
import org.gradle.api.tasks.TaskAction
|
||||||
|
|
||||||
|
open class BuildTask : DefaultTask() {
|
||||||
|
@Input
|
||||||
|
var rootDirRel: String? = null
|
||||||
|
@Input
|
||||||
|
var target: String? = null
|
||||||
|
@Input
|
||||||
|
var release: Boolean? = null
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun assemble() {
|
||||||
|
val executable = """pnpm""";
|
||||||
|
try {
|
||||||
|
runTauriCli(executable)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||||
|
runTauriCli("$executable.cmd")
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTauriCli(executable: String) {
|
||||||
|
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||||
|
val target = target ?: throw GradleException("target cannot be null")
|
||||||
|
val release = release ?: throw GradleException("release cannot be null")
|
||||||
|
val args = listOf("tauri", "android", "android-studio-script");
|
||||||
|
|
||||||
|
project.exec {
|
||||||
|
workingDir(File(project.projectDir, rootDirRel))
|
||||||
|
executable(executable)
|
||||||
|
args(args)
|
||||||
|
if (project.logger.isEnabled(LogLevel.DEBUG)) {
|
||||||
|
args("-vv")
|
||||||
|
} else if (project.logger.isEnabled(LogLevel.INFO)) {
|
||||||
|
args("-v")
|
||||||
|
}
|
||||||
|
if (release) {
|
||||||
|
args("--release")
|
||||||
|
}
|
||||||
|
args(listOf("--target", target))
|
||||||
|
}.assertNormalExitValue()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
import org.gradle.api.DefaultTask
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.get
|
||||||
|
|
||||||
|
const val TASK_GROUP = "rust"
|
||||||
|
|
||||||
|
open class Config {
|
||||||
|
lateinit var rootDirRel: String
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RustPlugin : Plugin<Project> {
|
||||||
|
private lateinit var config: Config
|
||||||
|
|
||||||
|
override fun apply(project: Project) = with(project) {
|
||||||
|
config = extensions.create("rust", Config::class.java)
|
||||||
|
|
||||||
|
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
|
||||||
|
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
|
||||||
|
|
||||||
|
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
|
||||||
|
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
|
||||||
|
|
||||||
|
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
|
||||||
|
|
||||||
|
extensions.configure<ApplicationExtension> {
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
flavorDimensions.add("abi")
|
||||||
|
productFlavors {
|
||||||
|
create("universal") {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters += abiList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultArchList.forEachIndexed { index, arch ->
|
||||||
|
create(arch) {
|
||||||
|
dimension = "abi"
|
||||||
|
ndk {
|
||||||
|
abiFilters.add(defaultAbiList[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
for (profile in listOf("debug", "release")) {
|
||||||
|
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
|
||||||
|
val buildTask = tasks.maybeCreate(
|
||||||
|
"rustBuildUniversal$profileCapitalized",
|
||||||
|
DefaultTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for all targets"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
|
||||||
|
|
||||||
|
for (targetPair in targetsList.withIndex()) {
|
||||||
|
val targetName = targetPair.value
|
||||||
|
val targetArch = archList[targetPair.index]
|
||||||
|
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
|
||||||
|
val targetBuildTask = project.tasks.maybeCreate(
|
||||||
|
"rustBuild$targetArchCapitalized$profileCapitalized",
|
||||||
|
BuildTask::class.java
|
||||||
|
).apply {
|
||||||
|
group = TASK_GROUP
|
||||||
|
description = "Build dynamic library in $profile mode for $targetArch"
|
||||||
|
rootDirRel = config.rootDirRel
|
||||||
|
target = targetName
|
||||||
|
release = profile == "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTask.dependsOn(targetBuildTask)
|
||||||
|
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
|
||||||
|
targetBuildTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
easytier-gui/src-tauri/gen/android/gradle.properties
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
android.nonFinalResIds=false
|
BIN
easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
vendored
Executable file
6
easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Tue May 10 19:22:52 CST 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
185
easytier-gui/src-tauri/gen/android/gradlew
vendored
Executable file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
easytier-gui/src-tauri/gen/android/gradlew.bat
vendored
Executable file
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
4
easytier-gui/src-tauri/gen/android/keystore.properties
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
storePassword=EasyTier
|
||||||
|
keyPassword=EasyTier
|
||||||
|
keyAlias=upload
|
||||||
|
storeFile=upload-keystore.jks
|
3
easytier-gui/src-tauri/gen/android/settings.gradle
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
include ':app'
|
||||||
|
|
||||||
|
apply from: 'tauri.settings.gradle'
|
408
easytier-gui/src-tauri/src/lib.rs
Normal file
|
@ -0,0 +1,408 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use auto_launch::AutoLaunchBuilder;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use easytier::{
|
||||||
|
common::config::{
|
||||||
|
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||||
|
VpnPortalConfig,
|
||||||
|
},
|
||||||
|
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
|
||||||
|
utils::{self, NewFilterSender},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use tauri::Manager as _;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
enum NetworkingMethod {
|
||||||
|
PublicServer,
|
||||||
|
Manual,
|
||||||
|
Standalone,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NetworkingMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
NetworkingMethod::PublicServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
|
struct NetworkConfig {
|
||||||
|
instance_id: String,
|
||||||
|
|
||||||
|
dhcp: bool,
|
||||||
|
virtual_ipv4: String,
|
||||||
|
hostname: Option<String>,
|
||||||
|
network_name: String,
|
||||||
|
network_secret: String,
|
||||||
|
networking_method: NetworkingMethod,
|
||||||
|
|
||||||
|
public_server_url: String,
|
||||||
|
peer_urls: Vec<String>,
|
||||||
|
|
||||||
|
proxy_cidrs: Vec<String>,
|
||||||
|
|
||||||
|
enable_vpn_portal: bool,
|
||||||
|
vpn_portal_listen_port: i32,
|
||||||
|
vpn_portal_client_network_addr: String,
|
||||||
|
vpn_portal_client_network_len: i32,
|
||||||
|
|
||||||
|
advanced_settings: bool,
|
||||||
|
|
||||||
|
listener_urls: Vec<String>,
|
||||||
|
rpc_port: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkConfig {
|
||||||
|
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
||||||
|
let cfg = TomlConfigLoader::default();
|
||||||
|
cfg.set_id(
|
||||||
|
self.instance_id
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
|
||||||
|
);
|
||||||
|
cfg.set_hostname(self.hostname.clone());
|
||||||
|
cfg.set_dhcp(self.dhcp);
|
||||||
|
cfg.set_inst_name(self.network_name.clone());
|
||||||
|
cfg.set_network_identity(NetworkIdentity::new(
|
||||||
|
self.network_name.clone(),
|
||||||
|
self.network_secret.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
if !self.dhcp {
|
||||||
|
if self.virtual_ipv4.len() > 0 {
|
||||||
|
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
|
||||||
|
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.networking_method {
|
||||||
|
NetworkingMethod::PublicServer => {
|
||||||
|
cfg.set_peers(vec![PeerConfig {
|
||||||
|
uri: self.public_server_url.parse().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse public server uri: {}",
|
||||||
|
self.public_server_url
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
NetworkingMethod::Manual => {
|
||||||
|
let mut peers = vec![];
|
||||||
|
for peer_url in self.peer_urls.iter() {
|
||||||
|
if peer_url.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
peers.push(PeerConfig {
|
||||||
|
uri: peer_url
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.set_peers(peers);
|
||||||
|
}
|
||||||
|
NetworkingMethod::Standalone => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut listener_urls = vec![];
|
||||||
|
for listener_url in self.listener_urls.iter() {
|
||||||
|
if listener_url.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
listener_urls.push(
|
||||||
|
listener_url
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cfg.set_listeners(listener_urls);
|
||||||
|
|
||||||
|
for n in self.proxy_cidrs.iter() {
|
||||||
|
cfg.add_proxy_cidr(
|
||||||
|
n.parse()
|
||||||
|
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.set_rpc_portal(
|
||||||
|
format!("127.0.0.1:{}", self.rpc_port)
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.enable_vpn_portal {
|
||||||
|
let cidr = format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
|
||||||
|
);
|
||||||
|
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||||
|
client_cidr: cidr
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
||||||
|
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port)
|
||||||
|
.parse()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse vpn portal wireguard listen port. {}",
|
||||||
|
self.vpn_portal_listen_port
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
|
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||||
|
once_cell::sync::Lazy::new(Default::default);
|
||||||
|
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||||
|
#[tauri::command]
|
||||||
|
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||||
|
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
|
Ok(toml.dump())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||||
|
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
|
||||||
|
return Err("instance already exists".to_string());
|
||||||
|
}
|
||||||
|
let instance_id = cfg.instance_id.clone();
|
||||||
|
|
||||||
|
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
|
let mut instance = NetworkInstance::new(cfg);
|
||||||
|
instance.start().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
println!("instance {} started", instance_id);
|
||||||
|
INSTANCE_MAP.insert(instance_id, instance);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn retain_network_instance(instance_ids: Vec<String>) -> Result<(), String> {
|
||||||
|
let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k));
|
||||||
|
println!(
|
||||||
|
"instance {:?} retained",
|
||||||
|
INSTANCE_MAP
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.key().clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn collect_network_infos() -> Result<BTreeMap<String, NetworkInstanceRunningInfo>, String> {
|
||||||
|
let mut ret = BTreeMap::new();
|
||||||
|
for instance in INSTANCE_MAP.iter() {
|
||||||
|
if let Some(info) = instance.get_running_info() {
|
||||||
|
ret.insert(instance.key().clone(), info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_os_hostname() -> Result<String, String> {
|
||||||
|
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
|
||||||
|
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_logging_level(level: String) -> Result<(), String> {
|
||||||
|
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
||||||
|
sender.send(level).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> {
|
||||||
|
let mut instance = INSTANCE_MAP
|
||||||
|
.get_mut(&instance_id)
|
||||||
|
.ok_or("instance not found")?;
|
||||||
|
instance.set_tun_fd(fd);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
if window.is_visible().unwrap_or_default() {
|
||||||
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
fn check_sudo() -> bool {
|
||||||
|
use std::env::current_exe;
|
||||||
|
let is_elevated = privilege::user::privileged();
|
||||||
|
if !is_elevated {
|
||||||
|
let Ok(my_exe) = current_exe() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
||||||
|
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
||||||
|
}
|
||||||
|
is_elevated
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub fn init_launch(_app_handle: &tauri::AppHandle, _enable: bool) -> Result<bool, anyhow::Error> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// init the auto launch
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
|
||||||
|
use std::env::current_exe;
|
||||||
|
let app_exe = current_exe()?;
|
||||||
|
let app_exe = dunce::canonicalize(app_exe)?;
|
||||||
|
let app_name = app_exe
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
|
||||||
|
|
||||||
|
let app_path = app_exe
|
||||||
|
.as_os_str()
|
||||||
|
.to_str()
|
||||||
|
.ok_or(anyhow::anyhow!("failed to get app_path"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let app_path = format!("\"{app_path}\"");
|
||||||
|
|
||||||
|
// use the /Applications/easytier-gui.app
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let app_path = (|| -> Option<String> {
|
||||||
|
let path = std::path::PathBuf::from(&app_path);
|
||||||
|
let path = path.parent()?.parent()?.parent()?;
|
||||||
|
let extension = path.extension()?.to_str()?;
|
||||||
|
match extension == "app" {
|
||||||
|
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||||
|
false => None,
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.unwrap_or(app_path);
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let app_path = {
|
||||||
|
let appimage = _app_handle.env().appimage;
|
||||||
|
appimage
|
||||||
|
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||||
|
.unwrap_or(app_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let auto = AutoLaunchBuilder::new()
|
||||||
|
.set_app_name(app_name)
|
||||||
|
.set_app_path(&app_path)
|
||||||
|
.build()
|
||||||
|
.with_context(|| "failed to build auto launch")?;
|
||||||
|
|
||||||
|
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||||
|
// 避免重复设置登录项
|
||||||
|
let _ = auto.disable();
|
||||||
|
auto.enable()
|
||||||
|
.with_context(|| "failed to enable auto launch")?
|
||||||
|
} else if !enable {
|
||||||
|
let _ = auto.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
let enabled = auto.is_enabled()?;
|
||||||
|
|
||||||
|
Ok(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
if !check_sudo() {
|
||||||
|
use std::process;
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_vpnservice::init())
|
||||||
|
.setup(|app| {
|
||||||
|
// for logging config
|
||||||
|
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let config = TomlConfigLoader::default();
|
||||||
|
config.set_file_logger_config(FileLoggerConfig {
|
||||||
|
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||||
|
level: None,
|
||||||
|
file: None,
|
||||||
|
});
|
||||||
|
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
unsafe { LOGGER_LEVEL_SENDER.replace(logger_reinit) };
|
||||||
|
|
||||||
|
// for tray icon, menu need to be built in js
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
let _tray_menu = TrayIconBuilder::with_id("main")
|
||||||
|
.menu_on_left_click(false)
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
let app = tray.app_handle();
|
||||||
|
toggle_window_visibility(app);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(app)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
parse_network_config,
|
||||||
|
run_network_instance,
|
||||||
|
retain_network_instance,
|
||||||
|
collect_network_infos,
|
||||||
|
get_os_hostname,
|
||||||
|
set_auto_launch_status,
|
||||||
|
set_logging_level,
|
||||||
|
set_tun_fd
|
||||||
|
])
|
||||||
|
.on_window_event(|_win, event| match event {
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||||
|
let _ = _win.hide();
|
||||||
|
api.prevent_close();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
|
@ -1,379 +1,5 @@
|
||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use std::{collections::BTreeMap, env::current_exe, process};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use auto_launch::AutoLaunchBuilder;
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use easytier::{
|
|
||||||
common::config::{
|
|
||||||
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
|
||||||
VpnPortalConfig,
|
|
||||||
},
|
|
||||||
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
|
|
||||||
utils::{self, NewFilterSender},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use tauri::Manager as _;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
|
||||||
enum NetworkingMethod {
|
|
||||||
PublicServer,
|
|
||||||
Manual,
|
|
||||||
Standalone,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NetworkingMethod {
|
|
||||||
fn default() -> Self {
|
|
||||||
NetworkingMethod::PublicServer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
|
||||||
struct NetworkConfig {
|
|
||||||
instance_id: String,
|
|
||||||
|
|
||||||
dhcp: bool,
|
|
||||||
virtual_ipv4: String,
|
|
||||||
hostname: Option<String>,
|
|
||||||
network_name: String,
|
|
||||||
network_secret: String,
|
|
||||||
networking_method: NetworkingMethod,
|
|
||||||
|
|
||||||
public_server_url: String,
|
|
||||||
peer_urls: Vec<String>,
|
|
||||||
|
|
||||||
proxy_cidrs: Vec<String>,
|
|
||||||
|
|
||||||
enable_vpn_portal: bool,
|
|
||||||
vpn_portal_listen_port: i32,
|
|
||||||
vpn_portal_client_network_addr: String,
|
|
||||||
vpn_portal_client_network_len: i32,
|
|
||||||
|
|
||||||
advanced_settings: bool,
|
|
||||||
|
|
||||||
listener_urls: Vec<String>,
|
|
||||||
rpc_port: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkConfig {
|
|
||||||
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
|
||||||
let cfg = TomlConfigLoader::default();
|
|
||||||
cfg.set_id(
|
|
||||||
self.instance_id
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
|
|
||||||
);
|
|
||||||
cfg.set_hostname(self.hostname.clone());
|
|
||||||
cfg.set_dhcp(self.dhcp);
|
|
||||||
cfg.set_inst_name(self.network_name.clone());
|
|
||||||
cfg.set_network_identity(NetworkIdentity::new(
|
|
||||||
self.network_name.clone(),
|
|
||||||
self.network_secret.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
if !self.dhcp {
|
|
||||||
if self.virtual_ipv4.len() > 0 {
|
|
||||||
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
|
|
||||||
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
|
||||||
})?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.networking_method {
|
|
||||||
NetworkingMethod::PublicServer => {
|
|
||||||
cfg.set_peers(vec![PeerConfig {
|
|
||||||
uri: self.public_server_url.parse().with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse public server uri: {}",
|
|
||||||
self.public_server_url
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
NetworkingMethod::Manual => {
|
|
||||||
let mut peers = vec![];
|
|
||||||
for peer_url in self.peer_urls.iter() {
|
|
||||||
if peer_url.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
peers.push(PeerConfig {
|
|
||||||
uri: peer_url
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.set_peers(peers);
|
|
||||||
}
|
|
||||||
NetworkingMethod::Standalone => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut listener_urls = vec![];
|
|
||||||
for listener_url in self.listener_urls.iter() {
|
|
||||||
if listener_url.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
listener_urls.push(
|
|
||||||
listener_url
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
cfg.set_listeners(listener_urls);
|
|
||||||
|
|
||||||
for n in self.proxy_cidrs.iter() {
|
|
||||||
cfg.add_proxy_cidr(
|
|
||||||
n.parse()
|
|
||||||
.with_context(|| format!("failed to parse proxy network: {}", n))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.set_rpc_portal(
|
|
||||||
format!("127.0.0.1:{}", self.rpc_port)
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
if self.enable_vpn_portal {
|
|
||||||
let cidr = format!(
|
|
||||||
"{}/{}",
|
|
||||||
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
|
|
||||||
);
|
|
||||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
|
||||||
client_cidr: cidr
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
|
||||||
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port)
|
|
||||||
.parse()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse vpn portal wireguard listen port. {}",
|
|
||||||
self.vpn_portal_listen_port
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
|
||||||
once_cell::sync::Lazy::new(DashMap::new);
|
|
||||||
|
|
||||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
|
||||||
once_cell::sync::Lazy::new(Default::default);
|
|
||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
|
||||||
#[tauri::command]
|
|
||||||
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
|
||||||
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
|
|
||||||
Ok(toml.dump())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
|
||||||
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
|
|
||||||
return Err("instance already exists".to_string());
|
|
||||||
}
|
|
||||||
let instance_id = cfg.instance_id.clone();
|
|
||||||
|
|
||||||
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
|
||||||
let mut instance = NetworkInstance::new(cfg);
|
|
||||||
instance.start().map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
println!("instance {} started", instance_id);
|
|
||||||
INSTANCE_MAP.insert(instance_id, instance);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn retain_network_instance(instance_ids: Vec<String>) -> Result<(), String> {
|
|
||||||
let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k));
|
|
||||||
println!(
|
|
||||||
"instance {:?} retained",
|
|
||||||
INSTANCE_MAP
|
|
||||||
.iter()
|
|
||||||
.map(|item| item.key().clone())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn collect_network_infos() -> Result<BTreeMap<String, NetworkInstanceRunningInfo>, String> {
|
|
||||||
let mut ret = BTreeMap::new();
|
|
||||||
for instance in INSTANCE_MAP.iter() {
|
|
||||||
if let Some(info) = instance.get_running_info() {
|
|
||||||
ret.insert(instance.key().clone(), info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn get_os_hostname() -> Result<String, String> {
|
|
||||||
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
|
|
||||||
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn set_logging_level(level: String) -> Result<(), String> {
|
|
||||||
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
|
||||||
sender.send(level).map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
if window.is_visible().unwrap_or_default() {
|
|
||||||
let _ = window.hide();
|
|
||||||
} else {
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_sudo() -> bool {
|
|
||||||
let is_elevated = privilege::user::privileged();
|
|
||||||
if !is_elevated {
|
|
||||||
let Ok(my_exe) = current_exe() else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
|
||||||
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
|
||||||
}
|
|
||||||
is_elevated
|
|
||||||
}
|
|
||||||
|
|
||||||
/// init the auto launch
|
|
||||||
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
|
|
||||||
let app_exe = current_exe()?;
|
|
||||||
let app_exe = dunce::canonicalize(app_exe)?;
|
|
||||||
let app_name = app_exe
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|f| f.to_str())
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
|
|
||||||
|
|
||||||
let app_path = app_exe
|
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.ok_or(anyhow::anyhow!("failed to get app_path"))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let app_path = format!("\"{app_path}\"");
|
|
||||||
|
|
||||||
// use the /Applications/easytier-gui.app
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let app_path = (|| -> Option<String> {
|
|
||||||
let path = std::path::PathBuf::from(&app_path);
|
|
||||||
let path = path.parent()?.parent()?.parent()?;
|
|
||||||
let extension = path.extension()?.to_str()?;
|
|
||||||
match extension == "app" {
|
|
||||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
.unwrap_or(app_path);
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let app_path = {
|
|
||||||
let appimage = _app_handle.env().appimage;
|
|
||||||
appimage
|
|
||||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
|
||||||
.unwrap_or(app_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
let auto = AutoLaunchBuilder::new()
|
|
||||||
.set_app_name(app_name)
|
|
||||||
.set_app_path(&app_path)
|
|
||||||
.build()
|
|
||||||
.with_context(|| "failed to build auto launch")?;
|
|
||||||
|
|
||||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
|
||||||
// 避免重复设置登录项
|
|
||||||
let _ = auto.disable();
|
|
||||||
auto.enable()
|
|
||||||
.with_context(|| "failed to enable auto launch")?
|
|
||||||
} else if !enable {
|
|
||||||
let _ = auto.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
let enabled = auto.is_enabled()?;
|
|
||||||
|
|
||||||
Ok(enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if !check_sudo() {
|
app_lib::run();
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
|
||||||
.plugin(tauri_plugin_process::init())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.setup(|app| {
|
|
||||||
// for logging config
|
|
||||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
config.set_file_logger_config(FileLoggerConfig {
|
|
||||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
|
||||||
level: None,
|
|
||||||
file: None,
|
|
||||||
});
|
|
||||||
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
unsafe { LOGGER_LEVEL_SENDER.replace(logger_reinit) };
|
|
||||||
|
|
||||||
// for tray icon, menu need to be built in js
|
|
||||||
let _tray_menu = TrayIconBuilder::with_id("main")
|
|
||||||
.menu_on_left_click(false)
|
|
||||||
.on_tray_icon_event(|tray, event| {
|
|
||||||
if let TrayIconEvent::Click {
|
|
||||||
button: MouseButton::Left,
|
|
||||||
button_state: MouseButtonState::Up,
|
|
||||||
..
|
|
||||||
} = event
|
|
||||||
{
|
|
||||||
let app = tray.app_handle();
|
|
||||||
toggle_window_visibility(app);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build(app)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
parse_network_config,
|
|
||||||
run_network_instance,
|
|
||||||
retain_network_instance,
|
|
||||||
collect_network_infos,
|
|
||||||
get_os_hostname,
|
|
||||||
set_auto_launch_status,
|
|
||||||
set_logging_level
|
|
||||||
])
|
|
||||||
.on_window_event(|win, event| match event {
|
|
||||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
||||||
let _ = win.hide();
|
|
||||||
api.prevent_close();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
})
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"identifier": "com.kkrainbow.easyiter-client",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {},
|
"plugins": {},
|
||||||
"app": {
|
"app": {
|
||||||
"trayIcon": {
|
"trayIcon": {
|
||||||
|
|
10
easytier-gui/src/auto-imports.d.ts
vendored
|
@ -26,6 +26,8 @@ declare global {
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
|
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
||||||
|
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
@ -55,6 +57,7 @@ declare global {
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
|
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
|
||||||
|
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: typeof import('vue')['provide']
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: typeof import('vue')['reactive']
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
@ -69,6 +72,7 @@ declare global {
|
||||||
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
||||||
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
|
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
|
||||||
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
|
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
|
||||||
|
const setTunFd: typeof import('./composables/network')['setTunFd']
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
@ -125,6 +129,7 @@ declare module 'vue' {
|
||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
@ -154,6 +159,7 @@ declare module 'vue' {
|
||||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||||
|
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
@ -168,6 +174,7 @@ declare module 'vue' {
|
||||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||||
|
readonly setTunFd: UnwrapRef<typeof import('./composables/network')['setTunFd']>
|
||||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
@ -217,6 +224,7 @@ declare module '@vue/runtime-core' {
|
||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
@ -246,6 +254,7 @@ declare module '@vue/runtime-core' {
|
||||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||||
|
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
@ -260,6 +269,7 @@ declare module '@vue/runtime-core' {
|
||||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||||
|
readonly setTunFd: UnwrapRef<typeof import('./composables/network')['setTunFd']>
|
||||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { getOsHostname } from '~/composables/network'
|
||||||
import { NetworkingMethod } from '~/types/network'
|
import { NetworkingMethod } from '~/types/network'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
import { ping } from 'tauri-plugin-vpnservice-api'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
configInvalid?: boolean
|
configInvalid?: boolean
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
|
@ -50,6 +52,7 @@ const osHostname = ref<string>('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
osHostname.value = await getOsHostname()
|
osHostname.value = await getOsHostname()
|
||||||
|
osHostname.value = await ping('ffdklsajflkdsjl') || ''
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -75,10 +78,8 @@ onMounted(async () => {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputText
|
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||||
id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
aria-describedby="virtual_ipv4-help" />
|
||||||
aria-describedby="virtual_ipv4-help"
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span>/24</span>
|
<span>/24</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
@ -93,10 +94,8 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||||
<InputText
|
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||||
id="network_secret" v-model="curNetwork.network_secret"
|
aria-describedby=" network_secret-help" />
|
||||||
aria-describedby=" network_secret-help"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -104,21 +103,15 @@ onMounted(async () => {
|
||||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
<label for="nm">{{ t('networking_method') }}</label>
|
<label for="nm">{{ t('networking_method') }}</label>
|
||||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||||
<Dropdown
|
<Dropdown v-model="curNetwork.networking_method" :options="networking_methods" option-label="label"
|
||||||
v-model="curNetwork.networking_method" :options="networking_methods" option-label="label"
|
option-value="value" placeholder="Select Method" class="" />
|
||||||
option-value="value" placeholder="Select Method" class=""
|
<Chips v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||||
/>
|
|
||||||
<Chips
|
|
||||||
v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
|
||||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||||
separator=" " class="grow"
|
separator=" " class="grow" />
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||||
v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
|
||||||
v-model="curNetwork.public_server_url" :editable="true" class="grow"
|
v-model="curNetwork.public_server_url" :editable="true" class="grow"
|
||||||
:options="presetPublicServers"
|
:options="presetPublicServers" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -132,20 +125,16 @@ onMounted(async () => {
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
<label for="hostname">{{ t('hostname') }}</label>
|
<label for="hostname">{{ t('hostname') }}</label>
|
||||||
<InputText
|
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||||
id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
|
||||||
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
<div class="flex flex-column gap-2 grow p-fluid">
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
<label for="username">{{ t('proxy_cidrs') }}</label>
|
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||||
<Chips
|
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
|
||||||
id="chips" v-model="curNetwork.proxy_cidrs"
|
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
|
||||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -153,25 +142,19 @@ onMounted(async () => {
|
||||||
<div class="flex flex-column gap-2 grow">
|
<div class="flex flex-column gap-2 grow">
|
||||||
<label for="username">VPN Portal</label>
|
<label for="username">VPN Portal</label>
|
||||||
<div class="items-center flex flex-row gap-x-4">
|
<div class="items-center flex flex-row gap-x-4">
|
||||||
<ToggleButton
|
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
:on-label="t('off_text')" :off-label="t('on_text')" />
|
||||||
:on-label="t('off_text')" :off-label="t('on_text')"
|
|
||||||
/>
|
|
||||||
<div v-if="curNetwork.enable_vpn_portal" class="grow">
|
<div v-if="curNetwork.enable_vpn_portal" class="grow">
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputText
|
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||||
v-model="curNetwork.vpn_portal_client_network_addr"
|
:placeholder="t('vpn_portal_client_network')" />
|
||||||
:placeholder="t('vpn_portal_client_network')"
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<InputNumber
|
<InputNumber v-if="curNetwork.enable_vpn_portal" v-model="curNetwork.vpn_portal_listen_port"
|
||||||
v-if="curNetwork.enable_vpn_portal" v-model="curNetwork.vpn_portal_listen_port"
|
:placeholder="t('vpn_portal_listen_port')" class="" :format="false" :min="0" :max="65535" />
|
||||||
:placeholder="t('vpn_portal_listen_port')" class="" :format="false" :min="0" :max="65535"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,30 +162,24 @@ onMounted(async () => {
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
<div class="flex flex-column gap-2 grow p-fluid">
|
<div class="flex flex-column gap-2 grow p-fluid">
|
||||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||||
<Chips
|
<Chips id="listener_urls" v-model="curNetwork.listener_urls"
|
||||||
id="listener_urls" v-model="curNetwork.listener_urls"
|
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" separator=" " class="w-full" />
|
||||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" separator=" " class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||||
<InputNumber
|
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
|
||||||
id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
|
:format="false" :min="0" :max="65535" />
|
||||||
:format="false" :min="0" :max="65535"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<div class="flex pt-4 justify-content-center">
|
<div class="flex pt-4 justify-content-center">
|
||||||
<Button
|
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||||
:label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
@click="$emit('runNetwork', curNetwork)" />
|
||||||
@click="$emit('runNetwork', curNetwork)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
129
easytier-gui/src/composables/mobile_vpn.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import { addPluginListener } from '@tauri-apps/api/core';
|
||||||
|
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api';
|
||||||
|
|
||||||
|
const networkStore = useNetworkStore()
|
||||||
|
|
||||||
|
interface vpnStatus {
|
||||||
|
running: boolean
|
||||||
|
ipv4Addr: string | null | undefined
|
||||||
|
ipv4Cidr: number | null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
var curVpnStatus: vpnStatus = {
|
||||||
|
running: false,
|
||||||
|
ipv4Addr: undefined,
|
||||||
|
ipv4Cidr: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||||
|
let start_time = Date.now()
|
||||||
|
while (curVpnStatus.running !== target_status) {
|
||||||
|
if (Date.now() - start_time > timeout_sec * 1000) {
|
||||||
|
throw new Error('wait vpn status timeout')
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doStopVpn() {
|
||||||
|
if (!curVpnStatus.running) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('stop vpn')
|
||||||
|
let stop_ret = await stop_vpn()
|
||||||
|
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||||
|
await waitVpnStatus(false, 3)
|
||||||
|
|
||||||
|
curVpnStatus.ipv4Addr = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doStartVpn(ipv4Addr: string, cidr: number) {
|
||||||
|
if (curVpnStatus.running) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('start vpn')
|
||||||
|
let start_ret = await start_vpn({
|
||||||
|
"ipv4Addr": ipv4Addr + '/' + cidr,
|
||||||
|
"routes": ["0.0.0.0/0"],
|
||||||
|
"disallowedApplications": ["com.kkrainbow.easytier"],
|
||||||
|
"mtu": 1300,
|
||||||
|
});
|
||||||
|
if (start_ret?.errorMsg?.length) {
|
||||||
|
throw new Error(start_ret.errorMsg)
|
||||||
|
}
|
||||||
|
await waitVpnStatus(true, 3)
|
||||||
|
|
||||||
|
curVpnStatus.ipv4Addr = ipv4Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onVpnServiceStart(payload: any) {
|
||||||
|
console.log('vpn service start', JSON.stringify(payload))
|
||||||
|
curVpnStatus.running = true
|
||||||
|
if (payload.fd) {
|
||||||
|
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onVpnServiceStop(payload: any) {
|
||||||
|
console.log('vpn service stop', JSON.stringify(payload))
|
||||||
|
curVpnStatus.running = false
|
||||||
|
networkStore.clearNetworkInstances()
|
||||||
|
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerVpnServiceListener() {
|
||||||
|
console.log('register vpn service listener')
|
||||||
|
await addPluginListener(
|
||||||
|
'vpnservice',
|
||||||
|
'vpn_service_start',
|
||||||
|
onVpnServiceStart
|
||||||
|
)
|
||||||
|
|
||||||
|
await addPluginListener(
|
||||||
|
'vpnservice',
|
||||||
|
'vpn_service_stop',
|
||||||
|
onVpnServiceStop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watchNetworkInstance() {
|
||||||
|
networkStore.$subscribe(async () => {
|
||||||
|
let insts = networkStore.networkInstanceIds
|
||||||
|
if (!insts) {
|
||||||
|
await doStopVpn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||||
|
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||||
|
await doStopVpn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
||||||
|
if (virtual_ip !== curVpnStatus.ipv4Addr) {
|
||||||
|
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||||
|
await doStopVpn()
|
||||||
|
if (virtual_ip.length > 0) {
|
||||||
|
await doStartVpn(virtual_ip, 24)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initMobileVpnService() {
|
||||||
|
await registerVpnServiceListener()
|
||||||
|
await watchNetworkInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareVpnService() {
|
||||||
|
console.log('prepare vpn')
|
||||||
|
let prepare_ret = await prepare_vpn()
|
||||||
|
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||||
|
if (prepare_ret?.errorMsg?.length) {
|
||||||
|
throw new Error(prepare_ret.errorMsg)
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,3 +29,7 @@ export async function setAutoLaunchStatus(enable: boolean) {
|
||||||
export async function setLoggingLevel(level: string) {
|
export async function setLoggingLevel(level: string) {
|
||||||
return await invoke('set_logging_level', { level })
|
return await invoke('set_logging_level', { level })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setTunFd(instanceId: string, fd: number) {
|
||||||
|
return await invoke('set_tun_fd', { instanceId, fd })
|
||||||
|
}
|
||||||
|
|
|
@ -15,20 +15,26 @@ async function toggleVisibility() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function useTray(init: boolean = false) {
|
export async function useTray(init: boolean = false) {
|
||||||
let tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
let tray;
|
||||||
if (!tray) {
|
try {
|
||||||
tray = await TrayIcon.new({
|
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
||||||
tooltip: `EasyTier\n${pkg.version}`,
|
if (!tray) {
|
||||||
title: `EasyTier\n${pkg.version}`,
|
tray = await TrayIcon.new({
|
||||||
id: DEFAULT_TRAY_NAME,
|
tooltip: `EasyTier\n${pkg.version}`,
|
||||||
menu: await Menu.new({
|
title: `EasyTier\n${pkg.version}`,
|
||||||
id: 'main',
|
id: DEFAULT_TRAY_NAME,
|
||||||
items: await generateMenuItem(),
|
menu: await Menu.new({
|
||||||
}),
|
id: 'main',
|
||||||
action: async () => {
|
items: await generateMenuItem(),
|
||||||
toggleVisibility()
|
}),
|
||||||
}
|
action: async () => {
|
||||||
})
|
toggleVisibility()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error while creating tray icon:', error)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (init) {
|
if (init) {
|
||||||
|
@ -63,13 +69,14 @@ export async function MenuItemShow(text: string) {
|
||||||
id: 'show',
|
id: 'show',
|
||||||
text,
|
text,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await toggleVisibility();
|
await toggleVisibility();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
||||||
const tray = await useTray()
|
const tray = await useTray()
|
||||||
|
if (!tray) return
|
||||||
const menu = await Menu.new({
|
const menu = await Menu.new({
|
||||||
id: 'main',
|
id: 'main',
|
||||||
items: items || await generateMenuItem(),
|
items: items || await generateMenuItem(),
|
||||||
|
@ -79,12 +86,14 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
|
||||||
|
|
||||||
export async function setTrayRunState(isRunning: boolean = false) {
|
export async function setTrayRunState(isRunning: boolean = false) {
|
||||||
const tray = await useTray()
|
const tray = await useTray()
|
||||||
|
if (!tray) return
|
||||||
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
|
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTrayTooltip(tooltip: string) {
|
export async function setTrayTooltip(tooltip: string) {
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
const tray = await useTray()
|
const tray = await useTray()
|
||||||
|
if (!tray) return
|
||||||
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
|
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||||
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
|
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { open } from '@tauri-apps/plugin-shell';
|
||||||
import { appLogDir } from '@tauri-apps/api/path'
|
import { appLogDir } from '@tauri-apps/api/path'
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { useTray } from '~/composables/tray';
|
import { useTray } from '~/composables/tray';
|
||||||
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
@ -82,8 +83,13 @@ networkStore.$subscribe(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||||
cb()
|
if (type() === 'android') {
|
||||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
await prepareVpnService()
|
||||||
|
networkStore.clearNetworkInstances()
|
||||||
|
} else {
|
||||||
|
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||||
|
}
|
||||||
|
|
||||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||||
networkStore.addNetworkInstance(cfg.instance_id)
|
networkStore.addNetworkInstance(cfg.instance_id)
|
||||||
|
|
||||||
|
@ -94,6 +100,8 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||||
// console.error(e)
|
// console.error(e)
|
||||||
toast.add({ severity: 'info', detail: e })
|
toast.add({ severity: 'info', detail: e })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||||
|
@ -112,9 +120,9 @@ onMounted(async () => {
|
||||||
intervalId = window.setInterval(async () => {
|
intervalId = window.setInterval(async () => {
|
||||||
await updateNetworkInfos()
|
await updateNetworkInfos()
|
||||||
}, 500)
|
}, 500)
|
||||||
await setTrayMenu([
|
await setTrayMenu([
|
||||||
await MenuItemExit(t('tray.exit')),
|
await MenuItemExit(t('tray.exit')),
|
||||||
await MenuItemShow(t('tray.show'))
|
await MenuItemShow(t('tray.show'))
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
onUnmounted(() => clearInterval(intervalId))
|
onUnmounted(() => clearInterval(intervalId))
|
||||||
|
@ -132,9 +140,9 @@ const setting_menu_items = ref([
|
||||||
icon: 'pi pi-language',
|
icon: 'pi pi-language',
|
||||||
command: async () => {
|
command: async () => {
|
||||||
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
||||||
await setTrayMenu([
|
await setTrayMenu([
|
||||||
await MenuItemExit(t('tray.exit')),
|
await MenuItemExit(t('tray.exit')),
|
||||||
await MenuItemShow(t('tray.show'))
|
await MenuItemShow(t('tray.show'))
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -206,11 +214,15 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (type() === 'android') {
|
||||||
|
await initMobileVpnService()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function isRunning(id: string) {
|
function isRunning(id: string) {
|
||||||
return networkStore.networkInstanceIds.includes(id)
|
return networkStore.networkInstanceIds.includes(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -233,14 +245,13 @@ function isRunning(id: string) {
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<template #start>
|
<template #start>
|
||||||
<div class="flex align-items-center gap-2">
|
<div class="flex align-items-center">
|
||||||
<Button icon="pi pi-plus" class="mr-2" severity="primary" :label="t('add_new_network')"
|
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
||||||
@click="addNewNetwork" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #center>
|
<template #center>
|
||||||
<div class="min-w-80 mr-20">
|
<div class="min-w-40">
|
||||||
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||||
:placeholder="t('select_network')" class="w-full">
|
:placeholder="t('select_network')" class="w-full">
|
||||||
<template #value="slotProps">
|
<template #value="slotProps">
|
||||||
|
@ -275,32 +286,32 @@ function isRunning(id: string) {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #end>
|
<template #end>
|
||||||
<Button icon="pi pi-cog" class="mr-2" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||||
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
||||||
</template>
|
</template>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel class="h-full overflow-y-auto" >
|
<Panel class="h-full overflow-y-auto">
|
||||||
<Stepper :value="activeStep">
|
<Stepper :value="activeStep">
|
||||||
<StepList value="1">
|
<StepList value="1">
|
||||||
<Step value="1">{{ t('config_network') }}</Step>
|
<Step value="1">{{ t('config_network') }}</Step>
|
||||||
<Step value="2">{{ t('running') }}</Step>
|
<Step value="2">{{ t('running') }}</Step>
|
||||||
</StepList>
|
</StepList>
|
||||||
<StepPanels value="1">
|
<StepPanels value="1">
|
||||||
<StepPanel v-slot="{ activateCallback = (s: string) => {} } = {}" value="1">
|
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||||
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||||
@run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
@run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||||
</StepPanel>
|
</StepPanel>
|
||||||
<StepPanel v-slot="{ activateCallback = (s: string) => {} } = {}" value="2">
|
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
||||||
<div class="flex flex-column">
|
<div class="flex flex-column">
|
||||||
<Status :instance-id="networkStore.curNetworkId" />
|
<Status :instance-id="networkStore.curNetworkId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-4 justify-content-center">
|
<div class="flex pt-4 justify-content-center">
|
||||||
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
||||||
</div>
|
</div>
|
||||||
</StepPanel>
|
</StepPanel>
|
||||||
</StepPanels>
|
</StepPanels>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
|
|
@ -60,6 +60,10 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearNetworkInstances() {
|
||||||
|
this.instances = {}
|
||||||
|
},
|
||||||
|
|
||||||
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
|
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
|
||||||
this.networkInfos = networkInfos
|
this.networkInfos = networkInfos
|
||||||
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
for (const [instanceId, info] of Object.entries(networkInfos)) {
|
||||||
|
|
|
@ -87,10 +87,15 @@ export default defineConfig(async () => ({
|
||||||
// 2. tauri expects a fixed port, fail if that port is not available
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
server: {
|
server: {
|
||||||
port: 1420,
|
port: 1420,
|
||||||
|
host: '10.147.223.128',
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
watch: {
|
watch: {
|
||||||
// 3. tell vite to ignore watching `src-tauri`
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
ignored: ['**/src-tauri/**'],
|
ignored: ['**/src-tauri/**'],
|
||||||
},
|
},
|
||||||
|
hmr: {
|
||||||
|
host: "10.147.223.128",
|
||||||
|
protocol: "ws",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -84,7 +84,7 @@ http = { version = "1", default-features = false, features = [
|
||||||
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
||||||
|
|
||||||
# for tap device
|
# for tap device
|
||||||
tun = { package = "tun-easytier", version = "0.6.1", features = [
|
tun = { package = "tun-easytier", version = "0.7.1", features = [
|
||||||
"async",
|
"async",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
# for net ns
|
# for net ns
|
||||||
|
@ -186,6 +186,8 @@ zip = "0.6.6"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3.0.0"
|
serial_test = "3.0.0"
|
||||||
rstest = "0.18.2"
|
rstest = "0.18.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||||
defguard_wireguard_rs = "0.4.2"
|
defguard_wireguard_rs = "0.4.2"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ pub struct VpnPortalConfig {
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
#[derivative(Default(value = "\"tcp\".to_string()"))]
|
#[derivative(Default(value = "\"tcp\".to_string()"))]
|
||||||
pub default_protocol: String,
|
pub default_protocol: String,
|
||||||
#[derivative(Default(value = "\"tun\".to_string()"))]
|
#[derivative(Default(value = "\"\".to_string()"))]
|
||||||
pub dev_name: String,
|
pub dev_name: String,
|
||||||
#[derivative(Default(value = "true"))]
|
#[derivative(Default(value = "true"))]
|
||||||
pub enable_encryption: bool,
|
pub enable_encryption: bool,
|
||||||
|
|
|
@ -9,28 +9,40 @@ use super::error::Error;
|
||||||
pub trait IfConfiguerTrait: Send + Sync {
|
pub trait IfConfiguerTrait: Send + Sync {
|
||||||
async fn add_ipv4_route(
|
async fn add_ipv4_route(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
_name: &str,
|
||||||
address: Ipv4Addr,
|
_address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
_cidr_prefix: u8,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
async fn remove_ipv4_route(
|
async fn remove_ipv4_route(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
_name: &str,
|
||||||
address: Ipv4Addr,
|
_address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
_cidr_prefix: u8,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
async fn add_ipv4_ip(
|
async fn add_ipv4_ip(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
_name: &str,
|
||||||
address: Ipv4Addr,
|
_address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
_cidr_prefix: u8,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error> {
|
||||||
async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error>;
|
Ok(())
|
||||||
async fn remove_ip(&self, name: &str, ip: Option<Ipv4Addr>) -> Result<(), Error>;
|
}
|
||||||
|
async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn remove_ip(&self, _name: &str, _ip: Option<Ipv4Addr>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> {
|
async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
async fn set_mtu(&self, _name: &str, _mtu: u32) -> Result<(), Error>;
|
async fn set_mtu(&self, _name: &str, _mtu: u32) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr {
|
fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr {
|
||||||
|
@ -379,6 +391,10 @@ impl IfConfiguerTrait for WindowsIfConfiger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct DummyIfConfiger {}
|
||||||
|
#[async_trait]
|
||||||
|
impl IfConfiguerTrait for DummyIfConfiger {}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub type IfConfiger = MacIfConfiger;
|
pub type IfConfiger = MacIfConfiger;
|
||||||
|
|
||||||
|
@ -387,3 +403,6 @@ pub type IfConfiger = LinuxIfConfiger;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub type IfConfiger = WindowsIfConfiger;
|
pub type IfConfiger = WindowsIfConfiger;
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||||
|
pub type IfConfiger = DummyIfConfiger;
|
||||||
|
|
|
@ -15,6 +15,13 @@ struct InterfaceFilter {
|
||||||
iface: NetworkInterface,
|
iface: NetworkInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
impl InterfaceFilter {
|
||||||
|
async fn filter_iface(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
impl InterfaceFilter {
|
impl InterfaceFilter {
|
||||||
async fn is_tun_tap_device(&self) -> bool {
|
async fn is_tun_tap_device(&self) -> bool {
|
||||||
|
|
|
@ -24,6 +24,10 @@ async fn set_bind_addr_for_peer_connector(
|
||||||
is_ipv4: bool,
|
is_ipv4: bool,
|
||||||
ip_collector: &Arc<IPCollector>,
|
ip_collector: &Arc<IPCollector>,
|
||||||
) {
|
) {
|
||||||
|
if cfg!(target_os = "android") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let ips = ip_collector.collect_ip_addrs().await;
|
let ips = ip_collector.collect_ip_addrs().await;
|
||||||
if is_ipv4 {
|
if is_ipv4 {
|
||||||
let mut bind_addrs = vec![];
|
let mut bind_addrs = vec![];
|
||||||
|
|
|
@ -172,8 +172,8 @@ and the vpn client is in network of 10.14.14.0/24"
|
||||||
#[arg(long, help = "do not use ipv6", default_value = "false")]
|
#[arg(long, help = "do not use ipv6", default_value = "false")]
|
||||||
disable_ipv6: bool,
|
disable_ipv6: bool,
|
||||||
|
|
||||||
#[arg(long, help = "interface name", default_value = "tun")]
|
#[arg(long, help = "optional tun interface name")]
|
||||||
dev_name: String,
|
dev_name: Option<String>,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
|
@ -427,7 +427,7 @@ impl From<Cli> for TomlConfigLoader {
|
||||||
f.enable_encryption = !cli.disable_encryption;
|
f.enable_encryption = !cli.disable_encryption;
|
||||||
f.enable_ipv6 = !cli.disable_ipv6;
|
f.enable_ipv6 = !cli.disable_ipv6;
|
||||||
f.latency_first = cli.latency_first;
|
f.latency_first = cli.latency_first;
|
||||||
f.dev_name = cli.dev_name;
|
f.dev_name = cli.dev_name.unwrap_or(Default::default());
|
||||||
if let Some(mtu) = cli.mtu {
|
if let Some(mtu) = cli.mtu {
|
||||||
f.mtu = mtu;
|
f.mtu = mtu;
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ impl Instance {
|
||||||
let peer_manager_c = self.peer_manager.clone();
|
let peer_manager_c = self.peer_manager.clone();
|
||||||
let global_ctx_c = self.get_global_ctx();
|
let global_ctx_c = self.get_global_ctx();
|
||||||
let nic_ctx = self.nic_ctx.clone();
|
let nic_ctx = self.nic_ctx.clone();
|
||||||
let peer_packet_receiver = self.peer_packet_receiver.clone();
|
let _peer_packet_receiver = self.peer_packet_receiver.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let default_ipv4_addr = Ipv4Inet::new(Ipv4Addr::new(10, 126, 126, 0), 24).unwrap();
|
let default_ipv4_addr = Ipv4Inet::new(Ipv4Addr::new(10, 126, 126, 0), 24).unwrap();
|
||||||
let mut current_dhcp_ip: Option<Ipv4Inet> = None;
|
let mut current_dhcp_ip: Option<Ipv4Inet> = None;
|
||||||
|
@ -286,26 +286,31 @@ impl Instance {
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut new_nic_ctx = NicCtx::new(
|
|
||||||
global_ctx_c.clone(),
|
#[cfg(not(target_os = "android"))]
|
||||||
&peer_manager_c,
|
{
|
||||||
peer_packet_receiver.clone(),
|
let mut new_nic_ctx = NicCtx::new(
|
||||||
);
|
global_ctx_c.clone(),
|
||||||
if let Err(e) = new_nic_ctx.run(ip.address()).await {
|
&peer_manager_c,
|
||||||
tracing::error!(
|
_peer_packet_receiver.clone(),
|
||||||
?current_dhcp_ip,
|
|
||||||
?candidate_ipv4_addr,
|
|
||||||
?e,
|
|
||||||
"add ip failed"
|
|
||||||
);
|
);
|
||||||
global_ctx_c.set_ipv4(None);
|
if let Err(e) = new_nic_ctx.run(ip.address()).await {
|
||||||
continue;
|
tracing::error!(
|
||||||
|
?current_dhcp_ip,
|
||||||
|
?candidate_ipv4_addr,
|
||||||
|
?e,
|
||||||
|
"add ip failed"
|
||||||
|
);
|
||||||
|
global_ctx_c.set_ipv4(None);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
current_dhcp_ip = Some(ip);
|
current_dhcp_ip = Some(ip);
|
||||||
global_ctx_c.set_ipv4(Some(ip.address()));
|
global_ctx_c.set_ipv4(Some(ip.address()));
|
||||||
global_ctx_c
|
global_ctx_c
|
||||||
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip.address())));
|
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip.address())));
|
||||||
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
|
||||||
} else {
|
} else {
|
||||||
current_dhcp_ip = None;
|
current_dhcp_ip = None;
|
||||||
global_ctx_c.set_ipv4(None);
|
global_ctx_c.set_ipv4(None);
|
||||||
|
@ -326,14 +331,17 @@ impl Instance {
|
||||||
|
|
||||||
if self.global_ctx.config.get_flags().no_tun {
|
if self.global_ctx.config.get_flags().no_tun {
|
||||||
self.peer_packet_receiver.lock().await.close();
|
self.peer_packet_receiver.lock().await.close();
|
||||||
} else if let Some(ipv4_addr) = self.global_ctx.get_ipv4() {
|
} else {
|
||||||
let mut new_nic_ctx = NicCtx::new(
|
#[cfg(not(target_os = "android"))]
|
||||||
self.global_ctx.clone(),
|
if let Some(ipv4_addr) = self.global_ctx.get_ipv4() {
|
||||||
&self.peer_manager,
|
let mut new_nic_ctx = NicCtx::new(
|
||||||
self.peer_packet_receiver.clone(),
|
self.global_ctx.clone(),
|
||||||
);
|
&self.peer_manager,
|
||||||
new_nic_ctx.run(ipv4_addr).await?;
|
self.peer_packet_receiver.clone(),
|
||||||
Self::use_new_nic_ctx(self.nic_ctx.clone(), new_nic_ctx).await;
|
);
|
||||||
|
new_nic_ctx.run(ipv4_addr).await?;
|
||||||
|
Self::use_new_nic_ctx(self.nic_ctx.clone(), new_nic_ctx).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.global_ctx.config.get_dhcp() {
|
if self.global_ctx.config.get_dhcp() {
|
||||||
|
@ -506,4 +514,38 @@ impl Instance {
|
||||||
pub fn get_vpn_portal_inst(&self) -> Arc<Mutex<Box<dyn VpnPortal>>> {
|
pub fn get_vpn_portal_inst(&self) -> Arc<Mutex<Box<dyn VpnPortal>>> {
|
||||||
self.vpn_portal.clone()
|
self.vpn_portal.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_nic_ctx(&self) -> ArcNicCtx {
|
||||||
|
self.nic_ctx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_peer_packet_receiver(&self) -> Arc<Mutex<PacketRecvChanReceiver>> {
|
||||||
|
self.peer_packet_receiver.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub async fn setup_nic_ctx_for_android(
|
||||||
|
nic_ctx: ArcNicCtx,
|
||||||
|
global_ctx: ArcGlobalCtx,
|
||||||
|
peer_manager: Arc<PeerManager>,
|
||||||
|
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||||
|
fd: i32,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
println!("setup_nic_ctx_for_android, fd: {}", fd);
|
||||||
|
Self::clear_nic_ctx(nic_ctx.clone()).await;
|
||||||
|
if fd <= 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut new_nic_ctx = NicCtx::new(
|
||||||
|
global_ctx.clone(),
|
||||||
|
&peer_manager,
|
||||||
|
peer_packet_receiver.clone(),
|
||||||
|
);
|
||||||
|
new_nic_ctx
|
||||||
|
.run_for_android(fd)
|
||||||
|
.await
|
||||||
|
.with_context(|| "add ip failed")?;
|
||||||
|
Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,7 +274,9 @@ impl VirtualNic {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||||
config.name(format!("{}{}", dev_name, 0));
|
if !dev_name.is_empty() {
|
||||||
|
config.name(format!("{}", dev_name));
|
||||||
|
}
|
||||||
config.platform(|config| {
|
config.platform(|config| {
|
||||||
// detect protocol by ourselves for cross platform
|
// detect protocol by ourselves for cross platform
|
||||||
config.packet_information(false);
|
config.packet_information(false);
|
||||||
|
@ -318,7 +320,37 @@ impl VirtualNic {
|
||||||
Ok(create_as_async(&config)?)
|
Ok(create_as_async(&config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_dev_ret_err(&mut self) -> Result<Box<dyn Tunnel>, Error> {
|
#[cfg(target_os = "android")]
|
||||||
|
pub async fn create_dev_for_android(
|
||||||
|
&mut self,
|
||||||
|
tun_fd: std::os::fd::RawFd,
|
||||||
|
) -> Result<Box<dyn Tunnel>, Error> {
|
||||||
|
println!("tun_fd: {}", tun_fd);
|
||||||
|
let mut config = Configuration::default();
|
||||||
|
config.layer(Layer::L3);
|
||||||
|
config.raw_fd(tun_fd);
|
||||||
|
config.platform(|config| {
|
||||||
|
config.no_close_fd_on_drop(true);
|
||||||
|
});
|
||||||
|
config.up();
|
||||||
|
|
||||||
|
let dev = create_as_async(&config)?;
|
||||||
|
let (a, b) = BiLock::new(dev);
|
||||||
|
let ft = TunnelWrapper::new(
|
||||||
|
TunStream::new(a, false),
|
||||||
|
FramedWriter::new_with_converter(
|
||||||
|
TunAsyncWrite { l: b },
|
||||||
|
TunZCPacketToBytes::new(false),
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.ifname = Some(format!("tunfd_{}", tun_fd));
|
||||||
|
|
||||||
|
Ok(Box::new(ft))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_dev(&mut self) -> Result<Box<dyn Tunnel>, Error> {
|
||||||
let dev = self.create_tun().await?;
|
let dev = self.create_tun().await?;
|
||||||
let ifname = dev.get_ref().name()?;
|
let ifname = dev.get_ref().name()?;
|
||||||
self.ifcfg.wait_interface_show(ifname.as_str()).await?;
|
self.ifcfg.wait_interface_show(ifname.as_str()).await?;
|
||||||
|
@ -351,10 +383,6 @@ impl VirtualNic {
|
||||||
Ok(Box::new(ft))
|
Ok(Box::new(ft))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_dev(&mut self) -> Result<Box<dyn Tunnel>, Error> {
|
|
||||||
self.create_dev_ret_err().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ifname(&self) -> &str {
|
pub fn ifname(&self) -> &str {
|
||||||
self.ifname.as_ref().unwrap().as_str()
|
self.ifname.as_ref().unwrap().as_str()
|
||||||
}
|
}
|
||||||
|
@ -589,6 +617,24 @@ impl NicCtx {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub async fn run_for_android(&mut self, tun_fd: std::os::fd::RawFd) -> Result<(), Error> {
|
||||||
|
let tunnel = {
|
||||||
|
let mut nic = self.nic.lock().await;
|
||||||
|
let ret = nic.create_dev_for_android(tun_fd).await?;
|
||||||
|
self.global_ctx
|
||||||
|
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
|
||||||
|
ret
|
||||||
|
};
|
||||||
|
|
||||||
|
let (stream, sink) = tunnel.split();
|
||||||
|
|
||||||
|
self.do_forward_nic_to_peers(stream)?;
|
||||||
|
self.do_forward_peers_to_nic(sink);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -19,6 +19,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct MyNodeInfo {
|
pub struct MyNodeInfo {
|
||||||
|
@ -35,6 +36,7 @@ struct EasyTierData {
|
||||||
node_info: Arc<RwLock<MyNodeInfo>>,
|
node_info: Arc<RwLock<MyNodeInfo>>,
|
||||||
routes: Arc<RwLock<Vec<Route>>>,
|
routes: Arc<RwLock<Vec<Route>>>,
|
||||||
peers: Arc<RwLock<Vec<PeerInfo>>>,
|
peers: Arc<RwLock<Vec<PeerInfo>>>,
|
||||||
|
tun_fd: Arc<RwLock<Option<i32>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EasyTierLauncher {
|
pub struct EasyTierLauncher {
|
||||||
|
@ -69,6 +71,40 @@ impl EasyTierLauncher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
async fn run_routine_for_android(
|
||||||
|
instance: &Instance,
|
||||||
|
data: &EasyTierData,
|
||||||
|
tasks: &mut JoinSet<()>,
|
||||||
|
) {
|
||||||
|
let global_ctx = instance.get_global_ctx();
|
||||||
|
let peer_mgr = instance.get_peer_manager();
|
||||||
|
let nic_ctx = instance.get_nic_ctx();
|
||||||
|
let peer_packet_receiver = instance.get_peer_packet_receiver();
|
||||||
|
let arc_tun_fd = data.tun_fd.clone();
|
||||||
|
|
||||||
|
tasks.spawn(async move {
|
||||||
|
let mut old_tun_fd = arc_tun_fd.read().unwrap().clone();
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
let tun_fd = arc_tun_fd.read().unwrap().clone();
|
||||||
|
if tun_fd != old_tun_fd && tun_fd.is_some() {
|
||||||
|
let res = Instance::setup_nic_ctx_for_android(
|
||||||
|
nic_ctx.clone(),
|
||||||
|
global_ctx.clone(),
|
||||||
|
peer_mgr.clone(),
|
||||||
|
peer_packet_receiver.clone(),
|
||||||
|
tun_fd.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if res.is_ok() {
|
||||||
|
old_tun_fd = tun_fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn easytier_routine(
|
async fn easytier_routine(
|
||||||
cfg: TomlConfigLoader,
|
cfg: TomlConfigLoader,
|
||||||
stop_signal: Arc<tokio::sync::Notify>,
|
stop_signal: Arc<tokio::sync::Notify>,
|
||||||
|
@ -77,10 +113,12 @@ impl EasyTierLauncher {
|
||||||
let mut instance = Instance::new(cfg);
|
let mut instance = Instance::new(cfg);
|
||||||
let peer_mgr = instance.get_peer_manager();
|
let peer_mgr = instance.get_peer_manager();
|
||||||
|
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
|
||||||
// Subscribe to global context events
|
// Subscribe to global context events
|
||||||
let global_ctx = instance.get_global_ctx();
|
let global_ctx = instance.get_global_ctx();
|
||||||
let data_c = data.clone();
|
let data_c = data.clone();
|
||||||
tokio::spawn(async move {
|
tasks.spawn(async move {
|
||||||
let mut receiver = global_ctx.subscribe();
|
let mut receiver = global_ctx.subscribe();
|
||||||
while let Ok(event) = receiver.recv().await {
|
while let Ok(event) = receiver.recv().await {
|
||||||
Self::handle_easytier_event(event, data_c.clone()).await;
|
Self::handle_easytier_event(event, data_c.clone()).await;
|
||||||
|
@ -92,7 +130,7 @@ impl EasyTierLauncher {
|
||||||
let global_ctx_c = instance.get_global_ctx();
|
let global_ctx_c = instance.get_global_ctx();
|
||||||
let peer_mgr_c = peer_mgr.clone();
|
let peer_mgr_c = peer_mgr.clone();
|
||||||
let vpn_portal = instance.get_vpn_portal_inst();
|
let vpn_portal = instance.get_vpn_portal_inst();
|
||||||
tokio::spawn(async move {
|
tasks.spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let node_info = MyNodeInfo {
|
let node_info = MyNodeInfo {
|
||||||
virtual_ipv4: global_ctx_c
|
virtual_ipv4: global_ctx_c
|
||||||
|
@ -123,8 +161,15 @@ impl EasyTierLauncher {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
Self::run_routine_for_android(&instance, &data, &mut tasks).await;
|
||||||
|
|
||||||
instance.run().await?;
|
instance.run().await?;
|
||||||
stop_signal.notified().await;
|
stop_signal.notified().await;
|
||||||
|
|
||||||
|
tasks.abort_all();
|
||||||
|
drop(tasks);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,6 +311,12 @@ impl NetworkInstance {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_tun_fd(&mut self, tun_fd: i32) {
|
||||||
|
if let Some(launcher) = self.launcher.as_ref() {
|
||||||
|
launcher.data.tun_fd.write().unwrap().replace(tun_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(&mut self) -> Result<(), anyhow::Error> {
|
pub fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
if self.is_easytier_running() {
|
if self.is_easytier_running() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::common::PeerId;
|
use crate::common::PeerId;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
mod three_node;
|
mod three_node;
|
||||||
|
|
||||||
pub fn get_guest_veth_name(net_ns: &str) -> &str {
|
pub fn get_guest_veth_name(net_ns: &str) -> &str {
|
||||||
|
|
32
tauri-plugin-vpnservice/.github/workflows/audit.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
name: Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/audit.yml"
|
||||||
|
- "**/Cargo.lock"
|
||||||
|
- "**/Cargo.toml"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/audit.yml"
|
||||||
|
- "**/Cargo.lock"
|
||||||
|
- "**/Cargo.toml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: rustsec/audit-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
53
tauri-plugin-vpnservice/.github/workflows/clippy.yml
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
name: Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/check.yml"
|
||||||
|
- "**/*.rs"
|
||||||
|
- "**/Cargo.toml"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/check.yml"
|
||||||
|
- "**/*.rs"
|
||||||
|
- "**/Cargo.toml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fmt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: clippy
|
||||||
|
- name: install webkit2gtk
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y webkit2gtk-4.1
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo clippy --all-targets --all-features -- -D warnings
|
33
tauri-plugin-vpnservice/.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: install webkit2gtk
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y webkit2gtk-4.1
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo test --all-targets --all-features -- -D warnings
|
17
tauri-plugin-vpnservice/.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/.vs
|
||||||
|
.DS_Store
|
||||||
|
.Thumbs.db
|
||||||
|
*.sublime*
|
||||||
|
.idea/
|
||||||
|
debug.log
|
||||||
|
package-lock.json
|
||||||
|
.vscode/settings.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
/.tauri
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
dist-js
|
||||||
|
dist
|
17
tauri-plugin-vpnservice/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "tauri-plugin-vpnservice"
|
||||||
|
version = "0.0.0"
|
||||||
|
authors = [ "You" ]
|
||||||
|
description = ""
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.70"
|
||||||
|
exclude = ["/examples", "/webview-dist", "/webview-src", "/node_modules"]
|
||||||
|
links = "tauri-plugin-vpnservice"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2.0.0-beta.23" }
|
||||||
|
serde = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-plugin = { version = "2.0.0-beta.18", features = ["build"] }
|
1
tauri-plugin-vpnservice/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Tauri Plugin vpnservice
|
2
tauri-plugin-vpnservice/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/build
|
||||||
|
/.tauri
|
45
tauri-plugin-vpnservice/android/build.gradle.kts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.plugin.vpnservice"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 34
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.9.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||||
|
implementation("com.google.android.material:material:1.7.0")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
implementation(project(":tauri-android"))
|
||||||
|
}
|
21
tauri-plugin-vpnservice/android/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
2
tauri-plugin-vpnservice/android/settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
include ':tauri-android'
|
||||||
|
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.plugin.vpnservice
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("com.plugin.vpnservice", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
11
tauri-plugin-vpnservice/android/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
10
tauri-plugin-vpnservice/android/src/main/java/Example.kt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package com.plugin.vpnservice
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
class Example {
|
||||||
|
fun pong(value: String): String {
|
||||||
|
Log.i("Pong", value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
115
tauri-plugin-vpnservice/android/src/main/java/TauriVpnService.kt
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package com.plugin.vpnservice
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.net.IpPrefix
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.Bundle
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.util.Arrays
|
||||||
|
|
||||||
|
import app.tauri.plugin.JSObject
|
||||||
|
|
||||||
|
fun stringToIpPrefix(ipPrefixString: String): IpPrefix {
|
||||||
|
val parts = ipPrefixString.split("/")
|
||||||
|
if (parts.size != 2) throw IllegalArgumentException("Invalid IP prefix string")
|
||||||
|
|
||||||
|
val address = InetAddress.getByName(parts[0])
|
||||||
|
val prefixLength = parts[1].toInt()
|
||||||
|
|
||||||
|
return IpPrefix(address, prefixLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TauriVpnService : VpnService() {
|
||||||
|
companion object {
|
||||||
|
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
||||||
|
@JvmField var self: TauriVpnService? = null
|
||||||
|
|
||||||
|
const val IPV4_ADDR = "IPV4_ADDR"
|
||||||
|
const val ROUTES = "ROUTES"
|
||||||
|
const val DNS = "DNS"
|
||||||
|
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
|
||||||
|
const val MTU = "MTU"
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var vpnInterface: ParcelFileDescriptor
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
println("vpn on start command ${intent?.getExtras()} $intent")
|
||||||
|
var args = intent?.getExtras()
|
||||||
|
|
||||||
|
vpnInterface = createVpnInterface(args)
|
||||||
|
println("vpn created ${vpnInterface.fd}")
|
||||||
|
|
||||||
|
var event_data = JSObject()
|
||||||
|
event_data.put("fd", vpnInterface.fd)
|
||||||
|
triggerCallback("vpn_service_start", event_data)
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
self = this
|
||||||
|
println("vpn on create")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
println("vpn on destroy")
|
||||||
|
self = null
|
||||||
|
super.onDestroy()
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRevoke() {
|
||||||
|
println("vpn on revoke")
|
||||||
|
self = null
|
||||||
|
super.onRevoke()
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnect() {
|
||||||
|
triggerCallback("vpn_service_stop", JSObject())
|
||||||
|
vpnInterface.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
|
||||||
|
var builder = Builder()
|
||||||
|
.setSession("TauriVpnService")
|
||||||
|
.setBlocking(false)
|
||||||
|
|
||||||
|
var mtu = args?.getInt(MTU) ?: 1500
|
||||||
|
var ipv4Addr = args?.getString(IPV4_ADDR) ?: "10.126.126.1/24"
|
||||||
|
var dns = args?.getString(DNS) ?: "114.114.114.114"
|
||||||
|
var routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||||
|
var disallowedApplications = args?.getStringArray(DISALLOWED_APPLICATIONS) ?: emptyArray()
|
||||||
|
|
||||||
|
println("vpn create vpn interface. mtu: $mtu, ipv4Addr: $ipv4Addr, dns:" +
|
||||||
|
"$dns, routes: ${java.util.Arrays.toString(routes)}," +
|
||||||
|
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
|
||||||
|
|
||||||
|
val ipParts = ipv4Addr.split("/")
|
||||||
|
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||||
|
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||||
|
|
||||||
|
builder.setMtu(mtu)
|
||||||
|
builder.addDnsServer(dns)
|
||||||
|
|
||||||
|
for (route in routes) {
|
||||||
|
builder.addRoute(stringToIpPrefix(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (app in disallowedApplications) {
|
||||||
|
builder.addDisallowedApplication(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.also {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
it.setMetered(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.establish()
|
||||||
|
?: throw IllegalStateException("Failed to init VpnService")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.plugin.vpnservice
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.VpnService
|
||||||
|
import app.tauri.annotation.Command
|
||||||
|
import app.tauri.annotation.InvokeArg
|
||||||
|
import app.tauri.annotation.TauriPlugin
|
||||||
|
import app.tauri.plugin.Invoke
|
||||||
|
import app.tauri.plugin.JSObject
|
||||||
|
import app.tauri.plugin.Plugin
|
||||||
|
import android.webkit.WebView
|
||||||
|
|
||||||
|
@InvokeArg
|
||||||
|
class PingArgs {
|
||||||
|
var value: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@InvokeArg
|
||||||
|
class StartVpnArgs {
|
||||||
|
var ipv4Addr: String? = null
|
||||||
|
var routes: Array<String> = emptyArray()
|
||||||
|
var dns: String? = null
|
||||||
|
var disallowedApplications: Array<String> = emptyArray()
|
||||||
|
var mtu: Int? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@TauriPlugin
|
||||||
|
class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||||
|
private val implementation = Example()
|
||||||
|
|
||||||
|
override fun load(webView: WebView) {
|
||||||
|
println("load vpn service plugin")
|
||||||
|
TauriVpnService.triggerCallback = { event, data ->
|
||||||
|
println("vpn: triggerCallback $event $data")
|
||||||
|
trigger(event, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command
|
||||||
|
fun ping(invoke: Invoke) {
|
||||||
|
val args = invoke.parseArgs(PingArgs::class.java)
|
||||||
|
|
||||||
|
val ret = JSObject()
|
||||||
|
ret.put("value", implementation.pong(args.value ?: "default value :("))
|
||||||
|
invoke.resolve(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command
|
||||||
|
fun prepareVpn(invoke: Invoke) {
|
||||||
|
println("prepare vpn in plugin")
|
||||||
|
val it = VpnService.prepare(activity)
|
||||||
|
var ret = JSObject()
|
||||||
|
if (it != null) {
|
||||||
|
activity.startActivityForResult(it, 0x0f)
|
||||||
|
ret.put("errorMsg", "again")
|
||||||
|
}
|
||||||
|
invoke.resolve(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command
|
||||||
|
fun startVpn(invoke: Invoke) {
|
||||||
|
val args = invoke.parseArgs(StartVpnArgs::class.java)
|
||||||
|
println("start vpn in plugin, args: $args")
|
||||||
|
|
||||||
|
TauriVpnService.self?.onRevoke()
|
||||||
|
|
||||||
|
val it = VpnService.prepare(activity)
|
||||||
|
var ret = JSObject()
|
||||||
|
if (it != null) {
|
||||||
|
ret.put("errorMsg", "need_prepare")
|
||||||
|
} else {
|
||||||
|
var intent = Intent(activity, TauriVpnService::class.java)
|
||||||
|
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||||
|
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||||
|
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||||
|
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||||
|
intent.putExtra(TauriVpnService.MTU, args.mtu)
|
||||||
|
|
||||||
|
activity.startService(intent)
|
||||||
|
}
|
||||||
|
invoke.resolve(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command
|
||||||
|
fun stopVpn(invoke: Invoke) {
|
||||||
|
println("stop vpn in plugin")
|
||||||
|
TauriVpnService.self?.onRevoke()
|
||||||
|
activity.stopService(Intent(activity, TauriVpnService::class.java))
|
||||||
|
println("stop vpn in plugin end")
|
||||||
|
invoke.resolve(JSObject())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.plugin.vpnservice
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
14
tauri-plugin-vpnservice/build.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const COMMANDS: &[&str] = &[
|
||||||
|
"ping",
|
||||||
|
"prepare_vpn",
|
||||||
|
"start_vpn",
|
||||||
|
"stop_vpn",
|
||||||
|
"register_listener",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri_plugin::Builder::new(COMMANDS)
|
||||||
|
.android_path("android")
|
||||||
|
.ios_path("ios")
|
||||||
|
.build();
|
||||||
|
}
|
35
tauri-plugin-vpnservice/guest-js/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export async function ping(value: string): Promise<string | null> {
|
||||||
|
return await invoke<{ value?: string }>('plugin:vpnservice|ping', {
|
||||||
|
payload: {
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
}).then((r) => (r.value ? r.value : null));
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvokeResponse {
|
||||||
|
errorMsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartVpnRequest {
|
||||||
|
ipv4Addr?: string;
|
||||||
|
routes?: string[];
|
||||||
|
dns?: string;
|
||||||
|
disallowedApplications?: string[];
|
||||||
|
mtu?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepare_vpn(): Promise<InvokeResponse | null> {
|
||||||
|
return await invoke<InvokeResponse>('plugin:vpnservice|prepare_vpn', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start_vpn(request: StartVpnRequest): Promise<InvokeResponse | null> {
|
||||||
|
return await invoke<InvokeResponse>('plugin:vpnservice|start_vpn', {
|
||||||
|
...request,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stop_vpn(): Promise<InvokeResponse | null> {
|
||||||
|
return await invoke<InvokeResponse>('plugin:vpnservice|stop_vpn', {})
|
||||||
|
}
|
10
tauri-plugin-vpnservice/ios/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
||||||
|
Package.resolved
|
31
tauri-plugin-vpnservice/ios/Package.swift
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// swift-tools-version:5.3
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "tauri-plugin-vpnservice",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v13),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "tauri-plugin-vpnservice",
|
||||||
|
type: .static,
|
||||||
|
targets: ["tauri-plugin-vpnservice"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
|
.target(
|
||||||
|
name: "tauri-plugin-vpnservice",
|
||||||
|
dependencies: [
|
||||||
|
.byName(name: "Tauri")
|
||||||
|
],
|
||||||
|
path: "Sources")
|
||||||
|
]
|
||||||
|
)
|
3
tauri-plugin-vpnservice/ios/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Tauri Plugin vpnservice
|
||||||
|
|
||||||
|
A description of this package.
|
20
tauri-plugin-vpnservice/ios/Sources/ExamplePlugin.swift
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import SwiftRs
|
||||||
|
import Tauri
|
||||||
|
import UIKit
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
class PingArgs: Decodable {
|
||||||
|
let value: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExamplePlugin: Plugin {
|
||||||
|
@objc public func ping(_ invoke: Invoke) throws {
|
||||||
|
let args = try invoke.parseArgs(PingArgs.self)
|
||||||
|
invoke.resolve(["value": args.value ?? ""])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@_cdecl("init_plugin_vpnservice")
|
||||||
|
func initPlugin() -> Plugin {
|
||||||
|
return ExamplePlugin()
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import ExamplePlugin
|
||||||
|
|
||||||
|
final class ExamplePluginTests: XCTestCase {
|
||||||
|
func testExample() throws {
|
||||||
|
let plugin = ExamplePlugin()
|
||||||
|
}
|
||||||
|
}
|
33
tauri-plugin-vpnservice/package.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "tauri-plugin-vpnservice-api",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"author": "You",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"main": "./dist-js/index.cjs",
|
||||||
|
"module": "./dist-js/index.js",
|
||||||
|
"exports": {
|
||||||
|
"types": "./dist-js/index.d.ts",
|
||||||
|
"import": "./dist-js/index.js",
|
||||||
|
"require": "./dist-js/index.cjs"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist-js",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"prepublishOnly": "yarn build",
|
||||||
|
"pretest": "yarn build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": ">=2.0.0-beta.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
|
"rollup": "^4.9.6",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-ping"
|
||||||
|
description = "Enables the ping command without any pre-configured scope."
|
||||||
|
commands.allow = ["ping"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-ping"
|
||||||
|
description = "Denies the ping command without any pre-configured scope."
|
||||||
|
commands.deny = ["ping"]
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Automatically generated - DO NOT EDIT!
|
||||||
|
|
||||||
|
"$schema" = "../../schemas/schema.json"
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "allow-prepare-vpn"
|
||||||
|
description = "Enables the prepare_vpn command without any pre-configured scope."
|
||||||
|
commands.allow = ["prepare_vpn"]
|
||||||
|
|
||||||
|
[[permission]]
|
||||||
|
identifier = "deny-prepare-vpn"
|
||||||
|
description = "Denies the prepare_vpn command without any pre-configured scope."
|
||||||
|
commands.deny = ["prepare_vpn"]
|