mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-10 07:55:36 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 349dbf7d8d | |||
| 7707b1cf5e | |||
| 2490bb9808 | |||
| 3f3e36e653 |
+54
-35
@@ -1,40 +1,29 @@
|
|||||||
# region Native
|
[target.x86_64-unknown-linux-musl]
|
||||||
|
linker = "rust-lld"
|
||||||
[target.x86_64-unknown-linux-gnu]
|
rustflags = ["-C", "linker-flavor=ld.lld"]
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-gnu]
|
[target.aarch64-unknown-linux-gnu]
|
||||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
linker = "aarch64-linux-gnu-gcc"
|
||||||
|
|
||||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
[target.aarch64-unknown-linux-ohos]
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||||
|
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||||
|
|
||||||
# region
|
[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"
|
||||||
# region CI
|
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"
|
||||||
[target.x86_64-unknown-linux-musl]
|
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-musl]
|
[target.aarch64-unknown-linux-musl]
|
||||||
|
linker = "aarch64-unknown-linux-musl-gcc"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
[target.riscv64gc-unknown-linux-musl]
|
[target.riscv64gc-unknown-linux-musl]
|
||||||
|
linker = "riscv64-unknown-linux-musl-gcc"
|
||||||
rustflags = ["-C", "target-feature=+crt-static"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
[target.armv7-unknown-linux-musleabihf]
|
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||||
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"]
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
[target.mipsel-unknown-linux-musl]
|
[target.mipsel-unknown-linux-musl]
|
||||||
@@ -75,14 +64,44 @@ rustflags = [
|
|||||||
"gcc",
|
"gcc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-ohos]
|
[target.armv7-unknown-linux-musleabihf]
|
||||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
linker = "armv7-unknown-linux-musleabihf-gcc"
|
||||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-ohos.env]
|
[target.armv7-unknown-linux-musleabi]
|
||||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
linker = "armv7-unknown-linux-musleabi-gcc"
|
||||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
rustflags = ["-C", "target-feature=+crt-static"]
|
||||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
|
||||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
|
||||||
|
|
||||||
# endregion
|
[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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,17 +2,10 @@ name: prepare-build
|
|||||||
author: Luna
|
author: Luna
|
||||||
description: Prepare build environment
|
description: Prepare build environment
|
||||||
inputs:
|
inputs:
|
||||||
target:
|
web:
|
||||||
description: 'The target to build for'
|
description: 'Whether to prepare the web build environment'
|
||||||
required: false
|
|
||||||
pnpm:
|
|
||||||
description: 'Whether to run pnpm build'
|
|
||||||
required: true
|
required: true
|
||||||
default: 'true'
|
default: 'true'
|
||||||
pnpm-build-filter:
|
|
||||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
|
||||||
required: false
|
|
||||||
default: './easytier-web/*'
|
|
||||||
gui:
|
gui:
|
||||||
description: 'Whether to prepare the GUI build environment'
|
description: 'Whether to prepare the GUI build environment'
|
||||||
required: true
|
required: true
|
||||||
@@ -26,61 +19,21 @@ runs:
|
|||||||
- run: mkdir -p easytier-gui/dist
|
- run: mkdir -p easytier-gui/dist
|
||||||
shell: bash
|
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
|
- name: Setup Frontend Environment
|
||||||
if: ${{ inputs.pnpm == 'true' }}
|
if: ${{ inputs.web == 'true' }}
|
||||||
uses: ./.github/actions/prepare-pnpm
|
uses: ./.github/actions/prepare-pnpm
|
||||||
with:
|
with:
|
||||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
build-filter: './easytier-web/*'
|
||||||
|
|
||||||
- name: Install GUI dependencies (Linux)
|
- name: Install GUI dependencies (Used by clippy)
|
||||||
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }}
|
if: ${{ inputs.gui == 'true' }}
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -qq xdg-utils \
|
bash ./.github/workflows/install_gui_dep.sh
|
||||||
libappindicator3-dev \
|
|
||||||
libgtk-3-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
libxdo-dev
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
- name: Install Rust
|
||||||
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: |
|
run: |
|
||||||
MUSL_TARGET=${{ inputs.target }}sf
|
bash ./.github/workflows/install_rust.sh
|
||||||
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
|
shell: bash
|
||||||
|
|
||||||
- name: Setup protoc
|
- name: Setup protoc
|
||||||
|
|||||||
+142
-119
@@ -2,14 +2,9 @@ name: EasyTier Core
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "develop", "main", "releases/**" ]
|
branches: ["develop", "main", "releases/**"]
|
||||||
pull_request:
|
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:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -23,7 +18,6 @@ jobs:
|
|||||||
pre_job:
|
pre_job:
|
||||||
# continue-on-error: true # Uncomment once integration is finished
|
# continue-on-error: true # Uncomment once integration is finished
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
# Map a step output to a job output
|
# Map a step output to a job output
|
||||||
outputs:
|
outputs:
|
||||||
# do not skip push on branch starts with releases/
|
# do not skip push on branch starts with releases/
|
||||||
@@ -36,7 +30,7 @@ jobs:
|
|||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh", "easytier-web/**"]'
|
||||||
build_web:
|
build_web:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
@@ -57,48 +51,41 @@ jobs:
|
|||||||
easytier-web/frontend/dist/*
|
easytier-web/frontend/dist/*
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: x86_64-unknown-linux-musl
|
|
||||||
OS: ubuntu-24.04
|
|
||||||
ARTIFACT_NAME: linux-x86_64
|
|
||||||
- TARGET: aarch64-unknown-linux-musl
|
- TARGET: aarch64-unknown-linux-musl
|
||||||
OS: ubuntu-24.04-arm
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-aarch64
|
ARTIFACT_NAME: linux-aarch64
|
||||||
|
- TARGET: x86_64-unknown-linux-musl
|
||||||
|
OS: ubuntu-22.04
|
||||||
|
ARTIFACT_NAME: linux-x86_64
|
||||||
- TARGET: riscv64gc-unknown-linux-musl
|
- TARGET: riscv64gc-unknown-linux-musl
|
||||||
OS: ubuntu-24.04
|
OS: ubuntu-22.04
|
||||||
ARTIFACT_NAME: linux-riscv64
|
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: loongarch64-unknown-linux-musl
|
- TARGET: loongarch64-unknown-linux-musl
|
||||||
OS: ubuntu-24.04
|
OS: ubuntu-24.04
|
||||||
ARTIFACT_NAME: linux-loongarch64
|
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
|
- TARGET: x86_64-apple-darwin
|
||||||
OS: macos-latest
|
OS: macos-latest
|
||||||
ARTIFACT_NAME: macos-x86_64
|
ARTIFACT_NAME: macos-x86_64
|
||||||
@@ -109,12 +96,17 @@ jobs:
|
|||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
ARTIFACT_NAME: windows-x86_64
|
ARTIFACT_NAME: windows-x86_64
|
||||||
|
- TARGET: aarch64-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
ARTIFACT_NAME: windows-arm64
|
||||||
- TARGET: i686-pc-windows-msvc
|
- TARGET: i686-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
ARTIFACT_NAME: windows-i686
|
ARTIFACT_NAME: windows-i686
|
||||||
- TARGET: aarch64-pc-windows-msvc
|
|
||||||
OS: windows-11-arm
|
- TARGET: x86_64-unknown-freebsd
|
||||||
ARTIFACT_NAME: windows-arm64
|
OS: ubuntu-22.04
|
||||||
|
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||||
|
BSD_VERSION: 13.2
|
||||||
|
|
||||||
runs-on: ${{ matrix.OS }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
@@ -139,15 +131,8 @@ jobs:
|
|||||||
name: easytier-web-dashboard
|
name: easytier-web-dashboard
|
||||||
path: easytier-web/frontend/dist/
|
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
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||||
with:
|
with:
|
||||||
# The prefix cache key, this can be changed to start a new cache manually.
|
# The prefix cache key, this can be changed to start a new cache manually.
|
||||||
# default: "v0-rust"
|
# default: "v0-rust"
|
||||||
@@ -155,54 +140,96 @@ jobs:
|
|||||||
shared-key: "core-registry"
|
shared-key: "core-registry"
|
||||||
cache-targets: "false"
|
cache-targets: "false"
|
||||||
|
|
||||||
- uses: mlugg/setup-zig@v2
|
- name: Setup protoc
|
||||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
uses: arduino/setup-protoc@v3
|
||||||
with:
|
with:
|
||||||
version: 0.16.0
|
# GitHub repo token to use to avoid rate limiter
|
||||||
use-cache: true
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: taiki-e/install-action@v2
|
- name: Build Core & Cli
|
||||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||||
with:
|
|
||||||
tool: cargo-zigbuild
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
if: ${{ !contains(matrix.TARGET, 'mips') }}
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "$TARGET" == *windows* ]]; then
|
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-2026-02-02 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||||
|
else
|
||||||
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
|
CORE_FEATURES="--features=mimalloc"
|
||||||
|
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ || $TARGET =~ ^aarch64.*$ ]]; then
|
||||||
|
CORE_FEATURES="--features=mimalloc"
|
||||||
else
|
else
|
||||||
SUFFIX=""
|
CORE_FEATURES="--features=jemalloc"
|
||||||
fi
|
fi
|
||||||
|
cargo build --release --target $TARGET --package=easytier-web --features=embed
|
||||||
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"
|
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||||
|
cargo build --release --target $TARGET $CORE_FEATURES
|
||||||
|
fi
|
||||||
|
|
||||||
cargo $BUILD --release --target $TARGET --features=$FEATURES
|
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||||
|
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||||
- name: Build (MIPS)
|
uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
|
||||||
if: ${{ contains(matrix.TARGET, 'mips') }}
|
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||||
env:
|
env:
|
||||||
RUSTC_BOOTSTRAP: 1
|
TARGET: ${{ matrix.TARGET }}
|
||||||
|
with:
|
||||||
|
envs: TARGET
|
||||||
|
release: ${{ matrix.BSD_VERSION }}
|
||||||
|
arch: x86_64
|
||||||
|
usesh: true
|
||||||
|
mem: 6144
|
||||||
|
cpu: 4
|
||||||
run: |
|
run: |
|
||||||
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
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.93
|
||||||
|
rustup default 1.93
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
mkdir -p built-bins/$TARGET/release/
|
||||||
|
mv ./target/$TARGET/release/easytier-web-embed ./built-bins/$TARGET/release/easytier-web-embed
|
||||||
|
mv ./target/$TARGET/release/easytier-web ./built-bins/$TARGET/release/easytier-web
|
||||||
|
mv ./target/$TARGET/release/easytier-core ./built-bins/$TARGET/release/easytier-core
|
||||||
|
mv ./target/$TARGET/release/easytier-cli ./built-bins/$TARGET/release/easytier-cli
|
||||||
|
|
||||||
|
# remove dirs to avoid copy many files back
|
||||||
|
rm -rf ./target ~/.cargo
|
||||||
|
mv ./built-bins ./target
|
||||||
|
|
||||||
- name: Compress
|
- name: Compress
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./artifacts/objects/
|
mkdir -p ./artifacts/objects/
|
||||||
|
|
||||||
# windows is the only OS using a different convention for executable file name
|
# windows is the only OS using a different convention for executable file name
|
||||||
if [[ $OS =~ ^windows.*$ ]]; then
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
@@ -215,38 +242,27 @@ jobs:
|
|||||||
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
|
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||||
TAG=$GITHUB_REF_NAME
|
TAG=$GITHUB_REF_NAME
|
||||||
else
|
else
|
||||||
TAG=$GITHUB_SHA
|
TAG=$GITHUB_SHA
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then
|
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ && ! $TARGET =~ ^riscv64.*$ ]]; then
|
||||||
HOST_ARCH=$(uname -m)
|
|
||||||
case $HOST_ARCH in
|
|
||||||
x86_64) UPX_ARCH="amd64" ;;
|
|
||||||
aarch64) UPX_ARCH="arm64" ;;
|
|
||||||
*) UPX_ARCH="amd64" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
UPX_VERSION=4.2.4
|
UPX_VERSION=4.2.4
|
||||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
|
||||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
cp upx-${UPX_VERSION}-amd64_linux/upx .
|
||||||
cp "${UPX_PKG}/upx" .
|
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||||
UPX_BIN=./upx
|
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do
|
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||||
if [[ -f "$BIN" ]]; then
|
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||||
if [[ -n "$UPX_BIN" ]]; then
|
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||||
$UPX_BIN --lzma --best "$BIN" || true
|
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||||
|
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv "$BIN" ./artifacts/objects/
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
mv ./artifacts/objects/* ./artifacts/
|
mv ./artifacts/objects/* ./artifacts/
|
||||||
rm -rf ./artifacts/objects/
|
rm -rf ./artifacts/objects/
|
||||||
|
|
||||||
@@ -257,10 +273,25 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
build_magisk:
|
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()
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||||
@@ -280,6 +311,7 @@ jobs:
|
|||||||
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
||||||
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
||||||
|
|
||||||
|
|
||||||
# 上传生成的模块
|
# 上传生成的模块
|
||||||
- name: Upload Magisk Module
|
- name: Upload Magisk Module
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
@@ -290,12 +322,3 @@ jobs:
|
|||||||
!./easytier-contrib/easytier-magisk/build.sh
|
!./easytier-contrib/easytier-magisk/build.sh
|
||||||
!./easytier-contrib/easytier-magisk/magisk_update.json
|
!./easytier-contrib/easytier-magisk/magisk_update.json
|
||||||
if-no-files-found: error
|
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:
|
image_tag:
|
||||||
description: 'Tag for this image build'
|
description: 'Tag for this image build'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.6.4'
|
default: 'v2.6.0'
|
||||||
required: true
|
required: true
|
||||||
mark_latest:
|
mark_latest:
|
||||||
description: 'Mark this image as latest'
|
description: 'Mark this image as latest'
|
||||||
|
|||||||
+84
-37
@@ -5,11 +5,6 @@ on:
|
|||||||
branches: ["develop", "main", "releases/**"]
|
branches: ["develop", "main", "releases/**"]
|
||||||
pull_request:
|
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:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -23,7 +18,6 @@ jobs:
|
|||||||
pre_job:
|
pre_job:
|
||||||
# continue-on-error: true # Uncomment once integration is finished
|
# continue-on-error: true # Uncomment once integration is finished
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
# Map a step output to a job output
|
# Map a step output to a job output
|
||||||
outputs:
|
outputs:
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||||
@@ -35,20 +29,20 @@ jobs:
|
|||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh", "easytier-web/frontend-lib/**"]'
|
||||||
build-gui:
|
build-gui:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: x86_64-unknown-linux-musl
|
|
||||||
OS: ubuntu-24.04
|
|
||||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
|
||||||
ARTIFACT_NAME: linux-x86_64
|
|
||||||
- TARGET: aarch64-unknown-linux-musl
|
- TARGET: aarch64-unknown-linux-musl
|
||||||
OS: ubuntu-24.04-arm
|
OS: ubuntu-22.04
|
||||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||||
ARTIFACT_NAME: linux-aarch64
|
ARTIFACT_NAME: linux-aarch64
|
||||||
|
- TARGET: x86_64-unknown-linux-musl
|
||||||
|
OS: ubuntu-22.04
|
||||||
|
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||||
|
ARTIFACT_NAME: linux-x86_64
|
||||||
|
|
||||||
- TARGET: x86_64-apple-darwin
|
- TARGET: x86_64-apple-darwin
|
||||||
OS: macos-latest
|
OS: macos-latest
|
||||||
@@ -63,14 +57,16 @@ jobs:
|
|||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
GUI_TARGET: x86_64-pc-windows-msvc
|
GUI_TARGET: x86_64-pc-windows-msvc
|
||||||
ARTIFACT_NAME: windows-x86_64
|
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
|
- TARGET: i686-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
GUI_TARGET: i686-pc-windows-msvc
|
GUI_TARGET: i686-pc-windows-msvc
|
||||||
ARTIFACT_NAME: windows-i686
|
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 }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
@@ -84,29 +80,75 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
|
||||||
|
- name: Install rpm package (Linux target only)
|
||||||
|
if: ${{ contains(matrix.TARGET, '-linux-') }}
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y rpm
|
||||||
|
|
||||||
- name: Set current ref as env variable
|
- name: Set current ref as env variable
|
||||||
run: |
|
run: |
|
||||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Prepare build environment
|
- name: Setup Frontend Environment
|
||||||
uses: ./.github/actions/prepare-build
|
uses: ./.github/actions/prepare-pnpm
|
||||||
with:
|
|
||||||
target: ${{ matrix.TARGET }}
|
|
||||||
gui: true
|
|
||||||
pnpm: true
|
|
||||||
pnpm-build-filter: ''
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
# The prefix cache key, this can be changed to start a new cache manually.
|
# The prefix cache key, this can be changed to start a new cache manually.
|
||||||
# default: "v0-rust"
|
# default: "v0-rust"
|
||||||
prefix-key: ""
|
prefix-key: ""
|
||||||
shared-key: "gui-registry"
|
|
||||||
cache-targets: "false"
|
- 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 }}
|
||||||
|
|
||||||
- name: copy correct DLLs
|
- name: copy correct DLLs
|
||||||
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
|
if: ${{ matrix.OS == 'windows-latest' }}
|
||||||
run: |
|
run: |
|
||||||
case $TARGET in
|
case $TARGET in
|
||||||
x86_64*) ARCH_DIR=x86_64 ;;
|
x86_64*) ARCH_DIR=x86_64 ;;
|
||||||
@@ -122,9 +164,10 @@ jobs:
|
|||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
with:
|
with:
|
||||||
projectPath: ./easytier-gui
|
projectPath: ./easytier-gui
|
||||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||||
|
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ contains(matrix.TARGET, '-linux-') && contains(matrix.TARGET, 'aarch64') && '--bundles deb,rpm' || '' }}
|
||||||
|
|
||||||
- name: Collect artifact
|
- name: Compress
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./artifacts/objects/
|
mkdir -p ./artifacts/objects/
|
||||||
|
|
||||||
@@ -133,17 +176,19 @@ jobs:
|
|||||||
else
|
else
|
||||||
TAG=$GITHUB_SHA
|
TAG=$GITHUB_SHA
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# copy gui bundle, gui is built without specific target
|
# copy gui bundle, gui is built without specific target
|
||||||
if [[ $GUI_TARGET =~ windows ]]; then
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
||||||
elif [[ $GUI_TARGET =~ darwin ]]; then
|
elif [[ $OS =~ ^macos.*$ ]]; then
|
||||||
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||||
elif [[ $GUI_TARGET =~ linux ]]; then
|
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
|
||||||
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||||
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
|
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
|
||||||
|
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||||
|
# currently only x86 appimage is supported
|
||||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
mv ./artifacts/objects/* ./artifacts/
|
mv ./artifacts/objects/* ./artifacts/
|
||||||
rm -rf ./artifacts/objects/
|
rm -rf ./artifacts/objects/
|
||||||
@@ -156,10 +201,12 @@ jobs:
|
|||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
gui-result:
|
gui-result:
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ pre_job, build-gui ]
|
needs:
|
||||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
- pre_job
|
||||||
|
- build-gui
|
||||||
steps:
|
steps:
|
||||||
- name: Mark result as failed
|
- name: Mark result as failed
|
||||||
if: contains(needs.*.result, 'failure')
|
if: needs.build-gui.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#!/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.93
|
||||||
|
rustup default 1.93
|
||||||
|
|
||||||
|
# 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-2026-02-02-x86_64-unknown-linux-gnu
|
||||||
|
rustup component add rust-src --toolchain nightly-2026-02-02-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,11 +5,6 @@ on:
|
|||||||
branches: ["develop", "main", "releases/**"]
|
branches: ["develop", "main", "releases/**"]
|
||||||
pull_request:
|
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:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -23,7 +18,6 @@ jobs:
|
|||||||
pre_job:
|
pre_job:
|
||||||
# continue-on-error: true # Uncomment once integration is finished
|
# continue-on-error: true # Uncomment once integration is finished
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
# Map a step output to a job output
|
# Map a step output to a job output
|
||||||
outputs:
|
outputs:
|
||||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||||
@@ -35,25 +29,20 @@ jobs:
|
|||||||
concurrent_skipping: 'same_content_newer'
|
concurrent_skipping: 'same_content_newer'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||||
build-mobile:
|
build-mobile:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- TARGET: aarch64-linux-android
|
- TARGET: android
|
||||||
ARCH: aarch64
|
OS: ubuntu-22.04
|
||||||
- TARGET: armv7-linux-androideabi
|
ARTIFACT_NAME: android
|
||||||
ARCH: armv7
|
runs-on: ${{ matrix.OS }}
|
||||||
- TARGET: i686-linux-android
|
|
||||||
ARCH: i686
|
|
||||||
- TARGET: x86_64-linux-android
|
|
||||||
ARCH: x86_64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
env:
|
||||||
NAME: easytier
|
NAME: easytier
|
||||||
TARGET: ${{ matrix.TARGET }}
|
TARGET: ${{ matrix.TARGET }}
|
||||||
ARCH: ${{ matrix.ARCH }}
|
OS: ${{ matrix.OS }}
|
||||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: needs.pre_job.outputs.should_skip != 'true'
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
@@ -72,41 +61,47 @@ jobs:
|
|||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
cmdline-tools-version: 12.0
|
cmdline-tools-version: 11076708
|
||||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 '
|
packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
|
||||||
|
|
||||||
- name: Setup Android Environment
|
- name: Setup Android Environment
|
||||||
run: |
|
run: |
|
||||||
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
||||||
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $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
|
||||||
|
|
||||||
- name: Prepare build environment
|
- name: Setup Frontend Environment
|
||||||
uses: ./.github/actions/prepare-build
|
uses: ./.github/actions/prepare-pnpm
|
||||||
with:
|
|
||||||
target: ${{ matrix.TARGET }}
|
|
||||||
gui: false
|
|
||||||
pnpm: true
|
|
||||||
pnpm-build-filter: ''
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
# The prefix cache key, this can be changed to start a new cache manually.
|
# The prefix cache key, this can be changed to start a new cache manually.
|
||||||
# default: "v0-rust"
|
# default: "v0-rust"
|
||||||
prefix-key: ""
|
prefix-key: ""
|
||||||
shared-key: "gui-registry"
|
|
||||||
cache-targets: "false"
|
|
||||||
|
|
||||||
- name: Build
|
- 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
|
||||||
run: |
|
run: |
|
||||||
cd easytier-gui
|
cd easytier-gui
|
||||||
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
|
pnpm tauri android build
|
||||||
|
|
||||||
- name: Collect artifact
|
- name: Compress
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./artifacts/objects/
|
mkdir -p ./artifacts/objects/
|
||||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
|
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
|
||||||
|
|
||||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||||
TAG=$GITHUB_REF_NAME
|
TAG=$GITHUB_REF_NAME
|
||||||
@@ -114,21 +109,23 @@ jobs:
|
|||||||
TAG=$GITHUB_SHA
|
TAG=$GITHUB_SHA
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv ./artifacts/objects/* ./artifacts/
|
mv ./artifacts/objects/* ./artifacts
|
||||||
rm -rf ./artifacts/objects/
|
rm -rf ./artifacts/objects/
|
||||||
|
|
||||||
- name: Archive artifact
|
- name: Archive artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: easytier-mobile-android-${{ matrix.ARCH }}
|
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||||
path: |
|
path: |
|
||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
mobile-result:
|
mobile-result:
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ pre_job, build-mobile ]
|
needs:
|
||||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
- pre_job
|
||||||
|
- build-mobile
|
||||||
steps:
|
steps:
|
||||||
- name: Mark result as failed
|
- name: Mark result as failed
|
||||||
if: contains(needs.*.result, 'failure')
|
if: needs.build-mobile.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
@@ -6,22 +6,14 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "**/*.nix"
|
- "**/*.nix"
|
||||||
- "flake.lock"
|
- "flake.lock"
|
||||||
- "rust-toolchain.toml"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main", "develop"]
|
branches: ["main", "develop"]
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
paths:
|
paths:
|
||||||
- "**/*.nix"
|
- "**/*.nix"
|
||||||
- "flake.lock"
|
- "flake.lock"
|
||||||
- "rust-toolchain.toml"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-full-shell:
|
check-full-shell:
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -34,11 +26,5 @@ jobs:
|
|||||||
- name: Magic Nix Cache
|
- name: Magic Nix Cache
|
||||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||||
|
|
||||||
- name: Warm up full devShell
|
- name: Check full devShell
|
||||||
run: nix develop .#full --command true
|
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
|
|
||||||
|
|||||||
+12
-33
@@ -8,13 +8,8 @@ on:
|
|||||||
- '!*-pre'
|
- '!*-pre'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["develop", "main"]
|
branches: ["develop", "main"]
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
@@ -25,29 +20,18 @@ defaults:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cargo_fmt_check:
|
cargo_fmt_check:
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
- name: fmt check
|
||||||
- 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
|
working-directory: ./easytier-contrib/easytier-ohrs
|
||||||
run: cargo fmt --all -- --check
|
run: |
|
||||||
|
bash ../../.github/workflows/install_rust.sh
|
||||||
|
rustup component add rustfmt
|
||||||
|
cargo fmt --all -- --check
|
||||||
pre_job:
|
pre_job:
|
||||||
# continue-on-error: true # Uncomment once integration is finished
|
# continue-on-error: true # Uncomment once integration is finished
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
|
||||||
# Map a step output to a job output
|
# Map a step output to a job output
|
||||||
outputs:
|
outputs:
|
||||||
# do not skip push on branch starts with releases/
|
# do not skip push on branch starts with releases/
|
||||||
@@ -60,8 +44,7 @@ jobs:
|
|||||||
concurrent_skipping: "same_content_newer"
|
concurrent_skipping: "same_content_newer"
|
||||||
skip_after_successful_duplicate: "true"
|
skip_after_successful_duplicate: "true"
|
||||||
cancel_others: "true"
|
cancel_others: "true"
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/actions/**"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
|
||||||
|
|
||||||
build-ohos:
|
build-ohos:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: pre_job
|
needs: pre_job
|
||||||
@@ -73,12 +56,13 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -qq \
|
sudo apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
wget \
|
wget \
|
||||||
unzip \
|
unzip \
|
||||||
git \
|
git \
|
||||||
pkg-config curl libgl1-mesa-dev expect
|
pkg-config curl libgl1-mesa-dev expect
|
||||||
|
sudo apt-get clean
|
||||||
|
|
||||||
- name: Resolve easytier version
|
- name: Resolve easytier version
|
||||||
run: |
|
run: |
|
||||||
@@ -150,15 +134,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
|
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
|
- name: Create clang wrapper script
|
||||||
run: |
|
run: |
|
||||||
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
||||||
@@ -177,7 +152,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y llvm clang lldb lld
|
sudo apt-get install -y llvm clang lldb lld
|
||||||
sudo apt-get install -y protobuf-compiler
|
sudo apt-get install -y protobuf-compiler
|
||||||
|
bash ../../.github/workflows/install_rust.sh
|
||||||
source env.sh
|
source env.sh
|
||||||
|
cargo install ohrs
|
||||||
|
rustup target add aarch64-unknown-linux-ohos
|
||||||
|
cargo update easytier
|
||||||
ohrs doctor
|
ohrs doctor
|
||||||
ohrs build --release --arch aarch
|
ohrs build --release --arch aarch
|
||||||
ohrs artifact
|
ohrs artifact
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ on:
|
|||||||
version:
|
version:
|
||||||
description: 'Version for this release'
|
description: 'Version for this release'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.6.4'
|
default: 'v2.6.0'
|
||||||
required: true
|
required: true
|
||||||
make_latest:
|
make_latest:
|
||||||
description: 'Mark this release as latest'
|
description: 'Mark this release as latest'
|
||||||
|
|||||||
+19
-28
@@ -6,10 +6,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "develop", "main" ]
|
branches: [ "develop", "main" ]
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
# RUSTC_WRAPPER: "sccache"
|
# RUSTC_WRAPPER: "sccache"
|
||||||
@@ -34,7 +30,7 @@ jobs:
|
|||||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||||
concurrent_skipping: 'never'
|
concurrent_skipping: 'never'
|
||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
|
||||||
|
|
||||||
check:
|
check:
|
||||||
name: Run linters & check
|
name: Run linters & check
|
||||||
@@ -48,36 +44,35 @@ jobs:
|
|||||||
uses: ./.github/actions/prepare-build
|
uses: ./.github/actions/prepare-build
|
||||||
with:
|
with:
|
||||||
gui: true
|
gui: true
|
||||||
pnpm: true
|
web: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
|
||||||
components: rustfmt,clippy
|
- name: Install rustfmt and clippy
|
||||||
rustflags: ''
|
run: |
|
||||||
|
rustup component add rustfmt
|
||||||
|
rustup component add clippy
|
||||||
|
|
||||||
- uses: taiki-e/install-action@cargo-hack
|
- uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
|
- name: Check Cargo.lock is up to date
|
||||||
|
run: |
|
||||||
|
if ! cargo metadata --format-version 1 --locked --no-deps > /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
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
- name: Check Clippy
|
- name: Check Clippy
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
run: cargo clippy --all-targets --features full --all -- -D warnings
|
run: cargo clippy --all-targets --features full --all -- -D warnings
|
||||||
|
|
||||||
- name: Check features
|
- name: Check features
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
|
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:
|
pre-test:
|
||||||
name: Build test
|
name: Build test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -90,7 +85,7 @@ jobs:
|
|||||||
uses: ./.github/actions/prepare-build
|
uses: ./.github/actions/prepare-build
|
||||||
with:
|
with:
|
||||||
gui: true
|
gui: true
|
||||||
pnpm: true
|
web: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
@@ -128,10 +123,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup tools for test
|
- name: Setup tools for test
|
||||||
run: sudo apt install bridge-utils
|
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
|
- name: Setup system for test
|
||||||
run: |
|
run: |
|
||||||
@@ -155,9 +146,9 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ pre_job, check, test_matrix ]
|
needs: [ pre_job, test_matrix ]
|
||||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
steps:
|
steps:
|
||||||
- name: Mark result as failed
|
- name: Mark result as failed
|
||||||
if: contains(needs.*.result, 'failure')
|
if: needs.test_matrix.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides
|
|||||||
#### Required Tools
|
#### Required Tools
|
||||||
- Node.js v21 or higher
|
- Node.js v21 or higher
|
||||||
- pnpm v9 or higher
|
- pnpm v9 or higher
|
||||||
- Rust toolchain (version 1.95)
|
- Rust toolchain (version 1.93)
|
||||||
- LLVM and Clang
|
- LLVM and Clang
|
||||||
- Protoc (Protocol Buffers compiler)
|
- Protoc (Protocol Buffers compiler)
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ sudo apt install -y bridge-utils
|
|||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
rustup install 1.95
|
rustup install 1.93
|
||||||
rustup default 1.95
|
rustup default 1.93
|
||||||
|
|
||||||
# Install project dependencies
|
# Install project dependencies
|
||||||
pnpm -r install
|
pnpm -r install
|
||||||
|
|||||||
+3
-3
@@ -34,7 +34,7 @@
|
|||||||
#### 必需工具
|
#### 必需工具
|
||||||
- Node.js v21 或更高版本
|
- Node.js v21 或更高版本
|
||||||
- pnpm v9 或更高版本
|
- pnpm v9 或更高版本
|
||||||
- Rust 工具链(版本 1.95)
|
- Rust 工具链(版本 1.93)
|
||||||
- LLVM 和 Clang
|
- LLVM 和 Clang
|
||||||
- Protoc(Protocol Buffers 编译器)
|
- Protoc(Protocol Buffers 编译器)
|
||||||
|
|
||||||
@@ -87,8 +87,8 @@ sudo apt install -y bridge-utils
|
|||||||
2. 安装依赖:
|
2. 安装依赖:
|
||||||
```bash
|
```bash
|
||||||
# 安装 Rust 工具链
|
# 安装 Rust 工具链
|
||||||
rustup install 1.95
|
rustup install 1.93
|
||||||
rustup default 1.95
|
rustup default 1.93
|
||||||
|
|
||||||
# 安装项目依赖
|
# 安装项目依赖
|
||||||
pnpm -r install
|
pnpm -r install
|
||||||
|
|||||||
Generated
+1093
-1665
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,6 @@ exclude = [
|
|||||||
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
|
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
edition = "2024"
|
|
||||||
rust-version = "1.95"
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
debug = 2
|
debug = 2
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ After successful execution, you can check the network status using `easytier-cli
|
|||||||
```text
|
```text
|
||||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
| 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.6.2-70e69a38~ |
|
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ |
|
||||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 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.0-70e69a38~ |
|
||||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ |
|
||||||
```
|
```
|
||||||
|
|
||||||
You can test connectivity between nodes:
|
You can test connectivity between nodes:
|
||||||
|
|||||||
+3
-3
@@ -108,9 +108,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享
|
|||||||
```text
|
```text
|
||||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
| 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.6.2-70e69a38~ |
|
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ |
|
||||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 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.0-70e69a38~ |
|
||||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ |
|
||||||
```
|
```
|
||||||
|
|
||||||
您可以测试节点之间的连通性:
|
您可以测试节点之间的连通性:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-android-jni"
|
name = "easytier-android-jni"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||||
use jni::JNIEnv;
|
|
||||||
use jni::objects::{JClass, JObjectArray, JString};
|
use jni::objects::{JClass, JObjectArray, JString};
|
||||||
use jni::sys::{jint, jstring};
|
use jni::sys::{jint, jstring};
|
||||||
|
use jni::JNIEnv;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
@@ -15,7 +15,7 @@ pub struct KeyValuePair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 声明外部 C 函数
|
// 声明外部 C 函数
|
||||||
unsafe extern "C" {
|
extern "C" {
|
||||||
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
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 get_error_msg(out: *mut *const std::ffi::c_char);
|
||||||
fn free_string(s: *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 文件描述符
|
/// 设置 TUN 文件描述符
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||||
mut env: JNIEnv,
|
mut env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
@@ -87,17 +87,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
|||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 解析配置
|
/// 解析配置
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||||
mut env: JNIEnv,
|
mut env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
@@ -115,17 +115,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
|||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = parse_config(config_cstr.as_ptr());
|
let result = parse_config(config_cstr.as_ptr());
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 运行网络实例
|
/// 运行网络实例
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||||
mut env: JNIEnv,
|
mut env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
@@ -143,17 +143,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
|||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = run_network_instance(config_cstr.as_ptr());
|
let result = run_network_instance(config_cstr.as_ptr());
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 保持网络实例
|
/// 保持网络实例
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||||
mut env: JNIEnv,
|
mut env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
@@ -165,11 +165,11 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
|||||||
if instance_names.is_null() {
|
if instance_names.is_null() {
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = retain_network_instance(ptr::null(), 0);
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,11 +187,11 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
|||||||
if array_length == 0 {
|
if array_length == 0 {
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = retain_network_instance(ptr::null(), 0);
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,17 +234,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
|||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||||
if result != 0
|
if result != 0 {
|
||||||
&& let Some(error) = get_last_error()
|
if let Some(error) = get_last_error() {
|
||||||
{
|
|
||||||
throw_exception(&mut env, &error);
|
throw_exception(&mut env, &error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 收集网络信息
|
/// 收集网络信息
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||||
mut env: JNIEnv,
|
mut env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
@@ -304,7 +304,7 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取最后的错误信息
|
/// 获取最后的错误信息
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||||
env: JNIEnv,
|
env: JNIEnv,
|
||||||
_class: JClass,
|
_class: JClass,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-ffi"
|
name = "easytier-ffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ fn set_error_msg(msg: &str) {
|
|||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Set the tun fd
|
/// Set the tun fd
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn set_tun_fd(
|
pub unsafe extern "C" fn set_tun_fd(
|
||||||
inst_name: *const std::ffi::c_char,
|
inst_name: *const std::ffi::c_char,
|
||||||
fd: std::ffi::c_int,
|
fd: std::ffi::c_int,
|
||||||
@@ -59,7 +59,7 @@ pub unsafe extern "C" fn set_tun_fd(
|
|||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Get the last error message
|
/// Get the last error message
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||||
let msg_buf = ERROR_MSG.lock().unwrap();
|
let msg_buf = ERROR_MSG.lock().unwrap();
|
||||||
if msg_buf.is_empty() {
|
if msg_buf.is_empty() {
|
||||||
@@ -74,7 +74,7 @@ pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||||
if s.is_null() {
|
if s.is_null() {
|
||||||
return;
|
return;
|
||||||
@@ -86,7 +86,7 @@ pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
|||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Parse the config
|
/// Parse the config
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
let cfg_str = unsafe {
|
let cfg_str = unsafe {
|
||||||
assert!(!cfg_str.is_null());
|
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
|
/// # Safety
|
||||||
/// Run the network instance
|
/// Run the network instance
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
let cfg_str = unsafe {
|
let cfg_str = unsafe {
|
||||||
assert!(!cfg_str.is_null());
|
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
|
/// # Safety
|
||||||
/// Retain the network instance
|
/// Retain the network instance
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn retain_network_instance(
|
pub unsafe extern "C" fn retain_network_instance(
|
||||||
inst_names: *const *const std::ffi::c_char,
|
inst_names: *const *const std::ffi::c_char,
|
||||||
length: usize,
|
length: usize,
|
||||||
@@ -188,7 +188,7 @@ pub unsafe extern "C" fn retain_network_instance(
|
|||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// Collect the network infos
|
/// Collect the network infos
|
||||||
#[unsafe(no_mangle)]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn collect_network_infos(
|
pub unsafe extern "C" fn collect_network_infos(
|
||||||
infos: *mut KeyValuePair,
|
infos: *mut KeyValuePair,
|
||||||
max_length: usize,
|
max_length: usize,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id=easytier_magisk
|
id=easytier_magisk
|
||||||
name=EasyTier_Magisk
|
name=EasyTier_Magisk
|
||||||
version=v2.6.4
|
version=v2.6.0
|
||||||
versionCode=1
|
versionCode=1
|
||||||
author=EasyTier
|
author=EasyTier
|
||||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-uptime"
|
name = "easytier-uptime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
@@ -12,7 +12,6 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
guarden = "0.1"
|
|
||||||
|
|
||||||
# Axum web framework
|
# Axum web framework
|
||||||
axum = { version = "0.8.4", features = ["macros"] }
|
axum = { version = "0.8.4", features = ["macros"] }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::ops::{Div, Mul};
|
use std::ops::{Div, Mul};
|
||||||
|
|
||||||
use axum::Json;
|
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
|
use axum::Json;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
||||||
QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
|
QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
|
||||||
@@ -14,7 +14,7 @@ use crate::api::{
|
|||||||
models::*,
|
models::*,
|
||||||
};
|
};
|
||||||
use crate::db::entity::{self, health_records, shared_nodes};
|
use crate::db::entity::{self, health_records, shared_nodes};
|
||||||
use crate::db::{Db, operations::*};
|
use crate::db::{operations::*, Db};
|
||||||
use crate::health_checker_manager::HealthCheckerManager;
|
use crate::health_checker_manager::HealthCheckerManager;
|
||||||
use axum_extra::extract::Query;
|
use axum_extra::extract::Query;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -273,7 +273,7 @@ pub struct InstanceFilterParams {
|
|||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -370,9 +370,8 @@ pub async fn admin_get_nodes(
|
|||||||
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
||||||
filtered_ids = Some(ids);
|
filtered_ids = Some(ids);
|
||||||
}
|
}
|
||||||
if let Some(tags) = filters.tags
|
if let Some(tags) = filters.tags {
|
||||||
&& !tags.is_empty()
|
if !tags.is_empty() {
|
||||||
{
|
|
||||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||||
filtered_ids = match filtered_ids {
|
filtered_ids = match filtered_ids {
|
||||||
Some(mut existing) => {
|
Some(mut existing) => {
|
||||||
@@ -384,6 +383,7 @@ pub async fn admin_get_nodes(
|
|||||||
None => Some(ids_any),
|
None => Some(ids_any),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let Some(ids) = filtered_ids {
|
if let Some(ids) = filtered_ids {
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::Router;
|
|
||||||
use axum::routing::{delete, get, post, put};
|
use axum::routing::{delete, get, post, put};
|
||||||
|
use axum::Router;
|
||||||
use tower_http::compression::CompressionLayer;
|
use tower_http::compression::CompressionLayer;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::db::Db;
|
|
||||||
use crate::db::entity::*;
|
use crate::db::entity::*;
|
||||||
|
use crate::db::Db;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{sleep, Duration};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
/// 数据清理策略配置
|
/// 数据清理策略配置
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ pub mod operations;
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
prelude::*, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||||
SqlxSqliteConnector, Statement, TransactionTrait as _, prelude::*, sea_query::OnConflict,
|
QueryFilter as _, Set, SqlxSqliteConnector, Statement, TransactionTrait as _,
|
||||||
};
|
};
|
||||||
use sea_orm_migration::MigratorTrait as _;
|
use sea_orm_migration::MigratorTrait as _;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _};
|
use sqlx::{migrate::MigrateDatabase as _, Sqlite, SqlitePool};
|
||||||
|
|
||||||
use crate::migrator;
|
use crate::migrator;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::api::CreateNodeRequest;
|
use crate::api::CreateNodeRequest;
|
||||||
|
use crate::db::entity::*;
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
use crate::db::HealthStats;
|
use crate::db::HealthStats;
|
||||||
use crate::db::HealthStatus;
|
use crate::db::HealthStatus;
|
||||||
use crate::db::entity::*;
|
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ use std::{
|
|||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::{
|
common::{
|
||||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
|
||||||
|
scoped_task::ScopedTask,
|
||||||
},
|
},
|
||||||
|
defer,
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
};
|
};
|
||||||
use guarden::defer;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::any;
|
use sqlx::any;
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
|
||||||
use tracing::{debug, error, info, instrument, warn};
|
use tracing::{debug, error, info, instrument, warn};
|
||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
Db, HealthStatus,
|
|
||||||
entity::shared_nodes,
|
entity::shared_nodes,
|
||||||
operations::{HealthOperations, NodeOperations},
|
operations::{HealthOperations, NodeOperations},
|
||||||
|
Db, HealthStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct HealthCheckOneNode {
|
pub struct HealthCheckOneNode {
|
||||||
@@ -240,7 +240,7 @@ pub struct HealthChecker {
|
|||||||
db: Db,
|
db: Db,
|
||||||
instance_mgr: Arc<NetworkInstanceManager>,
|
instance_mgr: Arc<NetworkInstanceManager>,
|
||||||
inst_id_map: DashMap<i32, uuid::Uuid>,
|
inst_id_map: DashMap<i32, uuid::Uuid>,
|
||||||
node_tasks: DashMap<i32, AbortOnDropHandle<()>>,
|
node_tasks: DashMap<i32, ScopedTask<()>>,
|
||||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||||
node_cfg: Arc<DashMap<i32, TomlConfigLoader>>,
|
node_cfg: Arc<DashMap<i32, TomlConfigLoader>>,
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,7 @@ impl HealthChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启动健康检查任务
|
// 启动健康检查任务
|
||||||
let task = AbortOnDropHandle::new(tokio::spawn(Self::node_health_check_task(
|
let task = ScopedTask::from(tokio::spawn(Self::node_health_check_task(
|
||||||
node_id,
|
node_id,
|
||||||
cfg.get_id(),
|
cfg.get_id(),
|
||||||
Arc::clone(&self.instance_mgr),
|
Arc::clone(&self.instance_mgr),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
use std::{collections::HashSet, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use tokio::time::{Interval, interval};
|
use tokio::time::{interval, Interval};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{Db, entity::shared_nodes, operations::NodeOperations},
|
db::{entity::shared_nodes, operations::NodeOperations, Db},
|
||||||
health_checker::HealthChecker,
|
health_checker::HealthChecker,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ mod migrator;
|
|||||||
use api::routes::create_routes;
|
use api::routes::create_routes;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use config::AppConfig;
|
use config::AppConfig;
|
||||||
use db::{Db, operations::NodeOperations};
|
use db::{operations::NodeOperations, Db};
|
||||||
use easytier::common::log;
|
use easytier::common::log;
|
||||||
use health_checker::HealthChecker;
|
use health_checker::HealthChecker;
|
||||||
use health_checker_manager::HealthCheckerManager;
|
use health_checker_manager::HealthCheckerManager;
|
||||||
@@ -49,10 +49,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
// 如果提供了管理员密码,设置环境变量
|
// 如果提供了管理员密码,设置环境变量
|
||||||
if let Some(password) = args.admin_password {
|
if let Some(password) = args.admin_password {
|
||||||
unsafe {
|
|
||||||
env::set_var("ADMIN_PASSWORD", password);
|
env::set_var("ADMIN_PASSWORD", password);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Admin password configured: {}",
|
"Admin password configured: {}",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "easytier-gui",
|
"name": "easytier-gui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.6.4",
|
"version": "2.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.6.4"
|
version = "2.6.0"
|
||||||
description = "EasyTier GUI"
|
description = "EasyTier GUI"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -11,6 +11,15 @@ edition.workspace = true
|
|||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
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]
|
[dependencies]
|
||||||
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
||||||
@@ -57,14 +66,6 @@ libc = "0.2"
|
|||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
security-framework-sys = "2.9.0"
|
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]
|
[features]
|
||||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
fn main() {
|
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
|
// 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") {
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("aarch64")
|
||||||
|
{
|
||||||
thunk::thunk();
|
thunk::thunk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@@ -15,35 +14,6 @@ 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 {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
namespace = "com.kkrainbow.easytier"
|
namespace = "com.kkrainbow.easytier"
|
||||||
@@ -52,8 +22,8 @@ android {
|
|||||||
applicationId = "com.kkrainbow.easytier"
|
applicationId = "com.kkrainbow.easytier"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = tauriVersionCode
|
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||||
versionName = tauriVersionName
|
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
use super::Command;
|
use super::Command;
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{anyhow, Result};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::process::{Command as StdCommand, Output};
|
use std::process::{Command as StdCommand, Output};
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ use std::os::unix::process::ExitStatusExt;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
|
|
||||||
use libc::{EINTR, SHUT_WR, fileno, wait};
|
use libc::{fileno, wait, EINTR, SHUT_WR};
|
||||||
use security_framework_sys::authorization::{
|
use security_framework_sys::authorization::{
|
||||||
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
|
|
||||||
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
||||||
|
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ENV_PATH: &str = "PATH";
|
const ENV_PATH: &str = "PATH";
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ use std::process::{ExitStatus, Output};
|
|||||||
use winapi::shared::minwindef::{DWORD, LPVOID};
|
use winapi::shared::minwindef::{DWORD, LPVOID};
|
||||||
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
|
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
|
||||||
use winapi::um::securitybaseapi::GetTokenInformation;
|
use winapi::um::securitybaseapi::GetTokenInformation;
|
||||||
use winapi::um::winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
|
use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||||
|
use windows::core::{w, HSTRING, PCWSTR};
|
||||||
use windows::Win32::Foundation::HWND;
|
use windows::Win32::Foundation::HWND;
|
||||||
use windows::Win32::UI::Shell::ShellExecuteW;
|
use windows::Win32::UI::Shell::ShellExecuteW;
|
||||||
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
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
|
/// The implementation of state check and elevated executing varies on each platform
|
||||||
impl Command {
|
impl Command {
|
||||||
|
|||||||
@@ -15,18 +15,16 @@ use easytier::rpc_service::remote_client::{
|
|||||||
use easytier::web_client::{self, WebClient};
|
use easytier::web_client::{self, WebClient};
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::{
|
common::{
|
||||||
config::{
|
config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||||
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
|
||||||
},
|
|
||||||
log,
|
log,
|
||||||
},
|
},
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
launcher::NetworkConfig,
|
launcher::NetworkConfig,
|
||||||
rpc_service::ApiRpcServer,
|
rpc_service::ApiRpcServer,
|
||||||
tunnel::TunnelListener,
|
|
||||||
tunnel::ring::RingTunnelListener,
|
tunnel::ring::RingTunnelListener,
|
||||||
tunnel::tcp::TcpTunnelListener,
|
tunnel::tcp::TcpTunnelListener,
|
||||||
utils::panic::setup_panic_handler,
|
tunnel::TunnelListener,
|
||||||
|
utils::{self},
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -120,7 +118,7 @@ async fn run_network_instance(
|
|||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User)
|
.pre_run_network_instance_hook(&app, &toml_config)
|
||||||
.await?;
|
.await?;
|
||||||
client_manager
|
client_manager
|
||||||
.handle_run_network_instance(app.clone(), cfg, save)
|
.handle_run_network_instance(app.clone(), cfg, save)
|
||||||
@@ -209,17 +207,13 @@ async fn update_network_config_state(
|
|||||||
.map_err(|e: uuid::Error| e.to_string())?;
|
.map_err(|e: uuid::Error| e.to_string())?;
|
||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
if !disabled {
|
if !disabled {
|
||||||
let (cfg, source) = client_manager
|
let cfg = client_manager
|
||||||
.handle_get_network_config_with_source(app.clone(), instance_id)
|
.handle_get_network_config(app.clone(), instance_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(
|
.pre_run_network_instance_hook(&app, &toml_config)
|
||||||
&app,
|
|
||||||
&toml_config,
|
|
||||||
manager::PersistedConfigSource::from_runtime_source(source),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
client_manager
|
client_manager
|
||||||
@@ -278,7 +272,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn load_configs(
|
async fn load_configs(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
configs: Vec<manager::StoredGuiConfig>,
|
configs: Vec<NetworkConfig>,
|
||||||
enabled_networks: Vec<String>,
|
enabled_networks: Vec<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
get_client_manager!()?
|
get_client_manager!()?
|
||||||
@@ -490,18 +484,10 @@ async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), Stri
|
|||||||
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
.ok_or_else(|| "Instance manager is not available".to_string())?;
|
||||||
|
|
||||||
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
|
||||||
let machine_id_state_dir = app
|
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.with_context(|| "Failed to resolve machine id state directory")
|
|
||||||
.map_err(|e| format!("{:#}", e))?;
|
|
||||||
|
|
||||||
let web_client = web_client::run_web_client(
|
let web_client = web_client::run_web_client(
|
||||||
url.as_str(),
|
url.as_str(),
|
||||||
easytier::common::MachineIdOptions {
|
None,
|
||||||
explicit_machine_id: None,
|
|
||||||
state_dir: Some(machine_id_state_dir),
|
|
||||||
},
|
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
instance_manager,
|
instance_manager,
|
||||||
@@ -573,11 +559,11 @@ fn toggle_window_visibility(app: &tauri::AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_exe_path() -> String {
|
fn get_exe_path() -> String {
|
||||||
if let Ok(appimage_path) = std::env::var("APPIMAGE")
|
if let Ok(appimage_path) = std::env::var("APPIMAGE") {
|
||||||
&& !appimage_path.is_empty()
|
if !appimage_path.is_empty() {
|
||||||
{
|
|
||||||
return appimage_path;
|
return appimage_path;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
std::env::current_exe()
|
std::env::current_exe()
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -610,8 +596,8 @@ mod manager {
|
|||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
use easytier::rpc_service::logger::LoggerRpcService;
|
use easytier::rpc_service::logger::LoggerRpcService;
|
||||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||||
use easytier::tunnel::TunnelConnector;
|
|
||||||
use easytier::tunnel::ring::RingTunnelConnector;
|
use easytier::tunnel::ring::RingTunnelConnector;
|
||||||
|
use easytier::tunnel::TunnelConnector;
|
||||||
use easytier::web_client::WebClientHooks;
|
use easytier::web_client::WebClientHooks;
|
||||||
|
|
||||||
pub(super) struct GuiHooks {
|
pub(super) struct GuiHooks {
|
||||||
@@ -626,11 +612,7 @@ mod manager {
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let client_manager = get_client_manager!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
client_manager
|
client_manager
|
||||||
.pre_run_network_instance_hook(
|
.pre_run_network_instance_hook(&self.app, cfg)
|
||||||
&self.app,
|
|
||||||
cfg,
|
|
||||||
PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,87 +631,14 @@ mod manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)]
|
#[derive(Clone)]
|
||||||
pub(super) struct GUIConfig {
|
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
||||||
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 {
|
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
||||||
fn get_network_inst_id(&self) -> &str {
|
fn get_network_inst_id(&self) -> &str {
|
||||||
&self.inst_id
|
&self.0
|
||||||
}
|
}
|
||||||
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
||||||
Ok(self.config.clone())
|
Ok(self.1.clone())
|
||||||
}
|
|
||||||
fn get_network_config_source(&self) -> ConfigSource {
|
|
||||||
self.source.to_runtime_source()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,12 +655,13 @@ mod manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||||
let configs = self
|
let configs: Result<Vec<String>, _> = self
|
||||||
.network_configs
|
.network_configs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| entry.value().clone().into_stored())
|
.map(|entry| serde_json::to_string(&entry.value().1))
|
||||||
.collect::<Vec<_>>();
|
.collect();
|
||||||
app.emit("save_configs", configs)?;
|
let payload = format!("[{}]", configs?.join(","));
|
||||||
|
app.emit_str("save_configs", payload)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,14 +680,8 @@ mod manager {
|
|||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
inst_id: Uuid,
|
inst_id: Uuid,
|
||||||
cfg: NetworkConfig,
|
cfg: NetworkConfig,
|
||||||
source: PersistedConfigSource,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let source = self
|
let config = GUIConfig(inst_id.to_string(), cfg);
|
||||||
.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.network_configs.insert(inst_id, config);
|
||||||
self.save_configs(app)
|
self.save_configs(app)
|
||||||
}
|
}
|
||||||
@@ -789,14 +693,8 @@ mod manager {
|
|||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
network_inst_id: Uuid,
|
network_inst_id: Uuid,
|
||||||
network_config: NetworkConfig,
|
network_config: NetworkConfig,
|
||||||
source: ConfigSource,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
self.save_config(
|
self.save_config(&app, network_inst_id, network_config)?;
|
||||||
&app,
|
|
||||||
network_inst_id,
|
|
||||||
network_config,
|
|
||||||
PersistedConfigSource::from_runtime_source(source),
|
|
||||||
)?;
|
|
||||||
self.enabled_networks.insert(network_inst_id);
|
self.enabled_networks.insert(network_inst_id);
|
||||||
self.save_enabled_networks(&app)?;
|
self.save_enabled_networks(&app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -913,36 +811,17 @@ mod manager {
|
|||||||
.network_configs
|
.network_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||||
.filter(|v| !v.config.no_tun())
|
.filter(|v| !v.1.no_tun())
|
||||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
.filter_map(|c| c.1.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")]
|
#[cfg(target_os = "android")]
|
||||||
pub(super) async fn disable_instances_with_tun(
|
pub(super) async fn disable_instances_with_tun(
|
||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
webhook_only: bool,
|
|
||||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||||
{
|
{
|
||||||
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
let inst_ids: Vec<uuid::Uuid> = self.get_enabled_instances_with_tun_ids().collect();
|
||||||
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 {
|
for inst_id in inst_ids {
|
||||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -963,7 +842,6 @@ mod manager {
|
|||||||
&self,
|
&self,
|
||||||
app: &AppHandle,
|
app: &AppHandle,
|
||||||
cfg: &easytier::common::config::TomlConfigLoader,
|
cfg: &easytier::common::config::TomlConfigLoader,
|
||||||
source: PersistedConfigSource,
|
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let instance_id = cfg.get_id();
|
let instance_id = cfg.get_id();
|
||||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||||
@@ -971,32 +849,16 @@ mod manager {
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
if !cfg.get_flags().no_tun {
|
if !cfg.get_flags().no_tun {
|
||||||
match source {
|
self.disable_instances_with_tun(app)
|
||||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
|
||||||
self.disable_instances_with_tun(app, false)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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
|
self.storage
|
||||||
.save_config(
|
.save_config(
|
||||||
app,
|
app,
|
||||||
instance_id,
|
instance_id,
|
||||||
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
||||||
source,
|
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -1100,15 +962,15 @@ mod manager {
|
|||||||
pub(super) async fn load_configs(
|
pub(super) async fn load_configs(
|
||||||
&self,
|
&self,
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
configs: Vec<StoredGuiConfig>,
|
configs: Vec<NetworkConfig>,
|
||||||
enabled_networks: Vec<String>,
|
enabled_networks: Vec<String>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
self.storage.network_configs.clear();
|
self.storage.network_configs.clear();
|
||||||
for stored in configs {
|
for cfg in configs {
|
||||||
let instance_id = stored.config.instance_id();
|
let instance_id = cfg.instance_id();
|
||||||
self.storage.network_configs.insert(
|
self.storage.network_configs.insert(
|
||||||
instance_id.parse()?,
|
instance_id.parse()?,
|
||||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
GUIConfig(instance_id.to_string(), cfg),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,19 +979,18 @@ mod manager {
|
|||||||
.get_rpc_client(app.clone())
|
.get_rpc_client(app.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
||||||
for id in enabled_networks {
|
for id in enabled_networks {
|
||||||
if let Ok(uuid) = id.parse()
|
if let Ok(uuid) = id.parse() {
|
||||||
&& !self.storage.enabled_networks.contains(&uuid)
|
if !self.storage.enabled_networks.contains(&uuid) {
|
||||||
{
|
|
||||||
let config = self
|
let config = self
|
||||||
.storage
|
.storage
|
||||||
.network_configs
|
.network_configs
|
||||||
.get(&uuid)
|
.get(&uuid)
|
||||||
.map(|i| (i.value().config.clone(), i.value().source));
|
.map(|i| i.value().1.clone());
|
||||||
let Some((config, source)) = config else {
|
let Some(config) = config else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let toml_config = config.gen_config()?;
|
let toml_config = config.gen_config()?;
|
||||||
self.pre_run_network_instance_hook(&app, &toml_config, source)
|
self.pre_run_network_instance_hook(&app, &toml_config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
client
|
client
|
||||||
@@ -1139,7 +1000,6 @@ mod manager {
|
|||||||
inst_id: None,
|
inst_id: None,
|
||||||
config: Some(config),
|
config: Some(config),
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
source: source.to_runtime_source().to_rpc(),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1148,6 +1008,7 @@ mod manager {
|
|||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
.map_err(|e| anyhow::anyhow!(e))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1171,44 +1032,6 @@ mod manager {
|
|||||||
&self.storage
|
&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"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
@@ -1297,7 +1120,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
|||||||
process::exit(0);
|
process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_panic_handler();
|
utils::setup_panic_handler();
|
||||||
|
|
||||||
let mut builder = tauri::Builder::default();
|
let mut builder = tauri::Builder::default();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"createUpdaterArtifacts": false
|
"createUpdaterArtifacts": false
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "2.6.4",
|
"version": "2.6.0",
|
||||||
"identifier": "com.kkrainbow.easytier",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist
|
|||||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||||
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
|
||||||
interface ServiceOptions {
|
interface ServiceOptions {
|
||||||
config_dir: string
|
config_dir: string
|
||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
@@ -17,39 +16,6 @@ interface ServiceOptions {
|
|||||||
|
|
||||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
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) {
|
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||||
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||||
}
|
}
|
||||||
@@ -105,12 +71,9 @@ export async function getConfig(instanceId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendConfigs(enabledNetworks: string[]) {
|
export async function sendConfigs(enabledNetworks: string[]) {
|
||||||
const networkList = parseStoredConfigs(localStorage.getItem('networkList'))
|
const networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||||
return await invoke('load_configs', {
|
return await invoke('load_configs', {
|
||||||
configs: networkList.map(({ config, source }) => ({
|
configs: networkList.map((config) => NetworkTypes.toBackendNetworkConfig(NetworkTypes.normalizeNetworkConfig(config))),
|
||||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
|
||||||
source,
|
|
||||||
})),
|
|
||||||
enabledNetworks
|
enabledNetworks
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ import { type } from "@tauri-apps/plugin-os";
|
|||||||
import { NetworkTypes } from "easytier-frontend-lib"
|
import { NetworkTypes } from "easytier-frontend-lib"
|
||||||
import { Utils } from "easytier-frontend-lib";
|
import { Utils } from "easytier-frontend-lib";
|
||||||
|
|
||||||
interface StoredGuiConfig {
|
|
||||||
config: NetworkTypes.NetworkConfig
|
|
||||||
source?: 'user' | 'webhook' | 'legacy'
|
|
||||||
}
|
|
||||||
|
|
||||||
const EVENTS = Object.freeze({
|
const EVENTS = Object.freeze({
|
||||||
SAVE_CONFIGS: 'save_configs',
|
SAVE_CONFIGS: 'save_configs',
|
||||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||||
@@ -18,15 +13,9 @@ const EVENTS = Object.freeze({
|
|||||||
EVENT_LAGGED: 'event_lagged',
|
EVENT_LAGGED: 'event_lagged',
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||||
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
||||||
localStorage.setItem(
|
localStorage.setItem('networkList', JSON.stringify(event.payload.map((config) => NetworkTypes.normalizeNetworkConfig(config))));
|
||||||
'networkList',
|
|
||||||
JSON.stringify(event.payload.map(({ config, source }) => ({
|
|
||||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
|
||||||
source: source ?? 'legacy',
|
|
||||||
}))),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeInstanceIdPayload(payload: unknown): string {
|
function normalizeInstanceIdPayload(payload: unknown): string {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface ServiceMode extends WebClientConfig {
|
|||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||||
file_log_dir: string
|
file_log_dir: string
|
||||||
installed_core_version?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteMode {
|
export interface RemoteMode {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useToast, useConfirm } from 'primevue'
|
|||||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||||
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
||||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||||
import { getEasytierVersion, getServiceStatus } from '~/composables/backend'
|
import { getServiceStatus } from '~/composables/backend'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
@@ -85,20 +85,6 @@ 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() {
|
async function onStopService() {
|
||||||
isModeSaving.value = true
|
isModeSaving.value = true
|
||||||
manualDisconnect.value = true
|
manualDisconnect.value = true
|
||||||
@@ -148,14 +134,13 @@ async function initWithMode(mode: Mode) {
|
|||||||
}
|
}
|
||||||
url = mode.remote_rpc_address
|
url = mode.remote_rpc_address
|
||||||
break;
|
break;
|
||||||
case 'service': {
|
case 'service':
|
||||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
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 })
|
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||||
return initWithMode({ ...mode, mode: 'normal' });
|
return initWithMode({ ...mode, mode: 'normal' });
|
||||||
}
|
}
|
||||||
let serviceStatus = await getServiceStatus()
|
let serviceStatus = await getServiceStatus()
|
||||||
const coreVersion = await getEasytierVersion()
|
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||||
if (serviceStatus === "NotInstalled" || modeConfigChanged(mode) || mode.installed_core_version !== coreVersion) {
|
|
||||||
mode.config_server_url = mode.config_server_url || undefined
|
mode.config_server_url = mode.config_server_url || undefined
|
||||||
await initService({
|
await initService({
|
||||||
config_dir: mode.config_dir,
|
config_dir: mode.config_dir,
|
||||||
@@ -164,7 +149,6 @@ async function initWithMode(mode: Mode) {
|
|||||||
rpc_portal: mode.rpc_portal,
|
rpc_portal: mode.rpc_portal,
|
||||||
config_server: mode.config_server_url,
|
config_server: mode.config_server_url,
|
||||||
})
|
})
|
||||||
mode.installed_core_version = coreVersion
|
|
||||||
serviceStatus = await getServiceStatus()
|
serviceStatus = await getServiceStatus()
|
||||||
}
|
}
|
||||||
if (serviceStatus === "Stopped") {
|
if (serviceStatus === "Stopped") {
|
||||||
@@ -173,7 +157,6 @@ async function initWithMode(mode: Mode) {
|
|||||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||||
retrys = 5
|
retrys = 5
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case 'normal':
|
case 'normal':
|
||||||
url = mode.rpc_portal;
|
url = mode.rpc_portal;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
name = "easytier-rpc-build"
|
name = "easytier-rpc-build"
|
||||||
description = "Protobuf RPC Service Generator for EasyTier"
|
description = "Protobuf RPC Service Generator for EasyTier"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
homepage = "https://github.com/EasyTier/EasyTier"
|
homepage = "https://github.com/EasyTier/EasyTier"
|
||||||
repository = "https://github.com/EasyTier/EasyTier"
|
repository = "https://github.com/EasyTier/EasyTier"
|
||||||
authors = ["kkrainbow"]
|
authors = ["kkrainbow"]
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||||
categories = ["network-programming", "command-line-utilities"]
|
categories = ["network-programming", "command-line-utilities"]
|
||||||
|
rust-version = "1.93.0"
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.6.4"
|
version = "2.6.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -10,7 +10,6 @@ tracing = { version = "0.1", features = ["log"] }
|
|||||||
anyhow = { version = "1.0" }
|
anyhow = { version = "1.0" }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
|
||||||
dashmap = "6.1"
|
dashmap = "6.1"
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -70,11 +69,13 @@ subtle = "2.6"
|
|||||||
|
|
||||||
mimalloc = { version = "*" }
|
mimalloc = { version = "*" }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
|
||||||
"win7",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
embed = ["dep:axum-embed"]
|
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() {
|
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
|
// 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") {
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("aarch64")
|
||||||
|
{
|
||||||
thunk::thunk();
|
thunk::thunk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AutoComplete, Button, Checkbox, Dialog, Divider, InputNumber, InputText, Panel, Password, SelectButton, ToggleButton } from 'primevue'
|
|
||||||
import InputGroup from 'primevue/inputgroup'
|
import InputGroup from 'primevue/inputgroup'
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
|
import { Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
|
||||||
import {
|
import {
|
||||||
addRow,
|
addRow,
|
||||||
DEFAULT_NETWORK_CONFIG,
|
DEFAULT_NETWORK_CONFIG,
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '../types/network'
|
} from '../types/network'
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import AclManager from './acl/AclManager.vue'
|
|
||||||
import UrlListInput from './UrlListInput.vue'
|
import UrlListInput from './UrlListInput.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -81,7 +80,6 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'latency_first', help: 'latency_first_help' },
|
{ field: 'latency_first', help: 'latency_first_help' },
|
||||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||||
{ field: 'disable_ipv6', help: 'disable_ipv6_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: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||||
@@ -99,8 +97,6 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_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_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||||
{ field: 'enable_udp_broadcast_relay', help: 'enable_udp_broadcast_relay_help' },
|
|
||||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
|
||||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||||
@@ -492,18 +488,6 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</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">
|
<div class="flex pt-6 justify-center">
|
||||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||||
@click="$emit('runNetwork', curNetwork)" />
|
@click="$emit('runNetwork', curNetwork)" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||||
import InputGroup from 'primevue/inputgroup'
|
import InputGroup from 'primevue/inputgroup'
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -13,9 +13,26 @@ const props = defineProps<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const url = defineModel<string>({ required: true })
|
const url = defineModel<string>({ required: true })
|
||||||
const editing = ref(false)
|
const editing = ref(false)
|
||||||
|
const container = ref<HTMLElement | null>(null)
|
||||||
|
const internalCompact = ref(false)
|
||||||
const hostFocused = ref(false)
|
const hostFocused = ref(false)
|
||||||
|
|
||||||
const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
onMounted(() => {
|
||||||
|
if (container.value) {
|
||||||
|
const observer = new ResizeObserver(entries => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
internalCompact.value = entry.contentRect.width < 400
|
||||||
|
}
|
||||||
|
})
|
||||||
|
observer.observe(container.value)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseUrl = (val: string | null | undefined) => {
|
||||||
const getValidPort = (portStr: string, proto: string) => {
|
const getValidPort = (portStr: string, proto: string) => {
|
||||||
const p = parseInt(portStr)
|
const p = parseInt(portStr)
|
||||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||||
@@ -38,16 +55,13 @@ const parseUrl = (val: string | null | undefined): { proto: string; host: string
|
|||||||
if (ipv6End > 0) {
|
if (ipv6End > 0) {
|
||||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||||
// null = no explicit port in URL; do not fabricate a default
|
const port = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : (props.protos[proto] ?? 11010)
|
||||||
const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null
|
|
||||||
return { proto, host, port }
|
return { proto, host, port }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||||
// null = no explicit port in URL; buildUrlValue will omit the port entirely,
|
const port = portMatch ? parseInt(portMatch[2]) : (props.protos[proto] ?? 11010)
|
||||||
// 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 }
|
return { proto, host, port }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,26 +72,28 @@ const parseUrl = (val: string | null | undefined): { proto: string; host: string
|
|||||||
if (parsedByPattern) {
|
if (parsedByPattern) {
|
||||||
return parsedByPattern
|
return parsedByPattern
|
||||||
}
|
}
|
||||||
return { proto: 'tcp', host: '', port: null }
|
return { proto: 'tcp', host: '', port: 11010 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalValue = ref(parseUrl(url.value))
|
const internalValue = ref(parseUrl(url.value))
|
||||||
const defaultHost = '0.0.0.0'
|
const defaultHost = '0.0.0.0'
|
||||||
|
|
||||||
const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => {
|
const buildUrlValue = (value: { proto: string, host: string, port: number }, forceDefaultHost = false) => {
|
||||||
const proto = value.proto || 'tcp'
|
const proto = value.proto || 'tcp'
|
||||||
const rawHost = (value.host ?? '').trim()
|
const rawHost = (value.host ?? '').trim()
|
||||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||||
if (!host) {
|
if (!host) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// Omit port when the protocol uses no port (protos value = 0), or when the
|
let port = value.port
|
||||||
// original URL had no explicit port (port === null) – avoids overwriting an
|
if (isNaN(parseInt(port as any))) {
|
||||||
// implicit standard port (e.g. 443 for wss) with an EasyTier default (11012).
|
port = props.protos[proto] ?? 11010
|
||||||
if (props.protos[proto] === 0 || value.port === null) {
|
}
|
||||||
|
|
||||||
|
if (props.protos[proto] === 0) {
|
||||||
return `${proto}://${host}`
|
return `${proto}://${host}`
|
||||||
}
|
}
|
||||||
return `${proto}://${host}:${value.port}`
|
return `${proto}://${host}:${port}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||||
@@ -152,30 +168,27 @@ const onProtoChange = (newProto: string) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
<div ref="container" class="w-full">
|
||||||
<InputGroup class="url-input-full w-full min-w-0">
|
<InputGroup v-if="!internalCompact" class="w-full">
|
||||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||||
@update:model-value="onProtoChange" />
|
@update:model-value="onProtoChange" />
|
||||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
||||||
@focus="onHostFocus" @blur="onHostBlur" />
|
@focus="onHostFocus" @blur="onHostBlur" />
|
||||||
<template v-if="!isNoPortProto">
|
<template v-if="!isNoPortProto">
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span style="font-weight: bold">:</span>
|
<span style="font-weight: bold">:</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
fluid />
|
||||||
</template>
|
</template>
|
||||||
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<div
|
<div v-else class="flex justify-between items-center p-2 border rounded w-full">
|
||||||
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">{{ url }}</span>
|
||||||
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
<div class="flex items-center">
|
||||||
<div class="flex items-center shrink-0">
|
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" @click="editing = true" />
|
||||||
<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>
|
<slot name="actions"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,8 +207,7 @@ const onProtoChange = (newProto: string) => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||||
<label>{{ t('port') }}</label>
|
<label>{{ t('port') }}</label>
|
||||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full"
|
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full" />
|
||||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -207,28 +219,6 @@ const onProtoChange = (newProto: string) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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,
|
||||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -104,9 +104,6 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
|||||||
disable_ipv6: 禁用IPv6
|
disable_ipv6: 禁用IPv6
|
||||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||||
|
|
||||||
ipv6_public_addr_auto: 自动获取公网 IPv6
|
|
||||||
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
|
||||||
|
|
||||||
enable_kcp_proxy: 启用 KCP 代理
|
enable_kcp_proxy: 启用 KCP 代理
|
||||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||||
|
|
||||||
@@ -160,12 +157,6 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
|||||||
disable_udp_hole_punching: 禁用UDP打洞
|
disable_udp_hole_punching: 禁用UDP打洞
|
||||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||||
|
|
||||||
enable_udp_broadcast_relay: UDP 广播中继
|
|
||||||
enable_udp_broadcast_relay_help: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
|
||||||
|
|
||||||
disable_upnp: 禁用 UPnP
|
|
||||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
|
||||||
|
|
||||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||||
|
|
||||||
@@ -263,7 +254,6 @@ event:
|
|||||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||||
PortForwardAdded: 端口转发添加
|
PortForwardAdded: 端口转发添加
|
||||||
ProxyCidrsUpdated: 子网代理CIDR更新
|
ProxyCidrsUpdated: 子网代理CIDR更新
|
||||||
UdpBroadcastRelayStartResult: UDP广播中继启动结果
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
login:
|
login:
|
||||||
@@ -296,6 +286,9 @@ web:
|
|||||||
logout: 退出登录
|
logout: 退出登录
|
||||||
language: 语言
|
language: 语言
|
||||||
change_password: 修改密码
|
change_password: 修改密码
|
||||||
|
change_password_now: 立即修改密码
|
||||||
|
default_password_warning: 当前账号仍在使用系统默认密码。为保障安全,请部署完成后立即修改密码。
|
||||||
|
password_changed_relogin: 密码已修改,请重新登录。
|
||||||
|
|
||||||
device:
|
device:
|
||||||
list: 设备列表
|
list: 设备列表
|
||||||
@@ -365,12 +358,16 @@ web:
|
|||||||
delete: 删除
|
delete: 删除
|
||||||
edit: 编辑
|
edit: 编辑
|
||||||
refresh: 刷新
|
refresh: 刷新
|
||||||
add: 添加
|
|
||||||
loading: 加载中...
|
loading: 加载中...
|
||||||
error: 错误
|
error: 错误
|
||||||
success: 成功
|
success: 成功
|
||||||
warning: 警告
|
warning: 警告
|
||||||
info: 提示
|
info: 提示
|
||||||
|
password_empty: 密码不能为空
|
||||||
|
password_min_length: 密码至少需要 8 位
|
||||||
|
password_too_weak: 密码强度不足
|
||||||
|
password_mismatch: 两次输入的密码不一致
|
||||||
|
password_strength_hint: 密码至少 8 位,且需包含大小写字母、数字、特殊字符中的至少 2 类
|
||||||
enable: 开启
|
enable: 开启
|
||||||
disable: 关闭
|
disable: 关闭
|
||||||
address: 地址
|
address: 地址
|
||||||
@@ -433,46 +430,3 @@ config-server:
|
|||||||
client:
|
client:
|
||||||
not_running: 无法连接至远程客户端
|
not_running: 无法连接至远程客户端
|
||||||
retry: 重试
|
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: 匹配
|
|
||||||
|
|||||||
@@ -103,9 +103,6 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
|||||||
disable_ipv6: Disable IPv6
|
disable_ipv6: Disable IPv6
|
||||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
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: Enable KCP Proxy
|
||||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||||
|
|
||||||
@@ -159,12 +156,6 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
|||||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||||
disable_udp_hole_punching_help: Disable udp hole punching
|
disable_udp_hole_punching_help: Disable udp hole punching
|
||||||
|
|
||||||
enable_udp_broadcast_relay: UDP Broadcast Relay
|
|
||||||
enable_udp_broadcast_relay_help: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
|
||||||
|
|
||||||
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: 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
|
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||||
|
|
||||||
@@ -263,7 +254,6 @@ event:
|
|||||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||||
PortForwardAdded: PortForwardAdded
|
PortForwardAdded: PortForwardAdded
|
||||||
ProxyCidrsUpdated: ProxyCidrsUpdated
|
ProxyCidrsUpdated: ProxyCidrsUpdated
|
||||||
UdpBroadcastRelayStartResult: UDP Broadcast Relay Start Result
|
|
||||||
|
|
||||||
web:
|
web:
|
||||||
login:
|
login:
|
||||||
@@ -296,6 +286,9 @@ web:
|
|||||||
logout: Logout
|
logout: Logout
|
||||||
language: Language
|
language: Language
|
||||||
change_password: Change Password
|
change_password: Change Password
|
||||||
|
change_password_now: Change Password Now
|
||||||
|
default_password_warning: This account is still using the default password. Change it immediately after deployment to keep your instance secure.
|
||||||
|
password_changed_relogin: Password changed. Please log in again.
|
||||||
|
|
||||||
device:
|
device:
|
||||||
list: Device List
|
list: Device List
|
||||||
@@ -365,12 +358,16 @@ web:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
edit: Edit
|
edit: Edit
|
||||||
refresh: Refresh
|
refresh: Refresh
|
||||||
add: Add
|
|
||||||
loading: Loading...
|
loading: Loading...
|
||||||
error: Error
|
error: Error
|
||||||
success: Success
|
success: Success
|
||||||
warning: Warning
|
warning: Warning
|
||||||
info: Info
|
info: Info
|
||||||
|
password_empty: Password cannot be empty
|
||||||
|
password_min_length: Password must be at least 8 characters long
|
||||||
|
password_too_weak: Password is too weak
|
||||||
|
password_mismatch: Passwords do not match
|
||||||
|
password_strength_hint: Password must be at least 8 characters and include at least 2 of uppercase letters, lowercase letters, numbers, or special characters
|
||||||
enable: Enable
|
enable: Enable
|
||||||
disable: Disable
|
disable: Disable
|
||||||
address: Address
|
address: Address
|
||||||
@@ -433,46 +430,3 @@ config-server:
|
|||||||
client:
|
client:
|
||||||
not_running: Unable to connect to remote client.
|
not_running: Unable to connect to remote client.
|
||||||
retry: Retry
|
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
|
|
||||||
|
|||||||
@@ -14,74 +14,6 @@ export interface SecureModeConfig {
|
|||||||
local_public_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 {
|
export interface NetworkConfig {
|
||||||
instance_id: string
|
instance_id: string
|
||||||
|
|
||||||
@@ -115,7 +47,6 @@ export interface NetworkConfig {
|
|||||||
|
|
||||||
use_smoltcp?: boolean
|
use_smoltcp?: boolean
|
||||||
disable_ipv6?: boolean
|
disable_ipv6?: boolean
|
||||||
ipv6_public_addr_auto?: boolean
|
|
||||||
enable_kcp_proxy?: boolean
|
enable_kcp_proxy?: boolean
|
||||||
disable_kcp_input?: boolean
|
disable_kcp_input?: boolean
|
||||||
enable_quic_proxy?: boolean
|
enable_quic_proxy?: boolean
|
||||||
@@ -133,8 +64,6 @@ export interface NetworkConfig {
|
|||||||
disable_encryption?: boolean
|
disable_encryption?: boolean
|
||||||
disable_tcp_hole_punching?: boolean
|
disable_tcp_hole_punching?: boolean
|
||||||
disable_udp_hole_punching?: boolean
|
disable_udp_hole_punching?: boolean
|
||||||
disable_upnp?: boolean
|
|
||||||
enable_udp_broadcast_relay?: boolean
|
|
||||||
disable_sym_hole_punching?: boolean
|
disable_sym_hole_punching?: boolean
|
||||||
|
|
||||||
enable_relay_network_whitelist?: boolean
|
enable_relay_network_whitelist?: boolean
|
||||||
@@ -156,7 +85,6 @@ export interface NetworkConfig {
|
|||||||
enable_private_mode?: boolean
|
enable_private_mode?: boolean
|
||||||
|
|
||||||
port_forwards: PortForwardConfig[]
|
port_forwards: PortForwardConfig[]
|
||||||
acl?: Acl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||||
@@ -193,7 +121,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
|
|
||||||
use_smoltcp: false,
|
use_smoltcp: false,
|
||||||
disable_ipv6: false,
|
disable_ipv6: false,
|
||||||
ipv6_public_addr_auto: false,
|
|
||||||
enable_kcp_proxy: false,
|
enable_kcp_proxy: false,
|
||||||
disable_kcp_input: false,
|
disable_kcp_input: false,
|
||||||
enable_quic_proxy: false,
|
enable_quic_proxy: false,
|
||||||
@@ -211,8 +138,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
disable_encryption: false,
|
disable_encryption: false,
|
||||||
disable_tcp_hole_punching: false,
|
disable_tcp_hole_punching: false,
|
||||||
disable_udp_hole_punching: false,
|
disable_udp_hole_punching: false,
|
||||||
disable_upnp: false,
|
|
||||||
enable_udp_broadcast_relay: false,
|
|
||||||
disable_sym_hole_punching: false,
|
disable_sym_hole_punching: false,
|
||||||
enable_relay_network_whitelist: false,
|
enable_relay_network_whitelist: false,
|
||||||
relay_network_whitelist: [],
|
relay_network_whitelist: [],
|
||||||
@@ -227,15 +152,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
enable_magic_dns: false,
|
enable_magic_dns: false,
|
||||||
enable_private_mode: false,
|
enable_private_mode: false,
|
||||||
port_forwards: [],
|
port_forwards: [],
|
||||||
acl: {
|
|
||||||
acl_v1: {
|
|
||||||
group: {
|
|
||||||
declares: [],
|
|
||||||
members: [],
|
|
||||||
},
|
|
||||||
chains: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +365,4 @@ export enum EventType {
|
|||||||
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||||
|
|
||||||
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
ProxyCidrsUpdated = 'ProxyCidrsUpdated', // string[], string[]
|
||||||
|
|
||||||
UdpBroadcastRelayStartResult = 'UdpBroadcastRelayStartResult', // { capture_backend?: string, error?: string }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,80 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
import { Card, Password, Button } from 'primevue';
|
import { Card, Password, Button } from 'primevue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import ApiClient from '../modules/api';
|
import ApiClient from '../modules/api';
|
||||||
|
import { clearMustChangePasswordFlag } from '../modules/auth-status';
|
||||||
|
import { validatePasswordStrength } from '../modules/password-policy';
|
||||||
|
|
||||||
const dialogRef = inject<any>('dialogRef');
|
const dialogRef = inject<any>('dialogRef');
|
||||||
|
|
||||||
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
||||||
|
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
|
const confirmPassword = ref('');
|
||||||
|
const toast = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const passwordValidation = computed(() => validatePasswordStrength(password.value));
|
||||||
|
const passwordMatches = computed(() => password.value === confirmPassword.value);
|
||||||
|
const passwordErrorMessage = computed(() => {
|
||||||
|
if (password.value.length === 0 || passwordValidation.value.valid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return t(passwordValidation.value.reasonKey!);
|
||||||
|
});
|
||||||
|
const confirmPasswordErrorMessage = computed(() => {
|
||||||
|
if (confirmPassword.value.length === 0 || passwordMatches.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('web.common.password_mismatch');
|
||||||
|
});
|
||||||
|
const canSubmit = computed(() => passwordValidation.value.valid && passwordMatches.value);
|
||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
|
if (!passwordValidation.value.valid) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('web.common.warning'),
|
||||||
|
detail: t(passwordValidation.value.reasonKey!),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordMatches.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('web.common.warning'),
|
||||||
|
detail: t('web.common.password_mismatch'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await api.value.change_password(password.value);
|
await api.value.change_password(password.value);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('web.common.success'),
|
||||||
|
detail: t('web.main.password_changed_relogin'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
clearMustChangePasswordFlag();
|
||||||
dialogRef.value.close();
|
dialogRef.value.close();
|
||||||
|
router.push({ name: 'login' });
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('web.common.error'),
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -19,13 +82,26 @@ const changePassword = async () => {
|
|||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<Card class="w-full max-w-md p-6">
|
<Card class="w-full max-w-md p-6">
|
||||||
<template #header>
|
<template #header>
|
||||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
<h2 class="text-2xl font-semibold text-center">{{ t('web.main.change_password') }}
|
||||||
</h2>
|
</h2>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
<Password v-model="password" :placeholder="t('web.settings.new_password')" :feedback="false"
|
||||||
<Button @click="changePassword" label="Ok" />
|
toggleMask />
|
||||||
|
<Password v-model="confirmPassword" :placeholder="t('web.settings.confirm_password')"
|
||||||
|
:feedback="false" toggleMask />
|
||||||
|
<small class="text-surface-500 dark:text-surface-400">
|
||||||
|
{{ t('web.common.password_strength_hint') }}
|
||||||
|
</small>
|
||||||
|
<small v-if="passwordErrorMessage" class="text-red-500 dark:text-red-400">
|
||||||
|
{{ passwordErrorMessage }}
|
||||||
|
</small>
|
||||||
|
<small v-if="confirmPasswordErrorMessage" class="text-red-500 dark:text-red-400">
|
||||||
|
{{ confirmPasswordErrorMessage }}
|
||||||
|
</small>
|
||||||
|
<Button @click="changePassword" :label="t('web.common.confirm')"
|
||||||
|
:disabled="!canSubmit" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { I18nUtils } from 'easytier-frontend-lib';
|
|||||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||||
|
import { setMustChangePasswordFlag } from '../modules/auth-status';
|
||||||
|
import { validatePasswordStrength } from '../modules/password-policy';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -22,8 +24,26 @@ const username = ref('');
|
|||||||
const password = ref('');
|
const password = ref('');
|
||||||
const registerUsername = ref('');
|
const registerUsername = ref('');
|
||||||
const registerPassword = ref('');
|
const registerPassword = ref('');
|
||||||
|
const registerConfirmPassword = ref('');
|
||||||
const captcha = ref('');
|
const captcha = ref('');
|
||||||
const captchaSrc = computed(() => api.value.captcha_url());
|
const captchaSrc = computed(() => api.value.captcha_url());
|
||||||
|
const registerPasswordValidation = computed(() => validatePasswordStrength(registerPassword.value));
|
||||||
|
const registerPasswordsMatch = computed(() => registerPassword.value === registerConfirmPassword.value);
|
||||||
|
const registerPasswordErrorMessage = computed(() => {
|
||||||
|
if (registerPassword.value.length === 0 || registerPasswordValidation.value.valid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return t(registerPasswordValidation.value.reasonKey!);
|
||||||
|
});
|
||||||
|
const registerConfirmPasswordErrorMessage = computed(() => {
|
||||||
|
if (registerConfirmPassword.value.length === 0 || registerPasswordsMatch.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('web.common.password_mismatch');
|
||||||
|
});
|
||||||
|
const canRegister = computed(() => registerPasswordValidation.value.valid && registerPasswordsMatch.value);
|
||||||
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@@ -33,6 +53,7 @@ const onSubmit = async () => {
|
|||||||
let ret = await api.value?.login(credential);
|
let ret = await api.value?.login(credential);
|
||||||
if (ret.success) {
|
if (ret.success) {
|
||||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||||
|
setMustChangePasswordFlag(Boolean(ret.mustChangePassword));
|
||||||
router.push({
|
router.push({
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
params: { apiHost: btoa(apiHost.value) },
|
params: { apiHost: btoa(apiHost.value) },
|
||||||
@@ -43,6 +64,26 @@ const onSubmit = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRegister = async () => {
|
const onRegister = async () => {
|
||||||
|
if (!registerPasswordValidation.value.valid) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('web.common.warning'),
|
||||||
|
detail: t(registerPasswordValidation.value.reasonKey!),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!registerPasswordsMatch.value) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: t('web.common.warning'),
|
||||||
|
detail: t('web.common.password_mismatch'),
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
saveApiHost(apiHost.value);
|
saveApiHost(apiHost.value);
|
||||||
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||||
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
||||||
@@ -156,6 +197,23 @@ onBeforeUnmount(() => {
|
|||||||
}}</label>
|
}}</label>
|
||||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||||
:feedback="false" class="w-full" />
|
:feedback="false" class="w-full" />
|
||||||
|
<small class="text-surface-500 dark:text-surface-400">
|
||||||
|
{{ t('web.common.password_strength_hint') }}
|
||||||
|
</small>
|
||||||
|
<small v-if="registerPasswordErrorMessage" class="block text-red-500 dark:text-red-400">
|
||||||
|
{{ registerPasswordErrorMessage }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="p-field">
|
||||||
|
<label for="register-confirm-password" class="block text-sm font-medium">
|
||||||
|
{{ t('web.settings.confirm_password') }}
|
||||||
|
</label>
|
||||||
|
<Password id="register-confirm-password" v-model="registerConfirmPassword" required toggleMask
|
||||||
|
:feedback="false" class="w-full" />
|
||||||
|
<small v-if="registerConfirmPasswordErrorMessage"
|
||||||
|
class="block text-red-500 dark:text-red-400">
|
||||||
|
{{ registerConfirmPasswordErrorMessage }}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-field">
|
<div class="p-field">
|
||||||
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
|
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
|
||||||
@@ -163,7 +221,8 @@ onBeforeUnmount(() => {
|
|||||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button :label="t('web.login.register')" type="submit" class="w-full" />
|
<Button :label="t('web.login.register')" type="submit" class="w-full"
|
||||||
|
:disabled="!canRegister" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Button :label="t('web.login.back_to_login')" type="button" class="w-full"
|
<Button :label="t('web.login.back_to_login')" type="button" class="w-full"
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { I18nUtils } from 'easytier-frontend-lib'
|
import { I18nUtils } from 'easytier-frontend-lib'
|
||||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||||
import { Button, TieredMenu } from 'primevue';
|
import { Button, Message, TieredMenu } from 'primevue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useDialog } from 'primevue/usedialog';
|
import { useDialog } from 'primevue/usedialog';
|
||||||
import ChangePassword from './ChangePassword.vue';
|
import ChangePassword from './ChangePassword.vue';
|
||||||
import Icon from '../assets/easytier.png'
|
import Icon from '../assets/easytier.png'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import ApiClient from '../modules/api';
|
import ApiClient from '../modules/api';
|
||||||
|
import {
|
||||||
|
clearMustChangePasswordFlag,
|
||||||
|
getMustChangePasswordFlag,
|
||||||
|
setMustChangePasswordFlag,
|
||||||
|
} from '../modules/auth-status';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -15,6 +20,7 @@ const router = useRouter();
|
|||||||
const api = computed<ApiClient | undefined>(() => {
|
const api = computed<ApiClient | undefined>(() => {
|
||||||
try {
|
try {
|
||||||
return new ApiClient(atob(route.params.apiHost as string), () => {
|
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||||
|
clearMustChangePasswordFlag();
|
||||||
router.push({ name: 'login' });
|
router.push({ name: 'login' });
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -23,15 +29,10 @@ const api = computed<ApiClient | undefined>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
const mustChangePassword = ref(false);
|
||||||
|
|
||||||
const userMenu = ref();
|
const openChangePasswordDialog = () => {
|
||||||
const userMenuItems = ref([
|
dialog.open(ChangePassword, {
|
||||||
{
|
|
||||||
label: t('web.main.change_password'),
|
|
||||||
icon: 'pi pi-key',
|
|
||||||
command: () => {
|
|
||||||
console.log('File');
|
|
||||||
let ret = dialog.open(ChangePassword, {
|
|
||||||
props: {
|
props: {
|
||||||
modal: true,
|
modal: true,
|
||||||
},
|
},
|
||||||
@@ -39,9 +40,31 @@ const userMenuItems = ref([
|
|||||||
api: api.value,
|
api: api.value,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
console.log("return", ret)
|
const loadAuthStatus = async () => {
|
||||||
},
|
const cachedStatus = getMustChangePasswordFlag();
|
||||||
|
if (cachedStatus !== null) {
|
||||||
|
mustChangePassword.value = cachedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const status = await api.value?.check_login_status();
|
||||||
|
mustChangePassword.value = Boolean(
|
||||||
|
status?.loggedIn && status?.mustChangePassword,
|
||||||
|
);
|
||||||
|
setMustChangePasswordFlag(mustChangePassword.value);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load auth status', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMenu = ref();
|
||||||
|
const userMenuItems = ref([
|
||||||
|
{
|
||||||
|
label: t('web.main.change_password'),
|
||||||
|
icon: 'pi pi-key',
|
||||||
|
command: openChangePasswordDialog,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('web.main.logout'),
|
label: t('web.main.logout'),
|
||||||
@@ -52,6 +75,7 @@ const userMenuItems = ref([
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("logout failed", e);
|
console.error("logout failed", e);
|
||||||
}
|
}
|
||||||
|
clearMustChangePasswordFlag();
|
||||||
router.push({ name: 'login' });
|
router.push({ name: 'login' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -92,6 +116,7 @@ onMounted(async () => {
|
|||||||
// 等待 DOM 渲染完成后添加事件监听器
|
// 等待 DOM 渲染完成后添加事件监听器
|
||||||
await nextTick();
|
await nextTick();
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
await loadAuthStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -171,6 +196,13 @@ onUnmounted(() => {
|
|||||||
<div class="p-4 sm:ml-64">
|
<div class="p-4 sm:ml-64">
|
||||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
|
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<Message v-if="mustChangePassword" severity="warn" :closable="false">
|
||||||
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<span>{{ t('web.main.default_password_warning') }}</span>
|
||||||
|
<Button size="small" icon="pi pi-key" :label="t('web.main.change_password_now')"
|
||||||
|
@click="openChangePasswordDialog" />
|
||||||
|
</div>
|
||||||
|
</Message>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<component :is="Component" :api="api" />
|
<component :is="Component" :api="api" />
|
||||||
</RouterView>
|
</RouterView>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
|
|||||||
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
|
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||||
import { Md5 } from 'ts-md5';
|
import { Md5 } from 'ts-md5';
|
||||||
|
|
||||||
|
const hashAuthPassword = (password: string) => Md5.hashStr(password);
|
||||||
|
|
||||||
export interface ValidateConfigResponse {
|
export interface ValidateConfigResponse {
|
||||||
toml_config: string;
|
toml_config: string;
|
||||||
}
|
}
|
||||||
@@ -14,6 +16,16 @@ export interface OidcConfigResponse {
|
|||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
mustChangePassword?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthStatusResponse {
|
||||||
|
must_change_password: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckLoginStatusResponse {
|
||||||
|
loggedIn: boolean;
|
||||||
|
mustChangePassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterResponse {
|
export interface RegisterResponse {
|
||||||
@@ -82,7 +94,6 @@ export class ApiClient {
|
|||||||
|
|
||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||||
console.debug('Axios Response:', response);
|
|
||||||
return response.data; // 假设服务器返回的数据都在data属性中
|
return response.data; // 假设服务器返回的数据都在data属性中
|
||||||
}, (error: any) => {
|
}, (error: any) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -108,9 +119,8 @@ export class ApiClient {
|
|||||||
// 注册
|
// 注册
|
||||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||||
try {
|
try {
|
||||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
data.credentials.password = hashAuthPassword(data.credentials.password);
|
||||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
await this.client.post<RegisterResponse>('/auth/register', data);
|
||||||
console.log("register response:", response);
|
|
||||||
return { success: true, message: 'Register success', };
|
return { success: true, message: 'Register success', };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
@@ -123,10 +133,13 @@ export class ApiClient {
|
|||||||
// 登录
|
// 登录
|
||||||
public async login(data: Credential): Promise<LoginResponse> {
|
public async login(data: Credential): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
data.password = Md5.hashStr(data.password);
|
data.password = hashAuthPassword(data.password);
|
||||||
const response = await this.client.post<any>('/auth/login', data);
|
const response = await this.client.post<any, AuthStatusResponse>('/auth/login', data);
|
||||||
console.log("login response:", response);
|
return {
|
||||||
return { success: true, message: 'Login success', };
|
success: true,
|
||||||
|
message: 'Login success',
|
||||||
|
mustChangePassword: response.must_change_password,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AxiosError) {
|
if (error instanceof AxiosError) {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
@@ -147,16 +160,26 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async change_password(new_password: string) {
|
public async change_password(new_password: string) {
|
||||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
await this.client.put('/auth/password', { new_password: hashAuthPassword(new_password) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async check_login_status() {
|
public async check_login_status(): Promise<CheckLoginStatusResponse> {
|
||||||
try {
|
try {
|
||||||
await this.client.get('/auth/check_login_status');
|
const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status');
|
||||||
return true;
|
return {
|
||||||
|
loggedIn: true,
|
||||||
|
mustChangePassword: response.must_change_password,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
if (error instanceof AxiosError && error.response?.status === 401) {
|
||||||
|
return {
|
||||||
|
loggedIn: false,
|
||||||
|
mustChangePassword: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async list_session() {
|
public async list_session() {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const MUST_CHANGE_PASSWORD_STORAGE_KEY = 'auth.mustChangePassword';
|
||||||
|
|
||||||
|
export const getMustChangePasswordFlag = (): boolean | null => {
|
||||||
|
const value = sessionStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMustChangePasswordFlag = (value: boolean) => {
|
||||||
|
sessionStorage.setItem(MUST_CHANGE_PASSWORD_STORAGE_KEY, value ? 'true' : 'false');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearMustChangePasswordFlag = () => {
|
||||||
|
sessionStorage.removeItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export type PasswordValidationReasonKey =
|
||||||
|
| 'web.common.password_empty'
|
||||||
|
| 'web.common.password_min_length'
|
||||||
|
| 'web.common.password_too_weak';
|
||||||
|
|
||||||
|
export interface PasswordValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
reasonKey?: PasswordValidationReasonKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PASSWORD_MIN_LENGTH = 8;
|
||||||
|
|
||||||
|
export const countPasswordClasses = (password: string) => {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
if (/[a-z]/.test(password)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if (/[A-Z]/.test(password)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if (/\d/.test(password)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
if (/[^A-Za-z0-9\s]/.test(password)) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePasswordStrength = (password: string): PasswordValidationResult => {
|
||||||
|
if (password.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reasonKey: 'web.common.password_empty',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reasonKey: 'web.common.password_min_length',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countPasswordClasses(password) < 2) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reasonKey: 'web.common.password_too_weak',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
@@ -2,8 +2,8 @@ pub mod session;
|
|||||||
pub mod storage;
|
pub mod storage;
|
||||||
|
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc,
|
|
||||||
atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, Ordering},
|
||||||
|
Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
@@ -19,11 +19,11 @@ use maxminddb::geoip2;
|
|||||||
use session::{Location, Session};
|
use session::{Location, Session};
|
||||||
use storage::{Storage, StorageToken};
|
use storage::{Storage, StorageToken};
|
||||||
|
|
||||||
use crate::FeatureFlags;
|
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::SharedWebhookConfig;
|
||||||
|
use crate::FeatureFlags;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
|
||||||
|
|
||||||
#[derive(rust_embed::Embed)]
|
#[derive(rust_embed::Embed)]
|
||||||
#[folder = "resources/"]
|
#[folder = "resources/"]
|
||||||
@@ -340,7 +340,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use sqlx::Executor;
|
use sqlx::Executor;
|
||||||
|
|
||||||
use crate::{FeatureFlags, client_manager::ClientManager, db::Db};
|
use crate::{client_manager::ClientManager, db::Db, FeatureFlags};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_client() {
|
async fn test_client() {
|
||||||
@@ -365,7 +365,6 @@ mod tests {
|
|||||||
let _c = WebClient::new(
|
let _c = WebClient::new(
|
||||||
connector,
|
connector,
|
||||||
"test",
|
"test",
|
||||||
uuid::Uuid::new_v4(),
|
|
||||||
"test",
|
"test",
|
||||||
false,
|
false,
|
||||||
Arc::new(NetworkInstanceManager::new()),
|
Arc::new(NetworkInstanceManager::new()),
|
||||||
@@ -380,26 +379,19 @@ mod tests {
|
|||||||
|
|
||||||
let req = tokio::time::timeout(Duration::from_secs(12), async {
|
let req = tokio::time::timeout(Duration::from_secs(12), async {
|
||||||
loop {
|
loop {
|
||||||
let sessions = mgr
|
let session = mgr
|
||||||
.client_sessions
|
.client_sessions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| item.value().clone())
|
.next()
|
||||||
.collect::<Vec<_>>();
|
.map(|item| item.value().clone());
|
||||||
if sessions.is_empty() {
|
let Some(session) = session else {
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
};
|
||||||
let mut found_req = None;
|
let mut waiter = session.data().read().await.heartbeat_waiter();
|
||||||
for session in sessions {
|
if let Ok(req) = waiter.recv().await {
|
||||||
if let Some(req) = session.data().read().await.req() {
|
|
||||||
found_req = Some(req);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(req) = found_req {
|
|
||||||
break req;
|
break req;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -7,69 +7,24 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::ConfigSource,
|
common::scoped_task::ScopedTask,
|
||||||
proto::{
|
proto::{
|
||||||
api::manage::{
|
api::manage::{
|
||||||
ConfigSource as RpcConfigSource, NetworkConfig, NetworkMeta, RunNetworkInstanceRequest,
|
NetworkConfig, RunNetworkInstanceRequest, WebClientService,
|
||||||
WebClientService, WebClientServiceClientFactory,
|
WebClientServiceClientFactory,
|
||||||
},
|
},
|
||||||
rpc_impl::bidirect::BidirectRpcManager,
|
rpc_impl::bidirect::BidirectRpcManager,
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
web::{HeartbeatRequest, HeartbeatResponse, WebServerService, WebServerServiceServer},
|
||||||
},
|
},
|
||||||
rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _},
|
rpc_service::remote_client::{ListNetworkProps, Storage as _},
|
||||||
tunnel::Tunnel,
|
tunnel::Tunnel,
|
||||||
};
|
};
|
||||||
use tokio::sync::{RwLock, broadcast};
|
use tokio::sync::{broadcast, RwLock};
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
|
||||||
|
|
||||||
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
||||||
use crate::FeatureFlags;
|
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::SharedWebhookConfig;
|
||||||
|
use crate::FeatureFlags;
|
||||||
const LEGACY_NETWORK_CONFIG_SOURCE: &str = "legacy";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum PersistedConfigSource {
|
|
||||||
User,
|
|
||||||
Webhook,
|
|
||||||
Legacy,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PersistedConfigSource {
|
|
||||||
fn from_db(source: &str) -> Self {
|
|
||||||
match source {
|
|
||||||
"webhook" => Self::Webhook,
|
|
||||||
"user" => Self::User,
|
|
||||||
LEGACY_NETWORK_CONFIG_SOURCE => Self::Legacy,
|
|
||||||
_ => Self::User,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_update_from_runtime(self, runtime_source: ConfigSource) -> bool {
|
|
||||||
match (self, runtime_source) {
|
|
||||||
// Older clients report missing source as `user`, which is not authoritative enough
|
|
||||||
// to downgrade an existing webhook-owned or legacy row.
|
|
||||||
(Self::Webhook | Self::Legacy, ConfigSource::User) => false,
|
|
||||||
_ => self.as_runtime_source() != runtime_source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_runtime_source(self) -> ConfigSource {
|
|
||||||
match self {
|
|
||||||
Self::User | Self::Legacy => ConfigSource::User,
|
|
||||||
Self::Webhook => ConfigSource::Webhook,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auto_run_rpc_source(self) -> Option<RpcConfigSource> {
|
|
||||||
match self {
|
|
||||||
Self::User => Some(RpcConfigSource::User),
|
|
||||||
Self::Webhook => Some(RpcConfigSource::Webhook),
|
|
||||||
Self::Legacy => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
@@ -132,9 +87,8 @@ impl SessionData {
|
|||||||
|
|
||||||
impl Drop for SessionData {
|
impl Drop for SessionData {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Ok(storage) = Storage::try_from(self.storage.clone())
|
if let Ok(storage) = Storage::try_from(self.storage.clone()) {
|
||||||
&& let Some(token) = self.storage_token.as_ref()
|
if let Some(token) = self.storage_token.as_ref() {
|
||||||
{
|
|
||||||
storage.remove_client(token);
|
storage.remove_client(token);
|
||||||
|
|
||||||
// Notify the webhook receiver when a node disconnects.
|
// Notify the webhook receiver when a node disconnects.
|
||||||
@@ -159,6 +113,7 @@ impl Drop for SessionData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type SharedSessionData = Arc<RwLock<SessionData>>;
|
pub type SharedSessionData = Arc<RwLock<SessionData>>;
|
||||||
@@ -193,7 +148,7 @@ impl SessionRpcService {
|
|||||||
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
|
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconcile_webhook_source_configs(
|
async fn reconcile_managed_network_configs(
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
machine_id: uuid::Uuid,
|
machine_id: uuid::Uuid,
|
||||||
@@ -204,19 +159,9 @@ impl SessionRpcService {
|
|||||||
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
||||||
let existing_sources = existing_configs
|
let existing_ids = existing_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|cfg| {
|
.filter_map(|cfg| uuid::Uuid::parse_str(&cfg.network_instance_id).ok())
|
||||||
uuid::Uuid::parse_str(&cfg.network_instance_id)
|
|
||||||
.ok()
|
|
||||||
.map(|inst_id| (inst_id, PersistedConfigSource::from_db(&cfg.source)))
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
let existing_webhook_ids = existing_sources
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(inst_id, source)| {
|
|
||||||
(*source == PersistedConfigSource::Webhook).then_some(*inst_id)
|
|
||||||
})
|
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
|
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
|
||||||
@@ -224,30 +169,10 @@ impl SessionRpcService {
|
|||||||
for desired in desired_configs {
|
for desired in desired_configs {
|
||||||
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
|
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"invalid desired webhook config instance id: {}",
|
"invalid desired managed instance id: {}",
|
||||||
desired.instance_id
|
desired.instance_id
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
match existing_sources.get(&inst_id) {
|
|
||||||
Some(PersistedConfigSource::User) => {
|
|
||||||
tracing::warn!(
|
|
||||||
?user_id,
|
|
||||||
?machine_id,
|
|
||||||
instance_id = %inst_id,
|
|
||||||
"skip webhook config because a user-owned config already exists"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Some(PersistedConfigSource::Legacy) => {
|
|
||||||
tracing::info!(
|
|
||||||
?user_id,
|
|
||||||
?machine_id,
|
|
||||||
instance_id = %inst_id,
|
|
||||||
"adopt legacy config as webhook-owned during reconciliation"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let config = Self::normalize_network_config(desired.network_config, inst_id)?;
|
let config = Self::normalize_network_config(desired.network_config, inst_id)?;
|
||||||
desired_ids.insert(inst_id);
|
desired_ids.insert(inst_id);
|
||||||
normalized.insert(inst_id, config);
|
normalized.insert(inst_id, config);
|
||||||
@@ -256,23 +181,18 @@ impl SessionRpcService {
|
|||||||
for (inst_id, config) in normalized {
|
for (inst_id, config) in normalized {
|
||||||
storage
|
storage
|
||||||
.db()
|
.db()
|
||||||
.insert_or_update_user_network_config(
|
.insert_or_update_user_network_config((user_id, machine_id), inst_id, config)
|
||||||
(user_id, machine_id),
|
|
||||||
inst_id,
|
|
||||||
config,
|
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
anyhow::anyhow!(
|
anyhow::anyhow!(
|
||||||
"failed to persist webhook network config {}: {:?}",
|
"failed to persist managed network config {}: {:?}",
|
||||||
inst_id,
|
inst_id,
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stale_ids = existing_webhook_ids
|
let stale_ids = existing_ids
|
||||||
.difference(&desired_ids)
|
.difference(&desired_ids)
|
||||||
.copied()
|
.copied()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -305,7 +225,7 @@ impl SessionRpcService {
|
|||||||
|
|
||||||
let (
|
let (
|
||||||
user_id,
|
user_id,
|
||||||
webhook_source_configs,
|
webhook_managed_network_configs,
|
||||||
webhook_config_revision,
|
webhook_config_revision,
|
||||||
webhook_validated,
|
webhook_validated,
|
||||||
binding_version,
|
binding_version,
|
||||||
@@ -313,7 +233,6 @@ impl SessionRpcService {
|
|||||||
let webhook_req = crate::webhook::ValidateTokenRequest {
|
let webhook_req = crate::webhook::ValidateTokenRequest {
|
||||||
token: req.user_token.clone(),
|
token: req.user_token.clone(),
|
||||||
machine_id: machine_id.to_string(),
|
machine_id: machine_id.to_string(),
|
||||||
public_ip: data.client_url.host_str().map(str::to_string),
|
|
||||||
hostname: req.hostname.clone(),
|
hostname: req.hostname.clone(),
|
||||||
version: req.easytier_version.clone(),
|
version: req.easytier_version.clone(),
|
||||||
os_type: req.device_os.as_ref().map(|info| info.os_type.clone()),
|
os_type: req.device_os.as_ref().map(|info| info.os_type.clone()),
|
||||||
@@ -386,11 +305,11 @@ impl SessionRpcService {
|
|||||||
if webhook_validated
|
if webhook_validated
|
||||||
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
|
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
|
||||||
{
|
{
|
||||||
Self::reconcile_webhook_source_configs(
|
Self::reconcile_managed_network_configs(
|
||||||
&storage,
|
&storage,
|
||||||
user_id,
|
user_id,
|
||||||
machine_id,
|
machine_id,
|
||||||
webhook_source_configs,
|
webhook_managed_network_configs,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(rpc_types::error::Error::from)?;
|
.map_err(rpc_types::error::Error::from)?;
|
||||||
@@ -466,7 +385,7 @@ impl WebServerService for SessionRpcService {
|
|||||||
_: easytier::proto::web::GetFeatureRequest,
|
_: easytier::proto::web::GetFeatureRequest,
|
||||||
) -> rpc_types::error::Result<easytier::proto::web::GetFeatureResponse> {
|
) -> rpc_types::error::Result<easytier::proto::web::GetFeatureResponse> {
|
||||||
Ok(easytier::proto::web::GetFeatureResponse {
|
Ok(easytier::proto::web::GetFeatureResponse {
|
||||||
support_encryption: easytier::web_client::security::web_secure_tunnel_supported(),
|
support_encryption: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,7 +395,7 @@ pub struct Session {
|
|||||||
|
|
||||||
data: SharedSessionData,
|
data: SharedSessionData,
|
||||||
|
|
||||||
run_network_on_start_task: Option<AbortOnDropHandle<()>>,
|
run_network_on_start_task: Option<ScopedTask<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Session {
|
impl Debug for Session {
|
||||||
@@ -518,134 +437,14 @@ impl Session {
|
|||||||
self.rpc_mgr.run_with_tunnel(tunnel);
|
self.rpc_mgr.run_with_tunnel(tunnel);
|
||||||
|
|
||||||
let data = self.data.read().await;
|
let data = self.data.read().await;
|
||||||
self.run_network_on_start_task
|
self.run_network_on_start_task.replace(
|
||||||
.replace(AbortOnDropHandle::new(tokio::spawn(
|
tokio::spawn(Self::run_network_on_start(
|
||||||
Self::run_network_on_start(
|
|
||||||
data.heartbeat_waiter(),
|
data.heartbeat_waiter(),
|
||||||
data.storage.clone(),
|
data.storage.clone(),
|
||||||
self.scoped_rpc_client(),
|
self.scoped_rpc_client(),
|
||||||
),
|
))
|
||||||
)));
|
.into(),
|
||||||
}
|
);
|
||||||
|
|
||||||
fn collect_webhook_source_instance_ids(
|
|
||||||
metas: Vec<easytier::proto::api::manage::NetworkMeta>,
|
|
||||||
) -> HashSet<String> {
|
|
||||||
metas
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|meta| {
|
|
||||||
(RpcConfigSource::try_from(meta.source).ok() == Some(RpcConfigSource::Webhook))
|
|
||||||
.then(|| {
|
|
||||||
meta.inst_id
|
|
||||||
.map(|inst_id| Into::<uuid::Uuid>::into(inst_id).to_string())
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sync_running_config_sources(
|
|
||||||
db: &crate::db::Db,
|
|
||||||
user_id: i32,
|
|
||||||
machine_id: uuid::Uuid,
|
|
||||||
local_configs: &[crate::db::entity::user_running_network_configs::Model],
|
|
||||||
metas: &[NetworkMeta],
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let local_configs_by_id = local_configs
|
|
||||||
.iter()
|
|
||||||
.map(|cfg| (cfg.network_instance_id.clone(), cfg))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
for meta in metas {
|
|
||||||
let Some(inst_id) = meta.inst_id.as_ref().map(|inst_id| {
|
|
||||||
let inst_id: uuid::Uuid = (*inst_id).into();
|
|
||||||
inst_id
|
|
||||||
}) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let inst_id_str = inst_id.to_string();
|
|
||||||
let Some(local_cfg) = local_configs_by_id.get(&inst_id_str) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(running_source) = ConfigSource::from_rpc(meta.source) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let local_source = PersistedConfigSource::from_db(&local_cfg.source);
|
|
||||||
if !local_source.should_update_from_runtime(running_source) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(
|
|
||||||
(user_id, machine_id),
|
|
||||||
inst_id,
|
|
||||||
local_cfg.get_network_config().map_err(|e| {
|
|
||||||
anyhow::anyhow!("failed to decode local network config {}: {:?}", inst_id, e)
|
|
||||||
})?,
|
|
||||||
running_source,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"failed to sync running network config source {}: {:?}",
|
|
||||||
inst_id,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn repair_legacy_running_config_sources(
|
|
||||||
db: &crate::db::Db,
|
|
||||||
user_id: i32,
|
|
||||||
machine_id: uuid::Uuid,
|
|
||||||
local_configs: &[crate::db::entity::user_running_network_configs::Model],
|
|
||||||
) -> anyhow::Result<bool> {
|
|
||||||
let legacy_configs = local_configs
|
|
||||||
.iter()
|
|
||||||
.filter(|cfg| {
|
|
||||||
PersistedConfigSource::from_db(&cfg.source) == PersistedConfigSource::Legacy
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if legacy_configs.is_empty() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for local_cfg in legacy_configs {
|
|
||||||
let inst_id =
|
|
||||||
uuid::Uuid::parse_str(&local_cfg.network_instance_id).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to parse legacy network config instance id {}",
|
|
||||||
local_cfg.network_instance_id
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(
|
|
||||||
(user_id, machine_id),
|
|
||||||
inst_id,
|
|
||||||
local_cfg.get_network_config().map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"failed to decode legacy network config {}: {:?}",
|
|
||||||
inst_id,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
ConfigSource::User,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"failed to repair legacy network config source {}: {:?}",
|
|
||||||
inst_id,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_network_on_start(
|
async fn run_network_on_start(
|
||||||
@@ -653,8 +452,8 @@ impl Session {
|
|||||||
storage: WeakRefStorage,
|
storage: WeakRefStorage,
|
||||||
rpc_client: SessionRpcClient,
|
rpc_client: SessionRpcClient,
|
||||||
) {
|
) {
|
||||||
let mut cleaned_webhook_source_instances = false;
|
let mut cleaned_web_managed_instances = false;
|
||||||
let mut last_desired_webhook_inst_ids: Option<HashSet<String>> = None;
|
let mut last_desired_inst_ids: Option<HashSet<String>> = None;
|
||||||
loop {
|
loop {
|
||||||
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
||||||
let req = heartbeat_waiter.recv().await;
|
let req = heartbeat_waiter.recv().await;
|
||||||
@@ -710,160 +509,37 @@ impl Session {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut local_configs = local_configs;
|
|
||||||
let running_metas = if req.support_config_source {
|
|
||||||
let ret = if running_inst_ids.is_empty() {
|
|
||||||
Ok(Vec::new())
|
|
||||||
} else {
|
|
||||||
rpc_client
|
|
||||||
.list_network_instance_meta(
|
|
||||||
BaseController::default(),
|
|
||||||
easytier::proto::api::manage::ListNetworkInstanceMetaRequest {
|
|
||||||
inst_ids: running_inst_ids
|
|
||||||
.iter()
|
|
||||||
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
|
||||||
.map(Into::into)
|
|
||||||
.collect(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|resp| resp.metas)
|
|
||||||
};
|
|
||||||
|
|
||||||
match ret {
|
|
||||||
Ok(metas) => {
|
|
||||||
if let Err(e) = Self::sync_running_config_sources(
|
|
||||||
&storage.db,
|
|
||||||
user_id,
|
|
||||||
machine_id.into(),
|
|
||||||
&local_configs,
|
|
||||||
&metas,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!(
|
|
||||||
?user_id,
|
|
||||||
?machine_id,
|
|
||||||
%e,
|
|
||||||
"Failed to sync running network config sources"
|
|
||||||
);
|
|
||||||
} else if !metas.is_empty() {
|
|
||||||
local_configs = match storage
|
|
||||||
.db
|
|
||||||
.list_network_configs(
|
|
||||||
(user_id, machine_id.into()),
|
|
||||||
ListNetworkProps::EnabledOnly,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(configs) => configs,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to reload network configs after source sync, error: {:?}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some(metas)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(
|
|
||||||
?user_id,
|
|
||||||
%e,
|
|
||||||
"Failed to list running network instance metadata"
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match Self::repair_legacy_running_config_sources(
|
|
||||||
&storage.db,
|
|
||||||
user_id,
|
|
||||||
machine_id.into(),
|
|
||||||
&local_configs,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(true) => {
|
|
||||||
local_configs = match storage
|
|
||||||
.db
|
|
||||||
.list_network_configs(
|
|
||||||
(user_id, machine_id.into()),
|
|
||||||
ListNetworkProps::EnabledOnly,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(configs) => configs,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to reload network configs after legacy source repair, error: {:?}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Ok(false) => {}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!(
|
|
||||||
?user_id,
|
|
||||||
?machine_id,
|
|
||||||
%e,
|
|
||||||
"Failed to repair legacy running network config sources"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut has_failed = false;
|
let mut has_failed = false;
|
||||||
let should_be_alive_webhook_inst_ids = local_configs
|
let should_be_alive_inst_ids = local_configs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|cfg| cfg.get_runtime_network_config_source() == ConfigSource::Webhook)
|
|
||||||
.map(|cfg| cfg.network_instance_id.clone())
|
.map(|cfg| cfg.network_instance_id.clone())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
let desired_changed = last_desired_webhook_inst_ids
|
let desired_changed = last_desired_inst_ids
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_none_or(|last| last != &should_be_alive_webhook_inst_ids);
|
.is_none_or(|last| last != &should_be_alive_inst_ids);
|
||||||
|
|
||||||
if !cleaned_webhook_source_instances || desired_changed {
|
if !cleaned_web_managed_instances || desired_changed {
|
||||||
let db_webhook_inst_ids = match storage
|
let all_local_configs = match storage
|
||||||
.db
|
.db
|
||||||
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
|
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(configs) => configs
|
Ok(configs) => configs,
|
||||||
.iter()
|
|
||||||
.filter(|cfg| {
|
|
||||||
cfg.get_runtime_network_config_source() == ConfigSource::Webhook
|
|
||||||
})
|
|
||||||
.map(|cfg| cfg.network_instance_id.clone())
|
|
||||||
.collect::<HashSet<_>>(),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to list all network configs, error: {:?}", e);
|
tracing::error!("Failed to list all network configs, error: {:?}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let running_webhook_inst_ids = if let Some(metas) = running_metas.as_ref() {
|
let all_inst_ids = all_local_configs
|
||||||
Self::collect_webhook_source_instance_ids(metas.clone())
|
.iter()
|
||||||
} else {
|
.map(|cfg| cfg.network_instance_id.clone())
|
||||||
running_inst_ids
|
|
||||||
.intersection(&db_webhook_inst_ids)
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let should_delete_inst_ids = running_webhook_inst_ids
|
|
||||||
.difference(&should_be_alive_webhook_inst_ids)
|
|
||||||
.cloned()
|
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let should_delete_ids = should_delete_inst_ids
|
let should_delete_ids = running_inst_ids
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(all_inst_ids.iter())
|
||||||
|
.filter(|inst_id| !should_be_alive_inst_ids.contains(*inst_id))
|
||||||
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
.filter_map(|inst_id| uuid::Uuid::parse_str(inst_id).ok())
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -879,7 +555,7 @@ impl Session {
|
|||||||
.await;
|
.await;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
?user_id,
|
?user_id,
|
||||||
"Clean stale webhook-source network instances on start: {:?}, user_token: {:?}",
|
"Clean non-web-managed network instances on start: {:?}, user_token: {:?}",
|
||||||
ret,
|
ret,
|
||||||
req.user_token
|
req.user_token
|
||||||
);
|
);
|
||||||
@@ -887,8 +563,8 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has_failed {
|
if !has_failed {
|
||||||
cleaned_webhook_source_instances = true;
|
cleaned_web_managed_instances = true;
|
||||||
last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids.clone());
|
last_desired_inst_ids = Some(should_be_alive_inst_ids.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,16 +572,6 @@ impl Session {
|
|||||||
if running_inst_ids.contains(&c.network_instance_id) {
|
if running_inst_ids.contains(&c.network_instance_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(source) = PersistedConfigSource::from_db(&c.source).auto_run_rpc_source()
|
|
||||||
else {
|
|
||||||
tracing::warn!(
|
|
||||||
?user_id,
|
|
||||||
?machine_id,
|
|
||||||
instance_id = %c.network_instance_id,
|
|
||||||
"skip auto-run for legacy config until source ownership is repaired"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let ret = rpc_client
|
let ret = rpc_client
|
||||||
.run_network_instance(
|
.run_network_instance(
|
||||||
BaseController::default(),
|
BaseController::default(),
|
||||||
@@ -915,7 +581,6 @@ impl Session {
|
|||||||
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
serde_json::from_str::<NetworkConfig>(&c.network_config).unwrap(),
|
||||||
),
|
),
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
source: source as i32,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -930,7 +595,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has_failed {
|
if !has_failed {
|
||||||
last_desired_webhook_inst_ids = Some(should_be_alive_webhook_inst_ids);
|
last_desired_inst_ids = Some(should_be_alive_inst_ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -968,17 +633,13 @@ impl Session {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use easytier::{
|
use easytier::rpc_service::remote_client::{ListNetworkProps, Storage as _};
|
||||||
common::config::ConfigSource,
|
|
||||||
rpc_service::remote_client::{ListNetworkProps, PersistentConfig as _, Storage as _},
|
|
||||||
};
|
|
||||||
use sea_orm::{ActiveModelTrait, Set};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::{super::storage::Storage, *};
|
use super::{super::storage::Storage, *};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn reconcile_webhook_source_configs_upserts_and_deletes_exact_set() {
|
async fn reconcile_managed_network_configs_upserts_and_deletes_exact_set() {
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||||
let user_id = storage
|
let user_id = storage
|
||||||
.db()
|
.db()
|
||||||
@@ -1000,7 +661,6 @@ mod tests {
|
|||||||
network_name: Some("old-name".to_string()),
|
network_name: Some("old-name".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1013,12 +673,11 @@ mod tests {
|
|||||||
network_name: Some("stale".to_string()),
|
network_name: Some("stale".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
SessionRpcService::reconcile_webhook_source_configs(
|
SessionRpcService::reconcile_managed_network_configs(
|
||||||
&storage,
|
&storage,
|
||||||
user_id,
|
user_id,
|
||||||
machine_id,
|
machine_id,
|
||||||
@@ -1069,353 +728,5 @@ mod tests {
|
|||||||
updated_keep_config.network_name.as_deref(),
|
updated_keep_config.network_name.as_deref(),
|
||||||
Some("updated-name")
|
Some("updated-name")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
updated_keep.get_network_config_source(),
|
|
||||||
ConfigSource::Webhook
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn reconcile_webhook_source_configs_keep_user_owned_configs() {
|
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
|
||||||
let user_id = storage
|
|
||||||
.db()
|
|
||||||
.auto_create_user("webhook-user-keep-user")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.id;
|
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
|
||||||
let user_owned_id = uuid::Uuid::new_v4();
|
|
||||||
let webhook_owned_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
storage
|
|
||||||
.db()
|
|
||||||
.insert_or_update_user_network_config(
|
|
||||||
(user_id, machine_id),
|
|
||||||
user_owned_id,
|
|
||||||
NetworkConfig {
|
|
||||||
network_name: Some("user-owned".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
ConfigSource::User,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
storage
|
|
||||||
.db()
|
|
||||||
.insert_or_update_user_network_config(
|
|
||||||
(user_id, machine_id),
|
|
||||||
webhook_owned_id,
|
|
||||||
NetworkConfig {
|
|
||||||
network_name: Some("webhook-owned".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SessionRpcService::reconcile_webhook_source_configs(
|
|
||||||
&storage,
|
|
||||||
user_id,
|
|
||||||
machine_id,
|
|
||||||
vec![crate::webhook::ManagedNetworkConfig {
|
|
||||||
instance_id: user_owned_id.to_string(),
|
|
||||||
network_config: json!({
|
|
||||||
"instance_id": user_owned_id.to_string(),
|
|
||||||
"network_name": "webhook-tries-to-take-over"
|
|
||||||
}),
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let user_owned = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &user_owned_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(user_owned.get_network_config_source(), ConfigSource::User);
|
|
||||||
let user_owned_cfg: NetworkConfig =
|
|
||||||
serde_json::from_str(&user_owned.network_config).unwrap();
|
|
||||||
assert_eq!(user_owned_cfg.network_name.as_deref(), Some("user-owned"));
|
|
||||||
|
|
||||||
let webhook_owned = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &webhook_owned_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(webhook_owned.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn reconcile_webhook_source_configs_adopts_legacy_rows_for_webhook() {
|
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
|
||||||
let user_id = storage
|
|
||||||
.db()
|
|
||||||
.auto_create_user("webhook-user-legacy")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.id;
|
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
|
||||||
let legacy_match_id = uuid::Uuid::new_v4();
|
|
||||||
let legacy_user_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
crate::db::entity::user_running_network_configs::ActiveModel {
|
|
||||||
user_id: Set(user_id),
|
|
||||||
device_id: Set(machine_id.to_string()),
|
|
||||||
network_instance_id: Set(legacy_match_id.to_string()),
|
|
||||||
network_config: Set(serde_json::to_string(&NetworkConfig {
|
|
||||||
network_name: Some("legacy-webhook".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.unwrap()),
|
|
||||||
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.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(storage.db().orm_db())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
crate::db::entity::user_running_network_configs::ActiveModel {
|
|
||||||
user_id: Set(user_id),
|
|
||||||
device_id: Set(machine_id.to_string()),
|
|
||||||
network_instance_id: Set(legacy_user_id.to_string()),
|
|
||||||
network_config: Set(serde_json::to_string(&NetworkConfig {
|
|
||||||
network_name: Some("legacy-user".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.unwrap()),
|
|
||||||
source: Set(LEGACY_NETWORK_CONFIG_SOURCE.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(storage.db().orm_db())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
SessionRpcService::reconcile_webhook_source_configs(
|
|
||||||
&storage,
|
|
||||||
user_id,
|
|
||||||
machine_id,
|
|
||||||
vec![crate::webhook::ManagedNetworkConfig {
|
|
||||||
instance_id: legacy_match_id.to_string(),
|
|
||||||
network_config: json!({
|
|
||||||
"instance_id": legacy_match_id.to_string(),
|
|
||||||
"network_name": "managed-by-webhook"
|
|
||||||
}),
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let adopted = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &legacy_match_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(adopted.source, ConfigSource::Webhook.as_str());
|
|
||||||
let adopted_cfg: NetworkConfig = serde_json::from_str(&adopted.network_config).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
adopted_cfg.network_name.as_deref(),
|
|
||||||
Some("managed-by-webhook")
|
|
||||||
);
|
|
||||||
|
|
||||||
let untouched_legacy = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &legacy_user_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(untouched_legacy.source, LEGACY_NETWORK_CONFIG_SOURCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sync_running_config_sources_updates_enabled_config_source_from_runtime() {
|
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
|
||||||
let user_id = storage
|
|
||||||
.db()
|
|
||||||
.auto_create_user("webhook-user-sync-source")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.id;
|
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
|
||||||
let inst_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
storage
|
|
||||||
.db()
|
|
||||||
.insert_or_update_user_network_config(
|
|
||||||
(user_id, machine_id),
|
|
||||||
inst_id,
|
|
||||||
NetworkConfig {
|
|
||||||
network_name: Some("webhook-owned".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let local_configs = storage
|
|
||||||
.db()
|
|
||||||
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
Session::sync_running_config_sources(
|
|
||||||
storage.db(),
|
|
||||||
user_id,
|
|
||||||
machine_id,
|
|
||||||
&local_configs,
|
|
||||||
&[easytier::proto::api::manage::NetworkMeta {
|
|
||||||
inst_id: Some(inst_id.into()),
|
|
||||||
source: RpcConfigSource::User as i32,
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let updated = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(updated.get_network_config_source(), ConfigSource::Webhook);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sync_running_config_sources_keeps_legacy_rows_when_runtime_source_is_user() {
|
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
|
||||||
let user_id = storage
|
|
||||||
.db()
|
|
||||||
.auto_create_user("webhook-user-sync-legacy")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.id;
|
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
|
||||||
let inst_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
crate::db::entity::user_running_network_configs::ActiveModel {
|
|
||||||
user_id: Set(user_id),
|
|
||||||
device_id: Set(machine_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_NETWORK_CONFIG_SOURCE.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(storage.db().orm_db())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let local_configs = storage
|
|
||||||
.db()
|
|
||||||
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
Session::sync_running_config_sources(
|
|
||||||
storage.db(),
|
|
||||||
user_id,
|
|
||||||
machine_id,
|
|
||||||
&local_configs,
|
|
||||||
&[easytier::proto::api::manage::NetworkMeta {
|
|
||||||
inst_id: Some(inst_id.into()),
|
|
||||||
source: RpcConfigSource::User as i32,
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let updated = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(updated.source, LEGACY_NETWORK_CONFIG_SOURCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn repair_legacy_running_config_sources_promotes_remaining_legacy_rows_to_user() {
|
|
||||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
|
||||||
let user_id = storage
|
|
||||||
.db()
|
|
||||||
.auto_create_user("webhook-user-repair-legacy")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.id;
|
|
||||||
let machine_id = uuid::Uuid::new_v4();
|
|
||||||
let inst_id = uuid::Uuid::new_v4();
|
|
||||||
|
|
||||||
crate::db::entity::user_running_network_configs::ActiveModel {
|
|
||||||
user_id: Set(user_id),
|
|
||||||
device_id: Set(machine_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_NETWORK_CONFIG_SOURCE.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(storage.db().orm_db())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let local_configs = storage
|
|
||||||
.db()
|
|
||||||
.list_network_configs((user_id, machine_id), ListNetworkProps::EnabledOnly)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
Session::repair_legacy_running_config_sources(
|
|
||||||
storage.db(),
|
|
||||||
user_id,
|
|
||||||
machine_id,
|
|
||||||
&local_configs,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let updated = storage
|
|
||||||
.db()
|
|
||||||
.get_network_config((user_id, machine_id), &inst_id.to_string())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(updated.source, ConfigSource::User.as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_configs_are_not_auto_run_until_repaired() {
|
|
||||||
assert_eq!(PersistedConfigSource::Legacy.auto_run_rpc_source(), None);
|
|
||||||
assert_eq!(
|
|
||||||
PersistedConfigSource::Webhook.auto_run_rpc_source(),
|
|
||||||
Some(RpcConfigSource::Webhook)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
PersistedConfigSource::User.auto_run_rpc_source(),
|
|
||||||
Some(RpcConfigSource::User)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||||
|
|
||||||
use easytier::{
|
use easytier::{launcher::NetworkConfig, rpc_service::remote_client::PersistentConfig};
|
||||||
common::config::ConfigSource, launcher::NetworkConfig,
|
|
||||||
rpc_service::remote_client::PersistentConfig,
|
|
||||||
};
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -15,12 +12,10 @@ pub struct Model {
|
|||||||
pub user_id: i32,
|
pub user_id: i32,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text", unique)]
|
||||||
pub network_instance_id: String,
|
pub network_instance_id: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub network_config: String,
|
pub network_config: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
|
||||||
pub source: String,
|
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
pub create_time: DateTimeWithTimeZone,
|
pub create_time: DateTimeWithTimeZone,
|
||||||
pub update_time: DateTimeWithTimeZone,
|
pub update_time: DateTimeWithTimeZone,
|
||||||
@@ -53,7 +48,4 @@ impl PersistentConfig<DbErr> for Model {
|
|||||||
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
||||||
serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string()))
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub struct Model {
|
|||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub must_change_password: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|||||||
+30
-73
@@ -3,17 +3,16 @@
|
|||||||
pub mod entity;
|
pub mod entity;
|
||||||
|
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::ConfigSource,
|
|
||||||
launcher::NetworkConfig,
|
launcher::NetworkConfig,
|
||||||
rpc_service::remote_client::{ListNetworkProps, Storage},
|
rpc_service::remote_client::{ListNetworkProps, Storage},
|
||||||
};
|
};
|
||||||
use entity::user_running_network_configs;
|
use entity::user_running_network_configs;
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||||
SqlxSqliteConnector, TransactionTrait as _, prelude::Expr, sea_query::OnConflict,
|
QueryFilter as _, Set, SqlxSqliteConnector, TransactionTrait as _,
|
||||||
};
|
};
|
||||||
use sea_orm_migration::MigratorTrait as _;
|
use sea_orm_migration::MigratorTrait as _;
|
||||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _, types::chrono};
|
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::migrator;
|
use crate::migrator;
|
||||||
@@ -97,6 +96,7 @@ impl Db {
|
|||||||
let user_active = users::ActiveModel {
|
let user_active = users::ActiveModel {
|
||||||
username: Set(username.to_string()),
|
username: Set(username.to_string()),
|
||||||
password: Set(password_hash),
|
password: Set(password_hash),
|
||||||
|
must_change_password: Set(false),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let insert_result = users::Entity::insert(user_active).exec(&txn).await?;
|
let insert_result = users::Entity::insert(user_active).exec(&txn).await?;
|
||||||
@@ -150,7 +150,6 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
(user_id, device_id): (UserIdInDb, Uuid),
|
(user_id, device_id): (UserIdInDb, Uuid),
|
||||||
network_inst_id: Uuid,
|
network_inst_id: Uuid,
|
||||||
network_config: NetworkConfig,
|
network_config: NetworkConfig,
|
||||||
source: ConfigSource,
|
|
||||||
) -> Result<(), DbErr> {
|
) -> Result<(), DbErr> {
|
||||||
let txn = self.orm_db().begin().await?;
|
let txn = self.orm_db().begin().await?;
|
||||||
|
|
||||||
@@ -163,7 +162,6 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
])
|
])
|
||||||
.update_columns([
|
.update_columns([
|
||||||
urnc::Column::NetworkConfig,
|
urnc::Column::NetworkConfig,
|
||||||
urnc::Column::Source,
|
|
||||||
urnc::Column::Disabled,
|
urnc::Column::Disabled,
|
||||||
urnc::Column::UpdateTime,
|
urnc::Column::UpdateTime,
|
||||||
])
|
])
|
||||||
@@ -175,7 +173,6 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
network_config: sea_orm::Set(
|
network_config: sea_orm::Set(
|
||||||
serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?,
|
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),
|
disabled: sea_orm::Set(false),
|
||||||
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||||
@@ -281,14 +278,31 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use easytier::{
|
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
|
||||||
common::config::ConfigSource,
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||||
proto::api::manage::NetworkConfig,
|
|
||||||
rpc_service::remote_client::{PersistentConfig, Storage},
|
|
||||||
};
|
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter as _, Set};
|
|
||||||
|
|
||||||
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs};
|
use crate::db::{
|
||||||
|
entity::{user_running_network_configs, users},
|
||||||
|
Db, ListNetworkProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn created_users_default_to_not_requiring_password_change() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
|
||||||
|
let user = db
|
||||||
|
.create_user_and_join_users_group("created-user", "pre-hashed-password".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let stored = users::Entity::find_by_id(user.id)
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!stored.must_change_password);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_user_network_config_management() {
|
async fn test_user_network_config_management() {
|
||||||
@@ -302,12 +316,7 @@ mod tests {
|
|||||||
let inst_id = uuid::Uuid::new_v4();
|
let inst_id = uuid::Uuid::new_v4();
|
||||||
let device_id = uuid::Uuid::new_v4();
|
let device_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
db.insert_or_update_user_network_config(
|
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||||
(user_id, device_id),
|
|
||||||
inst_id,
|
|
||||||
network_config,
|
|
||||||
ConfigSource::User,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -319,7 +328,6 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", result);
|
println!("{:?}", result);
|
||||||
assert_eq!(result.network_config, network_config_json);
|
assert_eq!(result.network_config, network_config_json);
|
||||||
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
|
||||||
|
|
||||||
// overwrite the config
|
// overwrite the config
|
||||||
let network_config = NetworkConfig {
|
let network_config = NetworkConfig {
|
||||||
@@ -327,12 +335,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
||||||
db.insert_or_update_user_network_config(
|
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||||
(user_id, device_id),
|
|
||||||
inst_id,
|
|
||||||
network_config,
|
|
||||||
ConfigSource::Webhook,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -344,11 +347,6 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
println!("device: {}, {:?}", device_id, result2);
|
println!("device: {}, {:?}", device_id, result2);
|
||||||
assert_eq!(result2.network_config, network_config_json);
|
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_eq!(result.create_time, result2.create_time);
|
||||||
assert_ne!(result.update_time, result2.update_time);
|
assert_ne!(result.update_time, result2.update_time);
|
||||||
@@ -372,45 +370,6 @@ mod tests {
|
|||||||
assert!(result3.is_none());
|
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]
|
#[tokio::test]
|
||||||
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
||||||
let db = Db::memory_db().await;
|
let db = Db::memory_db().await;
|
||||||
@@ -426,7 +385,6 @@ mod tests {
|
|||||||
network_name: Some("cfg-1".to_string()),
|
network_name: Some("cfg-1".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
ConfigSource::User,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -437,7 +395,6 @@ mod tests {
|
|||||||
network_name: Some("cfg-2".to_string()),
|
network_name: Some("cfg-2".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
ConfigSource::User,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use easytier::{
|
|||||||
log,
|
log,
|
||||||
network::{local_ipv4, local_ipv6},
|
network::{local_ipv4, local_ipv6},
|
||||||
},
|
},
|
||||||
tunnel::{TunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener},
|
tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, TunnelListener},
|
||||||
utils::panic::setup_panic_handler,
|
utils::setup_panic_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
use easytier::tunnel::IpScheme;
|
use easytier::tunnel::IpScheme;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
const DEFAULT_USER_PASSWORD_HASH: &str =
|
||||||
|
"$argon2i$v=19$m=16,t=2,p=1$aGVyRDBrcnRycnlaMDhkbw$449SEcv/qXf+0fnI9+fYVQ";
|
||||||
|
const DEFAULT_ADMIN_PASSWORD_HASH: &str =
|
||||||
|
"$argon2i$v=19$m=16,t=2,p=1$bW5idXl0cmY$61n+JxL4r3dwLPAEDlDdtg";
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Users {
|
||||||
|
Table,
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
MustChangePassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MigrationName for Migration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"m20260405_000003_add_must_change_password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Users::Table)
|
||||||
|
.add_column(
|
||||||
|
ColumnDef::new(Users::MustChangePassword)
|
||||||
|
.boolean()
|
||||||
|
.not_null()
|
||||||
|
.default(false),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.exec_stmt(
|
||||||
|
Query::update()
|
||||||
|
.table(Users::Table)
|
||||||
|
.value(Users::MustChangePassword, true)
|
||||||
|
.cond_where(any![
|
||||||
|
Expr::col(Users::Username)
|
||||||
|
.eq("admin")
|
||||||
|
.and(Expr::col(Users::Password).eq(DEFAULT_ADMIN_PASSWORD_HASH)),
|
||||||
|
Expr::col(Users::Username)
|
||||||
|
.eq("user")
|
||||||
|
.and(Expr::col(Users::Password).eq(DEFAULT_USER_PASSWORD_HASH)),
|
||||||
|
])
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(Users::Table)
|
||||||
|
.drop_column(Users::MustChangePassword)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _, SqlxSqliteConnector};
|
||||||
|
use sea_orm_migration::prelude::SchemaManager;
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
|
||||||
|
use super::{Migration, MigrationTrait, DEFAULT_USER_PASSWORD_HASH};
|
||||||
|
use crate::db::entity::users;
|
||||||
|
|
||||||
|
async fn find_user(db: &sea_orm::DatabaseConnection, username: &str) -> users::Model {
|
||||||
|
users::Entity::find()
|
||||||
|
.filter(users::Column::Username.eq(username))
|
||||||
|
.one(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn migration_only_marks_seeded_accounts_still_using_default_passwords() {
|
||||||
|
let pool = SqlitePoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect("sqlite::memory:")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let changed_admin_password = password_auth::generate_hash("already-changed");
|
||||||
|
|
||||||
|
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?), (?, ?)")
|
||||||
|
.bind("admin")
|
||||||
|
.bind(changed_admin_password)
|
||||||
|
.bind("user")
|
||||||
|
.bind(DEFAULT_USER_PASSWORD_HASH)
|
||||||
|
.execute(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let db = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
|
||||||
|
Migration.up(&SchemaManager::new(&db)).await.unwrap();
|
||||||
|
|
||||||
|
assert!(!find_user(&db, "admin").await.must_change_password);
|
||||||
|
assert!(find_user(&db, "user").await.must_change_password);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ use sea_orm_migration::prelude::*;
|
|||||||
|
|
||||||
mod m20241029_000001_init;
|
mod m20241029_000001_init;
|
||||||
mod m20260403_000002_scope_network_config_unique;
|
mod m20260403_000002_scope_network_config_unique;
|
||||||
mod m20260421_000003_add_network_config_source;
|
mod m20260405_000003_add_must_change_password;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ impl MigratorTrait for Migrator {
|
|||||||
vec![
|
vec![
|
||||||
Box::new(m20241029_000001_init::Migration),
|
Box::new(m20241029_000001_init::Migration),
|
||||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||||
Box::new(m20260421_000003_add_network_config_source::Migration),
|
Box::new(m20260405_000003_add_must_change_password::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
|
Router,
|
||||||
};
|
};
|
||||||
use axum_login::login_required;
|
use axum_login::login_required;
|
||||||
use axum_messages::Message;
|
use serde::Serialize;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::restful::users::Backend;
|
use crate::restful::users::Backend;
|
||||||
|
|
||||||
@@ -14,13 +13,13 @@ use std::sync::Arc;
|
|||||||
use crate::FeatureFlags;
|
use crate::FeatureFlags;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppStateInner,
|
|
||||||
users::{AuthSession, Credentials},
|
users::{AuthSession, Credentials},
|
||||||
|
AppStateInner,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct LoginResult {
|
pub struct AuthStatusResponse {
|
||||||
messages: Vec<Message>,
|
must_change_password: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<AppStateInner> {
|
pub fn router() -> Router<AppStateInner> {
|
||||||
@@ -40,12 +39,15 @@ pub fn router() -> Router<AppStateInner> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod put {
|
mod put {
|
||||||
|
use crate::restful::{
|
||||||
|
other_error,
|
||||||
|
users::{ChangePassword, ChangePasswordError},
|
||||||
|
HttpHandleError,
|
||||||
|
};
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum_login::AuthUser;
|
use axum_login::AuthUser;
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
|
|
||||||
use crate::restful::{HttpHandleError, other_error, users::ChangePassword};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub async fn change_password(
|
pub async fn change_password(
|
||||||
@@ -58,27 +60,33 @@ mod put {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::error!("Failed to change password: {:?}", e);
|
tracing::error!("Failed to change password: {:?}", e);
|
||||||
return Err((
|
let (status, message) = match &e {
|
||||||
|
ChangePasswordError::EmptyPassword => {
|
||||||
|
(StatusCode::BAD_REQUEST, "password cannot be empty")
|
||||||
|
}
|
||||||
|
ChangePasswordError::UserNotFound | ChangePasswordError::Db(_) => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json::from(other_error(format!("{:?}", e))),
|
"failed to change password",
|
||||||
));
|
),
|
||||||
|
};
|
||||||
|
return Err((status, Json::from(other_error(message.to_string()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = auth_session.logout().await;
|
let _ = auth_session.logout().await;
|
||||||
|
|
||||||
Ok(Void::default().into())
|
Ok(Json(Void::default()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod post {
|
mod post {
|
||||||
use axum::{Json, extract::Extension};
|
use axum::{extract::Extension, Json};
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
|
|
||||||
use crate::restful::{
|
use crate::restful::{
|
||||||
HttpHandleError,
|
captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil},
|
||||||
captcha::extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionStaticExt},
|
|
||||||
other_error,
|
other_error,
|
||||||
users::RegisterNewUser,
|
users::RegisterNewUser,
|
||||||
|
HttpHandleError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -86,7 +94,7 @@ mod post {
|
|||||||
pub async fn login(
|
pub async fn login(
|
||||||
mut auth_session: AuthSession,
|
mut auth_session: AuthSession,
|
||||||
Json(creds): Json<Credentials>,
|
Json(creds): Json<Credentials>,
|
||||||
) -> Result<Json<Void>, HttpHandleError> {
|
) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
|
||||||
let user = match auth_session.authenticate(creds.clone()).await {
|
let user = match auth_session.authenticate(creds.clone()).await {
|
||||||
Ok(Some(user)) => user,
|
Ok(Some(user)) => user,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
@@ -99,7 +107,7 @@ mod post {
|
|||||||
return Err((
|
return Err((
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json::from(other_error(format!("{:?}", e))),
|
Json::from(other_error(format!("{:?}", e))),
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,7 +118,9 @@ mod post {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Void::default().into())
|
Ok(Json(AuthStatusResponse {
|
||||||
|
must_change_password: user.db_user.must_change_password,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
@@ -150,15 +160,14 @@ mod post {
|
|||||||
|
|
||||||
mod get {
|
mod get {
|
||||||
use crate::restful::{
|
use crate::restful::{
|
||||||
HttpHandleError,
|
|
||||||
captcha::{
|
captcha::{
|
||||||
NewCaptcha as _,
|
|
||||||
builder::spec::SpecCaptcha,
|
builder::spec::SpecCaptcha,
|
||||||
extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionExt as _},
|
extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil},
|
||||||
|
NewCaptcha as _,
|
||||||
},
|
},
|
||||||
other_error,
|
other_error, HttpHandleError,
|
||||||
};
|
};
|
||||||
use axum::{Json, response::Response};
|
use axum::{response::Response, Json};
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
@@ -190,9 +199,11 @@ mod get {
|
|||||||
|
|
||||||
pub async fn check_login_status(
|
pub async fn check_login_status(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<Json<Void>, HttpHandleError> {
|
) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
|
||||||
if auth_session.user.is_some() {
|
if let Some(user) = auth_session.user {
|
||||||
Ok(Json(Void::default()))
|
Ok(Json(AuthStatusResponse {
|
||||||
|
must_change_password: user.db_user.must_change_password,
|
||||||
|
}))
|
||||||
} else {
|
} else {
|
||||||
Err((
|
Err((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use super::super::base::randoms::Randoms;
|
|||||||
|
|
||||||
use super::super::utils::color::Color;
|
use super::super::utils::color::Color;
|
||||||
use super::super::utils::font;
|
use super::super::utils::font;
|
||||||
use base64::Engine;
|
|
||||||
use base64::prelude::BASE64_STANDARD;
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
use rusttype::Font;
|
use rusttype::Font;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use super::super::{CaptchaFont, NewCaptcha};
|
|||||||
|
|
||||||
use image::{ImageBuffer, Rgba};
|
use image::{ImageBuffer, Rgba};
|
||||||
use imageproc::drawing;
|
use imageproc::drawing;
|
||||||
use rand::{Rng, rngs::ThreadRng};
|
use rand::{rngs::ThreadRng, Rng};
|
||||||
use rusttype::{Font, Scale};
|
use rusttype::{Font, Scale};
|
||||||
use std::io::{Cursor, Write};
|
use std::io::{Cursor, Write};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod color {
|
mod color {
|
||||||
use image::Rgba;
|
use image::Rgba;
|
||||||
use rand::{Rng, rngs::ThreadRng};
|
use rand::{rngs::ThreadRng, Rng};
|
||||||
pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> {
|
pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> {
|
||||||
let red = rng.gen_range(200..=255);
|
let red = rng.gen_range(200..=255);
|
||||||
let green = 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) {
|
fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
|
||||||
let line_color = color::gen_line_color(rng);
|
let line_color = color::gen_line_color(rng);
|
||||||
let is_h = rng.r#gen();
|
let is_h = rng.gen();
|
||||||
let (start, end) = if is_h {
|
let (start, end) = if is_h {
|
||||||
let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
|
let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
|
||||||
let ya = rng.gen_range(0.0..(self.height as f32));
|
let ya = rng.gen_range(0.0..(self.height as f32));
|
||||||
|
|||||||
@@ -8,32 +8,32 @@ mod users;
|
|||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::http::{Request, StatusCode, header};
|
use axum::http::{header, Request, StatusCode};
|
||||||
use axum::middleware::{self as axum_mw, Next};
|
use axum::middleware::{self as axum_mw, Next};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::routing::{delete, post};
|
use axum::routing::{delete, post};
|
||||||
use axum::{Extension, Json, Router, extract::State, routing::get};
|
use axum::{extract::State, routing::get, Extension, Json, Router};
|
||||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||||
use axum_login::{AuthManagerLayerBuilder, AuthUser, AuthzBackend, login_required};
|
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||||
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::rpc_types;
|
use easytier::proto::rpc_types;
|
||||||
use network::NetworkApi;
|
use network::NetworkApi;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
|
||||||
use tower_sessions::Expiry;
|
|
||||||
use tower_sessions::cookie::time::Duration;
|
use tower_sessions::cookie::time::Duration;
|
||||||
use tower_sessions::cookie::{Key, SameSite};
|
use tower_sessions::cookie::{Key, SameSite};
|
||||||
|
use tower_sessions::Expiry;
|
||||||
use tower_sessions_sqlx_store::SqliteStore;
|
use tower_sessions_sqlx_store::SqliteStore;
|
||||||
use users::{AuthSession, Backend};
|
use users::{AuthSession, Backend};
|
||||||
|
|
||||||
use crate::FeatureFlags;
|
|
||||||
use crate::client_manager::ClientManager;
|
|
||||||
use crate::client_manager::storage::StorageToken;
|
use crate::client_manager::storage::StorageToken;
|
||||||
|
use crate::client_manager::ClientManager;
|
||||||
use crate::db::{Db, UserIdInDb};
|
use crate::db::{Db, UserIdInDb};
|
||||||
use crate::webhook::SharedWebhookConfig;
|
use crate::webhook::SharedWebhookConfig;
|
||||||
|
use crate::FeatureFlags;
|
||||||
|
|
||||||
/// Embed assets for web dashboard, build frontend first
|
/// Embed assets for web dashboard, build frontend first
|
||||||
#[cfg(feature = "embed")]
|
#[cfg(feature = "embed")]
|
||||||
@@ -199,8 +199,8 @@ impl RestfulServer {
|
|||||||
mut self,
|
mut self,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
AbortOnDropHandle<()>,
|
ScopedTask<()>,
|
||||||
AbortOnDropHandle<tower_sessions::session_store::Result<()>>,
|
ScopedTask<tower_sessions::session_store::Result<()>>,
|
||||||
),
|
),
|
||||||
anyhow::Error,
|
anyhow::Error,
|
||||||
> {
|
> {
|
||||||
@@ -213,11 +213,13 @@ impl RestfulServer {
|
|||||||
let session_store = SqliteStore::new(self.db.inner());
|
let session_store = SqliteStore::new(self.db.inner());
|
||||||
session_store.migrate().await?;
|
session_store.migrate().await?;
|
||||||
|
|
||||||
let delete_task = AbortOnDropHandle::new(tokio::task::spawn(
|
let delete_task: ScopedTask<tower_sessions::session_store::Result<()>> =
|
||||||
|
tokio::task::spawn(
|
||||||
session_store
|
session_store
|
||||||
.clone()
|
.clone()
|
||||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||||
));
|
)
|
||||||
|
.into();
|
||||||
|
|
||||||
// Generate a cryptographic key to sign the session cookie.
|
// Generate a cryptographic key to sign the session cookie.
|
||||||
let key = Key::generate();
|
let key = Key::generate();
|
||||||
@@ -296,9 +298,10 @@ impl RestfulServer {
|
|||||||
app
|
app
|
||||||
};
|
};
|
||||||
|
|
||||||
let serve_task = AbortOnDropHandle::new(tokio::spawn(async move {
|
let serve_task: ScopedTask<()> = tokio::spawn(async move {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}));
|
})
|
||||||
|
.into();
|
||||||
|
|
||||||
Ok((serve_task, delete_task))
|
Ok((serve_task, delete_task))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::routing::{delete, post};
|
use axum::routing::{delete, post};
|
||||||
use axum::{Json, Router, extract::State, routing::get};
|
use axum::{extract::State, routing::get, Json, Router};
|
||||||
use axum_login::AuthUser;
|
use axum_login::AuthUser;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
@@ -16,7 +16,7 @@ use crate::db::UserIdInDb;
|
|||||||
|
|
||||||
use super::users::AuthSession;
|
use super::users::AuthSession;
|
||||||
use super::{
|
use super::{
|
||||||
AppState, AppStateInner, Error, HttpHandleError, RpcError, convert_db_error, other_error,
|
convert_db_error, other_error, AppState, AppStateInner, Error, HttpHandleError, RpcError,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
|
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
use axum::Router;
|
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
|
use axum::Router;
|
||||||
use openidconnect::core::{
|
use openidconnect::core::{
|
||||||
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
|
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
|
||||||
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
||||||
@@ -216,9 +216,7 @@ impl OidcConfig {
|
|||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() {
|
if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!("--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"));
|
||||||
"--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if oidc_username_claim.trim().is_empty() {
|
if oidc_username_claim.trim().is_empty() {
|
||||||
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
|
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
|
||||||
@@ -375,8 +373,8 @@ mod route {
|
|||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
if let Some(verifier) = pkce_verifier
|
if let Some(verifier) = pkce_verifier {
|
||||||
&& let Err(e) = session
|
if let Err(e) = session
|
||||||
.insert("oidc_pkce_verifier", verifier.secret().clone())
|
.insert("oidc_pkce_verifier", verifier.secret().clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -387,6 +385,7 @@ mod route {
|
|||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await {
|
if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await {
|
||||||
tracing::error!("Failed to store pkce_used in session: {:?}", e);
|
tracing::error!("Failed to store pkce_used in session: {:?}", e);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
routing::post,
|
routing::post,
|
||||||
|
Json, Router,
|
||||||
};
|
};
|
||||||
use axum_login::AuthUser as _;
|
use axum_login::AuthUser as _;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
|
|
||||||
use crate::db::UserIdInDb;
|
use crate::db::UserIdInDb;
|
||||||
|
|
||||||
use super::{AppState, HttpHandleError, other_error};
|
use super::{other_error, AppState, HttpHandleError};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct ProxyRpcRequest {
|
pub struct ProxyRpcRequest {
|
||||||
@@ -120,7 +120,7 @@ async fn handle_proxy_rpc_by_session(
|
|||||||
return Err((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
other_error(format!("Unknown service: {}", service_name)).into(),
|
other_error(format!("Unknown service: {}", service_name)).into(),
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use tokio::task;
|
|||||||
|
|
||||||
use crate::db::{self, entity};
|
use crate::db::{self, entity};
|
||||||
|
|
||||||
|
const EMPTY_PASSWORD_MD5: &str = "d41d8cd98f00b204e9800998ecf8427e";
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub(crate) db_user: entity::users::Model,
|
pub(crate) db_user: entity::users::Model,
|
||||||
@@ -64,6 +66,18 @@ pub struct ChangePassword {
|
|||||||
pub new_password: String,
|
pub new_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ChangePasswordError {
|
||||||
|
#[error("Password cannot be empty")]
|
||||||
|
EmptyPassword,
|
||||||
|
|
||||||
|
#[error("User not found")]
|
||||||
|
UserNotFound,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Db(#[from] sea_orm::DbErr),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Backend {
|
pub struct Backend {
|
||||||
db: db::Db,
|
db: db::Db,
|
||||||
@@ -119,7 +133,14 @@ impl Backend {
|
|||||||
&self,
|
&self,
|
||||||
id: <User as AuthUser>::Id,
|
id: <User as AuthUser>::Id,
|
||||||
req: &ChangePassword,
|
req: &ChangePassword,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<(), ChangePasswordError> {
|
||||||
|
// With the existing pre-hashed protocol the backend can only reject the
|
||||||
|
// exact empty-string digest; whitespace-only passwords must be blocked
|
||||||
|
// on the client before hashing.
|
||||||
|
if req.new_password == EMPTY_PASSWORD_MD5 {
|
||||||
|
return Err(ChangePasswordError::EmptyPassword);
|
||||||
|
}
|
||||||
|
|
||||||
let hashed_password = password_auth::generate_hash(req.new_password.as_str());
|
let hashed_password = password_auth::generate_hash(req.new_password.as_str());
|
||||||
|
|
||||||
use entity::users;
|
use entity::users;
|
||||||
@@ -127,9 +148,10 @@ impl Backend {
|
|||||||
let mut user = users::Entity::find_by_id(id)
|
let mut user = users::Entity::find_by_id(id)
|
||||||
.one(self.db.orm_db())
|
.one(self.db.orm_db())
|
||||||
.await?
|
.await?
|
||||||
.ok_or(anyhow::anyhow!("User not found"))?
|
.ok_or(ChangePasswordError::UserNotFound)?
|
||||||
.into_active_model();
|
.into_active_model();
|
||||||
user.password = Set(hashed_password.clone());
|
user.password = Set(hashed_password.clone());
|
||||||
|
user.must_change_password = Set(false);
|
||||||
|
|
||||||
entity::users::Entity::update(user)
|
entity::users::Entity::update(user)
|
||||||
.exec(self.db.orm_db())
|
.exec(self.db.orm_db())
|
||||||
@@ -242,6 +264,107 @@ impl AuthzBackend for Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use axum_login::AuthnBackend;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||||
|
|
||||||
|
use super::{Backend, ChangePassword, ChangePasswordError, EMPTY_PASSWORD_MD5};
|
||||||
|
use crate::db::{entity::users, Db};
|
||||||
|
|
||||||
|
async fn find_user(db: &Db, username: &str) -> users::Model {
|
||||||
|
users::Entity::find()
|
||||||
|
.filter(users::Column::Username.eq(username))
|
||||||
|
.one(db.orm_db())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn seeded_default_users_require_password_change() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
|
||||||
|
assert!(find_user(&db, "admin").await.must_change_password);
|
||||||
|
assert!(find_user(&db, "user").await.must_change_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auto_created_user_does_not_require_password_change() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
|
||||||
|
db.auto_create_user("oidc-user").await.unwrap();
|
||||||
|
|
||||||
|
assert!(!find_user(&db, "oidc-user").await.must_change_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn change_password_clears_must_change_password_flag() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
let backend = Backend::new(db.clone());
|
||||||
|
let admin = find_user(&db, "admin").await;
|
||||||
|
|
||||||
|
backend
|
||||||
|
.change_password(
|
||||||
|
admin.id,
|
||||||
|
&ChangePassword {
|
||||||
|
new_password: "f1086f68460b65771de50a970cd1242d".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!find_user(&db, "admin").await.must_change_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn change_password_rejects_empty_password_digest() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
let backend = Backend::new(db.clone());
|
||||||
|
let admin = find_user(&db, "admin").await;
|
||||||
|
|
||||||
|
let error = backend
|
||||||
|
.change_password(
|
||||||
|
admin.id,
|
||||||
|
&ChangePassword {
|
||||||
|
new_password: EMPTY_PASSWORD_MD5.to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, ChangePasswordError::EmptyPassword));
|
||||||
|
assert!(find_user(&db, "admin").await.must_change_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_authenticate_with_new_password_after_change() {
|
||||||
|
let db = Db::memory_db().await;
|
||||||
|
let backend = Backend::new(db.clone());
|
||||||
|
let admin = find_user(&db, "admin").await;
|
||||||
|
|
||||||
|
backend
|
||||||
|
.change_password(
|
||||||
|
admin.id,
|
||||||
|
&ChangePassword {
|
||||||
|
new_password: "f1086f68460b65771de50a970cd1242d".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let authenticated = backend
|
||||||
|
.authenticate(super::Credentials {
|
||||||
|
username: "admin".to_string(),
|
||||||
|
password: "f1086f68460b65771de50a970cd1242d".to_string(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(authenticated.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We use a type alias for convenience.
|
// We use a type alias for convenience.
|
||||||
//
|
//
|
||||||
// Note that we've supplied our concrete backend here.
|
// Note that we've supplied our concrete backend here.
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
|
||||||
extract::State,
|
extract::State,
|
||||||
http::header,
|
http::header,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing,
|
routing, Router,
|
||||||
};
|
};
|
||||||
use axum_embed::ServeEmbed;
|
use axum_embed::ServeEmbed;
|
||||||
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
|
||||||
|
|
||||||
/// Embed assets for web dashboard, build frontend first
|
/// Embed assets for web dashboard, build frontend first
|
||||||
#[derive(RustEmbed, Clone)]
|
#[derive(RustEmbed, Clone)]
|
||||||
@@ -59,7 +58,7 @@ pub fn build_router(api_host: Option<url::Url>) -> Router {
|
|||||||
pub struct WebServer {
|
pub struct WebServer {
|
||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
router: Router,
|
router: Router,
|
||||||
serve_task: Option<AbortOnDropHandle<()>>,
|
serve_task: Option<ScopedTask<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebServer {
|
impl WebServer {
|
||||||
@@ -71,13 +70,14 @@ impl WebServer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(self) -> Result<AbortOnDropHandle<()>, anyhow::Error> {
|
pub async fn start(self) -> Result<ScopedTask<()>, anyhow::Error> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
let app = self.router;
|
let app = self.router;
|
||||||
|
|
||||||
let task = AbortOnDropHandle::new(tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}));
|
})
|
||||||
|
.into();
|
||||||
|
|
||||||
Ok(task)
|
Ok(task)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ impl WebhookConfig {
|
|||||||
pub struct ValidateTokenRequest {
|
pub struct ValidateTokenRequest {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
pub machine_id: String,
|
pub machine_id: String,
|
||||||
pub public_ip: Option<String>,
|
|
||||||
pub hostname: String,
|
pub hostname: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub os_type: Option<String>,
|
pub os_type: Option<String>,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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." }
|
|
||||||
]
|
|
||||||
+27
-21
@@ -3,12 +3,12 @@ name = "easytier"
|
|||||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||||
homepage = "https://github.com/EasyTier/EasyTier"
|
homepage = "https://github.com/EasyTier/EasyTier"
|
||||||
repository = "https://github.com/EasyTier/EasyTier"
|
repository = "https://github.com/EasyTier/EasyTier"
|
||||||
version = "2.6.4"
|
version = "2.6.0"
|
||||||
edition.workspace = true
|
edition = "2021"
|
||||||
rust-version.workspace = true
|
|
||||||
authors = ["kkrainbow"]
|
authors = ["kkrainbow"]
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||||
categories = ["network-programming", "command-line-utilities"]
|
categories = ["network-programming", "command-line-utilities"]
|
||||||
|
rust-version = "1.93.0"
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
@@ -50,9 +50,7 @@ time = "0.3"
|
|||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
chrono = { version = "0.4.37", features = ["serde"] }
|
chrono = { version = "0.4.37", features = ["serde"] }
|
||||||
|
|
||||||
guarden = "0.1"
|
cfg-if = "1.0"
|
||||||
|
|
||||||
delegate = "0.13.5"
|
|
||||||
|
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
|
||||||
@@ -64,13 +62,12 @@ futures = { version = "0.3", features = ["bilock", "unstable"] }
|
|||||||
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] }
|
tokio-util = { version = "0.7.9", features = ["codec", "net", "io"] }
|
||||||
|
|
||||||
async-stream = "0.3.5"
|
async-stream = "0.3.5"
|
||||||
async-trait = "0.1.74"
|
async-trait = "0.1.74"
|
||||||
|
|
||||||
dashmap = "6.0"
|
dashmap = "6.0"
|
||||||
moka = { version = "0.12", features = ["future"] }
|
|
||||||
timedmap = "=1.0.1"
|
timedmap = "=1.0.1"
|
||||||
|
|
||||||
# for full-path zero-copy
|
# for full-path zero-copy
|
||||||
@@ -168,6 +165,7 @@ network-interface = "2.0"
|
|||||||
|
|
||||||
# for ospf route
|
# for ospf route
|
||||||
petgraph = "0.8.1"
|
petgraph = "0.8.1"
|
||||||
|
hashbrown = "0.15.3"
|
||||||
ordered_hash_map = "0.5.0"
|
ordered_hash_map = "0.5.0"
|
||||||
|
|
||||||
# for wireguard
|
# for wireguard
|
||||||
@@ -244,7 +242,6 @@ hickory-server = { version = "0.25.2", features = [
|
|||||||
"resolver",
|
"resolver",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
bon = "3.9.1"
|
|
||||||
derive_builder = "0.20.2"
|
derive_builder = "0.20.2"
|
||||||
humantime-serde = "1.1.1"
|
humantime-serde = "1.1.1"
|
||||||
multimap = "0.10.1"
|
multimap = "0.10.1"
|
||||||
@@ -255,8 +252,6 @@ shellexpand = "3.1.1"
|
|||||||
|
|
||||||
# for fake tcp
|
# for fake tcp
|
||||||
flume = { version = "0.12", optional = true }
|
flume = { version = "0.12", optional = true }
|
||||||
igd-next = { version = "0.17.0", features = ["aio_tokio"] }
|
|
||||||
natpmp = "0.5.0"
|
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||||
machine-uid = "0.5.3"
|
machine-uid = "0.5.3"
|
||||||
@@ -277,15 +272,11 @@ windivert = { git = "https://github.com/EasyTier/windivert-rust.git", rev = "adc
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.62.2", features = [
|
windows = { version = "0.52.0", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_NetworkManagement_IpHelper",
|
|
||||||
"Win32_NetworkManagement_Ndis",
|
|
||||||
"Win32_NetworkManagement_WindowsFirewall",
|
"Win32_NetworkManagement_WindowsFirewall",
|
||||||
"Win32_Networking",
|
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_System_Diagnostics",
|
"Win32_Networking",
|
||||||
"Win32_System_Diagnostics_Debug",
|
|
||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
"Win32_System_Variant",
|
"Win32_System_Variant",
|
||||||
"Win32_Networking_WinSock",
|
"Win32_Networking_WinSock",
|
||||||
@@ -294,6 +285,14 @@ windows = { version = "0.62.2", features = [
|
|||||||
encoding = "0.2"
|
encoding = "0.2"
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
windows-service = "0.7.0"
|
windows-service = "0.7.0"
|
||||||
|
windows-sys = { version = "0.52", features = [
|
||||||
|
"Win32_NetworkManagement_IpHelper",
|
||||||
|
"Win32_NetworkManagement_Ndis",
|
||||||
|
"Win32_Networking_WinSock",
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_Diagnostics",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
] }
|
||||||
winapi = { version = "0.3.9", features = ["impl-default"] }
|
winapi = { version = "0.3.9", features = ["impl-default"] }
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
@@ -325,14 +324,22 @@ easytier-rpc-build = { path = "../easytier-rpc-build", features = [
|
|||||||
"internal-namespace",
|
"internal-namespace",
|
||||||
] }
|
] }
|
||||||
prost-reflect-build = { version = "0.14.0" }
|
prost-reflect-build = { version = "0.14.0" }
|
||||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
|
||||||
"win7",
|
|
||||||
] }
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
reqwest = { version = "0.12.12", features = ["blocking"] }
|
reqwest = { version = "0.12.12", features = ["blocking"] }
|
||||||
zip = "4.0.0"
|
zip = "4.0.0"
|
||||||
|
|
||||||
|
# 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",
|
||||||
|
] }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3.0.0"
|
serial_test = "3.0.0"
|
||||||
@@ -340,7 +347,6 @@ rstest = "0.25.0"
|
|||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
tempfile = "3.22.0"
|
tempfile = "3.22.0"
|
||||||
ctor = "0.8.0"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||||
defguard_wireguard_rs = "0.4.2"
|
defguard_wireguard_rs = "0.4.2"
|
||||||
|
|||||||
+5
-10
@@ -86,10 +86,8 @@ impl WindowsBuild {
|
|||||||
} else {
|
} else {
|
||||||
Self::download_protoc()
|
Self::download_protoc()
|
||||||
};
|
};
|
||||||
unsafe {
|
|
||||||
std::env::set_var("PROTOC", protoc_path);
|
std::env::set_var("PROTOC", protoc_path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workdir() -> Option<String> {
|
fn workdir() -> Option<String> {
|
||||||
@@ -143,10 +141,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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") {
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("aarch64")
|
||||||
|
{
|
||||||
thunk::thunk();
|
thunk::thunk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +191,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
)
|
)
|
||||||
.type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[derive(Hash, Eq)]")
|
.type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[derive(Hash, Eq)]")
|
||||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||||
.type_attribute("acl.Acl", "#[serde(default)]")
|
|
||||||
.type_attribute("acl.AclV1", "#[serde(default)]")
|
|
||||||
.type_attribute("acl.Chain", "#[serde(default)]")
|
|
||||||
.type_attribute("acl.Rule", "#[serde(default)]")
|
|
||||||
.type_attribute("acl.GroupInfo", "#[serde(default)]")
|
|
||||||
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
.field_attribute(".api.manage.NetworkConfig", "#[serde(default)]")
|
||||||
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
||||||
.btree_map(["."])
|
.btree_map(["."])
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ core_clap:
|
|||||||
仅用户名:--config-server admin,将使用官方的服务器
|
仅用户名:--config-server admin,将使用官方的服务器
|
||||||
machine_id:
|
machine_id:
|
||||||
en: |+
|
en: |+
|
||||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. by default it is loaded from persisted local state; on first start it may be migrated from system information or generated, then remains fixed.
|
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. default is from system.
|
||||||
zh-CN: |+
|
zh-CN: |+
|
||||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从本地持久化状态读取;首次启动时可能基于系统信息迁移或生成,之后保持固定不变。
|
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从系统获得。
|
||||||
config_file:
|
config_file:
|
||||||
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
||||||
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
||||||
@@ -39,15 +39,6 @@ core_clap:
|
|||||||
ipv6:
|
ipv6:
|
||||||
en: "ipv6 address of this vpn node, can be used together with ipv4 for dual-stack operation"
|
en: "ipv6 address of this vpn node, can be used together with ipv4 for dual-stack operation"
|
||||||
zh-CN: "此VPN节点的IPv6地址,可与IPv4一起使用以进行双栈操作"
|
zh-CN: "此VPN节点的IPv6地址,可与IPv4一起使用以进行双栈操作"
|
||||||
ipv6_public_addr_provider:
|
|
||||||
en: "share this node's public IPv6 subnet with other peers so they can obtain public IPv6 addresses (Linux only)"
|
|
||||||
zh-CN: "将此节点的公网 IPv6 子网共享给其他节点,使它们也能获得公网 IPv6 地址(仅 Linux 支持)"
|
|
||||||
ipv6_public_addr_auto:
|
|
||||||
en: "auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet"
|
|
||||||
zh-CN: "自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址"
|
|
||||||
ipv6_public_addr_prefix:
|
|
||||||
en: "manually specify the public IPv6 subnet to share, instead of auto-detecting from system routes"
|
|
||||||
zh-CN: "手动指定要共享的公网 IPv6 子网,不自动从系统路由检测"
|
|
||||||
dhcp:
|
dhcp:
|
||||||
en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed."
|
en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed."
|
||||||
zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。"
|
zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。"
|
||||||
@@ -181,12 +172,6 @@ core_clap:
|
|||||||
disable_sym_hole_punching:
|
disable_sym_hole_punching:
|
||||||
en: "if true, disable udp nat hole punching for symmetric nat (NAT4), which is based on birthday attack and may be blocked by ISP."
|
en: "if true, disable udp nat hole punching for symmetric nat (NAT4), which is based on birthday attack and may be blocked by ISP."
|
||||||
zh-CN: "如果为true,则禁用基于生日攻击的对称NAT (NAT4) UDP 打洞功能,该打洞方式可能会被运营商封锁"
|
zh-CN: "如果为true,则禁用基于生日攻击的对称NAT (NAT4) UDP 打洞功能,该打洞方式可能会被运营商封锁"
|
||||||
disable_upnp:
|
|
||||||
en: "disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default"
|
|
||||||
zh-CN: "禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启"
|
|
||||||
enable_udp_broadcast_relay:
|
|
||||||
en: "Windows only: capture local UDP broadcast packets from physical interfaces and forward them to EasyTier peers. Helps games to find rooms in local network. Requires administrator privileges."
|
|
||||||
zh-CN: "仅 Windows:捕获物理网卡上的本机 UDP 广播包并转发给 EasyTier 对等节点,帮助局域网游戏发现房间。需要管理员权限。"
|
|
||||||
relay_all_peer_rpc:
|
relay_all_peer_rpc:
|
||||||
en: "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."
|
en: "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."
|
||||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||||
@@ -277,9 +262,6 @@ core_clap:
|
|||||||
check_config:
|
check_config:
|
||||||
en: Check config validity without starting the network
|
en: Check config validity without starting the network
|
||||||
zh-CN: 检查配置文件的有效性并退出
|
zh-CN: 检查配置文件的有效性并退出
|
||||||
daemon:
|
|
||||||
en: Run in daemon mode
|
|
||||||
zh-CN: 以守护进程模式运行
|
|
||||||
file_log_size_mb:
|
file_log_size_mb:
|
||||||
en: "per file log size in MB, default is 100MB"
|
en: "per file log size in MB, default is 100MB"
|
||||||
zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB"
|
zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB"
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ use std::{io, mem::ManuallyDrop, net::SocketAddr, os::windows::io::AsRawSocket};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use network_interface::NetworkInterfaceConfig;
|
use network_interface::NetworkInterfaceConfig;
|
||||||
use windows::{
|
use windows::{
|
||||||
|
core::BSTR,
|
||||||
Win32::{
|
Win32::{
|
||||||
Foundation::FALSE,
|
Foundation::{BOOL, FALSE},
|
||||||
NetworkManagement::WindowsFirewall::{
|
NetworkManagement::WindowsFirewall::{
|
||||||
INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_DOMAIN,
|
INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_DOMAIN,
|
||||||
NET_FW_PROFILE2_PRIVATE, NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN,
|
NET_FW_PROFILE2_PRIVATE, NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN,
|
||||||
NET_FW_RULE_DIR_OUT,
|
NET_FW_RULE_DIR_OUT,
|
||||||
},
|
},
|
||||||
Networking::WinSock::{
|
Networking::WinSock::{
|
||||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET,
|
htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6,
|
||||||
SOCKET_ERROR, WSAGetLastError, WSAIoctl, htonl, setsockopt,
|
IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR,
|
||||||
},
|
},
|
||||||
System::Com::{
|
System::Com::{
|
||||||
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
|
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
|
||||||
},
|
},
|
||||||
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
|
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
|
||||||
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
|
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
|
||||||
},
|
},
|
||||||
core::{BOOL, BSTR},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
|
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
|
||||||
@@ -88,7 +88,13 @@ pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ip_unicast_if(socket: SOCKET, addr: &SocketAddr, iface: &str) -> io::Result<()> {
|
pub fn set_ip_unicast_if<S: AsRawSocket>(
|
||||||
|
socket: &S,
|
||||||
|
addr: &SocketAddr,
|
||||||
|
iface: &str,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let handle = SOCKET(socket.as_raw_socket() as usize);
|
||||||
|
|
||||||
let if_index = find_interface_index(iface)?;
|
let if_index = find_interface_index(iface)?;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -97,12 +103,12 @@ pub fn set_ip_unicast_if(socket: SOCKET, addr: &SocketAddr, iface: &str) -> io::
|
|||||||
SocketAddr::V4(..) => {
|
SocketAddr::V4(..) => {
|
||||||
let if_index = htonl(if_index);
|
let if_index = htonl(if_index);
|
||||||
let if_index_bytes = if_index.to_ne_bytes();
|
let if_index_bytes = if_index.to_ne_bytes();
|
||||||
setsockopt(socket, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes))
|
setsockopt(handle, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes))
|
||||||
}
|
}
|
||||||
SocketAddr::V6(..) => {
|
SocketAddr::V6(..) => {
|
||||||
let if_index_bytes = if_index.to_ne_bytes();
|
let if_index_bytes = if_index.to_ne_bytes();
|
||||||
setsockopt(
|
setsockopt(
|
||||||
socket,
|
handle,
|
||||||
IPPROTO_IPV6.0,
|
IPPROTO_IPV6.0,
|
||||||
IPV6_UNICAST_IF,
|
IPV6_UNICAST_IF,
|
||||||
Some(&if_index_bytes),
|
Some(&if_index_bytes),
|
||||||
@@ -135,17 +141,8 @@ pub fn setup_socket_for_win<S: AsRawSocket>(
|
|||||||
disable_connection_reset(socket)?;
|
disable_connection_reset(socket)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket = SOCKET(socket.as_raw_socket() as usize);
|
|
||||||
|
|
||||||
// let optval = 1_i32.to_ne_bytes();
|
|
||||||
// unsafe {
|
|
||||||
// if setsockopt(socket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, Some(&optval)) == SOCKET_ERROR {
|
|
||||||
// return Err(io::Error::last_os_error());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if let Some(iface) = bind_dev {
|
if let Some(iface) = bind_dev {
|
||||||
set_ip_unicast_if(socket, bind_addr, &iface)?;
|
set_ip_unicast_if(socket, bind_addr, iface.as_str())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -155,7 +152,7 @@ struct ComInitializer;
|
|||||||
|
|
||||||
impl ComInitializer {
|
impl ComInitializer {
|
||||||
fn new() -> windows::core::Result<Self> {
|
fn new() -> windows::core::Result<Self> {
|
||||||
unsafe { CoInitializeEx(None, COINIT_MULTITHREADED).ok()? };
|
unsafe { CoInitializeEx(None, COINIT_MULTITHREADED)? };
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,7 +345,7 @@ fn add_protocol_firewall_rules(
|
|||||||
|
|
||||||
SafeArrayPutElement(
|
SafeArrayPutElement(
|
||||||
interface_array,
|
interface_array,
|
||||||
&index as *const _,
|
&index as *const _ as *const i32,
|
||||||
&variant_interface as *const _ as *const std::ffi::c_void,
|
&variant_interface as *const _ as *const std::ffi::c_void,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -357,7 +354,7 @@ fn add_protocol_firewall_rules(
|
|||||||
(*interface_variant.Anonymous.Anonymous).vt = VARENUM(VT_ARRAY.0 | VT_VARIANT.0);
|
(*interface_variant.Anonymous.Anonymous).vt = VARENUM(VT_ARRAY.0 | VT_VARIANT.0);
|
||||||
(*interface_variant.Anonymous.Anonymous).Anonymous.parray = interface_array;
|
(*interface_variant.Anonymous.Anonymous).Anonymous.parray = interface_array;
|
||||||
|
|
||||||
rule.SetInterfaces(&interface_variant)?;
|
rule.SetInterfaces(interface_variant)?;
|
||||||
|
|
||||||
// Get rule collection and add new rule
|
// Get rule collection and add new rule
|
||||||
let rules = policy.Rules()?;
|
let rules = policy.Rules()?;
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ impl AclProcessor {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Sort by priority (higher priority first)
|
// Sort by priority (higher priority first)
|
||||||
rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
|
rules.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||||
|
|
||||||
match chain.chain_type() {
|
match chain.chain_type() {
|
||||||
ChainType::Inbound => inbound_rules.extend(rules),
|
ChainType::Inbound => inbound_rules.extend(rules),
|
||||||
@@ -507,7 +507,7 @@ impl AclProcessor {
|
|||||||
matched_rule: Some(RuleId::Default),
|
matched_rule: Some(RuleId::Default),
|
||||||
should_log: false,
|
should_log: false,
|
||||||
log_context: Some(AclLogContext::UnsupportedChainType),
|
log_context: Some(AclLogContext::UnsupportedChainType),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -679,9 +679,8 @@ impl AclProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Source port check
|
// Source port check
|
||||||
if let Some(src_port) = packet_info.src_port
|
if let Some(src_port) = packet_info.src_port {
|
||||||
&& !rule.src_port_ranges.is_empty()
|
if !rule.src_port_ranges.is_empty() {
|
||||||
{
|
|
||||||
let matches = rule
|
let matches = rule
|
||||||
.src_port_ranges
|
.src_port_ranges
|
||||||
.iter()
|
.iter()
|
||||||
@@ -690,11 +689,11 @@ impl AclProcessor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Destination port check
|
// Destination port check
|
||||||
if let Some(dst_port) = packet_info.dst_port
|
if let Some(dst_port) = packet_info.dst_port {
|
||||||
&& !rule.dst_port_ranges.is_empty()
|
if !rule.dst_port_ranges.is_empty() {
|
||||||
{
|
|
||||||
let matches = rule
|
let matches = rule
|
||||||
.dst_port_ranges
|
.dst_port_ranges
|
||||||
.iter()
|
.iter()
|
||||||
@@ -703,6 +702,7 @@ impl AclProcessor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Source group check
|
// Source group check
|
||||||
if !rule.source_groups.is_empty() {
|
if !rule.source_groups.is_empty() {
|
||||||
@@ -1339,45 +1339,6 @@ mod tests {
|
|||||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(70)));
|
assert_eq!(result.matched_rule, Some(RuleId::Priority(70)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_forward_acl_source_ip_whitelist() {
|
|
||||||
let mut acl_config = Acl::default();
|
|
||||||
let mut acl_v1 = AclV1::default();
|
|
||||||
let mut chain = Chain {
|
|
||||||
name: "subnet_proxy_protect".to_string(),
|
|
||||||
chain_type: ChainType::Forward as i32,
|
|
||||||
enabled: true,
|
|
||||||
default_action: Action::Drop as i32,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
chain.rules.push(Rule {
|
|
||||||
name: "allow_my_devices".to_string(),
|
|
||||||
priority: 1000,
|
|
||||||
enabled: true,
|
|
||||||
action: Action::Allow as i32,
|
|
||||||
protocol: Protocol::Any as i32,
|
|
||||||
source_ips: vec!["10.172.192.2/32".to_string()],
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
acl_v1.chains.push(chain);
|
|
||||||
acl_config.acl_v1 = Some(acl_v1);
|
|
||||||
|
|
||||||
let processor = AclProcessor::new(acl_config);
|
|
||||||
let mut packet_info = create_test_packet_info();
|
|
||||||
packet_info.dst_ip = "192.168.1.10".parse().unwrap();
|
|
||||||
|
|
||||||
packet_info.src_ip = "10.172.192.2".parse().unwrap();
|
|
||||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
|
||||||
assert_eq!(result.action, Action::Allow);
|
|
||||||
assert_eq!(result.matched_rule, Some(RuleId::Priority(1000)));
|
|
||||||
|
|
||||||
packet_info.src_ip = "10.172.192.3".parse().unwrap();
|
|
||||||
let result = processor.process_packet(&packet_info, ChainType::Forward);
|
|
||||||
assert_eq!(result.action, Action::Drop);
|
|
||||||
assert_eq!(result.matched_rule, Some(RuleId::Default));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_test_acl_config() -> Acl {
|
fn create_test_acl_config() -> Acl {
|
||||||
let mut acl_config = Acl::default();
|
let mut acl_config = Acl::default();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use zstd::bulk;
|
|||||||
|
|
||||||
use zerocopy::{AsBytes as _, FromBytes as _};
|
use zerocopy::{AsBytes as _, FromBytes as _};
|
||||||
|
|
||||||
use crate::tunnel::packet_def::{COMPRESSOR_TAIL_SIZE, CompressorAlgo, CompressorTail, ZCPacket};
|
use crate::tunnel::packet_def::{CompressorAlgo, CompressorTail, ZCPacket, COMPRESSOR_TAIL_SIZE};
|
||||||
|
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
|||||||
+63
-387
@@ -6,9 +6,10 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||||
use clap::ValueEnum;
|
use cfg_if::cfg_if;
|
||||||
use clap::builder::PossibleValue;
|
use clap::builder::PossibleValue;
|
||||||
|
use clap::ValueEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum::{Display, EnumString, VariantArray};
|
use strum::{Display, EnumString, VariantArray};
|
||||||
use tokio::io::AsyncReadExt as _;
|
use tokio::io::AsyncReadExt as _;
|
||||||
@@ -18,10 +19,9 @@ use crate::{
|
|||||||
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
||||||
proto::{
|
proto::{
|
||||||
acl::Acl,
|
acl::Acl,
|
||||||
api::manage::ConfigSource as RpcConfigSource,
|
|
||||||
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
||||||
},
|
},
|
||||||
tunnel::{IpScheme, TunnelScheme, generate_digest_from_str},
|
tunnel::generate_digest_from_str,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::env_parser;
|
use super::env_parser;
|
||||||
@@ -70,42 +70,9 @@ pub fn gen_default_flags() -> Flags {
|
|||||||
quic_listen_port: u32::MAX,
|
quic_listen_port: u32::MAX,
|
||||||
need_p2p: false,
|
need_p2p: false,
|
||||||
instance_recv_bps_limit: u64::MAX,
|
instance_recv_bps_limit: u64::MAX,
|
||||||
disable_upnp: false,
|
|
||||||
disable_relay_data: false,
|
|
||||||
enable_udp_broadcast_relay: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mapped_listener_allows_implicit_port(url: &url::Url) -> bool {
|
|
||||||
TunnelScheme::try_from(url)
|
|
||||||
.ok()
|
|
||||||
.and_then(|scheme| IpScheme::try_from(scheme).ok())
|
|
||||||
.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_mapped_listener_url(url: &url::Url) -> Result<(), anyhow::Error> {
|
|
||||||
if url.port().is_none() && !mapped_listener_allows_implicit_port(url) {
|
|
||||||
anyhow::bail!("mapped listener port is missing: {}", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_mapped_listener_urls(
|
|
||||||
mapped_listeners: &[String],
|
|
||||||
) -> Result<Vec<url::Url>, anyhow::Error> {
|
|
||||||
mapped_listeners
|
|
||||||
.iter()
|
|
||||||
.map(|s| {
|
|
||||||
let url: url::Url = s
|
|
||||||
.parse()
|
|
||||||
.with_context(|| format!("mapped listener is not a valid url: {}", s))?;
|
|
||||||
validate_mapped_listener_url(&url)?;
|
|
||||||
Ok(url)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Display, EnumString, VariantArray)]
|
#[derive(Debug, Clone, PartialEq, Eq, Display, EnumString, VariantArray)]
|
||||||
#[strum(ascii_case_insensitive)]
|
#[strum(ascii_case_insensitive)]
|
||||||
pub enum EncryptionAlgorithm {
|
pub enum EncryptionAlgorithm {
|
||||||
@@ -142,9 +109,10 @@ impl ValueEnum for EncryptionAlgorithm {
|
|||||||
#[allow(clippy::derivable_impls)]
|
#[allow(clippy::derivable_impls)]
|
||||||
impl Default for EncryptionAlgorithm {
|
impl Default for EncryptionAlgorithm {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
cfg_select! {
|
cfg_if! {
|
||||||
any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto") => EncryptionAlgorithm::AesGcm,
|
if #[cfg(any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto"))] {
|
||||||
_ => {
|
EncryptionAlgorithm::AesGcm
|
||||||
|
} else {
|
||||||
crate::common::log::warn!("no AEAD encryption algorithm is available, using INSECURE XOR");
|
crate::common::log::warn!("no AEAD encryption algorithm is available, using INSECURE XOR");
|
||||||
EncryptionAlgorithm::Xor
|
EncryptionAlgorithm::Xor
|
||||||
}
|
}
|
||||||
@@ -172,15 +140,6 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
|
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
|
||||||
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
|
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
|
||||||
|
|
||||||
fn get_ipv6_public_addr_provider(&self) -> bool;
|
|
||||||
fn set_ipv6_public_addr_provider(&self, enabled: bool);
|
|
||||||
|
|
||||||
fn get_ipv6_public_addr_auto(&self) -> bool;
|
|
||||||
fn set_ipv6_public_addr_auto(&self, enabled: bool);
|
|
||||||
|
|
||||||
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr>;
|
|
||||||
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>);
|
|
||||||
|
|
||||||
fn get_dhcp(&self) -> bool;
|
fn get_dhcp(&self) -> bool;
|
||||||
fn set_dhcp(&self, dhcp: bool);
|
fn set_dhcp(&self, dhcp: bool);
|
||||||
|
|
||||||
@@ -248,11 +207,6 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
}
|
}
|
||||||
fn set_credential_file(&self, _path: Option<std::path::PathBuf>) {}
|
fn set_credential_file(&self, _path: Option<std::path::PathBuf>) {}
|
||||||
|
|
||||||
fn get_network_config_source(&self) -> ConfigSource {
|
|
||||||
ConfigSource::User
|
|
||||||
}
|
|
||||||
fn set_network_config_source(&self, _source: Option<ConfigSource>) {}
|
|
||||||
|
|
||||||
fn dump(&self) -> String;
|
fn dump(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,55 +226,6 @@ pub struct NetworkIdentity {
|
|||||||
pub network_secret_digest: Option<NetworkSecretDigest>,
|
pub network_secret_digest: Option<NetworkSecretDigest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ConfigSource {
|
|
||||||
#[default]
|
|
||||||
User,
|
|
||||||
Webhook,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigSource {
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::User => "user",
|
|
||||||
Self::Webhook => "webhook",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_rpc(source: i32) -> Option<Self> {
|
|
||||||
match RpcConfigSource::try_from(source).ok() {
|
|
||||||
Some(RpcConfigSource::Webhook) => Some(Self::Webhook),
|
|
||||||
Some(RpcConfigSource::User) => Some(Self::User),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_rpc(self) -> i32 {
|
|
||||||
match self {
|
|
||||||
Self::User => RpcConfigSource::User as i32,
|
|
||||||
Self::Webhook => RpcConfigSource::Webhook as i32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for ConfigSource {
|
|
||||||
type Err = String;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s {
|
|
||||||
"user" => Ok(Self::User),
|
|
||||||
"webhook" => Ok(Self::Webhook),
|
|
||||||
other => Err(format!("unknown network config source: {other}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
|
||||||
struct ConfigSourceConfig {
|
|
||||||
source: ConfigSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash)]
|
#[derive(Eq, PartialEq, Hash)]
|
||||||
struct NetworkIdentityWithOnlyDigest {
|
struct NetworkIdentityWithOnlyDigest {
|
||||||
network_name: String,
|
network_name: String,
|
||||||
@@ -530,9 +435,6 @@ struct Config {
|
|||||||
instance_id: Option<uuid::Uuid>,
|
instance_id: Option<uuid::Uuid>,
|
||||||
ipv4: Option<String>,
|
ipv4: Option<String>,
|
||||||
ipv6: Option<String>,
|
ipv6: Option<String>,
|
||||||
ipv6_public_addr_provider: Option<bool>,
|
|
||||||
ipv6_public_addr_auto: Option<bool>,
|
|
||||||
ipv6_public_addr_prefix: Option<String>,
|
|
||||||
dhcp: Option<bool>,
|
dhcp: Option<bool>,
|
||||||
network_identity: Option<NetworkIdentity>,
|
network_identity: Option<NetworkIdentity>,
|
||||||
listeners: Option<Vec<url::Url>>,
|
listeners: Option<Vec<url::Url>>,
|
||||||
@@ -565,7 +467,6 @@ struct Config {
|
|||||||
stun_servers_v6: Option<Vec<String>>,
|
stun_servers_v6: Option<Vec<String>>,
|
||||||
|
|
||||||
credential_file: Option<PathBuf>,
|
credential_file: Option<PathBuf>,
|
||||||
source: Option<ConfigSourceConfig>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -580,21 +481,10 @@ impl Default for TomlConfigLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TomlConfigLoader {
|
impl TomlConfigLoader {
|
||||||
fn normalize_config_source(config: &mut Config) {
|
|
||||||
if matches!(
|
|
||||||
config.source.as_ref().map(|source| source.source),
|
|
||||||
Some(ConfigSource::User)
|
|
||||||
) {
|
|
||||||
config.source = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
||||||
let mut config = toml::de::from_str::<Config>(config_str)
|
let mut config = toml::de::from_str::<Config>(config_str)
|
||||||
.with_context(|| format!("failed to parse config file: {}", config_str))?;
|
.with_context(|| format!("failed to parse config file: {}", config_str))?;
|
||||||
|
|
||||||
Self::normalize_config_source(&mut config);
|
|
||||||
|
|
||||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||||
|
|
||||||
let config = TomlConfigLoader {
|
let config = TomlConfigLoader {
|
||||||
@@ -714,43 +604,6 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().ipv6 = addr.map(|addr| addr.to_string());
|
self.config.lock().unwrap().ipv6 = addr.map(|addr| addr.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ipv6_public_addr_provider(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.ipv6_public_addr_provider
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_ipv6_public_addr_provider(&self, enabled: bool) {
|
|
||||||
self.config.lock().unwrap().ipv6_public_addr_provider = Some(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ipv6_public_addr_auto(&self) -> bool {
|
|
||||||
self.config
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.ipv6_public_addr_auto
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_ipv6_public_addr_auto(&self, enabled: bool) {
|
|
||||||
self.config.lock().unwrap().ipv6_public_addr_auto = Some(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
|
|
||||||
let locked_config = self.config.lock().unwrap();
|
|
||||||
locked_config
|
|
||||||
.ipv6_public_addr_prefix
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>) {
|
|
||||||
self.config.lock().unwrap().ipv6_public_addr_prefix =
|
|
||||||
prefix.map(|prefix| prefix.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_dhcp(&self) -> bool {
|
fn get_dhcp(&self) -> bool {
|
||||||
self.config.lock().unwrap().dhcp.unwrap_or_default()
|
self.config.lock().unwrap().dhcp.unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -768,15 +621,15 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
if locked_config.proxy_network.is_none() {
|
if locked_config.proxy_network.is_none() {
|
||||||
locked_config.proxy_network = Some(vec![]);
|
locked_config.proxy_network = Some(vec![]);
|
||||||
}
|
}
|
||||||
if let Some(mapped_cidr) = mapped_cidr.as_ref()
|
if let Some(mapped_cidr) = mapped_cidr.as_ref() {
|
||||||
&& cidr.network_length() != mapped_cidr.network_length()
|
if cidr.network_length() != mapped_cidr.network_length() {
|
||||||
{
|
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"Mapped CIDR must have the same network length as the original CIDR: {} != {}",
|
"Mapped CIDR must have the same network length as the original CIDR: {} != {}",
|
||||||
cidr.network_length(),
|
cidr.network_length(),
|
||||||
mapped_cidr.network_length()
|
mapped_cidr.network_length()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// insert if no duplicate
|
// insert if no duplicate
|
||||||
if !locked_config
|
if !locked_config
|
||||||
.proxy_network
|
.proxy_network
|
||||||
@@ -1015,23 +868,6 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().credential_file = path;
|
self.config.lock().unwrap().credential_file = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_network_config_source(&self) -> ConfigSource {
|
|
||||||
self.config
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.source
|
|
||||||
.as_ref()
|
|
||||||
.map(|source| source.source)
|
|
||||||
.unwrap_or(ConfigSource::User)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_network_config_source(&self, source: Option<ConfigSource>) {
|
|
||||||
self.config.lock().unwrap().source = source.and_then(|source| match source {
|
|
||||||
ConfigSource::User => None,
|
|
||||||
other => Some(ConfigSourceConfig { source: other }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump(&self) -> String {
|
fn dump(&self) -> String {
|
||||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||||
let default_flags_hashmap =
|
let default_flags_hashmap =
|
||||||
@@ -1045,15 +881,14 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
|
|
||||||
let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default();
|
let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default();
|
||||||
for (key, value) in default_flags_hashmap {
|
for (key, value) in default_flags_hashmap {
|
||||||
if let Some(v) = cur_flags_hashmap.get(&key)
|
if let Some(v) = cur_flags_hashmap.get(&key) {
|
||||||
&& *v != value
|
if *v != value {
|
||||||
{
|
|
||||||
flag_map.insert(key, v.clone());
|
flag_map.insert(key, v.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut config = self.config.lock().unwrap().clone();
|
let mut config = self.config.lock().unwrap().clone();
|
||||||
Self::normalize_config_source(&mut config);
|
|
||||||
config.flags = Some(flag_map);
|
config.flags = Some(flag_map);
|
||||||
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
||||||
config.stun_servers = None;
|
config.stun_servers = None;
|
||||||
@@ -1254,7 +1089,6 @@ pub async fn load_config_from_file(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::{remove_env_var, set_env_var};
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
@@ -1292,162 +1126,6 @@ stun_servers = [
|
|||||||
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
assert_eq!(stun_servers[2], "txt:stun.easytier.cn");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_network_config_source_toml_roundtrip() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
assert_eq!(config.get_network_config_source(), ConfigSource::User);
|
|
||||||
|
|
||||||
config.set_network_config_source(Some(ConfigSource::Webhook));
|
|
||||||
let dumped = config.dump();
|
|
||||||
|
|
||||||
assert!(dumped.contains("[source]"));
|
|
||||||
assert!(dumped.contains("source = \"webhook\""));
|
|
||||||
|
|
||||||
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
||||||
assert_eq!(loaded.get_network_config_source(), ConfigSource::Webhook);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_mapped_listener_urls_allows_ws_without_port() {
|
|
||||||
let parsed = parse_mapped_listener_urls(&[
|
|
||||||
"ws://example.com".to_string(),
|
|
||||||
"wss://example.com/path".to_string(),
|
|
||||||
])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(parsed.len(), 2);
|
|
||||||
assert_eq!(parsed[0].scheme(), "ws");
|
|
||||||
assert_eq!(parsed[0].port(), None);
|
|
||||||
assert_eq!(parsed[1].scheme(), "wss");
|
|
||||||
assert_eq!(parsed[1].port(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_mapped_listener_urls_allows_tcp_without_port() {
|
|
||||||
let parsed = parse_mapped_listener_urls(&["tcp://127.0.0.1".to_string()]).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(parsed.len(), 1);
|
|
||||||
assert_eq!(parsed[0].scheme(), "tcp");
|
|
||||||
assert_eq!(parsed[0].port(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_mapped_listener_urls_requires_port_for_non_ip_scheme() {
|
|
||||||
let err = parse_mapped_listener_urls(&["ring://peer-id".to_string()]).unwrap_err();
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("mapped listener port is missing"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
|
|
||||||
use crate::proto::acl::{Action, ChainType, Protocol};
|
|
||||||
|
|
||||||
let config_str = r#"
|
|
||||||
[[acl.acl_v1.chains]]
|
|
||||||
name = "subnet_proxy_protect"
|
|
||||||
chain_type = 3
|
|
||||||
enabled = true
|
|
||||||
default_action = 2
|
|
||||||
|
|
||||||
[[acl.acl_v1.chains.rules]]
|
|
||||||
name = "allow_my_devices"
|
|
||||||
priority = 1000
|
|
||||||
action = 1
|
|
||||||
source_ips = ["10.172.192.2/32"]
|
|
||||||
protocol = 5
|
|
||||||
enabled = true
|
|
||||||
"#;
|
|
||||||
|
|
||||||
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
|
||||||
let acl = config.get_acl().unwrap();
|
|
||||||
let acl_v1 = acl.acl_v1.unwrap();
|
|
||||||
let chain = &acl_v1.chains[0];
|
|
||||||
let rule = &chain.rules[0];
|
|
||||||
|
|
||||||
assert_eq!(chain.chain_type, ChainType::Forward as i32);
|
|
||||||
assert_eq!(chain.default_action, Action::Drop as i32);
|
|
||||||
assert_eq!(rule.action, Action::Allow as i32);
|
|
||||||
assert_eq!(rule.protocol, Protocol::Any as i32);
|
|
||||||
assert_eq!(rule.source_ips, vec!["10.172.192.2/32"]);
|
|
||||||
assert!(rule.ports.is_empty());
|
|
||||||
assert!(rule.source_ports.is_empty());
|
|
||||||
assert!(rule.destination_ips.is_empty());
|
|
||||||
assert!(rule.source_groups.is_empty());
|
|
||||||
assert!(rule.destination_groups.is_empty());
|
|
||||||
assert_eq!(rule.rate_limit, 0);
|
|
||||||
assert_eq!(rule.burst_limit, 0);
|
|
||||||
assert!(!rule.stateful);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_acl_toml_group_can_omit_declares_or_members() {
|
|
||||||
let declares_only = r#"
|
|
||||||
[acl.acl_v1.group]
|
|
||||||
|
|
||||||
[[acl.acl_v1.group.declares]]
|
|
||||||
group_name = "admin"
|
|
||||||
group_secret = "admin-pw"
|
|
||||||
"#;
|
|
||||||
let config = TomlConfigLoader::new_from_str(declares_only).unwrap();
|
|
||||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
|
||||||
assert_eq!(group.declares.len(), 1);
|
|
||||||
assert!(group.members.is_empty());
|
|
||||||
|
|
||||||
let members_only = r#"
|
|
||||||
[acl.acl_v1.group]
|
|
||||||
members = ["admin"]
|
|
||||||
"#;
|
|
||||||
let config = TomlConfigLoader::new_from_str(members_only).unwrap();
|
|
||||||
let group = config.get_acl().unwrap().acl_v1.unwrap().group.unwrap();
|
|
||||||
assert!(group.declares.is_empty());
|
|
||||||
assert_eq!(group.members, vec!["admin"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_network_config_source_user_is_implicit() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
config.set_network_config_source(Some(ConfigSource::User));
|
|
||||||
let dumped = config.dump();
|
|
||||||
|
|
||||||
assert!(!dumped.contains("[source]"));
|
|
||||||
|
|
||||||
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
||||||
assert_eq!(loaded.get_network_config_source(), ConfigSource::User);
|
|
||||||
|
|
||||||
let explicit_user = TomlConfigLoader::new_from_str(
|
|
||||||
r#"
|
|
||||||
[source]
|
|
||||||
source = "user"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
explicit_user.get_network_config_source(),
|
|
||||||
ConfigSource::User
|
|
||||||
);
|
|
||||||
assert!(!explicit_user.dump().contains("[source]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ipv6_public_addr_config_roundtrip() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let prefix: cidr::Ipv6Cidr = "2001:db8:100::/64".parse().unwrap();
|
|
||||||
|
|
||||||
config.set_ipv6_public_addr_provider(true);
|
|
||||||
config.set_ipv6_public_addr_auto(true);
|
|
||||||
config.set_ipv6_public_addr_prefix(Some(prefix));
|
|
||||||
|
|
||||||
assert!(config.get_ipv6_public_addr_provider());
|
|
||||||
assert!(config.get_ipv6_public_addr_auto());
|
|
||||||
assert_eq!(config.get_ipv6_public_addr_prefix(), Some(prefix));
|
|
||||||
|
|
||||||
let dumped = config.dump();
|
|
||||||
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
|
||||||
assert!(loaded.get_ipv6_public_addr_provider());
|
|
||||||
assert!(loaded.get_ipv6_public_addr_auto());
|
|
||||||
assert_eq!(loaded.get_ipv6_public_addr_prefix(), Some(prefix));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn full_example_test() {
|
async fn full_example_test() {
|
||||||
let config_str = r#"
|
let config_str = r#"
|
||||||
@@ -1534,8 +1212,8 @@ proto = "tcp"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_env_var_expansion_and_readonly_flag() {
|
async fn test_env_var_expansion_and_readonly_flag() {
|
||||||
// 设置测试环境变量
|
// 设置测试环境变量
|
||||||
set_env_var("TEST_SECRET", "my-test-secret-123");
|
std::env::set_var("TEST_SECRET", "my-test-secret-123");
|
||||||
set_env_var("TEST_NETWORK", "test-network");
|
std::env::set_var("TEST_NETWORK", "test-network");
|
||||||
|
|
||||||
// 创建临时配置文件,包含环境变量占位符
|
// 创建临时配置文件,包含环境变量占位符
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
@@ -1575,8 +1253,8 @@ network_secret = "${TEST_SECRET}"
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 清理环境变量
|
// 清理环境变量
|
||||||
remove_env_var("TEST_SECRET");
|
std::env::remove_var("TEST_SECRET");
|
||||||
remove_env_var("TEST_NETWORK");
|
std::env::remove_var("TEST_NETWORK");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RPC API 安全测试(只读配置保护)
|
/// RPC API 安全测试(只读配置保护)
|
||||||
@@ -1589,7 +1267,7 @@ network_secret = "${TEST_SECRET}"
|
|||||||
/// `easytier/src/rpc_service/instance_manage.rs` 中实现
|
/// `easytier/src/rpc_service/instance_manage.rs` 中实现
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_readonly_config_api_protection() {
|
async fn test_readonly_config_api_protection() {
|
||||||
set_env_var("API_TEST_SECRET", "secret-value");
|
std::env::set_var("API_TEST_SECRET", "secret-value");
|
||||||
|
|
||||||
// 创建包含环境变量的配置
|
// 创建包含环境变量的配置
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
@@ -1620,7 +1298,7 @@ network_secret = "${API_TEST_SECRET}"
|
|||||||
"Permission flag should be set correctly"
|
"Permission flag should be set correctly"
|
||||||
);
|
);
|
||||||
|
|
||||||
remove_env_var("API_TEST_SECRET");
|
std::env::remove_var("API_TEST_SECRET");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLI 参数测试(--disable-env-parsing 开关)
|
/// CLI 参数测试(--disable-env-parsing 开关)
|
||||||
@@ -1630,7 +1308,7 @@ network_secret = "${API_TEST_SECRET}"
|
|||||||
/// - 配置不会被标记为只读
|
/// - 配置不会被标记为只读
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_disable_env_parsing_flag() {
|
async fn test_disable_env_parsing_flag() {
|
||||||
set_env_var("DISABLED_TEST_VAR", "should-not-expand");
|
std::env::set_var("DISABLED_TEST_VAR", "should-not-expand");
|
||||||
|
|
||||||
// 创建包含环境变量占位符的配置
|
// 创建包含环境变量占位符的配置
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
@@ -1668,7 +1346,7 @@ network_secret = "${DISABLED_TEST_VAR}"
|
|||||||
"Config should be NO_DELETE due to no config_dir, not env vars"
|
"Config should be NO_DELETE due to no config_dir, not env vars"
|
||||||
);
|
);
|
||||||
|
|
||||||
remove_env_var("DISABLED_TEST_VAR");
|
std::env::remove_var("DISABLED_TEST_VAR");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 多实例隔离测试
|
/// 多实例隔离测试
|
||||||
@@ -1679,8 +1357,8 @@ network_secret = "${DISABLED_TEST_VAR}"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_multiple_instances_with_different_env_vars() {
|
async fn test_multiple_instances_with_different_env_vars() {
|
||||||
// 实例1:使用第一组环境变量
|
// 实例1:使用第一组环境变量
|
||||||
set_env_var("INSTANCE_SECRET", "instance1-secret");
|
std::env::set_var("INSTANCE_SECRET", "instance1-secret");
|
||||||
set_env_var("INSTANCE_NAME", "instance-one");
|
std::env::set_var("INSTANCE_NAME", "instance-one");
|
||||||
|
|
||||||
let mut temp_file1 = NamedTempFile::new().unwrap();
|
let mut temp_file1 = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -1710,8 +1388,8 @@ network_secret = "${INSTANCE_SECRET}"
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 实例2:修改环境变量后加载同一模板
|
// 实例2:修改环境变量后加载同一模板
|
||||||
set_env_var("INSTANCE_SECRET", "instance2-secret");
|
std::env::set_var("INSTANCE_SECRET", "instance2-secret");
|
||||||
set_env_var("INSTANCE_NAME", "instance-two");
|
std::env::set_var("INSTANCE_NAME", "instance-two");
|
||||||
|
|
||||||
let mut temp_file2 = NamedTempFile::new().unwrap();
|
let mut temp_file2 = NamedTempFile::new().unwrap();
|
||||||
temp_file2.write_all(config_content.as_bytes()).unwrap();
|
temp_file2.write_all(config_content.as_bytes()).unwrap();
|
||||||
@@ -1741,8 +1419,8 @@ network_secret = "${INSTANCE_SECRET}"
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
remove_env_var("INSTANCE_SECRET");
|
std::env::remove_var("INSTANCE_SECRET");
|
||||||
remove_env_var("INSTANCE_NAME");
|
std::env::remove_var("INSTANCE_NAME");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 实际配置字段测试(network_secret、peer.uri 等)
|
/// 实际配置字段测试(network_secret、peer.uri 等)
|
||||||
@@ -1755,11 +1433,11 @@ network_secret = "${INSTANCE_SECRET}"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_real_config_fields_expansion() {
|
async fn test_real_config_fields_expansion() {
|
||||||
// 设置各种实际场景的环境变量
|
// 设置各种实际场景的环境变量
|
||||||
set_env_var("ET_SECRET", "production-secret-key");
|
std::env::set_var("ET_SECRET", "production-secret-key");
|
||||||
set_env_var("PEER_HOST", "peer.example.com");
|
std::env::set_var("PEER_HOST", "peer.example.com");
|
||||||
set_env_var("PEER_PORT", "11011");
|
std::env::set_var("PEER_PORT", "11011");
|
||||||
set_env_var("LISTEN_PORT", "11010");
|
std::env::set_var("LISTEN_PORT", "11010");
|
||||||
set_env_var("NETWORK_NAME", "prod-network");
|
std::env::set_var("NETWORK_NAME", "prod-network");
|
||||||
|
|
||||||
// 创建包含多个实际字段的完整配置
|
// 创建包含多个实际字段的完整配置
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
@@ -1807,11 +1485,11 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
|
|||||||
assert!(control.is_no_delete());
|
assert!(control.is_no_delete());
|
||||||
|
|
||||||
// 清理环境变量
|
// 清理环境变量
|
||||||
remove_env_var("ET_SECRET");
|
std::env::remove_var("ET_SECRET");
|
||||||
remove_env_var("PEER_HOST");
|
std::env::remove_var("PEER_HOST");
|
||||||
remove_env_var("PEER_PORT");
|
std::env::remove_var("PEER_PORT");
|
||||||
remove_env_var("LISTEN_PORT");
|
std::env::remove_var("LISTEN_PORT");
|
||||||
remove_env_var("NETWORK_NAME");
|
std::env::remove_var("NETWORK_NAME");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 带默认值的环境变量
|
/// 带默认值的环境变量
|
||||||
@@ -1821,8 +1499,8 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_env_var_with_default_value() {
|
async fn test_env_var_with_default_value() {
|
||||||
// 确保变量未定义
|
// 确保变量未定义
|
||||||
remove_env_var("UNDEFINED_PORT");
|
std::env::remove_var("UNDEFINED_PORT");
|
||||||
remove_env_var("UNDEFINED_SECRET");
|
std::env::remove_var("UNDEFINED_SECRET");
|
||||||
|
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -1863,7 +1541,7 @@ network_secret = "${UNDEFINED_SECRET:-default-secret}"
|
|||||||
/// - 未定义的环境变量保持原样(shellexpand 的默认行为)
|
/// - 未定义的环境变量保持原样(shellexpand 的默认行为)
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_undefined_env_var_without_default() {
|
async fn test_undefined_env_var_without_default() {
|
||||||
remove_env_var("COMPLETELY_UNDEFINED");
|
std::env::remove_var("COMPLETELY_UNDEFINED");
|
||||||
|
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -1893,8 +1571,6 @@ network_secret = "${COMPLETELY_UNDEFINED}"
|
|||||||
|
|
||||||
// 注意:由于没有实际替换发生,控制标记不应因环境变量而设置
|
// 注意:由于没有实际替换发生,控制标记不应因环境变量而设置
|
||||||
// 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE
|
// 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE
|
||||||
// 这里我们主要验证 NO_DELETE 标记的逻辑
|
|
||||||
// 由于没有 config_dir,文件会被标记为 NO_DELETE,但不是因为环境变量
|
|
||||||
assert!(control.is_no_delete());
|
assert!(control.is_no_delete());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1906,9 +1582,9 @@ network_secret = "${COMPLETELY_UNDEFINED}"
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_boolean_type_env_vars() {
|
async fn test_boolean_type_env_vars() {
|
||||||
// 设置布尔类型的环境变量
|
// 设置布尔类型的环境变量
|
||||||
set_env_var("ENABLE_DHCP", "true");
|
std::env::set_var("ENABLE_DHCP", "true");
|
||||||
set_env_var("ENABLE_ENCRYPTION", "false");
|
std::env::set_var("ENABLE_ENCRYPTION", "false");
|
||||||
set_env_var("ENABLE_IPV6", "true");
|
std::env::set_var("ENABLE_IPV6", "true");
|
||||||
|
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -1946,9 +1622,9 @@ enable_ipv6 = ${ENABLE_IPV6}
|
|||||||
assert!(control.is_no_delete());
|
assert!(control.is_no_delete());
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
remove_env_var("ENABLE_DHCP");
|
std::env::remove_var("ENABLE_DHCP");
|
||||||
remove_env_var("ENABLE_ENCRYPTION");
|
std::env::remove_var("ENABLE_ENCRYPTION");
|
||||||
remove_env_var("ENABLE_IPV6");
|
std::env::remove_var("ENABLE_IPV6");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 数字类型环境变量
|
/// 数字类型环境变量
|
||||||
@@ -1959,8 +1635,8 @@ enable_ipv6 = ${ENABLE_IPV6}
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_numeric_type_env_vars() {
|
async fn test_numeric_type_env_vars() {
|
||||||
// 设置数字类型的环境变量
|
// 设置数字类型的环境变量
|
||||||
set_env_var("MTU_VALUE", "1400");
|
std::env::set_var("MTU_VALUE", "1400");
|
||||||
set_env_var("THREAD_COUNT", "4");
|
std::env::set_var("THREAD_COUNT", "4");
|
||||||
|
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -1995,8 +1671,8 @@ multi_thread_count = ${THREAD_COUNT}
|
|||||||
assert!(control.is_no_delete());
|
assert!(control.is_no_delete());
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
remove_env_var("MTU_VALUE");
|
std::env::remove_var("MTU_VALUE");
|
||||||
remove_env_var("THREAD_COUNT");
|
std::env::remove_var("THREAD_COUNT");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 混合类型环境变量
|
/// 混合类型环境变量
|
||||||
@@ -2008,12 +1684,12 @@ multi_thread_count = ${THREAD_COUNT}
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_mixed_type_env_vars() {
|
async fn test_mixed_type_env_vars() {
|
||||||
// 设置不同类型的环境变量
|
// 设置不同类型的环境变量
|
||||||
set_env_var("MIXED_SECRET", "mixed-secret-key");
|
std::env::set_var("MIXED_SECRET", "mixed-secret-key");
|
||||||
set_env_var("MIXED_NETWORK", "production");
|
std::env::set_var("MIXED_NETWORK", "production");
|
||||||
set_env_var("MIXED_DHCP", "true");
|
std::env::set_var("MIXED_DHCP", "true");
|
||||||
set_env_var("MIXED_MTU", "1500");
|
std::env::set_var("MIXED_MTU", "1500");
|
||||||
set_env_var("MIXED_ENCRYPTION", "false");
|
std::env::set_var("MIXED_ENCRYPTION", "false");
|
||||||
set_env_var("MIXED_LISTEN_PORT", "12345");
|
std::env::set_var("MIXED_LISTEN_PORT", "12345");
|
||||||
|
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
let config_content = r#"
|
let config_content = r#"
|
||||||
@@ -2065,11 +1741,11 @@ enable_encryption = ${MIXED_ENCRYPTION}
|
|||||||
assert!(control.is_no_delete());
|
assert!(control.is_no_delete());
|
||||||
|
|
||||||
// 清理
|
// 清理
|
||||||
remove_env_var("MIXED_SECRET");
|
std::env::remove_var("MIXED_SECRET");
|
||||||
remove_env_var("MIXED_NETWORK");
|
std::env::remove_var("MIXED_NETWORK");
|
||||||
remove_env_var("MIXED_DHCP");
|
std::env::remove_var("MIXED_DHCP");
|
||||||
remove_env_var("MIXED_MTU");
|
std::env::remove_var("MIXED_MTU");
|
||||||
remove_env_var("MIXED_ENCRYPTION");
|
std::env::remove_var("MIXED_ENCRYPTION");
|
||||||
remove_env_var("MIXED_LISTEN_PORT");
|
std::env::remove_var("MIXED_LISTEN_PORT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000);
|
|||||||
|
|
||||||
define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
||||||
|
|
||||||
|
define_global_var!(MACHINE_UID, Option<String>, None);
|
||||||
|
|
||||||
define_global_var!(MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK, u32, 3);
|
define_global_var!(MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK, u32, 3);
|
||||||
|
|
||||||
define_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER, bool, true);
|
define_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER, bool, true);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#[doc(hidden)]
|
||||||
|
pub struct Defer<F: FnOnce()> {
|
||||||
|
// internal struct used by defer! macro
|
||||||
|
func: Option<F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce()> Defer<F> {
|
||||||
|
pub fn new(func: F) -> Self {
|
||||||
|
Self { func: Some(func) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce()> Drop for Defer<F> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(f) = self.func.take() {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! defer {
|
||||||
|
( $($tt:tt)* ) => {
|
||||||
|
let _deferred = $crate::common::defer::Defer::new(|| { $($tt)* });
|
||||||
|
};
|
||||||
|
}
|
||||||
+13
-21
@@ -1,6 +1,6 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use hickory_proto::runtime::TokioRuntimeProvider;
|
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||||
@@ -73,6 +73,16 @@ pub async fn socket_addrs(
|
|||||||
.port()
|
.port()
|
||||||
.or_else(default_port_number)
|
.or_else(default_port_number)
|
||||||
.ok_or(Error::InvalidUrl(url.to_string()))?;
|
.ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||||
|
// See https://github.com/EasyTier/EasyTier/pull/947
|
||||||
|
// here is for compatibility with old version
|
||||||
|
let port = match port {
|
||||||
|
0 => match url.scheme() {
|
||||||
|
"ws" => 80,
|
||||||
|
"wss" => 443,
|
||||||
|
_ => port,
|
||||||
|
},
|
||||||
|
_ => port,
|
||||||
|
};
|
||||||
|
|
||||||
// if host is an ip address, return it directly
|
// if host is an ip address, return it directly
|
||||||
match host {
|
match host {
|
||||||
@@ -111,8 +121,9 @@ pub async fn socket_addrs(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::defer;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use guarden::defer;
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_socket_addrs() {
|
async fn test_socket_addrs() {
|
||||||
@@ -129,23 +140,4 @@ mod tests {
|
|||||||
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||||
println!("addrs2: {:?}", addrs);
|
println!("addrs2: {:?}", addrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn socket_addrs_preserves_explicit_zero_port() {
|
|
||||||
let cases = [
|
|
||||||
("ws://127.0.0.1:0", 80, 0),
|
|
||||||
("wss://127.0.0.1:0", 443, 0),
|
|
||||||
("ws://127.0.0.1", 80, 80),
|
|
||||||
("wss://127.0.0.1", 443, 443),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (raw_url, default_port, expected_port) in cases {
|
|
||||||
let url = url::Url::parse(raw_url).unwrap();
|
|
||||||
let addrs = socket_addrs(&url, || Some(default_port)).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
addrs,
|
|
||||||
vec![SocketAddr::from(([127, 0, 0, 1], expected_port))]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,10 @@ pub fn expand_env_vars(text: &str) -> (String, bool) {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::{remove_env_var, set_env_var};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_standard_syntax() {
|
fn test_expand_standard_syntax() {
|
||||||
set_env_var("TEST_VAR_STANDARD", "test_value");
|
std::env::set_var("TEST_VAR_STANDARD", "test_value");
|
||||||
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
|
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
|
||||||
assert_eq!(result, "secret=test_value");
|
assert_eq!(result, "secret=test_value");
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
@@ -54,7 +53,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_expand_short_syntax() {
|
fn test_expand_short_syntax() {
|
||||||
set_env_var("TEST_VAR_SHORT", "short_value");
|
std::env::set_var("TEST_VAR_SHORT", "short_value");
|
||||||
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
|
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
|
||||||
assert_eq!(result, "key=short_value");
|
assert_eq!(result, "key=short_value");
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
@@ -63,7 +62,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_expand_with_default() {
|
fn test_expand_with_default() {
|
||||||
// 确保变量未定义
|
// 确保变量未定义
|
||||||
remove_env_var("UNDEFINED_VAR_WITH_DEFAULT");
|
std::env::remove_var("UNDEFINED_VAR_WITH_DEFAULT");
|
||||||
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
|
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
|
||||||
assert_eq!(result, "port=8080");
|
assert_eq!(result, "port=8080");
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
@@ -85,8 +84,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_vars() {
|
fn test_multiple_vars() {
|
||||||
set_env_var("VAR1", "value1");
|
std::env::set_var("VAR1", "value1");
|
||||||
set_env_var("VAR2", "value2");
|
std::env::set_var("VAR2", "value2");
|
||||||
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
|
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
|
||||||
assert_eq!(result, "value1 and value2");
|
assert_eq!(result, "value1 and value2");
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
@@ -95,7 +94,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_undefined_var_without_default() {
|
fn test_undefined_var_without_default() {
|
||||||
// 确保变量未定义
|
// 确保变量未定义
|
||||||
remove_env_var("COMPLETELY_UNDEFINED_VAR");
|
std::env::remove_var("COMPLETELY_UNDEFINED_VAR");
|
||||||
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
|
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
|
||||||
// shellexpand::env 对未定义的变量会保持原样
|
// shellexpand::env 对未定义的变量会保持原样
|
||||||
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
|
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
|
||||||
@@ -104,8 +103,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_complex_toml_config() {
|
fn test_complex_toml_config() {
|
||||||
set_env_var("ET_SECRET", "my-secret-key");
|
std::env::set_var("ET_SECRET", "my-secret-key");
|
||||||
set_env_var("ET_PORT", "11010");
|
std::env::set_var("ET_PORT", "11010");
|
||||||
|
|
||||||
let config = r#"
|
let config = r#"
|
||||||
[network_identity]
|
[network_identity]
|
||||||
@@ -124,7 +123,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escape_syntax_double_dollar() {
|
fn test_escape_syntax_double_dollar() {
|
||||||
set_env_var("ESCAPED_VAR", "should_not_expand");
|
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||||
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
|
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
|
||||||
// $$ 会被转义为单个 $,不会触发变量扩展
|
// $$ 会被转义为单个 $,不会触发变量扩展
|
||||||
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
|
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
|
||||||
@@ -134,7 +133,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escape_syntax_backslash() {
|
fn test_escape_syntax_backslash() {
|
||||||
set_env_var("ESCAPED_VAR", "should_not_expand");
|
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||||
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
|
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
|
||||||
// 这不是推荐的转义方式,此测试仅为记录实际行为
|
// 这不是推荐的转义方式,此测试仅为记录实际行为
|
||||||
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
|
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
|
||||||
@@ -144,7 +143,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_dollar_signs() {
|
fn test_multiple_dollar_signs() {
|
||||||
set_env_var("TEST_VAR", "value");
|
std::env::set_var("TEST_VAR", "value");
|
||||||
// 测试多个连续的 $ 符号
|
// 测试多个连续的 $ 符号
|
||||||
let (result1, changed1) = expand_env_vars("$$");
|
let (result1, changed1) = expand_env_vars("$$");
|
||||||
assert_eq!(result1, "$");
|
assert_eq!(result1, "$");
|
||||||
@@ -162,7 +161,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_var_value() {
|
fn test_empty_var_value() {
|
||||||
set_env_var("EMPTY_VAR", "");
|
std::env::set_var("EMPTY_VAR", "");
|
||||||
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
|
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
|
||||||
// 变量存在但值为空
|
// 变量存在但值为空
|
||||||
assert_eq!(result, "value=");
|
assert_eq!(result, "value=");
|
||||||
@@ -171,7 +170,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_default_with_special_chars() {
|
fn test_default_with_special_chars() {
|
||||||
remove_env_var("UNDEFINED_SPECIAL");
|
std::env::remove_var("UNDEFINED_SPECIAL");
|
||||||
// 测试默认值包含冒号、等号、空格等特殊字符
|
// 测试默认值包含冒号、等号、空格等特殊字符
|
||||||
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
|
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
|
||||||
assert_eq!(result, "url=http://localhost:8080");
|
assert_eq!(result, "url=http://localhost:8080");
|
||||||
@@ -188,9 +187,9 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_var_name_with_numbers_underscores() {
|
fn test_var_name_with_numbers_underscores() {
|
||||||
set_env_var("VAR_123", "num_value");
|
std::env::set_var("VAR_123", "num_value");
|
||||||
set_env_var("_VAR", "underscore_prefix");
|
std::env::set_var("_VAR", "underscore_prefix");
|
||||||
set_env_var("VAR_", "underscore_suffix");
|
std::env::set_var("VAR_", "underscore_suffix");
|
||||||
|
|
||||||
let (result1, changed1) = expand_env_vars("${VAR_123}");
|
let (result1, changed1) = expand_env_vars("${VAR_123}");
|
||||||
assert_eq!(result1, "num_value");
|
assert_eq!(result1, "num_value");
|
||||||
@@ -215,7 +214,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
|
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
|
||||||
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
|
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
|
||||||
remove_env_var("VAR");
|
std::env::remove_var("VAR");
|
||||||
let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
|
let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
|
||||||
// 如果 VAR 未定义,shellexpand 会返回错误或保持原样
|
// 如果 VAR 未定义,shellexpand 会返回错误或保持原样
|
||||||
assert_eq!(result2, "incomplete ${VAR");
|
assert_eq!(result2, "incomplete ${VAR");
|
||||||
@@ -225,8 +224,8 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mixed_defined_undefined_vars() {
|
fn test_mixed_defined_undefined_vars() {
|
||||||
set_env_var("DEFINED_VAR", "defined");
|
std::env::set_var("DEFINED_VAR", "defined");
|
||||||
remove_env_var("UNDEFINED_VAR");
|
std::env::remove_var("UNDEFINED_VAR");
|
||||||
|
|
||||||
// 混合已定义和未定义的变量
|
// 混合已定义和未定义的变量
|
||||||
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
|
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
|
||||||
@@ -238,7 +237,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_nested_braces() {
|
fn test_nested_braces() {
|
||||||
set_env_var("OUTER", "outer_value");
|
std::env::set_var("OUTER", "outer_value");
|
||||||
// 嵌套的大括号是无效语法,shellexpand::env 会返回错误
|
// 嵌套的大括号是无效语法,shellexpand::env 会返回错误
|
||||||
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
|
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
|
||||||
// 由于语法错误,整个字符串保持不变
|
// 由于语法错误,整个字符串保持不变
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeSet, HashMap, hash_map::DefaultHasher},
|
collections::{hash_map::DefaultHasher, HashMap},
|
||||||
hash::Hasher,
|
hash::Hasher,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -10,11 +10,11 @@ use arc_swap::ArcSwap;
|
|||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
PeerId,
|
|
||||||
config::{ConfigLoader, Flags},
|
config::{ConfigLoader, Flags},
|
||||||
netns::NetNS,
|
netns::NetNS,
|
||||||
network::IPCollector,
|
network::IPCollector,
|
||||||
stun::{StunInfoCollector, StunInfoCollectorTrait},
|
stun::{StunInfoCollector, StunInfoCollectorTrait},
|
||||||
|
PeerId,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
@@ -28,7 +28,6 @@ use crate::{
|
|||||||
common::{PeerFeatureFlag, PortForwardConfigPb},
|
common::{PeerFeatureFlag, PortForwardConfigPb},
|
||||||
peer_rpc::PeerGroupInfo,
|
peer_rpc::PeerGroupInfo,
|
||||||
},
|
},
|
||||||
rpc_service::protected_port,
|
|
||||||
tunnel::matches_protocol,
|
tunnel::matches_protocol,
|
||||||
};
|
};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
@@ -53,11 +52,6 @@ pub enum GlobalCtxEvent {
|
|||||||
ListenerAcceptFailed(url::Url, String), // (url, error message)
|
ListenerAcceptFailed(url::Url, String), // (url, error message)
|
||||||
ConnectionAccepted(String, String), // (local url, remote url)
|
ConnectionAccepted(String, String), // (local url, remote url)
|
||||||
ConnectionError(String, String, String), // (local url, remote url, error message)
|
ConnectionError(String, String, String), // (local url, remote url, error message)
|
||||||
ListenerPortMappingEstablished {
|
|
||||||
local_listener: url::Url,
|
|
||||||
mapped_listener: url::Url,
|
|
||||||
backend: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
Connecting(url::Url),
|
Connecting(url::Url),
|
||||||
ConnectError(String, String, String), // (dst, ip version, error message)
|
ConnectError(String, String, String), // (dst, ip version, error message)
|
||||||
@@ -68,8 +62,6 @@ pub enum GlobalCtxEvent {
|
|||||||
|
|
||||||
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||||
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
||||||
PublicIpv6Changed(Option<cidr::Ipv6Inet>, Option<cidr::Ipv6Inet>), // (old, new)
|
|
||||||
PublicIpv6RoutesUpdated(Vec<cidr::Ipv6Inet>, Vec<cidr::Ipv6Inet>), // (added, removed)
|
|
||||||
|
|
||||||
PortForwardAdded(PortForwardConfigPb),
|
PortForwardAdded(PortForwardConfigPb),
|
||||||
|
|
||||||
@@ -77,11 +69,6 @@ pub enum GlobalCtxEvent {
|
|||||||
|
|
||||||
ProxyCidrsUpdated(Vec<cidr::Ipv4Cidr>, Vec<cidr::Ipv4Cidr>), // (added, removed)
|
ProxyCidrsUpdated(Vec<cidr::Ipv4Cidr>, Vec<cidr::Ipv4Cidr>), // (added, removed)
|
||||||
|
|
||||||
UdpBroadcastRelayStartResult {
|
|
||||||
capture_backend: Option<String>,
|
|
||||||
error: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
CredentialChanged,
|
CredentialChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +194,6 @@ pub struct GlobalCtx {
|
|||||||
|
|
||||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||||
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
|
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
|
||||||
public_ipv6_lease: AtomicCell<Option<cidr::Ipv6Inet>>,
|
|
||||||
public_ipv6_routes: Mutex<BTreeSet<std::net::Ipv6Addr>>,
|
|
||||||
cached_proxy_cidrs: AtomicCell<Option<Vec<ProxyNetworkConfig>>>,
|
cached_proxy_cidrs: AtomicCell<Option<Vec<ProxyNetworkConfig>>>,
|
||||||
|
|
||||||
ip_collector: Mutex<Option<Arc<IPCollector>>>,
|
ip_collector: Mutex<Option<Arc<IPCollector>>>,
|
||||||
@@ -218,16 +203,9 @@ pub struct GlobalCtx {
|
|||||||
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
||||||
|
|
||||||
running_listeners: Mutex<Vec<url::Url>>,
|
running_listeners: Mutex<Vec<url::Url>>,
|
||||||
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
|
|
||||||
|
|
||||||
flags: ArcSwap<Flags>,
|
flags: ArcSwap<Flags>,
|
||||||
|
|
||||||
// Runtime/base advertised feature flags before config-owned fields are
|
|
||||||
// overlaid by set_flags. Keep this separate so config patches do not erase
|
|
||||||
// runtime state such as public-server role, IPv6 provider status, or the
|
|
||||||
// non-whitelist avoid-relay preference.
|
|
||||||
base_feature_flags: AtomicCell<PeerFeatureFlag>,
|
|
||||||
|
|
||||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||||
|
|
||||||
token_bucket_manager: TokenBucketManager,
|
token_bucket_manager: TokenBucketManager,
|
||||||
@@ -258,17 +236,8 @@ impl std::fmt::Debug for GlobalCtx {
|
|||||||
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
||||||
|
|
||||||
impl GlobalCtx {
|
impl GlobalCtx {
|
||||||
fn apply_disable_relay_data_flag(
|
fn derive_feature_flags(flags: &Flags, current: Option<PeerFeatureFlag>) -> PeerFeatureFlag {
|
||||||
flags: &Flags,
|
let mut feature_flags = current.unwrap_or_default();
|
||||||
mut feature_flags: PeerFeatureFlag,
|
|
||||||
) -> PeerFeatureFlag {
|
|
||||||
if flags.disable_relay_data {
|
|
||||||
feature_flags.avoid_relay_data = true;
|
|
||||||
}
|
|
||||||
feature_flags
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_feature_flags(flags: &Flags, mut feature_flags: PeerFeatureFlag) -> PeerFeatureFlag {
|
|
||||||
feature_flags.kcp_input = !flags.disable_kcp_input;
|
feature_flags.kcp_input = !flags.disable_kcp_input;
|
||||||
feature_flags.no_relay_kcp = flags.disable_relay_kcp;
|
feature_flags.no_relay_kcp = flags.disable_relay_kcp;
|
||||||
feature_flags.support_conn_list_sync = true;
|
feature_flags.support_conn_list_sync = true;
|
||||||
@@ -276,7 +245,7 @@ impl GlobalCtx {
|
|||||||
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
||||||
feature_flags.need_p2p = flags.need_p2p;
|
feature_flags.need_p2p = flags.need_p2p;
|
||||||
feature_flags.disable_p2p = flags.disable_p2p;
|
feature_flags.disable_p2p = flags.disable_p2p;
|
||||||
Self::apply_disable_relay_data_flag(flags, feature_flags)
|
feature_flags
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
||||||
@@ -305,8 +274,7 @@ impl GlobalCtx {
|
|||||||
|
|
||||||
let flags = config_fs.get_flags();
|
let flags = config_fs.get_flags();
|
||||||
|
|
||||||
let base_feature_flags = PeerFeatureFlag::default();
|
let feature_flags = Self::derive_feature_flags(&flags, None);
|
||||||
let feature_flags = Self::derive_feature_flags(&flags, base_feature_flags);
|
|
||||||
|
|
||||||
let credential_storage_path = config_fs.get_credential_file();
|
let credential_storage_path = config_fs.get_credential_file();
|
||||||
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
|
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
|
||||||
@@ -321,8 +289,6 @@ impl GlobalCtx {
|
|||||||
event_bus,
|
event_bus,
|
||||||
cached_ipv4: AtomicCell::new(None),
|
cached_ipv4: AtomicCell::new(None),
|
||||||
cached_ipv6: AtomicCell::new(None),
|
cached_ipv6: AtomicCell::new(None),
|
||||||
public_ipv6_lease: AtomicCell::new(None),
|
|
||||||
public_ipv6_routes: Mutex::new(BTreeSet::new()),
|
|
||||||
cached_proxy_cidrs: AtomicCell::new(None),
|
cached_proxy_cidrs: AtomicCell::new(None),
|
||||||
|
|
||||||
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
||||||
@@ -335,12 +301,9 @@ impl GlobalCtx {
|
|||||||
stun_info_collection: Mutex::new(stun_info_collector),
|
stun_info_collection: Mutex::new(stun_info_collector),
|
||||||
|
|
||||||
running_listeners: Mutex::new(Vec::new()),
|
running_listeners: Mutex::new(Vec::new()),
|
||||||
advertised_ipv6_public_addr_prefix: Mutex::new(None),
|
|
||||||
|
|
||||||
flags: ArcSwap::new(Arc::new(flags)),
|
flags: ArcSwap::new(Arc::new(flags)),
|
||||||
|
|
||||||
base_feature_flags: AtomicCell::new(base_feature_flags),
|
|
||||||
|
|
||||||
feature_flags: AtomicCell::new(feature_flags),
|
feature_flags: AtomicCell::new(feature_flags),
|
||||||
|
|
||||||
token_bucket_manager: TokenBucketManager::new(),
|
token_bucket_manager: TokenBucketManager::new(),
|
||||||
@@ -412,45 +375,6 @@ impl GlobalCtx {
|
|||||||
self.cached_ipv6.store(None);
|
self.cached_ipv6.store(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_public_ipv6_lease(&self) -> Option<cidr::Ipv6Inet> {
|
|
||||||
self.public_ipv6_lease.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_public_ipv6_lease(&self, addr: Option<cidr::Ipv6Inet>) {
|
|
||||||
self.public_ipv6_lease.store(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_public_ipv6_routes(&self, routes: BTreeSet<cidr::Ipv6Inet>) {
|
|
||||||
*self.public_ipv6_routes.lock().unwrap() =
|
|
||||||
routes.into_iter().map(|route| route.address()).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
|
|
||||||
self.get_ipv6().map(|x| x.address() == *ip).unwrap_or(false)
|
|
||||||
|| self
|
|
||||||
.get_public_ipv6_lease()
|
|
||||||
.map(|x| x.address() == *ip)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ip_easytier_managed_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
|
|
||||||
self.is_ip_local_ipv6(ip) || self.public_ipv6_routes.lock().unwrap().contains(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_advertised_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
|
|
||||||
*self.advertised_ipv6_public_addr_prefix.lock().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_advertised_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>) -> bool {
|
|
||||||
let mut guard = self.advertised_ipv6_public_addr_prefix.lock().unwrap();
|
|
||||||
if *guard == prefix {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*guard = prefix;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id(&self) -> uuid::Uuid {
|
pub fn get_id(&self) -> uuid::Uuid {
|
||||||
self.config.get_id()
|
self.config.get_id()
|
||||||
}
|
}
|
||||||
@@ -465,7 +389,7 @@ impl GlobalCtx {
|
|||||||
pub fn is_ip_local_virtual_ip(&self, ip: &IpAddr) -> bool {
|
pub fn is_ip_local_virtual_ip(&self, ip: &IpAddr) -> bool {
|
||||||
match ip {
|
match ip {
|
||||||
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.address() == *v4).unwrap_or(false),
|
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.address() == *v4).unwrap_or(false),
|
||||||
IpAddr::V6(v6) => self.is_ip_local_ipv6(v6),
|
IpAddr::V6(v6) => self.get_ipv6().map(|x| x.address() == *v6).unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +460,7 @@ impl GlobalCtx {
|
|||||||
self.config.set_flags(flags.clone());
|
self.config.set_flags(flags.clone());
|
||||||
self.feature_flags.store(Self::derive_feature_flags(
|
self.feature_flags.store(Self::derive_feature_flags(
|
||||||
&flags,
|
&flags,
|
||||||
self.base_feature_flags.load(),
|
Some(self.feature_flags.load()),
|
||||||
));
|
));
|
||||||
self.flags.store(Arc::new(flags));
|
self.flags.store(Arc::new(flags));
|
||||||
}
|
}
|
||||||
@@ -601,53 +525,8 @@ impl GlobalCtx {
|
|||||||
self.feature_flags.load()
|
self.feature_flags.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the runtime/base advertised flags as a complete snapshot.
|
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
|
||||||
///
|
self.feature_flags.store(flags);
|
||||||
/// This is intended for foreign scoped contexts that inherit an already
|
|
||||||
/// computed feature-flag snapshot from their parent. Most callers should use
|
|
||||||
/// a narrower setter so they do not accidentally overwrite unrelated runtime
|
|
||||||
/// state.
|
|
||||||
pub fn set_base_advertised_feature_flags(&self, feature_flags: PeerFeatureFlag) {
|
|
||||||
self.base_feature_flags.store(feature_flags);
|
|
||||||
let flags = self.flags.load();
|
|
||||||
self.feature_flags
|
|
||||||
.store(Self::apply_disable_relay_data_flag(
|
|
||||||
flags.as_ref(),
|
|
||||||
feature_flags,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the avoid-relay preference that is independent of disable_relay_data.
|
|
||||||
///
|
|
||||||
/// disable_relay_data still forces the effective advertised flag to true,
|
|
||||||
/// but this base preference is preserved when that config flag is toggled.
|
|
||||||
pub fn set_avoid_relay_data_preference(&self, avoid_relay_data: bool) -> bool {
|
|
||||||
let mut base_feature_flags = self.base_feature_flags.load();
|
|
||||||
base_feature_flags.avoid_relay_data = avoid_relay_data;
|
|
||||||
self.base_feature_flags.store(base_feature_flags);
|
|
||||||
|
|
||||||
let mut feature_flags = self.feature_flags.load();
|
|
||||||
let previous = feature_flags.avoid_relay_data;
|
|
||||||
feature_flags.avoid_relay_data = avoid_relay_data || self.flags.load().disable_relay_data;
|
|
||||||
self.feature_flags.store(feature_flags);
|
|
||||||
previous != feature_flags.avoid_relay_data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the runtime IPv6-provider advertised bit without touching
|
|
||||||
/// config-derived feature flags.
|
|
||||||
pub fn set_ipv6_public_addr_provider_feature_flag(&self, enabled: bool) -> bool {
|
|
||||||
let mut base_feature_flags = self.base_feature_flags.load();
|
|
||||||
base_feature_flags.ipv6_public_addr_provider = enabled;
|
|
||||||
self.base_feature_flags.store(base_feature_flags);
|
|
||||||
|
|
||||||
let mut feature_flags = self.feature_flags.load();
|
|
||||||
if feature_flags.ipv6_public_addr_provider == enabled {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
feature_flags.ipv6_public_addr_provider = enabled;
|
|
||||||
self.feature_flags.store(feature_flags);
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
|
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
|
||||||
@@ -760,26 +639,25 @@ impl GlobalCtx {
|
|||||||
pub fn should_deny_proxy(&self, dst_addr: &SocketAddr, is_udp: bool) -> bool {
|
pub fn should_deny_proxy(&self, dst_addr: &SocketAddr, is_udp: bool) -> bool {
|
||||||
let _g = self.net_ns.guard();
|
let _g = self.net_ns.guard();
|
||||||
let ip = dst_addr.ip();
|
let ip = dst_addr.ip();
|
||||||
// first check if ip is an EasyTier-managed local address
|
// first check if ip is virtual ip
|
||||||
// then try bind this ip, if succ means it is local ip
|
// then try bind this ip, if succ means it is local ip
|
||||||
let dst_is_local_et_ip = self.is_ip_local_virtual_ip(&ip);
|
let dst_is_local_virtual_ip = self.is_ip_local_virtual_ip(&ip);
|
||||||
// this is an expensive operation, should be called sparingly
|
// this is an expensive operation, should be called sparingly
|
||||||
// 1. tcp/kcp/quic call this only after proxy conn is established
|
// 1. tcp/kcp/quic call this only after proxy conn is established
|
||||||
// 2. udp cache the result in nat entry
|
// 2. udp cache the result in nat entry
|
||||||
let dst_is_local_phy_ip = std::net::UdpSocket::bind(format!("{}:0", ip)).is_ok();
|
let dst_is_local_phy_ip = std::net::UdpSocket::bind(format!("{}:0", ip)).is_ok();
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"check should_deny_proxy: dst_addr={}, dst_is_local_et_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
"check should_deny_proxy: dst_addr={}, dst_is_local_virtual_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
||||||
dst_addr,
|
dst_addr,
|
||||||
dst_is_local_et_ip,
|
dst_is_local_virtual_ip,
|
||||||
dst_is_local_phy_ip,
|
dst_is_local_phy_ip,
|
||||||
is_udp
|
is_udp
|
||||||
);
|
);
|
||||||
|
|
||||||
if dst_is_local_et_ip || dst_is_local_phy_ip {
|
if dst_is_local_virtual_ip || dst_is_local_phy_ip {
|
||||||
// if is local ip, make sure the port is not one of the listening ports
|
// if is local ip, make sure the port is not one of the listening ports
|
||||||
self.is_port_in_running_listeners(dst_addr.port(), is_udp)
|
self.is_port_in_running_listeners(dst_addr.port(), is_udp)
|
||||||
|| (!is_udp && protected_port::is_protected_tcp_port(dst_addr.port()))
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -864,7 +742,7 @@ pub mod tests {
|
|||||||
let mut feature_flags = global_ctx.get_feature_flags();
|
let mut feature_flags = global_ctx.get_feature_flags();
|
||||||
feature_flags.avoid_relay_data = true;
|
feature_flags.avoid_relay_data = true;
|
||||||
feature_flags.is_public_server = true;
|
feature_flags.is_public_server = true;
|
||||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
global_ctx.set_feature_flags(feature_flags);
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
let mut flags = global_ctx.get_flags().clone();
|
||||||
flags.disable_kcp_input = true;
|
flags.disable_kcp_input = true;
|
||||||
@@ -885,135 +763,6 @@ pub mod tests {
|
|||||||
assert!(feature_flags.support_conn_list_sync);
|
assert!(feature_flags.support_conn_list_sync);
|
||||||
assert!(feature_flags.avoid_relay_data);
|
assert!(feature_flags.avoid_relay_data);
|
||||||
assert!(feature_flags.is_public_server);
|
assert!(feature_flags.is_public_server);
|
||||||
assert!(!feature_flags.ipv6_public_addr_provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn set_base_advertised_feature_flags_applies_current_values() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
|
|
||||||
let feature_flags = PeerFeatureFlag {
|
|
||||||
kcp_input: false,
|
|
||||||
no_relay_kcp: true,
|
|
||||||
quic_input: false,
|
|
||||||
no_relay_quic: true,
|
|
||||||
is_public_server: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
|
||||||
|
|
||||||
assert_eq!(global_ctx.get_feature_flags(), feature_flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn set_base_advertised_feature_flags_keeps_disable_relay_data_effective() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = true;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
let mut feature_flags = global_ctx.get_feature_flags();
|
|
||||||
feature_flags.avoid_relay_data = false;
|
|
||||||
feature_flags.is_public_server = true;
|
|
||||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
|
||||||
|
|
||||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
|
||||||
assert!(advertised_feature_flags.avoid_relay_data);
|
|
||||||
assert!(advertised_feature_flags.is_public_server);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = false;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
let advertised_feature_flags = global_ctx.get_feature_flags();
|
|
||||||
assert!(!advertised_feature_flags.avoid_relay_data);
|
|
||||||
assert!(advertised_feature_flags.is_public_server);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn disable_relay_data_sets_avoid_relay_feature_flag() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = true;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = false;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
assert!(!global_ctx.get_feature_flags().avoid_relay_data);
|
|
||||||
|
|
||||||
global_ctx.set_avoid_relay_data_preference(true);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = true;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
|
||||||
|
|
||||||
let mut flags = global_ctx.get_flags().clone();
|
|
||||||
flags.disable_relay_data = false;
|
|
||||||
global_ctx.set_flags(flags);
|
|
||||||
|
|
||||||
assert!(global_ctx.get_feature_flags().avoid_relay_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn should_deny_proxy_for_process_wide_rpc_port() {
|
|
||||||
protected_port::clear_protected_tcp_ports_for_test();
|
|
||||||
protected_port::register_protected_tcp_port(15888);
|
|
||||||
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
let rpc_addr = SocketAddr::from(([127, 0, 0, 1], 15888));
|
|
||||||
let other_tcp_addr = SocketAddr::from(([127, 0, 0, 1], 15889));
|
|
||||||
|
|
||||||
assert!(global_ctx.should_deny_proxy(&rpc_addr, false));
|
|
||||||
assert!(!global_ctx.should_deny_proxy(&rpc_addr, true));
|
|
||||||
assert!(!global_ctx.should_deny_proxy(&other_tcp_addr, false));
|
|
||||||
|
|
||||||
protected_port::clear_protected_tcp_ports_for_test();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn virtual_ipv6_and_public_ipv6_lease_are_stored_separately() {
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
let virtual_ipv6 = "fd00::1/64".parse().unwrap();
|
|
||||||
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
|
|
||||||
|
|
||||||
global_ctx.set_ipv6(Some(virtual_ipv6));
|
|
||||||
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
|
|
||||||
|
|
||||||
assert_eq!(global_ctx.get_ipv6(), Some(virtual_ipv6));
|
|
||||||
assert_eq!(global_ctx.get_public_ipv6_lease(), Some(public_ipv6));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn public_ipv6_lease_is_treated_as_local_ip() {
|
|
||||||
protected_port::clear_protected_tcp_ports_for_test();
|
|
||||||
|
|
||||||
let config = TomlConfigLoader::default();
|
|
||||||
let global_ctx = GlobalCtx::new(config);
|
|
||||||
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
|
|
||||||
let listener: url::Url = "tcp://[2001:db8::2]:11010".parse().unwrap();
|
|
||||||
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
|
|
||||||
global_ctx.add_running_listener(listener);
|
|
||||||
|
|
||||||
let ip = std::net::IpAddr::V6(public_ipv6.address());
|
|
||||||
let socket = SocketAddr::from((public_ipv6.address(), 11010));
|
|
||||||
|
|
||||||
assert!(global_ctx.is_ip_local_virtual_ip(&ip));
|
|
||||||
assert!(global_ctx.should_deny_proxy(&socket, false));
|
|
||||||
|
|
||||||
protected_port::clear_protected_tcp_ports_for_test();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mock_global_ctx_with_network(
|
pub fn get_mock_global_ctx_with_network(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd};
|
use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cidr::{Ipv4Inet, Ipv6Inet};
|
use cidr::{Ipv4Inet, Ipv6Inet};
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ impl IfConfiguerTrait for MacIfConfiger {
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
run_shell_cmd(
|
run_shell_cmd(
|
||||||
format!(
|
format!(
|
||||||
"ifconfig {} {:?}/{:?} {:?} up",
|
"ifconfig {} {:?}/{:?} 10.8.8.8 up",
|
||||||
name, address, cidr_prefix, address,
|
name, address, cidr_prefix,
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> {
|
|||||||
.creation_flags(CREATE_NO_WINDOW)
|
.creation_flags(CREATE_NO_WINDOW)
|
||||||
.output()
|
.output()
|
||||||
.await?;
|
.await?;
|
||||||
stdout = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
|
stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
|
||||||
stderr = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
|
stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
@@ -166,14 +166,3 @@ pub type IfConfiger = DummyIfConfiger;
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use windows::RegistryManager;
|
pub use windows::RegistryManager;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) fn list_ipv6_route_messages()
|
|
||||||
-> Result<Vec<netlink_packet_route::route::RouteMessage>, Error> {
|
|
||||||
netlink::NetlinkIfConfiger::list_ipv6_route_messages()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
|
|
||||||
netlink::NetlinkIfConfiger::get_interface_index(name)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,27 +10,27 @@ use anyhow::Context;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cidr::{IpInet, Ipv4Inet, Ipv6Inet};
|
use cidr::{IpInet, Ipv4Inet, Ipv6Inet};
|
||||||
use netlink_packet_core::{
|
use netlink_packet_core::{
|
||||||
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST, NetlinkDeserializable,
|
NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
|
||||||
NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
|
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST,
|
||||||
};
|
};
|
||||||
use netlink_packet_route::{
|
use netlink_packet_route::{
|
||||||
AddressFamily, RouteNetlinkMessage,
|
|
||||||
address::{AddressAttribute, AddressMessage},
|
address::{AddressAttribute, AddressMessage},
|
||||||
route::{
|
route::{
|
||||||
RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope,
|
RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope,
|
||||||
RouteType,
|
RouteType,
|
||||||
},
|
},
|
||||||
|
AddressFamily, RouteNetlinkMessage,
|
||||||
};
|
};
|
||||||
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE};
|
use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr};
|
||||||
use nix::{
|
use nix::{
|
||||||
ifaddrs::getifaddrs,
|
ifaddrs::getifaddrs,
|
||||||
libc::{self, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU, ifreq, ioctl},
|
libc::{self, ifreq, ioctl, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU},
|
||||||
net::if_::InterfaceFlags,
|
net::if_::InterfaceFlags,
|
||||||
sys::socket::SockaddrLike as _,
|
sys::socket::SockaddrLike as _,
|
||||||
};
|
};
|
||||||
use pnet::ipnetwork::ip_mask_to_prefix;
|
use pnet::ipnetwork::ip_mask_to_prefix;
|
||||||
|
|
||||||
use super::{Error, IfConfiguerTrait, route::Route};
|
use super::{route::Route, Error, IfConfiguerTrait};
|
||||||
|
|
||||||
pub(crate) fn dummy_socket() -> Result<std::net::UdpSocket, Error> {
|
pub(crate) fn dummy_socket() -> Result<std::net::UdpSocket, Error> {
|
||||||
Ok(std::net::UdpSocket::bind("0:0")?)
|
Ok(std::net::UdpSocket::bind("0:0")?)
|
||||||
@@ -160,7 +160,7 @@ impl From<RouteMessage> for Route {
|
|||||||
pub struct NetlinkIfConfiger {}
|
pub struct NetlinkIfConfiger {}
|
||||||
|
|
||||||
impl NetlinkIfConfiger {
|
impl NetlinkIfConfiger {
|
||||||
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
|
fn get_interface_index(name: &str) -> Result<u32, Error> {
|
||||||
let name = CString::new(name).with_context(|| "failed to convert interface name")?;
|
let name = CString::new(name).with_context(|| "failed to convert interface name")?;
|
||||||
match unsafe { libc::if_nametoindex(name.as_ptr()) } {
|
match unsafe { libc::if_nametoindex(name.as_ptr()) } {
|
||||||
0 => Err(std::io::Error::last_os_error().into()),
|
0 => Err(std::io::Error::last_os_error().into()),
|
||||||
@@ -311,7 +311,7 @@ impl NetlinkIfConfiger {
|
|||||||
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
|
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_route_messages(address_family: AddressFamily) -> Result<Vec<RouteMessage>, Error> {
|
fn list_routes() -> Result<Vec<RouteMessage>, Error> {
|
||||||
let mut message = RouteMessage::default();
|
let mut message = RouteMessage::default();
|
||||||
|
|
||||||
message.header.table = RouteHeader::RT_TABLE_UNSPEC;
|
message.header.table = RouteHeader::RT_TABLE_UNSPEC;
|
||||||
@@ -320,7 +320,7 @@ impl NetlinkIfConfiger {
|
|||||||
message.header.scope = RouteScope::Universe;
|
message.header.scope = RouteScope::Universe;
|
||||||
message.header.kind = RouteType::Unicast;
|
message.header.kind = RouteType::Unicast;
|
||||||
|
|
||||||
message.header.address_family = address_family;
|
message.header.address_family = AddressFamily::Inet;
|
||||||
message.header.destination_prefix_length = 0;
|
message.header.destination_prefix_length = 0;
|
||||||
message.header.source_prefix_length = 0;
|
message.header.source_prefix_length = 0;
|
||||||
|
|
||||||
@@ -367,14 +367,6 @@ impl NetlinkIfConfiger {
|
|||||||
|
|
||||||
Ok(ret_vec)
|
Ok(ret_vec)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_routes() -> Result<Vec<RouteMessage>, Error> {
|
|
||||||
Self::list_route_messages(AddressFamily::Inet)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
|
|
||||||
Self::list_route_messages(AddressFamily::Inet6)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -559,9 +551,12 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
message.header.scope = RouteScope::Universe;
|
message.header.scope = RouteScope::Universe;
|
||||||
message.header.kind = RouteType::Unicast;
|
message.header.kind = RouteType::Unicast;
|
||||||
|
|
||||||
|
// Add metric (cost) if specified
|
||||||
|
if let Some(cost) = cost {
|
||||||
message
|
message
|
||||||
.attributes
|
.attributes
|
||||||
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
|
.push(RouteAttribute::Priority(cost as u32));
|
||||||
|
}
|
||||||
|
|
||||||
message
|
message
|
||||||
.attributes
|
.attributes
|
||||||
@@ -569,11 +564,9 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
name,
|
name,
|
||||||
)?));
|
)?));
|
||||||
|
|
||||||
if cidr_prefix != 0 {
|
|
||||||
message
|
message
|
||||||
.attributes
|
.attributes
|
||||||
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
|
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
|
||||||
}
|
|
||||||
|
|
||||||
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
|
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
|
||||||
}
|
}
|
||||||
@@ -584,7 +577,7 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
address: std::net::Ipv6Addr,
|
address: std::net::Ipv6Addr,
|
||||||
cidr_prefix: u8,
|
cidr_prefix: u8,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let routes = Self::list_route_messages(AddressFamily::Inet6)?;
|
let routes = Self::list_routes()?;
|
||||||
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
|
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
|
||||||
|
|
||||||
for msg in routes {
|
for msg in routes {
|
||||||
@@ -605,82 +598,29 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
const DUMMY_IFACE_NAME: &str = "dummy";
|
const DUMMY_IFACE_NAME: &str = "dummy";
|
||||||
|
|
||||||
fn run_cmd(cmd: &str) -> String {
|
fn run_cmd(cmd: &str) -> String {
|
||||||
let output = Command::new("sh")
|
let output = std::process::Command::new("sh")
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(cmd)
|
.arg(cmd)
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to execute process");
|
.expect("failed to execute process");
|
||||||
assert!(
|
|
||||||
output.status.success(),
|
|
||||||
"command failed: {cmd}\nstdout: {}\nstderr: {}",
|
|
||||||
String::from_utf8_lossy(&output.stdout),
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
);
|
|
||||||
String::from_utf8(output.stdout).unwrap()
|
String::from_utf8(output.stdout).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ip(args: &[&str]) {
|
|
||||||
let output = Command::new("ip")
|
|
||||||
.args(args)
|
|
||||||
.output()
|
|
||||||
.expect("failed to execute ip process");
|
|
||||||
assert!(
|
|
||||||
output.status.success(),
|
|
||||||
"ip command failed: {:?}\nstdout: {}\nstderr: {}",
|
|
||||||
args,
|
|
||||||
String::from_utf8_lossy(&output.stdout),
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_iface_name(tag: &str) -> String {
|
|
||||||
format!("et{}{:x}", tag, std::process::id() & 0xffff)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ScopedDummyLink {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScopedDummyLink {
|
|
||||||
fn new(name: &str) -> Self {
|
|
||||||
let _ = Command::new("ip").args(["link", "del", name]).output();
|
|
||||||
run_ip(&["link", "add", name, "type", "dummy"]);
|
|
||||||
run_ip(&["link", "set", name, "up"]);
|
|
||||||
Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ScopedDummyLink {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = Command::new("ip")
|
|
||||||
.args(["link", "del", &self.name])
|
|
||||||
.output();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct PrepareEnv {}
|
struct PrepareEnv {}
|
||||||
impl PrepareEnv {
|
impl PrepareEnv {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let _ = Command::new("ip")
|
let _ = run_cmd(&format!("sudo ip link add {} type dummy", DUMMY_IFACE_NAME));
|
||||||
.args(["link", "del", DUMMY_IFACE_NAME])
|
|
||||||
.output();
|
|
||||||
let _ = run_cmd(&format!("ip link add {} type dummy", DUMMY_IFACE_NAME));
|
|
||||||
PrepareEnv {}
|
PrepareEnv {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for PrepareEnv {
|
impl Drop for PrepareEnv {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = Command::new("ip")
|
let _ = run_cmd(&format!("sudo ip link del {}", DUMMY_IFACE_NAME));
|
||||||
.args(["link", "del", DUMMY_IFACE_NAME])
|
|
||||||
.output();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,128 +701,4 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap())));
|
assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap())));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serial_test::serial]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn ipv6_addr_readback_test() {
|
|
||||||
let iface = test_iface_name("a");
|
|
||||||
let _link = ScopedDummyLink::new(&iface);
|
|
||||||
run_ip(&["-6", "addr", "add", "2001:db8:1234::2/64", "dev", &iface]);
|
|
||||||
|
|
||||||
let addrs = NetlinkIfConfiger::list_addresses(&iface).unwrap();
|
|
||||||
assert!(addrs.iter().any(|addr| {
|
|
||||||
addr.address() == IpAddr::V6("2001:db8:1234::2".parse().unwrap())
|
|
||||||
&& addr.network_length() == 64
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial_test::serial]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn ipv6_route_readback_test() {
|
|
||||||
let wan_if = test_iface_name("rw");
|
|
||||||
let lan_if = test_iface_name("rl");
|
|
||||||
let _wan = ScopedDummyLink::new(&wan_if);
|
|
||||||
let _lan = ScopedDummyLink::new(&lan_if);
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"addr",
|
|
||||||
"add",
|
|
||||||
"2001:db8:100:ffff::2/64",
|
|
||||||
"dev",
|
|
||||||
&wan_if,
|
|
||||||
]);
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
"default",
|
|
||||||
"from",
|
|
||||||
"2001:db8:100::/56",
|
|
||||||
"dev",
|
|
||||||
&wan_if,
|
|
||||||
]);
|
|
||||||
run_ip(&["-6", "route", "add", "2001:db8:100::/56", "dev", &lan_if]);
|
|
||||||
|
|
||||||
let wan_ifindex = NetlinkIfConfiger::get_interface_index(&wan_if).unwrap();
|
|
||||||
let lan_ifindex = NetlinkIfConfiger::get_interface_index(&lan_if).unwrap();
|
|
||||||
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
|
|
||||||
|
|
||||||
assert!(routes.iter().any(|route| {
|
|
||||||
route.header.kind == RouteType::Unicast
|
|
||||||
&& route.header.source_prefix_length == 56
|
|
||||||
&& route.attributes.iter().any(|attr| {
|
|
||||||
matches!(
|
|
||||||
attr,
|
|
||||||
RouteAttribute::Source(RouteAddress::Inet6(addr))
|
|
||||||
if *addr == "2001:db8:100::".parse::<std::net::Ipv6Addr>().unwrap()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
&& route
|
|
||||||
.attributes
|
|
||||||
.iter()
|
|
||||||
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == wan_ifindex))
|
|
||||||
&& !route
|
|
||||||
.attributes
|
|
||||||
.iter()
|
|
||||||
.any(|attr| matches!(attr, RouteAttribute::Destination(_)))
|
|
||||||
}));
|
|
||||||
|
|
||||||
assert!(routes.iter().any(|route| {
|
|
||||||
route.header.kind == RouteType::Unicast
|
|
||||||
&& route.header.destination_prefix_length == 56
|
|
||||||
&& route.attributes.iter().any(|attr| {
|
|
||||||
matches!(
|
|
||||||
attr,
|
|
||||||
RouteAttribute::Destination(RouteAddress::Inet6(addr))
|
|
||||||
if *addr == "2001:db8:100::".parse::<std::net::Ipv6Addr>().unwrap()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
&& route
|
|
||||||
.attributes
|
|
||||||
.iter()
|
|
||||||
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == lan_ifindex))
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial_test::serial]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn ipv6_route_remove_test() {
|
|
||||||
let iface = test_iface_name("rr");
|
|
||||||
let _link = ScopedDummyLink::new(&iface);
|
|
||||||
let ifcfg = NetlinkIfConfiger {};
|
|
||||||
let route_addr = "2001:db8:200::".parse::<std::net::Ipv6Addr>().unwrap();
|
|
||||||
|
|
||||||
ifcfg
|
|
||||||
.add_ipv6_route(&iface, route_addr, 56, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let ifindex = NetlinkIfConfiger::get_interface_index(&iface).unwrap();
|
|
||||||
let has_route = |routes: &[RouteMessage]| {
|
|
||||||
routes.iter().any(|route| {
|
|
||||||
route.header.destination_prefix_length == 56
|
|
||||||
&& route.attributes.iter().any(|attr| {
|
|
||||||
matches!(
|
|
||||||
attr,
|
|
||||||
RouteAttribute::Destination(RouteAddress::Inet6(addr)) if *addr == route_addr
|
|
||||||
)
|
|
||||||
})
|
|
||||||
&& route
|
|
||||||
.attributes
|
|
||||||
.iter()
|
|
||||||
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == ifindex))
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
|
|
||||||
assert!(has_route(&routes));
|
|
||||||
|
|
||||||
ifcfg
|
|
||||||
.remove_ipv6_route(&iface, route_addr, 56)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
|
|
||||||
assert!(!has_route(&routes));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -740,6 +740,10 @@ impl InterfaceLuid {
|
|||||||
|
|
||||||
// SAFETY: TODO
|
// SAFETY: TODO
|
||||||
let ret = unsafe { SetIpInterfaceEntry(&mut row) };
|
let ret = unsafe { SetIpInterfaceEntry(&mut row) };
|
||||||
if NO_ERROR == ret { Ok(()) } else { Err(ret) }
|
if NO_ERROR == ret {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ret)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,18 @@ use cidr::{Ipv4Inet, Ipv6Inet};
|
|||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
net::{Ipv4Addr, Ipv6Addr},
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
|
ptr::null_mut,
|
||||||
};
|
};
|
||||||
use windows::Win32::NetworkManagement::IpHelper::INTERNAL_IF_OPER_STATUS;
|
use windows_sys::Win32::{
|
||||||
use windows::Win32::{
|
|
||||||
Foundation::NO_ERROR,
|
Foundation::NO_ERROR,
|
||||||
NetworkManagement::IpHelper::{GetIfEntry, MIB_IFROW, SetIfEntry},
|
NetworkManagement::IpHelper::{GetIfEntry, SetIfEntry, MIB_IFROW},
|
||||||
System::Diagnostics::Debug::{
|
System::Diagnostics::Debug::{
|
||||||
FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS, FormatMessageW,
|
FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use windows::core::PWSTR;
|
|
||||||
use winreg::{
|
use winreg::{
|
||||||
RegKey,
|
|
||||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
|
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
|
||||||
|
RegKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Error, IfConfiguerTrait};
|
use super::{Error, IfConfiguerTrait};
|
||||||
@@ -33,12 +32,12 @@ fn format_win_error(error: u32) -> String {
|
|||||||
unsafe {
|
unsafe {
|
||||||
FormatMessageW(
|
FormatMessageW(
|
||||||
flags,
|
flags,
|
||||||
None,
|
null_mut(),
|
||||||
error,
|
error,
|
||||||
0,
|
0,
|
||||||
PWSTR(buffer.as_mut_ptr()),
|
buffer.as_mut_ptr(),
|
||||||
size,
|
size,
|
||||||
None,
|
null_mut(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let str_end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
|
let str_end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
|
||||||
@@ -101,7 +100,7 @@ impl WindowsIfConfiger {
|
|||||||
dwPhysAddrLen: 0,
|
dwPhysAddrLen: 0,
|
||||||
bPhysAddr: [0; 8],
|
bPhysAddr: [0; 8],
|
||||||
dwAdminStatus: if up { 1 } else { 2 }, // 1 = up, 2 = down
|
dwAdminStatus: if up { 1 } else { 2 }, // 1 = up, 2 = down
|
||||||
dwOperStatus: INTERNAL_IF_OPER_STATUS(0),
|
dwOperStatus: 0,
|
||||||
dwLastChange: 0,
|
dwLastChange: 0,
|
||||||
dwInOctets: 0,
|
dwInOctets: 0,
|
||||||
dwInUcastPkts: 0,
|
dwInUcastPkts: 0,
|
||||||
@@ -119,8 +118,8 @@ impl WindowsIfConfiger {
|
|||||||
bDescr: [0; 256],
|
bDescr: [0; 256],
|
||||||
};
|
};
|
||||||
|
|
||||||
if GetIfEntry(&mut if_row) == NO_ERROR.0 {
|
if GetIfEntry(&mut if_row) == NO_ERROR {
|
||||||
if SetIfEntry(&if_row) == NO_ERROR.0 {
|
if SetIfEntry(&if_row) == NO_ERROR {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow::anyhow!("Failed to set interface status").into())
|
Err(anyhow::anyhow!("Failed to set interface status").into())
|
||||||
@@ -332,7 +331,7 @@ impl RegistryManager {
|
|||||||
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
|
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
|
||||||
|
|
||||||
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
|
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
|
||||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
let profiles_key = hklm.open_subkey_with_flags(
|
let profiles_key = hklm.open_subkey_with_flags(
|
||||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||||
@@ -406,7 +405,7 @@ impl RegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
|
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
|
||||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
let profiles_key = hklm.open_subkey_with_flags(
|
let profiles_key = hklm.open_subkey_with_flags(
|
||||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||||
@@ -449,14 +448,15 @@ impl RegistryManager {
|
|||||||
for guid in network_key.enum_keys().map_while(Result::ok) {
|
for guid in network_key.enum_keys().map_while(Result::ok) {
|
||||||
if let Ok(guid_key) = network_key.open_subkey_with_flags(&guid, KEY_READ) {
|
if let Ok(guid_key) = network_key.open_subkey_with_flags(&guid, KEY_READ) {
|
||||||
// 检查 Connection/Name 是否匹配目标接口名
|
// 检查 Connection/Name 是否匹配目标接口名
|
||||||
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ)
|
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ) {
|
||||||
&& let Ok(name) = conn_key.get_value::<String, _>("Name")
|
if let Ok(name) = conn_key.get_value::<String, _>("Name") {
|
||||||
&& name == interface_name
|
if name == interface_name {
|
||||||
{
|
|
||||||
return Ok(guid);
|
return Ok(guid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有找到对应的接口
|
// 如果没有找到对应的接口
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user