mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-09 03:04:31 +00:00
Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d7a938e93 | |||
| 6229229b31 | |||
| 6a63853bad | |||
| 362aa7a9cd | |||
| 12a7b5a5c5 | |||
| 4eba9b07b6 | |||
| 1b48029bdc | |||
| 3542e944cb | |||
| 852d1c9e14 | |||
| 4958394469 | |||
| 41b6d65604 | |||
| aae30894dd | |||
| 81d169abfc | |||
| 9c6c210e89 | |||
| d1c6dcf754 | |||
| 97c8c4f55a | |||
| ed8df2d58f | |||
| f66010e6f9 | |||
| d5c4700d32 | |||
| 969ecfc4ca | |||
| 8f862997eb | |||
| b20075e3dc | |||
| eb3b5aae51 | |||
| af6b6ab6f1 | |||
| 5a1668c753 | |||
| 820d9095d3 | |||
| 2fb41ccbba | |||
| b4666be696 | |||
| 4688ad74ad | |||
| f7ea78d4f0 | |||
| ac112440c3 | |||
| 958b246f05 | |||
| 263f4c3bc9 | |||
| ffddc517e1 | |||
| 5cd0a3e846 | |||
| f4319c4d4f | |||
| 0091a535d5 | |||
| d7a5fb8d66 | |||
| f63054e937 | |||
| efc043abbb | |||
| 40c6de8e31 | |||
| 2db655bd6d | |||
| c49c56612b | |||
| 6ca074abae | |||
| 84430055ab | |||
| 432fcb3fc3 | |||
| fae32361f2 | |||
| bcb2e512d4 | |||
| 82ca04a8a7 | |||
| 2ef3b72224 | |||
| 6d319cba1d | |||
| 3687519ef3 | |||
| 3a4ac59467 | |||
| 1cfc135df3 | |||
| 5b35c51da9 | |||
| ec7ddd3bad | |||
| 6f3e708679 | |||
| 869e1b89f5 | |||
| 9e0a3b6936 | |||
| c6cb1a77d0 | |||
| 83010861ba | |||
| daa53e5168 | |||
| 51befdbf87 | |||
| 8311b11713 | |||
| 19c80c7b9c | |||
| a879dd1b14 | |||
| a8feb9ac2b | |||
| c5fbd29c0e | |||
| 26b1794723 | |||
| 371b4b70a3 | |||
| b2cc38ee63 | |||
| 79b562cdc9 | |||
| e3f089251c | |||
| cf6dcbc054 | |||
| 2cf2b0fcac | |||
| aa0cca3bb6 | |||
| fb59f01058 | |||
| e91a0da70a | |||
| 9cc617ae4c | |||
| e4b0f1f1bb | |||
| 443c3ca0b3 | |||
| 55a0e5952c | |||
| 1dff388717 | |||
| 61c741f887 | |||
| 01dd9a05c3 | |||
| 8c19a2293c | |||
| a1bec48dc9 | |||
| 7e289865b2 | |||
| 742c7edd57 | |||
| b71a2889ef | |||
| bcd75d6ce3 | |||
| d4c1b0e867 | |||
| b037ea9c3f | |||
| b5f475cd4c | |||
| eaa4d2c7b8 | |||
| e160d9b048 | |||
| 0aeea39fbe | |||
| e000636d83 | |||
| 8e4dc508bb | |||
| e2684a93de | |||
| 1d89ddbb16 | |||
| 2bfdd44759 | |||
| 77966916c4 | |||
| 26b7455c1e | |||
| 8922e7b991 | |||
| e6ac31fb20 | |||
| c8f3c5d6aa | |||
| 330659e449 | |||
| 80043df292 | |||
| ecd1ea6f8c | |||
| 694b8d349d | |||
| ef44027f57 | |||
| f3db348b01 | |||
| c4eacf4591 | |||
| 59d4475743 | |||
| 22b4c4be2c | |||
| 5f31583a84 | |||
| 1d25240d8c | |||
| eeb507d6ea | |||
| 9e9916efa5 | |||
| db6b9e3684 | |||
| ff24332e23 | |||
| d4ff0b1767 | |||
| 5716f7f16b | |||
| e5bd8f9e24 | |||
| b56bcfb4b0 | |||
| fb95b4827c | |||
| a8f7226195 | |||
| e6ee485352 | |||
| 73291a3a1c | |||
| f737708f45 | |||
| aa24d09aa2 | |||
| fe4e77979d | |||
| 7a26640c26 | |||
| 5a777959e3 | |||
| 3512a80597 | |||
| 011770a601 | |||
| 6475724d2e | |||
| 85e9029577 | |||
| b6e292cce3 | |||
| c58140fb47 | |||
| aebb7facfa | |||
| 1e2124cb99 | |||
| e1cbd07d1f | |||
| 7750e81168 | |||
| bf3edbd28f | |||
| cd2cf56358 | |||
| 21f4a944a7 | |||
| 9617005136 | |||
| c85d1d41b3 | |||
| 9e3c9228bb | |||
| acd7c85ff6 | |||
| 8727221513 | |||
| cdedaf3f63 | |||
| ffe5644ddc | |||
| ccc684a9ab | |||
| 977e502150 | |||
| 518d26b25f | |||
| 101f416268 | |||
| ffa08d1c43 | |||
| cf3f9169b7 | |||
| 8343cd5e76 | |||
| 005b321f62 | |||
| 53264f67bf | |||
| f8b34e3c86 | |||
| ce1bdac2bc | |||
| bd8f01fb26 | |||
| b590700540 | |||
| 48c5c23f9b | |||
| f4f591d14c | |||
| 0c16e2211b | |||
| 4bfea06a12 | |||
| 057ee9f2c5 | |||
| 7f48ca54a3 | |||
| ee5227130c | |||
| 2e0d9a2b54 | |||
| c5d732773f |
+35
-54
@@ -1,29 +1,40 @@
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "linker-flavor=ld.lld"]
|
||||
# region Native
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
# region
|
||||
|
||||
# region CI
|
||||
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.riscv64gc-unknown-linux-musl]
|
||||
linker = "riscv64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.armv7-unknown-linux-musleabi]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabi]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
@@ -64,44 +75,14 @@ rustflags = [
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "armv7-unknown-linux-musleabihf-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
|
||||
[target.armv7-unknown-linux-musleabi]
|
||||
linker = "armv7-unknown-linux-musleabi-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
linker = "loongarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
linker = "arm-unknown-linux-musleabihf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.arm-unknown-linux-musleabi]
|
||||
linker = "arm-unknown-linux-musleabi-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
# endregion
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: prepare-build
|
||||
author: Luna
|
||||
description: Prepare build environment
|
||||
inputs:
|
||||
target:
|
||||
description: 'The target to build for'
|
||||
required: false
|
||||
pnpm:
|
||||
description: 'Whether to run pnpm build'
|
||||
required: true
|
||||
default: 'true'
|
||||
pnpm-build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: './easytier-web/*'
|
||||
gui:
|
||||
description: 'Whether to prepare the GUI build environment'
|
||||
required: true
|
||||
default: 'true'
|
||||
token:
|
||||
description: 'GitHub token, used by setup-protoc action'
|
||||
required: false
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- run: mkdir -p easytier-gui/dist
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -qqy build-essential mold musl-tools
|
||||
shell: bash
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
||||
|
||||
- name: Install GUI dependencies (Linux)
|
||||
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
sudo apt-get install -qq xdg-utils \
|
||||
libappindicator3-dev \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev
|
||||
shell: bash
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.95
|
||||
target: ${{ !contains(inputs.target, 'mips') && inputs.target || '' }}
|
||||
components: ${{ contains(inputs.target, 'mips') && 'rust-src' || '' }}
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- name: Install Rust (MIPS)
|
||||
if: ${{ contains(inputs.target, 'mips') }}
|
||||
run: |
|
||||
MUSL_TARGET=${{ inputs.target }}sf
|
||||
mkdir -p ./musl_gcc
|
||||
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
|
||||
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
|
||||
sudo chmod -R a+rwx ./musl_gcc
|
||||
|
||||
if [[ -d "./musl_gcc/sysroot" ]]; then
|
||||
echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
|
||||
# for panic-abort
|
||||
cp libgcc_eh.a libunwind.a
|
||||
|
||||
# for mimalloc
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
@@ -0,0 +1,48 @@
|
||||
name: 'Setup pnpm'
|
||||
author: Luna
|
||||
description: 'Setup Node.js, pnpm, and install dependencies'
|
||||
|
||||
inputs:
|
||||
build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10
|
||||
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@v5
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install and build
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm -r install
|
||||
if [ -n "${{ inputs.build-filter }}" ]; then
|
||||
echo "Building with filter: ${{ inputs.build-filter }}"
|
||||
pnpm -r --filter "${{ inputs.build-filter }}" build
|
||||
else
|
||||
echo "No build filter provided, building all packages"
|
||||
pnpm -r build
|
||||
fi
|
||||
+142
-179
@@ -2,9 +2,14 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
branches: [ "develop", "main", "releases/**" ]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
# do not skip push on branch starts with releases/
|
||||
@@ -30,85 +36,69 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
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: |
|
||||
pnpm -r install
|
||||
pnpm -r --filter "./easytier-web/*" build
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-web-dashboard
|
||||
path: |
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: loongarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-loongarch64
|
||||
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
ARTIFACT_NAME: macos-x86_64
|
||||
@@ -119,17 +109,12 @@ jobs:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-i686
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -142,7 +127,7 @@ jobs:
|
||||
- build_web
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
@@ -154,158 +139,128 @@ jobs:
|
||||
name: easytier-web-dashboard
|
||||
path: easytier-web/frontend/dist/
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "core-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tool: cargo-zigbuild
|
||||
|
||||
- name: Build Core & Cli
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
|
||||
# loongarch need llvm-18
|
||||
if [[ $TARGET =~ ^loongarch.*$ ]]; then
|
||||
sudo apt-get install -qq llvm-18 clang-18
|
||||
export LLVM_CONFIG_PATH=/usr/lib/llvm-18/bin/llvm-config
|
||||
fi
|
||||
# we set the sysroot when sysroot is a dir
|
||||
# this dir is a soft link generated by install_rust.sh
|
||||
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
|
||||
if [[ -d "./musl_gcc/sysroot" ]]; then
|
||||
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly-2025-09-01 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
- name: Build
|
||||
if: ${{ !contains(matrix.TARGET, 'mips') }}
|
||||
run: |
|
||||
if [[ "$TARGET" == *windows* ]]; then
|
||||
SUFFIX=.exe
|
||||
else
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ || $TARGET =~ ^aarch64.*$ ]]; then
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
else
|
||||
CORE_FEATURES="--features=jemalloc"
|
||||
fi
|
||||
cargo build --release --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||
cargo build --release --target $TARGET $CORE_FEATURES
|
||||
SUFFIX=""
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
if [[ "$TARGET" =~ (x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|windows|darwin) ]]; then
|
||||
BUILD=build
|
||||
else
|
||||
BUILD=zigbuild
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ (freebsd|windows) ]]; then
|
||||
FEATURES="mimalloc"
|
||||
else
|
||||
FEATURES="jemalloc"
|
||||
fi
|
||||
|
||||
cargo $BUILD --release --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||
|
||||
cargo $BUILD --release --target $TARGET --features=$FEATURES
|
||||
|
||||
- name: Build (MIPS)
|
||||
if: ${{ contains(matrix.TARGET, 'mips') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
with:
|
||||
envs: TARGET
|
||||
release: ${{ matrix.BSD_VERSION }}
|
||||
arch: x86_64
|
||||
usesh: true
|
||||
mem: 6144
|
||||
cpu: 4
|
||||
run: |
|
||||
uname -a
|
||||
echo $SHELL
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
env | sort
|
||||
|
||||
pkg install -y git protobuf llvm-devel sudo curl
|
||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
. $HOME/.cargo/env
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.89
|
||||
rustup default 1.89
|
||||
|
||||
export CC=clang
|
||||
export CXX=clang++
|
||||
export CARGO_TERM_COLOR=always
|
||||
|
||||
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
|
||||
cargo build --release --verbose --target $TARGET --features=mimalloc
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
run: |
|
||||
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
# windows is the only OS using a different convention for executable file name
|
||||
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/x86_64/* ./artifacts/objects/
|
||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/i686/* ./artifacts/objects/
|
||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
cp easytier/third_party/arm64/* ./artifacts/objects/
|
||||
case $TARGET in
|
||||
x86_64*) ARCH_DIR=x86_64 ;;
|
||||
i686*) ARCH_DIR=i686 ;;
|
||||
aarch64*) ARCH_DIR=arm64 ;;
|
||||
esac
|
||||
if [[ -n "$ARCH_DIR" ]]; then
|
||||
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then
|
||||
HOST_ARCH=$(uname -m)
|
||||
case $HOST_ARCH in
|
||||
x86_64) UPX_ARCH="amd64" ;;
|
||||
aarch64) UPX_ARCH="arm64" ;;
|
||||
*) UPX_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ && ! $TARGET =~ ^riscv64.*$ ]]; then
|
||||
UPX_VERSION=4.2.4
|
||||
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
|
||||
cp upx-${UPX_VERSION}-amd64_linux/upx .
|
||||
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
UPX_VERSION=5.1.1
|
||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
||||
cp "${UPX_PKG}/upx" .
|
||||
UPX_BIN=./upx
|
||||
fi
|
||||
|
||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
|
||||
fi
|
||||
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do
|
||||
if [[ -f "$BIN" ]]; then
|
||||
if [[ -n "$UPX_BIN" ]]; then
|
||||
$UPX_BIN --lzma --best "$BIN" || true
|
||||
fi
|
||||
|
||||
mv "$BIN" ./artifacts/objects/
|
||||
fi
|
||||
done
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
core-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
- build
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
magisk_build:
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
- build
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
build_magisk:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置
|
||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||
|
||||
# 下载二进制文件到独立目录
|
||||
- name: Download Linux aarch64 binaries
|
||||
@@ -322,10 +277,9 @@ jobs:
|
||||
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
||||
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
||||
|
||||
|
||||
# 上传生成的模块
|
||||
- name: Upload Magisk Module
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: Easytier-Magisk
|
||||
path: |
|
||||
@@ -333,3 +287,12 @@ jobs:
|
||||
!./easytier-contrib/easytier-magisk/build.sh
|
||||
!./easytier-contrib/easytier-magisk/magisk_update.json
|
||||
if-no-files-found: error
|
||||
|
||||
core-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build, build_magisk ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.5.0'
|
||||
default: 'v2.6.3'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
name: Validate inputs
|
||||
run: |
|
||||
|
||||
+48
-112
@@ -5,7 +5,12 @@ on:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
@@ -29,20 +35,20 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
OS: ubuntu-24.04
|
||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
@@ -57,16 +63,14 @@ jobs:
|
||||
OS: windows-latest
|
||||
GUI_TARGET: x86_64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: i686-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-i686
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -78,103 +82,39 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install GUI dependencies (x86 only)
|
||||
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
|
||||
run: bash ./.github/workflows/install_gui_dep.sh
|
||||
|
||||
- name: Install GUI cross compile (aarch64 only)
|
||||
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
||||
run: |
|
||||
# see https://tauri.app/v1/guides/building/linux/
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt update
|
||||
sudo apt install aptitude
|
||||
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
|
||||
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
|
||||
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu libsoup-3.0-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
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: |
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: copy correct DLLs
|
||||
if: ${{ matrix.OS == 'windows-latest' }}
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
|
||||
run: |
|
||||
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
||||
cp ./easytier/third_party/arm64/* ./easytier-gui/src-tauri/
|
||||
elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then
|
||||
cp ./easytier/third_party/i686/* ./easytier-gui/src-tauri/
|
||||
else
|
||||
cp ./easytier/third_party/x86_64/* ./easytier-gui/src-tauri/
|
||||
case $TARGET in
|
||||
x86_64*) ARCH_DIR=x86_64 ;;
|
||||
i686*) ARCH_DIR=i686 ;;
|
||||
aarch64*) ARCH_DIR=arm64 ;;
|
||||
esac
|
||||
if [[ -n "$ARCH_DIR" ]]; then
|
||||
find "./easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./easytier-gui/src-tauri/ \;
|
||||
fi
|
||||
|
||||
- name: Build GUI
|
||||
@@ -182,10 +122,9 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Compress
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
@@ -194,36 +133,33 @@ jobs:
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
# copy gui bundle, gui is built without specific target
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
if [[ $GUI_TARGET =~ windows ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
||||
elif [[ $OS =~ ^macos.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ darwin ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ linux ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||
# currently only x86 appimage is supported
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
fi
|
||||
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
gui-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-gui
|
||||
needs: [ pre_job, build-gui ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-gui.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
sudo apt update
|
||||
sudo apt install -qq libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
patchelf
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# env needed:
|
||||
# - TARGET
|
||||
# - GUI_TARGET
|
||||
# - OS
|
||||
|
||||
# dependencies are only needed on ubuntu as that's the only place where
|
||||
# we make cross-compilation
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
|
||||
# https://github.com/cross-tools/musl-cross/releases
|
||||
# if "musl" is a substring of TARGET, we assume that we are using musl
|
||||
MUSL_TARGET=$TARGET
|
||||
# if target is mips or mipsel, we should use soft-float version of musl
|
||||
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET}sf
|
||||
elif [[ $TARGET =~ ^riscv64gc-.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET/#riscv64gc-/riscv64-}
|
||||
fi
|
||||
if [[ $MUSL_TARGET =~ musl ]]; then
|
||||
mkdir -p ./musl_gcc
|
||||
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
|
||||
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
|
||||
sudo chmod -R a+rwx ./musl_gcc
|
||||
fi
|
||||
fi
|
||||
|
||||
# see https://github.com/rust-lang/rustup/issues/3709
|
||||
rustup set auto-self-update disable
|
||||
rustup install 1.89
|
||||
rustup default 1.89
|
||||
|
||||
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
|
||||
# for panic-abort
|
||||
cp libgcc_eh.a libunwind.a
|
||||
|
||||
# for mimalloc
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
|
||||
rustup toolchain install nightly-2025-09-01-x86_64-unknown-linux-gnu
|
||||
rustup component add rust-src --toolchain nightly-2025-09-01-x86_64-unknown-linux-gnu
|
||||
|
||||
# https://github.com/rust-lang/rust/issues/128808
|
||||
# remove it after Cargo or rustc fix this.
|
||||
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
|
||||
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
|
||||
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
|
||||
fi
|
||||
else
|
||||
rustup target add $TARGET
|
||||
if [[ $GUI_TARGET != '' ]]; then
|
||||
rustup target add $GUI_TARGET
|
||||
fi
|
||||
fi
|
||||
@@ -5,7 +5,12 @@ on:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
@@ -29,25 +35,30 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: android
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: android
|
||||
runs-on: ${{ matrix.OS }}
|
||||
- TARGET: aarch64-linux-android
|
||||
ARCH: aarch64
|
||||
- TARGET: armv7-linux-androideabi
|
||||
ARCH: armv7
|
||||
- TARGET: i686-linux-android
|
||||
ARCH: i686
|
||||
- TARGET: x86_64-linux-android
|
||||
ARCH: x86_64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NAME: easytier
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
OS: ${{ matrix.OS }}
|
||||
ARCH: ${{ matrix.ARCH }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
@@ -61,72 +72,41 @@ jobs:
|
||||
- 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 '
|
||||
cmdline-tools-version: 12.0
|
||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 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
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
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: |
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: false
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Android
|
||||
- name: Build
|
||||
run: |
|
||||
cd easytier-gui
|
||||
pnpm tauri android build
|
||||
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
|
||||
|
||||
- name: Compress
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
@@ -134,23 +114,21 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
name: easytier-mobile-android-${{ matrix.ARCH }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
mobile-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-mobile
|
||||
needs: [ pre_job, build-mobile ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-mobile.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Nix Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "develop"]
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
pull_request:
|
||||
branches: ["main", "develop"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-full-shell:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v27
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- name: Magic Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||
|
||||
- name: Warm up full devShell
|
||||
run: nix develop .#full --command true
|
||||
|
||||
- name: Cargo check in flake environment
|
||||
run: nix develop .#full --command cargo check
|
||||
|
||||
- name: Cargo build in flake environment
|
||||
run: nix develop .#full --command cargo build
|
||||
+95
-34
@@ -3,10 +3,18 @@ name: EasyTier OHOS
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
tags:
|
||||
- 'v*'
|
||||
- '!*-pre'
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -17,18 +25,29 @@ defaults:
|
||||
|
||||
jobs:
|
||||
cargo_fmt_check:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: fmt check
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: false
|
||||
pnpm: false
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
bash ../../.github/workflows/install_rust.sh
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
# do not skip push on branch starts with releases/
|
||||
@@ -41,55 +60,71 @@ jobs:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
skip_after_successful_duplicate: "true"
|
||||
cancel_others: "true"
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/actions/**"]'
|
||||
|
||||
build-ohos:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
env:
|
||||
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
sudo apt-get install -qq \
|
||||
build-essential \
|
||||
wget \
|
||||
unzip \
|
||||
git \
|
||||
pkg-config curl libgl1-mesa-dev expect
|
||||
sudo apt-get clean
|
||||
|
||||
- name: Count commits since last tag on upstream main
|
||||
- name: Resolve easytier version
|
||||
run: |
|
||||
set -e
|
||||
|
||||
UPSTREAM_REPO="https://github.com/EasyTier/EasyTier.git"
|
||||
|
||||
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || true
|
||||
git fetch upstream --tags --force
|
||||
git fetch --unshallow upstream main || git fetch upstream main
|
||||
git fetch --tags upstream --force
|
||||
|
||||
# 获取 upstream/main 最新提交
|
||||
git fetch upstream main
|
||||
# 读取 cargo 版本
|
||||
CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path easytier/Cargo.toml \
|
||||
| jq -r '.packages[0].version')
|
||||
|
||||
# 获取 upstream/main 最新 tag
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 upstream/main 2>/dev/null || echo "")
|
||||
LAST_TAG_VERSION="${LAST_TAG#v}"
|
||||
|
||||
if [ -z "$LAST_TAG" ]; then
|
||||
# 语义版本比较
|
||||
version_gt() {
|
||||
[ "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ] && [ "$1" != "$2" ]
|
||||
}
|
||||
|
||||
if [ -z "$LAST_TAG_VERSION" ]; then
|
||||
BASE_VERSION="$CARGO_VERSION"
|
||||
DIFF_COUNT=$(git rev-list --count upstream/main)
|
||||
elif version_gt "$CARGO_VERSION" "$LAST_TAG_VERSION"; then
|
||||
BASE_VERSION="$CARGO_VERSION"
|
||||
DIFF_COUNT=0
|
||||
else
|
||||
BASE_VERSION="$LAST_TAG_VERSION"
|
||||
DIFF_COUNT=$(git rev-list --count "${LAST_TAG}..upstream/main")
|
||||
fi
|
||||
|
||||
echo "TAG_COMMIT_DIFF=$DIFF_COUNT"
|
||||
echo "TAG_COMMIT_DIFF=$DIFF_COUNT" >> $GITHUB_ENV
|
||||
|
||||
- name: Get easytier version
|
||||
run: |
|
||||
EASYTIER_CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path easytier/Cargo.toml \
|
||||
| jq -r '.packages[0].version')
|
||||
EASYTIER_VERSION="${EASYTIER_CARGO_VERSION}-${TAG_COMMIT_DIFF}"
|
||||
echo "EASYTIER_VERSION=${EASYTIER_VERSION}" >> $GITHUB_ENV
|
||||
COMMIT_HASH=$(git rev-parse --short upstream/main)
|
||||
EASYTIER_VERSION="${BASE_VERSION}-${DIFF_COUNT}-${COMMIT_HASH}"
|
||||
|
||||
echo "EASYTIER_VERSION=$EASYTIER_VERSION"
|
||||
echo "EASYTIER_VERSION=$EASYTIER_VERSION" >> $GITHUB_ENV
|
||||
|
||||
cd ./easytier-contrib/easytier-ohrs/package
|
||||
jq --arg v "$EASYTIER_VERSION" '.version = $v' oh-package.json5 > oh-package.tmp.json5
|
||||
mv oh-package.tmp.json5 oh-package.json5
|
||||
|
||||
|
||||
- name: Generate CHANGELOG.md for current commit
|
||||
working-directory: ./easytier-contrib/easytier-ohrs/package
|
||||
run: |
|
||||
@@ -115,6 +150,15 @@ jobs:
|
||||
run: |
|
||||
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
|
||||
|
||||
rustup install stable
|
||||
rustup default stable
|
||||
|
||||
rustup target add aarch64-unknown-linux-ohos
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: ohrs
|
||||
|
||||
- name: Create clang wrapper script
|
||||
run: |
|
||||
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
||||
@@ -128,38 +172,50 @@ jobs:
|
||||
EOF
|
||||
sudo chmod +x $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh
|
||||
|
||||
- name: Build
|
||||
- name: Build latest Har
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
sudo apt-get install -y llvm clang lldb lld
|
||||
sudo apt-get install -y protobuf-compiler
|
||||
bash ../../.github/workflows/install_rust.sh
|
||||
source env.sh
|
||||
cargo install ohrs
|
||||
rustup target add aarch64-unknown-linux-ohos
|
||||
cargo update easytier
|
||||
ohrs doctor
|
||||
ohrs build --release --arch aarch
|
||||
ohrs artifact
|
||||
mv package.har easytier-ohrs.har
|
||||
|
||||
- name: Build Release Package
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
echo "🎉 Official Release detected. Building easytier-release..."
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
echo "Release Version: $TAG_VERSION"
|
||||
cd package
|
||||
jq --arg v "$TAG_VERSION" '.name = "easytier-release" | .version = $v' oh-package.json5 > oh-package.tmp.json5 && mv oh-package.tmp.json5 oh-package.json5
|
||||
cd ..
|
||||
ohrs build --release --arch aarch
|
||||
cd dist/arm64-v8a
|
||||
mv libeasytier_ohrs.so libeasytier_release.so
|
||||
cd ../..
|
||||
ohrs artifact
|
||||
mv package.har easytier-release.har
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-ohos
|
||||
path: |
|
||||
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
|
||||
./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish To Center Ohpm
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
env:
|
||||
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
|
||||
OHPM_PRIVATE_KEY: ${{ secrets.OHPM_PRIVATE_KEY }}
|
||||
OHPM_KEY_PASSPHRASE: ${{ secrets.OHPM_KEY_PASSPHRASE }}
|
||||
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
|
||||
run: |
|
||||
ohpm config set publish_id "$OHPM_PUBLISH_CODE"
|
||||
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm
|
||||
@@ -176,10 +232,15 @@ jobs:
|
||||
ohpm publish easytier-ohrs.har
|
||||
|
||||
- name: Publish To Private Ohpm
|
||||
if: github.event_name == 'push'
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
|
||||
run: |
|
||||
printf '%s' "${{ secrets.CODEARTS_PRIVATE_OHPM }}" > ~/.ohpm/.ohpmrc
|
||||
ohpm config set strict_ssl false
|
||||
ohpm publish easytier-ohrs.har
|
||||
if [ -f "easytier-release.har" ]; then
|
||||
echo "🚀 Publishing Release package..."
|
||||
ohpm publish easytier-release.har
|
||||
fi
|
||||
curl --header "Content-Type: application/json" --request POST --data "{}" ${{ secrets.CODEARTS_WEBHOOKS }}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.5.0'
|
||||
default: 'v2.6.3'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
@@ -92,4 +92,4 @@ jobs:
|
||||
files: |
|
||||
./zipped_assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
|
||||
+116
-68
@@ -2,12 +2,18 @@ name: EasyTier Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# RUSTC_WRAPPER: "sccache"
|
||||
# SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -28,22 +34,104 @@ jobs:
|
||||
# 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/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
check:
|
||||
name: Run linters & check
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
rustflags: ''
|
||||
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: Check formatting
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Check Clippy
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo clippy --all-targets --features full --all -- -D warnings
|
||||
|
||||
- name: Check features
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
|
||||
|
||||
- name: Check Cargo.lock is up to date
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if ! cargo metadata --format-version 1 --locked > /dev/null; then
|
||||
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pre-test:
|
||||
name: Build test
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Archive test
|
||||
run: cargo nextest archive --archive-file tests.tar.zst --package easytier --features full
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: tests
|
||||
path: tests.tar.zst
|
||||
retention-days: 1
|
||||
|
||||
test_matrix:
|
||||
name: Test (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, pre-test ]
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: "easytier"
|
||||
opts: "-E 'not test(tests::three_node)' --test-threads 1 --no-fail-fast"
|
||||
|
||||
- name: "three_node"
|
||||
opts: "-E 'test(tests::three_node) and not test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
|
||||
|
||||
- name: "three_node::subnet_proxy_three_node_test"
|
||||
opts: "-E 'test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup tools for test
|
||||
run: sudo apt install bridge-utils
|
||||
- name: Setup upnpd for test
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y miniupnpd miniupnpd-iptables iptables
|
||||
|
||||
- name: Setup system for test
|
||||
run: |
|
||||
@@ -53,63 +141,23 @@ jobs:
|
||||
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
|
||||
sudo ip addr add 2001:db8::2/64 dev lo
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Download tests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
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: |
|
||||
pnpm -r install
|
||||
pnpm -r --filter "./easytier-web/*" build
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install GUI dependencies (Used by clippy)
|
||||
run: |
|
||||
bash ./.github/workflows/install_gui_dep.sh
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
rustup component add rustfmt
|
||||
rustup component add clippy
|
||||
|
||||
- name: Check formatting
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Check Clippy
|
||||
if: ${{ !cancelled() }}
|
||||
# NOTE: tauri need `dist` dir in build.rs
|
||||
run: |
|
||||
mkdir -p easytier-gui/dist
|
||||
cargo clippy --all-targets --all-features --all -- -D warnings
|
||||
name: tests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo prlimit --pid $$ --nofile=1048576:1048576
|
||||
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1
|
||||
sudo chown -R $USER:$USER ./target
|
||||
sudo chown -R $USER:$USER ~/.cargo
|
||||
sudo -E env "PATH=$PATH" cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, check, test_matrix ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides
|
||||
#### Required Tools
|
||||
- Node.js v21 or higher
|
||||
- pnpm v9 or higher
|
||||
- Rust toolchain (version 1.89)
|
||||
- Rust toolchain (version 1.95)
|
||||
- LLVM and Clang
|
||||
- Protoc (Protocol Buffers compiler)
|
||||
|
||||
@@ -79,8 +79,8 @@ sudo apt install -y bridge-utils
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# Install Rust toolchain
|
||||
rustup install 1.89
|
||||
rustup default 1.89
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# Install project dependencies
|
||||
pnpm -r install
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@
|
||||
#### 必需工具
|
||||
- Node.js v21 或更高版本
|
||||
- pnpm v9 或更高版本
|
||||
- Rust 工具链(版本 1.89)
|
||||
- Rust 工具链(版本 1.95)
|
||||
- LLVM 和 Clang
|
||||
- Protoc(Protocol Buffers 编译器)
|
||||
|
||||
@@ -87,8 +87,8 @@ sudo apt install -y bridge-utils
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
# 安装 Rust 工具链
|
||||
rustup install 1.89
|
||||
rustup default 1.89
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# 安装项目依赖
|
||||
pnpm -r install
|
||||
|
||||
Generated
+2202
-1135
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,10 @@ exclude = [
|
||||
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
debug = 2
|
||||
|
||||
@@ -48,40 +48,43 @@
|
||||
|
||||
Choose the installation method that best suits your needs:
|
||||
|
||||
Linux (Recommended):
|
||||
```bash
|
||||
# 1. Download pre-built binary (Recommended, All platforms supported)
|
||||
# Visit https://github.com/EasyTier/EasyTier/releases
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
# 2. Install via cargo (Latest development version)
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
|
||||
# 3. Install via Docker
|
||||
# See https://easytier.cn/en/guide/installation.html#installation-methods
|
||||
|
||||
# 4. Linux Quick Install
|
||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash -s install
|
||||
|
||||
# 5. MacOS via Homebrew
|
||||
Homebrew (MacOS/Linux):
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
|
||||
# 6. OpenWrt Luci Web UI
|
||||
# Visit https://github.com/EasyTier/luci-app-easytier
|
||||
|
||||
# 7. (Optional) Install shell completions:
|
||||
easytier-core --gen-autocomplete fish > ~/.config/fish/completions/easytier-core.fish
|
||||
easytier-cli gen-autocomplete fish > ~/.config/fish/completions/easytier-cli.fish
|
||||
|
||||
```
|
||||
|
||||
Windows (Recommended, run with administrator privileges):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
Install via cargo (Latest development version):
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
|
||||
|
||||
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
|
||||
|
||||
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
Additional steps:
|
||||
|
||||
[One-Click Register Service](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) (Automatically start when the system boots and run in the background)
|
||||
|
||||
### 🚀 Basic Usage
|
||||
|
||||
#### Quick Networking with Shared Nodes
|
||||
|
||||
EasyTier supports quick networking using shared public nodes. When you don't have a public IP, you can use the free shared nodes provided by the EasyTier community. Nodes will automatically attempt NAT traversal and establish P2P connections. When P2P fails, data will be relayed through shared nodes.
|
||||
|
||||
The currently deployed shared public node is `tcp://public.easytier.cn:11010`.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same `--network-name` and `--network-secret` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example (Please use more complex network name to avoid conflicts):
|
||||
@@ -90,14 +93,14 @@ Taking two nodes as an example (Please use more complex network name to avoid co
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
|
||||
2. Run on Node B:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
|
||||
After successful execution, you can check the network status using `easytier-cli`:
|
||||
@@ -105,9 +108,9 @@ After successful execution, you can check the network status using `easytier-cli
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.5.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
@@ -124,7 +127,7 @@ To improve availability, you can connect to multiple shared nodes simultaneously
|
||||
|
||||
```bash
|
||||
# Connect to multiple shared nodes
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010 -p udp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP1>:11010 -p udp://<SharedNodeIP2>:11010
|
||||
```
|
||||
|
||||
Once your network is set up successfully, you can easily configure it to start automatically on system boot. Refer to the [One-Click Register Service guide](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) for step-by-step instructions on registering EasyTier as a system service.
|
||||
|
||||
+32
-30
@@ -48,40 +48,42 @@
|
||||
|
||||
选择最适合您需求的安装方式:
|
||||
|
||||
Linux(推荐):
|
||||
```bash
|
||||
# 1. 下载预编译二进制文件(推荐,支持所有平台)
|
||||
# 访问 https://github.com/EasyTier/EasyTier/releases
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
# 2. 通过 cargo 安装(最新开发版本)
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
|
||||
# 3. 通过 Docker 安装
|
||||
# 参见 https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F
|
||||
|
||||
# 4. Linux 快速安装
|
||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash -s install
|
||||
|
||||
# 5. MacOS 通过 Homebrew 安装
|
||||
Homebrew(MacOS/Linux):
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
|
||||
# 6. OpenWrt Luci Web 界面
|
||||
# 访问 https://github.com/EasyTier/luci-app-easytier
|
||||
|
||||
# 7.(可选)安装 Shell 补全功能:
|
||||
# Fish 补全
|
||||
easytier-core --gen-autocomplete fish > ~/.config/fish/completions/easytier-core.fish
|
||||
easytier-cli gen-autocomplete fish > ~/.config/fish/completions/easytier-cli.fish
|
||||
|
||||
```
|
||||
|
||||
Windows(推荐,请以管理员权限运行):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
通过 cargo 安装(最新开发版本):
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
|
||||
|
||||
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
|
||||
|
||||
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
|
||||
|
||||
附加步骤:
|
||||
|
||||
[一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)(系统启动时自动后台运行)
|
||||
|
||||
### 🚀 基本用法
|
||||
|
||||
#### 使用共享节点快速组网
|
||||
|
||||
EasyTier 支持使用共享公共节点快速组网。当您没有公网 IP 时,可以使用 EasyTier 社区提供的免费共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
|
||||
|
||||
当前部署的共享公共节点是 `tcp://public.easytier.cn:11010`。
|
||||
EasyTier 支持使用共享节点快速组网。当您没有公网 IP 时,可以使用公共共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
|
||||
|
||||
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name` 和 `--network-secret` 参数作为网络的唯一标识符。
|
||||
|
||||
@@ -91,14 +93,14 @@ EasyTier 支持使用共享公共节点快速组网。当您没有公网 IP 时
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
|
||||
2. 在节点 B 上运行:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
|
||||
执行成功后,可以使用 `easytier-cli` 检查网络状态:
|
||||
@@ -106,9 +108,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.5.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.5.0-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
@@ -125,7 +127,7 @@ ping 10.126.126.2
|
||||
|
||||
```bash
|
||||
# 连接多个共享节点
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010 -p udp://public.easytier.cn:11010
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<公共节点IP>:11010 -p udp://<公共节点IP>:11010
|
||||
```
|
||||
|
||||
#### 去中心化组网
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-android-jni"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::JNIEnv;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
@@ -15,7 +15,7 @@ pub struct KeyValuePair {
|
||||
}
|
||||
|
||||
// 声明外部 C 函数
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
||||
fn get_error_msg(out: *mut *const std::ffi::c_char);
|
||||
fn free_string(s: *const std::ffi::c_char);
|
||||
@@ -68,7 +68,7 @@ fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
}
|
||||
|
||||
/// 设置 TUN 文件描述符
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -87,17 +87,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析配置
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -115,17 +115,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行网络实例
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -143,17 +143,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 保持网络实例
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -165,10 +165,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
if instance_names.is_null() {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -187,10 +187,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
if array_length == 0 {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -234,17 +234,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 收集网络信息
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -304,7 +304,7 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
}
|
||||
|
||||
/// 获取最后的错误信息
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -30,7 +30,7 @@ fn set_error_msg(msg: &str) {
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn set_tun_fd(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
fd: std::ffi::c_int,
|
||||
@@ -59,7 +59,7 @@ pub unsafe extern "C" fn set_tun_fd(
|
||||
|
||||
/// # Safety
|
||||
/// Get the last error message
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||
let msg_buf = ERROR_MSG.lock().unwrap();
|
||||
if msg_buf.is_empty() {
|
||||
@@ -74,7 +74,7 @@ pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||
if s.is_null() {
|
||||
return;
|
||||
@@ -86,7 +86,7 @@ pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||
|
||||
/// # Safety
|
||||
/// Parse the config
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
@@ -105,7 +105,7 @@ pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::
|
||||
|
||||
/// # Safety
|
||||
/// Run the network instance
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
@@ -144,7 +144,7 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
|
||||
|
||||
/// # Safety
|
||||
/// Retain the network instance
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
@@ -188,7 +188,7 @@ pub unsafe extern "C" fn retain_network_instance(
|
||||
|
||||
/// # Safety
|
||||
/// Collect the network infos
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
@@ -215,7 +215,7 @@ pub unsafe extern "C" fn collect_network_infos(
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = INSTANCE_MANAGER.get_network_instance_name(instance_id) else {
|
||||
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
@@ -228,7 +228,7 @@ pub unsafe extern "C" fn collect_network_infos(
|
||||
};
|
||||
|
||||
infos[index] = KeyValuePair {
|
||||
key: std::ffi::CString::new(key.clone()).unwrap().into_raw(),
|
||||
key: std::ffi::CString::new(key).unwrap().into_raw(),
|
||||
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||
};
|
||||
index += 1;
|
||||
|
||||
@@ -1,43 +1,74 @@
|
||||
#!/data/adb/magisk/busybox sh
|
||||
MODDIR=${0%/*}
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
IP_RULE_SCRIPT="${MODDIR}/hotspot_iprule.sh"
|
||||
|
||||
ET_STATUS=""
|
||||
REDIR_STATUS=""
|
||||
# 更新module.prop文件中的description
|
||||
IS_RUNNING=false
|
||||
|
||||
# 确保辅助脚本有执行权限
|
||||
chmod +x "${IP_RULE_SCRIPT}" 2>/dev/null
|
||||
|
||||
# 更新 module.prop 文件中的 description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# 判断程序启动状态
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
ET_STATUS="已关闭"
|
||||
elif pgrep -f 'easytier-core' >/dev/null; then
|
||||
if [ -f "${MODDIR}/config/command_args"]; then
|
||||
ET_STATUS="主程序已开启(启动参数模式)"
|
||||
IS_RUNNING=false
|
||||
ET_STATUS="主程序已关闭"
|
||||
|
||||
elif pgrep -f "${MODDIR}/easytier-core" >/dev/null; then
|
||||
IS_RUNNING=true
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
ET_STATUS="主程序正在运行(启动参数模式)"
|
||||
else
|
||||
ET_STATUS="主程序已开启(配置文件模式)"
|
||||
ET_STATUS="主程序正在运行(配置文件模式)"
|
||||
fi
|
||||
|
||||
elif [ -z "$ET_STATUS" ]; then
|
||||
# 既没 disable 也没运行,说明是异常停止或未启动
|
||||
ET_STATUS="主程序启动失败或未运行"
|
||||
fi
|
||||
|
||||
#ET_STATUS不存在说明开启模块未正常运行,不修改状态
|
||||
if [ -n "$ET_STATUS" ]; then
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
rm -f "${MODDIR}/enable_IP_rule"
|
||||
${MODDIR}/hotspot_iprule.sh del
|
||||
REDIR_STATUS="转发已禁用"
|
||||
echo "热点子网转发已禁用"
|
||||
echo "[ET-NAT] IP rule disabled." >> "${MODDIR}/log.log"
|
||||
else
|
||||
touch "${MODDIR}/enable_IP_rule"
|
||||
${MODDIR}/hotspot_iprule.sh del
|
||||
${MODDIR}/hotspot_iprule.sh add_once
|
||||
REDIR_STATUS="转发已激活"
|
||||
echo "热点子网转发已激活,热点开启后将自动将热点加入转发网络(要求已配置本地网络cidr=参数)。转发规则将随着热点开关而自动开关。该状态将保持到转发被禁用为止。"
|
||||
echo "[ET-NAT] IP rule enabled." >> "${MODDIR}/log.log"
|
||||
fi
|
||||
update_module_description "${ET_STATUS} | ${REDIR_STATUS}"
|
||||
# 无论主程序是否运行,都允许切换“开关文件”的状态,以便下次生效
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
rm -f "${MODDIR}/enable_IP_rule"
|
||||
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
|
||||
REDIR_STATUS="转发已禁用"
|
||||
echo "热点子网转发已禁用"
|
||||
echo "[ET-NAT] Action: IP rule disabled." >> "${MODDIR}/log.log"
|
||||
else
|
||||
echo "主程序未正常启动,请先检查配置文件"
|
||||
touch "${MODDIR}/enable_IP_rule"
|
||||
|
||||
if [ "$IS_RUNNING" = true ]; then
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
"${IP_RULE_SCRIPT}" add_once
|
||||
echo "转发规则将立即生效,无需重启"
|
||||
else
|
||||
echo "主程序未运行,转发规则将在下次启动时生效"
|
||||
fi
|
||||
|
||||
REDIR_STATUS="转发已激活"
|
||||
echo "----------------------------------"
|
||||
echo "热点子网转发已激活"
|
||||
echo "热点开启后将自动将热点加入转发网络"
|
||||
echo "需要在配置中提前配置好 cidr 参数"
|
||||
echo "----------------------------------"
|
||||
echo "[ET-NAT] Action: IP rule enabled." >> "${MODDIR}/log.log"
|
||||
fi
|
||||
|
||||
sync
|
||||
update_module_description "${ET_STATUS}| ${REDIR_STATUS}"
|
||||
@@ -1,9 +1,19 @@
|
||||
ui_print '安装完成'
|
||||
ui_print '当前架构为' + $ARCH
|
||||
ui_print '当前系统版本为' + $API
|
||||
ui_print '安装目录为: /data/adb/modules/easytier_magisk'
|
||||
ui_print '配置文件位置: /data/adb/modules/easytier_magisk/config/config.toml'
|
||||
ui_print '如果需要自定义启动参数,可将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中内容,使用自定义启动参数时会忽略配置文件'
|
||||
ui_print '修改配置文件后在magisk app禁用应用再启动即可生效'
|
||||
ui_print '点击操作按钮可启动/关闭热点子网转发,配合easytier的子网代理功能实现手机热点访问easytier网络'
|
||||
ui_print '记得重启'
|
||||
SKIPMOUNT=false
|
||||
PROPFILE=true
|
||||
POSTFSDATA=true
|
||||
LATESTARTSERVICE=true
|
||||
|
||||
set_perm_recursive $MODPATH 0 0 0777 0777
|
||||
|
||||
ui_print "系统架构为:$ARCH"
|
||||
ui_print "系统 SDK 版本:$API"
|
||||
ui_print "EasyTier 安装位置:/data/adb/modules/easytier_magisk"
|
||||
ui_print "配置文件位置:/data/adb/modules/easytier_magisk/config/config.toml"
|
||||
ui_print "如需使用启动参数模式,请将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中的内容"
|
||||
ui_print "config 目录中存在 command_args 文件时,模块会自动忽略 config.toml 文件"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "注意!启动参数文件中不能存在 \" 和 ',配置文件则没有这个限制"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "修改配置后无需重启设备,在 Magisk 中禁用 EasyTier 模块,等待 10 秒后重新启用即可让新配置生效"
|
||||
ui_print "点击 Magisk 中模块左下角的“操作”按钮可以禁用或激活热点子网转发,使用该功能前需要在配置中提前配置好 cidr 参数"
|
||||
ui_print "模块安装完成,重启设备生效"
|
||||
@@ -2,64 +2,111 @@
|
||||
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_FILE="${MODDIR}/config/config.toml"
|
||||
COMMAND_ARGS="${MODDIR}/config/command_args"
|
||||
LOG_FILE="${MODDIR}/log.log"
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
EASYTIER="${MODDIR}/easytier-core"
|
||||
|
||||
# 处理获取到的设备型号中可能出现的空格
|
||||
BRAND=$(getprop ro.product.brand | tr ' ' '-')
|
||||
MODEL=$(getprop ro.product.model | tr ' ' '-')
|
||||
DEVICE_HOSTNAME="${BRAND}-${MODEL}"
|
||||
REDIR_STATUS=""
|
||||
|
||||
# 更新module.prop文件中的description
|
||||
# 更新 module.prop 文件中的 description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
REDIR_STATUS="转发已激活"
|
||||
else
|
||||
REDIR_STATUS="转发已禁用"
|
||||
fi
|
||||
|
||||
# 检查并初始化 TUN 设备
|
||||
if [ ! -e /dev/net/tun ]; then
|
||||
if [ ! -d /dev/net ]; then
|
||||
mkdir -p /dev/net
|
||||
fi
|
||||
|
||||
|
||||
ln -s /dev/tun /dev/net/tun
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if ls $MODDIR | grep -q "disable"; then
|
||||
update_module_description "关闭中 | ${REDIR_STATUS}"
|
||||
if pgrep -f 'easytier-core' >/dev/null; then
|
||||
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭 ..."
|
||||
pkill easytier-core # 关闭进程
|
||||
fi
|
||||
# 获取子网转发激活状态
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
REDIR_STATUS="转发已激活"
|
||||
else
|
||||
if ! pgrep -f 'easytier-core' >/dev/null; then
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
update_module_description "config.toml不存在"
|
||||
sleep 3s
|
||||
continue
|
||||
fi
|
||||
REDIR_STATUS="转发已禁用"
|
||||
fi
|
||||
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
TZ=Asia/Shanghai ${EASYTIER} $(cat ${MODDIR}/config/command_args) --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(启动参数模式) | ${REDIR_STATUS}"
|
||||
else
|
||||
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(配置文件模式) | ${REDIR_STATUS}"
|
||||
fi
|
||||
ip rule add from all lookup main
|
||||
if ! pgrep -f 'easytier-core' >/dev/null; then
|
||||
update_module_descriptio "主程序启动失败,请检查配置文件"
|
||||
fi
|
||||
else
|
||||
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在"
|
||||
# 检查模块是否被禁用
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
update_module_description "主程序已关闭 | ${REDIR_STATUS}"
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
echo "开关控制 $(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭"
|
||||
pkill -f "${EASYTIER}"
|
||||
fi
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
sleep 3s # 暂停3秒后再次执行循环
|
||||
done
|
||||
# 检查进程是否已经在运行
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${COMMAND_ARGS}" ]; then
|
||||
update_module_description "缺少配置文件或启动参数文件"
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${COMMAND_ARGS}" ]; then
|
||||
# 启动参数模式
|
||||
CMD_CONTENT=$(tr '\r\n' ' ' < "${COMMAND_ARGS}")
|
||||
|
||||
if echo "${CMD_CONTENT}" | grep -q "\-\-hostname"; then
|
||||
FINAL_ARGS="${CMD_CONTENT}"
|
||||
else
|
||||
FINAL_ARGS="${CMD_CONTENT} --hostname ${DEVICE_HOSTNAME}"
|
||||
fi
|
||||
|
||||
TZ=Asia/Shanghai "${EASYTIER}" ${FINAL_ARGS} > "${LOG_FILE}" 2>&1 &
|
||||
STR_MODE="启动参数模式"
|
||||
|
||||
# 否则读取 config.toml 的内容作为启动参数
|
||||
else
|
||||
# 配置文件模式
|
||||
if grep -q "^[[:space:]]*hostname[[:space:]]*=" "${CONFIG_FILE}"; then
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" > "${LOG_FILE}" 2>&1 &
|
||||
else
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" --hostname "${DEVICE_HOSTNAME}" > "${LOG_FILE}" 2>&1 &
|
||||
fi
|
||||
|
||||
STR_MODE="配置文件模式"
|
||||
fi
|
||||
|
||||
# 等待进程启动
|
||||
sleep 5s
|
||||
|
||||
# 启动后的扫尾工作
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
|
||||
if ! ip rule show | grep -q "lookup main"; then
|
||||
ip rule add from all lookup main
|
||||
fi
|
||||
|
||||
update_module_description "主程序正在运行(${STR_MODE})| ${REDIR_STATUS}"
|
||||
else
|
||||
update_module_description "主程序启动失败,请检查配置文件或启动参数"
|
||||
fi
|
||||
|
||||
sleep 10s
|
||||
done
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.5.0
|
||||
version=v2.6.3
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
MODDIR=${0%/*}
|
||||
pkill easytier-core # 结束 easytier-core 进程
|
||||
rm -rf $MODDIR/*
|
||||
pkill -f "${MODDIR}/easytier-core"
|
||||
|
||||
# 使用 ${MODDIR:?} 确保变量非空,避免执行 rm -rf /*
|
||||
rm -rf "${MODDIR:?}/"*
|
||||
+434
-46
@@ -38,6 +38,20 @@ dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -133,6 +147,12 @@ version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
@@ -202,6 +222,12 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
||||
|
||||
[[package]]
|
||||
name = "auto_impl"
|
||||
version = "1.3.0"
|
||||
@@ -254,14 +280,14 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.11.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -540,6 +566,16 @@ dependencies = [
|
||||
"clap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete_nushell"
|
||||
version = "4.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685bc86fd34b7467e0532a4f8435ab107960d69a243785ef0275e571b35b641a"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.47"
|
||||
@@ -598,6 +634,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -746,6 +791,15 @@ version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -894,6 +948,17 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -936,6 +1001,29 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case 0.10.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.106",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -995,7 +1083,7 @@ checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.4.5"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1004,6 +1092,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"atomic_refcell",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
@@ -1011,16 +1100,23 @@ dependencies = [
|
||||
"bytecodec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_complete_nushell",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"dbus",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more",
|
||||
"easytier-rpc-build",
|
||||
"encoding",
|
||||
"flume",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"git-version",
|
||||
@@ -1036,6 +1132,8 @@ dependencies = [
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"multimap",
|
||||
@@ -1046,7 +1144,9 @@ dependencies = [
|
||||
"network-interface",
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"petgraph 0.8.2",
|
||||
"pin-project-lite",
|
||||
@@ -1056,8 +1156,11 @@ dependencies = [
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
"prost-reflect-build",
|
||||
"prost-types",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"prost-wkt-types",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"regex",
|
||||
@@ -1073,10 +1176,13 @@ dependencies = [
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"socket2 0.5.10",
|
||||
"strum",
|
||||
"stun_codec",
|
||||
"sys-locale",
|
||||
"tabled",
|
||||
"terminal_size",
|
||||
"thiserror 1.0.69",
|
||||
"thunk-rs",
|
||||
"time",
|
||||
@@ -1091,16 +1197,19 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tun-easytier",
|
||||
"unicode-width 0.1.11",
|
||||
"url",
|
||||
"uuid",
|
||||
"version-compare",
|
||||
"which 7.0.3",
|
||||
"wildmatch",
|
||||
"winapi",
|
||||
"windivert",
|
||||
"windows 0.52.0",
|
||||
"windows-service",
|
||||
"windows-sys 0.52.0",
|
||||
"winreg 0.52.0",
|
||||
"x25519-dalek",
|
||||
"zerocopy 0.7.35",
|
||||
"zip",
|
||||
"zstd",
|
||||
@@ -1126,8 +1235,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "easytier-rpc-build"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24829168c28f6a448f57d18116c255dcbd2b8c25e76dbc60f6cd16d68ad2cf07"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost-build",
|
||||
@@ -1253,6 +1360,17 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -1264,10 +1382,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastbloom"
|
||||
version = "0.14.0"
|
||||
name = "etherparse"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18c1ddb9231d8554c2d6bdf4cfaabf0c59251658c68b6c95cd52dd0c513a912a"
|
||||
checksum = "827292ea592108849932ad8e30218f8b1f21c0dfd0696698a18b5d0aed62d990"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastbloom"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"libm",
|
||||
@@ -1280,6 +1407,9 @@ name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
@@ -1310,6 +1440,18 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1346,6 +1488,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -1496,6 +1648,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -1605,9 +1767,9 @@ checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5"
|
||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
@@ -2064,6 +2226,15 @@ dependencies = [
|
||||
"hashbrown 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@@ -2073,6 +2244,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
@@ -2161,15 +2341,6 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
@@ -2230,7 +2401,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kcp-sys"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/EasyTier/kcp-sys?rev=71eff18c573a4a71bf99c7fabc6a8b9f211c84c1#71eff18c573a4a71bf99c7fabc6a8b9f211c84c1"
|
||||
source = "git+https://github.com/EasyTier/kcp-sys?rev=94964794caaed5d388463137da59b97499619e5f#94964794caaed5d388463137da59b97499619e5f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_impl",
|
||||
@@ -2503,7 +2674,7 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b27250baa967a15214e57384dd6228c59afbccb15ab8f97207c9758917544bf5"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"convert_case 0.8.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
@@ -2516,7 +2687,7 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c844efa85d53b5adc3b326520f3a108c3a737b7534ee10d406f81884e7e71b3c"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"convert_case 0.8.0",
|
||||
"ctor",
|
||||
"napi-derive-backend-ohos",
|
||||
"proc-macro2",
|
||||
@@ -2564,7 +2735,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.1.6",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
@@ -2689,6 +2860,12 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.0"
|
||||
@@ -2810,6 +2987,12 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.109"
|
||||
@@ -2822,6 +3005,15 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered_hash_map"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6c699f8a30f345785be969deed7eee4c73a5de58c7faf61d6a3251ef798ff61"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "papergrid"
|
||||
version = "0.12.0"
|
||||
@@ -2874,12 +3066,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3045,6 +3237,18 @@ dependencies = [
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
@@ -3251,6 +3455,52 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"inventory",
|
||||
"prost",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"typetag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-build"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-types"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
@@ -3281,10 +3531,22 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.13"
|
||||
name = "quinn-plaintext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
checksum = "f3e617feaeb6493018fa35fc47ae8b630ac8903d8159e9e747018841b99bad3d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"quinn-proto",
|
||||
"seahash",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fastbloom",
|
||||
@@ -3652,14 +3914,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.1"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.2.1",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework 3.5.0",
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3683,9 +3945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
@@ -3696,10 +3958,10 @@ dependencies = [
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.5.0",
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3761,6 +4023,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@@ -3776,9 +4044,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.0"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.10.1",
|
||||
@@ -3956,6 +4224,12 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
@@ -3987,6 +4261,23 @@ dependencies = [
|
||||
"managed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snow"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "599b506ccc4aff8cf7844bc42cf783009a434c1e26c964432560fb6d6ad02d82"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"blake2",
|
||||
"chacha20poly1305",
|
||||
"curve25519-dalek",
|
||||
"getrandom 0.3.3",
|
||||
"ring",
|
||||
"rustc_version",
|
||||
"sha2",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
@@ -4007,6 +4298,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -4019,6 +4319,27 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stun_codec"
|
||||
version = "0.3.5"
|
||||
@@ -4369,9 +4690,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.8.3"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -4382,10 +4703,11 @@ dependencies = [
|
||||
"httparse",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"simdutf8",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"webpki-roots 0.26.11",
|
||||
"webpki-roots 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4617,12 +4939,42 @@ dependencies = [
|
||||
"wintun",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "typetag"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"typetag-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typetag-impl"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
@@ -4653,6 +5005,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
@@ -4895,9 +5253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
|
||||
checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -4987,6 +5345,36 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windivert"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/EasyTier/windivert-rust.git?rev=adcc56d1550f7b5377ec2b3429f413ee24a77375#adcc56d1550f7b5377ec2b3429f413ee24a77375"
|
||||
dependencies = [
|
||||
"etherparse",
|
||||
"thiserror 1.0.69",
|
||||
"windivert-sys",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windivert-sys"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/EasyTier/windivert-rust.git?rev=adcc56d1550f7b5377ec2b3429f413ee24a77375#adcc56d1550f7b5377ec2b3429f413ee24a77375"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"thiserror 1.0.69",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-uptime"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
@@ -12,6 +12,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
guarden = "0.1"
|
||||
|
||||
# Axum web framework
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
|
||||
+9
-9
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"element-plus": "^2.8.8",
|
||||
"vue": "^3.5.18",
|
||||
@@ -1220,13 +1220,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -1616,9 +1616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.13.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"easytier-uptime-frontend": "link:",
|
||||
"element-plus": "^2.8.8",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
||||
QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
|
||||
@@ -14,7 +14,7 @@ use crate::api::{
|
||||
models::*,
|
||||
};
|
||||
use crate::db::entity::{self, health_records, shared_nodes};
|
||||
use crate::db::{operations::*, Db};
|
||||
use crate::db::{Db, operations::*};
|
||||
use crate::health_checker_manager::HealthCheckerManager;
|
||||
use axum_extra::extract::Query;
|
||||
use std::sync::Arc;
|
||||
@@ -273,7 +273,7 @@ pub struct InstanceFilterParams {
|
||||
use crate::config::AppConfig;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use chrono::{Duration, Utc};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -370,19 +370,19 @@ pub async fn admin_get_nodes(
|
||||
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
||||
filtered_ids = Some(ids);
|
||||
}
|
||||
if let Some(tags) = filters.tags {
|
||||
if !tags.is_empty() {
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(tags) = filters.tags
|
||||
&& !tags.is_empty()
|
||||
{
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use axum::Router;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::db::entity::*;
|
||||
use crate::db::Db;
|
||||
use crate::db::entity::*;
|
||||
use sea_orm::*;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
/// 数据清理策略配置
|
||||
|
||||
@@ -5,12 +5,12 @@ pub mod operations;
|
||||
use std::fmt;
|
||||
|
||||
use sea_orm::{
|
||||
prelude::*, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||
QueryFilter as _, Set, SqlxSqliteConnector, Statement, TransactionTrait as _,
|
||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
||||
SqlxSqliteConnector, Statement, TransactionTrait as _, prelude::*, sea_query::OnConflict,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{migrate::MigrateDatabase as _, Sqlite, SqlitePool};
|
||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _};
|
||||
|
||||
use crate::migrator;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::api::CreateNodeRequest;
|
||||
use crate::db::entity::*;
|
||||
use crate::db::Db;
|
||||
use crate::db::HealthStats;
|
||||
use crate::db::HealthStatus;
|
||||
use crate::db::entity::*;
|
||||
use sea_orm::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@ use std::{
|
||||
use anyhow::Context as _;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
|
||||
scoped_task::ScopedTask,
|
||||
common::config::{
|
||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
},
|
||||
defer,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
};
|
||||
use guarden::defer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use crate::db::{
|
||||
Db, HealthStatus,
|
||||
entity::shared_nodes,
|
||||
operations::{HealthOperations, NodeOperations},
|
||||
Db, HealthStatus,
|
||||
};
|
||||
|
||||
pub struct HealthCheckOneNode {
|
||||
@@ -240,7 +240,7 @@ pub struct HealthChecker {
|
||||
db: Db,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
inst_id_map: DashMap<i32, uuid::Uuid>,
|
||||
node_tasks: DashMap<i32, ScopedTask<()>>,
|
||||
node_tasks: DashMap<i32, AbortOnDropHandle<()>>,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
node_cfg: Arc<DashMap<i32, TomlConfigLoader>>,
|
||||
}
|
||||
@@ -359,6 +359,7 @@ impl HealthChecker {
|
||||
)
|
||||
.parse()
|
||||
.with_context(|| "failed to parse peer uri")?,
|
||||
peer_public_key: None,
|
||||
}]);
|
||||
|
||||
let inst_id = inst_id.unwrap_or(uuid::Uuid::new_v4());
|
||||
@@ -464,7 +465,7 @@ impl HealthChecker {
|
||||
}
|
||||
|
||||
// 启动健康检查任务
|
||||
let task = ScopedTask::from(tokio::spawn(Self::node_health_check_task(
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(Self::node_health_check_task(
|
||||
node_id,
|
||||
cfg.get_id(),
|
||||
Arc::clone(&self.instance_mgr),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use tokio::time::{interval, Interval};
|
||||
use tokio::time::{Interval, interval};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
db::{entity::shared_nodes, operations::NodeOperations, Db},
|
||||
db::{Db, entity::shared_nodes, operations::NodeOperations},
|
||||
health_checker::HealthChecker,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ mod migrator;
|
||||
use api::routes::create_routes;
|
||||
use clap::Parser;
|
||||
use config::AppConfig;
|
||||
use db::{operations::NodeOperations, Db};
|
||||
use easytier::utils::init_logger;
|
||||
use db::{Db, operations::NodeOperations};
|
||||
use easytier::common::log;
|
||||
use health_checker::HealthChecker;
|
||||
use health_checker_manager::HealthCheckerManager;
|
||||
use std::env;
|
||||
@@ -42,14 +42,16 @@ async fn main() -> anyhow::Result<()> {
|
||||
let config = AppConfig::default();
|
||||
|
||||
// 初始化日志
|
||||
let _ = init_logger(&config.logging, false);
|
||||
let _ = log::init(&config.logging, false);
|
||||
|
||||
// 解析命令行参数
|
||||
let args = Args::parse();
|
||||
|
||||
// 如果提供了管理员密码,设置环境变量
|
||||
if let Some(password) = args.admin_password {
|
||||
env::set_var("ADMIN_PASSWORD", password);
|
||||
unsafe {
|
||||
env::set_var("ADMIN_PASSWORD", password);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.3",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
@@ -53,10 +53,10 @@
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.5.0"
|
||||
version = "2.6.3"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -11,15 +11,6 @@ edition = "2021"
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
|
||||
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[target.i686-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[dependencies]
|
||||
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
||||
@@ -54,6 +45,8 @@ tauri-plugin-os = "2.3.0"
|
||||
uuid = "1.17.0"
|
||||
async-trait = "0.1.89"
|
||||
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
winapi = { version = "0.3.9", features = ["securitybaseapi", "processthreadsapi"] }
|
||||
@@ -64,6 +57,14 @@ libc = "0.2"
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework-sys = "2.9.0"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
fn main() {
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
.unwrap_or_default()
|
||||
.contains("aarch64")
|
||||
{
|
||||
thunk::thunk();
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"core:tray:allow-set-show-menu-on-left-click",
|
||||
"core:tray:allow-set-tooltip",
|
||||
"vpnservice:allow-ping",
|
||||
"vpnservice:allow-get-vpn-status",
|
||||
"vpnservice:allow-prepare-vpn",
|
||||
"vpnservice:allow-start-vpn",
|
||||
"vpnservice:allow-stop-vpn",
|
||||
@@ -47,4 +48,4 @@
|
||||
"os:allow-platform",
|
||||
"os:allow-locale"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@@ -14,6 +15,35 @@ val tauriProperties = Properties().apply {
|
||||
}
|
||||
}
|
||||
|
||||
val versionPattern = Regex("""^(\d+)\.(\d+)\.(\d+)$""")
|
||||
|
||||
val tauriVersionName = tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } ?: run {
|
||||
val tauriConfFile = file("../../../tauri.conf.json")
|
||||
check(tauriConfFile.exists()) { "Missing tauri.conf.json at ${tauriConfFile.path}" }
|
||||
|
||||
val tauriConf = tauriConfFile.reader(Charsets.UTF_8).use { JsonSlurper().parse(it) as? Map<*, *> }
|
||||
?: error("Failed to parse ${tauriConfFile.path} as a JSON object")
|
||||
tauriConf["version"] as? String
|
||||
?: error("Missing string field \"version\" in ${tauriConfFile.path}")
|
||||
}
|
||||
|
||||
val tauriVersionMatch = versionPattern.matchEntire(tauriVersionName)
|
||||
?: error("Android version must use x.y.z format, but got \"$tauriVersionName\"")
|
||||
|
||||
val tauriVersionCode = if (tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } != null) {
|
||||
val versionCodeProp = tauriProperties.getProperty("tauri.android.versionCode")
|
||||
if (versionCodeProp != null) {
|
||||
versionCodeProp.toIntOrNull()
|
||||
?: error("Property \"tauri.android.versionCode\" must be an integer, but got \"$versionCodeProp\"")
|
||||
} else {
|
||||
val (major, minor, patch) = tauriVersionMatch.destructured
|
||||
major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt()
|
||||
}
|
||||
} else {
|
||||
val (major, minor, patch) = tauriVersionMatch.destructured
|
||||
major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
namespace = "com.kkrainbow.easytier"
|
||||
@@ -22,8 +52,8 @@ android {
|
||||
applicationId = "com.kkrainbow.easytier"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
versionCode = tauriVersionCode
|
||||
versionName = tauriVersionName
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
@@ -82,4 +112,4 @@ dependencies {
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
use super::Command;
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::process::{Command as StdCommand, Output};
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
use super::Command;
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read as _;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{ExitStatus, Output};
|
||||
|
||||
@@ -23,13 +25,15 @@ use std::ffi::{CString, OsString};
|
||||
use std::io;
|
||||
use std::mem;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
|
||||
use libc::{fcntl, fileno, waitpid, EINTR, F_GETOWN};
|
||||
use libc::{EINTR, SHUT_WR, fileno, wait};
|
||||
use security_framework_sys::authorization::{
|
||||
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
||||
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
|
||||
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
||||
};
|
||||
|
||||
const ENV_PATH: &str = "PATH";
|
||||
@@ -71,7 +75,7 @@ macro_rules! make_cstring {
|
||||
};
|
||||
}
|
||||
|
||||
unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> i32 {
|
||||
unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> io::Result<ExitStatus> {
|
||||
let mut authref: AuthorizationRef = ptr::null_mut();
|
||||
let mut pipe: *mut libc::FILE = ptr::null_mut();
|
||||
|
||||
@@ -82,7 +86,7 @@ unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> i32 {
|
||||
&mut authref,
|
||||
) != errAuthorizationSuccess
|
||||
{
|
||||
return -1;
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if AuthorizationExecuteWithPrivileges(
|
||||
authref,
|
||||
@@ -93,22 +97,66 @@ unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> i32 {
|
||||
) != errAuthorizationSuccess
|
||||
{
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return -1;
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let fd = fileno(pipe);
|
||||
if fd == -1 {
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
// We never send input to the elevated GUI. Close the parent write half so
|
||||
// the child sees EOF on stdin instead of waiting forever.
|
||||
if libc::shutdown(fd, SHUT_WR) == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
libc::fclose(pipe);
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// AuthorizationExecuteWithPrivileges wires the tool's stdin/stdout to a
|
||||
// bidirectional pipe. Drain stdout so the child can't block on a full pipe.
|
||||
let read_fd = libc::dup(fd);
|
||||
if read_fd == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
libc::fclose(pipe);
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return Err(err);
|
||||
}
|
||||
let mut pipe_file = unsafe { File::from_raw_fd(read_fd) };
|
||||
let mut sink = [0_u8; 8192];
|
||||
loop {
|
||||
match pipe_file.read(&mut sink) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(err) => {
|
||||
libc::fclose(pipe);
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pid = fcntl(fileno(pipe), F_GETOWN, 0);
|
||||
let mut status = 0;
|
||||
loop {
|
||||
let r = waitpid(pid, &mut status, 0);
|
||||
let r = wait(&mut status);
|
||||
if r == -1 && io::Error::last_os_error().raw_os_error() == Some(EINTR) {
|
||||
continue;
|
||||
} else if r == -1 {
|
||||
let err = io::Error::last_os_error();
|
||||
libc::fclose(pipe);
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
return Err(err);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
libc::fclose(pipe);
|
||||
AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
|
||||
status
|
||||
Ok(ExitStatus::from_raw(status))
|
||||
}
|
||||
|
||||
fn runas_root_gui(cmd: &Command) -> io::Result<ExitStatus> {
|
||||
@@ -126,7 +174,7 @@ fn runas_root_gui(cmd: &Command) -> io::Result<ExitStatus> {
|
||||
let mut argv: Vec<_> = args.iter().map(|x| x.as_ptr()).collect();
|
||||
argv.push(ptr::null());
|
||||
|
||||
unsafe { Ok(mem::transmute(gui_runas(prog.as_ptr(), argv.as_ptr()))) }
|
||||
unsafe { gui_runas(prog.as_ptr(), argv.as_ptr()) }
|
||||
}
|
||||
|
||||
/// The implementation of state check and elevated executing varies on each platform
|
||||
|
||||
@@ -11,11 +11,11 @@ use std::process::{ExitStatus, Output};
|
||||
use winapi::shared::minwindef::{DWORD, LPVOID};
|
||||
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
|
||||
use winapi::um::securitybaseapi::GetTokenInformation;
|
||||
use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||
use windows::core::{w, HSTRING, PCWSTR};
|
||||
use winapi::um::winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::Shell::ShellExecuteW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
use windows::core::{HSTRING, PCWSTR, w};
|
||||
|
||||
/// The implementation of state check and elevated executing varies on each platform
|
||||
impl Command {
|
||||
|
||||
+386
-111
@@ -14,16 +14,23 @@ use easytier::rpc_service::remote_client::{
|
||||
};
|
||||
use easytier::web_client::{self, WebClient};
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||
common::{
|
||||
config::{
|
||||
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
||||
},
|
||||
log,
|
||||
},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::ApiRpcServer,
|
||||
tunnel::TunnelListener,
|
||||
tunnel::ring::RingTunnelListener,
|
||||
utils::{self},
|
||||
tunnel::tcp::TcpTunnelListener,
|
||||
utils::panic::setup_panic_handler,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
||||
use uuid::Uuid;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager as _};
|
||||
@@ -40,8 +47,21 @@ static RPC_RING_UUID: once_cell::sync::Lazy<uuid::Uuid> =
|
||||
static CLIENT_MANAGER: once_cell::sync::Lazy<RwLock<Option<manager::GUIClientManager>>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
||||
|
||||
static RING_RPC_SERVER: once_cell::sync::Lazy<RwLock<Option<ApiRpcServer<RingTunnelListener>>>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
||||
type BoxedTunnelListener = Box<dyn TunnelListener>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum RpcServerKind {
|
||||
Ring,
|
||||
Tcp,
|
||||
}
|
||||
|
||||
struct RpcServer {
|
||||
kind: RpcServerKind,
|
||||
_server: ApiRpcServer<BoxedTunnelListener>,
|
||||
bind_url: Option<url::Url>,
|
||||
}
|
||||
static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(None));
|
||||
|
||||
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
||||
@@ -100,7 +120,7 @@ async fn run_network_instance(
|
||||
let client_manager = get_client_manager!()?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User)
|
||||
.await?;
|
||||
client_manager
|
||||
.handle_run_network_instance(app.clone(), cfg, save)
|
||||
@@ -128,7 +148,6 @@ async fn collect_network_info(
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_logging_level(level: String) -> Result<(), String> {
|
||||
println!("Setting logging level to: {}", level);
|
||||
get_client_manager!()?
|
||||
.set_logging_level(level.clone())
|
||||
.await
|
||||
@@ -173,7 +192,7 @@ async fn remove_network_instance(app: AppHandle, instance_id: String) -> Result<
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.post_remove_network_instances_hook(&app, &[instance_id])
|
||||
.post_stop_network_instances_hook(&app)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -189,6 +208,20 @@ async fn update_network_config_state(
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
let client_manager = get_client_manager!()?;
|
||||
if !disabled {
|
||||
let (cfg, source) = client_manager
|
||||
.handle_get_network_config_with_source(app.clone(), instance_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(
|
||||
&app,
|
||||
&toml_config,
|
||||
manager::PersistedConfigSource::from_runtime_source(source),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
client_manager
|
||||
.handle_update_network_state(app.clone(), instance_id, disabled)
|
||||
.await
|
||||
@@ -196,7 +229,11 @@ async fn update_network_config_state(
|
||||
|
||||
if disabled {
|
||||
client_manager
|
||||
.post_remove_network_instances_hook(&app, &[instance_id])
|
||||
.post_stop_network_instances_hook(&app)
|
||||
.await?;
|
||||
} else {
|
||||
client_manager
|
||||
.post_run_network_instance_hook(&app, &instance_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -241,7 +278,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
|
||||
#[tauri::command]
|
||||
async fn load_configs(
|
||||
app: AppHandle,
|
||||
configs: Vec<NetworkConfig>,
|
||||
configs: Vec<manager::StoredGuiConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
get_client_manager!()?
|
||||
@@ -322,8 +359,25 @@ fn get_service_status() -> Result<&'static str, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_normal_mode_rpc_portal(portal: &str) -> Result<(url::Url, url::Url), String> {
|
||||
let portal_url: url::Url = portal
|
||||
.parse()
|
||||
.map_err(|e| format!("invalid rpc portal: {:#}", e))?;
|
||||
let bind_url = portal_url.clone();
|
||||
let mut connect_url = portal_url.clone();
|
||||
// if bind addr is 0.0.0.0, should convert to 127.0.0.1
|
||||
if connect_url.host_str() == Some("0.0.0.0") {
|
||||
connect_url.set_host(Some("127.0.0.1")).unwrap();
|
||||
}
|
||||
Ok((bind_url, connect_url))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(), String> {
|
||||
async fn init_rpc_connection(
|
||||
_app: AppHandle,
|
||||
is_normal_mode: bool,
|
||||
url: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let mut client_manager_guard =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), CLIENT_MANAGER.write())
|
||||
.await
|
||||
@@ -331,41 +385,72 @@ async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(),
|
||||
let mut instance_manager_guard = INSTANCE_MANAGER
|
||||
.try_write()
|
||||
.map_err(|_| "Failed to acquire write lock for instance manager")?;
|
||||
let mut ring_rpc_server_guard = RING_RPC_SERVER
|
||||
.try_write()
|
||||
.map_err(|_| "Failed to acquire write lock for ring rpc server")?;
|
||||
let mut rpc_server_guard = RPC_SERVER
|
||||
.try_lock()
|
||||
.map_err(|_| "Failed to acquire lock for rpc server")?;
|
||||
|
||||
let normal_mode = url.is_none();
|
||||
if normal_mode {
|
||||
let mut client_url = url.clone();
|
||||
if is_normal_mode {
|
||||
let instance_manager = if let Some(im) = instance_manager_guard.take() {
|
||||
im
|
||||
} else {
|
||||
Arc::new(NetworkInstanceManager::new())
|
||||
};
|
||||
let rpc_server = if let Some(rpc_server) = ring_rpc_server_guard.take() {
|
||||
rpc_server
|
||||
|
||||
let portal = url.and_then(|s| {
|
||||
let trimmed = s.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
});
|
||||
|
||||
let (desired_kind, bind_url, connect_url) = if let Some(portal) = portal {
|
||||
let (bind_url, connect_url) = normalize_normal_mode_rpc_portal(&portal)?;
|
||||
(RpcServerKind::Tcp, Some(bind_url), Some(connect_url))
|
||||
} else {
|
||||
ApiRpcServer::from_tunnel(
|
||||
RingTunnelListener::new(
|
||||
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
|
||||
),
|
||||
instance_manager.clone(),
|
||||
)
|
||||
.with_rx_timeout(None)
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
(RpcServerKind::Ring, None, None)
|
||||
};
|
||||
|
||||
let need_restart = rpc_server_guard
|
||||
.as_ref()
|
||||
.map(|x| x.kind != desired_kind || x.bind_url != bind_url)
|
||||
.unwrap_or(true);
|
||||
|
||||
if need_restart {
|
||||
*rpc_server_guard = None;
|
||||
|
||||
let tunnel: BoxedTunnelListener = match desired_kind {
|
||||
RpcServerKind::Ring => Box::new(RingTunnelListener::new(
|
||||
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
|
||||
)),
|
||||
RpcServerKind::Tcp => Box::new(TcpTunnelListener::new(
|
||||
bind_url.clone().expect("tcp rpc must have bind url"),
|
||||
)),
|
||||
};
|
||||
|
||||
let rpc_server = ApiRpcServer::from_tunnel(tunnel, instance_manager.clone())
|
||||
.with_rx_timeout(None)
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
*rpc_server_guard = Some(RpcServer {
|
||||
kind: desired_kind,
|
||||
_server: rpc_server,
|
||||
bind_url,
|
||||
});
|
||||
}
|
||||
|
||||
*instance_manager_guard = Some(instance_manager);
|
||||
*ring_rpc_server_guard = Some(rpc_server);
|
||||
client_url = connect_url.map(|u| u.to_string());
|
||||
} else {
|
||||
*ring_rpc_server_guard = None;
|
||||
*rpc_server_guard = None;
|
||||
}
|
||||
|
||||
let client_manager = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
manager::GUIClientManager::new(url),
|
||||
manager::GUIClientManager::new(client_url),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "connect remote rpc timed out".to_string())?
|
||||
@@ -373,7 +458,7 @@ async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(),
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
*client_manager_guard = Some(client_manager);
|
||||
|
||||
if !normal_mode {
|
||||
if !is_normal_mode {
|
||||
drop(WEB_CLIENT.write().await.take());
|
||||
if let Some(instance_manager) = instance_manager_guard.take() {
|
||||
instance_manager
|
||||
@@ -406,11 +491,17 @@ async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), Stri
|
||||
|
||||
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
||||
|
||||
let web_client =
|
||||
web_client::run_web_client(url.as_str(), None, None, instance_manager, Some(hooks))
|
||||
.await
|
||||
.with_context(|| "Failed to initialize web client")
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
let web_client = web_client::run_web_client(
|
||||
url.as_str(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
instance_manager,
|
||||
Some(hooks),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "Failed to initialize web client")
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
*web_client_guard = Some(web_client);
|
||||
Ok(())
|
||||
}
|
||||
@@ -450,31 +541,34 @@ async fn get_log_dir_path(app: tauri::AppHandle) -> Result<String, String> {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn toggle_window_visibility(app: &tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let visible = if window.is_visible().unwrap_or_default() {
|
||||
if window.is_minimized().unwrap_or_default() {
|
||||
let _ = window.unminimize();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
let visible = window.is_visible().unwrap_or_default();
|
||||
let minimized = window.is_minimized().unwrap_or_default();
|
||||
let focused = window.is_focused().unwrap_or_default();
|
||||
|
||||
let should_show = !visible || minimized || !focused;
|
||||
if should_show {
|
||||
if !visible {
|
||||
let _ = window.show();
|
||||
}
|
||||
if minimized {
|
||||
let _ = window.unminimize();
|
||||
}
|
||||
if !focused {
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
let _ = set_dock_visibility(app.clone(), true);
|
||||
} else {
|
||||
let _ = window.show();
|
||||
false
|
||||
};
|
||||
if visible {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
let _ = window.set_focus();
|
||||
let _ = set_dock_visibility(app.clone(), false);
|
||||
}
|
||||
let _ = set_dock_visibility(app.clone(), !visible);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exe_path() -> String {
|
||||
if let Ok(appimage_path) = std::env::var("APPIMAGE") {
|
||||
if !appimage_path.is_empty() {
|
||||
return appimage_path;
|
||||
}
|
||||
if let Ok(appimage_path) = std::env::var("APPIMAGE")
|
||||
&& !appimage_path.is_empty()
|
||||
{
|
||||
return appimage_path;
|
||||
}
|
||||
std::env::current_exe()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
@@ -508,8 +602,8 @@ mod manager {
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::rpc_service::logger::LoggerRpcService;
|
||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::tunnel::TunnelConnector;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::web_client::WebClientHooks;
|
||||
|
||||
pub(super) struct GuiHooks {
|
||||
@@ -524,7 +618,11 @@ mod manager {
|
||||
) -> Result<(), String> {
|
||||
let client_manager = get_client_manager!()?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&self.app, cfg)
|
||||
.pre_run_network_instance_hook(
|
||||
&self.app,
|
||||
cfg,
|
||||
PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -538,19 +636,92 @@ mod manager {
|
||||
async fn post_remove_network_instances(&self, ids: &[uuid::Uuid]) -> Result<(), String> {
|
||||
let client_manager = get_client_manager!()?;
|
||||
client_manager
|
||||
.post_remove_network_instances_hook(&self.app, ids)
|
||||
.post_remote_remove_network_instances_hook(&self.app, ids)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub(super) enum PersistedConfigSource {
|
||||
User,
|
||||
Webhook,
|
||||
#[serde(other)]
|
||||
#[default]
|
||||
Legacy,
|
||||
}
|
||||
|
||||
impl PersistedConfigSource {
|
||||
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
||||
match source {
|
||||
ConfigSource::User => Self::User,
|
||||
ConfigSource::Webhook => Self::Webhook,
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_persisted(self, incoming: Self) -> Self {
|
||||
match (self, incoming) {
|
||||
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
||||
// ownership until webhook sync or an explicit user save repairs it.
|
||||
(Self::Webhook, Self::User) | (Self::Legacy, Self::User) => self,
|
||||
(_, next) => next,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_runtime_source(self) -> ConfigSource {
|
||||
match self {
|
||||
Self::User | Self::Legacy => ConfigSource::User,
|
||||
Self::Webhook => ConfigSource::Webhook,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, target_os = "android"))]
|
||||
fn is_webhook_like(self) -> bool {
|
||||
matches!(self, Self::Webhook)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
||||
pub(super) struct GUIConfig {
|
||||
inst_id: String,
|
||||
pub(crate) config: NetworkConfig,
|
||||
source: PersistedConfigSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(super) struct StoredGuiConfig {
|
||||
config: NetworkConfig,
|
||||
#[serde(default)]
|
||||
source: PersistedConfigSource,
|
||||
}
|
||||
|
||||
impl GUIConfig {
|
||||
fn new(inst_id: String, config: NetworkConfig, source: PersistedConfigSource) -> Self {
|
||||
Self {
|
||||
inst_id,
|
||||
config,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_stored(self) -> StoredGuiConfig {
|
||||
StoredGuiConfig {
|
||||
config: self.config,
|
||||
source: self.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
||||
fn get_network_inst_id(&self) -> &str {
|
||||
&self.0
|
||||
&self.inst_id
|
||||
}
|
||||
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
||||
Ok(self.1.clone())
|
||||
Ok(self.config.clone())
|
||||
}
|
||||
fn get_network_config_source(&self) -> ConfigSource {
|
||||
self.source.to_runtime_source()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,13 +738,12 @@ mod manager {
|
||||
}
|
||||
|
||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let configs: Result<Vec<String>, _> = self
|
||||
let configs = self
|
||||
.network_configs
|
||||
.iter()
|
||||
.map(|entry| serde_json::to_string(&entry.value().1))
|
||||
.collect();
|
||||
let payload = format!("[{}]", configs?.join(","));
|
||||
app.emit_str("save_configs", payload)?;
|
||||
.map(|entry| entry.value().clone().into_stored())
|
||||
.collect::<Vec<_>>();
|
||||
app.emit("save_configs", configs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -592,8 +762,14 @@ mod manager {
|
||||
app: &AppHandle,
|
||||
inst_id: Uuid,
|
||||
cfg: NetworkConfig,
|
||||
source: PersistedConfigSource,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = GUIConfig(inst_id.to_string(), cfg);
|
||||
let source = self
|
||||
.network_configs
|
||||
.get(&inst_id)
|
||||
.map(|existing| existing.source.merge_persisted(source))
|
||||
.unwrap_or(source);
|
||||
let config = GUIConfig::new(inst_id.to_string(), cfg, source);
|
||||
self.network_configs.insert(inst_id, config);
|
||||
self.save_configs(app)
|
||||
}
|
||||
@@ -605,8 +781,14 @@ mod manager {
|
||||
app: AppHandle,
|
||||
network_inst_id: Uuid,
|
||||
network_config: NetworkConfig,
|
||||
source: ConfigSource,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.save_config(&app, network_inst_id, network_config)?;
|
||||
self.save_config(
|
||||
&app,
|
||||
network_inst_id,
|
||||
network_config,
|
||||
PersistedConfigSource::from_runtime_source(source),
|
||||
)?;
|
||||
self.enabled_networks.insert(network_inst_id);
|
||||
self.save_enabled_networks(&app)?;
|
||||
Ok(())
|
||||
@@ -621,7 +803,9 @@ mod manager {
|
||||
self.network_configs.remove(network_inst_id);
|
||||
self.enabled_networks.remove(network_inst_id);
|
||||
}
|
||||
self.save_configs(&app)
|
||||
self.save_configs(&app)?;
|
||||
self.save_enabled_networks(&app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_network_config_state(
|
||||
@@ -721,17 +905,36 @@ mod manager {
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.1.no_tun())
|
||||
.filter_map(|c| c.1.instance_id().parse::<uuid::Uuid>().ok())
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_enabled_instances_with_webhook_like_tun_ids(
|
||||
&self,
|
||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
self.storage
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter(|v| v.source.is_webhook_like())
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
webhook_only: bool,
|
||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
let inst_ids: Vec<uuid::Uuid> = self.get_enabled_instances_with_tun_ids().collect();
|
||||
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
||||
self.get_enabled_instances_with_webhook_like_tun_ids()
|
||||
.collect()
|
||||
} else {
|
||||
self.get_enabled_instances_with_tun_ids().collect()
|
||||
};
|
||||
for inst_id in inst_ids {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
@@ -752,16 +955,32 @@ mod manager {
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
cfg: &easytier::common::config::TomlConfigLoader,
|
||||
source: PersistedConfigSource,
|
||||
) -> Result<(), String> {
|
||||
let instance_id = cfg.get_id();
|
||||
app.emit("pre_run_network_instance", instance_id)
|
||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !cfg.get_flags().no_tun {
|
||||
self.disable_instances_with_tun(app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
match source {
|
||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||
self.disable_instances_with_tun(app, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
PersistedConfigSource::Webhook => {
|
||||
self.disable_instances_with_tun(app, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||
return Err(
|
||||
"Android only supports one active TUN network; user-managed VPN remains active"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.storage
|
||||
@@ -769,6 +988,7 @@ mod manager {
|
||||
app,
|
||||
instance_id,
|
||||
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
||||
source,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -791,20 +1011,21 @@ mod manager {
|
||||
let app_clone = app.clone();
|
||||
let instance_id_clone = *instance_id;
|
||||
tokio::spawn(async move {
|
||||
let instance_id_str = instance_id_clone.to_string();
|
||||
loop {
|
||||
match event_receiver.recv().await {
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _)) => {
|
||||
let _ = app_clone.emit("dhcp_ip_changed", instance_id_clone);
|
||||
let _ = app_clone.emit("dhcp_ip_changed", &instance_id_str);
|
||||
}
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::ProxyCidrsUpdated(_, _)) => {
|
||||
let _ = app_clone.emit("proxy_cidrs_updated", instance_id_clone);
|
||||
let _ = app_clone.emit("proxy_cidrs_updated", &instance_id_str);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
let _ = app_clone.emit("event_lagged", instance_id_clone);
|
||||
let _ = app_clone.emit("event_lagged", &instance_id_str);
|
||||
event_receiver = event_receiver.resubscribe();
|
||||
}
|
||||
}
|
||||
@@ -816,20 +1037,29 @@ mod manager {
|
||||
|
||||
self.storage.enabled_networks.insert(*instance_id);
|
||||
|
||||
app.emit("post_run_network_instance", instance_id)
|
||||
app.emit("post_run_network_instance", instance_id.to_string())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn post_remove_network_instances_hook(
|
||||
pub(super) async fn post_remote_remove_network_instances_hook(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
_ids: &[uuid::Uuid],
|
||||
ids: &[uuid::Uuid],
|
||||
) -> Result<(), String> {
|
||||
self.storage
|
||||
.enabled_networks
|
||||
.retain(|id| !_ids.contains(id));
|
||||
.delete_network_configs(app.clone(), ids)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.notify_vpn_stop_if_no_tun(app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn post_stop_network_instances_hook(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
) -> Result<(), String> {
|
||||
self.notify_vpn_stop_if_no_tun(app)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -862,15 +1092,15 @@ mod manager {
|
||||
pub(super) async fn load_configs(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
configs: Vec<NetworkConfig>,
|
||||
configs: Vec<StoredGuiConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
for cfg in configs {
|
||||
let instance_id = cfg.instance_id();
|
||||
for stored in configs {
|
||||
let instance_id = stored.config.instance_id();
|
||||
self.storage.network_configs.insert(
|
||||
instance_id.parse()?,
|
||||
GUIConfig(instance_id.to_string(), cfg),
|
||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -879,28 +1109,35 @@ mod manager {
|
||||
.get_rpc_client(app.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
||||
for id in enabled_networks {
|
||||
if let Ok(uuid) = id.parse() {
|
||||
if !self.storage.enabled_networks.contains(&uuid) {
|
||||
let config = self
|
||||
.storage
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| i.value().1.clone());
|
||||
if config.is_none() {
|
||||
continue;
|
||||
}
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config,
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.storage.enabled_networks.insert(uuid);
|
||||
}
|
||||
if let Ok(uuid) = id.parse()
|
||||
&& !self.storage.enabled_networks.contains(&uuid)
|
||||
{
|
||||
let config = self
|
||||
.storage
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| (i.value().config.clone(), i.value().source));
|
||||
let Some((config, source)) = config else {
|
||||
continue;
|
||||
};
|
||||
let toml_config = config.gen_config()?;
|
||||
self.pre_run_network_instance_hook(&app, &toml_config, source)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config: Some(config),
|
||||
overwrite: false,
|
||||
source: source.to_runtime_source().to_rpc(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.post_run_network_instance_hook(&app, &uuid)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -926,6 +1163,44 @@ mod manager {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PersistedConfigSource, StoredGuiConfig};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
|
||||
#[test]
|
||||
fn stored_gui_config_defaults_missing_source_to_legacy() {
|
||||
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||
"config": NetworkConfig::default(),
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() {
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Legacy
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_webhook_configs_are_webhook_like() {
|
||||
assert!(!PersistedConfigSource::Legacy.is_webhook_like());
|
||||
assert!(!PersistedConfigSource::User.is_webhook_like());
|
||||
assert!(PersistedConfigSource::Webhook.is_webhook_like());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -1014,7 +1289,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
utils::setup_panic_handler();
|
||||
setup_panic_handler();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
@@ -1053,7 +1328,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let Ok(_) = utils::init_logger(&config, true) else {
|
||||
let Ok(_) = log::init(&config, true) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.3",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
@@ -36,4 +36,4 @@
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -43,6 +43,7 @@ declare global {
|
||||
const isWebClientConnected: typeof import('./composables/backend')['isWebClientConnected']
|
||||
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
|
||||
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
|
||||
const loadLastNetworkInstanceId: typeof import('./composables/config')['loadLastNetworkInstanceId']
|
||||
const loadMode: typeof import('./composables/mode')['loadMode']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
@@ -76,6 +77,7 @@ declare global {
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const runNetworkInstance: typeof import('./composables/backend')['runNetworkInstance']
|
||||
const saveLastNetworkInstanceId: typeof import('./composables/config')['saveLastNetworkInstanceId']
|
||||
const saveMode: typeof import('./composables/mode')['saveMode']
|
||||
const saveNetworkConfig: typeof import('./composables/backend')['saveNetworkConfig']
|
||||
const sendConfigs: typeof import('./composables/backend')['sendConfigs']
|
||||
@@ -91,6 +93,7 @@ declare global {
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const syncMobileVpnService: typeof import('./composables/mobile_vpn')['syncMobileVpnService']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
@@ -165,6 +168,7 @@ declare module 'vue' {
|
||||
readonly isWebClientConnected: UnwrapRef<typeof import('./composables/backend')['isWebClientConnected']>
|
||||
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
|
||||
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
|
||||
readonly loadLastNetworkInstanceId: UnwrapRef<typeof import('./composables/config')['loadLastNetworkInstanceId']>
|
||||
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -198,6 +202,7 @@ declare module 'vue' {
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/backend')['runNetworkInstance']>
|
||||
readonly saveLastNetworkInstanceId: UnwrapRef<typeof import('./composables/config')['saveLastNetworkInstanceId']>
|
||||
readonly saveMode: UnwrapRef<typeof import('./composables/mode')['saveMode']>
|
||||
readonly saveNetworkConfig: UnwrapRef<typeof import('./composables/backend')['saveNetworkConfig']>
|
||||
readonly sendConfigs: UnwrapRef<typeof import('./composables/backend')['sendConfigs']>
|
||||
@@ -213,6 +218,7 @@ declare module 'vue' {
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly syncMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['syncMobileVpnService']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import type { Mode, ServiceMode, RemoteMode } from '~/composables/mode';
|
||||
import type { Mode, ServiceMode, RemoteMode, NormalMode } from '~/composables/mode';
|
||||
import { appConfigDir, appLogDir } from '@tauri-apps/api/path';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend';
|
||||
@@ -15,6 +15,14 @@ const defaultLogDir = ref('')
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
const isServiceStatusLoaded = ref(false)
|
||||
|
||||
function normalizeRpcListenPort(port: unknown): number {
|
||||
const defaultPort = 15999
|
||||
const numericPort = typeof port === 'number' ? port : Number.parseInt(String(port ?? ''), 10)
|
||||
if (Number.isNaN(numericPort))
|
||||
return defaultPort
|
||||
return Math.min(65535, Math.max(1, Math.floor(numericPort)))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
defaultConfigDir.value = await join(await appConfigDir(), 'config.d')
|
||||
defaultLogDir.value = await appLogDir()
|
||||
@@ -26,6 +34,43 @@ const modeOptions = computed(() => [
|
||||
{ label: t('mode.remote'), value: 'remote' },
|
||||
]);
|
||||
|
||||
const normalMode = computed({
|
||||
get: () => model.value.mode === 'normal' ? model.value as NormalMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const rpcListenOptions = computed(() => [
|
||||
{ label: t('web.common.disable'), value: false },
|
||||
{ label: t('web.common.enable'), value: true },
|
||||
])
|
||||
|
||||
const rpcListenEnabled = computed<boolean>({
|
||||
get: () => !!normalMode.value?.enable_rpc_port_listen,
|
||||
set: (value) => {
|
||||
if (!normalMode.value)
|
||||
return
|
||||
normalMode.value.enable_rpc_port_listen = value
|
||||
},
|
||||
})
|
||||
|
||||
const rpcListenPort = computed<string>({
|
||||
get: () => String(normalMode.value?.rpc_listen_port ?? 15999),
|
||||
set: (value) => {
|
||||
if (!normalMode.value)
|
||||
return
|
||||
const trimmed = value.trim()
|
||||
if (trimmed === '')
|
||||
return
|
||||
if (!/^\d+$/.test(trimmed))
|
||||
return
|
||||
normalMode.value.rpc_listen_port = Number.parseInt(trimmed, 10)
|
||||
},
|
||||
})
|
||||
|
||||
const serviceMode = computed({
|
||||
get: () => model.value.mode === 'service' ? model.value as ServiceMode : undefined,
|
||||
set: (value) => {
|
||||
@@ -57,6 +102,24 @@ const statusColorClass = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => [normalMode.value?.enable_rpc_port_listen, normalMode.value?.rpc_listen_port], ([enabled, port]) => {
|
||||
if (!normalMode.value)
|
||||
return
|
||||
|
||||
if (!enabled) {
|
||||
normalMode.value.rpc_portal = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedPort = normalizeRpcListenPort(port)
|
||||
if (normalMode.value.rpc_listen_port !== normalizedPort)
|
||||
normalMode.value.rpc_listen_port = normalizedPort
|
||||
|
||||
const desiredPortal = `tcp://0.0.0.0:${normalizedPort}`
|
||||
if (normalMode.value.rpc_portal !== desiredPortal)
|
||||
normalMode.value.rpc_portal = desiredPortal
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
if (newMode === oldMode)
|
||||
return
|
||||
@@ -69,8 +132,12 @@ watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
const oldModelValue = { ...model.value }
|
||||
|
||||
if (newMode === 'normal') {
|
||||
const portal = normalMode.value?.rpc_portal?.trim()
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
rpc_portal: portal || undefined,
|
||||
enable_rpc_port_listen: normalMode.value?.enable_rpc_port_listen,
|
||||
rpc_listen_port: normalMode.value?.rpc_listen_port,
|
||||
mode: 'normal',
|
||||
}
|
||||
}
|
||||
@@ -113,6 +180,20 @@ watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
{{ t('mode.remote_description') }}
|
||||
</div>
|
||||
|
||||
<div v-if="normalMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="rpc-listen-toggle">{{ t('mode.enable_rpc_tcp_listen') }}</label>
|
||||
<SelectButton id="rpc-listen-toggle" v-model="rpcListenEnabled" :options="rpcListenOptions" option-label="label"
|
||||
option-value="value" />
|
||||
</div>
|
||||
<div v-if="rpcListenEnabled" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="rpc-listen-port">{{ t('mode.rpc_listen_port') }}</label>
|
||||
<InputText id="rpc-listen-port" v-model="rpcListenPort" class="flex-1" inputmode="numeric" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="serviceMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="config-dir">{{ t('mode.config_dir') }}</label>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Api, type NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { Api, NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
@@ -16,16 +17,50 @@ interface ServiceOptions {
|
||||
|
||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||
|
||||
interface StoredGuiConfig {
|
||||
config: NetworkConfig
|
||||
source: ConfigSource
|
||||
}
|
||||
|
||||
function parseStoredConfigs(raw: string | null): StoredGuiConfig[] {
|
||||
const parsed: unknown = JSON.parse(raw || '[]')
|
||||
if (!Array.isArray(parsed)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return parsed.flatMap((entry): StoredGuiConfig[] => {
|
||||
if (entry && typeof entry === 'object' && 'config' in entry) {
|
||||
const { config, source } = entry as {
|
||||
config?: NetworkConfig
|
||||
source?: ConfigSource
|
||||
}
|
||||
if (!config) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: source === 'user' || source === 'webhook' ? source : 'legacy',
|
||||
}]
|
||||
}
|
||||
|
||||
return [{
|
||||
config: NetworkTypes.normalizeNetworkConfig(entry as NetworkConfig),
|
||||
source: 'legacy',
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||
}
|
||||
|
||||
export async function generateNetworkConfig(tomlConfig: string) {
|
||||
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||
const config = await invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||
return NetworkTypes.normalizeNetworkConfig(config)
|
||||
}
|
||||
|
||||
export async function runNetworkInstance(cfg: NetworkConfig, save: boolean) {
|
||||
return invoke('run_network_instance', { cfg, save })
|
||||
return invoke('run_network_instance', { cfg: NetworkTypes.toBackendNetworkConfig(cfg), save })
|
||||
}
|
||||
|
||||
export async function collectNetworkInfo(instanceId: string) {
|
||||
@@ -57,20 +92,27 @@ export async function updateNetworkConfigState(instanceId: string, disabled: boo
|
||||
}
|
||||
|
||||
export async function saveNetworkConfig(cfg: NetworkConfig) {
|
||||
return await invoke('save_network_config', { cfg })
|
||||
return await invoke('save_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||
}
|
||||
|
||||
export async function validateConfig(cfg: NetworkConfig) {
|
||||
return await invoke<ValidateConfigResponse>('validate_config', { cfg })
|
||||
return await invoke<ValidateConfigResponse>('validate_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||
}
|
||||
|
||||
export async function getConfig(instanceId: string) {
|
||||
return await invoke<NetworkConfig>('get_config', { instanceId })
|
||||
const config = await invoke<NetworkConfig>('get_config', { instanceId })
|
||||
return NetworkTypes.normalizeNetworkConfig(config)
|
||||
}
|
||||
|
||||
export async function sendConfigs(enabledNetworks: string[]) {
|
||||
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||
return await invoke('load_configs', { configs: networkList, enabledNetworks })
|
||||
const networkList = parseStoredConfigs(localStorage.getItem('networkList'))
|
||||
return await invoke('load_configs', {
|
||||
configs: networkList.map(({ config, source }) => ({
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
source,
|
||||
})),
|
||||
enabledNetworks
|
||||
})
|
||||
}
|
||||
|
||||
export async function getNetworkMetas(instanceIds: string[]) {
|
||||
@@ -89,8 +131,8 @@ export async function getServiceStatus() {
|
||||
return await invoke<ServiceStatus>('get_service_status')
|
||||
}
|
||||
|
||||
export async function initRpcConnection(url?: string) {
|
||||
return await invoke('init_rpc_connection', { url })
|
||||
export async function initRpcConnection(isNormalMode: boolean, url?: string) {
|
||||
return await invoke('init_rpc_connection', { isNormalMode, url })
|
||||
}
|
||||
|
||||
export async function isClientRunning() {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 配置持久化相关的函数
|
||||
* 用于保存和加载应用程序的各种配置状态
|
||||
*/
|
||||
|
||||
/**
|
||||
* 保存上次使用的网络实例 ID
|
||||
* @param instanceId 网络实例 ID
|
||||
*/
|
||||
export function saveLastNetworkInstanceId(instanceId: string) {
|
||||
localStorage.setItem('last_network_instance_id', instanceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载上次使用的网络实例 ID
|
||||
* @returns 上次使用的网络实例 ID,如果没有则返回 null
|
||||
*/
|
||||
export function loadLastNetworkInstanceId(): string | null {
|
||||
return localStorage.getItem('last_network_instance_id')
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Event, listen } from "@tauri-apps/api/event";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { NetworkTypes } from "easytier-frontend-lib"
|
||||
import { Utils } from "easytier-frontend-lib";
|
||||
|
||||
interface StoredGuiConfig {
|
||||
config: NetworkTypes.NetworkConfig
|
||||
source?: 'user' | 'webhook' | 'legacy'
|
||||
}
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
SAVE_CONFIGS: 'save_configs',
|
||||
@@ -12,44 +18,82 @@ const EVENTS = Object.freeze({
|
||||
EVENT_LAGGED: 'event_lagged',
|
||||
});
|
||||
|
||||
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
||||
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
||||
localStorage.setItem('networkList', JSON.stringify(event.payload));
|
||||
localStorage.setItem(
|
||||
'networkList',
|
||||
JSON.stringify(event.payload.map(({ config, source }) => ({
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: source ?? 'legacy',
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
async function onPreRunNetworkInstance(event: Event<string>) {
|
||||
function normalizeInstanceIdPayload(payload: unknown): string {
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object') {
|
||||
const uuid = payload as Partial<Utils.UUID>
|
||||
if (
|
||||
typeof uuid.part1 === 'number'
|
||||
&& typeof uuid.part2 === 'number'
|
||||
&& typeof uuid.part3 === 'number'
|
||||
&& typeof uuid.part4 === 'number'
|
||||
) {
|
||||
return Utils.UuidToStr(uuid as Utils.UUID)
|
||||
}
|
||||
}
|
||||
|
||||
if (payload == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const fallback = String(payload)
|
||||
return fallback === '[object Object]' ? '' : fallback
|
||||
}
|
||||
|
||||
async function onPreRunNetworkInstance(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.PRE_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService(event.payload);
|
||||
await prepareVpnService(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPostRunNetworkInstance(event: Event<string>) {
|
||||
async function onPostRunNetworkInstance(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.POST_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(event: Event<string>) {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
async function onVpnServiceStop(event: Event<unknown>) {
|
||||
console.log(`Received event '${EVENTS.VPN_SERVICE_STOP}', raw payload:`, event.payload)
|
||||
await syncMobileVpnService();
|
||||
}
|
||||
|
||||
async function onDhcpIpChanged(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${event.payload}`);
|
||||
async function onDhcpIpChanged(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onProxyCidrsUpdated(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${event.payload}`);
|
||||
async function onProxyCidrsUpdated(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${instanceId}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
async function onEventLagged(event: Event<string>) {
|
||||
async function onEventLagged(event: Event<unknown>) {
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
await onNetworkInstanceChange(normalizeInstanceIdPayload(event.payload));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
|
||||
type Route = NetworkTypes.Route
|
||||
|
||||
@@ -24,6 +24,53 @@ const curVpnStatus: vpnStatus = {
|
||||
dns: undefined,
|
||||
}
|
||||
|
||||
async function requestVpnPermission() {
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
|
||||
const granted = prepare_ret?.granted ?? true
|
||||
if (!granted) {
|
||||
console.info('vpn permission request was denied or dismissed')
|
||||
}
|
||||
|
||||
return granted
|
||||
}
|
||||
|
||||
function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
}
|
||||
|
||||
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
|
||||
curVpnStatus.running = status?.running ?? false
|
||||
if (!curVpnStatus.running) {
|
||||
resetVpnConfigStatus()
|
||||
return
|
||||
}
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
|
||||
const parsedCidr = Number(cidr)
|
||||
curVpnStatus.ipv4Cidr = Number.isInteger(parsedCidr) ? parsedCidr : undefined
|
||||
}
|
||||
else {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
}
|
||||
|
||||
curVpnStatus.routes = [...(status?.routes ?? [])]
|
||||
curVpnStatus.dns = status?.dns ?? undefined
|
||||
}
|
||||
|
||||
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
const start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
@@ -34,18 +81,19 @@ async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function doStopVpn() {
|
||||
if (!curVpnStatus.running) {
|
||||
async function doStopVpn(force = false) {
|
||||
const wasRunning = curVpnStatus.running
|
||||
if (!force && !wasRunning) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
await waitVpnStatus(false, 3)
|
||||
if (wasRunning) {
|
||||
await waitVpnStatus(false, 3)
|
||||
}
|
||||
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
||||
@@ -54,19 +102,32 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?
|
||||
}
|
||||
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
||||
const start_ret = await start_vpn({
|
||||
const request = {
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
mtu: 1300,
|
||||
})
|
||||
}
|
||||
|
||||
let start_ret = await start_vpn(request)
|
||||
console.log('start vpn response', JSON.stringify(start_ret))
|
||||
if (start_ret?.errorMsg === 'need_prepare') {
|
||||
const granted = await requestVpnPermission()
|
||||
if (!granted) {
|
||||
throw new Error('vpn_permission_denied')
|
||||
}
|
||||
start_ret = await start_vpn(request)
|
||||
console.log('start vpn retry response', JSON.stringify(start_ret))
|
||||
}
|
||||
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.ipv4Cidr = cidr
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
}
|
||||
@@ -75,13 +136,16 @@ async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(payload.fd)
|
||||
await setTunFd(payload.fd).catch((e) => {
|
||||
console.error('set tun fd failed', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function registerVpnServiceListener() {
|
||||
@@ -135,15 +199,25 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
}
|
||||
|
||||
if (!instanceId) {
|
||||
await doStopVpn()
|
||||
console.warn('vpn service skipped because instance id is empty')
|
||||
if (curVpnStatus.running) {
|
||||
await doStopVpn()
|
||||
}
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
}))
|
||||
if (config.no_tun) {
|
||||
console.log('vpn service skipped because no_tun is enabled', instanceId)
|
||||
return
|
||||
}
|
||||
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg)
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
@@ -170,27 +244,39 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
||||
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined;
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const cidrChanged = network_length !== curVpnStatus.ipv4Cidr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
const dnsChanged = dns != curVpnStatus.dns
|
||||
const configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
||||
const shouldStartVpn = !curVpnStatus.running
|
||||
|
||||
if (ipChanged || routesChanged || dnsChanged) {
|
||||
if (shouldStartVpn || configChanged) {
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
if (curVpnStatus.running) {
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('start vpn service failed, stop all other network insts.', e)
|
||||
await runNetworkInstance(config, true); //on android config should always be saved
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
console.info('vpn permission is required before starting the Android VPN service')
|
||||
return
|
||||
}
|
||||
if (e instanceof Error && e.message === 'vpn_permission_denied') {
|
||||
console.info('vpn permission request was denied or dismissed')
|
||||
return
|
||||
}
|
||||
console.error('start vpn service failed', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +288,22 @@ async function isNoTunEnabled(instanceId: string | undefined) {
|
||||
return (await getConfig(instanceId)).no_tun ?? false
|
||||
}
|
||||
|
||||
async function findRunningTunInstanceId() {
|
||||
const instanceIds = await listNetworkInstanceIds()
|
||||
const runningIds = instanceIds.running_inst_ids.map(Utils.UuidToStr)
|
||||
console.log('vpn service sync running instances', JSON.stringify(runningIds))
|
||||
|
||||
for (const instanceId of runningIds) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return instanceId
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
await registerVpnServiceListener()
|
||||
}
|
||||
@@ -210,10 +312,22 @@ export async function prepareVpnService(instanceId: string) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
return
|
||||
}
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
await requestVpnPermission()
|
||||
}
|
||||
|
||||
export async function syncMobileVpnService() {
|
||||
syncVpnStatusFromNative(await get_vpn_status())
|
||||
const instanceId = await findRunningTunInstanceId()
|
||||
if (instanceId) {
|
||||
console.log('vpn service sync selected instance', instanceId)
|
||||
await onNetworkInstanceChange(instanceId)
|
||||
return
|
||||
}
|
||||
|
||||
if (dhcpPollingTimer) {
|
||||
clearTimeout(dhcpPollingTimer)
|
||||
dhcpPollingTimer = null
|
||||
}
|
||||
|
||||
await doStopVpn(true)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,12 @@ export interface WebClientConfig {
|
||||
config_server_url?: string
|
||||
}
|
||||
|
||||
interface NormalMode extends WebClientConfig {
|
||||
export interface NormalMode extends WebClientConfig {
|
||||
mode: 'normal'
|
||||
// if not provided will use ring tunnel rpc server
|
||||
rpc_portal?: string
|
||||
enable_rpc_port_listen?: boolean
|
||||
rpc_listen_port?: number
|
||||
}
|
||||
|
||||
export interface ServiceMode extends WebClientConfig {
|
||||
@@ -14,6 +18,7 @@ export interface ServiceMode extends WebClientConfig {
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
installed_core_version?: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
|
||||
@@ -9,12 +9,14 @@ import { exit } from '@tauri-apps/plugin-process'
|
||||
import { I18nUtils, RemoteManagement, Utils } from "easytier-frontend-lib"
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { initMobileVpnService } from '~/composables/mobile_vpn'
|
||||
import { GUIRemoteClient } from '~/modules/api'
|
||||
|
||||
import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getServiceStatus } from '~/composables/backend'
|
||||
import { getEasytierVersion, getServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const confirm = useConfirm()
|
||||
@@ -83,6 +85,20 @@ async function onUninstallService() {
|
||||
});
|
||||
}
|
||||
|
||||
function stripModeMetadata(mode: Mode) {
|
||||
if (mode.mode !== 'service') {
|
||||
return mode
|
||||
}
|
||||
|
||||
const serviceConfig = { ...mode }
|
||||
delete serviceConfig.installed_core_version
|
||||
return serviceConfig
|
||||
}
|
||||
|
||||
function modeConfigChanged(next: Mode) {
|
||||
return JSON.stringify(stripModeMetadata(next)) !== JSON.stringify(stripModeMetadata(currentMode.value))
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
manualDisconnect.value = true
|
||||
@@ -132,13 +148,14 @@ async function initWithMode(mode: Mode) {
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service':
|
||||
case 'service': {
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
const coreVersion = await getEasytierVersion()
|
||||
if (serviceStatus === "NotInstalled" || modeConfigChanged(mode) || mode.installed_core_version !== coreVersion) {
|
||||
mode.config_server_url = mode.config_server_url || undefined
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
@@ -147,6 +164,7 @@ async function initWithMode(mode: Mode) {
|
||||
rpc_portal: mode.rpc_portal,
|
||||
config_server: mode.config_server_url,
|
||||
})
|
||||
mode.installed_core_version = coreVersion
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
@@ -155,13 +173,24 @@ async function initWithMode(mode: Mode) {
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
case 'normal':
|
||||
url = mode.rpc_portal;
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < retrys; i++) {
|
||||
try {
|
||||
await connectRpcClient(url)
|
||||
await connectRpcClient(mode.mode === 'normal', url)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (i === retrys - 1) {
|
||||
const errMsg = e instanceof Error ? e.message : String(e)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: t('mode.rpc_connection_failed', { error: errMsg }),
|
||||
life: 1000,
|
||||
})
|
||||
throw e;
|
||||
}
|
||||
console.error("Error connecting rpc client, retrying...", e)
|
||||
@@ -178,9 +207,25 @@ async function initWithMode(mode: Mode) {
|
||||
clientRunning.value = await isClientRunning()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const cleanupFns: Array<() => void> = []
|
||||
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
await initMobileVpnService()
|
||||
console.error("easytier init vpn service done")
|
||||
} catch (e: any) {
|
||||
console.error("easytier init vpn service failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
cleanupFns.push(await listenGlobalEvents())
|
||||
currentMode.value = loadMode()
|
||||
initWithMode(currentMode.value);
|
||||
await initWithMode(currentMode.value);
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupFns.forEach(unlisten => unlisten())
|
||||
})
|
||||
});
|
||||
|
||||
useTray(true)
|
||||
@@ -190,6 +235,12 @@ const remoteClient = computed(() => new GUIRemoteClient());
|
||||
const instanceId = ref<string | undefined>(undefined);
|
||||
const clientRunning = ref(false);
|
||||
|
||||
watch(instanceId, (newVal) => {
|
||||
if (newVal) {
|
||||
saveLastNetworkInstanceId(newVal);
|
||||
}
|
||||
});
|
||||
|
||||
watch(clientRunning, async (newVal, oldVal) => {
|
||||
if (!newVal && oldVal) {
|
||||
if (manualDisconnect.value) {
|
||||
@@ -197,6 +248,11 @@ watch(clientRunning, async (newVal, oldVal) => {
|
||||
return
|
||||
}
|
||||
await reconnectClient()
|
||||
} else if (newVal && !oldVal) {
|
||||
const lastInstanceId = loadLastNetworkInstanceId();
|
||||
if (lastInstanceId) {
|
||||
instanceId.value = lastInstanceId;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -320,27 +376,11 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
},
|
||||
])
|
||||
|
||||
async function connectRpcClient(url?: string) {
|
||||
await initRpcConnection(url)
|
||||
console.log("easytier rpc connection established")
|
||||
async function connectRpcClient(isNormalMode: boolean, url?: string) {
|
||||
await initRpcConnection(isNormalMode, url)
|
||||
console.log("easytier rpc connection established, isNormalMode: ", isNormalMode)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
await initMobileVpnService()
|
||||
console.error("easytier init vpn service done")
|
||||
} catch (e: any) {
|
||||
console.error("easytier init vpn service failed", e)
|
||||
}
|
||||
}
|
||||
const unlisten = await listenGlobalEvents()
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
})
|
||||
|
||||
async function openConfigServerDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
configServerDialogVisible.value = true
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
name = "easytier-rpc-build"
|
||||
description = "Protobuf RPC Service Generator for EasyTier"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
rust-version = "1.89.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
|
||||
|
||||
let mut trait_methods = String::new();
|
||||
let mut weak_impl_methods = String::new();
|
||||
let mut enum_methods = String::new();
|
||||
let mut list_enum_methods = String::new();
|
||||
let mut client_methods = String::new();
|
||||
@@ -40,6 +41,8 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
let mut match_output_type_methods = String::new();
|
||||
let mut match_output_proto_type_methods = String::new();
|
||||
let mut match_handle_methods = String::new();
|
||||
// generate trait default method Xxx::json_call_method match branch
|
||||
let mut match_trait_json_methods = String::new();
|
||||
|
||||
let mut match_method_try_from = String::new();
|
||||
|
||||
@@ -66,6 +69,21 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
weak_impl_methods,
|
||||
r#" async fn {method_name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
|
||||
let Some(service) = self.upgrade() else {{
|
||||
return Err({namespace}::error::Error::Shutdown);
|
||||
}};
|
||||
service.{method_name}(ctrl, input).await
|
||||
}}"#,
|
||||
method_name = method.name,
|
||||
input_type = method.input_type,
|
||||
output_type = method.output_type,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
|
||||
writeln!(
|
||||
enum_methods,
|
||||
@@ -164,6 +182,22 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
write!(
|
||||
match_trait_json_methods,
|
||||
r#" "{name}" | "{proto_name}" => {{
|
||||
let req: {input_type} = ::serde_json::from_value(json).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?;
|
||||
let resp = self.{typed_method}(ctrl, req).await?;
|
||||
Ok(::serde_json::to_value(resp).map_err(|e| {namespace}::error::Error::MalformatRpcPacket(format!("json error: {{}}", e)))?)
|
||||
}}
|
||||
"#,
|
||||
name = method.name,
|
||||
proto_name = method.proto_name,
|
||||
input_type = method.input_type,
|
||||
typed_method = method.name,
|
||||
namespace = NAMESPACE,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
|
||||
@@ -176,6 +210,29 @@ pub trait {name} {{
|
||||
type Controller: {namespace}::controller::Controller;
|
||||
|
||||
{trait_methods}
|
||||
|
||||
async fn json_call_method(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
method_name: &str,
|
||||
json: ::serde_json::Value,
|
||||
) -> {namespace}::error::Result<::serde_json::Value> {{
|
||||
match method_name {{
|
||||
{match_trait_json_methods}
|
||||
_ => Err({namespace}::error::Error::InvalidMethodIndex(0, method_name.to_string())),
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T> {name} for ::std::sync::Weak<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
type Controller = <::std::sync::Arc<T> as {name}>::Controller;
|
||||
|
||||
{weak_impl_methods}
|
||||
}}
|
||||
|
||||
/// A service descriptor for a `{name}`.
|
||||
@@ -235,7 +292,7 @@ impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {
|
||||
|
||||
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
|
||||
type Descriptor = {descriptor_name};
|
||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + 'static>;
|
||||
type ClientImpl = Box<dyn {name}<Controller = C> + Send + Sync + 'static>;
|
||||
type Controller = C;
|
||||
|
||||
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
|
||||
@@ -250,6 +307,16 @@ impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C:
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
|
||||
|
||||
impl<T> {server_name}<::std::sync::Weak<T>>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
::std::sync::Arc<T>: {name},
|
||||
{{
|
||||
pub fn new_arc(service: ::std::sync::Arc<T>) -> {server_name}<::std::sync::Weak<T>> {{
|
||||
{server_name}(::std::sync::Arc::downgrade(&service))
|
||||
}}
|
||||
}}
|
||||
|
||||
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
|
||||
/// Creates a new server instance that dispatches all calls to the supplied service.
|
||||
pub fn new(service: A) -> {server_name}<A> {{
|
||||
@@ -345,6 +412,7 @@ impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
||||
proto_name = service.proto_name,
|
||||
package = service.package,
|
||||
trait_methods = trait_methods,
|
||||
weak_impl_methods = weak_impl_methods,
|
||||
enum_methods = enum_methods,
|
||||
list_enum_methods = list_enum_methods,
|
||||
client_own_methods = client_own_methods,
|
||||
@@ -356,6 +424,7 @@ impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
|
||||
match_output_type_methods = match_output_type_methods,
|
||||
match_output_proto_type_methods = match_output_proto_type_methods,
|
||||
match_handle_methods = match_handle_methods,
|
||||
match_trait_json_methods = match_trait_json_methods,
|
||||
namespace = NAMESPACE,
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
+11
-9
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.5.0"
|
||||
edition = "2021"
|
||||
version = "2.6.3"
|
||||
edition.workspace = true
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
[dependencies]
|
||||
@@ -10,6 +10,7 @@ tracing = { version = "0.1", features = ["log"] }
|
||||
anyhow = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
dashmap = "6.1"
|
||||
url = "2.2"
|
||||
async-trait = "0.1"
|
||||
@@ -63,16 +64,17 @@ uuid = { version = "1.5.0", features = [
|
||||
] }
|
||||
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
openidconnect = { version = "4.0", default-features = false, features = ["accept-rfc3339-timestamps", "reqwest"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
subtle = "2.6"
|
||||
|
||||
mimalloc = { version = "*" }
|
||||
|
||||
[build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embed = ["dep:axum-embed"]
|
||||
|
||||
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[target.i686-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
.unwrap_or_default()
|
||||
.contains("aarch64")
|
||||
{
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.13.5",
|
||||
"chart.js": "^4.5.0",
|
||||
"floating-vue": "^5.2",
|
||||
"ip-num": "1.5.1",
|
||||
@@ -41,7 +41,7 @@
|
||||
"postcss-nested": "^7.0.2",
|
||||
"tailwindcss": "=3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { AutoComplete, Button, Checkbox, Dialog, Divider, InputNumber, InputText, Panel, Password, SelectButton, ToggleButton } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
|
||||
import {
|
||||
addRow,
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
NetworkConfig,
|
||||
NetworkingMethod,
|
||||
normalizeNetworkConfig,
|
||||
removeRow
|
||||
} from '../types/network'
|
||||
import { defineProps, defineEmits, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AclManager from './acl/AclManager.vue'
|
||||
import UrlListInput from './UrlListInput.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean
|
||||
@@ -26,63 +28,18 @@ const curNetwork = defineModel('curNetwork', {
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
|
||||
])
|
||||
|
||||
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
|
||||
|
||||
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
const query = e.query
|
||||
const ret = []
|
||||
// if query match "^\w+:.*", then no proto prefix
|
||||
if (query.match(/^\w+:.*/)) {
|
||||
// if query is a valid url, then add to suggestions
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(query)
|
||||
ret.push(query)
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else {
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://${query}`
|
||||
// if query match ":\d+$", then no port suffix
|
||||
if (!query.match(/:\d+$/)) {
|
||||
item += `:${protos[proto]}`
|
||||
}
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
const publicServerSuggestions = ref([''])
|
||||
|
||||
function searchPresetPublicServers(e: { query: string }) {
|
||||
const presetPublicServers = [
|
||||
'tcp://public.easytier.top:11010',
|
||||
]
|
||||
|
||||
const query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter(item => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
|
||||
publicServerSuggestions.value = ret
|
||||
}
|
||||
|
||||
const peerSuggestions = ref([''])
|
||||
|
||||
function searchPeerSuggestions(e: { query: string }) {
|
||||
peerSuggestions.value = searchUrlSuggestions(e)
|
||||
const protos: { [proto: string]: number } = {
|
||||
tcp: 11010,
|
||||
udp: 11010,
|
||||
wg: 11011,
|
||||
ws: 11011,
|
||||
wss: 11012,
|
||||
quic: 11012,
|
||||
faketcp: 11013,
|
||||
http: 80,
|
||||
https: 443,
|
||||
txt: 0,
|
||||
srv: 0,
|
||||
}
|
||||
|
||||
const inetSuggestions = ref([''])
|
||||
@@ -99,34 +56,6 @@ function searchInetSuggestions(e: { query: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const listenerSuggestions = ref([''])
|
||||
|
||||
function searchListenerSuggestions(e: { query: string }) {
|
||||
const ret = []
|
||||
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://0.0.0.0:`
|
||||
// if query is a number, use it as port
|
||||
if (e.query.match(/^\d+$/)) {
|
||||
item += e.query
|
||||
}
|
||||
else {
|
||||
item += protos[proto]
|
||||
}
|
||||
|
||||
if (item.includes(e.query)) {
|
||||
ret.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (ret.length === 0) {
|
||||
ret.push(e.query)
|
||||
}
|
||||
|
||||
listenerSuggestions.value = ret
|
||||
}
|
||||
|
||||
|
||||
const exitNodesSuggestions = ref([''])
|
||||
|
||||
function searchExitNodesSuggestions(e: { query: string }) {
|
||||
@@ -152,21 +81,25 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'latency_first', help: 'latency_first_help' },
|
||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
||||
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
|
||||
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
|
||||
{ field: 'disable_p2p', help: 'disable_p2p_help' },
|
||||
{ field: 'p2p_only', help: 'p2p_only_help' },
|
||||
{ field: 'lazy_p2p', help: 'lazy_p2p_help' },
|
||||
{ field: 'bind_device', help: 'bind_device_help' },
|
||||
{ field: 'no_tun', help: 'no_tun_help' },
|
||||
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
|
||||
{ field: 'relay_all_peer_rpc', help: 'relay_all_peer_rpc_help' },
|
||||
{ field: 'need_p2p', help: 'need_p2p_help' },
|
||||
{ field: 'multi_thread', help: 'multi_thread_help' },
|
||||
{ field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' },
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||
@@ -217,6 +150,16 @@ onMounted(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function syncNormalizedNetwork(network: NetworkConfig | undefined): void {
|
||||
if (!network) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(network, normalizeNetworkConfig(network))
|
||||
}
|
||||
|
||||
watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -263,17 +206,14 @@ onMounted(() => {
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="nm">{{ t('networking_method') }}</label>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
|
||||
:option-label="(v) => v.label()" option-value="value" />
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions" class="grow"
|
||||
dropdown :complete-on-focus="false" @complete="searchPresetPublicServers" />
|
||||
<div class="flex items-center">
|
||||
<label for="initial_nodes">{{ t('initial_nodes') }}</label>
|
||||
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('initial_nodes_help')"></span>
|
||||
</div>
|
||||
<div class="items-center flex flex-col p-fluid gap-y-2">
|
||||
<UrlListInput id="initial_nodes" v-model="curNetwork.peer_urls" :protos="protos"
|
||||
defaultUrl="tcp://:11010" :add-label="t('add_initial_node')"
|
||||
:placeholder="t('initial_node_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,10 +285,8 @@ onMounted(() => {
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
|
||||
class="w-full" dropdown :complete-on-focus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
|
||||
@complete="searchListenerSuggestions" />
|
||||
<UrlListInput v-model="curNetwork.listener_urls" :protos="protos" :add-label="t('add_listener_url')"
|
||||
placeholder="0.0.0.0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -371,6 +309,19 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex">
|
||||
<label for="instance_recv_bps_limit">{{ t('instance_recv_bps_limit') }}</label>
|
||||
<span class="pi pi-question-circle ml-2 self-center"
|
||||
v-tooltip="t('instance_recv_bps_limit_help')"></span>
|
||||
</div>
|
||||
<InputNumber id="instance_recv_bps_limit" v-model="curNetwork.instance_recv_bps_limit"
|
||||
aria-describedby="instance_recv_bps_limit-help" :format="false"
|
||||
:placeholder="t('instance_recv_bps_limit_placeholder')" :min="1" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex">
|
||||
@@ -443,9 +394,8 @@ onMounted(() => {
|
||||
<label for="mapped_listeners">{{ t('mapped_listeners') }}</label>
|
||||
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mapped_listeners_help')"></span>
|
||||
</div>
|
||||
<AutoComplete id="mapped_listeners" v-model="curNetwork.mapped_listeners"
|
||||
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full" multiple fluid
|
||||
:suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
<UrlListInput v-model="curNetwork.mapped_listeners" :protos="protos"
|
||||
:add-label="t('add_mapped_listener')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -541,6 +491,18 @@ onMounted(() => {
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('acl.title')" toggleable collapsed>
|
||||
<div v-if="curNetwork.acl" class="flex flex-col gap-y-2">
|
||||
<AclManager v-model="curNetwork.acl" />
|
||||
</div>
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('acl.enabled')"
|
||||
@click="curNetwork.acl = { acl_v1: { chains: [], group: { declares: [], members: [] } } }" />
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-6 justify-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
|
||||
@@ -206,27 +206,39 @@ const confirmDeleteNetwork = (event: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const saveAndRunNewNetwork = async () => {
|
||||
if (!currentNetworkConfig.value) {
|
||||
const saveAndRunNewNetwork = async (config?: NetworkTypes.NetworkConfig) => {
|
||||
const cfg = config ?? currentNetworkConfig.value;
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetInstanceId = instanceId.value ?? cfg.instance_id;
|
||||
if (targetInstanceId && cfg.instance_id !== targetInstanceId) {
|
||||
cfg.instance_id = targetInstanceId;
|
||||
}
|
||||
|
||||
try {
|
||||
await props.api.delete_network(instanceId.value!);
|
||||
let ret = await props.api.run_network(currentNetworkConfig.value, currentNetworkControl.remoteSave.value);
|
||||
console.debug("saveAndRunNewNetwork", ret);
|
||||
if (networkIsDisabled.value) {
|
||||
await props.api.save_config(cfg);
|
||||
await props.api.update_network_instance_state(cfg.instance_id, false);
|
||||
} else {
|
||||
await props.api.run_network(cfg, currentNetworkControl.remoteSave.value);
|
||||
}
|
||||
|
||||
delete networkMetaCache.value[currentNetworkConfig.value.instance_id];
|
||||
await loadNetworkMetas([currentNetworkConfig.value.instance_id]);
|
||||
delete networkMetaCache.value[cfg.instance_id];
|
||||
await loadNetworkMetas([cfg.instance_id]);
|
||||
|
||||
selectedInstanceId.value = { uuid: currentNetworkConfig.value.instance_id };
|
||||
selectedInstanceId.value = { uuid: cfg.instance_id };
|
||||
await loadNetworkInstanceIds();
|
||||
await loadCurrentNetworkInfo();
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to create network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to run network, error: ' + JSON.stringify(e.response?.data ?? e), life: 2000 });
|
||||
return;
|
||||
}
|
||||
|
||||
emits('update');
|
||||
// showCreateNetworkDialog.value = false;
|
||||
isEditingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
isEditingNetwork.value = false;
|
||||
}
|
||||
|
||||
const saveNetworkConfig = async () => {
|
||||
@@ -388,18 +400,18 @@ const updateScreenWidth = () => {
|
||||
const menuRef = ref();
|
||||
const actionMenu: Ref<MenuItem[]> = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
label: () => t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
visible: () => !(networkIsDisabled.value ?? true) && currentNetworkControl.editable.value,
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.export_config'),
|
||||
label: () => t('web.device_management.export_config'),
|
||||
icon: 'pi pi-download',
|
||||
command: () => exportConfig()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.delete_network'),
|
||||
label: () => t('web.device_management.delete_network'),
|
||||
icon: 'pi pi-trash',
|
||||
class: 'p-error',
|
||||
visible: () => currentNetworkControl.deletable.value,
|
||||
@@ -539,13 +551,15 @@ onUnmounted(() => {
|
||||
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
|
||||
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
|
||||
iconPos="left" severity="help" />
|
||||
<Button v-if="networkIsDisabled" @click="saveNetworkConfig" icon="pi pi-save"
|
||||
:label="t('web.device_management.save_config')" iconPos="left" severity="success" />
|
||||
<Button v-if="networkIsDisabled" @click="saveNetworkConfig" :disabled="!currentNetworkConfig"
|
||||
icon="pi pi-save" :label="t('web.device_management.save_config')" iconPos="left"
|
||||
severity="success" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Config :cur-network="currentNetworkConfig" @run-network="saveAndRunNewNetwork"></Config>
|
||||
<Config :cur-network="currentNetworkConfig" :config-invalid="!currentNetworkConfig"
|
||||
@run-network="saveAndRunNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
|
||||
@@ -183,6 +183,12 @@ const myNodeInfoChips = computed(() => {
|
||||
if (!my_node_info)
|
||||
return chips
|
||||
|
||||
// peer id
|
||||
chips.push({
|
||||
label: `Peer ID: ${my_node_info.peer_id}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
|
||||
// TUN Device Name
|
||||
const dev_name = props.curNetworkInst.detail?.dev_name
|
||||
if (dev_name) {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
placeholder?: string
|
||||
protos: { [proto: string]: number }
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const url = defineModel<string>({ required: true })
|
||||
const editing = ref(false)
|
||||
const hostFocused = ref(false)
|
||||
|
||||
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
||||
const getValidPort = (portStr: string, proto: string) => {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||
}
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : 'tcp'
|
||||
const rest = match ? match[2] : trimmed
|
||||
const authority = rest.split(/[/?#]/)[0]
|
||||
if (!authority) {
|
||||
return null
|
||||
}
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
// null = no explicit port in URL; do not fabricate a default
|
||||
const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
}
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||
// null = no explicit port in URL; buildUrlValue will omit the port entirely,
|
||||
// preserving the protocol's implied standard port (e.g. 443 for wss://).
|
||||
const port: number | null = portMatch ? parseInt(portMatch[2]) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 }
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: null }
|
||||
}
|
||||
|
||||
const internalValue = ref(parseUrl(url.value))
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => {
|
||||
const proto = value.proto || 'tcp'
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
// Omit port when the protocol uses no port (protos value = 0), or when the
|
||||
// original URL had no explicit port (port === null) – avoids overwriting an
|
||||
// implicit standard port (e.g. 443 for wss) with an EasyTier default (11012).
|
||||
if (props.protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
return `${proto}://${host}:${value.port}`
|
||||
}
|
||||
|
||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
const nextUrl = buildUrlValue(internalValue.value, forceDefaultHost)
|
||||
if (!nextUrl || nextUrl === url.value) {
|
||||
return
|
||||
}
|
||||
url.value = nextUrl
|
||||
}
|
||||
|
||||
const onHostBlur = () => {
|
||||
hostFocused.value = false
|
||||
syncUrlFromInternal(true)
|
||||
}
|
||||
|
||||
const onHostFocus = () => {
|
||||
hostFocused.value = true
|
||||
}
|
||||
|
||||
const onDialogConfirm = () => {
|
||||
syncUrlFromInternal(true)
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
const isNoPortProto = computed(() => {
|
||||
return props.protos[internalValue.value.proto] === 0
|
||||
})
|
||||
|
||||
// Sync from external
|
||||
watch(() => url.value, (newVal) => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
const parsed = parseUrl(newVal)
|
||||
const internalHost = internalValue.value.host ?? ''
|
||||
const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost)
|
||||
if (parsed.proto !== internalValue.value.proto ||
|
||||
!sameHost ||
|
||||
parsed.port !== internalValue.value.port) {
|
||||
internalValue.value = parsed
|
||||
}
|
||||
})
|
||||
|
||||
// Sync to external
|
||||
watch(internalValue, () => {
|
||||
syncUrlFromInternal(false)
|
||||
}, { deep: true })
|
||||
|
||||
const protoOptions = computed(() => Object.keys(props.protos))
|
||||
const filteredProtos = ref<string[]>([])
|
||||
|
||||
const searchProtos = (event: { query: string }) => {
|
||||
if (!event.query.trim().length) {
|
||||
filteredProtos.value = [...protoOptions.value]
|
||||
} else {
|
||||
filteredProtos.value = protoOptions.value.filter((proto) => {
|
||||
return proto.toLowerCase().startsWith(event.query.toLowerCase())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onProtoChange = (newProto: string) => {
|
||||
const oldProto = internalValue.value.proto
|
||||
const oldDefault = props.protos[oldProto]
|
||||
const newDefault = props.protos[newProto]
|
||||
|
||||
if (oldDefault !== undefined && internalValue.value.port === oldDefault && newDefault !== undefined) {
|
||||
internalValue.value.port = newDefault
|
||||
}
|
||||
internalValue.value.proto = newProto
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
||||
<InputGroup class="url-input-full w-full min-w-0">
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||
@update:model-value="onProtoChange" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
||||
</template>
|
||||
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
||||
<slot name="actions"></slot>
|
||||
</InputGroup>
|
||||
|
||||
<div
|
||||
class="url-input-compact flex justify-between items-center p-2 border rounded w-full min-w-0 overflow-hidden">
|
||||
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
||||
<div class="flex items-center shrink-0">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" :aria-label="t('web.common.edit')"
|
||||
@click="editing = true" />
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="editing" modal :header="placeholder" :style="{ width: '90vw', maxWidth: '500px' }">
|
||||
<div class="flex flex-col gap-4 py-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('tunnel_proto') }}</label>
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown fluid
|
||||
@complete="searchProtos" @update:model-value="onProtoChange" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('web.common.address') || 'Address' }}</label>
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
</div>
|
||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||
<label>{{ t('port') }}</label>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full"
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.confirm') || 'Done'" icon="pi pi-check" @click="onDialogConfirm"
|
||||
autofocus />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.url-input-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.url-input-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.url-input-full {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.proto-autocomplete-in-group,
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from 'primevue'
|
||||
import UrlInput from './UrlInput.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
protos: { [proto: string]: number }
|
||||
addLabel: string
|
||||
placeholder?: string
|
||||
defaultUrl?: string
|
||||
}>()
|
||||
|
||||
const list = defineModel<string[]>({ required: true })
|
||||
|
||||
const addUrl = () => {
|
||||
list.value.push(props.defaultUrl || 'tcp://0.0.0.0:11010')
|
||||
}
|
||||
|
||||
const removeUrl = (index: number) => {
|
||||
list.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-2 w-full">
|
||||
<div v-for="(_, index) in list" :key="index" class="flex gap-2 items-center w-full">
|
||||
<UrlInput v-model="list[index]" :protos="protos" :placeholder="placeholder">
|
||||
<template #actions>
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded @click="removeUrl(index)" />
|
||||
</template>
|
||||
</UrlInput>
|
||||
</div>
|
||||
<div class="flex justify-center items-center w-full h-10 border-2 border-dashed border-surface-300 dark:border-surface-600 rounded-lg cursor-pointer hover:border-primary hover:bg-surface-50 dark:hover:bg-surface-800 transition-colors duration-200 gap-2 text-surface-500 dark:text-surface-400"
|
||||
@click="addUrl">
|
||||
<i class="pi pi-plus text-sm"></i>
|
||||
<span class="text-sm font-medium">{{ addLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Column, DataTable, Divider, InputText, Select, SelectButton, ToggleButton } from 'primevue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AclAction, AclChain, AclChainType, AclProtocol, AclRule } from '../../types/network'
|
||||
import AclRuleDialog from './AclRuleDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const chain = defineModel<AclChain>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
watch(() => chain.value.rules, (newRules) => {
|
||||
if (!newRules) return
|
||||
const isSorted = newRules.every((rule, i) => i === 0 || (rule.priority || 0) <= (newRules[i - 1].priority || 0))
|
||||
if (!isSorted) {
|
||||
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const actionOptions = [
|
||||
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||
]
|
||||
|
||||
const chainTypeOptions = [
|
||||
{ label: () => t('acl.inbound'), value: AclChainType.Inbound },
|
||||
{ label: () => t('acl.outbound'), value: AclChainType.Outbound },
|
||||
{ label: () => t('acl.forward'), value: AclChainType.Forward },
|
||||
]
|
||||
|
||||
const editingRule = ref<AclRule | null>(null)
|
||||
const editingRuleIndex = ref(-1)
|
||||
const showRuleDialog = ref(false)
|
||||
|
||||
function getProtocolLabel(proto: AclProtocol) {
|
||||
switch (proto) {
|
||||
case AclProtocol.Any: return t('acl.any')
|
||||
case AclProtocol.TCP: return 'TCP'
|
||||
case AclProtocol.UDP: return 'UDP'
|
||||
case AclProtocol.ICMP: return 'ICMP'
|
||||
case AclProtocol.ICMPv6: return 'ICMPv6'
|
||||
default: return t('event.Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
function getActionLabel(action: AclAction) {
|
||||
switch (action) {
|
||||
case AclAction.Allow: return t('acl.allow')
|
||||
case AclAction.Drop: return t('acl.drop')
|
||||
default: return t('event.Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
editingRuleIndex.value = -1
|
||||
editingRule.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
priority: chain.value.rules.length,
|
||||
enabled: true,
|
||||
protocol: AclProtocol.Any,
|
||||
ports: [],
|
||||
source_ips: [],
|
||||
destination_ips: [],
|
||||
source_ports: [],
|
||||
action: AclAction.Allow,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: false,
|
||||
source_groups: [],
|
||||
destination_groups: [],
|
||||
}
|
||||
showRuleDialog.value = true
|
||||
}
|
||||
|
||||
function editRule(index: number) {
|
||||
editingRuleIndex.value = index
|
||||
editingRule.value = JSON.parse(JSON.stringify(chain.value.rules[index]))
|
||||
showRuleDialog.value = true
|
||||
}
|
||||
|
||||
function deleteRule(index: number) {
|
||||
chain.value.rules.splice(index, 1)
|
||||
}
|
||||
|
||||
function saveRule(rule: AclRule) {
|
||||
if (editingRuleIndex.value === -1) {
|
||||
chain.value.rules.push(rule)
|
||||
} else {
|
||||
chain.value.rules[editingRuleIndex.value] = rule
|
||||
}
|
||||
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
}
|
||||
|
||||
function onRowReorder(event: any) {
|
||||
chain.value.rules = event.value
|
||||
// Update priorities based on new order (higher priority at top)
|
||||
chain.value.rules.forEach((rule, index) => {
|
||||
rule.priority = chain.value.rules.length - index - 1
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Chain Metadata Section -->
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.chain.name') }}</label>
|
||||
<InputText v-model="chain.name" size="small" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.rule.description') }}</label>
|
||||
<InputText v-model="chain.description" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 col-span-full border-t pt-2 mt-2 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.rule.enabled') }}</label>
|
||||
<ToggleButton v-model="chain.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.chain.type') }}</label>
|
||||
<Select v-model="chain.chain_type" :options="chainTypeOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" size="small" class="w-40" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<label class="font-bold text-sm">{{ t('acl.default_action') }}</label>
|
||||
<SelectButton v-model="chain.default_action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" :allow-empty="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center gap-4 justify-between">
|
||||
<h4 class="text-md font-bold">{{ t('acl.rules') }}</h4>
|
||||
<Button icon="pi pi-plus" :label="t('acl.add_rule')" severity="success" size="small" @click="addRule" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="chain.rules" @row-reorder="onRowReorder" responsiveLayout="scroll">
|
||||
<Column rowReorder headerStyle="width: 3rem" />
|
||||
<Column field="enabled" :header="t('acl.rule.enabled')">
|
||||
<template #body="{ data }">
|
||||
<i class="pi" :class="data.enabled ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'"></i>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" :header="t('acl.rule.name')" />
|
||||
<Column :header="t('acl.match')">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col gap-2 py-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md text-[10px] font-bold uppercase tracking-wider">
|
||||
{{ getProtocolLabel(data.protocol) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Src</span>
|
||||
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||
<span v-for="ip in data.source_ips" :key="ip"
|
||||
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||
<span v-for="grp in data.source_groups" :key="grp"
|
||||
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||
<span v-if="data.source_ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||
data.source_ports.join(',') }}</span>
|
||||
<span v-if="!data.source_ips.length && !data.source_groups.length" class="text-gray-400">*</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i class="pi pi-arrow-right hidden sm:block text-gray-300 text-xs"></i>
|
||||
<Divider layout="horizontal" class="sm:hidden my-1" />
|
||||
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Dst</span>
|
||||
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||
<span v-for="ip in data.destination_ips" :key="ip"
|
||||
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||
<span v-for="grp in data.destination_groups" :key="grp"
|
||||
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||
<span v-if="data.ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||
data.ports.join(',') }}</span>
|
||||
<span v-if="!data.destination_ips.length && !data.destination_groups.length"
|
||||
class="text-gray-400">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="action" :header="t('acl.rule.action')">
|
||||
<template #body="{ data }">
|
||||
<span :class="data.action === AclAction.Allow ? 'text-green-600' : 'text-red-600 font-bold'">
|
||||
{{ getActionLabel(data.action) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :header="t('web.common.edit')">
|
||||
<template #body="{ index }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" text rounded @click="editRule(index)" />
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteRule(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<AclRuleDialog v-if="showRuleDialog && editingRule" v-model:visible="showRuleDialog" v-model:rule="editingRule"
|
||||
:group-names="props.groupNames" @save="saveRule" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Column, DataTable, Dialog, InputText, MultiSelect, Password } from 'primevue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { GroupIdentity, GroupInfo } from '../../types/network';
|
||||
|
||||
const props = defineProps<{
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const group = defineModel<GroupInfo>({ required: true })
|
||||
const emit = defineEmits(['rename-group'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const editingGroup = ref<GroupIdentity | null>(null)
|
||||
const editingGroupIndex = ref(-1)
|
||||
const showGroupDialog = ref(false)
|
||||
const oldGroupName = ref('')
|
||||
|
||||
function addGroup() {
|
||||
editingGroupIndex.value = -1
|
||||
editingGroup.value = {
|
||||
group_name: '',
|
||||
group_secret: '',
|
||||
}
|
||||
oldGroupName.value = ''
|
||||
showGroupDialog.value = true
|
||||
}
|
||||
|
||||
function editGroup(index: number) {
|
||||
editingGroupIndex.value = index
|
||||
editingGroup.value = JSON.parse(JSON.stringify(group.value.declares[index]))
|
||||
oldGroupName.value = editingGroup.value?.group_name || ''
|
||||
showGroupDialog.value = true
|
||||
}
|
||||
|
||||
function deleteGroup(index: number) {
|
||||
group.value.declares.splice(index, 1)
|
||||
}
|
||||
|
||||
function saveGroup() {
|
||||
if (!editingGroup.value) return
|
||||
const newName = editingGroup.value.group_name
|
||||
|
||||
if (editingGroupIndex.value === -1) {
|
||||
group.value.declares.push(editingGroup.value)
|
||||
} else {
|
||||
if (oldGroupName.value && oldGroupName.value !== newName) {
|
||||
// Sync in members
|
||||
group.value.members = group.value.members.map(m => m === oldGroupName.value ? newName : m)
|
||||
// Notify parent to sync in rules
|
||||
emit('rename-group', { oldName: oldGroupName.value, newName })
|
||||
}
|
||||
group.value.declares[editingGroupIndex.value] = editingGroup.value
|
||||
}
|
||||
showGroupDialog.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold text-lg">{{ t('acl.group.declares') }}</label>
|
||||
<small class="text-gray-500">{{ t('acl.group.help') }}</small>
|
||||
</div>
|
||||
<Button icon="pi pi-plus" :label="t('web.common.add')" severity="success" @click="addGroup" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="group.declares" responsiveLayout="scroll">
|
||||
<Column field="group_name" :header="t('acl.group.name')" />
|
||||
<Column field="group_secret" :header="t('acl.group.secret')">
|
||||
<template #body="{ data }">
|
||||
<Password v-model="data.group_secret" :feedback="false" toggleMask readonly plain class="w-full" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column :header="t('web.common.edit')" headerStyle="width: 8rem">
|
||||
<template #body="{ index }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" text rounded @click="editGroup(index)" />
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteGroup(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-lg">{{ t('acl.group.members') }}</label>
|
||||
<MultiSelect v-model="group.members" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.group.members')" />
|
||||
</div>
|
||||
|
||||
<!-- Group Identity Dialog -->
|
||||
<Dialog v-model:visible="showGroupDialog" modal :header="t('acl.groups')" :style="{ width: '400px' }">
|
||||
<div v-if="editingGroup" class="flex flex-col gap-4 pt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.group.name') }}</label>
|
||||
<InputText v-model="editingGroup.group_name" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.group.secret') }}</label>
|
||||
<Password v-model="editingGroup.group_secret" :feedback="false" toggleMask fluid />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="showGroupDialog = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="saveGroup" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Menu, Tab, TabList, TabPanel, TabPanels, Tabs } from 'primevue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Acl, AclAction, AclChainType } from '../../types/network'
|
||||
import AclChainEditor from './AclChainEditor.vue'
|
||||
import AclGroupEditor from './AclGroupEditor.vue'
|
||||
|
||||
const acl = defineModel<Acl>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref(0)
|
||||
const menu = ref()
|
||||
|
||||
const addMenuModel = ref([
|
||||
{ label: () => t('acl.inbound'), command: () => addChain(AclChainType.Inbound) },
|
||||
{ label: () => t('acl.outbound'), command: () => addChain(AclChainType.Outbound) },
|
||||
{ label: () => t('acl.forward'), command: () => addChain(AclChainType.Forward) },
|
||||
])
|
||||
|
||||
function addChain(type: AclChainType) {
|
||||
if (!acl.value.acl_v1) {
|
||||
acl.value.acl_v1 = { chains: [], group: { declares: [], members: [] } }
|
||||
}
|
||||
|
||||
let defaultName = ''
|
||||
switch (type) {
|
||||
case AclChainType.Inbound: defaultName = 'Inbound'; break;
|
||||
case AclChainType.Outbound: defaultName = 'Outbound'; break;
|
||||
case AclChainType.Forward: defaultName = 'Forward'; break;
|
||||
}
|
||||
|
||||
acl.value.acl_v1.chains.push({
|
||||
name: defaultName,
|
||||
chain_type: type,
|
||||
description: '',
|
||||
enabled: true,
|
||||
rules: [],
|
||||
default_action: AclAction.Allow
|
||||
})
|
||||
|
||||
activeTab.value = acl.value.acl_v1.chains.length - 1
|
||||
}
|
||||
|
||||
function removeChain(index: number) {
|
||||
if (confirm(t('acl.delete_chain_confirm'))) {
|
||||
acl.value.acl_v1?.chains.splice(index, 1)
|
||||
if (activeTab.value >= (acl.value.acl_v1?.chains.length || 0)) {
|
||||
activeTab.value = Math.max(0, (acl.value.acl_v1?.chains.length || 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRenameGroup({ oldName, newName }: { oldName: string, newName: string }) {
|
||||
if (!acl.value.acl_v1) return
|
||||
acl.value.acl_v1.chains.forEach(chain => {
|
||||
chain.rules.forEach(rule => {
|
||||
rule.source_groups = rule.source_groups.map(g => g === oldName ? newName : g)
|
||||
rule.destination_groups = rule.destination_groups.map(g => g === oldName ? newName : g)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const groupNames = computed(() => {
|
||||
return acl.value.acl_v1?.group?.declares.map(g => g.group_name) || []
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
const chains = acl.value.acl_v1?.chains || []
|
||||
const result: { type: string, label: string, index: number }[] = []
|
||||
|
||||
if (chains.length === 0) {
|
||||
result.push({ type: 'empty', label: t('acl.chains'), index: 0 })
|
||||
}
|
||||
else {
|
||||
chains.forEach((c, index) => {
|
||||
result.push({
|
||||
type: 'chain',
|
||||
label: c.name || `Chain ${index}`,
|
||||
index
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
result.push({ type: 'groups', label: t('acl.groups'), index: result.length })
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Tabs v-model:value="activeTab">
|
||||
<div class="flex items-center border-b border-surface-200 dark:border-surface-700">
|
||||
<TabList class="flex-grow min-w-0 overflow-x-auto" style="border-bottom: none;">
|
||||
<Tab v-for="tab in tabs" :key="tab.type + tab.index" :value="tab.index">
|
||||
<div class="flex items-center gap-2 whitespace-nowrap">
|
||||
{{ tab.label }}
|
||||
<Button v-if="tab.type === 'chain'" icon="pi pi-times" severity="danger" text rounded size="small"
|
||||
class="w-6 h-6 p-0" @click.stop="removeChain(tab.index)" />
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center px-2 bg-white dark:bg-gray-900 border-l border-surface-100 dark:border-surface-800">
|
||||
<Button icon="pi pi-plus" text rounded size="small" class="w-8 h-8 p-0"
|
||||
@click="(event) => menu.toggle(event)" />
|
||||
<Menu ref="menu" :model="addMenuModel" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
<TabPanels>
|
||||
<TabPanel v-for="tab in tabs" :key="'panel' + tab.type + tab.index" :value="tab.index">
|
||||
<!-- Empty State within TabPanel -->
|
||||
<div v-if="tab.type === 'empty'"
|
||||
class="py-8 flex flex-col items-center justify-center border-2 border-dashed border-surface-200 rounded-lg bg-surface-50 dark:bg-surface-900 dark:border-surface-700">
|
||||
<i class="pi pi-shield text-5xl mb-4 text-primary" />
|
||||
<div class="text-xl font-bold mb-2">{{ t('acl.chains') }}</div>
|
||||
<p class="text-surface-500 mb-8 text-center max-w-sm px-4">{{ t('acl.help') }}</p>
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<Button :label="t('acl.inbound')" icon="pi pi-arrow-down-left" @click="addChain(AclChainType.Inbound)" />
|
||||
<Button :label="t('acl.outbound')" icon="pi pi-arrow-up-right" @click="addChain(AclChainType.Outbound)" />
|
||||
<Button :label="t('acl.forward')" icon="pi pi-directions" @click="addChain(AclChainType.Forward)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rule Chains -->
|
||||
<div v-if="tab.type === 'chain' && acl.acl_v1 && acl.acl_v1.chains[tab.index]" class="py-4">
|
||||
<AclChainEditor v-model="acl.acl_v1.chains[tab.index]" :group-names="groupNames" />
|
||||
</div>
|
||||
|
||||
<!-- Group Management -->
|
||||
<div v-if="tab.type === 'groups'" class="py-4">
|
||||
<template v-if="acl.acl_v1">
|
||||
<AclGroupEditor v-if="acl.acl_v1.group" v-model="acl.acl_v1.group" :group-names="groupNames"
|
||||
@rename-group="handleRenameGroup" />
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('web.common.add') + ' ' + t('acl.groups')"
|
||||
@click="acl.acl_v1.group = { declares: [], members: [] }" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('acl.enabled')"
|
||||
@click="acl.acl_v1 = { chains: [], group: { declares: [], members: [] } }" />
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { AutoComplete, Button, Checkbox, Dialog, InputNumber, InputText, MultiSelect, Panel, SelectButton, ToggleButton } from 'primevue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AclAction, AclProtocol, AclRule } from '../../types/network';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save'])
|
||||
|
||||
const rule = defineModel<AclRule>('rule', { required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const protocolOptions = [
|
||||
{ label: () => t('acl.any'), value: AclProtocol.Any },
|
||||
{ label: 'TCP', value: AclProtocol.TCP },
|
||||
{ label: 'UDP', value: AclProtocol.UDP },
|
||||
{ label: 'ICMP', value: AclProtocol.ICMP },
|
||||
{ label: 'ICMPv6', value: AclProtocol.ICMPv6 },
|
||||
]
|
||||
|
||||
const actionOptions = [
|
||||
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||
]
|
||||
|
||||
const showPorts = computed(() => {
|
||||
return rule.value.protocol === AclProtocol.TCP || rule.value.protocol === AclProtocol.UDP || rule.value.protocol === AclProtocol.Any
|
||||
})
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', rule.value)
|
||||
close()
|
||||
}
|
||||
|
||||
// Suggestions for IP/Port AutoComplete
|
||||
const genericSuggestions = ref<string[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal :header="t('acl.edit_rule')"
|
||||
:style="{ width: '90vw', maxWidth: '600px' }">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.name') }}</label>
|
||||
<InputText v-model="rule.name" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.enabled') }}</label>
|
||||
<ToggleButton v-model="rule.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.description') }}</label>
|
||||
<InputText v-model="rule.description" fluid />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.action') }}</label>
|
||||
<SelectButton v-model="rule.action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" :allow-empty="false" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.protocol') }}</label>
|
||||
<SelectButton v-model="rule.protocol" :options="protocolOptions"
|
||||
:option-label="opt => typeof opt.label === 'function' ? opt.label() : opt.label" option-value="value"
|
||||
:allow-empty="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Panel :header="t('acl.rules')" toggleable>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.src_ips') }}</label>
|
||||
<AutoComplete v-model="rule.source_ips" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]"
|
||||
:placeholder="t('chips_placeholder', ['10.126.126.0/24'])" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_ips') }}</label>
|
||||
<AutoComplete v-model="rule.destination_ips" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]"
|
||||
:placeholder="t('chips_placeholder', ['10.126.126.2/32'])" />
|
||||
</div>
|
||||
|
||||
<div v-if="showPorts" class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.src_ports') }}</label>
|
||||
<AutoComplete v-model="rule.source_ports" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_ports') }}</label>
|
||||
<AutoComplete v-model="rule.ports" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="rule.stateful" :binary="true" inputId="rule-stateful" />
|
||||
<label for="rule-stateful" class="font-bold">{{ t('acl.rule.stateful') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.rate_limit') }}</label>
|
||||
<InputNumber v-model="rule.rate_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.burst_limit') }}</label>
|
||||
<InputNumber v-model="rule.burst_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.src_groups') }}</label>
|
||||
<MultiSelect v-model="rule.source_groups" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.rule.src_groups')" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_groups') }}</label>
|
||||
<MultiSelect v-model="rule.destination_groups" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.rule.dst_groups')" />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="close" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="save" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -3,6 +3,14 @@ networking_method: 网络方式
|
||||
public_server: 公共服务器
|
||||
manual: 手动
|
||||
standalone: 独立
|
||||
initial_nodes: 初始节点
|
||||
initial_nodes_help: |
|
||||
EasyTier 不分服务端/客户端。
|
||||
• 填“初始节点” = 插上网线,直接加入已有网络。
|
||||
• 留空 = 节点独立启动,等别人来连,或你后续手动连。
|
||||
• 无论直接还是间接连通(通过其他节点搭桥),都能组网互通。
|
||||
初始节点可以用自己的,也可以用别人分享的。
|
||||
initial_node_placeholder: 例如:node.example.com
|
||||
virtual_ipv4: 虚拟IPv4地址
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: 网络名称
|
||||
@@ -18,12 +26,17 @@ advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
rpc_port: RPC端口
|
||||
port: 端口
|
||||
rpc_portal_whitelists: RPC白名单
|
||||
config_network: 配置网络
|
||||
running: 运行中
|
||||
error_msg: 错误信息
|
||||
detail: 详情
|
||||
add_new_network: 添加新网络
|
||||
add_peer_url: 添加节点
|
||||
add_initial_node: 添加初始节点
|
||||
add_listener_url: 添加监听地址
|
||||
add_mapped_listener: 添加监听映射
|
||||
del_cur_network: 删除当前网络
|
||||
select_network: 选择网络
|
||||
network_instances: 网络实例
|
||||
@@ -91,6 +104,9 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
||||
disable_ipv6: 禁用IPv6
|
||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||
|
||||
ipv6_public_addr_auto: 自动获取公网 IPv6
|
||||
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
||||
|
||||
enable_kcp_proxy: 启用 KCP 代理
|
||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
@@ -104,11 +120,14 @@ disable_quic_input: 禁用 QUIC 输入
|
||||
disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的节点仍然使用 TCP 连接到本节点。
|
||||
|
||||
disable_p2p: 禁用 P2P
|
||||
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
||||
disable_p2p_help: 禁用普通自动 P2P。开启 need-p2p 的节点仍可与当前节点建立 P2P。
|
||||
|
||||
p2p_only: 仅 P2P
|
||||
p2p_only_help: 仅与已经建立P2P连接的对等节点通信,不通过其他节点中转。
|
||||
|
||||
lazy_p2p: 延迟 P2P
|
||||
lazy_p2p_help: 仅在实际流量需要某个对等节点时才尝试建立 P2P。开启 need-p2p 的节点仍会被主动连接。
|
||||
|
||||
bind_device: 仅使用物理网卡
|
||||
bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网建立连接。
|
||||
|
||||
@@ -123,6 +142,9 @@ relay_all_peer_rpc_help: |
|
||||
允许转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。
|
||||
这可以帮助白名单外网络中的对等节点建立P2P连接。
|
||||
|
||||
need_p2p: 需要 P2P
|
||||
need_p2p_help: 即使其他节点启用了 lazy p2p,也要求它们主动与当前节点建立 P2P 连接。
|
||||
|
||||
multi_thread: 启用多线程
|
||||
multi_thread_help: 使用多线程运行时
|
||||
|
||||
@@ -130,7 +152,7 @@ proxy_forward_by_system: 系统转发
|
||||
proxy_forward_by_system_help: 通过系统内核转发子网代理数据包,禁用内置NAT
|
||||
|
||||
disable_encryption: 禁用加密
|
||||
disable_encryption_help: 禁用对等节点通信的加密,默认为false,必须与对等节点相同
|
||||
disable_encryption_help: 禁用对等节点通信的加密。注意:默认启用加密,若勾选此项则关闭,必须与对等节点设置一致。
|
||||
|
||||
disable_tcp_hole_punching: 禁用TCP打洞
|
||||
disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
@@ -138,6 +160,9 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
disable_upnp: 禁用 UPnP
|
||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||
|
||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||
|
||||
@@ -177,6 +202,12 @@ mtu_help: |
|
||||
TUN设备的MTU,默认为非加密时为1380,加密时为1360。范围:400-1380
|
||||
mtu_placeholder: 留空为默认值1380
|
||||
|
||||
instance_recv_bps_limit: 实例接收限速
|
||||
instance_recv_bps_limit_help: |
|
||||
限制当前实例整体入站流量的总接收速率,单位为字节每秒。
|
||||
留空表示不限速。
|
||||
instance_recv_bps_limit_placeholder: 留空表示不限速
|
||||
|
||||
mapped_listeners: 监听映射
|
||||
mapped_listeners_help: |
|
||||
手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。
|
||||
@@ -242,6 +273,7 @@ web:
|
||||
captcha: 验证码
|
||||
back_to_login: 返回登录
|
||||
login: 登录
|
||||
sso_login: "SSO 登录"
|
||||
|
||||
register:
|
||||
title: 注册
|
||||
@@ -329,11 +361,15 @@ web:
|
||||
delete: 删除
|
||||
edit: 编辑
|
||||
refresh: 刷新
|
||||
add: 添加
|
||||
loading: 加载中...
|
||||
error: 错误
|
||||
success: 成功
|
||||
warning: 警告
|
||||
info: 提示
|
||||
enable: 开启
|
||||
disable: 关闭
|
||||
address: 地址
|
||||
|
||||
settings:
|
||||
title: 设置
|
||||
@@ -350,6 +386,8 @@ mode:
|
||||
switch_mode: 切换模式
|
||||
config_dir: 配置目录
|
||||
rpc_portal: RPC端口
|
||||
enable_rpc_tcp_listen: 开启 RPC 端口监听(TCP)
|
||||
rpc_listen_port: RPC 监听端口
|
||||
log_level: 日志级别
|
||||
log_dir: 日志目录
|
||||
remote_rpc_address: 远程RPC地址
|
||||
@@ -370,6 +408,7 @@ mode:
|
||||
stop_service_success: 服务停止成功
|
||||
remote_rpc_address_empty: 远程RPC地址不能为空
|
||||
service_config_empty: 服务配置不能为空
|
||||
rpc_connection_failed: "RPC 连接失败:{error}"
|
||||
|
||||
config-server:
|
||||
title: 配置服务器
|
||||
@@ -390,3 +429,46 @@ config-server:
|
||||
client:
|
||||
not_running: 无法连接至远程客户端
|
||||
retry: 重试
|
||||
|
||||
acl:
|
||||
title: 访问控制
|
||||
help: 访问控制列表,用于限制节点间的通信。
|
||||
enabled: 启用 ACL
|
||||
default_action: 默认动作
|
||||
chains: 规则链
|
||||
inbound: 入站
|
||||
outbound: 出站
|
||||
forward: 转发
|
||||
rules: 规则
|
||||
add_rule: 添加规则
|
||||
edit_rule: 编辑规则
|
||||
rule:
|
||||
name: 规则名称
|
||||
description: 描述
|
||||
enabled: 启用
|
||||
protocol: 协议
|
||||
action: 动作
|
||||
src_ips: 来源 IP
|
||||
dst_ips: 目的 IP
|
||||
src_ports: 来源端口
|
||||
dst_ports: 目的端口
|
||||
rate_limit: 速率限制 (pps)
|
||||
burst_limit: 爆发限制
|
||||
stateful: 状态追踪
|
||||
src_groups: 来源组
|
||||
dst_groups: 目的组
|
||||
groups: 组管理
|
||||
group:
|
||||
declares: 声明组
|
||||
members: 加入组
|
||||
name: 组名
|
||||
secret: 密钥
|
||||
help: 在此处定义网络中的组身份,以便在规则中使用。
|
||||
any: 任意
|
||||
allow: 允许
|
||||
drop: 丢弃
|
||||
delete_chain_confirm: 确定要删除此规则链及其所有规则吗?
|
||||
chain:
|
||||
name: 名称
|
||||
type: 类型
|
||||
match: 匹配
|
||||
|
||||
@@ -3,6 +3,14 @@ networking_method: Networking Method
|
||||
public_server: Public Server
|
||||
manual: Manual
|
||||
standalone: Standalone
|
||||
initial_nodes: Initial Nodes
|
||||
initial_nodes_help: |
|
||||
EasyTier does not distinguish between server and client roles.
|
||||
• Filling in Initial Nodes = plugging in the cable and joining an existing network.
|
||||
• Leaving it empty = the node starts alone until others connect to it, or you connect it later yourself.
|
||||
• Direct or indirect connectivity, including through relay nodes, can form one network.
|
||||
Initial nodes can be your own nodes or ones shared by others.
|
||||
initial_node_placeholder: "Example: node.example.com"
|
||||
virtual_ipv4: Virtual IPv4
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: Network Name
|
||||
@@ -18,12 +26,17 @@ advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
rpc_port: RPC Port
|
||||
port: Port
|
||||
rpc_portal_whitelists: RPC Whitelist
|
||||
config_network: Config Network
|
||||
running: Running
|
||||
error_msg: Error Message
|
||||
detail: Detail
|
||||
add_new_network: New Network
|
||||
add_peer_url: Add Peer
|
||||
add_initial_node: Add Initial Node
|
||||
add_listener_url: Add Listener
|
||||
add_mapped_listener: Add Mapped Listener
|
||||
del_cur_network: Delete Current Network
|
||||
select_network: Select Network
|
||||
network_instances: Network Instances
|
||||
@@ -90,6 +103,9 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
||||
disable_ipv6: Disable IPv6
|
||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
||||
|
||||
ipv6_public_addr_auto: Auto Public IPv6
|
||||
ipv6_public_addr_auto_help: Auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet.
|
||||
|
||||
enable_kcp_proxy: Enable KCP Proxy
|
||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||
|
||||
@@ -103,11 +119,14 @@ disable_quic_input: Disable QUIC Input
|
||||
disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC proxy enabled continue to connect using TCP.
|
||||
|
||||
disable_p2p: Disable P2P
|
||||
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
|
||||
disable_p2p_help: Disable ordinary automatic P2P. Nodes with need-p2p enabled can still establish P2P with this node.
|
||||
|
||||
p2p_only: P2P Only
|
||||
p2p_only_help: Only communicate with peers that have already established P2P connections, do not relay through other nodes.
|
||||
|
||||
lazy_p2p: Lazy P2P
|
||||
lazy_p2p_help: Only try to establish P2P when traffic actually targets a peer. Peers with need-p2p enabled are still connected proactively.
|
||||
|
||||
bind_device: Bind to Physical Device Only
|
||||
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
|
||||
|
||||
@@ -122,6 +141,9 @@ relay_all_peer_rpc_help: |
|
||||
Relay all peer rpc packets, even if the peer is not in the relay network whitelist.
|
||||
This can help peers not in relay network whitelist to establish p2p connection.
|
||||
|
||||
need_p2p: Need P2P
|
||||
need_p2p_help: Ask other peers to proactively establish P2P connections to this node even when they enable lazy P2P.
|
||||
|
||||
multi_thread: Multi Thread
|
||||
multi_thread_help: Use multi-thread runtime
|
||||
|
||||
@@ -129,7 +151,7 @@ proxy_forward_by_system: System Forward
|
||||
proxy_forward_by_system_help: Forward packet to proxy networks via system kernel, disable internal nat for network proxy
|
||||
|
||||
disable_encryption: Disable Encryption
|
||||
disable_encryption_help: Disable encryption for peers communication, default is false, must be same with peers
|
||||
disable_encryption_help: Disable encryption for peers communication. Encryption is enabled by default, this option must be same with peers.
|
||||
|
||||
disable_tcp_hole_punching: Disable TCP Hole Punching
|
||||
disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
@@ -137,6 +159,9 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
disable_upnp: Disable UPnP
|
||||
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||
|
||||
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
||||
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||
|
||||
@@ -177,6 +202,12 @@ mtu_help: |
|
||||
MTU of the TUN device, default is 1380 for non-encryption, 1360 for encryption. Range:400-1380
|
||||
mtu_placeholder: Leave blank as default value 1380
|
||||
|
||||
instance_recv_bps_limit: Instance Receive Limit
|
||||
instance_recv_bps_limit_help: |
|
||||
Limit the total receive bandwidth for the whole instance. Unit: bytes per second.
|
||||
Leave blank for no limit.
|
||||
instance_recv_bps_limit_placeholder: Leave blank for no limit
|
||||
|
||||
mapped_listeners: Map Listeners
|
||||
mapped_listeners_help: |
|
||||
Manually specify the public address of the listener, other nodes can use this address to connect to this node.
|
||||
@@ -242,6 +273,7 @@ web:
|
||||
captcha: Captcha
|
||||
back_to_login: Back to Login
|
||||
login: Login
|
||||
sso_login: "SSO Login"
|
||||
|
||||
register:
|
||||
title: Register
|
||||
@@ -329,11 +361,15 @@ web:
|
||||
delete: Delete
|
||||
edit: Edit
|
||||
refresh: Refresh
|
||||
add: Add
|
||||
loading: Loading...
|
||||
error: Error
|
||||
success: Success
|
||||
warning: Warning
|
||||
info: Info
|
||||
enable: Enable
|
||||
disable: Disable
|
||||
address: Address
|
||||
|
||||
settings:
|
||||
title: Settings
|
||||
@@ -350,6 +386,8 @@ mode:
|
||||
switch_mode: Switch Mode
|
||||
config_dir: Config Dir
|
||||
rpc_portal: RPC Portal
|
||||
enable_rpc_tcp_listen: Enable RPC port listening (TCP)
|
||||
rpc_listen_port: RPC Listen Port
|
||||
log_level: Log Level
|
||||
log_dir: Log Dir
|
||||
remote_rpc_address: Remote RPC Address
|
||||
@@ -370,6 +408,7 @@ mode:
|
||||
stop_service_success: Service stopped successfully
|
||||
remote_rpc_address_empty: Remote RPC Address cannot be empty
|
||||
service_config_empty: Service Config cannot be empty
|
||||
rpc_connection_failed: "RPC connection failed: {error}"
|
||||
|
||||
config-server:
|
||||
title: Config Server
|
||||
@@ -390,3 +429,46 @@ config-server:
|
||||
client:
|
||||
not_running: Unable to connect to remote client.
|
||||
retry: Retry
|
||||
|
||||
acl:
|
||||
title: Access Control (ACL)
|
||||
help: Access control list to restrict communication between nodes.
|
||||
enabled: Enable ACL
|
||||
default_action: Default Action
|
||||
chains: Rule Chains
|
||||
inbound: Inbound
|
||||
outbound: Outbound
|
||||
forward: Forward
|
||||
rules: Rules
|
||||
add_rule: Add Rule
|
||||
edit_rule: Edit Rule
|
||||
rule:
|
||||
name: Rule Name
|
||||
description: Description
|
||||
enabled: Enabled
|
||||
protocol: Protocol
|
||||
action: Action
|
||||
src_ips: Source IPs
|
||||
dst_ips: Destination IPs
|
||||
src_ports: Source Ports
|
||||
dst_ports: Destination Ports
|
||||
rate_limit: Rate Limit (pps)
|
||||
burst_limit: Burst Limit
|
||||
stateful: Stateful
|
||||
src_groups: Source Groups
|
||||
dst_groups: Destination Groups
|
||||
groups: Groups
|
||||
group:
|
||||
declares: Declared Groups
|
||||
members: Node Memberships
|
||||
name: Group Name
|
||||
secret: Group Secret
|
||||
help: Define group identities in the network to use them in rules.
|
||||
any: Any
|
||||
allow: Allow
|
||||
drop: Drop
|
||||
delete_chain_confirm: Are you sure you want to delete this rule chain and all its rules?
|
||||
chain:
|
||||
name: Name
|
||||
type: Type
|
||||
match: Match
|
||||
|
||||
@@ -49,4 +49,6 @@
|
||||
|
||||
.v-popper__inner {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
max-width: 32rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,82 @@ export enum NetworkingMethod {
|
||||
Standalone = 2,
|
||||
}
|
||||
|
||||
export interface SecureModeConfig {
|
||||
enabled: boolean
|
||||
// Keep protocol compatibility with backend/import-export flows even though the GUI
|
||||
// does not render secure-mode or credential inputs.
|
||||
local_private_key?: string
|
||||
local_public_key?: string
|
||||
}
|
||||
|
||||
export enum AclProtocol {
|
||||
Unspecified = 0,
|
||||
TCP = 1,
|
||||
UDP = 2,
|
||||
ICMP = 3,
|
||||
ICMPv6 = 4,
|
||||
Any = 5,
|
||||
}
|
||||
|
||||
export enum AclAction {
|
||||
Noop = 0,
|
||||
Allow = 1,
|
||||
Drop = 2,
|
||||
}
|
||||
|
||||
export enum AclChainType {
|
||||
UnspecifiedChain = 0,
|
||||
Inbound = 1,
|
||||
Outbound = 2,
|
||||
Forward = 3,
|
||||
}
|
||||
|
||||
export interface AclRule {
|
||||
name: string
|
||||
description: string
|
||||
priority: number
|
||||
enabled: boolean
|
||||
protocol: AclProtocol
|
||||
ports: string[]
|
||||
source_ips: string[]
|
||||
destination_ips: string[]
|
||||
source_ports: string[]
|
||||
action: AclAction
|
||||
rate_limit: number
|
||||
burst_limit: number
|
||||
stateful: boolean
|
||||
source_groups: string[]
|
||||
destination_groups: string[]
|
||||
}
|
||||
|
||||
export interface AclChain {
|
||||
name: string
|
||||
chain_type: AclChainType
|
||||
description: string
|
||||
enabled: boolean
|
||||
rules: AclRule[]
|
||||
default_action: AclAction
|
||||
}
|
||||
|
||||
export interface GroupIdentity {
|
||||
group_name: string
|
||||
group_secret: string
|
||||
}
|
||||
|
||||
export interface GroupInfo {
|
||||
declares: GroupIdentity[]
|
||||
members: string[]
|
||||
}
|
||||
|
||||
export interface AclV1 {
|
||||
chains: AclChain[]
|
||||
group?: GroupInfo
|
||||
}
|
||||
|
||||
export interface Acl {
|
||||
acl_v1?: AclV1
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
instance_id: string
|
||||
|
||||
@@ -14,7 +90,9 @@ export interface NetworkConfig {
|
||||
network_length: number
|
||||
hostname?: string
|
||||
network_name: string
|
||||
network_secret: string
|
||||
network_secret?: string
|
||||
credential_file?: string
|
||||
secure_mode?: SecureModeConfig
|
||||
|
||||
networking_method: NetworkingMethod
|
||||
|
||||
@@ -37,21 +115,25 @@ export interface NetworkConfig {
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
ipv6_public_addr_auto?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
disable_quic_input?: boolean
|
||||
disable_p2p?: boolean
|
||||
p2p_only?: boolean
|
||||
lazy_p2p?: boolean
|
||||
bind_device?: boolean
|
||||
no_tun?: boolean
|
||||
enable_exit_node?: boolean
|
||||
relay_all_peer_rpc?: boolean
|
||||
need_p2p?: boolean
|
||||
multi_thread?: boolean
|
||||
proxy_forward_by_system?: boolean
|
||||
disable_encryption?: boolean
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_upnp?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
enable_relay_network_whitelist?: boolean
|
||||
@@ -66,12 +148,14 @@ export interface NetworkConfig {
|
||||
socks5_port: number
|
||||
|
||||
mtu: number | null
|
||||
instance_recv_bps_limit: number | null
|
||||
mapped_listeners: string[]
|
||||
|
||||
enable_magic_dns?: boolean
|
||||
enable_private_mode?: boolean
|
||||
|
||||
port_forwards: PortForwardConfig[]
|
||||
acl?: Acl
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -83,10 +167,10 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
network_length: 24,
|
||||
network_name: 'easytier',
|
||||
network_secret: '',
|
||||
credential_file: '',
|
||||
|
||||
networking_method: NetworkingMethod.PublicServer,
|
||||
|
||||
public_server_url: 'tcp://public.easytier.top:11010',
|
||||
networking_method: NetworkingMethod.Manual,
|
||||
public_server_url: '',
|
||||
peer_urls: [],
|
||||
|
||||
proxy_cidrs: [],
|
||||
@@ -108,21 +192,25 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
use_smoltcp: false,
|
||||
disable_ipv6: false,
|
||||
ipv6_public_addr_auto: false,
|
||||
enable_kcp_proxy: false,
|
||||
disable_kcp_input: false,
|
||||
enable_quic_proxy: false,
|
||||
disable_quic_input: false,
|
||||
disable_p2p: false,
|
||||
p2p_only: false,
|
||||
lazy_p2p: false,
|
||||
bind_device: true,
|
||||
no_tun: false,
|
||||
enable_exit_node: false,
|
||||
relay_all_peer_rpc: false,
|
||||
need_p2p: false,
|
||||
multi_thread: true,
|
||||
proxy_forward_by_system: false,
|
||||
disable_encryption: false,
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_upnp: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
relay_network_whitelist: [],
|
||||
@@ -132,13 +220,56 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
enable_socks5: false,
|
||||
socks5_port: 1080,
|
||||
mtu: null,
|
||||
instance_recv_bps_limit: null,
|
||||
mapped_listeners: [],
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
port_forwards: [],
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: {
|
||||
declares: [],
|
||||
members: [],
|
||||
},
|
||||
chains: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function cleanPeerUrls(urls: string[] | undefined): string[] {
|
||||
return (urls ?? []).map((url) => url.trim()).filter((url) => url.length > 0)
|
||||
}
|
||||
|
||||
export function normalizeNetworkConfig(config: NetworkConfig): NetworkConfig {
|
||||
const normalized: NetworkConfig = {
|
||||
...config,
|
||||
peer_urls: cleanPeerUrls(config.peer_urls),
|
||||
}
|
||||
|
||||
const publicServerUrl = normalized.public_server_url?.trim() ?? ''
|
||||
|
||||
switch (normalized.networking_method) {
|
||||
case NetworkingMethod.PublicServer:
|
||||
normalized.peer_urls = publicServerUrl ? [publicServerUrl] : []
|
||||
break
|
||||
case NetworkingMethod.Manual:
|
||||
break
|
||||
case NetworkingMethod.Standalone:
|
||||
default:
|
||||
normalized.peer_urls = []
|
||||
break
|
||||
}
|
||||
|
||||
normalized.networking_method = NetworkingMethod.Manual
|
||||
normalized.public_server_url = ''
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function toBackendNetworkConfig(config: NetworkConfig): NetworkConfig {
|
||||
return normalizeNetworkConfig(config)
|
||||
}
|
||||
|
||||
export interface NetworkInstance {
|
||||
instance_id: string
|
||||
|
||||
@@ -204,6 +335,7 @@ export interface NodeInfo {
|
||||
stun_info: StunInfo
|
||||
listeners: Url[]
|
||||
vpn_portal_cfg?: string
|
||||
peer_id: number
|
||||
}
|
||||
|
||||
export interface StunInfo {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||
"@primeuix/themes": "^1.2.3",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.13.5",
|
||||
"easytier-frontend-lib": "workspace:*",
|
||||
"primevue": "^4.3.9",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
@@ -28,7 +28,7 @@
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "=3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.10",
|
||||
"vite": "^5.4.21",
|
||||
"vite-plugin-singlefile": "^2.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -68,8 +68,43 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const oidcEnabled = ref(false);
|
||||
const lastCheckedHost = ref('');
|
||||
const oidcCheckTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||
const checkOidcConfig = () => {
|
||||
if (oidcCheckTimer.value) clearTimeout(oidcCheckTimer.value);
|
||||
oidcCheckTimer.value = setTimeout(async () => {
|
||||
const host = apiHost.value;
|
||||
if (host === lastCheckedHost.value) return;
|
||||
|
||||
const enabled = (await new ApiClient(host).getOidcConfig()).enabled;
|
||||
// If host changes while request is in-flight, do not overwrite UI state.
|
||||
if (apiHost.value !== host) return;
|
||||
|
||||
lastCheckedHost.value = host;
|
||||
oidcEnabled.value = enabled;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
watch(apiHost, () => {
|
||||
checkOidcConfig();
|
||||
});
|
||||
|
||||
const onSsoLogin = () => {
|
||||
saveApiHost(apiHost.value);
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
window.location.href = api.value.oidcLoginUrl();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkOidcConfig();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (oidcCheckTimer.value) {
|
||||
clearTimeout(oidcCheckTimer.value);
|
||||
oidcCheckTimer.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -104,6 +139,10 @@ onMounted(() => {
|
||||
<Button :label="t('web.login.register')" type="button" class="w-full"
|
||||
@click="saveApiHost(apiHost); $router.replace({ name: 'register' })" severity="secondary" />
|
||||
</div>
|
||||
<div v-if="oidcEnabled" class="flex items-center justify-between">
|
||||
<Button :label="t('web.login.sso_login')" type="button" class="w-full" severity="info"
|
||||
@click="onSsoLogin" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
@@ -144,4 +183,4 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { type Api, type NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
export interface OidcConfigResponse {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 定义接口返回的数据结构
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
@@ -174,6 +178,19 @@ export class ApiClient {
|
||||
return this.client.defaults.baseURL + '/auth/captcha';
|
||||
}
|
||||
|
||||
public async getOidcConfig(): Promise<OidcConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.get<any, OidcConfigResponse>('/auth/oidc/config');
|
||||
return response;
|
||||
} catch (error) {
|
||||
return { enabled: false };
|
||||
}
|
||||
}
|
||||
|
||||
public oidcLoginUrl() {
|
||||
return this.client.defaults.baseURL + '/auth/oidc/login';
|
||||
}
|
||||
|
||||
public get_remote_client(machine_id: string): Api.RemoteClient {
|
||||
return new WebRemoteClient(machine_id, this.client);
|
||||
}
|
||||
@@ -189,13 +206,13 @@ class WebRemoteClient implements Api.RemoteClient {
|
||||
}
|
||||
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
|
||||
const response = await this.client.post<NetworkTypes.NetworkConfig, ValidateConfigResponse>(`/machines/${this.machine_id}/validate-config`, {
|
||||
config: config,
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
async run_network(config: NetworkTypes.NetworkConfig, save: boolean): Promise<undefined> {
|
||||
await this.client.post<string>(`/machines/${this.machine_id}/networks`, {
|
||||
config: config,
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
save: save
|
||||
});
|
||||
}
|
||||
@@ -216,15 +233,19 @@ class WebRemoteClient implements Api.RemoteClient {
|
||||
});
|
||||
}
|
||||
async save_config(config: NetworkTypes.NetworkConfig): Promise<undefined> {
|
||||
await this.client.put(`/machines/${this.machine_id}/networks/config/${config.instance_id}`, { config });
|
||||
await this.client.put(`/machines/${this.machine_id}/networks/config/${config.instance_id}`, {
|
||||
config: NetworkTypes.toBackendNetworkConfig(config)
|
||||
});
|
||||
}
|
||||
async get_network_config(inst_id: string): Promise<NetworkTypes.NetworkConfig> {
|
||||
const response = await this.client.get<any, NetworkTypes.NetworkConfig>('/machines/' + this.machine_id + '/networks/config/' + inst_id);
|
||||
return response;
|
||||
return NetworkTypes.normalizeNetworkConfig(response);
|
||||
}
|
||||
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', { config });
|
||||
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', {
|
||||
config: NetworkTypes.toBackendNetworkConfig(config)
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -236,6 +257,9 @@ class WebRemoteClient implements Api.RemoteClient {
|
||||
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', { toml_config });
|
||||
if (response.config) {
|
||||
response.config = NetworkTypes.normalizeNetworkConfig(response.config);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -252,4 +276,4 @@ class WebRemoteClient implements Api.RemoteClient {
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
||||
export default ApiClient;
|
||||
|
||||
@@ -17,14 +17,20 @@ cli:
|
||||
en: "The port to listen for the config server, used by the easytier-core to connect to"
|
||||
zh-CN: "配置服务器的监听端口,用于被 easytier-core 连接"
|
||||
config_server_protocol:
|
||||
en: "The protocol to listen for the config server, used by the easytier-core to connect to"
|
||||
zh-CN: "配置服务器的监听协议,用于被 easytier-core 连接, 可能的值:udp, tcp"
|
||||
en: "The protocol to listen for the config server, used by the easytier-core to connect to, possible values: udp, tcp, ws"
|
||||
zh-CN: "配置服务器的监听协议,用于被 easytier-core 连接, 可能的值:udp, tcp, ws"
|
||||
api_server_port:
|
||||
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
|
||||
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"
|
||||
api_server_addr:
|
||||
en: "The listen address for the restful server, e.g. 0.0.0.0, ::, 127.0.0.1"
|
||||
zh-CN: "restful 服务器的监听地址, 例如 0.0.0.0, ::, 127.0.0.1"
|
||||
web_server_port:
|
||||
en: "The port to listen for the web dashboard server, default is same as the api server port"
|
||||
zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同"
|
||||
web_server_addr:
|
||||
en: "The listen address for the web dashboard server (only effective when web_server_port differs from api_server_port or web_server_addr differs from api_server_addr), e.g. 0.0.0.0, ::, 127.0.0.1"
|
||||
zh-CN: "web dashboard 服务器的监听地址(仅在 web_server_port 与 api_server_port 不同,或 web_server_addr 与 api_server_addr 不同时生效), 例如 0.0.0.0, ::, 127.0.0.1"
|
||||
no_web:
|
||||
en: "Do not run the web dashboard server"
|
||||
zh-CN: "不运行 web dashboard 服务器"
|
||||
@@ -33,4 +39,34 @@ cli:
|
||||
zh-CN: "API 服务器的 URL,用于 web 前端连接"
|
||||
geoip_db:
|
||||
en: "The path to the GeoIP2 database file, used to lookup the location of the client, default is the embedded file (only country information) , recommend https://github.com/P3TERX/GeoLite.mmdb"
|
||||
zh-CN: "GeoIP2 数据库文件路径,用于查找客户端的位置,默认为嵌入文件(仅国家信息),推荐 https://github.com/P3TERX/GeoLite.mmdb"
|
||||
zh-CN: "GeoIP2 数据库文件路径,用于查找客户端的位置,默认为嵌入文件(仅国家信息),推荐 https://github.com/P3TERX/GeoLite.mmdb"
|
||||
disable_registration:
|
||||
en: "Disable user registration"
|
||||
zh-CN: "禁用用户注册"
|
||||
oidc_issuer_url:
|
||||
en: "The OIDC issuer URL for single sign-on authentication"
|
||||
zh-CN: "OIDC 签发者 URL,用于单点登录认证"
|
||||
oidc_client_id:
|
||||
en: "The OIDC client ID"
|
||||
zh-CN: "OIDC 客户端 ID"
|
||||
oidc_client_secret:
|
||||
en: "The OIDC client secret (can also be set via OIDC_CLIENT_SECRET env var)"
|
||||
zh-CN: "OIDC 客户端密钥(也可通过 OIDC_CLIENT_SECRET 环境变量设置)"
|
||||
oidc_username_claim:
|
||||
en: "The OIDC claim to use as the local username, default: preferred_username"
|
||||
zh-CN: "用作本地用户名的 OIDC claim 字段,默认: preferred_username"
|
||||
oidc_scopes:
|
||||
en: "OIDC scopes to request during login. Supports comma-separated values or repeated --oidc-scopes flags, default: openid,profile"
|
||||
zh-CN: "登录时请求的 OIDC scopes。支持逗号分隔或多次指定 --oidc-scopes,默认: openid,profile"
|
||||
oidc_redirect_url:
|
||||
en: "The OIDC redirect URL (callback URL), must match exactly what is registered with your Identity Provider. Required when using OIDC. Example: http://your-domain.com:11211/api/v1/auth/oidc/callback"
|
||||
zh-CN: "OIDC 重定向 URL(回调 URL),必须与身份提供商注册的地址完全一致。使用 OIDC 时必须提供。示例: http://your-domain.com:11211/api/v1/auth/oidc/callback"
|
||||
allow_auto_create_user:
|
||||
en: "Allow auto-creating local user when easytier-core connects with an unknown username"
|
||||
zh-CN: "当 easytier-core 使用未知用户名连接时,允许自动创建本地用户"
|
||||
oidc_disable_pkce:
|
||||
en: "Disable PKCE (Proof Key for Code Exchange) for OIDC authentication"
|
||||
zh-CN: "禁用 OIDC 认证的 PKCE(授权码交换证明密钥)"
|
||||
oidc_frontend_base_url:
|
||||
en: "Frontend base URL to redirect to after successful OIDC callback. Required when frontend and API are deployed separately (non-embed build, --no-web mode, or different web_server_port)"
|
||||
zh-CN: "OIDC 回调成功后跳转的前端入口地址。当前端与 API 分离部署时必须提供(非 embed 构建、--no-web 模式、或 web_server_port 与 api_server_port 不同)"
|
||||
|
||||
@@ -2,8 +2,8 @@ pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
@@ -13,13 +13,17 @@ use easytier::{
|
||||
},
|
||||
rpc_service::remote_client::{self, RemoteClientManager},
|
||||
tunnel::TunnelListener,
|
||||
web_client::security,
|
||||
};
|
||||
use maxminddb::geoip2;
|
||||
use session::{Location, Session};
|
||||
use storage::{Storage, StorageToken};
|
||||
|
||||
use crate::FeatureFlags;
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
|
||||
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
||||
|
||||
#[derive(rust_embed::Embed)]
|
||||
#[folder = "resources/"]
|
||||
@@ -55,11 +59,19 @@ pub struct ClientManager {
|
||||
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
|
||||
storage: Storage,
|
||||
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
|
||||
geoip_db: Arc<Option<maxminddb::Reader<Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new(db: Db, geoip_db: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
db: Db,
|
||||
geoip_db: Option<String>,
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
) -> Self {
|
||||
let client_sessions = Arc::new(DashMap::new());
|
||||
let sessions: Arc<DashMap<url::Url, Arc<Session>>> = client_sessions.clone();
|
||||
let mut tasks = JoinSet::new();
|
||||
@@ -76,6 +88,9 @@ impl ClientManager {
|
||||
|
||||
client_sessions,
|
||||
storage: Storage::new(db),
|
||||
feature_flags,
|
||||
webhook_config,
|
||||
|
||||
geoip_db: Arc::new(load_geoip_db(geoip_db)),
|
||||
}
|
||||
}
|
||||
@@ -90,17 +105,33 @@ impl ClientManager {
|
||||
let storage = self.storage.weak_ref();
|
||||
let listeners_cnt = self.listeners_cnt.clone();
|
||||
let geoip_db = self.geoip_db.clone();
|
||||
let feature_flags = self.feature_flags.clone();
|
||||
let webhook_config = self.webhook_config.clone();
|
||||
self.tasks.spawn(async move {
|
||||
while let Ok(tunnel) = listener.accept().await {
|
||||
let (tunnel, secure) = match security::accept_or_upgrade_server_tunnel(tunnel).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to accept secure tunnel, dropping connection");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let info = tunnel.info().unwrap();
|
||||
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||
let location = Self::lookup_location(&client_url, geoip_db.clone());
|
||||
tracing::info!(
|
||||
"New session from {:?}, location: {:?}",
|
||||
"New session from {:?}, secure: {}, location: {:?}",
|
||||
client_url,
|
||||
secure,
|
||||
location
|
||||
);
|
||||
let mut session = Session::new(storage.clone(), client_url.clone(), location);
|
||||
let mut session = Session::new(
|
||||
storage.clone(),
|
||||
client_url.clone(),
|
||||
location,
|
||||
feature_flags.clone(),
|
||||
webhook_config.clone(),
|
||||
);
|
||||
session.serve(tunnel).await;
|
||||
sessions.insert(client_url, Arc::new(session));
|
||||
}
|
||||
@@ -144,6 +175,24 @@ impl ClientManager {
|
||||
.map(|item| item.value().clone())
|
||||
}
|
||||
|
||||
pub async fn disconnect_session_by_machine_id(
|
||||
&self,
|
||||
user_id: UserIdInDb,
|
||||
machine_id: &uuid::Uuid,
|
||||
) -> bool {
|
||||
let Some(client_url) = self
|
||||
.storage
|
||||
.get_client_url_by_machine_id(user_id, machine_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some((_, session)) = self.client_sessions.remove(&client_url) else {
|
||||
return false;
|
||||
};
|
||||
session.stop().await;
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn list_machine_by_user_id(&self, user_id: UserIdInDb) -> Vec<url::Url> {
|
||||
self.storage.list_user_clients(user_id)
|
||||
}
|
||||
@@ -291,12 +340,19 @@ mod tests {
|
||||
};
|
||||
use sqlx::Executor;
|
||||
|
||||
use crate::{client_manager::ClientManager, db::Db};
|
||||
use crate::{FeatureFlags, client_manager::ClientManager, db::Db};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client() {
|
||||
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
|
||||
let mut mgr = ClientManager::new(Db::memory_db().await, None);
|
||||
let mut mgr = ClientManager::new(
|
||||
Db::memory_db().await,
|
||||
None,
|
||||
Arc::new(FeatureFlags::default()),
|
||||
Arc::new(crate::webhook::WebhookConfig::new(
|
||||
None, None, None, None, None,
|
||||
)),
|
||||
);
|
||||
mgr.add_listener(Box::new(listener)).await.unwrap();
|
||||
|
||||
mgr.db()
|
||||
@@ -310,26 +366,43 @@ mod tests {
|
||||
connector,
|
||||
"test",
|
||||
"test",
|
||||
false,
|
||||
Arc::new(NetworkInstanceManager::new()),
|
||||
None,
|
||||
);
|
||||
|
||||
wait_for_condition(
|
||||
|| async { mgr.client_sessions.len() == 1 },
|
||||
Duration::from_secs(6),
|
||||
|| async { !mgr.client_sessions.is_empty() },
|
||||
Duration::from_secs(12),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut a = mgr
|
||||
.client_sessions
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.data()
|
||||
.read()
|
||||
.await
|
||||
.heartbeat_waiter();
|
||||
let req = a.recv().await.unwrap();
|
||||
let req = tokio::time::timeout(Duration::from_secs(12), async {
|
||||
loop {
|
||||
let sessions = mgr
|
||||
.client_sessions
|
||||
.iter()
|
||||
.map(|item| item.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
if sessions.is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
continue;
|
||||
}
|
||||
let mut found_req = None;
|
||||
for session in sessions {
|
||||
if let Some(req) = session.data().read().await.req() {
|
||||
found_req = Some(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(req) = found_req {
|
||||
break req;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{:?}", req);
|
||||
println!("{:?}", mgr);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,6 @@ struct ClientInfo {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StorageInner {
|
||||
// some map for indexing
|
||||
user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
|
||||
pub db: Db,
|
||||
}
|
||||
@@ -46,18 +45,14 @@ impl Storage {
|
||||
}))
|
||||
}
|
||||
|
||||
fn remove_mid_to_client_info_map(
|
||||
map: &DashMap<uuid::Uuid, ClientInfo>,
|
||||
machine_id: &uuid::Uuid,
|
||||
client_url: &url::Url,
|
||||
) {
|
||||
map.remove_if(machine_id, |_, v| v.storage_token.client_url == *client_url);
|
||||
fn remove_client_info_map(map: &DashMap<uuid::Uuid, ClientInfo>, stoken: &StorageToken) {
|
||||
map.remove_if(&stoken.machine_id, |_, v| {
|
||||
v.storage_token.client_url == stoken.client_url
|
||||
&& v.storage_token.user_id == stoken.user_id
|
||||
});
|
||||
}
|
||||
|
||||
fn update_mid_to_client_info_map(
|
||||
map: &DashMap<uuid::Uuid, ClientInfo>,
|
||||
client_info: &ClientInfo,
|
||||
) {
|
||||
fn update_client_info_map(map: &DashMap<uuid::Uuid, ClientInfo>, client_info: &ClientInfo) {
|
||||
map.entry(client_info.storage_token.machine_id)
|
||||
.and_modify(|e| {
|
||||
if e.report_time < client_info.report_time {
|
||||
@@ -78,15 +73,14 @@ impl Storage {
|
||||
storage_token: stoken.clone(),
|
||||
report_time,
|
||||
};
|
||||
|
||||
Self::update_mid_to_client_info_map(&inner, &client_info);
|
||||
Self::update_client_info_map(&inner, &client_info);
|
||||
}
|
||||
|
||||
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||
self.0
|
||||
.user_clients_map
|
||||
.remove_if(&stoken.user_id, |_, set| {
|
||||
Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url);
|
||||
Self::remove_client_info_map(set, stoken);
|
||||
set.is_empty()
|
||||
});
|
||||
}
|
||||
@@ -123,4 +117,61 @@ impl Storage {
|
||||
pub fn db(&self) -> &Db {
|
||||
&self.0.db
|
||||
}
|
||||
|
||||
pub async fn auto_create_user(&self, username: &str) -> anyhow::Result<UserIdInDb> {
|
||||
let new_user = self.db().auto_create_user(username).await?;
|
||||
tracing::info!("Auto-created user '{}' with id {}", username, new_user.id);
|
||||
Ok(new_user.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_storage_token(
|
||||
user_id: UserIdInDb,
|
||||
machine_id: uuid::Uuid,
|
||||
client_url: &str,
|
||||
) -> StorageToken {
|
||||
StorageToken {
|
||||
token: format!("token-{machine_id}"),
|
||||
client_url: client_url.parse().unwrap(),
|
||||
machine_id,
|
||||
user_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn machine_id_is_scoped_within_each_user() {
|
||||
let storage = Storage::new(Db::memory_db().await);
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
|
||||
let user1_token = make_storage_token(1, machine_id, "tcp://127.0.0.1:1001");
|
||||
let user2_token = make_storage_token(2, machine_id, "tcp://127.0.0.1:1002");
|
||||
|
||||
storage.update_client(user1_token.clone(), 10);
|
||||
storage.update_client(user2_token.clone(), 20);
|
||||
|
||||
assert_eq!(
|
||||
storage.get_client_url_by_machine_id(1, &machine_id),
|
||||
Some(user1_token.client_url.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
storage.get_client_url_by_machine_id(2, &machine_id),
|
||||
Some(user2_token.client_url.clone())
|
||||
);
|
||||
|
||||
storage.remove_client(&user1_token);
|
||||
|
||||
assert_eq!(storage.get_client_url_by_machine_id(1, &machine_id), None);
|
||||
assert_eq!(
|
||||
storage.get_client_url_by_machine_id(2, &machine_id),
|
||||
Some(user2_token.client_url.clone())
|
||||
);
|
||||
|
||||
storage.remove_client(&user2_token);
|
||||
|
||||
assert_eq!(storage.get_client_url_by_machine_id(2, &machine_id), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
use easytier::{launcher::NetworkConfig, rpc_service::remote_client::PersistentConfig};
|
||||
use easytier::{
|
||||
common::config::ConfigSource, launcher::NetworkConfig,
|
||||
rpc_service::remote_client::PersistentConfig,
|
||||
};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -12,10 +15,12 @@ pub struct Model {
|
||||
pub user_id: i32,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub device_id: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub network_instance_id: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub network_config: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub source: String,
|
||||
pub disabled: bool,
|
||||
pub create_time: DateTimeWithTimeZone,
|
||||
pub update_time: DateTimeWithTimeZone,
|
||||
@@ -48,4 +53,7 @@ impl PersistentConfig<DbErr> for Model {
|
||||
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
||||
serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string()))
|
||||
}
|
||||
fn get_network_config_source(&self) -> ConfigSource {
|
||||
self.source.parse().unwrap_or(ConfigSource::User)
|
||||
}
|
||||
}
|
||||
|
||||
+199
-21
@@ -3,16 +3,17 @@
|
||||
pub mod entity;
|
||||
|
||||
use easytier::{
|
||||
common::config::ConfigSource,
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::remote_client::{ListNetworkProps, Storage},
|
||||
};
|
||||
use entity::user_running_network_configs;
|
||||
use sea_orm::{
|
||||
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||
QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _,
|
||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
||||
SqlxSqliteConnector, TransactionTrait as _, prelude::Expr, sea_query::OnConflict,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait as _;
|
||||
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _, types::chrono};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::migrator;
|
||||
@@ -82,6 +83,57 @@ impl Db {
|
||||
Ok(user.map(|u| u.id))
|
||||
}
|
||||
|
||||
/// `password_hash` must be pre-hashed by the caller.
|
||||
/// Creates user + joins "users" group in one transaction. Returns the created user model.
|
||||
pub async fn create_user_and_join_users_group(
|
||||
&self,
|
||||
username: &str,
|
||||
password_hash: String,
|
||||
) -> Result<entity::users::Model, DbErr> {
|
||||
use entity::{groups, users, users_groups};
|
||||
|
||||
let txn = self.orm_db().begin().await?;
|
||||
|
||||
let user_active = users::ActiveModel {
|
||||
username: Set(username.to_string()),
|
||||
password: Set(password_hash),
|
||||
..Default::default()
|
||||
};
|
||||
let insert_result = users::Entity::insert(user_active).exec(&txn).await?;
|
||||
|
||||
let new_user = users::Entity::find_by_id(insert_result.last_insert_id)
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| DbErr::Custom("Failed to find newly created user".to_string()))?;
|
||||
|
||||
let users_group = groups::Entity::find()
|
||||
.filter(groups::Column::Name.eq("users"))
|
||||
.one(&txn)
|
||||
.await?
|
||||
.ok_or_else(|| DbErr::Custom("Users group not found".to_string()))?;
|
||||
|
||||
let ug_active = users_groups::ActiveModel {
|
||||
user_id: Set(new_user.id),
|
||||
group_id: Set(users_group.id),
|
||||
..Default::default()
|
||||
};
|
||||
users_groups::Entity::insert(ug_active).exec(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
|
||||
Ok(new_user)
|
||||
}
|
||||
|
||||
pub async fn auto_create_user(&self, username: &str) -> Result<entity::users::Model, DbErr> {
|
||||
let random_password = uuid::Uuid::new_v4().to_string();
|
||||
let hashed_password =
|
||||
tokio::task::spawn_blocking(move || password_auth::generate_hash(&random_password))
|
||||
.await
|
||||
.map_err(|e| DbErr::Custom(format!("Failed to hash password: {}", e)))?;
|
||||
self.create_user_and_join_users_group(username, hashed_password)
|
||||
.await
|
||||
}
|
||||
|
||||
// TODO: currently we don't have a token system, so we just use the user name as token
|
||||
pub async fn get_user_id_by_token<T: ToString>(
|
||||
&self,
|
||||
@@ -98,18 +150,24 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
network_inst_id: Uuid,
|
||||
network_config: NetworkConfig,
|
||||
source: ConfigSource,
|
||||
) -> Result<(), DbErr> {
|
||||
let txn = self.orm_db().begin().await?;
|
||||
|
||||
use entity::user_running_network_configs as urnc;
|
||||
|
||||
let on_conflict = OnConflict::column(urnc::Column::NetworkInstanceId)
|
||||
.update_columns([
|
||||
urnc::Column::NetworkConfig,
|
||||
urnc::Column::Disabled,
|
||||
urnc::Column::UpdateTime,
|
||||
])
|
||||
.to_owned();
|
||||
let on_conflict = OnConflict::columns([
|
||||
urnc::Column::UserId,
|
||||
urnc::Column::DeviceId,
|
||||
urnc::Column::NetworkInstanceId,
|
||||
])
|
||||
.update_columns([
|
||||
urnc::Column::NetworkConfig,
|
||||
urnc::Column::Source,
|
||||
urnc::Column::Disabled,
|
||||
urnc::Column::UpdateTime,
|
||||
])
|
||||
.to_owned();
|
||||
let insert_m = urnc::ActiveModel {
|
||||
user_id: sea_orm::Set(user_id),
|
||||
device_id: sea_orm::Set(device_id.to_string()),
|
||||
@@ -117,6 +175,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
network_config: sea_orm::Set(
|
||||
serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?,
|
||||
),
|
||||
source: sea_orm::Set(source.as_str().to_string()),
|
||||
disabled: sea_orm::Set(false),
|
||||
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||
@@ -133,13 +192,14 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
async fn delete_network_configs(
|
||||
&self,
|
||||
(user_id, _): (UserIdInDb, Uuid),
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
network_inst_ids: &[Uuid],
|
||||
) -> Result<(), DbErr> {
|
||||
use entity::user_running_network_configs as urnc;
|
||||
|
||||
urnc::Entity::delete_many()
|
||||
.filter(urnc::Column::UserId.eq(user_id))
|
||||
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||
.filter(
|
||||
urnc::Column::NetworkInstanceId
|
||||
.is_in(network_inst_ids.iter().map(|id| id.to_string())),
|
||||
@@ -152,7 +212,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
async fn update_network_config_state(
|
||||
&self,
|
||||
(user_id, _): (UserIdInDb, Uuid),
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
network_inst_id: Uuid,
|
||||
disabled: bool,
|
||||
) -> Result<(), DbErr> {
|
||||
@@ -160,6 +220,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
urnc::Entity::update_many()
|
||||
.filter(urnc::Column::UserId.eq(user_id))
|
||||
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
|
||||
.col_expr(urnc::Column::Disabled, Expr::value(disabled))
|
||||
.col_expr(
|
||||
@@ -220,10 +281,14 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||
use easytier::{
|
||||
common::config::ConfigSource,
|
||||
proto::api::manage::NetworkConfig,
|
||||
rpc_service::remote_client::{PersistentConfig, Storage},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter as _, Set};
|
||||
|
||||
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
|
||||
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_management() {
|
||||
@@ -237,9 +302,14 @@ mod tests {
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
let device_id = uuid::Uuid::new_v4();
|
||||
|
||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device_id),
|
||||
inst_id,
|
||||
network_config,
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
@@ -249,6 +319,7 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("{:?}", result);
|
||||
assert_eq!(result.network_config, network_config_json);
|
||||
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||
|
||||
// overwrite the config
|
||||
let network_config = NetworkConfig {
|
||||
@@ -256,9 +327,14 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device_id),
|
||||
inst_id,
|
||||
network_config,
|
||||
ConfigSource::Webhook,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result2 = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
@@ -268,6 +344,11 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("device: {}, {:?}", device_id, result2);
|
||||
assert_eq!(result2.network_config, network_config_json);
|
||||
assert_eq!(result2.get_network_config_source(), ConfigSource::Webhook);
|
||||
assert_eq!(
|
||||
result2.get_runtime_network_config_source(),
|
||||
ConfigSource::Webhook
|
||||
);
|
||||
|
||||
assert_eq!(result.create_time, result2.create_time);
|
||||
assert_ne!(result.update_time, result2.update_time);
|
||||
@@ -290,4 +371,101 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(result3.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_legacy_network_config_defaults_to_user_runtime_source() {
|
||||
let db = Db::memory_db().await;
|
||||
let user_id = 1;
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
let device_id = uuid::Uuid::new_v4();
|
||||
|
||||
user_running_network_configs::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
device_id: Set(device_id.to_string()),
|
||||
network_instance_id: Set(inst_id.to_string()),
|
||||
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||
network_name: Some("legacy".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap()),
|
||||
source: Set("legacy".to_string()),
|
||||
disabled: Set(false),
|
||||
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db.orm_db())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
.one(db.orm_db())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||
assert_eq!(
|
||||
result.get_runtime_network_config_source(),
|
||||
ConfigSource::User
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
||||
let db = Db::memory_db().await;
|
||||
let user_id = db.auto_create_user("user-1").await.unwrap().id;
|
||||
let device1 = uuid::Uuid::new_v4();
|
||||
let device2 = uuid::Uuid::new_v4();
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device1),
|
||||
inst_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("cfg-1".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device2),
|
||||
inst_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("cfg-2".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = db
|
||||
.get_network_config((user_id, device1), &inst_id.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let second = db
|
||||
.get_network_config((user_id, device2), &inst_id.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(first.user_id, user_id);
|
||||
assert_eq!(first.device_id, device1.to_string());
|
||||
assert_eq!(second.user_id, user_id);
|
||||
assert_eq!(second.device_id, device2.to_string());
|
||||
|
||||
let device1_configs = db
|
||||
.list_network_configs((user_id, device1), ListNetworkProps::All)
|
||||
.await
|
||||
.unwrap();
|
||||
let device2_configs = db
|
||||
.list_network_configs((user_id, device2), ListNetworkProps::All)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(device1_configs.len(), 1);
|
||||
assert_eq!(device2_configs.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+153
-27
@@ -3,28 +3,32 @@
|
||||
#[macro_use]
|
||||
extern crate rust_i18n;
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use easytier::tunnel::websocket::WsTunnelListener;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader},
|
||||
constants::EASYTIER_VERSION,
|
||||
error::Error,
|
||||
log,
|
||||
network::{local_ipv4, local_ipv6},
|
||||
},
|
||||
tunnel::{
|
||||
tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener,
|
||||
},
|
||||
utils::{init_logger, setup_panic_handler},
|
||||
tunnel::{TunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener},
|
||||
utils::panic::setup_panic_handler,
|
||||
};
|
||||
|
||||
use easytier::tunnel::IpScheme;
|
||||
use easytier::utils::BoxExt;
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
mod client_manager;
|
||||
mod db;
|
||||
mod migrator;
|
||||
mod restful;
|
||||
mod webhook;
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
mod web;
|
||||
@@ -82,6 +86,13 @@ struct Cli {
|
||||
)]
|
||||
api_server_port: u16,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "0.0.0.0",
|
||||
help = t!("cli.api_server_addr").to_string(),
|
||||
)]
|
||||
api_server_addr: IpAddr,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("cli.geoip_db").to_string(),
|
||||
@@ -96,6 +107,14 @@ struct Cli {
|
||||
)]
|
||||
web_server_port: Option<u16>,
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
#[arg(
|
||||
long,
|
||||
default_value = "0.0.0.0",
|
||||
help = t!("cli.web_server_addr").to_string(),
|
||||
)]
|
||||
web_server_addr: IpAddr,
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
#[arg(
|
||||
long,
|
||||
@@ -110,6 +129,51 @@ struct Cli {
|
||||
help = t!("cli.api_host").to_string()
|
||||
)]
|
||||
api_host: Option<url::Url>,
|
||||
|
||||
#[command(flatten)]
|
||||
feature_flags: FeatureFlags,
|
||||
|
||||
#[command(flatten)]
|
||||
oidc: restful::oidc::OidcOptions,
|
||||
|
||||
#[command(flatten)]
|
||||
webhook: WebhookOptions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, clap::Args)]
|
||||
pub struct WebhookOptions {
|
||||
/// Base URL of the webhook endpoint for token validation and event delivery.
|
||||
/// When set, incoming tokens are validated via this webhook before local fallback.
|
||||
#[arg(long)]
|
||||
pub webhook_url: Option<String>,
|
||||
|
||||
/// Shared secret used to authenticate outbound webhook calls.
|
||||
#[arg(long)]
|
||||
pub webhook_secret: Option<String>,
|
||||
|
||||
/// Token for X-Internal-Auth header. When set, API requests with this header
|
||||
/// bypass session authentication.
|
||||
#[arg(long)]
|
||||
pub internal_auth_token: Option<String>,
|
||||
|
||||
/// Stable identifier for this easytier-web instance when routing webhook callbacks.
|
||||
#[arg(long)]
|
||||
pub web_instance_id: Option<String>,
|
||||
|
||||
/// Reachable base URL for this easytier-web instance's internal REST API.
|
||||
#[arg(long)]
|
||||
pub web_instance_api_base_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, clap::Args)]
|
||||
pub struct FeatureFlags {
|
||||
/// Whether user registration via the web UI is disabled.
|
||||
#[arg(long, default_value = "false", help = t!("cli.disable_registration").to_string())]
|
||||
pub disable_registration: bool,
|
||||
|
||||
/// Whether to auto-create users when they connect via heartbeat with an unknown token.
|
||||
#[arg(long, default_value = "false", help = t!("cli.allow_auto_create_user").to_string())]
|
||||
pub allow_auto_create_user: bool,
|
||||
}
|
||||
|
||||
impl LoggingConfigLoader for &Cli {
|
||||
@@ -130,14 +194,12 @@ impl LoggingConfigLoader for &Cli {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
|
||||
Ok(match l.scheme() {
|
||||
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
|
||||
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
|
||||
"ws" => Box::new(WSTunnelListener::new(l.clone())),
|
||||
_ => {
|
||||
return Err(Error::InvalidUrl(l.to_string()));
|
||||
}
|
||||
pub fn get_listener_by_url(scheme: IpScheme, l: &url::Url) -> Option<Box<dyn TunnelListener>> {
|
||||
Some(match scheme {
|
||||
IpScheme::Tcp => TcpTunnelListener::new(l.clone()).boxed(),
|
||||
IpScheme::Udp => UdpTunnelListener::new(l.clone()).boxed(),
|
||||
IpScheme::Ws => WsTunnelListener::new(l.clone()).boxed(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -151,15 +213,23 @@ async fn get_dual_stack_listener(
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let is_protocol_support_dual_stack =
|
||||
protocol.trim().to_lowercase() == "tcp" || protocol.trim().to_lowercase() == "udp";
|
||||
let v6_listener = if is_protocol_support_dual_stack && local_ipv6().await.is_ok() {
|
||||
get_listener_by_url(&format!("{}://[::0]:{}", protocol, port).parse().unwrap()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let scheme = protocol
|
||||
.parse()
|
||||
.map_err(|_| Error::InvalidUrl(protocol.to_string()))?;
|
||||
let v6_listener =
|
||||
if local_ipv6().await.is_ok() && matches!(scheme, IpScheme::Tcp | IpScheme::Udp) {
|
||||
get_listener_by_url(
|
||||
scheme,
|
||||
&format!("{protocol}://[::]:{port}").parse().unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let v4_listener = if local_ipv4().await.is_ok() {
|
||||
get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok()
|
||||
get_listener_by_url(
|
||||
scheme,
|
||||
&format!("{protocol}://0.0.0.0:{port}").parse().unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -173,11 +243,50 @@ async fn main() {
|
||||
setup_panic_handler();
|
||||
|
||||
let cli = Cli::parse();
|
||||
init_logger(&cli, false).unwrap();
|
||||
log::init(&cli, false).unwrap();
|
||||
|
||||
// Validate OIDC configuration: check split-deploy specific requirements
|
||||
// Basic OIDC parameter validation is handled in OidcConfig::from_params
|
||||
if cli.oidc.any_param_provided() {
|
||||
let is_split_deploy = {
|
||||
#[cfg(feature = "embed")]
|
||||
{
|
||||
let embed_split_by_port = cli.web_server_port.is_some()
|
||||
&& cli.web_server_port != Some(cli.api_server_port);
|
||||
cli.no_web || embed_split_by_port
|
||||
}
|
||||
#[cfg(not(feature = "embed"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if is_split_deploy && cli.oidc.oidc_frontend_base_url.is_none() {
|
||||
eprintln!("Error: --oidc-frontend-base-url is required in split-deploy mode");
|
||||
eprintln!(
|
||||
"When frontend and API are deployed separately, you must specify the frontend URL"
|
||||
);
|
||||
eprintln!("Example: --oidc-frontend-base-url http://your-frontend-domain.com");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// let db = db::Db::new(":memory:").await.unwrap();
|
||||
let db = db::Db::new(cli.db).await.unwrap();
|
||||
let mut mgr = client_manager::ClientManager::new(db.clone(), cli.geoip_db);
|
||||
let feature_flags = Arc::new(cli.feature_flags);
|
||||
let webhook_config = Arc::new(webhook::WebhookConfig::new(
|
||||
cli.webhook.webhook_url,
|
||||
cli.webhook.webhook_secret,
|
||||
cli.webhook.internal_auth_token,
|
||||
cli.webhook.web_instance_id,
|
||||
cli.webhook.web_instance_api_base_url,
|
||||
));
|
||||
let mut mgr = client_manager::ClientManager::new(
|
||||
db.clone(),
|
||||
cli.geoip_db,
|
||||
feature_flags.clone(),
|
||||
webhook_config.clone(),
|
||||
);
|
||||
let (v6_listener, v4_listener) =
|
||||
get_dual_stack_listener(&cli.config_server_protocol, cli.config_server_port)
|
||||
.await
|
||||
@@ -199,7 +308,10 @@ async fn main() {
|
||||
(None, None)
|
||||
} else {
|
||||
let web_router = web::build_router(cli.api_host.clone());
|
||||
if cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port) {
|
||||
if cli.web_server_port.is_none()
|
||||
|| (cli.web_server_port == Some(cli.api_server_port)
|
||||
&& cli.web_server_addr == cli.api_server_addr)
|
||||
{
|
||||
(Some(web_router), None)
|
||||
} else {
|
||||
(None, Some(web_router))
|
||||
@@ -208,11 +320,27 @@ async fn main() {
|
||||
#[cfg(not(feature = "embed"))]
|
||||
let web_router_restful = None;
|
||||
|
||||
let oidc_config = if cli.oidc.oidc_issuer_url.is_some() {
|
||||
match restful::oidc::OidcConfig::from_params(cli.oidc).await {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize OIDC: {:?}", e);
|
||||
eprintln!("Please check your OIDC configuration (issuer URL, client ID, etc.)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
restful::oidc::OidcConfig::disabled()
|
||||
};
|
||||
|
||||
let _restful_server_tasks = restful::RestfulServer::new(
|
||||
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
|
||||
std::net::SocketAddr::new(cli.api_server_addr, cli.api_server_port),
|
||||
mgr.clone(),
|
||||
db,
|
||||
web_router_restful,
|
||||
feature_flags,
|
||||
oidc_config,
|
||||
webhook_config,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -224,9 +352,7 @@ async fn main() {
|
||||
let _web_server_task = if let Some(web_router) = web_router_static {
|
||||
Some(
|
||||
web::WebServer::new(
|
||||
format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0))
|
||||
.parse()
|
||||
.unwrap(),
|
||||
std::net::SocketAddr::new(cli.web_server_addr, cli.web_server_port.unwrap_or(0)),
|
||||
web_router,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260403_000002_scope_network_config_unique"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_new (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL UNIQUE,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_old (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260421_000003_add_network_config_source"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'user',
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_new (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
source,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
'legacy',
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_old (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20241029_000001_init;
|
||||
mod m20260403_000002_scope_network_config_unique;
|
||||
mod m20260421_000003_add_network_config_source;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20241029_000001_init::Migration)]
|
||||
vec![
|
||||
Box::new(m20241029_000001_init::Migration),
|
||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||
Box::new(m20260421_000003_add_network_config_source::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Router,
|
||||
http::StatusCode,
|
||||
routing::{get, post, put},
|
||||
Router,
|
||||
};
|
||||
use axum_login::login_required;
|
||||
use axum_messages::Message;
|
||||
@@ -9,9 +9,13 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::restful::users::Backend;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::FeatureFlags;
|
||||
|
||||
use super::{
|
||||
users::{AuthSession, Credentials},
|
||||
AppStateInner,
|
||||
users::{AuthSession, Credentials},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -40,7 +44,7 @@ mod put {
|
||||
use axum_login::AuthUser;
|
||||
use easytier::proto::common::Void;
|
||||
|
||||
use crate::restful::{other_error, users::ChangePassword, HttpHandleError};
|
||||
use crate::restful::{HttpHandleError, other_error, users::ChangePassword};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -67,14 +71,14 @@ mod put {
|
||||
}
|
||||
|
||||
mod post {
|
||||
use axum::Json;
|
||||
use axum::{Json, extract::Extension};
|
||||
use easytier::proto::common::Void;
|
||||
|
||||
use crate::restful::{
|
||||
captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil},
|
||||
HttpHandleError,
|
||||
captcha::extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionStaticExt},
|
||||
other_error,
|
||||
users::RegisterNewUser,
|
||||
HttpHandleError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -95,7 +99,7 @@ mod post {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json::from(other_error(format!("{:?}", e))),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,10 +114,20 @@ mod post {
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
Extension(feature_flags): Extension<Arc<FeatureFlags>>,
|
||||
auth_session: AuthSession,
|
||||
captcha_session: tower_sessions::Session,
|
||||
Json(req): Json<RegisterNewUser>,
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
// Check if registration is disabled
|
||||
if feature_flags.disable_registration {
|
||||
tracing::warn!("Registration attempt blocked: registration is disabled");
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
other_error("Registration is disabled").into(),
|
||||
));
|
||||
}
|
||||
|
||||
// 调用CaptchaUtil的静态方法验证验证码是否正确
|
||||
if !CaptchaUtil::ver(&req.captcha, &captcha_session).await {
|
||||
return Err((
|
||||
@@ -136,14 +150,15 @@ mod post {
|
||||
|
||||
mod get {
|
||||
use crate::restful::{
|
||||
HttpHandleError,
|
||||
captcha::{
|
||||
builder::spec::SpecCaptcha,
|
||||
extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil},
|
||||
NewCaptcha as _,
|
||||
builder::spec::SpecCaptcha,
|
||||
extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionExt as _},
|
||||
},
|
||||
other_error, HttpHandleError,
|
||||
other_error,
|
||||
};
|
||||
use axum::{response::Response, Json};
|
||||
use axum::{Json, response::Response};
|
||||
use easytier::proto::common::Void;
|
||||
use tower_sessions::Session;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::super::base::randoms::Randoms;
|
||||
|
||||
use super::super::utils::color::Color;
|
||||
use super::super::utils::font;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
|
||||
use rusttype::Font;
|
||||
use std::fmt::Debug;
|
||||
|
||||
@@ -9,14 +9,14 @@ use super::super::{CaptchaFont, NewCaptcha};
|
||||
|
||||
use image::{ImageBuffer, Rgba};
|
||||
use imageproc::drawing;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use rand::{Rng, rngs::ThreadRng};
|
||||
use rusttype::{Font, Scale};
|
||||
use std::io::{Cursor, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod color {
|
||||
use image::Rgba;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use rand::{Rng, rngs::ThreadRng};
|
||||
pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> {
|
||||
let red = rng.gen_range(200..=255);
|
||||
let green = rng.gen_range(200..=255);
|
||||
@@ -133,7 +133,7 @@ impl<'a, 'b> CaptchaBuilder<'a, 'b> {
|
||||
|
||||
fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
|
||||
let line_color = color::gen_line_color(rng);
|
||||
let is_h = rng.gen();
|
||||
let is_h = rng.r#gen();
|
||||
let (start, end) = if is_h {
|
||||
let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
|
||||
let ya = rng.gen_range(0.0..(self.height as f32));
|
||||
|
||||
+116
-32
@@ -1,32 +1,39 @@
|
||||
mod auth;
|
||||
pub(crate) mod captcha;
|
||||
mod network;
|
||||
pub(crate) mod oidc;
|
||||
mod rpc;
|
||||
mod users;
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::post;
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use axum::extract::Path;
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use axum::middleware::{self as axum_mw, Next};
|
||||
use axum::response::Response;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{Extension, Json, Router, extract::State, routing::get};
|
||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||
use axum_login::{AuthManagerLayerBuilder, AuthUser, AuthzBackend, login_required};
|
||||
use axum_messages::MessagesManagerLayer;
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::scoped_task::ScopedTask;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::rpc_types;
|
||||
use network::NetworkApi;
|
||||
use sea_orm::DbErr;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_sessions::cookie::time::Duration;
|
||||
use tower_sessions::cookie::Key;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tower_sessions::Expiry;
|
||||
use tower_sessions::cookie::time::Duration;
|
||||
use tower_sessions::cookie::{Key, SameSite};
|
||||
use tower_sessions_sqlx_store::SqliteStore;
|
||||
use users::{AuthSession, Backend};
|
||||
|
||||
use crate::client_manager::storage::StorageToken;
|
||||
use crate::FeatureFlags;
|
||||
use crate::client_manager::ClientManager;
|
||||
use crate::db::Db;
|
||||
use crate::client_manager::storage::StorageToken;
|
||||
use crate::db::{Db, UserIdInDb};
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
|
||||
/// Embed assets for web dashboard, build frontend first
|
||||
#[cfg(feature = "embed")]
|
||||
@@ -37,11 +44,10 @@ struct Assets;
|
||||
pub struct RestfulServer {
|
||||
bind_addr: SocketAddr,
|
||||
client_mgr: Arc<ClientManager>,
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
db: Db,
|
||||
|
||||
// serve_task: Option<ScopedTask<()>>,
|
||||
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
||||
// network_api: NetworkApi<WebClientManager>,
|
||||
oidc_config: oidc::OidcConfig,
|
||||
web_router: Option<Router>,
|
||||
}
|
||||
|
||||
@@ -104,18 +110,19 @@ impl RestfulServer {
|
||||
client_mgr: Arc<ClientManager>,
|
||||
db: Db,
|
||||
web_router: Option<Router>,
|
||||
feature_flags: Arc<FeatureFlags>,
|
||||
oidc_config: oidc::OidcConfig,
|
||||
webhook_config: SharedWebhookConfig,
|
||||
) -> anyhow::Result<Self> {
|
||||
assert!(client_mgr.is_running());
|
||||
|
||||
// let network_api = NetworkApi::new();
|
||||
|
||||
Ok(RestfulServer {
|
||||
bind_addr,
|
||||
client_mgr,
|
||||
feature_flags,
|
||||
webhook_config,
|
||||
db,
|
||||
// serve_task: None,
|
||||
// delete_task: None,
|
||||
// network_api,
|
||||
oidc_config,
|
||||
web_router,
|
||||
})
|
||||
}
|
||||
@@ -192,8 +199,8 @@ impl RestfulServer {
|
||||
mut self,
|
||||
) -> Result<
|
||||
(
|
||||
ScopedTask<()>,
|
||||
ScopedTask<tower_sessions::session_store::Result<()>>,
|
||||
AbortOnDropHandle<()>,
|
||||
AbortOnDropHandle<tower_sessions::session_store::Result<()>>,
|
||||
),
|
||||
anyhow::Error,
|
||||
> {
|
||||
@@ -206,19 +213,18 @@ impl RestfulServer {
|
||||
let session_store = SqliteStore::new(self.db.inner());
|
||||
session_store.migrate().await?;
|
||||
|
||||
let delete_task: ScopedTask<tower_sessions::session_store::Result<()>> =
|
||||
tokio::task::spawn(
|
||||
session_store
|
||||
.clone()
|
||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||
)
|
||||
.into();
|
||||
let delete_task = AbortOnDropHandle::new(tokio::task::spawn(
|
||||
session_store
|
||||
.clone()
|
||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||
));
|
||||
|
||||
// Generate a cryptographic key to sign the session cookie.
|
||||
let key = Key::generate();
|
||||
|
||||
let session_layer = SessionManagerLayer::new(session_store)
|
||||
.with_secure(false)
|
||||
.with_same_site(SameSite::Lax)
|
||||
.with_expiry(Expiry::OnInactivity(Duration::days(1)))
|
||||
.with_signed(key);
|
||||
|
||||
@@ -235,23 +241,54 @@ impl RestfulServer {
|
||||
.zstd(true)
|
||||
.quality(tower_http::compression::CompressionLevel::Default);
|
||||
|
||||
let app = Router::new()
|
||||
// Token-authenticated management routes that bypass session auth.
|
||||
let internal_app = if self.webhook_config.has_internal_auth() {
|
||||
let internal_token = self.webhook_config.internal_auth_token.clone().unwrap();
|
||||
let internal_routes = Router::new()
|
||||
.route(
|
||||
"/api/internal/sessions",
|
||||
get(Self::handle_list_all_sessions_internal),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/users/:user-id/sessions/:machine-id",
|
||||
delete(Self::handle_disconnect_session_internal),
|
||||
)
|
||||
.merge(NetworkApi::build_route_internal())
|
||||
.merge(rpc::router_internal())
|
||||
.with_state(self.client_mgr.clone())
|
||||
.layer(axum_mw::from_fn(move |req, next| {
|
||||
let token = internal_token.clone();
|
||||
internal_auth_middleware(token, req, next)
|
||||
}));
|
||||
Some(internal_routes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut app = Router::new()
|
||||
.route("/api/v1/summary", get(Self::handle_get_summary))
|
||||
.route("/api/v1/sessions", get(Self::handle_list_all_sessions))
|
||||
.merge(NetworkApi::build_route())
|
||||
.merge(rpc::router())
|
||||
.route_layer(login_required!(Backend))
|
||||
.merge(auth::router())
|
||||
.merge(auth::router().layer(Extension(self.feature_flags.clone())))
|
||||
.merge(oidc::router())
|
||||
.with_state(self.client_mgr.clone())
|
||||
.route(
|
||||
"/api/v1/generate-config",
|
||||
post(Self::handle_generate_config),
|
||||
)
|
||||
.route("/api/v1/parse-config", post(Self::handle_parse_config))
|
||||
.layer(Extension(self.oidc_config.clone()))
|
||||
.layer(MessagesManagerLayer)
|
||||
.layer(auth_layer)
|
||||
.layer(tower_http::cors::CorsLayer::very_permissive())
|
||||
.layer(compression_layer);
|
||||
|
||||
if let Some(internal_routes) = internal_app {
|
||||
app = app.merge(internal_routes);
|
||||
}
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
let app = if let Some(web_router) = self.web_router.take() {
|
||||
app.merge(web_router)
|
||||
@@ -259,11 +296,58 @@ impl RestfulServer {
|
||||
app
|
||||
};
|
||||
|
||||
let serve_task: ScopedTask<()> = tokio::spawn(async move {
|
||||
let serve_task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
})
|
||||
.into();
|
||||
}));
|
||||
|
||||
Ok((serve_task, delete_task))
|
||||
}
|
||||
|
||||
/// Session listing endpoint for token-authenticated management clients.
|
||||
async fn handle_list_all_sessions_internal(
|
||||
State(client_mgr): AppState,
|
||||
) -> Result<Json<ListSessionJsonResp>, HttpHandleError> {
|
||||
let ret = client_mgr.list_sessions().await;
|
||||
Ok(ListSessionJsonResp(ret).into())
|
||||
}
|
||||
|
||||
async fn handle_disconnect_session_internal(
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
State(client_mgr): AppState,
|
||||
) -> Result<StatusCode, HttpHandleError> {
|
||||
if client_mgr
|
||||
.disconnect_session_by_machine_id(user_id, &machine_id)
|
||||
.await
|
||||
{
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
other_error("session not found").into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware that validates X-Internal-Auth for token-authenticated routes.
|
||||
async fn internal_auth_middleware(
|
||||
expected_token: String,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("X-Internal-Auth")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
match auth_header {
|
||||
Some(token) if token == expected_token => next.run(req).await,
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(axum::body::Body::from(
|
||||
r#"{"error":"unauthorized: invalid or missing X-Internal-Auth header"}"#,
|
||||
))
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use axum::{Json, Router, extract::State, routing::get};
|
||||
use axum_login::AuthUser;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::common::Void;
|
||||
@@ -16,7 +16,7 @@ use crate::db::UserIdInDb;
|
||||
|
||||
use super::users::AuthSession;
|
||||
use super::{
|
||||
convert_db_error, other_error, AppState, AppStateInner, Error, HttpHandleError, RpcError,
|
||||
AppState, AppStateInner, Error, HttpHandleError, RpcError, convert_db_error, other_error,
|
||||
};
|
||||
|
||||
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
|
||||
@@ -295,6 +295,70 @@ impl NetworkApi {
|
||||
.into())
|
||||
}
|
||||
|
||||
// --- Token-authenticated machine-scoped handlers (no AuthSession) ---
|
||||
|
||||
async fn handle_run_network_instance_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
Json(payload): Json<RunNetworkJsonReq>,
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
client_mgr
|
||||
.handle_run_network_instance((user_id, machine_id), payload.config, payload.save)
|
||||
.await
|
||||
.map_err(convert_error)?;
|
||||
Ok(Void::default().into())
|
||||
}
|
||||
|
||||
async fn handle_remove_network_instance_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id, inst_id)): Path<(UserIdInDb, uuid::Uuid, uuid::Uuid)>,
|
||||
) -> Result<(), HttpHandleError> {
|
||||
client_mgr
|
||||
.handle_remove_network_instances((user_id, machine_id), vec![inst_id])
|
||||
.await
|
||||
.map_err(convert_error)
|
||||
}
|
||||
|
||||
async fn handle_list_network_instance_ids_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
) -> Result<Json<ListNetworkInstanceIdsJsonResp>, HttpHandleError> {
|
||||
Ok(client_mgr
|
||||
.handle_list_network_instance_ids((user_id, machine_id))
|
||||
.await
|
||||
.map_err(convert_error)?
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn handle_collect_network_info_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
Json(payload): Json<CollectNetworkInfoJsonReq>,
|
||||
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
|
||||
Ok(client_mgr
|
||||
.handle_collect_network_info((user_id, machine_id), payload.inst_ids)
|
||||
.await
|
||||
.map_err(convert_error)?
|
||||
.into())
|
||||
}
|
||||
|
||||
pub fn build_route_internal() -> Router<AppStateInner> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/api/internal/users/:user-id/machines/:machine-id/networks",
|
||||
post(Self::handle_run_network_instance_internal)
|
||||
.get(Self::handle_list_network_instance_ids_internal),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/users/:user-id/machines/:machine-id/networks/:inst-id",
|
||||
delete(Self::handle_remove_network_instance_internal),
|
||||
)
|
||||
.route(
|
||||
"/api/internal/users/:user-id/machines/:machine-id/networks/info",
|
||||
get(Self::handle_collect_network_info_internal),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_route() -> Router<AppStateInner> {
|
||||
Router::new()
|
||||
.route("/api/v1/machines", get(Self::handle_list_machines))
|
||||
|
||||
@@ -0,0 +1,735 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use openidconnect::core::{
|
||||
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
|
||||
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
||||
CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType,
|
||||
};
|
||||
use openidconnect::{
|
||||
Client, ClientId, ClientSecret, EmptyExtraTokenFields, EndpointMaybeSet, EndpointNotSet,
|
||||
EndpointSet, IdTokenFields, IssuerUrl, RedirectUrl, StandardErrorResponse,
|
||||
StandardTokenResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::AppStateInner;
|
||||
|
||||
const DEFAULT_OIDC_SCOPES: [&str; 2] = ["openid", "profile"];
|
||||
|
||||
fn normalize_oidc_scopes(scopes: &[String]) -> Vec<String> {
|
||||
let mut normalized: Vec<String> = scopes
|
||||
.iter()
|
||||
.map(|scope| scope.trim().to_string())
|
||||
.filter(|scope| !scope.is_empty())
|
||||
.collect();
|
||||
|
||||
if normalized.is_empty() {
|
||||
normalized = DEFAULT_OIDC_SCOPES
|
||||
.iter()
|
||||
.map(|scope| scope.to_string())
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !normalized.iter().any(|scope| scope == "openid") {
|
||||
normalized.insert(0, "openid".to_string());
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct JsonAdditionalClaims {
|
||||
#[serde(flatten)]
|
||||
pub claims: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl openidconnect::AdditionalClaims for JsonAdditionalClaims {}
|
||||
|
||||
pub type AppIdTokenFields = IdTokenFields<
|
||||
JsonAdditionalClaims,
|
||||
EmptyExtraTokenFields,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJwsSigningAlgorithm,
|
||||
>;
|
||||
|
||||
pub type AppTokenResponse = StandardTokenResponse<AppIdTokenFields, CoreTokenType>;
|
||||
|
||||
pub type AppClient<
|
||||
HasAuthUrl = EndpointNotSet,
|
||||
HasDeviceAuthUrl = EndpointNotSet,
|
||||
HasIntrospectionUrl = EndpointNotSet,
|
||||
HasRevocationUrl = EndpointNotSet,
|
||||
HasTokenUrl = EndpointNotSet,
|
||||
HasUserInfoUrl = EndpointNotSet,
|
||||
> = Client<
|
||||
JsonAdditionalClaims,
|
||||
CoreAuthDisplay,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJsonWebKey,
|
||||
CoreAuthPrompt,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
AppTokenResponse,
|
||||
CoreTokenIntrospectionResponse,
|
||||
CoreRevocableToken,
|
||||
CoreRevocationErrorResponse,
|
||||
HasAuthUrl,
|
||||
HasDeviceAuthUrl,
|
||||
HasIntrospectionUrl,
|
||||
HasRevocationUrl,
|
||||
HasTokenUrl,
|
||||
HasUserInfoUrl,
|
||||
>;
|
||||
|
||||
pub type ConfiguredAppClient = AppClient<
|
||||
EndpointSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointMaybeSet,
|
||||
EndpointMaybeSet,
|
||||
>;
|
||||
|
||||
/// Convert a dot-path (e.g. `realm_access.roles.0`) to a JSON Pointer (e.g. `/realm_access/roles/0`).
|
||||
/// Each segment is escaped per RFC 6901: `~` → `~0`, `/` → `~1`.
|
||||
fn dot_path_to_json_pointer(dot_path: &str) -> String {
|
||||
let mut pointer = String::new();
|
||||
for segment in dot_path.split('.') {
|
||||
pointer.push('/');
|
||||
for ch in segment.chars() {
|
||||
match ch {
|
||||
'~' => pointer.push_str("~0"),
|
||||
'/' => pointer.push_str("~1"),
|
||||
_ => pointer.push(ch),
|
||||
}
|
||||
}
|
||||
}
|
||||
pointer
|
||||
}
|
||||
|
||||
/// Timing-safe string comparison via constant-time equality check.
|
||||
/// Prevents timing side-channel attacks on CSRF token verification.
|
||||
fn timing_safe_eq(a: &str, b: &str) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
a.as_bytes().ct_eq(b.as_bytes()).into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Args)]
|
||||
pub struct OidcOptions {
|
||||
#[arg(long, help = t!("cli.oidc_issuer_url").to_string())]
|
||||
pub oidc_issuer_url: Option<String>,
|
||||
|
||||
#[arg(long, help = t!("cli.oidc_client_id").to_string())]
|
||||
pub oidc_client_id: Option<String>,
|
||||
|
||||
#[arg(long, env = "OIDC_CLIENT_SECRET", help = t!("cli.oidc_client_secret").to_string())]
|
||||
pub oidc_client_secret: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "preferred_username", help = t!("cli.oidc_username_claim").to_string())]
|
||||
pub oidc_username_claim: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
default_values = DEFAULT_OIDC_SCOPES,
|
||||
help = t!("cli.oidc_scopes").to_string()
|
||||
)]
|
||||
pub oidc_scopes: Vec<String>,
|
||||
|
||||
#[arg(long, help = t!("cli.oidc_redirect_url").to_string())]
|
||||
pub oidc_redirect_url: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "false", help = t!("cli.oidc_disable_pkce").to_string())]
|
||||
pub oidc_disable_pkce: bool,
|
||||
|
||||
#[arg(long, help = t!("cli.oidc_frontend_base_url").to_string())]
|
||||
pub oidc_frontend_base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl OidcOptions {
|
||||
pub fn any_param_provided(&self) -> bool {
|
||||
self.oidc_issuer_url.is_some()
|
||||
|| self.oidc_client_id.is_some()
|
||||
|| self.oidc_client_secret.is_some()
|
||||
|| self.oidc_redirect_url.is_some()
|
||||
|| self.oidc_frontend_base_url.is_some()
|
||||
|| self.oidc_username_claim != "preferred_username"
|
||||
|| self.oidc_scopes != DEFAULT_OIDC_SCOPES
|
||||
|| self.oidc_disable_pkce
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OidcConfig {
|
||||
pub enabled: bool,
|
||||
pub provider_metadata: Option<Arc<CoreProviderMetadata>>,
|
||||
pub client_id: String,
|
||||
pub client_secret: Option<String>,
|
||||
pub redirect_url: Option<RedirectUrl>,
|
||||
pub username_claim: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub pkce_enabled: bool,
|
||||
pub frontend_base_url: Option<String>,
|
||||
pub http_client: Option<reqwest::Client>,
|
||||
cached_client: Option<Arc<ConfiguredAppClient>>,
|
||||
}
|
||||
|
||||
impl OidcConfig {
|
||||
pub fn disabled() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
provider_metadata: None,
|
||||
client_id: String::new(),
|
||||
client_secret: None,
|
||||
redirect_url: None,
|
||||
username_claim: "preferred_username".to_string(),
|
||||
scopes: DEFAULT_OIDC_SCOPES
|
||||
.iter()
|
||||
.map(|scope| scope.to_string())
|
||||
.collect(),
|
||||
pkce_enabled: false,
|
||||
frontend_base_url: None,
|
||||
http_client: None,
|
||||
cached_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_params(opts: OidcOptions) -> anyhow::Result<Self> {
|
||||
let OidcOptions {
|
||||
oidc_issuer_url,
|
||||
oidc_client_id,
|
||||
oidc_client_secret,
|
||||
oidc_username_claim,
|
||||
oidc_scopes,
|
||||
oidc_redirect_url,
|
||||
oidc_disable_pkce,
|
||||
oidc_frontend_base_url,
|
||||
} = opts;
|
||||
|
||||
if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"
|
||||
));
|
||||
}
|
||||
if oidc_username_claim.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
|
||||
}
|
||||
let http_client = reqwest::ClientBuilder::new()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?;
|
||||
|
||||
let issuer_url = oidc_issuer_url.ok_or_else(|| {
|
||||
anyhow::anyhow!("--oidc-issuer-url is required when using OIDC authentication")
|
||||
})?;
|
||||
|
||||
let provider_metadata =
|
||||
CoreProviderMetadata::discover_async(IssuerUrl::new(issuer_url)?, &http_client).await?;
|
||||
|
||||
let client_id = oidc_client_id.ok_or_else(|| {
|
||||
anyhow::anyhow!("--oidc-client-id is required when using OIDC authentication")
|
||||
})?;
|
||||
|
||||
let redirect_url = oidc_redirect_url
|
||||
.ok_or_else(|| anyhow::anyhow!("--oidc-redirect-url is required when using OIDC authentication. The redirect URL must match exactly what is registered with your Identity Provider. Example: --oidc-redirect-url http://your-domain.com:11211/api/v1/auth/oidc/callback"))?;
|
||||
|
||||
let provider_metadata = Arc::new(provider_metadata);
|
||||
let redirect_url = RedirectUrl::new(redirect_url)?;
|
||||
let client_secret = oidc_client_secret;
|
||||
|
||||
let cached_client = {
|
||||
let c = AppClient::from_provider_metadata(
|
||||
provider_metadata.as_ref().clone(),
|
||||
ClientId::new(client_id.clone()),
|
||||
client_secret.as_ref().map(|s| ClientSecret::new(s.clone())),
|
||||
)
|
||||
.set_redirect_uri(redirect_url.clone());
|
||||
Arc::new(c)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
enabled: true,
|
||||
provider_metadata: Some(provider_metadata),
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_url: Some(redirect_url),
|
||||
username_claim: oidc_username_claim,
|
||||
scopes: normalize_oidc_scopes(&oidc_scopes),
|
||||
pkce_enabled: !oidc_disable_pkce,
|
||||
frontend_base_url: oidc_frontend_base_url,
|
||||
http_client: Some(http_client),
|
||||
cached_client: Some(cached_client),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Option<&ConfiguredAppClient> {
|
||||
self.cached_client.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppStateInner> {
|
||||
Router::new()
|
||||
.route("/api/v1/auth/oidc/config", get(self::route::oidc_config))
|
||||
.route("/api/v1/auth/oidc/login", get(self::route::oidc_login))
|
||||
.route(
|
||||
"/api/v1/auth/oidc/callback",
|
||||
get(self::route::oidc_callback),
|
||||
)
|
||||
}
|
||||
|
||||
mod route {
|
||||
use axum::extract::Query;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::{Extension, Json};
|
||||
use openidconnect::core::CoreAuthenticationFlow;
|
||||
use openidconnect::{
|
||||
AccessTokenHash, AuthorizationCode, CsrfToken, Nonce, OAuth2TokenResponse,
|
||||
PkceCodeChallenge, PkceCodeVerifier, Scope, TokenResponse,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::restful::other_error;
|
||||
use crate::restful::users::AuthSession;
|
||||
|
||||
use super::OidcConfig;
|
||||
|
||||
pub async fn oidc_config(Extension(oidc): Extension<OidcConfig>) -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({ "enabled": oidc.enabled }))
|
||||
}
|
||||
|
||||
pub async fn oidc_login(
|
||||
Extension(oidc): Extension<OidcConfig>,
|
||||
session: tower_sessions::Session,
|
||||
) -> Response {
|
||||
if !oidc.enabled {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("OIDC is not enabled")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let client = match oidc.client() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("OIDC client not initialized")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let scopes = oidc.scopes.clone();
|
||||
let pkce_enabled = oidc.pkce_enabled;
|
||||
|
||||
let (pkce_challenge, pkce_verifier) = if pkce_enabled {
|
||||
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
(Some(challenge), Some(verifier))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let mut auth_request = client.authorize_url(
|
||||
CoreAuthenticationFlow::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
);
|
||||
|
||||
for scope in &scopes {
|
||||
auth_request = auth_request.add_scope(Scope::new(scope.clone()));
|
||||
}
|
||||
|
||||
if let Some(challenge) = pkce_challenge {
|
||||
auth_request = auth_request.set_pkce_challenge(challenge);
|
||||
}
|
||||
|
||||
let (auth_url, csrf_token, nonce) = auth_request.url();
|
||||
|
||||
if let Err(e) = session
|
||||
.insert("oidc_csrf_token", csrf_token.secret().clone())
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to store csrf_token in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = session.insert("oidc_nonce", nonce.secret().clone()).await {
|
||||
tracing::error!("Failed to store nonce in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Some(verifier) = pkce_verifier
|
||||
&& let Err(e) = session
|
||||
.insert("oidc_pkce_verifier", verifier.secret().clone())
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to store pkce_verifier in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await {
|
||||
tracing::error!("Failed to store pkce_used in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
Redirect::temporary(auth_url.as_str()).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CallbackParams {
|
||||
code: Option<String>,
|
||||
state: Option<String>,
|
||||
error: Option<String>,
|
||||
error_description: Option<String>,
|
||||
}
|
||||
|
||||
async fn cleanup_oidc_session(session: &tower_sessions::Session) {
|
||||
let _ = session.remove::<String>("oidc_csrf_token").await;
|
||||
let _ = session.remove::<String>("oidc_nonce").await;
|
||||
let _ = session.remove::<String>("oidc_pkce_verifier").await;
|
||||
let _ = session.remove::<bool>("oidc_pkce_used").await;
|
||||
}
|
||||
|
||||
pub async fn oidc_callback(
|
||||
Extension(oidc): Extension<OidcConfig>,
|
||||
Query(params): Query<CallbackParams>,
|
||||
session: tower_sessions::Session,
|
||||
mut auth_session: AuthSession,
|
||||
) -> Response {
|
||||
if !oidc.enabled {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("OIDC is not enabled")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Some(ref error) = params.error {
|
||||
tracing::error!(
|
||||
"OIDC provider returned error: {}, description: {:?}",
|
||||
error,
|
||||
params.error_description
|
||||
);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error(
|
||||
"Authentication failed at the identity provider",
|
||||
)),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let code = match params.code {
|
||||
Some(ref c) => c.clone(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("Missing authorization code")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let callback_state = match params.state {
|
||||
Some(ref s) => s.clone(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("Missing state parameter in callback")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let stored_csrf: String = match session.get("oidc_csrf_token").await {
|
||||
Ok(Some(v)) => v,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("Missing or invalid CSRF token in session")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if !super::timing_safe_eq(&stored_csrf, &callback_state) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("CSRF state mismatch")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let stored_nonce: String = match session.get("oidc_nonce").await {
|
||||
Ok(Some(v)) => v,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("Missing nonce in session")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let stored_pkce_verifier: Option<String> =
|
||||
session.get("oidc_pkce_verifier").await.ok().flatten();
|
||||
let pkce_was_used: Option<bool> = session.get("oidc_pkce_used").await.ok().flatten();
|
||||
|
||||
cleanup_oidc_session(&session).await;
|
||||
|
||||
let client = match oidc.client() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("OIDC client not initialized")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let http_client = match oidc.http_client.as_ref() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
tracing::error!("HTTP client not initialized in OIDC config");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("OIDC internal error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut token_request = match client.exchange_code(AuthorizationCode::new(code)) {
|
||||
Ok(req) => req,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create token request: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to create token exchange request")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(stored_pkce_verifier) = stored_pkce_verifier {
|
||||
token_request =
|
||||
token_request.set_pkce_verifier(PkceCodeVerifier::new(stored_pkce_verifier));
|
||||
} else if pkce_was_used == Some(true) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error(
|
||||
"PKCE was enabled but verifier is missing from session (session may have expired)",
|
||||
)),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token_response = match token_request.request_async(http_client).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to exchange code for token: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Token exchange failed")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let id_token = match token_response.id_token() {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("No ID token in response")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let claims = match id_token.claims(&client.id_token_verifier(), &Nonce::new(stored_nonce)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to verify ID token: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(other_error("ID token verification failed")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(expected_at_hash) = claims.access_token_hash() {
|
||||
let id_token_verifier = client.id_token_verifier();
|
||||
let (Ok(signing_alg), Ok(signing_key)) = (
|
||||
id_token.signing_alg(),
|
||||
id_token.signing_key(&id_token_verifier),
|
||||
) else {
|
||||
tracing::error!("Failed to get signing algorithm or key for at_hash verification");
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to determine token signing algorithm")),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
let actual_at_hash = match AccessTokenHash::from_token(
|
||||
token_response.access_token(),
|
||||
signing_alg,
|
||||
signing_key,
|
||||
) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to compute access token hash: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to verify access token hash")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if actual_at_hash != *expected_at_hash {
|
||||
tracing::error!("Access token hash mismatch");
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(other_error("Access token hash mismatch")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let claims_json = match serde_json::to_value(claims) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to serialize claims: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to process ID token claims")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let pointer = super::dot_path_to_json_pointer(&oidc.username_claim);
|
||||
let username: Option<String> = claims_json
|
||||
.pointer(&pointer)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let username = match username {
|
||||
Some(u) if !u.is_empty() => u,
|
||||
_ => {
|
||||
tracing::error!(
|
||||
"Could not extract username from claim '{}' in token",
|
||||
oidc.username_claim
|
||||
);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(other_error("Could not extract username from token claims")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match auth_session
|
||||
.backend
|
||||
.find_or_create_oidc_user(&username)
|
||||
.await
|
||||
{
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to find or create OIDC user '{}': {:?}", username, e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to provision user account")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = auth_session.login(&user).await {
|
||||
tracing::error!("Failed to login user via OIDC: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Failed to establish session")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if let Err(e) = session.cycle_id().await {
|
||||
tracing::error!("Failed to cycle session ID after OIDC login: {:?}", e);
|
||||
}
|
||||
if let Some(frontend_url) = &oidc.frontend_base_url {
|
||||
Redirect::temporary(frontend_url).into_response()
|
||||
} else {
|
||||
Redirect::temporary("/").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dot_path_to_json_pointer() {
|
||||
use serde_json::json;
|
||||
|
||||
let cases = vec![
|
||||
(
|
||||
"realm_access.roles.0",
|
||||
"/realm_access/roles/0",
|
||||
json!({ "realm_access": { "roles": ["admin", "user"] } }),
|
||||
"admin",
|
||||
),
|
||||
(
|
||||
"preferred_username",
|
||||
"/preferred_username",
|
||||
json!({ "preferred_username": "bob" }),
|
||||
"bob",
|
||||
),
|
||||
("a~b.c", "/a~0b/c", json!({ "a~b": { "c": "v" } }), "v"),
|
||||
("a/b.c", "/a~1b/c", json!({ "a/b": { "c": "w" } }), "w"),
|
||||
("~/.x", "/~0~1/x", json!({ "~/": { "x": "z" } }), "z"),
|
||||
("a..b", "/a//b", json!({ "a": { "": { "b": "x" } } }), "x"),
|
||||
("", "/", json!({ "": "root" }), "root"),
|
||||
];
|
||||
|
||||
for (path, expected_ptr, json_val, expected_val) in cases {
|
||||
let ptr = dot_path_to_json_pointer(path);
|
||||
assert_eq!(ptr, expected_ptr, "Pointer mismatch for path: {}", path);
|
||||
assert_eq!(
|
||||
json_val.pointer(&ptr).and_then(|v| v.as_str()),
|
||||
Some(expected_val),
|
||||
"Value extraction failed for path: {}, pointer: {}",
|
||||
path,
|
||||
ptr
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
routing::post,
|
||||
};
|
||||
use axum_login::AuthUser as _;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
|
||||
use crate::db::UserIdInDb;
|
||||
|
||||
use super::{AppState, HttpHandleError, other_error};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ProxyRpcRequest {
|
||||
pub service_name: String,
|
||||
pub method_name: String,
|
||||
pub payload: serde_json::Value,
|
||||
}
|
||||
|
||||
macro_rules! match_service {
|
||||
($factory:ty, $method_name:expr, $payload:expr, $session:expr) => {{
|
||||
let client = $session.scoped_client::<$factory>();
|
||||
client
|
||||
.json_call_method(BaseController::default(), &$method_name, $payload)
|
||||
.await
|
||||
}};
|
||||
}
|
||||
|
||||
async fn handle_proxy_rpc_by_session(
|
||||
session: &crate::client_manager::session::Session,
|
||||
req: ProxyRpcRequest,
|
||||
) -> Result<Json<serde_json::Value>, HttpHandleError> {
|
||||
let ProxyRpcRequest {
|
||||
service_name,
|
||||
method_name,
|
||||
payload,
|
||||
} = req;
|
||||
|
||||
let resp = match service_name.as_str() {
|
||||
"api.manage.WebClientService" => match_service!(
|
||||
easytier::proto::api::manage::WebClientServiceClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.PeerManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::PeerManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.PeerCenterManageRpcService" => match_service!(
|
||||
easytier::proto::peer_rpc::PeerCenterRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.ConnectorManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::ConnectorManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.MappedListenerManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::MappedListenerManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.VpnPortalRpcService" => match_service!(
|
||||
easytier::proto::api::instance::VpnPortalRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.TcpProxyRpcService" => match_service!(
|
||||
easytier::proto::api::instance::TcpProxyRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.AclManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::AclManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.PortForwardManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::PortForwardManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.StatsRpcService" => match_service!(
|
||||
easytier::proto::api::instance::StatsRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.instance.CredentialManageRpcService" => match_service!(
|
||||
easytier::proto::api::instance::CredentialManageRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.logger.LoggerRpcService" => match_service!(
|
||||
easytier::proto::api::logger::LoggerRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
"api.config.ConfigRpcService" => match_service!(
|
||||
easytier::proto::api::config::ConfigRpcClientFactory<BaseController>,
|
||||
method_name,
|
||||
payload,
|
||||
session
|
||||
),
|
||||
_ => {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
other_error(format!("Unknown service: {}", service_name)).into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
match resp {
|
||||
Ok(v) => Ok(Json(v)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
other_error(format!("RPC Error: {:?}", e)).into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_proxy_rpc(
|
||||
auth_session: super::users::AuthSession,
|
||||
State(client_mgr): AppState,
|
||||
Path(machine_id): Path<uuid::Uuid>,
|
||||
Json(req): Json<ProxyRpcRequest>,
|
||||
) -> Result<Json<serde_json::Value>, HttpHandleError> {
|
||||
let user_id = auth_session
|
||||
.user
|
||||
.as_ref()
|
||||
.ok_or((StatusCode::UNAUTHORIZED, other_error("Unauthorized").into()))?
|
||||
.id();
|
||||
|
||||
let session = client_mgr
|
||||
.get_session_by_machine_id(user_id, &machine_id)
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
other_error("Session not found").into(),
|
||||
))?;
|
||||
handle_proxy_rpc_by_session(session.as_ref(), req).await
|
||||
}
|
||||
|
||||
pub fn router() -> Router<super::AppStateInner> {
|
||||
Router::new().route(
|
||||
"/api/v1/machines/:machine-id/proxy-rpc",
|
||||
post(handle_proxy_rpc),
|
||||
)
|
||||
}
|
||||
|
||||
/// Internal proxy-rpc handler: no AuthSession, resolves the active session by machine_id.
|
||||
pub async fn handle_proxy_rpc_internal(
|
||||
State(client_mgr): AppState,
|
||||
Path((user_id, machine_id)): Path<(UserIdInDb, uuid::Uuid)>,
|
||||
Json(req): Json<ProxyRpcRequest>,
|
||||
) -> Result<Json<serde_json::Value>, HttpHandleError> {
|
||||
let session = client_mgr
|
||||
.get_session_by_machine_id(user_id, &machine_id)
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
other_error("Session not found").into(),
|
||||
))?;
|
||||
handle_proxy_rpc_by_session(session.as_ref(), req).await
|
||||
}
|
||||
|
||||
pub fn router_internal() -> Router<super::AppStateInner> {
|
||||
Router::new().route(
|
||||
"/api/internal/users/:user-id/machines/:machine-id/proxy-rpc",
|
||||
post(handle_proxy_rpc_internal),
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,8 @@ use async_trait::async_trait;
|
||||
use axum_login::{AuthUser, AuthnBackend, AuthzBackend, UserId};
|
||||
use password_auth::verify_password;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait as _, ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, JoinType,
|
||||
QueryFilter, QuerySelect as _, RelationTrait, Set, TransactionTrait,
|
||||
ColumnTrait, EntityTrait, FromQueryResult, IntoActiveModel, JoinType, QueryFilter,
|
||||
QuerySelect as _, RelationTrait, Set,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
@@ -14,7 +14,7 @@ use crate::db::{self, entity};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
db_user: entity::users::Model,
|
||||
pub(crate) db_user: entity::users::Model,
|
||||
pub tokens: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ impl AuthUser for User {
|
||||
|
||||
fn session_auth_hash(&self) -> &[u8] {
|
||||
self.db_user.password.as_bytes() // We use the password hash as the auth
|
||||
// hash--what this means
|
||||
// is when the user changes their password the
|
||||
// auth session becomes invalid.
|
||||
// hash--what this means
|
||||
// is when the user changes their password the
|
||||
// auth session becomes invalid.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,40 +74,47 @@ impl Backend {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
pub fn db(&self) -> &db::Db {
|
||||
&self.db
|
||||
}
|
||||
|
||||
pub async fn register_new_user(&self, new_user: &RegisterNewUser) -> anyhow::Result<()> {
|
||||
let hashed_password = password_auth::generate_hash(new_user.credentials.password.as_str());
|
||||
let txn = self.db.orm_db().begin().await?;
|
||||
|
||||
entity::users::ActiveModel {
|
||||
username: Set(new_user.credentials.username.clone()),
|
||||
password: Set(hashed_password.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&txn)
|
||||
.await?;
|
||||
|
||||
entity::users_groups::ActiveModel {
|
||||
user_id: Set(entity::users::Entity::find()
|
||||
.filter(entity::users::Column::Username.eq(new_user.credentials.username.as_str()))
|
||||
.one(&txn)
|
||||
.await?
|
||||
.unwrap()
|
||||
.id),
|
||||
group_id: Set(entity::groups::Entity::find()
|
||||
.filter(entity::groups::Column::Name.eq("users"))
|
||||
.one(&txn)
|
||||
.await?
|
||||
.unwrap()
|
||||
.id),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&txn)
|
||||
.await?;
|
||||
txn.commit().await?;
|
||||
|
||||
self.db
|
||||
.create_user_and_join_users_group(&new_user.credentials.username, hashed_password)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find a user by username, or auto-create one for OIDC-authenticated users.
|
||||
///
|
||||
/// Unlike the heartbeat auto-creation path (controlled by `allow_auto_create_user`),
|
||||
/// OIDC users are always provisioned automatically because their identity has already
|
||||
/// been verified by a trusted external Identity Provider (IdP).
|
||||
pub async fn find_or_create_oidc_user(&self, username: &str) -> anyhow::Result<User> {
|
||||
use entity::users;
|
||||
|
||||
// Try to find an existing user first.
|
||||
if let Some(db_user) = users::Entity::find()
|
||||
.filter(users::Column::Username.eq(username))
|
||||
.one(self.db.orm_db())
|
||||
.await?
|
||||
{
|
||||
return Ok(User {
|
||||
tokens: vec![db_user.username.clone()],
|
||||
db_user,
|
||||
});
|
||||
}
|
||||
|
||||
// User not found – auto-provision a local account backed by the IdP identity.
|
||||
let db_user = self.db.auto_create_user(username).await?;
|
||||
tracing::info!("Auto-provisioned OIDC user '{username}'");
|
||||
Ok(User {
|
||||
tokens: vec![db_user.username.clone()],
|
||||
db_user,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
id: <User as AuthUser>::Id,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
routing, Router,
|
||||
routing,
|
||||
};
|
||||
use axum_embed::ServeEmbed;
|
||||
use easytier::common::scoped_task::ScopedTask;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
/// Embed assets for web dashboard, build frontend first
|
||||
#[derive(RustEmbed, Clone)]
|
||||
@@ -58,7 +59,7 @@ pub fn build_router(api_host: Option<url::Url>) -> Router {
|
||||
pub struct WebServer {
|
||||
bind_addr: SocketAddr,
|
||||
router: Router,
|
||||
serve_task: Option<ScopedTask<()>>,
|
||||
serve_task: Option<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
@@ -70,14 +71,13 @@ impl WebServer {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(self) -> Result<ScopedTask<()>, anyhow::Error> {
|
||||
pub async fn start(self) -> Result<AbortOnDropHandle<()>, anyhow::Error> {
|
||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||
let app = self.router;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
})
|
||||
.into();
|
||||
}));
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Webhook configuration for external integrations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebhookConfig {
|
||||
pub webhook_url: Option<String>,
|
||||
pub webhook_secret: Option<String>,
|
||||
pub internal_auth_token: Option<String>,
|
||||
pub web_instance_id: Option<String>,
|
||||
pub web_instance_api_base_url: Option<String>,
|
||||
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl WebhookConfig {
|
||||
pub fn new(
|
||||
webhook_url: Option<String>,
|
||||
webhook_secret: Option<String>,
|
||||
internal_auth_token: Option<String>,
|
||||
web_instance_id: Option<String>,
|
||||
web_instance_api_base_url: Option<String>,
|
||||
) -> Self {
|
||||
WebhookConfig {
|
||||
webhook_url,
|
||||
webhook_secret,
|
||||
internal_auth_token,
|
||||
web_instance_id,
|
||||
web_instance_api_base_url,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.webhook_url
|
||||
.as_deref()
|
||||
.is_some_and(|url| !url.trim().is_empty())
|
||||
}
|
||||
|
||||
pub fn has_internal_auth(&self) -> bool {
|
||||
self.internal_auth_token.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Request/Response types ---
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ValidateTokenRequest {
|
||||
pub token: String,
|
||||
pub machine_id: String,
|
||||
pub public_ip: Option<String>,
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub os_type: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub os_distribution: Option<String>,
|
||||
pub web_instance_id: Option<String>,
|
||||
pub web_instance_api_base_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ValidateTokenResponse {
|
||||
pub valid: bool,
|
||||
#[serde(default)]
|
||||
pub pre_approved: bool,
|
||||
#[serde(default)]
|
||||
pub binding_version: u64,
|
||||
pub managed_network_configs: Vec<ManagedNetworkConfig>,
|
||||
pub config_revision: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ManagedNetworkConfig {
|
||||
pub instance_id: String,
|
||||
pub network_config: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NodeConnectedRequest {
|
||||
pub machine_id: String,
|
||||
pub token: String,
|
||||
pub user_id: Option<i32>,
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub os_type: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub os_distribution: Option<String>,
|
||||
pub web_instance_id: Option<String>,
|
||||
pub binding_version: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NodeDisconnectedRequest {
|
||||
pub machine_id: String,
|
||||
pub token: String,
|
||||
pub user_id: Option<i32>,
|
||||
pub web_instance_id: Option<String>,
|
||||
pub binding_version: Option<u64>,
|
||||
}
|
||||
|
||||
// --- Webhook client ---
|
||||
|
||||
impl WebhookConfig {
|
||||
fn webhook_base_url(&self) -> anyhow::Result<&str> {
|
||||
self.webhook_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|url| !url.is_empty())
|
||||
.ok_or_else(|| anyhow::anyhow!("webhook_url is not configured"))
|
||||
}
|
||||
|
||||
fn webhook_endpoint(&self, path: &str) -> anyhow::Result<String> {
|
||||
Ok(format!(
|
||||
"{}/{}",
|
||||
self.webhook_base_url()?.trim_end_matches('/'),
|
||||
path.trim_start_matches('/'),
|
||||
))
|
||||
}
|
||||
|
||||
/// Validate a token through the configured webhook endpoint.
|
||||
pub async fn validate_token(
|
||||
&self,
|
||||
req: &ValidateTokenRequest,
|
||||
) -> anyhow::Result<ValidateTokenResponse> {
|
||||
let url = self.webhook_endpoint("validate-token")?;
|
||||
let resp = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("X-Internal-Auth", self.webhook_auth_secret())
|
||||
.json(req)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("webhook validate-token returned status {}", resp.status());
|
||||
}
|
||||
|
||||
Ok(resp.json().await?)
|
||||
}
|
||||
|
||||
/// Notify the webhook receiver that a node has connected.
|
||||
pub async fn notify_node_connected(&self, req: &NodeConnectedRequest) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let Ok(url) = self.webhook_endpoint("webhook/node-connected") else {
|
||||
tracing::warn!("skip node-connected webhook because webhook_url is not configured");
|
||||
return;
|
||||
};
|
||||
let _ = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("X-Internal-Auth", self.webhook_auth_secret())
|
||||
.json(req)
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Notify the webhook receiver that a node has disconnected.
|
||||
pub async fn notify_node_disconnected(&self, req: &NodeDisconnectedRequest) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
let Ok(url) = self.webhook_endpoint("webhook/node-disconnected") else {
|
||||
tracing::warn!("skip node-disconnected webhook because webhook_url is not configured");
|
||||
return;
|
||||
};
|
||||
let _ = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("X-Internal-Auth", self.webhook_auth_secret())
|
||||
.json(req)
|
||||
.send()
|
||||
.await;
|
||||
}
|
||||
|
||||
fn webhook_auth_secret(&self) -> &str {
|
||||
self.webhook_secret
|
||||
.as_deref()
|
||||
.or(self.internal_auth_token.as_deref())
|
||||
.unwrap_or("")
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedWebhookConfig = Arc<WebhookConfig>;
|
||||
@@ -0,0 +1,11 @@
|
||||
disallowed-methods = [
|
||||
{ path = "itertools::Itertools::map_into", reason = "Blocks underlying iterator optimizations. Use the native `.map(Into::into)` instead." },
|
||||
{ path = "itertools::Itertools::map_ok", reason = "Blocks underlying iterator optimizations. Use the native `.map(|r| r.map(f))` instead." },
|
||||
{ path = "itertools::Itertools::filter_ok", reason = "Blocks underlying iterator optimizations. Use a native approach, e.g., `.filter(|r| r.as_ref().map_or(true, condition))`." },
|
||||
{ path = "itertools::Itertools::filter_map_ok", reason = "Blocks underlying iterator optimizations. Use native `.map()` and `.flatten()`, or extract logic into a standard `.filter_map()`." },
|
||||
|
||||
{ path = "itertools::Itertools::collect_vec", reason = "Non-standard idiom. Directly use the standard library's `.collect::<Vec<_>>()`." },
|
||||
{ path = "itertools::Itertools::try_collect", reason = "Non-standard idiom. Standard `collect()` already supports Result/Option inversion; use `.collect::<Result<_, _>>()`." },
|
||||
{ path = "itertools::Itertools::set_from", reason = "Non-standard idiom. Directly use the `.extend()` method provided by the standard library's `Extend` trait." },
|
||||
{ path = "itertools::Itertools::concat", reason = "Non-standard idiom. Use native `.flatten().collect()` or a slice's `.concat()` instead." }
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user