mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-09 03:04:31 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d7a938e93 | |||
| 6229229b31 | |||
| 6a63853bad | |||
| 362aa7a9cd | |||
| 12a7b5a5c5 | |||
| 4eba9b07b6 | |||
| 1b48029bdc | |||
| 3542e944cb | |||
| 852d1c9e14 | |||
| 4958394469 | |||
| 41b6d65604 | |||
| aae30894dd | |||
| 81d169abfc | |||
| 9c6c210e89 | |||
| d1c6dcf754 | |||
| 97c8c4f55a | |||
| ed8df2d58f | |||
| f66010e6f9 | |||
| d5c4700d32 | |||
| 969ecfc4ca | |||
| 8f862997eb | |||
| b20075e3dc | |||
| eb3b5aae51 | |||
| af6b6ab6f1 | |||
| 5a1668c753 | |||
| 820d9095d3 | |||
| 2fb41ccbba | |||
| b4666be696 | |||
| 4688ad74ad | |||
| f7ea78d4f0 | |||
| ac112440c3 | |||
| 958b246f05 | |||
| 263f4c3bc9 | |||
| ffddc517e1 | |||
| 5cd0a3e846 | |||
| f4319c4d4f | |||
| 0091a535d5 | |||
| d7a5fb8d66 | |||
| f63054e937 | |||
| efc043abbb | |||
| 40c6de8e31 | |||
| 2db655bd6d | |||
| c49c56612b | |||
| 6ca074abae | |||
| 84430055ab | |||
| 432fcb3fc3 | |||
| fae32361f2 | |||
| bcb2e512d4 | |||
| 82ca04a8a7 | |||
| 2ef3b72224 | |||
| 6d319cba1d | |||
| 3687519ef3 | |||
| 3a4ac59467 | |||
| 1cfc135df3 | |||
| 5b35c51da9 | |||
| ec7ddd3bad | |||
| 6f3e708679 | |||
| 869e1b89f5 | |||
| 9e0a3b6936 | |||
| c6cb1a77d0 | |||
| 83010861ba | |||
| daa53e5168 | |||
| 51befdbf87 | |||
| 8311b11713 | |||
| 19c80c7b9c | |||
| a879dd1b14 | |||
| a8feb9ac2b | |||
| c5fbd29c0e | |||
| 26b1794723 | |||
| 371b4b70a3 | |||
| b2cc38ee63 |
+35
-54
@@ -1,29 +1,40 @@
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
linker = "rust-lld"
|
||||
rustflags = ["-C", "linker-flavor=ld.lld"]
|
||||
# region Native
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
# region
|
||||
|
||||
# region CI
|
||||
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.riscv64gc-unknown-linux-musl]
|
||||
linker = "riscv64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.armv7-unknown-linux-musleabi]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabi]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.mipsel-unknown-linux-musl]
|
||||
@@ -64,44 +75,14 @@ rustflags = [
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "armv7-unknown-linux-musleabihf-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
|
||||
[target.armv7-unknown-linux-musleabi]
|
||||
linker = "armv7-unknown-linux-musleabi-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
linker = "loongarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
linker = "arm-unknown-linux-musleabihf-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
|
||||
[target.arm-unknown-linux-musleabi]
|
||||
linker = "arm-unknown-linux-musleabi-gcc"
|
||||
rustflags = [
|
||||
"-C",
|
||||
"target-feature=+crt-static",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
|
||||
"-L",
|
||||
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
|
||||
"-l",
|
||||
"atomic",
|
||||
"-l",
|
||||
"gcc",
|
||||
]
|
||||
# endregion
|
||||
|
||||
@@ -2,10 +2,17 @@ name: prepare-build
|
||||
author: Luna
|
||||
description: Prepare build environment
|
||||
inputs:
|
||||
web:
|
||||
description: 'Whether to prepare the web build environment'
|
||||
target:
|
||||
description: 'The target to build for'
|
||||
required: false
|
||||
pnpm:
|
||||
description: 'Whether to run pnpm build'
|
||||
required: true
|
||||
default: 'true'
|
||||
pnpm-build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: './easytier-web/*'
|
||||
gui:
|
||||
description: 'Whether to prepare the GUI build environment'
|
||||
required: true
|
||||
@@ -19,21 +26,61 @@ runs:
|
||||
- run: mkdir -p easytier-gui/dist
|
||||
shell: bash
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.web == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Install GUI dependencies (Used by clippy)
|
||||
if: ${{ inputs.gui == 'true' }}
|
||||
- name: Install dependencies
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_gui_dep.sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install -qqy build-essential mold musl-tools
|
||||
shell: bash
|
||||
|
||||
- name: Install Rust
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
||||
|
||||
- name: Install GUI dependencies (Linux)
|
||||
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
sudo apt-get install -qq xdg-utils \
|
||||
libappindicator3-dev \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev
|
||||
shell: bash
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.95
|
||||
target: ${{ !contains(inputs.target, 'mips') && inputs.target || '' }}
|
||||
components: ${{ contains(inputs.target, 'mips') && 'rust-src' || '' }}
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- name: Install Rust (MIPS)
|
||||
if: ${{ contains(inputs.target, 'mips') }}
|
||||
run: |
|
||||
MUSL_TARGET=${{ inputs.target }}sf
|
||||
mkdir -p ./musl_gcc
|
||||
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
|
||||
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
|
||||
sudo chmod -R a+rwx ./musl_gcc
|
||||
|
||||
if [[ -d "./musl_gcc/sysroot" ]]; then
|
||||
echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
|
||||
# for panic-abort
|
||||
cp libgcc_eh.a libunwind.a
|
||||
|
||||
# for mimalloc
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
|
||||
+123
-149
@@ -2,9 +2,14 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
branches: [ "develop", "main", "releases/**" ]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [ "develop", "main" ]
|
||||
types: [ opened, synchronize, reopened, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
# do not skip push on branch starts with releases/
|
||||
@@ -30,7 +36,7 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh", "easytier-web/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
@@ -51,41 +57,48 @@ jobs:
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: loongarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-loongarch64
|
||||
|
||||
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7hf
|
||||
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armv7
|
||||
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-armhf
|
||||
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
- TARGET: mipsel-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-mipsel
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
ARTIFACT_NAME: macos-x86_64
|
||||
@@ -96,17 +109,12 @@ jobs:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-i686
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -131,8 +139,15 @@ jobs:
|
||||
name: easytier-web-dashboard
|
||||
path: easytier-web/frontend/dist/
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
@@ -140,96 +155,51 @@ jobs:
|
||||
shared-key: "core-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
- uses: mlugg/setup-zig@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
if: ${{ contains(matrix.OS, 'ubuntu') }}
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tool: cargo-zigbuild
|
||||
|
||||
- name: Build Core & Cli
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
|
||||
# loongarch need llvm-18
|
||||
if [[ $TARGET =~ ^loongarch.*$ ]]; then
|
||||
sudo apt-get install -qq llvm-18 clang-18
|
||||
export LLVM_CONFIG_PATH=/usr/lib/llvm-18/bin/llvm-config
|
||||
fi
|
||||
# we set the sysroot when sysroot is a dir
|
||||
# this dir is a soft link generated by install_rust.sh
|
||||
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
|
||||
if [[ -d "./musl_gcc/sysroot" ]]; then
|
||||
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly-2026-02-02 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
- name: Build
|
||||
if: ${{ !contains(matrix.TARGET, 'mips') }}
|
||||
run: |
|
||||
if [[ "$TARGET" == *windows* ]]; then
|
||||
SUFFIX=.exe
|
||||
else
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ || $TARGET =~ ^aarch64.*$ ]]; then
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
else
|
||||
CORE_FEATURES="--features=jemalloc"
|
||||
fi
|
||||
cargo build --release --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||
cargo build --release --target $TARGET $CORE_FEATURES
|
||||
SUFFIX=""
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
if [[ "$TARGET" =~ (x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|windows|darwin) ]]; then
|
||||
BUILD=build
|
||||
else
|
||||
BUILD=zigbuild
|
||||
fi
|
||||
|
||||
if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ (freebsd|windows) ]]; then
|
||||
FEATURES="mimalloc"
|
||||
else
|
||||
FEATURES="jemalloc"
|
||||
fi
|
||||
|
||||
cargo $BUILD --release --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||
|
||||
cargo $BUILD --release --target $TARGET --features=$FEATURES
|
||||
|
||||
- name: Build (MIPS)
|
||||
if: ${{ contains(matrix.TARGET, 'mips') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
with:
|
||||
envs: TARGET
|
||||
release: ${{ matrix.BSD_VERSION }}
|
||||
arch: x86_64
|
||||
usesh: true
|
||||
mem: 6144
|
||||
cpu: 4
|
||||
run: |
|
||||
uname -a
|
||||
echo $SHELL
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
env | sort
|
||||
|
||||
pkg install -y git protobuf llvm-devel sudo curl
|
||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
. $HOME/.cargo/env
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.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
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
run: |
|
||||
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
# windows is the only OS using a different convention for executable file name
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
@@ -242,26 +212,37 @@ jobs:
|
||||
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then
|
||||
HOST_ARCH=$(uname -m)
|
||||
case $HOST_ARCH in
|
||||
x86_64) UPX_ARCH="amd64" ;;
|
||||
aarch64) UPX_ARCH="arm64" ;;
|
||||
*) UPX_ARCH="amd64" ;;
|
||||
esac
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ && ! $TARGET =~ ^riscv64.*$ ]]; then
|
||||
UPX_VERSION=4.2.4
|
||||
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
|
||||
cp upx-${UPX_VERSION}-amd64_linux/upx .
|
||||
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
UPX_VERSION=5.1.1
|
||||
UPX_PKG="upx-${UPX_VERSION}-${UPX_ARCH}_linux"
|
||||
curl -L "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/${UPX_PKG}.tar.xz" -s | tar xJvf -
|
||||
cp "${UPX_PKG}/upx" .
|
||||
UPX_BIN=./upx
|
||||
fi
|
||||
|
||||
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
|
||||
fi
|
||||
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do
|
||||
if [[ -f "$BIN" ]]; then
|
||||
if [[ -n "$UPX_BIN" ]]; then
|
||||
$UPX_BIN --lzma --best "$BIN" || true
|
||||
fi
|
||||
|
||||
mv "$BIN" ./artifacts/objects/
|
||||
fi
|
||||
done
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
@@ -273,25 +254,10 @@ jobs:
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
core-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
- build
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
magisk_build:
|
||||
needs:
|
||||
- pre_job
|
||||
- build_web
|
||||
- build
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
build_magisk:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||
@@ -311,7 +277,6 @@ jobs:
|
||||
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
||||
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
||||
|
||||
|
||||
# 上传生成的模块
|
||||
- name: Upload Magisk Module
|
||||
uses: actions/upload-artifact@v5
|
||||
@@ -322,3 +287,12 @@ jobs:
|
||||
!./easytier-contrib/easytier-magisk/build.sh
|
||||
!./easytier-contrib/easytier-magisk/magisk_update.json
|
||||
if-no-files-found: error
|
||||
|
||||
core-result:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, build_web, build, build_magisk ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.6.0'
|
||||
default: 'v2.6.3'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
+39
-86
@@ -5,7 +5,12 @@ on:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
@@ -29,20 +35,20 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh", "easytier-web/frontend-lib/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
OS: ubuntu-24.04
|
||||
GUI_TARGET: x86_64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: aarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04-arm
|
||||
GUI_TARGET: aarch64-unknown-linux-gnu
|
||||
ARTIFACT_NAME: linux-aarch64
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
@@ -57,16 +63,14 @@ jobs:
|
||||
OS: windows-latest
|
||||
GUI_TARGET: x86_64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
OS: windows-latest
|
||||
GUI_TARGET: i686-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-i686
|
||||
- TARGET: aarch64-pc-windows-msvc
|
||||
OS: windows-11-arm
|
||||
GUI_TARGET: aarch64-pc-windows-msvc
|
||||
ARTIFACT_NAME: windows-arm64
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
@@ -80,75 +84,29 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: true
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: copy correct DLLs
|
||||
if: ${{ matrix.OS == 'windows-latest' }}
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'windows') }}
|
||||
run: |
|
||||
case $TARGET in
|
||||
x86_64*) ARCH_DIR=x86_64 ;;
|
||||
@@ -164,10 +122,9 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ contains(matrix.TARGET, '-linux-') && contains(matrix.TARGET, 'aarch64') && '--bundles deb,rpm' || '' }}
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Compress
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
@@ -176,18 +133,16 @@ jobs:
|
||||
else
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
# copy gui bundle, gui is built without specific target
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
if [[ $GUI_TARGET =~ windows ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
|
||||
elif [[ $OS =~ ^macos.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ darwin ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
|
||||
elif [[ $GUI_TARGET =~ linux ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||
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/
|
||||
fi
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
@@ -201,12 +156,10 @@ jobs:
|
||||
./artifacts/*
|
||||
|
||||
gui-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-gui
|
||||
needs: [ pre_job, build-gui ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-gui.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
sudo apt update
|
||||
sudo apt install -qq libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
patchelf
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# env needed:
|
||||
# - TARGET
|
||||
# - GUI_TARGET
|
||||
# - OS
|
||||
|
||||
# dependencies are only needed on ubuntu as that's the only place where
|
||||
# we make cross-compilation
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
|
||||
# https://github.com/cross-tools/musl-cross/releases
|
||||
# if "musl" is a substring of TARGET, we assume that we are using musl
|
||||
MUSL_TARGET=$TARGET
|
||||
# if target is mips or mipsel, we should use soft-float version of musl
|
||||
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET}sf
|
||||
elif [[ $TARGET =~ ^riscv64gc-.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET/#riscv64gc-/riscv64-}
|
||||
fi
|
||||
if [[ $MUSL_TARGET =~ musl ]]; then
|
||||
mkdir -p ./musl_gcc
|
||||
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
|
||||
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
|
||||
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
|
||||
sudo chmod -R a+rwx ./musl_gcc
|
||||
fi
|
||||
fi
|
||||
|
||||
# see https://github.com/rust-lang/rustup/issues/3709
|
||||
rustup set auto-self-update disable
|
||||
rustup install 1.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,7 +5,12 @@ on:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -18,6 +23,7 @@ jobs:
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
@@ -29,20 +35,25 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: android
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: android
|
||||
runs-on: ${{ matrix.OS }}
|
||||
- TARGET: aarch64-linux-android
|
||||
ARCH: aarch64
|
||||
- TARGET: armv7-linux-androideabi
|
||||
ARCH: armv7
|
||||
- TARGET: i686-linux-android
|
||||
ARCH: i686
|
||||
- TARGET: x86_64-linux-android
|
||||
ARCH: x86_64
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NAME: easytier
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
OS: ${{ matrix.OS }}
|
||||
ARCH: ${{ matrix.ARCH }}
|
||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
@@ -61,47 +72,41 @@ jobs:
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
cmdline-tools-version: 11076708
|
||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
|
||||
cmdline-tools-version: 12.0
|
||||
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 '
|
||||
|
||||
- name: Setup Android Environment
|
||||
run: |
|
||||
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
|
||||
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: ${{ matrix.TARGET }}
|
||||
gui: false
|
||||
pnpm: true
|
||||
pnpm-build-filter: ''
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The prefix cache key, this can be changed to start a new cache manually.
|
||||
# default: "v0-rust"
|
||||
prefix-key: ""
|
||||
shared-key: "gui-registry"
|
||||
cache-targets: "false"
|
||||
|
||||
- name: Install rust target
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
rustup target add aarch64-linux-android
|
||||
rustup target add armv7-linux-androideabi
|
||||
rustup target add i686-linux-android
|
||||
rustup target add x86_64-linux-android
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Android
|
||||
- name: Build
|
||||
run: |
|
||||
cd easytier-gui
|
||||
pnpm tauri android build
|
||||
pnpm tauri android build --apk --target "$ARCH" --split-per-abi
|
||||
|
||||
- name: Compress
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
|
||||
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/
|
||||
|
||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||
TAG=$GITHUB_REF_NAME
|
||||
@@ -109,23 +114,21 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
mv ./artifacts/objects/* ./artifacts
|
||||
mv ./artifacts/objects/* ./artifacts/
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
name: easytier-mobile-android-${{ matrix.ARCH }}
|
||||
path: |
|
||||
./artifacts/*
|
||||
|
||||
mobile-result:
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- pre_job
|
||||
- build-mobile
|
||||
needs: [ pre_job, build-mobile ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.build-mobile.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
@@ -6,14 +6,22 @@ on:
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
pull_request:
|
||||
branches: ["main", "develop"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "**/*.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-full-shell:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -26,5 +34,11 @@ jobs:
|
||||
- name: Magic Nix Cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||
|
||||
- name: Check full devShell
|
||||
- name: Warm up full devShell
|
||||
run: nix develop .#full --command true
|
||||
|
||||
- name: Cargo check in flake environment
|
||||
run: nix develop .#full --command cargo check
|
||||
|
||||
- name: Cargo build in flake environment
|
||||
run: nix develop .#full --command cargo build
|
||||
|
||||
+33
-12
@@ -8,8 +8,13 @@ on:
|
||||
- '!*-pre'
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -20,18 +25,29 @@ defaults:
|
||||
|
||||
jobs:
|
||||
cargo_fmt_check:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@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
|
||||
run: |
|
||||
bash ../../.github/workflows/install_rust.sh
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
# do not skip push on branch starts with releases/
|
||||
@@ -44,7 +60,8 @@ jobs:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
skip_after_successful_duplicate: "true"
|
||||
cancel_others: "true"
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/actions/**"]'
|
||||
|
||||
build-ohos:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
@@ -56,13 +73,12 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
sudo apt-get install -qq \
|
||||
build-essential \
|
||||
wget \
|
||||
unzip \
|
||||
git \
|
||||
pkg-config curl libgl1-mesa-dev expect
|
||||
sudo apt-get clean
|
||||
|
||||
- name: Resolve easytier version
|
||||
run: |
|
||||
@@ -134,6 +150,15 @@ jobs:
|
||||
run: |
|
||||
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
|
||||
|
||||
rustup install stable
|
||||
rustup default stable
|
||||
|
||||
rustup target add aarch64-unknown-linux-ohos
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: ohrs
|
||||
|
||||
- name: Create clang wrapper script
|
||||
run: |
|
||||
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
||||
@@ -152,11 +177,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get install -y llvm clang lldb lld
|
||||
sudo apt-get install -y protobuf-compiler
|
||||
bash ../../.github/workflows/install_rust.sh
|
||||
source env.sh
|
||||
cargo install ohrs
|
||||
rustup target add aarch64-unknown-linux-ohos
|
||||
cargo update easytier
|
||||
ohrs doctor
|
||||
ohrs build --release --arch aarch
|
||||
ohrs artifact
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.6.0'
|
||||
default: 'v2.6.3'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -92,4 +92,4 @@ jobs:
|
||||
files: |
|
||||
./zipped_assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
|
||||
+28
-19
@@ -6,6 +6,10 @@ on:
|
||||
pull_request:
|
||||
branches: [ "develop", "main" ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# RUSTC_WRAPPER: "sccache"
|
||||
@@ -30,7 +34,7 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
|
||||
check:
|
||||
name: Run linters & check
|
||||
@@ -44,35 +48,36 @@ jobs:
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
web: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install rustfmt and clippy
|
||||
run: |
|
||||
rustup component add rustfmt
|
||||
rustup component add clippy
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt,clippy
|
||||
rustflags: ''
|
||||
|
||||
- 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
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Check Clippy
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo clippy --all-targets --features full --all -- -D warnings
|
||||
|
||||
- name: Check features
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
|
||||
|
||||
- name: Check Cargo.lock is up to date
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
if ! cargo metadata --format-version 1 --locked > /dev/null; then
|
||||
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pre-test:
|
||||
name: Build test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -85,7 +90,7 @@ jobs:
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
gui: true
|
||||
web: true
|
||||
pnpm: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -123,6 +128,10 @@ jobs:
|
||||
|
||||
- name: Setup tools for test
|
||||
run: sudo apt install bridge-utils
|
||||
- name: Setup upnpd for test
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y miniupnpd miniupnpd-iptables iptables
|
||||
|
||||
- name: Setup system for test
|
||||
run: |
|
||||
@@ -146,9 +155,9 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ pre_job, test_matrix ]
|
||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||
needs: [ pre_job, check, test_matrix ]
|
||||
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.test_matrix.result != 'success'
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides
|
||||
#### Required Tools
|
||||
- Node.js v21 or higher
|
||||
- pnpm v9 or higher
|
||||
- Rust toolchain (version 1.93)
|
||||
- Rust toolchain (version 1.95)
|
||||
- LLVM and Clang
|
||||
- Protoc (Protocol Buffers compiler)
|
||||
|
||||
@@ -79,8 +79,8 @@ sudo apt install -y bridge-utils
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# Install Rust toolchain
|
||||
rustup install 1.93
|
||||
rustup default 1.93
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# Install project dependencies
|
||||
pnpm -r install
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@
|
||||
#### 必需工具
|
||||
- Node.js v21 或更高版本
|
||||
- pnpm v9 或更高版本
|
||||
- Rust 工具链(版本 1.93)
|
||||
- Rust 工具链(版本 1.95)
|
||||
- LLVM 和 Clang
|
||||
- Protoc(Protocol Buffers 编译器)
|
||||
|
||||
@@ -87,8 +87,8 @@ sudo apt install -y bridge-utils
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
# 安装 Rust 工具链
|
||||
rustup install 1.93
|
||||
rustup default 1.93
|
||||
rustup install 1.95
|
||||
rustup default 1.95
|
||||
|
||||
# 安装项目依赖
|
||||
pnpm -r install
|
||||
|
||||
Generated
+1678
-1099
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,10 @@ exclude = [
|
||||
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
|
||||
[profile.dev]
|
||||
panic = "unwind"
|
||||
debug = 2
|
||||
|
||||
@@ -108,9 +108,9 @@ After successful execution, you can check the network status using `easytier-cli
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-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.0-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
|
||||
+3
-3
@@ -108,9 +108,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-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.0-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.2-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.2-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.2-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-android-jni"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObjectArray, JString};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::JNIEnv;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
@@ -15,7 +15,7 @@ pub struct KeyValuePair {
|
||||
}
|
||||
|
||||
// 声明外部 C 函数
|
||||
extern "C" {
|
||||
unsafe extern "C" {
|
||||
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
||||
fn get_error_msg(out: *mut *const std::ffi::c_char);
|
||||
fn free_string(s: *const std::ffi::c_char);
|
||||
@@ -68,7 +68,7 @@ fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||
}
|
||||
|
||||
/// 设置 TUN 文件描述符
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -87,17 +87,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||
|
||||
unsafe {
|
||||
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析配置
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -115,17 +115,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||
|
||||
unsafe {
|
||||
let result = parse_config(config_cstr.as_ptr());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行网络实例
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -143,17 +143,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||
|
||||
unsafe {
|
||||
let result = run_network_instance(config_cstr.as_ptr());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 保持网络实例
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -165,10 +165,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
if instance_names.is_null() {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -187,10 +187,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
if array_length == 0 {
|
||||
unsafe {
|
||||
let result = retain_network_instance(ptr::null(), 0);
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -234,17 +234,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||
|
||||
unsafe {
|
||||
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||
if result != 0 {
|
||||
if let Some(error) = get_last_error() {
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
if result != 0
|
||||
&& let Some(error) = get_last_error()
|
||||
{
|
||||
throw_exception(&mut env, &error);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// 收集网络信息
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
@@ -304,7 +304,7 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||
}
|
||||
|
||||
/// 获取最后的错误信息
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -30,7 +30,7 @@ fn set_error_msg(msg: &str) {
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn set_tun_fd(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
fd: std::ffi::c_int,
|
||||
@@ -59,7 +59,7 @@ pub unsafe extern "C" fn set_tun_fd(
|
||||
|
||||
/// # Safety
|
||||
/// Get the last error message
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||
let msg_buf = ERROR_MSG.lock().unwrap();
|
||||
if msg_buf.is_empty() {
|
||||
@@ -74,7 +74,7 @@ pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||
if s.is_null() {
|
||||
return;
|
||||
@@ -86,7 +86,7 @@ pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||
|
||||
/// # Safety
|
||||
/// Parse the config
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
@@ -105,7 +105,7 @@ pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::
|
||||
|
||||
/// # Safety
|
||||
/// Run the network instance
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||
let cfg_str = unsafe {
|
||||
assert!(!cfg_str.is_null());
|
||||
@@ -144,7 +144,7 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
|
||||
|
||||
/// # Safety
|
||||
/// Retain the network instance
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn retain_network_instance(
|
||||
inst_names: *const *const std::ffi::c_char,
|
||||
length: usize,
|
||||
@@ -188,7 +188,7 @@ pub unsafe extern "C" fn retain_network_instance(
|
||||
|
||||
/// # Safety
|
||||
/// Collect the network infos
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn collect_network_infos(
|
||||
infos: *mut KeyValuePair,
|
||||
max_length: usize,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.0
|
||||
version=v2.6.3
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-uptime"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
@@ -12,6 +12,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
guarden = "0.1"
|
||||
|
||||
# Axum web framework
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
||||
QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
|
||||
@@ -14,7 +14,7 @@ use crate::api::{
|
||||
models::*,
|
||||
};
|
||||
use crate::db::entity::{self, health_records, shared_nodes};
|
||||
use crate::db::{operations::*, Db};
|
||||
use crate::db::{Db, operations::*};
|
||||
use crate::health_checker_manager::HealthCheckerManager;
|
||||
use axum_extra::extract::Query;
|
||||
use std::sync::Arc;
|
||||
@@ -273,7 +273,7 @@ pub struct InstanceFilterParams {
|
||||
use crate::config::AppConfig;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use chrono::{Duration, Utc};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -370,19 +370,19 @@ pub async fn admin_get_nodes(
|
||||
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
||||
filtered_ids = Some(ids);
|
||||
}
|
||||
if let Some(tags) = filters.tags {
|
||||
if !tags.is_empty() {
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(tags) = filters.tags
|
||||
&& !tags.is_empty()
|
||||
{
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use axum::Router;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::db::entity::*;
|
||||
use crate::db::Db;
|
||||
use crate::db::entity::*;
|
||||
use sea_orm::*;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio::time::{Duration, sleep};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
/// 数据清理策略配置
|
||||
|
||||
@@ -5,12 +5,12 @@ pub mod operations;
|
||||
use std::fmt;
|
||||
|
||||
use sea_orm::{
|
||||
prelude::*, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||
QueryFilter as _, Set, SqlxSqliteConnector, Statement, TransactionTrait as _,
|
||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
||||
SqlxSqliteConnector, Statement, TransactionTrait as _, prelude::*, sea_query::OnConflict,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{migrate::MigrateDatabase as _, Sqlite, SqlitePool};
|
||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _};
|
||||
|
||||
use crate::migrator;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::api::CreateNodeRequest;
|
||||
use crate::db::entity::*;
|
||||
use crate::db::Db;
|
||||
use crate::db::HealthStats;
|
||||
use crate::db::HealthStatus;
|
||||
use crate::db::entity::*;
|
||||
use sea_orm::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@ use std::{
|
||||
use anyhow::Context as _;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader},
|
||||
scoped_task::ScopedTask,
|
||||
common::config::{
|
||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
},
|
||||
defer,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
};
|
||||
use guarden::defer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
|
||||
use crate::db::{
|
||||
Db, HealthStatus,
|
||||
entity::shared_nodes,
|
||||
operations::{HealthOperations, NodeOperations},
|
||||
Db, HealthStatus,
|
||||
};
|
||||
|
||||
pub struct HealthCheckOneNode {
|
||||
@@ -240,7 +240,7 @@ pub struct HealthChecker {
|
||||
db: Db,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
inst_id_map: DashMap<i32, uuid::Uuid>,
|
||||
node_tasks: DashMap<i32, ScopedTask<()>>,
|
||||
node_tasks: DashMap<i32, AbortOnDropHandle<()>>,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
node_cfg: Arc<DashMap<i32, TomlConfigLoader>>,
|
||||
}
|
||||
@@ -465,7 +465,7 @@ impl HealthChecker {
|
||||
}
|
||||
|
||||
// 启动健康检查任务
|
||||
let task = ScopedTask::from(tokio::spawn(Self::node_health_check_task(
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(Self::node_health_check_task(
|
||||
node_id,
|
||||
cfg.get_id(),
|
||||
Arc::clone(&self.instance_mgr),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use tokio::time::{interval, Interval};
|
||||
use tokio::time::{Interval, interval};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
db::{entity::shared_nodes, operations::NodeOperations, Db},
|
||||
db::{Db, entity::shared_nodes, operations::NodeOperations},
|
||||
health_checker::HealthChecker,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ mod migrator;
|
||||
use api::routes::create_routes;
|
||||
use clap::Parser;
|
||||
use config::AppConfig;
|
||||
use db::{operations::NodeOperations, Db};
|
||||
use db::{Db, operations::NodeOperations};
|
||||
use easytier::common::log;
|
||||
use health_checker::HealthChecker;
|
||||
use health_checker_manager::HealthCheckerManager;
|
||||
@@ -49,7 +49,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
// 如果提供了管理员密码,设置环境变量
|
||||
if let Some(password) = args.admin_password {
|
||||
env::set_var("ADMIN_PASSWORD", password);
|
||||
unsafe {
|
||||
env::set_var("ADMIN_PASSWORD", password);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.3",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
@@ -59,4 +59,4 @@
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.0"
|
||||
version = "2.6.3"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -11,15 +11,6 @@ edition = "2021"
|
||||
name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
|
||||
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[target.i686-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[dependencies]
|
||||
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
||||
@@ -66,6 +57,14 @@ libc = "0.2"
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework-sys = "2.9.0"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
fn main() {
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
.unwrap_or_default()
|
||||
.contains("aarch64")
|
||||
{
|
||||
thunk::thunk();
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@@ -14,6 +15,35 @@ val tauriProperties = Properties().apply {
|
||||
}
|
||||
}
|
||||
|
||||
val versionPattern = Regex("""^(\d+)\.(\d+)\.(\d+)$""")
|
||||
|
||||
val tauriVersionName = tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } ?: run {
|
||||
val tauriConfFile = file("../../../tauri.conf.json")
|
||||
check(tauriConfFile.exists()) { "Missing tauri.conf.json at ${tauriConfFile.path}" }
|
||||
|
||||
val tauriConf = tauriConfFile.reader(Charsets.UTF_8).use { JsonSlurper().parse(it) as? Map<*, *> }
|
||||
?: error("Failed to parse ${tauriConfFile.path} as a JSON object")
|
||||
tauriConf["version"] as? String
|
||||
?: error("Missing string field \"version\" in ${tauriConfFile.path}")
|
||||
}
|
||||
|
||||
val tauriVersionMatch = versionPattern.matchEntire(tauriVersionName)
|
||||
?: error("Android version must use x.y.z format, but got \"$tauriVersionName\"")
|
||||
|
||||
val tauriVersionCode = if (tauriProperties.getProperty("tauri.android.versionName")?.ifBlank { null } != null) {
|
||||
val versionCodeProp = tauriProperties.getProperty("tauri.android.versionCode")
|
||||
if (versionCodeProp != null) {
|
||||
versionCodeProp.toIntOrNull()
|
||||
?: error("Property \"tauri.android.versionCode\" must be an integer, but got \"$versionCodeProp\"")
|
||||
} else {
|
||||
val (major, minor, patch) = tauriVersionMatch.destructured
|
||||
major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt()
|
||||
}
|
||||
} else {
|
||||
val (major, minor, patch) = tauriVersionMatch.destructured
|
||||
major.toInt() * 1_000_000 + minor.toInt() * 1_000 + patch.toInt()
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
namespace = "com.kkrainbow.easytier"
|
||||
@@ -22,8 +52,8 @@ android {
|
||||
applicationId = "com.kkrainbow.easytier"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
versionCode = tauriVersionCode
|
||||
versionName = tauriVersionName
|
||||
}
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
@@ -82,4 +112,4 @@ dependencies {
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
apply(from = "tauri.build.gradle.kts")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
use super::Command;
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::process::{Command as StdCommand, Output};
|
||||
|
||||
@@ -30,10 +30,10 @@ use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
|
||||
use libc::{fileno, wait, EINTR, SHUT_WR};
|
||||
use libc::{EINTR, SHUT_WR, fileno, wait};
|
||||
use security_framework_sys::authorization::{
|
||||
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
||||
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
|
||||
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
|
||||
};
|
||||
|
||||
const ENV_PATH: &str = "PATH";
|
||||
|
||||
@@ -11,11 +11,11 @@ use std::process::{ExitStatus, Output};
|
||||
use winapi::shared::minwindef::{DWORD, LPVOID};
|
||||
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
|
||||
use winapi::um::securitybaseapi::GetTokenInformation;
|
||||
use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY};
|
||||
use windows::core::{w, HSTRING, PCWSTR};
|
||||
use winapi::um::winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation};
|
||||
use windows::Win32::Foundation::HWND;
|
||||
use windows::Win32::UI::Shell::ShellExecuteW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
use windows::core::{HSTRING, PCWSTR, w};
|
||||
|
||||
/// The implementation of state check and elevated executing varies on each platform
|
||||
impl Command {
|
||||
|
||||
@@ -15,16 +15,18 @@ use easytier::rpc_service::remote_client::{
|
||||
use easytier::web_client::{self, WebClient};
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||
config::{
|
||||
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
||||
},
|
||||
log,
|
||||
},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::ApiRpcServer,
|
||||
tunnel::TunnelListener,
|
||||
tunnel::ring::RingTunnelListener,
|
||||
tunnel::tcp::TcpTunnelListener,
|
||||
tunnel::TunnelListener,
|
||||
utils::{self},
|
||||
utils::panic::setup_panic_handler,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
@@ -118,7 +120,7 @@ async fn run_network_instance(
|
||||
let client_manager = get_client_manager!()?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User)
|
||||
.await?;
|
||||
client_manager
|
||||
.handle_run_network_instance(app.clone(), cfg, save)
|
||||
@@ -207,13 +209,17 @@ async fn update_network_config_state(
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
let client_manager = get_client_manager!()?;
|
||||
if !disabled {
|
||||
let cfg = client_manager
|
||||
.handle_get_network_config(app.clone(), instance_id)
|
||||
let (cfg, source) = client_manager
|
||||
.handle_get_network_config_with_source(app.clone(), instance_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.pre_run_network_instance_hook(
|
||||
&app,
|
||||
&toml_config,
|
||||
manager::PersistedConfigSource::from_runtime_source(source),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
client_manager
|
||||
@@ -272,7 +278,7 @@ async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig
|
||||
#[tauri::command]
|
||||
async fn load_configs(
|
||||
app: AppHandle,
|
||||
configs: Vec<NetworkConfig>,
|
||||
configs: Vec<manager::StoredGuiConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
get_client_manager!()?
|
||||
@@ -559,10 +565,10 @@ fn toggle_window_visibility(app: &tauri::AppHandle) {
|
||||
}
|
||||
|
||||
fn get_exe_path() -> String {
|
||||
if let Ok(appimage_path) = std::env::var("APPIMAGE") {
|
||||
if !appimage_path.is_empty() {
|
||||
return appimage_path;
|
||||
}
|
||||
if let Ok(appimage_path) = std::env::var("APPIMAGE")
|
||||
&& !appimage_path.is_empty()
|
||||
{
|
||||
return appimage_path;
|
||||
}
|
||||
std::env::current_exe()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
@@ -596,8 +602,8 @@ mod manager {
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::rpc_service::logger::LoggerRpcService;
|
||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::tunnel::TunnelConnector;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::web_client::WebClientHooks;
|
||||
|
||||
pub(super) struct GuiHooks {
|
||||
@@ -612,7 +618,11 @@ mod manager {
|
||||
) -> Result<(), String> {
|
||||
let client_manager = get_client_manager!()?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&self.app, cfg)
|
||||
.pre_run_network_instance_hook(
|
||||
&self.app,
|
||||
cfg,
|
||||
PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -631,14 +641,87 @@ 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)]
|
||||
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
||||
pub(super) struct GUIConfig {
|
||||
inst_id: String,
|
||||
pub(crate) config: NetworkConfig,
|
||||
source: PersistedConfigSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(super) struct StoredGuiConfig {
|
||||
config: NetworkConfig,
|
||||
#[serde(default)]
|
||||
source: PersistedConfigSource,
|
||||
}
|
||||
|
||||
impl GUIConfig {
|
||||
fn new(inst_id: String, config: NetworkConfig, source: PersistedConfigSource) -> Self {
|
||||
Self {
|
||||
inst_id,
|
||||
config,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_stored(self) -> StoredGuiConfig {
|
||||
StoredGuiConfig {
|
||||
config: self.config,
|
||||
source: self.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
||||
fn get_network_inst_id(&self) -> &str {
|
||||
&self.0
|
||||
&self.inst_id
|
||||
}
|
||||
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
||||
Ok(self.1.clone())
|
||||
Ok(self.config.clone())
|
||||
}
|
||||
fn get_network_config_source(&self) -> ConfigSource {
|
||||
self.source.to_runtime_source()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,13 +738,12 @@ mod manager {
|
||||
}
|
||||
|
||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let configs: Result<Vec<String>, _> = self
|
||||
let configs = self
|
||||
.network_configs
|
||||
.iter()
|
||||
.map(|entry| serde_json::to_string(&entry.value().1))
|
||||
.collect();
|
||||
let payload = format!("[{}]", configs?.join(","));
|
||||
app.emit_str("save_configs", payload)?;
|
||||
.map(|entry| entry.value().clone().into_stored())
|
||||
.collect::<Vec<_>>();
|
||||
app.emit("save_configs", configs)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -680,8 +762,14 @@ mod manager {
|
||||
app: &AppHandle,
|
||||
inst_id: Uuid,
|
||||
cfg: NetworkConfig,
|
||||
source: PersistedConfigSource,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = GUIConfig(inst_id.to_string(), cfg);
|
||||
let source = self
|
||||
.network_configs
|
||||
.get(&inst_id)
|
||||
.map(|existing| existing.source.merge_persisted(source))
|
||||
.unwrap_or(source);
|
||||
let config = GUIConfig::new(inst_id.to_string(), cfg, source);
|
||||
self.network_configs.insert(inst_id, config);
|
||||
self.save_configs(app)
|
||||
}
|
||||
@@ -693,8 +781,14 @@ mod manager {
|
||||
app: AppHandle,
|
||||
network_inst_id: Uuid,
|
||||
network_config: NetworkConfig,
|
||||
source: ConfigSource,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.save_config(&app, network_inst_id, network_config)?;
|
||||
self.save_config(
|
||||
&app,
|
||||
network_inst_id,
|
||||
network_config,
|
||||
PersistedConfigSource::from_runtime_source(source),
|
||||
)?;
|
||||
self.enabled_networks.insert(network_inst_id);
|
||||
self.save_enabled_networks(&app)?;
|
||||
Ok(())
|
||||
@@ -811,17 +905,36 @@ mod manager {
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.1.no_tun())
|
||||
.filter_map(|c| c.1.instance_id().parse::<uuid::Uuid>().ok())
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn get_enabled_instances_with_webhook_like_tun_ids(
|
||||
&self,
|
||||
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
self.storage
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.config.no_tun())
|
||||
.filter(|v| v.source.is_webhook_like())
|
||||
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
webhook_only: bool,
|
||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
let inst_ids: Vec<uuid::Uuid> = self.get_enabled_instances_with_tun_ids().collect();
|
||||
let inst_ids: Vec<uuid::Uuid> = if webhook_only {
|
||||
self.get_enabled_instances_with_webhook_like_tun_ids()
|
||||
.collect()
|
||||
} else {
|
||||
self.get_enabled_instances_with_tun_ids().collect()
|
||||
};
|
||||
for inst_id in inst_ids {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
@@ -842,6 +955,7 @@ mod manager {
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
cfg: &easytier::common::config::TomlConfigLoader,
|
||||
source: PersistedConfigSource,
|
||||
) -> Result<(), String> {
|
||||
let instance_id = cfg.get_id();
|
||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||
@@ -849,9 +963,24 @@ mod manager {
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if !cfg.get_flags().no_tun {
|
||||
self.disable_instances_with_tun(app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
match source {
|
||||
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
||||
self.disable_instances_with_tun(app, false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
PersistedConfigSource::Webhook => {
|
||||
self.disable_instances_with_tun(app, true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
||||
return Err(
|
||||
"Android only supports one active TUN network; user-managed VPN remains active"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.storage
|
||||
@@ -859,6 +988,7 @@ mod manager {
|
||||
app,
|
||||
instance_id,
|
||||
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
||||
source,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -962,15 +1092,15 @@ mod manager {
|
||||
pub(super) async fn load_configs(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
configs: Vec<NetworkConfig>,
|
||||
configs: Vec<StoredGuiConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
for cfg in configs {
|
||||
let instance_id = cfg.instance_id();
|
||||
for stored in configs {
|
||||
let instance_id = stored.config.instance_id();
|
||||
self.storage.network_configs.insert(
|
||||
instance_id.parse()?,
|
||||
GUIConfig(instance_id.to_string(), cfg),
|
||||
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -979,34 +1109,35 @@ mod manager {
|
||||
.get_rpc_client(app.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
||||
for id in enabled_networks {
|
||||
if let Ok(uuid) = id.parse() {
|
||||
if !self.storage.enabled_networks.contains(&uuid) {
|
||||
let config = self
|
||||
.storage
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| i.value().1.clone());
|
||||
let Some(config) = config else {
|
||||
continue;
|
||||
};
|
||||
let toml_config = config.gen_config()?;
|
||||
self.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config: Some(config),
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.post_run_network_instance_hook(&app, &uuid)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
}
|
||||
if let Ok(uuid) = id.parse()
|
||||
&& !self.storage.enabled_networks.contains(&uuid)
|
||||
{
|
||||
let config = self
|
||||
.storage
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| (i.value().config.clone(), i.value().source));
|
||||
let Some((config, source)) = config else {
|
||||
continue;
|
||||
};
|
||||
let toml_config = config.gen_config()?;
|
||||
self.pre_run_network_instance_hook(&app, &toml_config, source)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config: Some(config),
|
||||
overwrite: false,
|
||||
source: source.to_runtime_source().to_rpc(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.post_run_network_instance_hook(&app, &uuid)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1032,6 +1163,44 @@ mod manager {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PersistedConfigSource, StoredGuiConfig};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
|
||||
#[test]
|
||||
fn stored_gui_config_defaults_missing_source_to_legacy() {
|
||||
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
||||
"config": NetworkConfig::default(),
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persisted_source_merge_keeps_legacy_and_webhook_over_ambiguous_user() {
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Legacy
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Webhook.merge_persisted(PersistedConfigSource::User),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
assert_eq!(
|
||||
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Webhook),
|
||||
PersistedConfigSource::Webhook
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_webhook_configs_are_webhook_like() {
|
||||
assert!(!PersistedConfigSource::Legacy.is_webhook_like());
|
||||
assert!(!PersistedConfigSource::User.is_webhook_like());
|
||||
assert!(PersistedConfigSource::Webhook.is_webhook_like());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -1120,7 +1289,7 @@ pub fn run_gui() -> std::process::ExitCode {
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
utils::setup_panic_handler();
|
||||
setup_panic_handler();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.3",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
@@ -36,4 +36,4 @@
|
||||
"csp": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
type ConfigSource = 'user' | 'webhook' | 'legacy'
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
@@ -16,6 +17,39 @@ interface ServiceOptions {
|
||||
|
||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||
|
||||
interface StoredGuiConfig {
|
||||
config: NetworkConfig
|
||||
source: ConfigSource
|
||||
}
|
||||
|
||||
function parseStoredConfigs(raw: string | null): StoredGuiConfig[] {
|
||||
const parsed: unknown = JSON.parse(raw || '[]')
|
||||
if (!Array.isArray(parsed)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return parsed.flatMap((entry): StoredGuiConfig[] => {
|
||||
if (entry && typeof entry === 'object' && 'config' in entry) {
|
||||
const { config, source } = entry as {
|
||||
config?: NetworkConfig
|
||||
source?: ConfigSource
|
||||
}
|
||||
if (!config) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: source === 'user' || source === 'webhook' ? source : 'legacy',
|
||||
}]
|
||||
}
|
||||
|
||||
return [{
|
||||
config: NetworkTypes.normalizeNetworkConfig(entry as NetworkConfig),
|
||||
source: 'legacy',
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg: NetworkTypes.toBackendNetworkConfig(cfg) })
|
||||
}
|
||||
@@ -71,9 +105,12 @@ export async function getConfig(instanceId: string) {
|
||||
}
|
||||
|
||||
export async function sendConfigs(enabledNetworks: string[]) {
|
||||
const networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||
const networkList = parseStoredConfigs(localStorage.getItem('networkList'))
|
||||
return await invoke('load_configs', {
|
||||
configs: networkList.map((config) => NetworkTypes.toBackendNetworkConfig(NetworkTypes.normalizeNetworkConfig(config))),
|
||||
configs: networkList.map(({ config, source }) => ({
|
||||
config: NetworkTypes.toBackendNetworkConfig(config),
|
||||
source,
|
||||
})),
|
||||
enabledNetworks
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ import { type } from "@tauri-apps/plugin-os";
|
||||
import { NetworkTypes } from "easytier-frontend-lib"
|
||||
import { Utils } from "easytier-frontend-lib";
|
||||
|
||||
interface StoredGuiConfig {
|
||||
config: NetworkTypes.NetworkConfig
|
||||
source?: 'user' | 'webhook' | 'legacy'
|
||||
}
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
SAVE_CONFIGS: 'save_configs',
|
||||
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
|
||||
@@ -13,9 +18,15 @@ const EVENTS = Object.freeze({
|
||||
EVENT_LAGGED: 'event_lagged',
|
||||
});
|
||||
|
||||
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
function onSaveConfigs(event: Event<StoredGuiConfig[]>) {
|
||||
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
|
||||
localStorage.setItem('networkList', JSON.stringify(event.payload.map((config) => NetworkTypes.normalizeNetworkConfig(config))));
|
||||
localStorage.setItem(
|
||||
'networkList',
|
||||
JSON.stringify(event.payload.map(({ config, source }) => ({
|
||||
config: NetworkTypes.normalizeNetworkConfig(config),
|
||||
source: source ?? 'legacy',
|
||||
}))),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInstanceIdPayload(payload: unknown): string {
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ServiceMode extends WebClientConfig {
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
installed_core_version?: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||
import { saveLastNetworkInstanceId, loadLastNetworkInstanceId } from '~/composables/config'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getServiceStatus } from '~/composables/backend'
|
||||
import { getEasytierVersion, getServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const confirm = useConfirm()
|
||||
@@ -85,6 +85,20 @@ async function onUninstallService() {
|
||||
});
|
||||
}
|
||||
|
||||
function stripModeMetadata(mode: Mode) {
|
||||
if (mode.mode !== 'service') {
|
||||
return mode
|
||||
}
|
||||
|
||||
const serviceConfig = { ...mode }
|
||||
delete serviceConfig.installed_core_version
|
||||
return serviceConfig
|
||||
}
|
||||
|
||||
function modeConfigChanged(next: Mode) {
|
||||
return JSON.stringify(stripModeMetadata(next)) !== JSON.stringify(stripModeMetadata(currentMode.value))
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
manualDisconnect.value = true
|
||||
@@ -134,13 +148,14 @@ async function initWithMode(mode: Mode) {
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service':
|
||||
case 'service': {
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
const coreVersion = await getEasytierVersion()
|
||||
if (serviceStatus === "NotInstalled" || modeConfigChanged(mode) || mode.installed_core_version !== coreVersion) {
|
||||
mode.config_server_url = mode.config_server_url || undefined
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
@@ -149,6 +164,7 @@ async function initWithMode(mode: Mode) {
|
||||
rpc_portal: mode.rpc_portal,
|
||||
config_server: mode.config_server_url,
|
||||
})
|
||||
mode.installed_core_version = coreVersion
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
@@ -157,6 +173,7 @@ async function initWithMode(mode: Mode) {
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
case 'normal':
|
||||
url = mode.rpc_portal;
|
||||
break;
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
name = "easytier-rpc-build"
|
||||
description = "Protobuf RPC Service Generator for EasyTier"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
rust-version = "1.93.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.6.0"
|
||||
edition = "2021"
|
||||
version = "2.6.3"
|
||||
edition.workspace = true
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
[dependencies]
|
||||
@@ -10,6 +10,7 @@ tracing = { version = "0.1", features = ["log"] }
|
||||
anyhow = { version = "1.0" }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
dashmap = "6.1"
|
||||
url = "2.2"
|
||||
async-trait = "0.1"
|
||||
@@ -69,13 +70,11 @@ subtle = "2.6"
|
||||
|
||||
mimalloc = { version = "*" }
|
||||
|
||||
[build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
embed = ["dep:axum-embed"]
|
||||
|
||||
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
[target.i686-pc-windows-msvc.build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
.unwrap_or_default()
|
||||
.contains("aarch64")
|
||||
{
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { AutoComplete, Button, Checkbox, Dialog, Divider, InputNumber, InputText, Panel, Password, SelectButton, ToggleButton } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
|
||||
import {
|
||||
addRow,
|
||||
DEFAULT_NETWORK_CONFIG,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../types/network'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AclManager from './acl/AclManager.vue'
|
||||
import UrlListInput from './UrlListInput.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -80,6 +81,7 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'latency_first', help: 'latency_first_help' },
|
||||
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
|
||||
{ field: 'disable_ipv6', help: 'disable_ipv6_help' },
|
||||
{ field: 'ipv6_public_addr_auto', help: 'ipv6_public_addr_auto_help' },
|
||||
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
|
||||
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
|
||||
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
|
||||
@@ -97,6 +99,7 @@ const bool_flags: BoolFlag[] = [
|
||||
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||
{ field: 'disable_tcp_hole_punching', help: 'disable_tcp_hole_punching_help' },
|
||||
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||
{ field: 'disable_upnp', help: 'disable_upnp_help' },
|
||||
{ field: 'disable_sym_hole_punching', help: 'disable_sym_hole_punching_help' },
|
||||
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||
@@ -488,6 +491,18 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel :header="t('acl.title')" toggleable collapsed>
|
||||
<div v-if="curNetwork.acl" class="flex flex-col gap-y-2">
|
||||
<AclManager v-model="curNetwork.acl" />
|
||||
</div>
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('acl.enabled')"
|
||||
@click="curNetwork.acl = { acl_v1: { chains: [], group: { declares: [], members: [] } } }" />
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-6 justify-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { AutoComplete, Button, Dialog, InputNumber, InputText } from 'primevue'
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -13,26 +13,9 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
const url = defineModel<string>({ required: true })
|
||||
const editing = ref(false)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const internalCompact = ref(false)
|
||||
const hostFocused = ref(false)
|
||||
|
||||
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 parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => {
|
||||
const getValidPort = (portStr: string, proto: string) => {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||
@@ -55,13 +38,16 @@ const parseUrl = (val: string | null | undefined) => {
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
const port = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : (props.protos[proto] ?? 11010)
|
||||
// null = no explicit port in URL; do not fabricate a default
|
||||
const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
}
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||
const port = portMatch ? parseInt(portMatch[2]) : (props.protos[proto] ?? 11010)
|
||||
// null = no explicit port in URL; buildUrlValue will omit the port entirely,
|
||||
// preserving the protocol's implied standard port (e.g. 443 for wss://).
|
||||
const port: number | null = portMatch ? parseInt(portMatch[2]) : null
|
||||
return { proto, host, port }
|
||||
}
|
||||
|
||||
@@ -72,28 +58,26 @@ const parseUrl = (val: string | null | undefined) => {
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: 11010 }
|
||||
return { proto: 'tcp', host: '', port: null }
|
||||
}
|
||||
|
||||
const internalValue = ref(parseUrl(url.value))
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number }, forceDefaultHost = false) => {
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => {
|
||||
const proto = value.proto || 'tcp'
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
let port = value.port
|
||||
if (isNaN(parseInt(port as any))) {
|
||||
port = props.protos[proto] ?? 11010
|
||||
}
|
||||
|
||||
if (props.protos[proto] === 0) {
|
||||
// Omit port when the protocol uses no port (protos value = 0), or when the
|
||||
// original URL had no explicit port (port === null) – avoids overwriting an
|
||||
// implicit standard port (e.g. 443 for wss) with an EasyTier default (11012).
|
||||
if (props.protos[proto] === 0 || value.port === null) {
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
return `${proto}://${host}:${port}`
|
||||
return `${proto}://${host}:${value.port}`
|
||||
}
|
||||
|
||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
@@ -168,27 +152,30 @@ const onProtoChange = (newProto: string) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" class="w-full">
|
||||
<InputGroup v-if="!internalCompact" class="w-full">
|
||||
<div class="url-input-container w-full min-w-0 overflow-hidden">
|
||||
<InputGroup class="url-input-full w-full min-w-0">
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||
@update:model-value="onProtoChange" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow min-w-0"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
|
||||
fluid />
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
|
||||
</template>
|
||||
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
|
||||
<slot name="actions"></slot>
|
||||
</InputGroup>
|
||||
|
||||
<div v-else class="flex justify-between items-center p-2 border rounded w-full">
|
||||
<span class="truncate mr-2">{{ url }}</span>
|
||||
<div class="flex items-center">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" @click="editing = true" />
|
||||
<div
|
||||
class="url-input-compact flex justify-between items-center p-2 border rounded w-full min-w-0 overflow-hidden">
|
||||
<span class="truncate mr-2 min-w-0 flex-1 overflow-hidden">{{ url }}</span>
|
||||
<div class="flex items-center shrink-0">
|
||||
<Button icon="pi pi-pencil" class="p-button-sm p-button-text" :aria-label="t('web.common.edit')"
|
||||
@click="editing = true" />
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +194,8 @@ const onProtoChange = (newProto: string) => {
|
||||
</div>
|
||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||
<label>{{ t('port') }}</label>
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full" />
|
||||
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full"
|
||||
:placeholder="String(protos[internalValue.proto] ?? 11010)" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -219,6 +207,28 @@ const onProtoChange = (newProto: string) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.url-input-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.url-input-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.url-input-full {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.url-input-compact {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.proto-autocomplete-in-group,
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-input),
|
||||
.proto-autocomplete-in-group :deep(.p-autocomplete-dropdown) {
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Column, DataTable, Divider, InputText, Select, SelectButton, ToggleButton } from 'primevue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AclAction, AclChain, AclChainType, AclProtocol, AclRule } from '../../types/network'
|
||||
import AclRuleDialog from './AclRuleDialog.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const chain = defineModel<AclChain>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
watch(() => chain.value.rules, (newRules) => {
|
||||
if (!newRules) return
|
||||
const isSorted = newRules.every((rule, i) => i === 0 || (rule.priority || 0) <= (newRules[i - 1].priority || 0))
|
||||
if (!isSorted) {
|
||||
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const actionOptions = [
|
||||
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||
]
|
||||
|
||||
const chainTypeOptions = [
|
||||
{ label: () => t('acl.inbound'), value: AclChainType.Inbound },
|
||||
{ label: () => t('acl.outbound'), value: AclChainType.Outbound },
|
||||
{ label: () => t('acl.forward'), value: AclChainType.Forward },
|
||||
]
|
||||
|
||||
const editingRule = ref<AclRule | null>(null)
|
||||
const editingRuleIndex = ref(-1)
|
||||
const showRuleDialog = ref(false)
|
||||
|
||||
function getProtocolLabel(proto: AclProtocol) {
|
||||
switch (proto) {
|
||||
case AclProtocol.Any: return t('acl.any')
|
||||
case AclProtocol.TCP: return 'TCP'
|
||||
case AclProtocol.UDP: return 'UDP'
|
||||
case AclProtocol.ICMP: return 'ICMP'
|
||||
case AclProtocol.ICMPv6: return 'ICMPv6'
|
||||
default: return t('event.Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
function getActionLabel(action: AclAction) {
|
||||
switch (action) {
|
||||
case AclAction.Allow: return t('acl.allow')
|
||||
case AclAction.Drop: return t('acl.drop')
|
||||
default: return t('event.Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
editingRuleIndex.value = -1
|
||||
editingRule.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
priority: chain.value.rules.length,
|
||||
enabled: true,
|
||||
protocol: AclProtocol.Any,
|
||||
ports: [],
|
||||
source_ips: [],
|
||||
destination_ips: [],
|
||||
source_ports: [],
|
||||
action: AclAction.Allow,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: false,
|
||||
source_groups: [],
|
||||
destination_groups: [],
|
||||
}
|
||||
showRuleDialog.value = true
|
||||
}
|
||||
|
||||
function editRule(index: number) {
|
||||
editingRuleIndex.value = index
|
||||
editingRule.value = JSON.parse(JSON.stringify(chain.value.rules[index]))
|
||||
showRuleDialog.value = true
|
||||
}
|
||||
|
||||
function deleteRule(index: number) {
|
||||
chain.value.rules.splice(index, 1)
|
||||
}
|
||||
|
||||
function saveRule(rule: AclRule) {
|
||||
if (editingRuleIndex.value === -1) {
|
||||
chain.value.rules.push(rule)
|
||||
} else {
|
||||
chain.value.rules[editingRuleIndex.value] = rule
|
||||
}
|
||||
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
}
|
||||
|
||||
function onRowReorder(event: any) {
|
||||
chain.value.rules = event.value
|
||||
// Update priorities based on new order (higher priority at top)
|
||||
chain.value.rules.forEach((rule, index) => {
|
||||
rule.priority = chain.value.rules.length - index - 1
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<!-- Chain Metadata Section -->
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-900 dark:border-gray-700">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.chain.name') }}</label>
|
||||
<InputText v-model="chain.name" size="small" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.rule.description') }}</label>
|
||||
<InputText v-model="chain.description" size="small" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 col-span-full border-t pt-2 mt-2 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.rule.enabled') }}</label>
|
||||
<ToggleButton v-model="chain.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="font-bold text-sm">{{ t('acl.chain.type') }}</label>
|
||||
<Select v-model="chain.chain_type" :options="chainTypeOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" size="small" class="w-40" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-auto">
|
||||
<label class="font-bold text-sm">{{ t('acl.default_action') }}</label>
|
||||
<SelectButton v-model="chain.default_action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" :allow-empty="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center gap-4 justify-between">
|
||||
<h4 class="text-md font-bold">{{ t('acl.rules') }}</h4>
|
||||
<Button icon="pi pi-plus" :label="t('acl.add_rule')" severity="success" size="small" @click="addRule" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="chain.rules" @row-reorder="onRowReorder" responsiveLayout="scroll">
|
||||
<Column rowReorder headerStyle="width: 3rem" />
|
||||
<Column field="enabled" :header="t('acl.rule.enabled')">
|
||||
<template #body="{ data }">
|
||||
<i class="pi" :class="data.enabled ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'"></i>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="name" :header="t('acl.rule.name')" />
|
||||
<Column :header="t('acl.match')">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-col gap-2 py-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md text-[10px] font-bold uppercase tracking-wider">
|
||||
{{ getProtocolLabel(data.protocol) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Src</span>
|
||||
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||
<span v-for="ip in data.source_ips" :key="ip"
|
||||
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||
<span v-for="grp in data.source_groups" :key="grp"
|
||||
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||
<span v-if="data.source_ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||
data.source_ports.join(',') }}</span>
|
||||
<span v-if="!data.source_ips.length && !data.source_groups.length" class="text-gray-400">*</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<i class="pi pi-arrow-right hidden sm:block text-gray-300 text-xs"></i>
|
||||
<Divider layout="horizontal" class="sm:hidden my-1" />
|
||||
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Dst</span>
|
||||
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||
<span v-for="ip in data.destination_ips" :key="ip"
|
||||
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||
<span v-for="grp in data.destination_groups" :key="grp"
|
||||
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||
<span v-if="data.ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||
data.ports.join(',') }}</span>
|
||||
<span v-if="!data.destination_ips.length && !data.destination_groups.length"
|
||||
class="text-gray-400">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="action" :header="t('acl.rule.action')">
|
||||
<template #body="{ data }">
|
||||
<span :class="data.action === AclAction.Allow ? 'text-green-600' : 'text-red-600 font-bold'">
|
||||
{{ getActionLabel(data.action) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :header="t('web.common.edit')">
|
||||
<template #body="{ index }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" text rounded @click="editRule(index)" />
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteRule(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<AclRuleDialog v-if="showRuleDialog && editingRule" v-model:visible="showRuleDialog" v-model:rule="editingRule"
|
||||
:group-names="props.groupNames" @save="saveRule" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Column, DataTable, Dialog, InputText, MultiSelect, Password } from 'primevue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { GroupIdentity, GroupInfo } from '../../types/network';
|
||||
|
||||
const props = defineProps<{
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const group = defineModel<GroupInfo>({ required: true })
|
||||
const emit = defineEmits(['rename-group'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const editingGroup = ref<GroupIdentity | null>(null)
|
||||
const editingGroupIndex = ref(-1)
|
||||
const showGroupDialog = ref(false)
|
||||
const oldGroupName = ref('')
|
||||
|
||||
function addGroup() {
|
||||
editingGroupIndex.value = -1
|
||||
editingGroup.value = {
|
||||
group_name: '',
|
||||
group_secret: '',
|
||||
}
|
||||
oldGroupName.value = ''
|
||||
showGroupDialog.value = true
|
||||
}
|
||||
|
||||
function editGroup(index: number) {
|
||||
editingGroupIndex.value = index
|
||||
editingGroup.value = JSON.parse(JSON.stringify(group.value.declares[index]))
|
||||
oldGroupName.value = editingGroup.value?.group_name || ''
|
||||
showGroupDialog.value = true
|
||||
}
|
||||
|
||||
function deleteGroup(index: number) {
|
||||
group.value.declares.splice(index, 1)
|
||||
}
|
||||
|
||||
function saveGroup() {
|
||||
if (!editingGroup.value) return
|
||||
const newName = editingGroup.value.group_name
|
||||
|
||||
if (editingGroupIndex.value === -1) {
|
||||
group.value.declares.push(editingGroup.value)
|
||||
} else {
|
||||
if (oldGroupName.value && oldGroupName.value !== newName) {
|
||||
// Sync in members
|
||||
group.value.members = group.value.members.map(m => m === oldGroupName.value ? newName : m)
|
||||
// Notify parent to sync in rules
|
||||
emit('rename-group', { oldName: oldGroupName.value, newName })
|
||||
}
|
||||
group.value.declares[editingGroupIndex.value] = editingGroup.value
|
||||
}
|
||||
showGroupDialog.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold text-lg">{{ t('acl.group.declares') }}</label>
|
||||
<small class="text-gray-500">{{ t('acl.group.help') }}</small>
|
||||
</div>
|
||||
<Button icon="pi pi-plus" :label="t('web.common.add')" severity="success" @click="addGroup" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="group.declares" responsiveLayout="scroll">
|
||||
<Column field="group_name" :header="t('acl.group.name')" />
|
||||
<Column field="group_secret" :header="t('acl.group.secret')">
|
||||
<template #body="{ data }">
|
||||
<Password v-model="data.group_secret" :feedback="false" toggleMask readonly plain class="w-full" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column :header="t('web.common.edit')" headerStyle="width: 8rem">
|
||||
<template #body="{ index }">
|
||||
<div class="flex gap-2">
|
||||
<Button icon="pi pi-pencil" text rounded @click="editGroup(index)" />
|
||||
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteGroup(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold text-lg">{{ t('acl.group.members') }}</label>
|
||||
<MultiSelect v-model="group.members" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.group.members')" />
|
||||
</div>
|
||||
|
||||
<!-- Group Identity Dialog -->
|
||||
<Dialog v-model:visible="showGroupDialog" modal :header="t('acl.groups')" :style="{ width: '400px' }">
|
||||
<div v-if="editingGroup" class="flex flex-col gap-4 pt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.group.name') }}</label>
|
||||
<InputText v-model="editingGroup.group_name" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.group.secret') }}</label>
|
||||
<Password v-model="editingGroup.group_secret" :feedback="false" toggleMask fluid />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="showGroupDialog = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="saveGroup" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Menu, Tab, TabList, TabPanel, TabPanels, Tabs } from 'primevue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Acl, AclAction, AclChainType } from '../../types/network'
|
||||
import AclChainEditor from './AclChainEditor.vue'
|
||||
import AclGroupEditor from './AclGroupEditor.vue'
|
||||
|
||||
const acl = defineModel<Acl>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref(0)
|
||||
const menu = ref()
|
||||
|
||||
const addMenuModel = ref([
|
||||
{ label: () => t('acl.inbound'), command: () => addChain(AclChainType.Inbound) },
|
||||
{ label: () => t('acl.outbound'), command: () => addChain(AclChainType.Outbound) },
|
||||
{ label: () => t('acl.forward'), command: () => addChain(AclChainType.Forward) },
|
||||
])
|
||||
|
||||
function addChain(type: AclChainType) {
|
||||
if (!acl.value.acl_v1) {
|
||||
acl.value.acl_v1 = { chains: [], group: { declares: [], members: [] } }
|
||||
}
|
||||
|
||||
let defaultName = ''
|
||||
switch (type) {
|
||||
case AclChainType.Inbound: defaultName = 'Inbound'; break;
|
||||
case AclChainType.Outbound: defaultName = 'Outbound'; break;
|
||||
case AclChainType.Forward: defaultName = 'Forward'; break;
|
||||
}
|
||||
|
||||
acl.value.acl_v1.chains.push({
|
||||
name: defaultName,
|
||||
chain_type: type,
|
||||
description: '',
|
||||
enabled: true,
|
||||
rules: [],
|
||||
default_action: AclAction.Allow
|
||||
})
|
||||
|
||||
activeTab.value = acl.value.acl_v1.chains.length - 1
|
||||
}
|
||||
|
||||
function removeChain(index: number) {
|
||||
if (confirm(t('acl.delete_chain_confirm'))) {
|
||||
acl.value.acl_v1?.chains.splice(index, 1)
|
||||
if (activeTab.value >= (acl.value.acl_v1?.chains.length || 0)) {
|
||||
activeTab.value = Math.max(0, (acl.value.acl_v1?.chains.length || 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRenameGroup({ oldName, newName }: { oldName: string, newName: string }) {
|
||||
if (!acl.value.acl_v1) return
|
||||
acl.value.acl_v1.chains.forEach(chain => {
|
||||
chain.rules.forEach(rule => {
|
||||
rule.source_groups = rule.source_groups.map(g => g === oldName ? newName : g)
|
||||
rule.destination_groups = rule.destination_groups.map(g => g === oldName ? newName : g)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const groupNames = computed(() => {
|
||||
return acl.value.acl_v1?.group?.declares.map(g => g.group_name) || []
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
const chains = acl.value.acl_v1?.chains || []
|
||||
const result: { type: string, label: string, index: number }[] = []
|
||||
|
||||
if (chains.length === 0) {
|
||||
result.push({ type: 'empty', label: t('acl.chains'), index: 0 })
|
||||
}
|
||||
else {
|
||||
chains.forEach((c, index) => {
|
||||
result.push({
|
||||
type: 'chain',
|
||||
label: c.name || `Chain ${index}`,
|
||||
index
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
result.push({ type: 'groups', label: t('acl.groups'), index: result.length })
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Tabs v-model:value="activeTab">
|
||||
<div class="flex items-center border-b border-surface-200 dark:border-surface-700">
|
||||
<TabList class="flex-grow min-w-0 overflow-x-auto" style="border-bottom: none;">
|
||||
<Tab v-for="tab in tabs" :key="tab.type + tab.index" :value="tab.index">
|
||||
<div class="flex items-center gap-2 whitespace-nowrap">
|
||||
{{ tab.label }}
|
||||
<Button v-if="tab.type === 'chain'" icon="pi pi-times" severity="danger" text rounded size="small"
|
||||
class="w-6 h-6 p-0" @click.stop="removeChain(tab.index)" />
|
||||
</div>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center px-2 bg-white dark:bg-gray-900 border-l border-surface-100 dark:border-surface-800">
|
||||
<Button icon="pi pi-plus" text rounded size="small" class="w-8 h-8 p-0"
|
||||
@click="(event) => menu.toggle(event)" />
|
||||
<Menu ref="menu" :model="addMenuModel" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
<TabPanels>
|
||||
<TabPanel v-for="tab in tabs" :key="'panel' + tab.type + tab.index" :value="tab.index">
|
||||
<!-- Empty State within TabPanel -->
|
||||
<div v-if="tab.type === 'empty'"
|
||||
class="py-8 flex flex-col items-center justify-center border-2 border-dashed border-surface-200 rounded-lg bg-surface-50 dark:bg-surface-900 dark:border-surface-700">
|
||||
<i class="pi pi-shield text-5xl mb-4 text-primary" />
|
||||
<div class="text-xl font-bold mb-2">{{ t('acl.chains') }}</div>
|
||||
<p class="text-surface-500 mb-8 text-center max-w-sm px-4">{{ t('acl.help') }}</p>
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<Button :label="t('acl.inbound')" icon="pi pi-arrow-down-left" @click="addChain(AclChainType.Inbound)" />
|
||||
<Button :label="t('acl.outbound')" icon="pi pi-arrow-up-right" @click="addChain(AclChainType.Outbound)" />
|
||||
<Button :label="t('acl.forward')" icon="pi pi-directions" @click="addChain(AclChainType.Forward)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rule Chains -->
|
||||
<div v-if="tab.type === 'chain' && acl.acl_v1 && acl.acl_v1.chains[tab.index]" class="py-4">
|
||||
<AclChainEditor v-model="acl.acl_v1.chains[tab.index]" :group-names="groupNames" />
|
||||
</div>
|
||||
|
||||
<!-- Group Management -->
|
||||
<div v-if="tab.type === 'groups'" class="py-4">
|
||||
<template v-if="acl.acl_v1">
|
||||
<AclGroupEditor v-if="acl.acl_v1.group" v-model="acl.acl_v1.group" :group-names="groupNames"
|
||||
@rename-group="handleRenameGroup" />
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('web.common.add') + ' ' + t('acl.groups')"
|
||||
@click="acl.acl_v1.group = { declares: [], members: [] }" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex justify-center p-4">
|
||||
<Button :label="t('acl.enabled')"
|
||||
@click="acl.acl_v1 = { chains: [], group: { declares: [], members: [] } }" />
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,150 @@
|
||||
<script setup lang="ts">
|
||||
import { AutoComplete, Button, Checkbox, Dialog, InputNumber, InputText, MultiSelect, Panel, SelectButton, ToggleButton } from 'primevue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AclAction, AclProtocol, AclRule } from '../../types/network';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
groupNames?: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save'])
|
||||
|
||||
const rule = defineModel<AclRule>('rule', { required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const protocolOptions = [
|
||||
{ label: () => t('acl.any'), value: AclProtocol.Any },
|
||||
{ label: 'TCP', value: AclProtocol.TCP },
|
||||
{ label: 'UDP', value: AclProtocol.UDP },
|
||||
{ label: 'ICMP', value: AclProtocol.ICMP },
|
||||
{ label: 'ICMPv6', value: AclProtocol.ICMPv6 },
|
||||
]
|
||||
|
||||
const actionOptions = [
|
||||
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||
]
|
||||
|
||||
const showPorts = computed(() => {
|
||||
return rule.value.protocol === AclProtocol.TCP || rule.value.protocol === AclProtocol.UDP || rule.value.protocol === AclProtocol.Any
|
||||
})
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', rule.value)
|
||||
close()
|
||||
}
|
||||
|
||||
// Suggestions for IP/Port AutoComplete
|
||||
const genericSuggestions = ref<string[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal :header="t('acl.edit_rule')"
|
||||
:style="{ width: '90vw', maxWidth: '600px' }">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.name') }}</label>
|
||||
<InputText v-model="rule.name" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.enabled') }}</label>
|
||||
<ToggleButton v-model="rule.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.description') }}</label>
|
||||
<InputText v-model="rule.description" fluid />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.action') }}</label>
|
||||
<SelectButton v-model="rule.action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||
option-value="value" :allow-empty="false" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.protocol') }}</label>
|
||||
<SelectButton v-model="rule.protocol" :options="protocolOptions"
|
||||
:option-label="opt => typeof opt.label === 'function' ? opt.label() : opt.label" option-value="value"
|
||||
:allow-empty="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Panel :header="t('acl.rules')" toggleable>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.src_ips') }}</label>
|
||||
<AutoComplete v-model="rule.source_ips" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]"
|
||||
:placeholder="t('chips_placeholder', ['10.126.126.0/24'])" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_ips') }}</label>
|
||||
<AutoComplete v-model="rule.destination_ips" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]"
|
||||
:placeholder="t('chips_placeholder', ['10.126.126.2/32'])" />
|
||||
</div>
|
||||
|
||||
<div v-if="showPorts" class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.src_ports') }}</label>
|
||||
<AutoComplete v-model="rule.source_ports" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_ports') }}</label>
|
||||
<AutoComplete v-model="rule.ports" multiple fluid :suggestions="genericSuggestions"
|
||||
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox v-model="rule.stateful" :binary="true" inputId="rule-stateful" />
|
||||
<label for="rule-stateful" class="font-bold">{{ t('acl.rule.stateful') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-4 flex-wrap">
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.rate_limit') }}</label>
|
||||
<InputNumber v-model="rule.rate_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<label class="font-bold">{{ t('acl.rule.burst_limit') }}</label>
|
||||
<InputNumber v-model="rule.burst_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.src_groups') }}</label>
|
||||
<MultiSelect v-model="rule.source_groups" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.rule.src_groups')" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-bold">{{ t('acl.rule.dst_groups') }}</label>
|
||||
<MultiSelect v-model="rule.destination_groups" :options="props.groupNames" multiple fluid filter
|
||||
:placeholder="t('acl.rule.dst_groups')" />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="close" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="save" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -104,6 +104,9 @@ use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙
|
||||
disable_ipv6: 禁用IPv6
|
||||
disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
|
||||
|
||||
ipv6_public_addr_auto: 自动获取公网 IPv6
|
||||
ipv6_public_addr_auto_help: 自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址。
|
||||
|
||||
enable_kcp_proxy: 启用 KCP 代理
|
||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
@@ -157,6 +160,9 @@ disable_tcp_hole_punching_help: 禁用TCP打洞功能
|
||||
disable_udp_hole_punching: 禁用UDP打洞
|
||||
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||
|
||||
disable_upnp: 禁用 UPnP
|
||||
disable_upnp_help: 禁用符合条件监听器的运行时 UPnP/NAT-PMP 端口映射;自动端口映射默认开启。
|
||||
|
||||
disable_sym_hole_punching: 禁用对称NAT打洞
|
||||
disable_sym_hole_punching_help: 禁用对称NAT的打洞(生日攻击),将对称NAT视为锥形NAT处理
|
||||
|
||||
@@ -355,6 +361,7 @@ web:
|
||||
delete: 删除
|
||||
edit: 编辑
|
||||
refresh: 刷新
|
||||
add: 添加
|
||||
loading: 加载中...
|
||||
error: 错误
|
||||
success: 成功
|
||||
@@ -422,3 +429,46 @@ config-server:
|
||||
client:
|
||||
not_running: 无法连接至远程客户端
|
||||
retry: 重试
|
||||
|
||||
acl:
|
||||
title: 访问控制
|
||||
help: 访问控制列表,用于限制节点间的通信。
|
||||
enabled: 启用 ACL
|
||||
default_action: 默认动作
|
||||
chains: 规则链
|
||||
inbound: 入站
|
||||
outbound: 出站
|
||||
forward: 转发
|
||||
rules: 规则
|
||||
add_rule: 添加规则
|
||||
edit_rule: 编辑规则
|
||||
rule:
|
||||
name: 规则名称
|
||||
description: 描述
|
||||
enabled: 启用
|
||||
protocol: 协议
|
||||
action: 动作
|
||||
src_ips: 来源 IP
|
||||
dst_ips: 目的 IP
|
||||
src_ports: 来源端口
|
||||
dst_ports: 目的端口
|
||||
rate_limit: 速率限制 (pps)
|
||||
burst_limit: 爆发限制
|
||||
stateful: 状态追踪
|
||||
src_groups: 来源组
|
||||
dst_groups: 目的组
|
||||
groups: 组管理
|
||||
group:
|
||||
declares: 声明组
|
||||
members: 加入组
|
||||
name: 组名
|
||||
secret: 密钥
|
||||
help: 在此处定义网络中的组身份,以便在规则中使用。
|
||||
any: 任意
|
||||
allow: 允许
|
||||
drop: 丢弃
|
||||
delete_chain_confirm: 确定要删除此规则链及其所有规则吗?
|
||||
chain:
|
||||
name: 名称
|
||||
type: 类型
|
||||
match: 匹配
|
||||
|
||||
@@ -103,6 +103,9 @@ use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating s
|
||||
disable_ipv6: Disable IPv6
|
||||
disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
|
||||
|
||||
ipv6_public_addr_auto: Auto Public IPv6
|
||||
ipv6_public_addr_auto_help: Auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet.
|
||||
|
||||
enable_kcp_proxy: Enable KCP Proxy
|
||||
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||
|
||||
@@ -156,6 +159,9 @@ disable_tcp_hole_punching_help: Disable tcp hole punching
|
||||
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||
disable_udp_hole_punching_help: Disable udp hole punching
|
||||
|
||||
disable_upnp: Disable UPnP
|
||||
disable_upnp_help: Disable runtime UPnP/NAT-PMP port mapping for eligible listeners; automatic port mapping is enabled by default.
|
||||
|
||||
disable_sym_hole_punching: Disable Symmetric NAT Hole Punching
|
||||
disable_sym_hole_punching_help: Disable special hole punching handling for symmetric NAT (based on birthday attack), treat symmetric NAT as cone NAT
|
||||
|
||||
@@ -355,6 +361,7 @@ web:
|
||||
delete: Delete
|
||||
edit: Edit
|
||||
refresh: Refresh
|
||||
add: Add
|
||||
loading: Loading...
|
||||
error: Error
|
||||
success: Success
|
||||
@@ -422,3 +429,46 @@ config-server:
|
||||
client:
|
||||
not_running: Unable to connect to remote client.
|
||||
retry: Retry
|
||||
|
||||
acl:
|
||||
title: Access Control (ACL)
|
||||
help: Access control list to restrict communication between nodes.
|
||||
enabled: Enable ACL
|
||||
default_action: Default Action
|
||||
chains: Rule Chains
|
||||
inbound: Inbound
|
||||
outbound: Outbound
|
||||
forward: Forward
|
||||
rules: Rules
|
||||
add_rule: Add Rule
|
||||
edit_rule: Edit Rule
|
||||
rule:
|
||||
name: Rule Name
|
||||
description: Description
|
||||
enabled: Enabled
|
||||
protocol: Protocol
|
||||
action: Action
|
||||
src_ips: Source IPs
|
||||
dst_ips: Destination IPs
|
||||
src_ports: Source Ports
|
||||
dst_ports: Destination Ports
|
||||
rate_limit: Rate Limit (pps)
|
||||
burst_limit: Burst Limit
|
||||
stateful: Stateful
|
||||
src_groups: Source Groups
|
||||
dst_groups: Destination Groups
|
||||
groups: Groups
|
||||
group:
|
||||
declares: Declared Groups
|
||||
members: Node Memberships
|
||||
name: Group Name
|
||||
secret: Group Secret
|
||||
help: Define group identities in the network to use them in rules.
|
||||
any: Any
|
||||
allow: Allow
|
||||
drop: Drop
|
||||
delete_chain_confirm: Are you sure you want to delete this rule chain and all its rules?
|
||||
chain:
|
||||
name: Name
|
||||
type: Type
|
||||
match: Match
|
||||
|
||||
@@ -14,6 +14,74 @@ export interface SecureModeConfig {
|
||||
local_public_key?: string
|
||||
}
|
||||
|
||||
export enum AclProtocol {
|
||||
Unspecified = 0,
|
||||
TCP = 1,
|
||||
UDP = 2,
|
||||
ICMP = 3,
|
||||
ICMPv6 = 4,
|
||||
Any = 5,
|
||||
}
|
||||
|
||||
export enum AclAction {
|
||||
Noop = 0,
|
||||
Allow = 1,
|
||||
Drop = 2,
|
||||
}
|
||||
|
||||
export enum AclChainType {
|
||||
UnspecifiedChain = 0,
|
||||
Inbound = 1,
|
||||
Outbound = 2,
|
||||
Forward = 3,
|
||||
}
|
||||
|
||||
export interface AclRule {
|
||||
name: string
|
||||
description: string
|
||||
priority: number
|
||||
enabled: boolean
|
||||
protocol: AclProtocol
|
||||
ports: string[]
|
||||
source_ips: string[]
|
||||
destination_ips: string[]
|
||||
source_ports: string[]
|
||||
action: AclAction
|
||||
rate_limit: number
|
||||
burst_limit: number
|
||||
stateful: boolean
|
||||
source_groups: string[]
|
||||
destination_groups: string[]
|
||||
}
|
||||
|
||||
export interface AclChain {
|
||||
name: string
|
||||
chain_type: AclChainType
|
||||
description: string
|
||||
enabled: boolean
|
||||
rules: AclRule[]
|
||||
default_action: AclAction
|
||||
}
|
||||
|
||||
export interface GroupIdentity {
|
||||
group_name: string
|
||||
group_secret: string
|
||||
}
|
||||
|
||||
export interface GroupInfo {
|
||||
declares: GroupIdentity[]
|
||||
members: string[]
|
||||
}
|
||||
|
||||
export interface AclV1 {
|
||||
chains: AclChain[]
|
||||
group?: GroupInfo
|
||||
}
|
||||
|
||||
export interface Acl {
|
||||
acl_v1?: AclV1
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
instance_id: string
|
||||
|
||||
@@ -47,6 +115,7 @@ export interface NetworkConfig {
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
ipv6_public_addr_auto?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
@@ -64,6 +133,7 @@ export interface NetworkConfig {
|
||||
disable_encryption?: boolean
|
||||
disable_tcp_hole_punching?: boolean
|
||||
disable_udp_hole_punching?: boolean
|
||||
disable_upnp?: boolean
|
||||
disable_sym_hole_punching?: boolean
|
||||
|
||||
enable_relay_network_whitelist?: boolean
|
||||
@@ -85,6 +155,7 @@ export interface NetworkConfig {
|
||||
enable_private_mode?: boolean
|
||||
|
||||
port_forwards: PortForwardConfig[]
|
||||
acl?: Acl
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -121,6 +192,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
use_smoltcp: false,
|
||||
disable_ipv6: false,
|
||||
ipv6_public_addr_auto: false,
|
||||
enable_kcp_proxy: false,
|
||||
disable_kcp_input: false,
|
||||
enable_quic_proxy: false,
|
||||
@@ -138,6 +210,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
disable_encryption: false,
|
||||
disable_tcp_hole_punching: false,
|
||||
disable_udp_hole_punching: false,
|
||||
disable_upnp: false,
|
||||
disable_sym_hole_punching: false,
|
||||
enable_relay_network_whitelist: false,
|
||||
relay_network_whitelist: [],
|
||||
@@ -152,6 +225,15 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
port_forwards: [],
|
||||
acl: {
|
||||
acl_v1: {
|
||||
group: {
|
||||
declares: [],
|
||||
members: [],
|
||||
},
|
||||
chains: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
use std::sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
@@ -19,11 +19,11 @@ use maxminddb::geoip2;
|
||||
use session::{Location, Session};
|
||||
use storage::{Storage, StorageToken};
|
||||
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use crate::FeatureFlags;
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
|
||||
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs};
|
||||
|
||||
#[derive(rust_embed::Embed)]
|
||||
#[folder = "resources/"]
|
||||
@@ -340,7 +340,7 @@ mod tests {
|
||||
};
|
||||
use sqlx::Executor;
|
||||
|
||||
use crate::{client_manager::ClientManager, db::Db, FeatureFlags};
|
||||
use crate::{FeatureFlags, client_manager::ClientManager, db::Db};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client() {
|
||||
@@ -379,19 +379,26 @@ mod tests {
|
||||
|
||||
let req = tokio::time::timeout(Duration::from_secs(12), async {
|
||||
loop {
|
||||
let session = mgr
|
||||
let sessions = mgr
|
||||
.client_sessions
|
||||
.iter()
|
||||
.next()
|
||||
.map(|item| item.value().clone());
|
||||
let Some(session) = session else {
|
||||
.map(|item| item.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
if sessions.is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
continue;
|
||||
};
|
||||
let mut waiter = session.data().read().await.heartbeat_waiter();
|
||||
if let Ok(req) = waiter.recv().await {
|
||||
}
|
||||
let mut found_req = None;
|
||||
for session in sessions {
|
||||
if let Some(req) = session.data().read().await.req() {
|
||||
found_req = Some(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(req) = found_req {
|
||||
break req;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
use easytier::{launcher::NetworkConfig, rpc_service::remote_client::PersistentConfig};
|
||||
use easytier::{
|
||||
common::config::ConfigSource, launcher::NetworkConfig,
|
||||
rpc_service::remote_client::PersistentConfig,
|
||||
};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -12,10 +15,12 @@ pub struct Model {
|
||||
pub user_id: i32,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub device_id: String,
|
||||
#[sea_orm(column_type = "Text", unique)]
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub network_instance_id: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub network_config: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub source: String,
|
||||
pub disabled: bool,
|
||||
pub create_time: DateTimeWithTimeZone,
|
||||
pub update_time: DateTimeWithTimeZone,
|
||||
@@ -48,4 +53,7 @@ impl PersistentConfig<DbErr> for Model {
|
||||
fn get_network_config(&self) -> Result<NetworkConfig, DbErr> {
|
||||
serde_json::from_str(&self.network_config).map_err(|e| DbErr::Json(e.to_string()))
|
||||
}
|
||||
fn get_network_config_source(&self) -> ConfigSource {
|
||||
self.source.parse().unwrap_or(ConfigSource::User)
|
||||
}
|
||||
}
|
||||
|
||||
+77
-12
@@ -3,16 +3,17 @@
|
||||
pub mod entity;
|
||||
|
||||
use easytier::{
|
||||
common::config::ConfigSource,
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::remote_client::{ListNetworkProps, Storage},
|
||||
};
|
||||
use entity::user_running_network_configs;
|
||||
use sea_orm::{
|
||||
prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
|
||||
QueryFilter as _, Set, SqlxSqliteConnector, TransactionTrait as _,
|
||||
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set,
|
||||
SqlxSqliteConnector, TransactionTrait as _, prelude::Expr, sea_query::OnConflict,
|
||||
};
|
||||
use sea_orm_migration::MigratorTrait as _;
|
||||
use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
||||
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _, types::chrono};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::migrator;
|
||||
@@ -149,6 +150,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
network_inst_id: Uuid,
|
||||
network_config: NetworkConfig,
|
||||
source: ConfigSource,
|
||||
) -> Result<(), DbErr> {
|
||||
let txn = self.orm_db().begin().await?;
|
||||
|
||||
@@ -161,6 +163,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
])
|
||||
.update_columns([
|
||||
urnc::Column::NetworkConfig,
|
||||
urnc::Column::Source,
|
||||
urnc::Column::Disabled,
|
||||
urnc::Column::UpdateTime,
|
||||
])
|
||||
@@ -172,6 +175,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
network_config: sea_orm::Set(
|
||||
serde_json::to_string(&network_config).map_err(|e| DbErr::Json(e.to_string()))?,
|
||||
),
|
||||
source: sea_orm::Set(source.as_str().to_string()),
|
||||
disabled: sea_orm::Set(false),
|
||||
create_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||
update_time: sea_orm::Set(chrono::Local::now().fixed_offset()),
|
||||
@@ -277,10 +281,14 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||
use easytier::{
|
||||
common::config::ConfigSource,
|
||||
proto::api::manage::NetworkConfig,
|
||||
rpc_service::remote_client::{PersistentConfig, Storage},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter as _, Set};
|
||||
|
||||
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
|
||||
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_management() {
|
||||
@@ -294,9 +302,14 @@ mod tests {
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
let device_id = uuid::Uuid::new_v4();
|
||||
|
||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device_id),
|
||||
inst_id,
|
||||
network_config,
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
@@ -306,6 +319,7 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("{:?}", result);
|
||||
assert_eq!(result.network_config, network_config_json);
|
||||
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||
|
||||
// overwrite the config
|
||||
let network_config = NetworkConfig {
|
||||
@@ -313,9 +327,14 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let network_config_json = serde_json::to_string(&network_config).unwrap();
|
||||
db.insert_or_update_user_network_config((user_id, device_id), inst_id, network_config)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device_id),
|
||||
inst_id,
|
||||
network_config,
|
||||
ConfigSource::Webhook,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result2 = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
@@ -325,6 +344,11 @@ mod tests {
|
||||
.unwrap();
|
||||
println!("device: {}, {:?}", device_id, result2);
|
||||
assert_eq!(result2.network_config, network_config_json);
|
||||
assert_eq!(result2.get_network_config_source(), ConfigSource::Webhook);
|
||||
assert_eq!(
|
||||
result2.get_runtime_network_config_source(),
|
||||
ConfigSource::Webhook
|
||||
);
|
||||
|
||||
assert_eq!(result.create_time, result2.create_time);
|
||||
assert_ne!(result.update_time, result2.update_time);
|
||||
@@ -348,6 +372,45 @@ mod tests {
|
||||
assert!(result3.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_legacy_network_config_defaults_to_user_runtime_source() {
|
||||
let db = Db::memory_db().await;
|
||||
let user_id = 1;
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
let device_id = uuid::Uuid::new_v4();
|
||||
|
||||
user_running_network_configs::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
device_id: Set(device_id.to_string()),
|
||||
network_instance_id: Set(inst_id.to_string()),
|
||||
network_config: Set(serde_json::to_string(&NetworkConfig {
|
||||
network_name: Some("legacy".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap()),
|
||||
source: Set("legacy".to_string()),
|
||||
disabled: Set(false),
|
||||
create_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
update_time: Set(sqlx::types::chrono::Local::now().fixed_offset()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(db.orm_db())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = user_running_network_configs::Entity::find()
|
||||
.filter(user_running_network_configs::Column::UserId.eq(user_id))
|
||||
.one(db.orm_db())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(result.get_network_config_source(), ConfigSource::User);
|
||||
assert_eq!(
|
||||
result.get_runtime_network_config_source(),
|
||||
ConfigSource::User
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
||||
let db = Db::memory_db().await;
|
||||
@@ -363,6 +426,7 @@ mod tests {
|
||||
network_name: Some("cfg-1".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -373,6 +437,7 @@ mod tests {
|
||||
network_name: Some("cfg-2".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigSource::User,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -16,8 +16,8 @@ use easytier::{
|
||||
log,
|
||||
network::{local_ipv4, local_ipv6},
|
||||
},
|
||||
tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, TunnelListener},
|
||||
utils::setup_panic_handler,
|
||||
tunnel::{TunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener},
|
||||
utils::panic::setup_panic_handler,
|
||||
};
|
||||
|
||||
use easytier::tunnel::IpScheme;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260421_000003_add_network_config_source"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT 'user',
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_new (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
source,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
'legacy',
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_old (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20241029_000001_init;
|
||||
mod m20260403_000002_scope_network_config_unique;
|
||||
mod m20260421_000003_add_network_config_source;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
||||
vec![
|
||||
Box::new(m20241029_000001_init::Migration),
|
||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||
Box::new(m20260421_000003_add_network_config_source::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{
|
||||
Router,
|
||||
http::StatusCode,
|
||||
routing::{get, post, put},
|
||||
Router,
|
||||
};
|
||||
use axum_login::login_required;
|
||||
use axum_messages::Message;
|
||||
@@ -14,8 +14,8 @@ use std::sync::Arc;
|
||||
use crate::FeatureFlags;
|
||||
|
||||
use super::{
|
||||
users::{AuthSession, Credentials},
|
||||
AppStateInner,
|
||||
users::{AuthSession, Credentials},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -44,7 +44,7 @@ mod put {
|
||||
use axum_login::AuthUser;
|
||||
use easytier::proto::common::Void;
|
||||
|
||||
use crate::restful::{other_error, users::ChangePassword, HttpHandleError};
|
||||
use crate::restful::{HttpHandleError, other_error, users::ChangePassword};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -71,14 +71,14 @@ mod put {
|
||||
}
|
||||
|
||||
mod post {
|
||||
use axum::{extract::Extension, Json};
|
||||
use axum::{Json, extract::Extension};
|
||||
use easytier::proto::common::Void;
|
||||
|
||||
use crate::restful::{
|
||||
captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil},
|
||||
HttpHandleError,
|
||||
captcha::extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionStaticExt},
|
||||
other_error,
|
||||
users::RegisterNewUser,
|
||||
HttpHandleError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -99,7 +99,7 @@ mod post {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json::from(other_error(format!("{:?}", e))),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -150,14 +150,15 @@ mod post {
|
||||
|
||||
mod get {
|
||||
use crate::restful::{
|
||||
HttpHandleError,
|
||||
captcha::{
|
||||
builder::spec::SpecCaptcha,
|
||||
extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil},
|
||||
NewCaptcha as _,
|
||||
builder::spec::SpecCaptcha,
|
||||
extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionExt as _},
|
||||
},
|
||||
other_error, HttpHandleError,
|
||||
other_error,
|
||||
};
|
||||
use axum::{response::Response, Json};
|
||||
use axum::{Json, response::Response};
|
||||
use easytier::proto::common::Void;
|
||||
use tower_sessions::Session;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use super::super::base::randoms::Randoms;
|
||||
|
||||
use super::super::utils::color::Color;
|
||||
use super::super::utils::font;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
|
||||
use rusttype::Font;
|
||||
use std::fmt::Debug;
|
||||
|
||||
@@ -9,14 +9,14 @@ use super::super::{CaptchaFont, NewCaptcha};
|
||||
|
||||
use image::{ImageBuffer, Rgba};
|
||||
use imageproc::drawing;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use rand::{Rng, rngs::ThreadRng};
|
||||
use rusttype::{Font, Scale};
|
||||
use std::io::{Cursor, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod color {
|
||||
use image::Rgba;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use rand::{Rng, rngs::ThreadRng};
|
||||
pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> {
|
||||
let red = rng.gen_range(200..=255);
|
||||
let green = rng.gen_range(200..=255);
|
||||
@@ -133,7 +133,7 @@ impl<'a, 'b> CaptchaBuilder<'a, 'b> {
|
||||
|
||||
fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
|
||||
let line_color = color::gen_line_color(rng);
|
||||
let is_h = rng.gen();
|
||||
let is_h = rng.r#gen();
|
||||
let (start, end) = if is_h {
|
||||
let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
|
||||
let ya = rng.gen_range(0.0..(self.height as f32));
|
||||
|
||||
@@ -8,32 +8,32 @@ mod users;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::extract::Path;
|
||||
use axum::http::{header, Request, StatusCode};
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use axum::middleware::{self as axum_mw, Next};
|
||||
use axum::response::Response;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{extract::State, routing::get, Extension, Json, Router};
|
||||
use axum::{Extension, Json, Router, extract::State, routing::get};
|
||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||
use axum_login::{AuthManagerLayerBuilder, AuthUser, AuthzBackend, login_required};
|
||||
use axum_messages::MessagesManagerLayer;
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::scoped_task::ScopedTask;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::rpc_types;
|
||||
use network::NetworkApi;
|
||||
use sea_orm::DbErr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tower_sessions::Expiry;
|
||||
use tower_sessions::cookie::time::Duration;
|
||||
use tower_sessions::cookie::{Key, SameSite};
|
||||
use tower_sessions::Expiry;
|
||||
use tower_sessions_sqlx_store::SqliteStore;
|
||||
use users::{AuthSession, Backend};
|
||||
|
||||
use crate::client_manager::storage::StorageToken;
|
||||
use crate::FeatureFlags;
|
||||
use crate::client_manager::ClientManager;
|
||||
use crate::client_manager::storage::StorageToken;
|
||||
use crate::db::{Db, UserIdInDb};
|
||||
use crate::webhook::SharedWebhookConfig;
|
||||
use crate::FeatureFlags;
|
||||
|
||||
/// Embed assets for web dashboard, build frontend first
|
||||
#[cfg(feature = "embed")]
|
||||
@@ -199,8 +199,8 @@ impl RestfulServer {
|
||||
mut self,
|
||||
) -> Result<
|
||||
(
|
||||
ScopedTask<()>,
|
||||
ScopedTask<tower_sessions::session_store::Result<()>>,
|
||||
AbortOnDropHandle<()>,
|
||||
AbortOnDropHandle<tower_sessions::session_store::Result<()>>,
|
||||
),
|
||||
anyhow::Error,
|
||||
> {
|
||||
@@ -213,13 +213,11 @@ impl RestfulServer {
|
||||
let session_store = SqliteStore::new(self.db.inner());
|
||||
session_store.migrate().await?;
|
||||
|
||||
let delete_task: ScopedTask<tower_sessions::session_store::Result<()>> =
|
||||
tokio::task::spawn(
|
||||
session_store
|
||||
.clone()
|
||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||
)
|
||||
.into();
|
||||
let delete_task = AbortOnDropHandle::new(tokio::task::spawn(
|
||||
session_store
|
||||
.clone()
|
||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||
));
|
||||
|
||||
// Generate a cryptographic key to sign the session cookie.
|
||||
let key = Key::generate();
|
||||
@@ -298,10 +296,9 @@ impl RestfulServer {
|
||||
app
|
||||
};
|
||||
|
||||
let serve_task: ScopedTask<()> = tokio::spawn(async move {
|
||||
let serve_task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
})
|
||||
.into();
|
||||
}));
|
||||
|
||||
Ok((serve_task, delete_task))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::Path;
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{delete, post};
|
||||
use axum::{extract::State, routing::get, Json, Router};
|
||||
use axum::{Json, Router, extract::State, routing::get};
|
||||
use axum_login::AuthUser;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::common::Void;
|
||||
@@ -16,7 +16,7 @@ use crate::db::UserIdInDb;
|
||||
|
||||
use super::users::AuthSession;
|
||||
use super::{
|
||||
convert_db_error, other_error, AppState, AppStateInner, Error, HttpHandleError, RpcError,
|
||||
AppState, AppStateInner, Error, HttpHandleError, RpcError, convert_db_error, other_error,
|
||||
};
|
||||
|
||||
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::time::Duration;
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use openidconnect::core::{
|
||||
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
|
||||
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
|
||||
@@ -216,7 +216,9 @@ impl OidcConfig {
|
||||
} = opts;
|
||||
|
||||
if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() {
|
||||
return Err(anyhow::anyhow!("--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"
|
||||
));
|
||||
}
|
||||
if oidc_username_claim.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
|
||||
@@ -373,18 +375,17 @@ mod route {
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Some(verifier) = pkce_verifier {
|
||||
if let Err(e) = session
|
||||
if let Some(verifier) = pkce_verifier
|
||||
&& let Err(e) = session
|
||||
.insert("oidc_pkce_verifier", verifier.secret().clone())
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to store pkce_verifier in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
{
|
||||
tracing::error!("Failed to store pkce_verifier in session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(other_error("Session error")),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await {
|
||||
tracing::error!("Failed to store pkce_used in session: {:?}", e);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
routing::post,
|
||||
Json, Router,
|
||||
};
|
||||
use axum_login::AuthUser as _;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
|
||||
use crate::db::UserIdInDb;
|
||||
|
||||
use super::{other_error, AppState, HttpHandleError};
|
||||
use super::{AppState, HttpHandleError, other_error};
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct ProxyRpcRequest {
|
||||
@@ -120,7 +120,7 @@ async fn handle_proxy_rpc_by_session(
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
other_error(format!("Unknown service: {}", service_name)).into(),
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ impl AuthUser for User {
|
||||
|
||||
fn session_auth_hash(&self) -> &[u8] {
|
||||
self.db_user.password.as_bytes() // We use the password hash as the auth
|
||||
// hash--what this means
|
||||
// is when the user changes their password the
|
||||
// auth session becomes invalid.
|
||||
// hash--what this means
|
||||
// is when the user changes their password the
|
||||
// auth session becomes invalid.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
http::header,
|
||||
response::{IntoResponse, Response},
|
||||
routing, Router,
|
||||
routing,
|
||||
};
|
||||
use axum_embed::ServeEmbed;
|
||||
use easytier::common::scoped_task::ScopedTask;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
/// Embed assets for web dashboard, build frontend first
|
||||
#[derive(RustEmbed, Clone)]
|
||||
@@ -58,7 +59,7 @@ pub fn build_router(api_host: Option<url::Url>) -> Router {
|
||||
pub struct WebServer {
|
||||
bind_addr: SocketAddr,
|
||||
router: Router,
|
||||
serve_task: Option<ScopedTask<()>>,
|
||||
serve_task: Option<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
impl WebServer {
|
||||
@@ -70,14 +71,13 @@ impl WebServer {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start(self) -> Result<ScopedTask<()>, anyhow::Error> {
|
||||
pub async fn start(self) -> Result<AbortOnDropHandle<()>, anyhow::Error> {
|
||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||
let app = self.router;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
})
|
||||
.into();
|
||||
}));
|
||||
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ impl WebhookConfig {
|
||||
pub struct ValidateTokenRequest {
|
||||
pub token: String,
|
||||
pub machine_id: String,
|
||||
pub public_ip: Option<String>,
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub os_type: Option<String>,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
disallowed-methods = [
|
||||
{ path = "itertools::Itertools::map_into", reason = "Blocks underlying iterator optimizations. Use the native `.map(Into::into)` instead." },
|
||||
{ path = "itertools::Itertools::map_ok", reason = "Blocks underlying iterator optimizations. Use the native `.map(|r| r.map(f))` instead." },
|
||||
{ path = "itertools::Itertools::filter_ok", reason = "Blocks underlying iterator optimizations. Use a native approach, e.g., `.filter(|r| r.as_ref().map_or(true, condition))`." },
|
||||
{ path = "itertools::Itertools::filter_map_ok", reason = "Blocks underlying iterator optimizations. Use native `.map()` and `.flatten()`, or extract logic into a standard `.filter_map()`." },
|
||||
|
||||
{ path = "itertools::Itertools::collect_vec", reason = "Non-standard idiom. Directly use the standard library's `.collect::<Vec<_>>()`." },
|
||||
{ path = "itertools::Itertools::try_collect", reason = "Non-standard idiom. Standard `collect()` already supports Result/Option inversion; use `.collect::<Result<_, _>>()`." },
|
||||
{ path = "itertools::Itertools::set_from", reason = "Non-standard idiom. Directly use the `.extend()` method provided by the standard library's `Extend` trait." },
|
||||
{ path = "itertools::Itertools::concat", reason = "Non-standard idiom. Use native `.flatten().collect()` or a slice's `.concat()` instead." }
|
||||
]
|
||||
+24
-27
@@ -3,12 +3,12 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.0"
|
||||
edition = "2021"
|
||||
version = "2.6.3"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
rust-version = "1.93.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -50,7 +50,9 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
cfg-if = "1.0"
|
||||
guarden = "0.1"
|
||||
|
||||
delegate = "0.13.5"
|
||||
|
||||
itertools = "0.14.0"
|
||||
|
||||
@@ -62,7 +64,7 @@ futures = { version = "0.3", features = ["bilock", "unstable"] }
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-util = { version = "0.7.9", features = ["codec", "net", "io"] }
|
||||
tokio-util = { version = "0.7.9", features = ["codec", "net", "io", "rt"] }
|
||||
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
@@ -165,7 +167,6 @@ network-interface = "2.0"
|
||||
|
||||
# for ospf route
|
||||
petgraph = "0.8.1"
|
||||
hashbrown = "0.15.3"
|
||||
ordered_hash_map = "0.5.0"
|
||||
|
||||
# for wireguard
|
||||
@@ -220,6 +221,7 @@ async-ringbuf = "0.3.1"
|
||||
service-manager = { git = "https://github.com/EasyTier/service-manager-rs.git", branch = "main" }
|
||||
|
||||
zstd = { version = "0.13", optional = true }
|
||||
lzokay-native = { version = "0.1", optional = true }
|
||||
|
||||
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "94964794caaed5d388463137da59b97499619e5f", optional = true }
|
||||
|
||||
@@ -242,6 +244,7 @@ hickory-server = { version = "0.25.2", features = [
|
||||
"resolver",
|
||||
], optional = true }
|
||||
|
||||
bon = "3.9.1"
|
||||
derive_builder = "0.20.2"
|
||||
humantime-serde = "1.1.1"
|
||||
multimap = "0.10.1"
|
||||
@@ -252,6 +255,8 @@ shellexpand = "3.1.1"
|
||||
|
||||
# for fake tcp
|
||||
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]
|
||||
machine-uid = "0.5.3"
|
||||
@@ -272,11 +277,15 @@ windivert = { git = "https://github.com/EasyTier/windivert-rust.git", rev = "adc
|
||||
] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.52.0", features = [
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_NetworkManagement_WindowsFirewall",
|
||||
"Win32_System_Com",
|
||||
"Win32_Networking",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Diagnostics",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_Variant",
|
||||
"Win32_Networking_WinSock",
|
||||
@@ -285,14 +294,6 @@ windows = { version = "0.52.0", features = [
|
||||
encoding = "0.2"
|
||||
winreg = "0.52"
|
||||
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"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
@@ -324,22 +325,14 @@ easytier-rpc-build = { path = "../easytier-rpc-build", features = [
|
||||
"internal-namespace",
|
||||
] }
|
||||
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]
|
||||
reqwest = { version = "0.12.12", features = ["blocking"] }
|
||||
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]
|
||||
serial_test = "3.0.0"
|
||||
@@ -347,6 +340,7 @@ rstest = "0.25.0"
|
||||
futures-util = "0.3.31"
|
||||
maplit = "1.0.2"
|
||||
tempfile = "3.22.0"
|
||||
ctor = "0.8.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
@@ -365,6 +359,7 @@ default = [
|
||||
"faketcp",
|
||||
"magic-dns",
|
||||
"zstd",
|
||||
"lzo",
|
||||
]
|
||||
full = [
|
||||
"websocket",
|
||||
@@ -379,6 +374,7 @@ full = [
|
||||
"faketcp",
|
||||
"magic-dns",
|
||||
"zstd",
|
||||
"lzo",
|
||||
]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:quinn-plaintext", "dep:rustls", "dep:rcgen"]
|
||||
@@ -409,5 +405,6 @@ tracing = ["tokio/tracing", "dep:console-subscriber"]
|
||||
magic-dns = ["dep:hickory-client", "dep:hickory-server"]
|
||||
faketcp = ["dep:flume"]
|
||||
zstd = ["dep:zstd"]
|
||||
lzo = ["dep:lzokay-native"]
|
||||
# For Network Extension on macOS
|
||||
macos-ne = []
|
||||
|
||||
+11
-6
@@ -86,7 +86,9 @@ impl WindowsBuild {
|
||||
} else {
|
||||
Self::download_protoc()
|
||||
};
|
||||
std::env::set_var("PROTOC", protoc_path);
|
||||
unsafe {
|
||||
std::env::set_var("PROTOC", protoc_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,12 +143,10 @@ 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
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
.unwrap_or_default()
|
||||
.contains("aarch64")
|
||||
{
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
|
||||
@@ -191,6 +191,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.type_attribute("peer_rpc.RouteForeignNetworkSummary", "#[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)]")
|
||||
.service_generator(Box::new(easytier_rpc_build::ServiceGenerator::default()))
|
||||
.btree_map(["."])
|
||||
|
||||
@@ -39,6 +39,15 @@ core_clap:
|
||||
ipv6:
|
||||
en: "ipv6 address of this vpn node, can be used together with ipv4 for dual-stack operation"
|
||||
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:
|
||||
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将自动更改。"
|
||||
@@ -172,6 +181,9 @@ core_clap:
|
||||
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."
|
||||
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 端口映射;自动端口映射默认开启"
|
||||
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."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
@@ -182,8 +194,8 @@ core_clap:
|
||||
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
|
||||
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
|
||||
compression:
|
||||
en: "compression algorithm to use, support none, zstd. default is none"
|
||||
zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none"
|
||||
en: "compression algorithm to use, supported: %{algorithms}. default is none"
|
||||
zh-CN: "要使用的压缩算法,支持:%{algorithms}。默认为 none"
|
||||
mapped_listeners:
|
||||
en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple."
|
||||
zh-CN: "手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。例如:tcp://123.123.123.123:11223,可以指定多个。"
|
||||
|
||||
@@ -3,24 +3,25 @@ use std::{io, mem::ManuallyDrop, net::SocketAddr, os::windows::io::AsRawSocket};
|
||||
use anyhow::Context;
|
||||
use network_interface::NetworkInterfaceConfig;
|
||||
use windows::{
|
||||
core::BSTR,
|
||||
Win32::{
|
||||
Foundation::{BOOL, FALSE},
|
||||
Foundation::FALSE,
|
||||
NetworkManagement::WindowsFirewall::{
|
||||
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_RULE_DIR_OUT,
|
||||
},
|
||||
Networking::WinSock::{
|
||||
htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6,
|
||||
IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR,
|
||||
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET,
|
||||
SO_EXCLUSIVEADDRUSE, SOCKET, SOCKET_ERROR, SOL_SOCKET, WSAGetLastError, WSAIoctl,
|
||||
htonl, setsockopt,
|
||||
},
|
||||
System::Com::{
|
||||
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
|
||||
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
|
||||
},
|
||||
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
|
||||
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
|
||||
},
|
||||
core::{BOOL, BSTR},
|
||||
};
|
||||
|
||||
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
|
||||
@@ -88,13 +89,7 @@ pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
|
||||
))
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
pub fn set_ip_unicast_if(socket: SOCKET, addr: &SocketAddr, iface: &str) -> io::Result<()> {
|
||||
let if_index = find_interface_index(iface)?;
|
||||
|
||||
unsafe {
|
||||
@@ -103,12 +98,12 @@ pub fn set_ip_unicast_if<S: AsRawSocket>(
|
||||
SocketAddr::V4(..) => {
|
||||
let if_index = htonl(if_index);
|
||||
let if_index_bytes = if_index.to_ne_bytes();
|
||||
setsockopt(handle, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes))
|
||||
setsockopt(socket, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes))
|
||||
}
|
||||
SocketAddr::V6(..) => {
|
||||
let if_index_bytes = if_index.to_ne_bytes();
|
||||
setsockopt(
|
||||
handle,
|
||||
socket,
|
||||
IPPROTO_IPV6.0,
|
||||
IPV6_UNICAST_IF,
|
||||
Some(&if_index_bytes),
|
||||
@@ -141,8 +136,17 @@ pub fn setup_socket_for_win<S: AsRawSocket>(
|
||||
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 {
|
||||
set_ip_unicast_if(socket, bind_addr, iface.as_str())?;
|
||||
set_ip_unicast_if(socket, bind_addr, &iface)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -152,7 +156,7 @@ struct ComInitializer;
|
||||
|
||||
impl ComInitializer {
|
||||
fn new() -> windows::core::Result<Self> {
|
||||
unsafe { CoInitializeEx(None, COINIT_MULTITHREADED)? };
|
||||
unsafe { CoInitializeEx(None, COINIT_MULTITHREADED).ok()? };
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
@@ -345,7 +349,7 @@ fn add_protocol_firewall_rules(
|
||||
|
||||
SafeArrayPutElement(
|
||||
interface_array,
|
||||
&index as *const _ as *const i32,
|
||||
&index as *const _,
|
||||
&variant_interface as *const _ as *const std::ffi::c_void,
|
||||
)?;
|
||||
|
||||
@@ -354,7 +358,7 @@ fn add_protocol_firewall_rules(
|
||||
(*interface_variant.Anonymous.Anonymous).vt = VARENUM(VT_ARRAY.0 | VT_VARIANT.0);
|
||||
(*interface_variant.Anonymous.Anonymous).Anonymous.parray = interface_array;
|
||||
|
||||
rule.SetInterfaces(interface_variant)?;
|
||||
rule.SetInterfaces(&interface_variant)?;
|
||||
|
||||
// Get rule collection and add new rule
|
||||
let rules = policy.Rules()?;
|
||||
|
||||
@@ -345,7 +345,7 @@ impl AclProcessor {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort by priority (higher priority first)
|
||||
rules.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||
rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
|
||||
|
||||
match chain.chain_type() {
|
||||
ChainType::Inbound => inbound_rules.extend(rules),
|
||||
@@ -507,7 +507,7 @@ impl AclProcessor {
|
||||
matched_rule: Some(RuleId::Default),
|
||||
should_log: false,
|
||||
log_context: Some(AclLogContext::UnsupportedChainType),
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -679,28 +679,28 @@ impl AclProcessor {
|
||||
}
|
||||
|
||||
// Source port check
|
||||
if let Some(src_port) = packet_info.src_port {
|
||||
if !rule.src_port_ranges.is_empty() {
|
||||
let matches = rule
|
||||
.src_port_ranges
|
||||
.iter()
|
||||
.any(|(start, end)| src_port >= *start && src_port <= *end);
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
if let Some(src_port) = packet_info.src_port
|
||||
&& !rule.src_port_ranges.is_empty()
|
||||
{
|
||||
let matches = rule
|
||||
.src_port_ranges
|
||||
.iter()
|
||||
.any(|(start, end)| src_port >= *start && src_port <= *end);
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Destination port check
|
||||
if let Some(dst_port) = packet_info.dst_port {
|
||||
if !rule.dst_port_ranges.is_empty() {
|
||||
let matches = rule
|
||||
.dst_port_ranges
|
||||
.iter()
|
||||
.any(|(start, end)| dst_port >= *start && dst_port <= *end);
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
if let Some(dst_port) = packet_info.dst_port
|
||||
&& !rule.dst_port_ranges.is_empty()
|
||||
{
|
||||
let matches = rule
|
||||
.dst_port_ranges
|
||||
.iter()
|
||||
.any(|(start, end)| dst_port >= *start && dst_port <= *end);
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1339,6 +1339,45 @@ mod tests {
|
||||
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 {
|
||||
let mut acl_config = Acl::default();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(feature = "zstd")]
|
||||
#[cfg(any(feature = "zstd", feature = "lzo"))]
|
||||
use anyhow::Context;
|
||||
#[cfg(feature = "zstd")]
|
||||
use dashmap::DashMap;
|
||||
@@ -9,7 +9,7 @@ use zstd::bulk;
|
||||
|
||||
use zerocopy::{AsBytes as _, FromBytes as _};
|
||||
|
||||
use crate::tunnel::packet_def::{CompressorAlgo, CompressorTail, ZCPacket, COMPRESSOR_TAIL_SIZE};
|
||||
use crate::tunnel::packet_def::{COMPRESSOR_TAIL_SIZE, CompressorAlgo, CompressorTail, ZCPacket};
|
||||
|
||||
type Error = anyhow::Error;
|
||||
|
||||
@@ -53,6 +53,13 @@ impl DefaultCompressor {
|
||||
)
|
||||
})
|
||||
}),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressorAlgo::Lzo => lzokay_native::compress(data).with_context(|| {
|
||||
format!(
|
||||
"Failed to compress data with algorithm: {:?}",
|
||||
compress_algo
|
||||
)
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -85,6 +92,13 @@ impl DefaultCompressor {
|
||||
compress_algo
|
||||
))
|
||||
}),
|
||||
#[cfg(feature = "lzo")]
|
||||
CompressorAlgo::Lzo => lzokay_native::decompress_all(data, None).with_context(|| {
|
||||
format!(
|
||||
"Failed to decompress data with algorithm: {:?}",
|
||||
compress_algo
|
||||
)
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -181,14 +195,13 @@ thread_local! {
|
||||
static DCTX_MAP: RefCell<DashMap<CompressorAlgo, bulk::Decompressor<'static>>> = RefCell::new(DashMap::new());
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "zstd"))]
|
||||
#[cfg(all(test, any(feature = "zstd", feature = "lzo")))]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compress() {
|
||||
let text = b"12345670000000000000000000";
|
||||
let mut packet = ZCPacket::new_with_payload(text);
|
||||
async fn test_compress_algo(compress_algo: CompressorAlgo) {
|
||||
let text = vec![b'a'; 4096];
|
||||
let mut packet = ZCPacket::new_with_payload(&text);
|
||||
packet.fill_peer_manager_hdr(0, 0, 0);
|
||||
|
||||
let compressor = DefaultCompressor {};
|
||||
@@ -200,7 +213,7 @@ pub mod tests {
|
||||
);
|
||||
|
||||
compressor
|
||||
.compress(&mut packet, CompressorAlgo::ZstdDefault)
|
||||
.compress(&mut packet, compress_algo)
|
||||
.await
|
||||
.unwrap();
|
||||
println!(
|
||||
@@ -215,8 +228,7 @@ pub mod tests {
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_short_text_compress() {
|
||||
async fn test_short_text_compress_algo(compress_algo: CompressorAlgo) {
|
||||
let text = b"1234";
|
||||
let mut packet = ZCPacket::new_with_payload(text);
|
||||
packet.fill_peer_manager_hdr(0, 0, 0);
|
||||
@@ -225,7 +237,7 @@ pub mod tests {
|
||||
|
||||
// short text can't be compressed
|
||||
compressor
|
||||
.compress(&mut packet, CompressorAlgo::ZstdDefault)
|
||||
.compress(&mut packet, compress_algo)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
@@ -234,4 +246,28 @@ pub mod tests {
|
||||
assert_eq!(packet.payload(), text);
|
||||
assert!(!packet.peer_manager_header().unwrap().is_compressed());
|
||||
}
|
||||
|
||||
#[cfg(feature = "zstd")]
|
||||
#[tokio::test]
|
||||
async fn test_zstd_compress() {
|
||||
test_compress_algo(CompressorAlgo::ZstdDefault).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "zstd")]
|
||||
#[tokio::test]
|
||||
async fn test_zstd_short_text_compress() {
|
||||
test_short_text_compress_algo(CompressorAlgo::ZstdDefault).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzo")]
|
||||
#[tokio::test]
|
||||
async fn test_lzo_compress() {
|
||||
test_compress_algo(CompressorAlgo::Lzo).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "lzo")]
|
||||
#[tokio::test]
|
||||
async fn test_lzo_short_text_compress() {
|
||||
test_short_text_compress_algo(CompressorAlgo::Lzo).await;
|
||||
}
|
||||
}
|
||||
|
||||
+392
-69
@@ -6,10 +6,9 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::{prelude::BASE64_STANDARD, Engine as _};
|
||||
use cfg_if::cfg_if;
|
||||
use clap::builder::PossibleValue;
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use clap::ValueEnum;
|
||||
use clap::builder::PossibleValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString, VariantArray};
|
||||
use tokio::io::AsyncReadExt as _;
|
||||
@@ -19,9 +18,10 @@ use crate::{
|
||||
instance::dns_server::DEFAULT_ET_DNS_ZONE,
|
||||
proto::{
|
||||
acl::Acl,
|
||||
api::manage::ConfigSource as RpcConfigSource,
|
||||
common::{CompressionAlgoPb, PortForwardConfigPb, SecureModeConfig, SocketType},
|
||||
},
|
||||
tunnel::generate_digest_from_str,
|
||||
tunnel::{IpScheme, TunnelScheme, generate_digest_from_str},
|
||||
};
|
||||
|
||||
use super::env_parser;
|
||||
@@ -70,9 +70,41 @@ pub fn gen_default_flags() -> Flags {
|
||||
quic_listen_port: u32::MAX,
|
||||
need_p2p: false,
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
disable_upnp: false,
|
||||
disable_relay_data: 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)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum EncryptionAlgorithm {
|
||||
@@ -109,10 +141,9 @@ impl ValueEnum for EncryptionAlgorithm {
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for EncryptionAlgorithm {
|
||||
fn default() -> Self {
|
||||
cfg_if! {
|
||||
if #[cfg(any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto"))] {
|
||||
EncryptionAlgorithm::AesGcm
|
||||
} else {
|
||||
cfg_select! {
|
||||
any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto") => EncryptionAlgorithm::AesGcm,
|
||||
_ => {
|
||||
crate::common::log::warn!("no AEAD encryption algorithm is available, using INSECURE XOR");
|
||||
EncryptionAlgorithm::Xor
|
||||
}
|
||||
@@ -140,6 +171,15 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_ipv6(&self) -> 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 set_dhcp(&self, dhcp: bool);
|
||||
|
||||
@@ -207,6 +247,11 @@ pub trait ConfigLoader: Send + Sync {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -226,6 +271,55 @@ pub struct NetworkIdentity {
|
||||
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)]
|
||||
struct NetworkIdentityWithOnlyDigest {
|
||||
network_name: String,
|
||||
@@ -435,6 +529,9 @@ struct Config {
|
||||
instance_id: Option<uuid::Uuid>,
|
||||
ipv4: 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>,
|
||||
network_identity: Option<NetworkIdentity>,
|
||||
listeners: Option<Vec<url::Url>>,
|
||||
@@ -467,6 +564,7 @@ struct Config {
|
||||
stun_servers_v6: Option<Vec<String>>,
|
||||
|
||||
credential_file: Option<PathBuf>,
|
||||
source: Option<ConfigSourceConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -481,10 +579,21 @@ impl Default for 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> {
|
||||
let mut config = toml::de::from_str::<Config>(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()));
|
||||
|
||||
let config = TomlConfigLoader {
|
||||
@@ -604,6 +713,43 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
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 {
|
||||
self.config.lock().unwrap().dhcp.unwrap_or_default()
|
||||
}
|
||||
@@ -621,14 +767,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
if locked_config.proxy_network.is_none() {
|
||||
locked_config.proxy_network = Some(vec![]);
|
||||
}
|
||||
if let Some(mapped_cidr) = mapped_cidr.as_ref() {
|
||||
if cidr.network_length() != mapped_cidr.network_length() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Mapped CIDR must have the same network length as the original CIDR: {} != {}",
|
||||
cidr.network_length(),
|
||||
mapped_cidr.network_length()
|
||||
));
|
||||
}
|
||||
if let Some(mapped_cidr) = mapped_cidr.as_ref()
|
||||
&& cidr.network_length() != mapped_cidr.network_length()
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Mapped CIDR must have the same network length as the original CIDR: {} != {}",
|
||||
cidr.network_length(),
|
||||
mapped_cidr.network_length()
|
||||
));
|
||||
}
|
||||
// insert if no duplicate
|
||||
if !locked_config
|
||||
@@ -868,6 +1014,23 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
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 {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
@@ -881,14 +1044,15 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
|
||||
let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default();
|
||||
for (key, value) in default_flags_hashmap {
|
||||
if let Some(v) = cur_flags_hashmap.get(&key) {
|
||||
if *v != value {
|
||||
flag_map.insert(key, v.clone());
|
||||
}
|
||||
if let Some(v) = cur_flags_hashmap.get(&key)
|
||||
&& *v != value
|
||||
{
|
||||
flag_map.insert(key, v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = self.config.lock().unwrap().clone();
|
||||
Self::normalize_config_source(&mut config);
|
||||
config.flags = Some(flag_map);
|
||||
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
||||
config.stun_servers = None;
|
||||
@@ -1089,6 +1253,7 @@ pub async fn load_config_from_file(
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{remove_env_var, set_env_var};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::NamedTempFile;
|
||||
@@ -1126,6 +1291,162 @@ stun_servers = [
|
||||
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]
|
||||
async fn full_example_test() {
|
||||
let config_str = r#"
|
||||
@@ -1212,8 +1533,8 @@ proto = "tcp"
|
||||
#[tokio::test]
|
||||
async fn test_env_var_expansion_and_readonly_flag() {
|
||||
// 设置测试环境变量
|
||||
std::env::set_var("TEST_SECRET", "my-test-secret-123");
|
||||
std::env::set_var("TEST_NETWORK", "test-network");
|
||||
set_env_var("TEST_SECRET", "my-test-secret-123");
|
||||
set_env_var("TEST_NETWORK", "test-network");
|
||||
|
||||
// 创建临时配置文件,包含环境变量占位符
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
@@ -1253,8 +1574,8 @@ network_secret = "${TEST_SECRET}"
|
||||
);
|
||||
|
||||
// 清理环境变量
|
||||
std::env::remove_var("TEST_SECRET");
|
||||
std::env::remove_var("TEST_NETWORK");
|
||||
remove_env_var("TEST_SECRET");
|
||||
remove_env_var("TEST_NETWORK");
|
||||
}
|
||||
|
||||
/// RPC API 安全测试(只读配置保护)
|
||||
@@ -1267,7 +1588,7 @@ network_secret = "${TEST_SECRET}"
|
||||
/// `easytier/src/rpc_service/instance_manage.rs` 中实现
|
||||
#[tokio::test]
|
||||
async fn test_readonly_config_api_protection() {
|
||||
std::env::set_var("API_TEST_SECRET", "secret-value");
|
||||
set_env_var("API_TEST_SECRET", "secret-value");
|
||||
|
||||
// 创建包含环境变量的配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
@@ -1298,7 +1619,7 @@ network_secret = "${API_TEST_SECRET}"
|
||||
"Permission flag should be set correctly"
|
||||
);
|
||||
|
||||
std::env::remove_var("API_TEST_SECRET");
|
||||
remove_env_var("API_TEST_SECRET");
|
||||
}
|
||||
|
||||
/// CLI 参数测试(--disable-env-parsing 开关)
|
||||
@@ -1308,7 +1629,7 @@ network_secret = "${API_TEST_SECRET}"
|
||||
/// - 配置不会被标记为只读
|
||||
#[tokio::test]
|
||||
async fn test_disable_env_parsing_flag() {
|
||||
std::env::set_var("DISABLED_TEST_VAR", "should-not-expand");
|
||||
set_env_var("DISABLED_TEST_VAR", "should-not-expand");
|
||||
|
||||
// 创建包含环境变量占位符的配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
@@ -1346,7 +1667,7 @@ network_secret = "${DISABLED_TEST_VAR}"
|
||||
"Config should be NO_DELETE due to no config_dir, not env vars"
|
||||
);
|
||||
|
||||
std::env::remove_var("DISABLED_TEST_VAR");
|
||||
remove_env_var("DISABLED_TEST_VAR");
|
||||
}
|
||||
|
||||
/// 多实例隔离测试
|
||||
@@ -1357,8 +1678,8 @@ network_secret = "${DISABLED_TEST_VAR}"
|
||||
#[tokio::test]
|
||||
async fn test_multiple_instances_with_different_env_vars() {
|
||||
// 实例1:使用第一组环境变量
|
||||
std::env::set_var("INSTANCE_SECRET", "instance1-secret");
|
||||
std::env::set_var("INSTANCE_NAME", "instance-one");
|
||||
set_env_var("INSTANCE_SECRET", "instance1-secret");
|
||||
set_env_var("INSTANCE_NAME", "instance-one");
|
||||
|
||||
let mut temp_file1 = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1388,8 +1709,8 @@ network_secret = "${INSTANCE_SECRET}"
|
||||
);
|
||||
|
||||
// 实例2:修改环境变量后加载同一模板
|
||||
std::env::set_var("INSTANCE_SECRET", "instance2-secret");
|
||||
std::env::set_var("INSTANCE_NAME", "instance-two");
|
||||
set_env_var("INSTANCE_SECRET", "instance2-secret");
|
||||
set_env_var("INSTANCE_NAME", "instance-two");
|
||||
|
||||
let mut temp_file2 = NamedTempFile::new().unwrap();
|
||||
temp_file2.write_all(config_content.as_bytes()).unwrap();
|
||||
@@ -1419,8 +1740,8 @@ network_secret = "${INSTANCE_SECRET}"
|
||||
);
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("INSTANCE_SECRET");
|
||||
std::env::remove_var("INSTANCE_NAME");
|
||||
remove_env_var("INSTANCE_SECRET");
|
||||
remove_env_var("INSTANCE_NAME");
|
||||
}
|
||||
|
||||
/// 实际配置字段测试(network_secret、peer.uri 等)
|
||||
@@ -1433,11 +1754,11 @@ network_secret = "${INSTANCE_SECRET}"
|
||||
#[tokio::test]
|
||||
async fn test_real_config_fields_expansion() {
|
||||
// 设置各种实际场景的环境变量
|
||||
std::env::set_var("ET_SECRET", "production-secret-key");
|
||||
std::env::set_var("PEER_HOST", "peer.example.com");
|
||||
std::env::set_var("PEER_PORT", "11011");
|
||||
std::env::set_var("LISTEN_PORT", "11010");
|
||||
std::env::set_var("NETWORK_NAME", "prod-network");
|
||||
set_env_var("ET_SECRET", "production-secret-key");
|
||||
set_env_var("PEER_HOST", "peer.example.com");
|
||||
set_env_var("PEER_PORT", "11011");
|
||||
set_env_var("LISTEN_PORT", "11010");
|
||||
set_env_var("NETWORK_NAME", "prod-network");
|
||||
|
||||
// 创建包含多个实际字段的完整配置
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
@@ -1485,11 +1806,11 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理环境变量
|
||||
std::env::remove_var("ET_SECRET");
|
||||
std::env::remove_var("PEER_HOST");
|
||||
std::env::remove_var("PEER_PORT");
|
||||
std::env::remove_var("LISTEN_PORT");
|
||||
std::env::remove_var("NETWORK_NAME");
|
||||
remove_env_var("ET_SECRET");
|
||||
remove_env_var("PEER_HOST");
|
||||
remove_env_var("PEER_PORT");
|
||||
remove_env_var("LISTEN_PORT");
|
||||
remove_env_var("NETWORK_NAME");
|
||||
}
|
||||
|
||||
/// 带默认值的环境变量
|
||||
@@ -1499,8 +1820,8 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
|
||||
#[tokio::test]
|
||||
async fn test_env_var_with_default_value() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("UNDEFINED_PORT");
|
||||
std::env::remove_var("UNDEFINED_SECRET");
|
||||
remove_env_var("UNDEFINED_PORT");
|
||||
remove_env_var("UNDEFINED_SECRET");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1541,7 +1862,7 @@ network_secret = "${UNDEFINED_SECRET:-default-secret}"
|
||||
/// - 未定义的环境变量保持原样(shellexpand 的默认行为)
|
||||
#[tokio::test]
|
||||
async fn test_undefined_env_var_without_default() {
|
||||
std::env::remove_var("COMPLETELY_UNDEFINED");
|
||||
remove_env_var("COMPLETELY_UNDEFINED");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1571,6 +1892,8 @@ network_secret = "${COMPLETELY_UNDEFINED}"
|
||||
|
||||
// 注意:由于没有实际替换发生,控制标记不应因环境变量而设置
|
||||
// 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE
|
||||
// 这里我们主要验证 NO_DELETE 标记的逻辑
|
||||
// 由于没有 config_dir,文件会被标记为 NO_DELETE,但不是因为环境变量
|
||||
assert!(control.is_no_delete());
|
||||
}
|
||||
|
||||
@@ -1582,9 +1905,9 @@ network_secret = "${COMPLETELY_UNDEFINED}"
|
||||
#[tokio::test]
|
||||
async fn test_boolean_type_env_vars() {
|
||||
// 设置布尔类型的环境变量
|
||||
std::env::set_var("ENABLE_DHCP", "true");
|
||||
std::env::set_var("ENABLE_ENCRYPTION", "false");
|
||||
std::env::set_var("ENABLE_IPV6", "true");
|
||||
set_env_var("ENABLE_DHCP", "true");
|
||||
set_env_var("ENABLE_ENCRYPTION", "false");
|
||||
set_env_var("ENABLE_IPV6", "true");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1622,9 +1945,9 @@ enable_ipv6 = ${ENABLE_IPV6}
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("ENABLE_DHCP");
|
||||
std::env::remove_var("ENABLE_ENCRYPTION");
|
||||
std::env::remove_var("ENABLE_IPV6");
|
||||
remove_env_var("ENABLE_DHCP");
|
||||
remove_env_var("ENABLE_ENCRYPTION");
|
||||
remove_env_var("ENABLE_IPV6");
|
||||
}
|
||||
|
||||
/// 数字类型环境变量
|
||||
@@ -1635,8 +1958,8 @@ enable_ipv6 = ${ENABLE_IPV6}
|
||||
#[tokio::test]
|
||||
async fn test_numeric_type_env_vars() {
|
||||
// 设置数字类型的环境变量
|
||||
std::env::set_var("MTU_VALUE", "1400");
|
||||
std::env::set_var("THREAD_COUNT", "4");
|
||||
set_env_var("MTU_VALUE", "1400");
|
||||
set_env_var("THREAD_COUNT", "4");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1671,8 +1994,8 @@ multi_thread_count = ${THREAD_COUNT}
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("MTU_VALUE");
|
||||
std::env::remove_var("THREAD_COUNT");
|
||||
remove_env_var("MTU_VALUE");
|
||||
remove_env_var("THREAD_COUNT");
|
||||
}
|
||||
|
||||
/// 混合类型环境变量
|
||||
@@ -1684,12 +2007,12 @@ multi_thread_count = ${THREAD_COUNT}
|
||||
#[tokio::test]
|
||||
async fn test_mixed_type_env_vars() {
|
||||
// 设置不同类型的环境变量
|
||||
std::env::set_var("MIXED_SECRET", "mixed-secret-key");
|
||||
std::env::set_var("MIXED_NETWORK", "production");
|
||||
std::env::set_var("MIXED_DHCP", "true");
|
||||
std::env::set_var("MIXED_MTU", "1500");
|
||||
std::env::set_var("MIXED_ENCRYPTION", "false");
|
||||
std::env::set_var("MIXED_LISTEN_PORT", "12345");
|
||||
set_env_var("MIXED_SECRET", "mixed-secret-key");
|
||||
set_env_var("MIXED_NETWORK", "production");
|
||||
set_env_var("MIXED_DHCP", "true");
|
||||
set_env_var("MIXED_MTU", "1500");
|
||||
set_env_var("MIXED_ENCRYPTION", "false");
|
||||
set_env_var("MIXED_LISTEN_PORT", "12345");
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let config_content = r#"
|
||||
@@ -1741,11 +2064,11 @@ enable_encryption = ${MIXED_ENCRYPTION}
|
||||
assert!(control.is_no_delete());
|
||||
|
||||
// 清理
|
||||
std::env::remove_var("MIXED_SECRET");
|
||||
std::env::remove_var("MIXED_NETWORK");
|
||||
std::env::remove_var("MIXED_DHCP");
|
||||
std::env::remove_var("MIXED_MTU");
|
||||
std::env::remove_var("MIXED_ENCRYPTION");
|
||||
std::env::remove_var("MIXED_LISTEN_PORT");
|
||||
remove_env_var("MIXED_SECRET");
|
||||
remove_env_var("MIXED_NETWORK");
|
||||
remove_env_var("MIXED_DHCP");
|
||||
remove_env_var("MIXED_MTU");
|
||||
remove_env_var("MIXED_ENCRYPTION");
|
||||
remove_env_var("MIXED_LISTEN_PORT");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#[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)* });
|
||||
};
|
||||
}
|
||||
+21
-13
@@ -1,6 +1,6 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Context;
|
||||
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||
@@ -73,16 +73,6 @@ pub async fn socket_addrs(
|
||||
.port()
|
||||
.or_else(default_port_number)
|
||||
.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
|
||||
match host {
|
||||
@@ -121,9 +111,8 @@ pub async fn socket_addrs(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::defer;
|
||||
|
||||
use super::*;
|
||||
use guarden::defer;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_socket_addrs() {
|
||||
@@ -140,4 +129,23 @@ mod tests {
|
||||
assert_eq!(2, addrs.len(), "addrs: {:?}", 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,10 +42,11 @@ pub fn expand_env_vars(text: &str) -> (String, bool) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tests::{remove_env_var, set_env_var};
|
||||
|
||||
#[test]
|
||||
fn test_expand_standard_syntax() {
|
||||
std::env::set_var("TEST_VAR_STANDARD", "test_value");
|
||||
set_env_var("TEST_VAR_STANDARD", "test_value");
|
||||
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
|
||||
assert_eq!(result, "secret=test_value");
|
||||
assert!(changed);
|
||||
@@ -53,7 +54,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_expand_short_syntax() {
|
||||
std::env::set_var("TEST_VAR_SHORT", "short_value");
|
||||
set_env_var("TEST_VAR_SHORT", "short_value");
|
||||
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
|
||||
assert_eq!(result, "key=short_value");
|
||||
assert!(changed);
|
||||
@@ -62,7 +63,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_expand_with_default() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("UNDEFINED_VAR_WITH_DEFAULT");
|
||||
remove_env_var("UNDEFINED_VAR_WITH_DEFAULT");
|
||||
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
|
||||
assert_eq!(result, "port=8080");
|
||||
assert!(changed);
|
||||
@@ -84,8 +85,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_vars() {
|
||||
std::env::set_var("VAR1", "value1");
|
||||
std::env::set_var("VAR2", "value2");
|
||||
set_env_var("VAR1", "value1");
|
||||
set_env_var("VAR2", "value2");
|
||||
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
|
||||
assert_eq!(result, "value1 and value2");
|
||||
assert!(changed);
|
||||
@@ -94,7 +95,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_undefined_var_without_default() {
|
||||
// 确保变量未定义
|
||||
std::env::remove_var("COMPLETELY_UNDEFINED_VAR");
|
||||
remove_env_var("COMPLETELY_UNDEFINED_VAR");
|
||||
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
|
||||
// shellexpand::env 对未定义的变量会保持原样
|
||||
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
|
||||
@@ -103,8 +104,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_complex_toml_config() {
|
||||
std::env::set_var("ET_SECRET", "my-secret-key");
|
||||
std::env::set_var("ET_PORT", "11010");
|
||||
set_env_var("ET_SECRET", "my-secret-key");
|
||||
set_env_var("ET_PORT", "11010");
|
||||
|
||||
let config = r#"
|
||||
[network_identity]
|
||||
@@ -123,7 +124,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_escape_syntax_double_dollar() {
|
||||
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||
set_env_var("ESCAPED_VAR", "should_not_expand");
|
||||
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
|
||||
// $$ 会被转义为单个 $,不会触发变量扩展
|
||||
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
|
||||
@@ -133,7 +134,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_escape_syntax_backslash() {
|
||||
std::env::set_var("ESCAPED_VAR", "should_not_expand");
|
||||
set_env_var("ESCAPED_VAR", "should_not_expand");
|
||||
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
|
||||
// 这不是推荐的转义方式,此测试仅为记录实际行为
|
||||
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
|
||||
@@ -143,7 +144,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_multiple_dollar_signs() {
|
||||
std::env::set_var("TEST_VAR", "value");
|
||||
set_env_var("TEST_VAR", "value");
|
||||
// 测试多个连续的 $ 符号
|
||||
let (result1, changed1) = expand_env_vars("$$");
|
||||
assert_eq!(result1, "$");
|
||||
@@ -161,7 +162,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_empty_var_value() {
|
||||
std::env::set_var("EMPTY_VAR", "");
|
||||
set_env_var("EMPTY_VAR", "");
|
||||
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
|
||||
// 变量存在但值为空
|
||||
assert_eq!(result, "value=");
|
||||
@@ -170,7 +171,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_default_with_special_chars() {
|
||||
std::env::remove_var("UNDEFINED_SPECIAL");
|
||||
remove_env_var("UNDEFINED_SPECIAL");
|
||||
// 测试默认值包含冒号、等号、空格等特殊字符
|
||||
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
|
||||
assert_eq!(result, "url=http://localhost:8080");
|
||||
@@ -187,9 +188,9 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_var_name_with_numbers_underscores() {
|
||||
std::env::set_var("VAR_123", "num_value");
|
||||
std::env::set_var("_VAR", "underscore_prefix");
|
||||
std::env::set_var("VAR_", "underscore_suffix");
|
||||
set_env_var("VAR_123", "num_value");
|
||||
set_env_var("_VAR", "underscore_prefix");
|
||||
set_env_var("VAR_", "underscore_suffix");
|
||||
|
||||
let (result1, changed1) = expand_env_vars("${VAR_123}");
|
||||
assert_eq!(result1, "num_value");
|
||||
@@ -214,7 +215,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
|
||||
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
|
||||
std::env::remove_var("VAR");
|
||||
remove_env_var("VAR");
|
||||
let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
|
||||
// 如果 VAR 未定义,shellexpand 会返回错误或保持原样
|
||||
assert_eq!(result2, "incomplete ${VAR");
|
||||
@@ -224,8 +225,8 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_mixed_defined_undefined_vars() {
|
||||
std::env::set_var("DEFINED_VAR", "defined");
|
||||
std::env::remove_var("UNDEFINED_VAR");
|
||||
set_env_var("DEFINED_VAR", "defined");
|
||||
remove_env_var("UNDEFINED_VAR");
|
||||
|
||||
// 混合已定义和未定义的变量
|
||||
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
|
||||
@@ -237,7 +238,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
|
||||
|
||||
#[test]
|
||||
fn test_nested_braces() {
|
||||
std::env::set_var("OUTER", "outer_value");
|
||||
set_env_var("OUTER", "outer_value");
|
||||
// 嵌套的大括号是无效语法,shellexpand::env 会返回错误
|
||||
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
|
||||
// 由于语法错误,整个字符串保持不变
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashMap},
|
||||
collections::{BTreeSet, HashMap, hash_map::DefaultHasher},
|
||||
hash::Hasher,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
@@ -10,11 +10,11 @@ use arc_swap::ArcSwap;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use super::{
|
||||
PeerId,
|
||||
config::{ConfigLoader, Flags},
|
||||
netns::NetNS,
|
||||
network::IPCollector,
|
||||
stun::{StunInfoCollector, StunInfoCollectorTrait},
|
||||
PeerId,
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
@@ -28,6 +28,7 @@ use crate::{
|
||||
common::{PeerFeatureFlag, PortForwardConfigPb},
|
||||
peer_rpc::PeerGroupInfo,
|
||||
},
|
||||
rpc_service::protected_port,
|
||||
tunnel::matches_protocol,
|
||||
};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
@@ -52,6 +53,11 @@ pub enum GlobalCtxEvent {
|
||||
ListenerAcceptFailed(url::Url, String), // (url, error message)
|
||||
ConnectionAccepted(String, String), // (local url, remote url)
|
||||
ConnectionError(String, String, String), // (local url, remote url, error message)
|
||||
ListenerPortMappingEstablished {
|
||||
local_listener: url::Url,
|
||||
mapped_listener: url::Url,
|
||||
backend: String,
|
||||
},
|
||||
|
||||
Connecting(url::Url),
|
||||
ConnectError(String, String, String), // (dst, ip version, error message)
|
||||
@@ -62,6 +68,8 @@ pub enum GlobalCtxEvent {
|
||||
|
||||
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
||||
PublicIpv6Changed(Option<cidr::Ipv6Inet>, Option<cidr::Ipv6Inet>), // (old, new)
|
||||
PublicIpv6RoutesUpdated(Vec<cidr::Ipv6Inet>, Vec<cidr::Ipv6Inet>), // (added, removed)
|
||||
|
||||
PortForwardAdded(PortForwardConfigPb),
|
||||
|
||||
@@ -194,6 +202,8 @@ pub struct GlobalCtx {
|
||||
|
||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||
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>>>,
|
||||
|
||||
ip_collector: Mutex<Option<Arc<IPCollector>>>,
|
||||
@@ -203,9 +213,16 @@ pub struct GlobalCtx {
|
||||
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
||||
|
||||
running_listeners: Mutex<Vec<url::Url>>,
|
||||
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
|
||||
|
||||
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>,
|
||||
|
||||
token_bucket_manager: TokenBucketManager,
|
||||
@@ -236,8 +253,17 @@ impl std::fmt::Debug for GlobalCtx {
|
||||
pub type ArcGlobalCtx = std::sync::Arc<GlobalCtx>;
|
||||
|
||||
impl GlobalCtx {
|
||||
fn derive_feature_flags(flags: &Flags, current: Option<PeerFeatureFlag>) -> PeerFeatureFlag {
|
||||
let mut feature_flags = current.unwrap_or_default();
|
||||
fn apply_disable_relay_data_flag(
|
||||
flags: &Flags,
|
||||
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.no_relay_kcp = flags.disable_relay_kcp;
|
||||
feature_flags.support_conn_list_sync = true;
|
||||
@@ -245,7 +271,7 @@ impl GlobalCtx {
|
||||
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
||||
feature_flags.need_p2p = flags.need_p2p;
|
||||
feature_flags.disable_p2p = flags.disable_p2p;
|
||||
feature_flags
|
||||
Self::apply_disable_relay_data_flag(flags, feature_flags)
|
||||
}
|
||||
|
||||
pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
|
||||
@@ -274,7 +300,8 @@ impl GlobalCtx {
|
||||
|
||||
let flags = config_fs.get_flags();
|
||||
|
||||
let feature_flags = Self::derive_feature_flags(&flags, None);
|
||||
let base_feature_flags = PeerFeatureFlag::default();
|
||||
let feature_flags = Self::derive_feature_flags(&flags, base_feature_flags);
|
||||
|
||||
let credential_storage_path = config_fs.get_credential_file();
|
||||
let credential_manager = Arc::new(CredentialManager::new(credential_storage_path));
|
||||
@@ -289,6 +316,8 @@ impl GlobalCtx {
|
||||
event_bus,
|
||||
cached_ipv4: 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),
|
||||
|
||||
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
||||
@@ -301,9 +330,12 @@ impl GlobalCtx {
|
||||
stun_info_collection: Mutex::new(stun_info_collector),
|
||||
|
||||
running_listeners: Mutex::new(Vec::new()),
|
||||
advertised_ipv6_public_addr_prefix: Mutex::new(None),
|
||||
|
||||
flags: ArcSwap::new(Arc::new(flags)),
|
||||
|
||||
base_feature_flags: AtomicCell::new(base_feature_flags),
|
||||
|
||||
feature_flags: AtomicCell::new(feature_flags),
|
||||
|
||||
token_bucket_manager: TokenBucketManager::new(),
|
||||
@@ -375,6 +407,45 @@ impl GlobalCtx {
|
||||
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 {
|
||||
self.config.get_id()
|
||||
}
|
||||
@@ -389,7 +460,7 @@ impl GlobalCtx {
|
||||
pub fn is_ip_local_virtual_ip(&self, ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.address() == *v4).unwrap_or(false),
|
||||
IpAddr::V6(v6) => self.get_ipv6().map(|x| x.address() == *v6).unwrap_or(false),
|
||||
IpAddr::V6(v6) => self.is_ip_local_ipv6(v6),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +531,7 @@ impl GlobalCtx {
|
||||
self.config.set_flags(flags.clone());
|
||||
self.feature_flags.store(Self::derive_feature_flags(
|
||||
&flags,
|
||||
Some(self.feature_flags.load()),
|
||||
self.base_feature_flags.load(),
|
||||
));
|
||||
self.flags.store(Arc::new(flags));
|
||||
}
|
||||
@@ -525,8 +596,53 @@ impl GlobalCtx {
|
||||
self.feature_flags.load()
|
||||
}
|
||||
|
||||
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
|
||||
self.feature_flags.store(flags);
|
||||
/// Replace the runtime/base advertised flags as a complete snapshot.
|
||||
///
|
||||
/// 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 {
|
||||
@@ -639,25 +755,26 @@ impl GlobalCtx {
|
||||
pub fn should_deny_proxy(&self, dst_addr: &SocketAddr, is_udp: bool) -> bool {
|
||||
let _g = self.net_ns.guard();
|
||||
let ip = dst_addr.ip();
|
||||
// first check if ip is virtual ip
|
||||
// first check if ip is an EasyTier-managed local address
|
||||
// then try bind this ip, if succ means it is local ip
|
||||
let dst_is_local_virtual_ip = self.is_ip_local_virtual_ip(&ip);
|
||||
let dst_is_local_et_ip = self.is_ip_local_virtual_ip(&ip);
|
||||
// this is an expensive operation, should be called sparingly
|
||||
// 1. tcp/kcp/quic call this only after proxy conn is established
|
||||
// 2. udp cache the result in nat entry
|
||||
let dst_is_local_phy_ip = std::net::UdpSocket::bind(format!("{}:0", ip)).is_ok();
|
||||
|
||||
tracing::trace!(
|
||||
"check should_deny_proxy: dst_addr={}, dst_is_local_virtual_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
||||
"check should_deny_proxy: dst_addr={}, dst_is_local_et_ip={}, dst_is_local_phy_ip={}, is_udp={}",
|
||||
dst_addr,
|
||||
dst_is_local_virtual_ip,
|
||||
dst_is_local_et_ip,
|
||||
dst_is_local_phy_ip,
|
||||
is_udp
|
||||
);
|
||||
|
||||
if dst_is_local_virtual_ip || dst_is_local_phy_ip {
|
||||
if dst_is_local_et_ip || dst_is_local_phy_ip {
|
||||
// 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)
|
||||
|| (!is_udp && protected_port::is_protected_tcp_port(dst_addr.port()))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -742,7 +859,7 @@ pub mod tests {
|
||||
let mut feature_flags = global_ctx.get_feature_flags();
|
||||
feature_flags.avoid_relay_data = true;
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_feature_flags(feature_flags);
|
||||
global_ctx.set_base_advertised_feature_flags(feature_flags);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
flags.disable_kcp_input = true;
|
||||
@@ -763,6 +880,135 @@ pub mod tests {
|
||||
assert!(feature_flags.support_conn_list_sync);
|
||||
assert!(feature_flags.avoid_relay_data);
|
||||
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(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
|
||||
use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd};
|
||||
use async_trait::async_trait;
|
||||
use cidr::{Ipv4Inet, Ipv6Inet};
|
||||
|
||||
@@ -53,8 +53,8 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
) -> Result<(), Error> {
|
||||
run_shell_cmd(
|
||||
format!(
|
||||
"ifconfig {} {:?}/{:?} 10.8.8.8 up",
|
||||
name, address, cidr_prefix,
|
||||
"ifconfig {} {:?}/{:?} {:?} up",
|
||||
name, address, cidr_prefix, address,
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
|
||||
@@ -119,8 +119,8 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> {
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.await?;
|
||||
stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
|
||||
stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
|
||||
stdout = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
|
||||
stderr = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -166,3 +166,14 @@ pub type IfConfiger = DummyIfConfiger;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
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 cidr::{IpInet, Ipv4Inet, Ipv6Inet};
|
||||
use netlink_packet_core::{
|
||||
NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
|
||||
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST,
|
||||
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST, NetlinkDeserializable,
|
||||
NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
|
||||
};
|
||||
use netlink_packet_route::{
|
||||
AddressFamily, RouteNetlinkMessage,
|
||||
address::{AddressAttribute, AddressMessage},
|
||||
route::{
|
||||
RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope,
|
||||
RouteType,
|
||||
},
|
||||
AddressFamily, RouteNetlinkMessage,
|
||||
};
|
||||
use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr};
|
||||
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE};
|
||||
use nix::{
|
||||
ifaddrs::getifaddrs,
|
||||
libc::{self, ifreq, ioctl, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU},
|
||||
libc::{self, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU, ifreq, ioctl},
|
||||
net::if_::InterfaceFlags,
|
||||
sys::socket::SockaddrLike as _,
|
||||
};
|
||||
use pnet::ipnetwork::ip_mask_to_prefix;
|
||||
|
||||
use super::{route::Route, Error, IfConfiguerTrait};
|
||||
use super::{Error, IfConfiguerTrait, route::Route};
|
||||
|
||||
pub(crate) fn dummy_socket() -> Result<std::net::UdpSocket, Error> {
|
||||
Ok(std::net::UdpSocket::bind("0:0")?)
|
||||
@@ -160,7 +160,7 @@ impl From<RouteMessage> for Route {
|
||||
pub struct NetlinkIfConfiger {}
|
||||
|
||||
impl NetlinkIfConfiger {
|
||||
fn get_interface_index(name: &str) -> Result<u32, Error> {
|
||||
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
|
||||
let name = CString::new(name).with_context(|| "failed to convert interface name")?;
|
||||
match unsafe { libc::if_nametoindex(name.as_ptr()) } {
|
||||
0 => Err(std::io::Error::last_os_error().into()),
|
||||
@@ -311,7 +311,7 @@ impl NetlinkIfConfiger {
|
||||
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
|
||||
}
|
||||
|
||||
fn list_routes() -> Result<Vec<RouteMessage>, Error> {
|
||||
fn list_route_messages(address_family: AddressFamily) -> Result<Vec<RouteMessage>, Error> {
|
||||
let mut message = RouteMessage::default();
|
||||
|
||||
message.header.table = RouteHeader::RT_TABLE_UNSPEC;
|
||||
@@ -320,7 +320,7 @@ impl NetlinkIfConfiger {
|
||||
message.header.scope = RouteScope::Universe;
|
||||
message.header.kind = RouteType::Unicast;
|
||||
|
||||
message.header.address_family = AddressFamily::Inet;
|
||||
message.header.address_family = address_family;
|
||||
message.header.destination_prefix_length = 0;
|
||||
message.header.source_prefix_length = 0;
|
||||
|
||||
@@ -367,6 +367,14 @@ impl NetlinkIfConfiger {
|
||||
|
||||
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]
|
||||
@@ -551,12 +559,9 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
message.header.scope = RouteScope::Universe;
|
||||
message.header.kind = RouteType::Unicast;
|
||||
|
||||
// Add metric (cost) if specified
|
||||
if let Some(cost) = cost {
|
||||
message
|
||||
.attributes
|
||||
.push(RouteAttribute::Priority(cost as u32));
|
||||
}
|
||||
message
|
||||
.attributes
|
||||
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
|
||||
|
||||
message
|
||||
.attributes
|
||||
@@ -564,9 +569,11 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
name,
|
||||
)?));
|
||||
|
||||
message
|
||||
.attributes
|
||||
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
|
||||
if cidr_prefix != 0 {
|
||||
message
|
||||
.attributes
|
||||
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
|
||||
}
|
||||
|
||||
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
|
||||
}
|
||||
@@ -577,7 +584,7 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
address: std::net::Ipv6Addr,
|
||||
cidr_prefix: u8,
|
||||
) -> Result<(), Error> {
|
||||
let routes = Self::list_routes()?;
|
||||
let routes = Self::list_route_messages(AddressFamily::Inet6)?;
|
||||
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
|
||||
|
||||
for msg in routes {
|
||||
@@ -598,29 +605,82 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::Command;
|
||||
|
||||
const DUMMY_IFACE_NAME: &str = "dummy";
|
||||
|
||||
fn run_cmd(cmd: &str) -> String {
|
||||
let output = std::process::Command::new("sh")
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.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()
|
||||
}
|
||||
|
||||
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 {}
|
||||
impl PrepareEnv {
|
||||
fn new() -> Self {
|
||||
let _ = run_cmd(&format!("sudo ip link add {} type dummy", DUMMY_IFACE_NAME));
|
||||
let _ = Command::new("ip")
|
||||
.args(["link", "del", DUMMY_IFACE_NAME])
|
||||
.output();
|
||||
let _ = run_cmd(&format!("ip link add {} type dummy", DUMMY_IFACE_NAME));
|
||||
PrepareEnv {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PrepareEnv {
|
||||
fn drop(&mut self) {
|
||||
let _ = run_cmd(&format!("sudo ip link del {}", DUMMY_IFACE_NAME));
|
||||
let _ = Command::new("ip")
|
||||
.args(["link", "del", DUMMY_IFACE_NAME])
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,4 +761,128 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
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,10 +740,6 @@ impl InterfaceLuid {
|
||||
|
||||
// SAFETY: TODO
|
||||
let ret = unsafe { SetIpInterfaceEntry(&mut row) };
|
||||
if NO_ERROR == ret {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ret)
|
||||
}
|
||||
if NO_ERROR == ret { Ok(()) } else { Err(ret) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,18 +6,19 @@ use cidr::{Ipv4Inet, Ipv6Inet};
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
ptr::null_mut,
|
||||
};
|
||||
use windows_sys::Win32::{
|
||||
use windows::Win32::NetworkManagement::IpHelper::INTERNAL_IF_OPER_STATUS;
|
||||
use windows::Win32::{
|
||||
Foundation::NO_ERROR,
|
||||
NetworkManagement::IpHelper::{GetIfEntry, SetIfEntry, MIB_IFROW},
|
||||
NetworkManagement::IpHelper::{GetIfEntry, MIB_IFROW, SetIfEntry},
|
||||
System::Diagnostics::Debug::{
|
||||
FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS, FormatMessageW,
|
||||
},
|
||||
};
|
||||
use windows::core::PWSTR;
|
||||
use winreg::{
|
||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
|
||||
RegKey,
|
||||
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
|
||||
};
|
||||
|
||||
use super::{Error, IfConfiguerTrait};
|
||||
@@ -32,12 +33,12 @@ fn format_win_error(error: u32) -> String {
|
||||
unsafe {
|
||||
FormatMessageW(
|
||||
flags,
|
||||
null_mut(),
|
||||
None,
|
||||
error,
|
||||
0,
|
||||
buffer.as_mut_ptr(),
|
||||
PWSTR(buffer.as_mut_ptr()),
|
||||
size,
|
||||
null_mut(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
let str_end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
|
||||
@@ -100,7 +101,7 @@ impl WindowsIfConfiger {
|
||||
dwPhysAddrLen: 0,
|
||||
bPhysAddr: [0; 8],
|
||||
dwAdminStatus: if up { 1 } else { 2 }, // 1 = up, 2 = down
|
||||
dwOperStatus: 0,
|
||||
dwOperStatus: INTERNAL_IF_OPER_STATUS(0),
|
||||
dwLastChange: 0,
|
||||
dwInOctets: 0,
|
||||
dwInUcastPkts: 0,
|
||||
@@ -118,8 +119,8 @@ impl WindowsIfConfiger {
|
||||
bDescr: [0; 256],
|
||||
};
|
||||
|
||||
if GetIfEntry(&mut if_row) == NO_ERROR {
|
||||
if SetIfEntry(&if_row) == NO_ERROR {
|
||||
if GetIfEntry(&mut if_row) == NO_ERROR.0 {
|
||||
if SetIfEntry(&if_row) == NO_ERROR.0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to set interface status").into())
|
||||
@@ -331,7 +332,7 @@ impl RegistryManager {
|
||||
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
|
||||
|
||||
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
|
||||
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let profiles_key = hklm.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||
@@ -405,7 +406,7 @@ impl RegistryManager {
|
||||
}
|
||||
|
||||
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
|
||||
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let profiles_key = hklm.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||
@@ -448,12 +449,11 @@ impl RegistryManager {
|
||||
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) {
|
||||
// 检查 Connection/Name 是否匹配目标接口名
|
||||
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ) {
|
||||
if let Ok(name) = conn_key.get_value::<String, _>("Name") {
|
||||
if name == interface_name {
|
||||
return Ok(guid);
|
||||
}
|
||||
}
|
||||
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ)
|
||||
&& let Ok(name) = conn_key.get_value::<String, _>("Name")
|
||||
&& name == interface_name
|
||||
{
|
||||
return Ok(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+387
-127
@@ -1,19 +1,18 @@
|
||||
use std::io::IsTerminal as _;
|
||||
|
||||
use crate::common::config::LoggingConfigLoader;
|
||||
use crate::common::config::{FileLoggerConfig, LoggingConfigLoader};
|
||||
use crate::common::get_logger_timer_rfc3339;
|
||||
use crate::common::tracing_rolling_appender::{FileAppenderWrapper, RollingFileAppenderBase};
|
||||
use crate::rpc_service::logger::{CURRENT_LOG_LEVEL, LOGGER_LEVEL_SENDER};
|
||||
use anyhow::Context;
|
||||
use paste::paste;
|
||||
use regex::Regex;
|
||||
use std::io::IsTerminal;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing::{Level, Metadata};
|
||||
use tracing_subscriber::filter::{filter_fn, FilterExt};
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::filter::{FilterExt, filter_fn};
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
use tracing_subscriber::fmt::layer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::{EnvFilter, Layer};
|
||||
|
||||
macro_rules! __log__ {
|
||||
@@ -47,18 +46,31 @@ macro_rules! __log__ {
|
||||
|
||||
__log__!(const LOG_TARGET = "CORE");
|
||||
|
||||
fn parse_env_filter(default_level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
let mut filter = EnvFilter::builder()
|
||||
.with_default_directive(default_level.into())
|
||||
fn parse_env_filter(default_level: Option<LevelFilter>) -> Result<EnvFilter, anyhow::Error> {
|
||||
let directive = match default_level {
|
||||
Some(level) => level.into(),
|
||||
None => format!("{LOG_TARGET}=info").parse()?,
|
||||
};
|
||||
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(directive)
|
||||
.from_env()
|
||||
.with_context(|| "failed to create env filter")?;
|
||||
.with_context(|| "failed to create env filter")
|
||||
}
|
||||
|
||||
let pattern = Regex::new(&format!(r"(^|,){}\s*=", regex::escape(LOG_TARGET)))?;
|
||||
if !pattern.is_match(&filter.to_string()) {
|
||||
filter = filter.add_directive(format!("{LOG_TARGET}=info").parse()?);
|
||||
fn parse_static_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(level.into())
|
||||
.parse("")
|
||||
.with_context(|| "failed to create static filter")
|
||||
}
|
||||
|
||||
fn parse_file_filter(level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
|
||||
if matches!(level, LevelFilter::OFF) {
|
||||
parse_static_filter(level)
|
||||
} else {
|
||||
parse_env_filter(Some(level))
|
||||
}
|
||||
|
||||
Ok(filter)
|
||||
}
|
||||
|
||||
fn is_log(meta: &Metadata) -> bool {
|
||||
@@ -78,7 +90,6 @@ macro_rules! log_layer {
|
||||
$layer
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.with_ansi(true)
|
||||
.with_filter(filter_fn(is_log))
|
||||
.boxed()
|
||||
};
|
||||
@@ -86,127 +97,68 @@ macro_rules! log_layer {
|
||||
|
||||
pub fn init(
|
||||
config: impl LoggingConfigLoader,
|
||||
need_reload: bool,
|
||||
reload: bool,
|
||||
) -> Result<Option<NewFilterSender>, anyhow::Error> {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let file_config = config.get_file_logger_config();
|
||||
let file_level = file_config
|
||||
.level
|
||||
.map(|s| s.parse().unwrap())
|
||||
.unwrap_or(LevelFilter::OFF);
|
||||
let console_layers = console_layers(
|
||||
config
|
||||
.get_console_logger_config()
|
||||
.level
|
||||
.map(|s| s.parse().unwrap()),
|
||||
)?;
|
||||
layers.extend(console_layers);
|
||||
|
||||
let mut ret_sender: Option<NewFilterSender> = None;
|
||||
let sender = if cfg!(not(test)) {
|
||||
let (file_layers, sender) = file_layers(config.get_file_logger_config(), reload)?;
|
||||
layers.extend(file_layers);
|
||||
sender
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// logger to a rolling file
|
||||
if file_level != LevelFilter::OFF || need_reload {
|
||||
let dir = file_config.dir.as_deref().unwrap_or(".");
|
||||
let file = file_config.file.as_deref().unwrap_or("easytier.log");
|
||||
let path = std::path::Path::new(dir).join(file);
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
Registry::default()
|
||||
.with(layers)
|
||||
.try_init()
|
||||
.map(|_| sender)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
let builder = RollingFileAppenderBase::builder();
|
||||
let file_appender = builder
|
||||
.filename(path_str)
|
||||
.condition_daily()
|
||||
.max_filecount(file_config.count.unwrap_or(10))
|
||||
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
|
||||
.build()
|
||||
.unwrap();
|
||||
type BoxLayer = Box<dyn Layer<Registry> + Send + Sync>;
|
||||
|
||||
// Create a simple wrapper that implements MakeWriter
|
||||
let wrapper = FileAppenderWrapper::new(file_appender);
|
||||
|
||||
let (file_filter, file_filter_reloader) =
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_filter(file_level)?);
|
||||
|
||||
let layer = |wrapper| {
|
||||
layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(wrapper)
|
||||
.with_timer(get_logger_timer_rfc3339())
|
||||
};
|
||||
|
||||
layers.push(
|
||||
vec![
|
||||
tracing_layer!(layer(wrapper.clone())),
|
||||
log_layer!(layer(wrapper.clone())),
|
||||
]
|
||||
.with_filter(file_filter)
|
||||
.boxed(),
|
||||
);
|
||||
|
||||
if need_reload {
|
||||
let (sender, recver) = std::sync::mpsc::channel();
|
||||
ret_sender = Some(sender.clone());
|
||||
|
||||
// 初始化全局状态
|
||||
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(sender));
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(file_level.to_string()));
|
||||
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(lf) = recver.recv() {
|
||||
let parsed_level = match lf.parse::<LevelFilter>() {
|
||||
Ok(level) => level,
|
||||
Err(e) => {
|
||||
error!("Failed to parse new log level {:?}: {}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_filter = match EnvFilter::builder()
|
||||
.with_default_directive(parsed_level.into())
|
||||
.from_env()
|
||||
.with_context(|| "failed to create file filter")
|
||||
{
|
||||
Ok(filter) => Some(filter),
|
||||
Err(e) => {
|
||||
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match file_filter_reloader.modify(|f| {
|
||||
*f = new_filter
|
||||
.take()
|
||||
.expect("log filter reloader only applies one filter per reload");
|
||||
}) {
|
||||
Ok(()) => {
|
||||
info!("Reload log filter succeed, new filter level: {:?}", lf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to reload log filter: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Stop log filter reloader");
|
||||
});
|
||||
}
|
||||
fn console_layers(default_level: Option<LevelFilter>) -> anyhow::Result<Vec<BoxLayer>> {
|
||||
let mut layers = Vec::new();
|
||||
if matches!(default_level, Some(LevelFilter::OFF)) {
|
||||
return Ok(layers);
|
||||
}
|
||||
|
||||
// logger to console
|
||||
let console_config = config.get_console_logger_config();
|
||||
let console_level = console_config
|
||||
.level
|
||||
.map(|s| s.parse().unwrap())
|
||||
.unwrap_or(LevelFilter::OFF);
|
||||
|
||||
let (console_filter, _) =
|
||||
tracing_subscriber::reload::Layer::new(parse_env_filter(console_level)?);
|
||||
tracing_subscriber::reload::Layer::new(parse_env_filter(default_level)?);
|
||||
|
||||
let (stdout, stderr) = cfg_select! {
|
||||
test => {{
|
||||
let w = tracing_subscriber::fmt::TestWriter::new;
|
||||
(w, w)
|
||||
}}
|
||||
_ => (std::io::stdout, std::io::stderr),
|
||||
};
|
||||
|
||||
let ansi = std::io::stderr().is_terminal() || cfg!(test);
|
||||
|
||||
let layer = || {
|
||||
layer()
|
||||
.compact()
|
||||
.with_ansi(std::io::stderr().is_terminal())
|
||||
.with_timer(get_logger_timer_rfc3339())
|
||||
.with_writer(std::io::stderr)
|
||||
.with_ansi(ansi)
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.with_writer(stderr)
|
||||
};
|
||||
|
||||
layers.push(
|
||||
vec![
|
||||
tracing_layer!(layer()),
|
||||
log_layer!(layer()).with_filter(LevelFilter::WARN).boxed(),
|
||||
log_layer!(layer().with_writer(std::io::stdout))
|
||||
log_layer!(layer().with_writer(stdout))
|
||||
.with_filter(filter_fn(|metadata| *metadata.level() > Level::WARN))
|
||||
.boxed(),
|
||||
]
|
||||
@@ -219,23 +171,331 @@ pub fn init(
|
||||
layers.push(console_subscriber::ConsoleLayer::builder().spawn().boxed());
|
||||
}
|
||||
|
||||
Registry::default().with(layers).init();
|
||||
Ok(layers)
|
||||
}
|
||||
|
||||
Ok(ret_sender)
|
||||
fn file_layers(
|
||||
config: FileLoggerConfig,
|
||||
reload: bool,
|
||||
) -> anyhow::Result<(Vec<BoxLayer>, Option<NewFilterSender>)> {
|
||||
let mut layers = Vec::new();
|
||||
|
||||
let level = config
|
||||
.level
|
||||
.map(|s| s.parse().unwrap())
|
||||
.unwrap_or(LevelFilter::OFF);
|
||||
|
||||
if matches!(level, LevelFilter::OFF) && !reload {
|
||||
return Ok((layers, None));
|
||||
}
|
||||
|
||||
let (file_filter, file_filter_reloader) =
|
||||
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_file_filter(level)?);
|
||||
|
||||
let layer = |wrapper| {
|
||||
layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(wrapper)
|
||||
.with_timer(get_logger_timer_rfc3339())
|
||||
};
|
||||
|
||||
let wrapper = {
|
||||
let path = {
|
||||
let dir = config.dir.as_deref().unwrap_or(".");
|
||||
let file = config.file.as_deref().unwrap_or("easytier.log");
|
||||
let path = std::path::Path::new(dir).join(file);
|
||||
path.to_string_lossy().into_owned()
|
||||
};
|
||||
|
||||
let builder = RollingFileAppenderBase::builder();
|
||||
let file_appender = builder
|
||||
.filename(path)
|
||||
.condition_daily()
|
||||
.max_filecount(config.count.unwrap_or(10))
|
||||
.condition_max_file_size(config.size_mb.unwrap_or(100) * 1024 * 1024)
|
||||
.build()
|
||||
.with_context(|| "failed to initialize rolling file appender")?;
|
||||
|
||||
FileAppenderWrapper::new(file_appender)
|
||||
};
|
||||
|
||||
layers.push(
|
||||
vec![
|
||||
tracing_layer!(layer(wrapper.clone())),
|
||||
log_layer!(layer(wrapper.clone())),
|
||||
]
|
||||
.with_filter(file_filter)
|
||||
.boxed(),
|
||||
);
|
||||
|
||||
if !reload {
|
||||
return Ok((layers, None));
|
||||
}
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
// 初始化全局状态
|
||||
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(tx.clone()));
|
||||
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(level.to_string()));
|
||||
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(lf) = rx.recv() {
|
||||
let parsed_level = match lf.parse::<LevelFilter>() {
|
||||
Ok(level) => level,
|
||||
Err(e) => {
|
||||
error!("Failed to parse new log level {:?}: {}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_filter = match parse_file_filter(parsed_level) {
|
||||
Ok(filter) => Some(filter),
|
||||
Err(e) => {
|
||||
error!("Failed to build new log filter for {:?}: {:?}", lf, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match file_filter_reloader.modify(|f| {
|
||||
*f = new_filter
|
||||
.take()
|
||||
.expect("log filter reloader only applies one filter per reload");
|
||||
}) {
|
||||
Ok(()) => {
|
||||
info!("Reload log filter succeed, new filter level: {:?}", lf);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to reload log filter: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Stop log filter reloader");
|
||||
});
|
||||
|
||||
Ok((layers, Some(tx)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::config::{self};
|
||||
use crate::common::config::FileLoggerConfig;
|
||||
|
||||
async fn test_logger_reload() {
|
||||
println!("current working dir: {:?}", std::env::current_dir());
|
||||
let config = config::LoggingConfigBuilder::default().build().unwrap();
|
||||
let s = init(&config, true).unwrap();
|
||||
tracing::debug!("test not display debug");
|
||||
s.unwrap().send(LevelFilter::DEBUG.to_string()).unwrap();
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
tracing::debug!("test display debug");
|
||||
const RUST_LOG: &str = "RUST_LOG";
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<std::ffi::OsString>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
Self { key, previous }
|
||||
}
|
||||
|
||||
fn unset(key: &'static str) -> Self {
|
||||
let previous = std::env::var_os(key);
|
||||
unsafe { std::env::remove_var(key) };
|
||||
Self { key, previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
match &self.previous {
|
||||
Some(value) => unsafe { std::env::set_var(self.key, value) },
|
||||
None => unsafe { std::env::remove_var(self.key) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init() {
|
||||
let _ = Registry::default()
|
||||
.with(console_layers(Some(LevelFilter::WARN)).unwrap())
|
||||
.try_init();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_file_logger_level_is_off_without_reload() {
|
||||
let (layers, sender) = file_layers(FileLoggerConfig::default(), false).unwrap();
|
||||
|
||||
assert!(layers.is_empty());
|
||||
assert!(sender.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn default_file_logger_level_filters_info_with_reload() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "default-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "default-file-logger-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"default file logger level should filter info logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_level_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, _sender) = file_layers(cfg, true).unwrap();
|
||||
let marker = "file-logger-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"enabled file logger should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_uses_env_filter_when_enabled() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "debug");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-env-filter-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-env-filter-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::INFO.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::debug!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
content.contains(marker),
|
||||
"file logger enabled by reload should use RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn file_logger_reload_off_ignores_env_filter() {
|
||||
let _guard = EnvVarGuard::set(RUST_LOG, "info");
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-off-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
let marker = "file-logger-reload-off-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
sender.send(LevelFilter::OFF.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::info!(target: LOG_TARGET, "{}", marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(marker),
|
||||
"disabled file logger should ignore RUST_LOG directives"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_logger_reload() {
|
||||
let _guard = EnvVarGuard::unset(RUST_LOG);
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let log_file_name = "reload-test.log".to_string();
|
||||
let log_path = temp_dir.path().join(&log_file_name);
|
||||
|
||||
let cfg = FileLoggerConfig {
|
||||
level: Some(LevelFilter::INFO.to_string()),
|
||||
file: Some(log_file_name),
|
||||
dir: Some(temp_dir.path().to_string_lossy().to_string()),
|
||||
size_mb: Some(10),
|
||||
count: Some(1),
|
||||
};
|
||||
|
||||
let (layers, sender) = file_layers(cfg, true).unwrap();
|
||||
let sender = sender.expect("reload=true should return a sender");
|
||||
|
||||
let before_marker = "reload-before-debug-marker";
|
||||
let after_marker = "reload-after-debug-marker";
|
||||
let subscriber = Registry::default().with(layers);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::debug!("{}", before_marker);
|
||||
|
||||
sender.send(LevelFilter::DEBUG.to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
|
||||
tracing::debug!("{}", after_marker);
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
});
|
||||
|
||||
let content = std::fs::read_to_string(&log_path).unwrap_or_default();
|
||||
assert!(
|
||||
!content.contains(before_marker),
|
||||
"debug log should be filtered before reload"
|
||||
);
|
||||
assert!(
|
||||
content.contains(after_marker),
|
||||
"debug log should be visible after reload"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ pub mod acl_processor;
|
||||
pub mod compressor;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod defer;
|
||||
pub mod dns;
|
||||
pub mod env_parser;
|
||||
pub mod error;
|
||||
@@ -25,12 +24,12 @@ pub mod log;
|
||||
pub mod netns;
|
||||
pub mod network;
|
||||
pub mod os_info;
|
||||
pub mod scoped_task;
|
||||
pub mod stats_manager;
|
||||
pub mod stun;
|
||||
pub mod stun_codec_ext;
|
||||
pub mod token_bucket;
|
||||
pub mod tracing_rolling_appender;
|
||||
pub mod upnp;
|
||||
|
||||
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||
format: F,
|
||||
@@ -41,8 +40,8 @@ pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||
tracing_subscriber::fmt::time::OffsetTime::new(local_offset, format)
|
||||
}
|
||||
|
||||
pub fn get_logger_timer_rfc3339(
|
||||
) -> tracing_subscriber::fmt::time::OffsetTime<time::format_description::well_known::Rfc3339> {
|
||||
pub fn get_logger_timer_rfc3339()
|
||||
-> tracing_subscriber::fmt::time::OffsetTime<time::format_description::well_known::Rfc3339> {
|
||||
get_logger_timer(time::format_description::well_known::Rfc3339)
|
||||
}
|
||||
|
||||
@@ -117,10 +116,10 @@ pub fn get_machine_id() -> uuid::Uuid {
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id"));
|
||||
|
||||
// try load from local file
|
||||
if let Ok(mid) = std::fs::read_to_string(&machine_id_file) {
|
||||
if let Ok(mid) = uuid::Uuid::parse_str(mid.trim()) {
|
||||
return mid;
|
||||
}
|
||||
if let Ok(mid) = std::fs::read_to_string(&machine_id_file)
|
||||
&& let Ok(mid) = uuid::Uuid::parse_str(mid.trim())
|
||||
{
|
||||
return mid;
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use futures::Future;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use nix::sched::{setns, CloneFlags};
|
||||
use nix::sched::{CloneFlags, setns};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::os::fd::AsFd;
|
||||
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
//! This crate provides a wrapper type of Tokio's JoinHandle: `ScopedTask`, which aborts the task when it's dropped.
|
||||
//! `ScopedTask` can still be awaited to join the child-task, and abort-on-drop will still trigger while it is being awaited.
|
||||
//!
|
||||
//! For example, if task A spawned task B but is doing something else, and task B is waiting for task C to join,
|
||||
//! aborting A will also abort both B and C.
|
||||
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScopedTask<T> {
|
||||
inner: JoinHandle<T>,
|
||||
}
|
||||
|
||||
impl<T> Drop for ScopedTask<T> {
|
||||
fn drop(&mut self) {
|
||||
self.inner.abort()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ScopedTask<T> {
|
||||
type Output = <JoinHandle<T> as Future>::Output;
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner).poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<JoinHandle<T>> for ScopedTask<T> {
|
||||
fn from(inner: JoinHandle<T>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ScopedTask<T> {
|
||||
type Target = JoinHandle<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ScopedTask;
|
||||
use futures_util::future::pending;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::task::yield_now;
|
||||
|
||||
struct Sentry(Arc<RwLock<bool>>);
|
||||
impl Drop for Sentry {
|
||||
fn drop(&mut self) {
|
||||
*self.0.write().unwrap() = true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_while_not_waiting_for_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
drop(task);
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_while_waiting_for_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let handle = tokio::spawn(async move {
|
||||
ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}))
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
handle.abort();
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_drop_only_join() {
|
||||
assert_eq!(
|
||||
ScopedTask::from(tokio::spawn(async {
|
||||
yield_now().await;
|
||||
5
|
||||
}))
|
||||
.await
|
||||
.unwrap(),
|
||||
5
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manually_abort_before_drop() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
task.abort();
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manually_abort_then_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
task.abort();
|
||||
yield_now().await;
|
||||
assert!(task.await.is_err());
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::common::scoped_task::ScopedTask;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
/// Predefined metric names for type safety
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
@@ -42,6 +41,8 @@ pub enum MetricName {
|
||||
TrafficControlBytesRxByInstance,
|
||||
/// Traffic bytes forwarded
|
||||
TrafficBytesForwarded,
|
||||
/// Control-plane traffic bytes forwarded
|
||||
TrafficControlBytesForwarded,
|
||||
/// Traffic bytes sent to self
|
||||
TrafficBytesSelfTx,
|
||||
/// Traffic bytes received from self
|
||||
@@ -71,6 +72,8 @@ pub enum MetricName {
|
||||
TrafficControlPacketsRxByInstance,
|
||||
/// Traffic packets forwarded
|
||||
TrafficPacketsForwarded,
|
||||
/// Control-plane traffic packets forwarded
|
||||
TrafficControlPacketsForwarded,
|
||||
/// Traffic packets sent to self
|
||||
TrafficPacketsSelfTx,
|
||||
/// Traffic packets received from self
|
||||
@@ -117,6 +120,9 @@ impl fmt::Display for MetricName {
|
||||
write!(f, "traffic_control_bytes_rx_by_instance")
|
||||
}
|
||||
MetricName::TrafficBytesForwarded => write!(f, "traffic_bytes_forwarded"),
|
||||
MetricName::TrafficControlBytesForwarded => {
|
||||
write!(f, "traffic_control_bytes_forwarded")
|
||||
}
|
||||
MetricName::TrafficBytesSelfTx => write!(f, "traffic_bytes_self_tx"),
|
||||
MetricName::TrafficBytesSelfRx => write!(f, "traffic_bytes_self_rx"),
|
||||
MetricName::TrafficBytesForeignForwardRx => {
|
||||
@@ -146,6 +152,9 @@ impl fmt::Display for MetricName {
|
||||
write!(f, "traffic_control_packets_rx_by_instance")
|
||||
}
|
||||
MetricName::TrafficPacketsForwarded => write!(f, "traffic_packets_forwarded"),
|
||||
MetricName::TrafficControlPacketsForwarded => {
|
||||
write!(f, "traffic_control_packets_forwarded")
|
||||
}
|
||||
MetricName::TrafficPacketsSelfTx => write!(f, "traffic_packets_self_tx"),
|
||||
MetricName::TrafficPacketsSelfRx => write!(f, "traffic_packets_self_rx"),
|
||||
MetricName::TrafficPacketsForeignForwardRx => {
|
||||
@@ -374,7 +383,9 @@ impl UnsafeCounter {
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn add(&self, delta: u64) {
|
||||
let ptr = self.value.get();
|
||||
*ptr = (*ptr).saturating_add(delta);
|
||||
unsafe {
|
||||
*ptr = (*ptr).saturating_add(delta);
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the counter by 1
|
||||
@@ -382,7 +393,9 @@ impl UnsafeCounter {
|
||||
/// This method is unsafe because it uses UnsafeCell. The caller must ensure
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn inc(&self) {
|
||||
self.add(1);
|
||||
unsafe {
|
||||
self.add(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current value of the counter
|
||||
@@ -391,7 +404,7 @@ impl UnsafeCounter {
|
||||
/// that no other thread is modifying this counter simultaneously.
|
||||
pub unsafe fn get(&self) -> u64 {
|
||||
let ptr = self.value.get();
|
||||
*ptr
|
||||
unsafe { *ptr }
|
||||
}
|
||||
|
||||
/// Reset the counter to zero
|
||||
@@ -400,7 +413,9 @@ impl UnsafeCounter {
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn reset(&self) {
|
||||
let ptr = self.value.get();
|
||||
*ptr = 0;
|
||||
unsafe {
|
||||
*ptr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the counter to a specific value
|
||||
@@ -409,7 +424,9 @@ impl UnsafeCounter {
|
||||
/// that no other thread is accessing this counter simultaneously.
|
||||
pub unsafe fn set(&self, value: u64) {
|
||||
let ptr = self.value.get();
|
||||
*ptr = value;
|
||||
unsafe {
|
||||
*ptr = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +463,9 @@ impl MetricData {
|
||||
/// that no other thread is accessing this timestamp simultaneously.
|
||||
unsafe fn touch(&self) {
|
||||
let ptr = self.last_updated.get();
|
||||
*ptr = Instant::now();
|
||||
unsafe {
|
||||
*ptr = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last updated timestamp
|
||||
@@ -455,7 +474,7 @@ impl MetricData {
|
||||
/// that no other thread is modifying this timestamp simultaneously.
|
||||
unsafe fn get_last_updated(&self) -> Instant {
|
||||
let ptr = self.last_updated.get();
|
||||
*ptr
|
||||
unsafe { *ptr }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +577,7 @@ impl MetricSnapshot {
|
||||
/// StatsManager manages global statistics with high performance counters
|
||||
pub struct StatsManager {
|
||||
counters: Arc<DashMap<MetricKey, Arc<MetricData>>>,
|
||||
cleanup_task: ScopedTask<()>,
|
||||
cleanup_task: AbortOnDropHandle<()>,
|
||||
}
|
||||
|
||||
impl StatsManager {
|
||||
@@ -591,7 +610,7 @@ impl StatsManager {
|
||||
|
||||
Self {
|
||||
counters,
|
||||
cleanup_task: cleanup_task.into(),
|
||||
cleanup_task: AbortOnDropHandle::new(cleanup_task),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-39
@@ -11,8 +11,8 @@ use crossbeam::atomic::AtomicCell;
|
||||
use rand::seq::IteratorRandom;
|
||||
use socket2::{SockAddr, SockRef};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{lookup_host, UdpSocket};
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::net::{UdpSocket, lookup_host};
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{Instrument, Level};
|
||||
|
||||
@@ -239,15 +239,11 @@ impl StunClient {
|
||||
let mut mapped_addr = None;
|
||||
for x in msg.attributes() {
|
||||
match x {
|
||||
Attribute::MappedAddress(addr) => {
|
||||
if mapped_addr.is_none() {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::MappedAddress(addr) if mapped_addr.is_none() => {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::XorMappedAddress(addr) => {
|
||||
if mapped_addr.is_none() {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::XorMappedAddress(addr) if mapped_addr.is_none() => {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -259,15 +255,11 @@ impl StunClient {
|
||||
let mut changed_addr = None;
|
||||
for x in msg.attributes() {
|
||||
match x {
|
||||
Attribute::OtherAddress(m) => {
|
||||
if changed_addr.is_none() {
|
||||
let _ = changed_addr.insert(m.address());
|
||||
}
|
||||
Attribute::OtherAddress(m) if changed_addr.is_none() => {
|
||||
let _ = changed_addr.insert(m.address());
|
||||
}
|
||||
Attribute::ChangedAddress(m) => {
|
||||
if changed_addr.is_none() {
|
||||
let _ = changed_addr.insert(m.address());
|
||||
}
|
||||
Attribute::ChangedAddress(m) if changed_addr.is_none() => {
|
||||
let _ = changed_addr.insert(m.address());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -284,7 +276,6 @@ impl StunClient {
|
||||
let stun_host = self.stun_server;
|
||||
// repeat req in case of packet loss
|
||||
let mut tids = vec![];
|
||||
|
||||
for _ in 0..self.req_repeat {
|
||||
let tid = rand::random::<u32>();
|
||||
// let tid = 1;
|
||||
@@ -714,15 +705,11 @@ impl TcpStunClient {
|
||||
let mut mapped_addr = None;
|
||||
for x in msg.attributes() {
|
||||
match x {
|
||||
Attribute::MappedAddress(addr) => {
|
||||
if mapped_addr.is_none() {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::MappedAddress(addr) if mapped_addr.is_none() => {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::XorMappedAddress(addr) => {
|
||||
if mapped_addr.is_none() {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
Attribute::XorMappedAddress(addr) if mapped_addr.is_none() => {
|
||||
let _ = mapped_addr.insert(addr.address());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -924,6 +911,10 @@ impl TcpNatTypeDetector {
|
||||
pub trait StunInfoCollectorTrait: Send + Sync {
|
||||
fn get_stun_info(&self) -> StunInfo;
|
||||
async fn get_udp_port_mapping(&self, local_port: u16) -> Result<SocketAddr, Error>;
|
||||
async fn get_udp_port_mapping_with_socket(
|
||||
&self,
|
||||
udp: Arc<UdpSocket>,
|
||||
) -> Result<SocketAddr, Error>;
|
||||
async fn get_tcp_port_mapping(&self, local_port: u16) -> Result<SocketAddr, Error>;
|
||||
}
|
||||
|
||||
@@ -987,6 +978,14 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, local_port: u16) -> Result<SocketAddr, Error> {
|
||||
let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?);
|
||||
self.get_udp_port_mapping_with_socket(udp).await
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping_with_socket(
|
||||
&self,
|
||||
udp: Arc<UdpSocket>,
|
||||
) -> Result<SocketAddr, Error> {
|
||||
self.start_stun_routine();
|
||||
|
||||
let mut stun_servers = self
|
||||
@@ -1012,7 +1011,6 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?);
|
||||
let mut client_builder = StunClientBuilder::new(udp.clone());
|
||||
|
||||
for server in stun_servers.iter() {
|
||||
@@ -1328,6 +1326,13 @@ impl StunInfoCollectorTrait for MockStunInfoCollector {
|
||||
Ok(format!("127.0.0.1:{}", port).parse().unwrap())
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping_with_socket(
|
||||
&self,
|
||||
udp: Arc<UdpSocket>,
|
||||
) -> Result<std::net::SocketAddr, Error> {
|
||||
self.get_udp_port_mapping(udp.local_addr()?.port()).await
|
||||
}
|
||||
|
||||
async fn get_tcp_port_mapping(&self, mut port: u16) -> Result<std::net::SocketAddr, Error> {
|
||||
if port == 0 {
|
||||
port = 40144;
|
||||
@@ -1338,11 +1343,9 @@ impl StunInfoCollectorTrait for MockStunInfoCollector {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
common::scoped_task::ScopedTask,
|
||||
tunnel::{udp::UdpTunnelListener, TunnelListener},
|
||||
};
|
||||
use crate::tunnel::{TunnelListener, udp::UdpTunnelListener};
|
||||
use tokio::time::{sleep, timeout};
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1404,10 +1407,10 @@ mod tests {
|
||||
loop {
|
||||
let ret = detector.detect_nat_type(0).await;
|
||||
println!("{:#?}, {:?}", ret, ret.as_ref().map(|x| x.nat_type()));
|
||||
if let Ok(resp) = ret {
|
||||
if !resp.stun_resps.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Ok(resp) = ret
|
||||
&& !resp.stun_resps.is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
@@ -1436,7 +1439,7 @@ mod tests {
|
||||
use stun_codec::rfc5389::attributes::XorMappedAddress;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
async fn spawn_tcp_stun_server() -> (SocketAddr, ScopedTask<()>) {
|
||||
async fn spawn_tcp_stun_server() -> (SocketAddr, AbortOnDropHandle<()>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let server_addr = listener.local_addr().unwrap();
|
||||
|
||||
@@ -1460,7 +1463,7 @@ mod tests {
|
||||
stream.write_all(rsp_buf.as_slice()).await.unwrap();
|
||||
});
|
||||
|
||||
(server_addr, task.into())
|
||||
(server_addr, AbortOnDropHandle::new(task))
|
||||
}
|
||||
|
||||
let (server1, _t1) = spawn_tcp_stun_server().await;
|
||||
@@ -1499,7 +1502,7 @@ mod tests {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let server_addr = listener.local_addr().unwrap();
|
||||
|
||||
let _t = ScopedTask::from(tokio::spawn(async move {
|
||||
let _t = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
for _ in 0..8 {
|
||||
let Ok((mut stream, peer_addr)) = listener.accept().await else {
|
||||
break;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use bytecodec::fixnum::{U32beDecoder, U32beEncoder};
|
||||
use stun_codec::net::{socket_addr_xor, SocketAddrDecoder, SocketAddrEncoder};
|
||||
use stun_codec::net::{SocketAddrDecoder, SocketAddrEncoder, socket_addr_xor};
|
||||
|
||||
use stun_codec::rfc5389::attributes::{
|
||||
MappedAddress, Software, XorMappedAddress, XorMappedAddress2,
|
||||
};
|
||||
use stun_codec::rfc5780::attributes::{OtherAddress, ResponseOrigin};
|
||||
use stun_codec::{define_attribute_enums, AttributeType, Message, TransactionId};
|
||||
use stun_codec::{AttributeType, Message, TransactionId, define_attribute_enums};
|
||||
|
||||
use bytecodec::{ByteCount, Decode, Encode, Eos, Result, SizedEncode, TryTaggedDecode};
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use crate::common::scoped_task::ScopedTask;
|
||||
use crate::proto::common::LimiterConfig;
|
||||
|
||||
/// Token Bucket rate limiter using atomic operations
|
||||
@@ -14,7 +14,7 @@ pub struct TokenBucket {
|
||||
available_tokens: AtomicU64, // Current token count (atomic)
|
||||
last_refill_time: AtomicU64, // Last refill time as micros since epoch
|
||||
config: BucketConfig, // Immutable configuration
|
||||
refill_task: Mutex<Option<ScopedTask<()>>>, // Background refill task
|
||||
refill_task: Mutex<Option<AbortOnDropHandle<()>>>, // Background refill task
|
||||
start_time: Instant, // Bucket creation time
|
||||
|
||||
refill_notifier: Arc<Notify>,
|
||||
@@ -91,7 +91,7 @@ impl TokenBucket {
|
||||
.refill_task
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(refill_task.into());
|
||||
.replace(AbortOnDropHandle::new(refill_task));
|
||||
arc_self
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ impl TokenBucket {
|
||||
pub struct TokenBucketManager {
|
||||
buckets: Arc<DashMap<String, Arc<TokenBucket>>>,
|
||||
|
||||
retain_task: ScopedTask<()>,
|
||||
retain_task: AbortOnDropHandle<()>,
|
||||
}
|
||||
|
||||
impl Default for TokenBucketManager {
|
||||
@@ -205,7 +205,7 @@ impl TokenBucketManager {
|
||||
|
||||
Self {
|
||||
buckets,
|
||||
retain_task: retain_task.into(),
|
||||
retain_task: AbortOnDropHandle::new(retain_task),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
/// Test initial state after creation
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RollingConditionBase {
|
||||
@@ -57,17 +58,16 @@ impl Default for RollingConditionBase {
|
||||
impl RollingCondition for RollingConditionBase {
|
||||
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
|
||||
let mut rollover = false;
|
||||
if let Some(frequency) = self.frequency_opt.as_ref() {
|
||||
if let Some(last_write) = self.last_write_opt.as_ref() {
|
||||
if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
|
||||
rollover = true;
|
||||
}
|
||||
}
|
||||
if let Some(frequency) = self.frequency_opt.as_ref()
|
||||
&& let Some(last_write) = self.last_write_opt.as_ref()
|
||||
&& frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write)
|
||||
{
|
||||
rollover = true;
|
||||
}
|
||||
if let Some(max_size) = self.max_size_opt.as_ref() {
|
||||
if current_filesize >= *max_size {
|
||||
rollover = true;
|
||||
}
|
||||
if let Some(max_size) = self.max_size_opt.as_ref()
|
||||
&& current_filesize >= *max_size
|
||||
{
|
||||
rollover = true;
|
||||
}
|
||||
self.last_write_opt = Some(*now);
|
||||
rollover
|
||||
@@ -136,9 +136,11 @@ impl RollingFileAppenderBaseBuilder {
|
||||
/// Builds a RollingFileAppenderBase instance from the current settings.
|
||||
///
|
||||
/// Returns an error if the filename is empty.
|
||||
pub fn build(self) -> Result<RollingFileAppenderBase, &'static str> {
|
||||
pub fn build(self) -> anyhow::Result<RollingFileAppenderBase> {
|
||||
if self.filename.is_empty() {
|
||||
return Err("A filename is required to be set and can not be blank");
|
||||
return Err(anyhow!(
|
||||
"A filename is required to be set and can not be blank"
|
||||
));
|
||||
}
|
||||
Ok(RollingFileAppenderBase {
|
||||
condition: self.condition,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user