Compare commits

..

4 Commits

Author SHA1 Message Date
fanyang 349dbf7d8d fix(web): avoid false default-password reminders
Only flag seeded accounts that still use the shipped password hash,
and keep auth status and password change responses stable during
review follow-up.
2026-04-05 17:54:12 +08:00
fanyang 7707b1cf5e fix(web): require password confirmation in auth forms
Require users to enter new passwords twice in the registration
and password change forms so typos are caught before credentials
are stored.
2026-04-05 17:31:22 +08:00
fanyang 2490bb9808 fix(web): enforce password strength in auth forms
Apply the same password policy to registration and password
changes so operators cannot replace default credentials with
another weak password and users see consistent guidance.
2026-04-05 17:31:22 +08:00
fanyang 3f3e36e653 feat(web): warn on default-password accounts
Track built-in admin and user accounts that still use their
seeded password so the web UI can prompt operators to
rotate credentials after deployment.

- Persist must-change-password state for seeded accounts.
- Clear the reminder after password changes and validate
  empty-password updates.
- Keep the migration and auth API behavior explicit.
2026-04-05 17:31:22 +08:00
219 changed files with 5229 additions and 8432 deletions
+54 -35
View File
@@ -1,40 +1,29 @@
# region Native [target.x86_64-unknown-linux-musl]
linker = "rust-lld"
[target.x86_64-unknown-linux-gnu] rustflags = ["-C", "linker-flavor=ld.lld"]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu] [target.aarch64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "aarch64-linux-gnu-gcc"
[target.'cfg(all(windows, target_env = "msvc"))'] [target.aarch64-unknown-linux-ohos]
rustflags = ["-C", "target-feature=+crt-static"] ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
# region [target.aarch64-unknown-linux-ohos.env]
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
# region CI PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
[target.x86_64-unknown-linux-musl] SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-unknown-linux-musl] [target.aarch64-unknown-linux-musl]
linker = "aarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.riscv64gc-unknown-linux-musl] [target.riscv64gc-unknown-linux-musl]
linker = "riscv64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabihf] [target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabi]
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabihf]
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabi]
rustflags = ["-C", "target-feature=+crt-static"]
[target.loongarch64-unknown-linux-musl]
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.mipsel-unknown-linux-musl] [target.mipsel-unknown-linux-musl]
@@ -75,14 +64,44 @@ rustflags = [
"gcc", "gcc",
] ]
[target.aarch64-unknown-linux-ohos] [target.armv7-unknown-linux-musleabihf]
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar" linker = "armv7-unknown-linux-musleabihf-gcc"
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh" rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-unknown-linux-ohos.env] [target.armv7-unknown-linux-musleabi]
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig" linker = "armv7-unknown-linux-musleabi-gcc"
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib" rustflags = ["-C", "target-feature=+crt-static"]
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
# endregion [target.loongarch64-unknown-linux-musl]
linker = "loongarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabihf]
linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
"-L",
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
[target.arm-unknown-linux-musleabi]
linker = "arm-unknown-linux-musleabi-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
"-L",
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
+9 -56
View File
@@ -2,17 +2,10 @@ name: prepare-build
author: Luna author: Luna
description: Prepare build environment description: Prepare build environment
inputs: inputs:
target: web:
description: 'The target to build for' description: 'Whether to prepare the web build environment'
required: false
pnpm:
description: 'Whether to run pnpm build'
required: true required: true
default: 'true' default: 'true'
pnpm-build-filter:
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
required: false
default: './easytier-web/*'
gui: gui:
description: 'Whether to prepare the GUI build environment' description: 'Whether to prepare the GUI build environment'
required: true required: true
@@ -26,61 +19,21 @@ runs:
- run: mkdir -p easytier-gui/dist - run: mkdir -p easytier-gui/dist
shell: bash shell: bash
- name: Install dependencies
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt-get update
sudo apt-get install -qqy build-essential mold musl-tools
shell: bash
- name: Setup Frontend Environment - name: Setup Frontend Environment
if: ${{ inputs.pnpm == 'true' }} if: ${{ inputs.web == 'true' }}
uses: ./.github/actions/prepare-pnpm uses: ./.github/actions/prepare-pnpm
with: with:
build-filter: ${{ inputs.pnpm-build-filter }} build-filter: './easytier-web/*'
- name: Install GUI dependencies (Linux) - name: Install GUI dependencies (Used by clippy)
if: ${{ inputs.gui == 'true' && runner.os == 'Linux' }} if: ${{ inputs.gui == 'true' }}
run: | run: |
sudo apt-get install -qq xdg-utils \ bash ./.github/workflows/install_gui_dep.sh
libappindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev
shell: bash shell: bash
- uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install Rust
with:
toolchain: 1.95
target: ${{ !contains(inputs.target, 'mips') && inputs.target || '' }}
components: ${{ contains(inputs.target, 'mips') && 'rust-src' || '' }}
cache: false
rustflags: ''
- name: Install Rust (MIPS)
if: ${{ contains(inputs.target, 'mips') }}
run: | run: |
MUSL_TARGET=${{ inputs.target }}sf bash ./.github/workflows/install_rust.sh
mkdir -p ./musl_gcc
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
if [[ -d "./musl_gcc/sysroot" ]]; then
echo "BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)" >> $GITHUB_ENV
fi
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a
# for mimalloc
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
shell: bash shell: bash
- name: Setup protoc - name: Setup protoc
+145 -119
View File
@@ -2,14 +2,9 @@ name: EasyTier Core
on: on:
push: push:
branches: [ "develop", "main", "releases/**" ] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: [ "develop", "main" ] branches: ["develop", "main"]
types: [ opened, synchronize, reopened, ready_for_review ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -23,7 +18,6 @@ jobs:
pre_job: pre_job:
# continue-on-error: true # Uncomment once integration is finished # continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
# do not skip push on branch starts with releases/ # do not skip push on branch starts with releases/
@@ -36,7 +30,7 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
cancel_others: 'true' cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh", "easytier-web/**"]'
build_web: build_web:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_job needs: pre_job
@@ -57,48 +51,41 @@ jobs:
easytier-web/frontend/dist/* easytier-web/frontend/dist/*
build: build:
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
include: include:
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-x86_64
- TARGET: aarch64-unknown-linux-musl - TARGET: aarch64-unknown-linux-musl
OS: ubuntu-24.04-arm OS: ubuntu-22.04
ARTIFACT_NAME: linux-aarch64 ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-x86_64
- TARGET: riscv64gc-unknown-linux-musl - TARGET: riscv64gc-unknown-linux-musl
OS: ubuntu-24.04 OS: ubuntu-22.04
ARTIFACT_NAME: linux-riscv64 ARTIFACT_NAME: linux-riscv64
- TARGET: mips-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mipsel
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-22.04
ARTIFACT_NAME: linux-arm
- TARGET: loongarch64-unknown-linux-musl - TARGET: loongarch64-unknown-linux-musl
OS: ubuntu-24.04 OS: ubuntu-24.04
ARTIFACT_NAME: linux-loongarch64 ARTIFACT_NAME: linux-loongarch64
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-24.04
ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-24.04
ARTIFACT_NAME: linux-arm
- TARGET: mips-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-24.04
ARTIFACT_NAME: linux-mipsel
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-24.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
- TARGET: x86_64-apple-darwin - TARGET: x86_64-apple-darwin
OS: macos-latest OS: macos-latest
ARTIFACT_NAME: macos-x86_64 ARTIFACT_NAME: macos-x86_64
@@ -109,12 +96,17 @@ jobs:
- TARGET: x86_64-pc-windows-msvc - TARGET: x86_64-pc-windows-msvc
OS: windows-latest OS: windows-latest
ARTIFACT_NAME: windows-x86_64 ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc - TARGET: i686-pc-windows-msvc
OS: windows-latest OS: windows-latest
ARTIFACT_NAME: windows-i686 ARTIFACT_NAME: windows-i686
- TARGET: aarch64-pc-windows-msvc
OS: windows-11-arm - TARGET: x86_64-unknown-freebsd
ARTIFACT_NAME: windows-arm64 OS: ubuntu-22.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
runs-on: ${{ matrix.OS }} runs-on: ${{ matrix.OS }}
env: env:
@@ -139,15 +131,8 @@ jobs:
name: easytier-web-dashboard name: easytier-web-dashboard
path: easytier-web/frontend/dist/ path: easytier-web/frontend/dist/
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
target: ${{ matrix.TARGET }}
gui: true
pnpm: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
with: with:
# The prefix cache key, this can be changed to start a new cache manually. # The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust" # default: "v0-rust"
@@ -155,51 +140,96 @@ jobs:
shared-key: "core-registry" shared-key: "core-registry"
cache-targets: "false" cache-targets: "false"
- uses: mlugg/setup-zig@v2 - name: Setup protoc
if: ${{ contains(matrix.OS, 'ubuntu') }} uses: arduino/setup-protoc@v3
- uses: taiki-e/install-action@v2
if: ${{ contains(matrix.OS, 'ubuntu') }}
with: with:
tool: cargo-zigbuild # GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build - name: Build Core & Cli
if: ${{ !contains(matrix.TARGET, 'mips') }} if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
run: | run: |
if [[ "$TARGET" == *windows* ]]; then bash ./.github/workflows/install_rust.sh
SUFFIX=.exe
else # loongarch need llvm-18
SUFFIX="" 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 fi
if [[ "$TARGET" =~ (x86_64-unknown-linux-musl|aarch64-unknown-linux-musl|windows|darwin) ]]; then if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
BUILD=build cargo +nightly-2026-02-02 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
else else
BUILD=zigbuild 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
fi fi
if [[ "$TARGET" =~ ^(riscv64|loongarch64|aarch64).*$ || "$TARGET" =~ windows ]]; then # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
FEATURES="mimalloc" - name: Build Core & Cli (X86_64 FreeBSD)
else uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
FEATURES="jemalloc" if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
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: env:
RUSTC_BOOTSTRAP: 1 TARGET: ${{ matrix.TARGET }}
run: | with:
cargo build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc 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
- name: Compress - name: Compress
run: | run: |
mkdir -p ./artifacts/objects/ mkdir -p ./artifacts/objects/
# windows is the only OS using a different convention for executable file name # windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ ]]; then if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe SUFFIX=.exe
@@ -212,37 +242,26 @@ jobs:
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \; find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
fi fi
fi fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME TAG=$GITHUB_REF_NAME
else else
TAG=$GITHUB_SHA TAG=$GITHUB_SHA
fi fi
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ (loongarch|freebsd) ]]; then if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ && ! $TARGET =~ ^riscv64.*$ ]]; then
HOST_ARCH=$(uname -m) UPX_VERSION=4.2.4
case $HOST_ARCH in curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
x86_64) UPX_ARCH="amd64" ;; cp upx-${UPX_VERSION}-amd64_linux/upx .
aarch64) UPX_ARCH="arm64" ;; ./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
*) UPX_ARCH="amd64" ;; ./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
esac
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 fi
for BIN in ./target/$TARGET/release/easytier-{core,cli,web,web-embed}"$SUFFIX"; do mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
if [[ -f "$BIN" ]]; then mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ -n "$UPX_BIN" ]]; then if [[ ! $TARGET =~ ^mips.*$ ]]; then
$UPX_BIN --lzma --best "$BIN" || true mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
fi mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
fi
mv "$BIN" ./artifacts/objects/
fi
done
mv ./artifacts/objects/* ./artifacts/ mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/ rm -rf ./artifacts/objects/
@@ -254,10 +273,25 @@ jobs:
path: | path: |
./artifacts/* ./artifacts/*
build_magisk: core-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build_web
- build
steps:
- name: Mark result as failed
if: needs.build.result != 'success'
run: exit 1
magisk_build:
needs:
- pre_job
- build_web
- build
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ pre_job, build_web, build ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置 uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
@@ -277,6 +311,7 @@ jobs:
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/ cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/ cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
# 上传生成的模块 # 上传生成的模块
- name: Upload Magisk Module - name: Upload Magisk Module
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
@@ -287,12 +322,3 @@ jobs:
!./easytier-contrib/easytier-magisk/build.sh !./easytier-contrib/easytier-magisk/build.sh
!./easytier-contrib/easytier-magisk/magisk_update.json !./easytier-contrib/easytier-magisk/magisk_update.json
if-no-files-found: error if-no-files-found: error
core-result:
runs-on: ubuntu-latest
needs: [ pre_job, build_web, build, build_magisk ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled()
steps:
- name: Mark result as failed
if: contains(needs.*.result, 'failure')
run: exit 1
+85 -38
View File
@@ -5,11 +5,6 @@ on:
branches: ["develop", "main", "releases/**"] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -23,7 +18,6 @@ jobs:
pre_job: pre_job:
# continue-on-error: true # Uncomment once integration is finished # continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }} should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
@@ -35,20 +29,20 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
cancel_others: 'true' cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh", "easytier-web/frontend-lib/**"]'
build-gui: build-gui:
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
include: include:
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-24.04
GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64
- TARGET: aarch64-unknown-linux-musl - TARGET: aarch64-unknown-linux-musl
OS: ubuntu-24.04-arm OS: ubuntu-22.04
GUI_TARGET: aarch64-unknown-linux-gnu GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64 ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-22.04
GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64
- TARGET: x86_64-apple-darwin - TARGET: x86_64-apple-darwin
OS: macos-latest OS: macos-latest
@@ -63,14 +57,16 @@ jobs:
OS: windows-latest OS: windows-latest
GUI_TARGET: x86_64-pc-windows-msvc GUI_TARGET: x86_64-pc-windows-msvc
ARTIFACT_NAME: windows-x86_64 ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc - TARGET: i686-pc-windows-msvc
OS: windows-latest OS: windows-latest
GUI_TARGET: i686-pc-windows-msvc GUI_TARGET: i686-pc-windows-msvc
ARTIFACT_NAME: windows-i686 ARTIFACT_NAME: windows-i686
- TARGET: aarch64-pc-windows-msvc
OS: windows-11-arm
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
runs-on: ${{ matrix.OS }} runs-on: ${{ matrix.OS }}
env: env:
@@ -84,29 +80,75 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: bash ./.github/workflows/install_gui_dep.sh
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: |
# see https://tauri.app/v1/guides/building/linux/
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt update
sudo apt install aptitude
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu libsoup-3.0-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- name: Install rpm package (Linux target only)
if: ${{ contains(matrix.TARGET, '-linux-') }}
run: |
sudo apt update
sudo apt install -y rpm
- name: Set current ref as env variable - name: Set current ref as env variable
run: | run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Prepare build environment - name: Setup Frontend Environment
uses: ./.github/actions/prepare-build uses: ./.github/actions/prepare-pnpm
with:
target: ${{ matrix.TARGET }}
gui: true
pnpm: true
pnpm-build-filter: ''
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
# The prefix cache key, this can be changed to start a new cache manually. # The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust" # default: "v0-rust"
prefix-key: "" prefix-key: ""
shared-key: "gui-registry"
cache-targets: "false" - name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: copy correct DLLs - name: copy correct DLLs
if: ${{ contains(matrix.GUI_TARGET, 'windows') }} if: ${{ matrix.OS == 'windows-latest' }}
run: | run: |
case $TARGET in case $TARGET in
x86_64*) ARCH_DIR=x86_64 ;; x86_64*) ARCH_DIR=x86_64 ;;
@@ -122,9 +164,10 @@ jobs:
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
with: with:
projectPath: ./easytier-gui projectPath: ./easytier-gui
args: --verbose --target ${{ matrix.GUI_TARGET }} # https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ contains(matrix.TARGET, '-linux-') && contains(matrix.TARGET, 'aarch64') && '--bundles deb,rpm' || '' }}
- name: Collect artifact - name: Compress
run: | run: |
mkdir -p ./artifacts/objects/ mkdir -p ./artifacts/objects/
@@ -133,16 +176,18 @@ jobs:
else else
TAG=$GITHUB_SHA TAG=$GITHUB_SHA
fi fi
# copy gui bundle, gui is built without specific target # copy gui bundle, gui is built without specific target
if [[ $GUI_TARGET =~ windows ]]; then if [[ $OS =~ ^windows.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/ mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
elif [[ $GUI_TARGET =~ darwin ]]; then elif [[ $OS =~ ^macos.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/ mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
elif [[ $GUI_TARGET =~ linux ]]; then elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/ mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/ mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./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
fi fi
mv ./artifacts/objects/* ./artifacts/ mv ./artifacts/objects/* ./artifacts/
@@ -156,10 +201,12 @@ jobs:
./artifacts/* ./artifacts/*
gui-result: gui-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ pre_job, build-gui ] needs:
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled() - pre_job
- build-gui
steps: steps:
- name: Mark result as failed - name: Mark result as failed
if: contains(needs.*.result, 'failure') if: needs.build-gui.result != 'success'
run: exit 1 run: exit 1
+11
View File
@@ -0,0 +1,11 @@
sudo apt update
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# env needed:
# - TARGET
# - GUI_TARGET
# - OS
# dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
# https://github.com/cross-tools/musl-cross/releases
# if "musl" is a substring of TARGET, we assume that we are using musl
MUSL_TARGET=$TARGET
# if target is mips or mipsel, we should use soft-float version of musl
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
MUSL_TARGET=${TARGET}sf
elif [[ $TARGET =~ ^riscv64gc-.*$ ]]; then
MUSL_TARGET=${TARGET/#riscv64gc-/riscv64-}
fi
if [[ $MUSL_TARGET =~ musl ]]; then
mkdir -p ./musl_gcc
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
fi
fi
# see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable
rustup install 1.93
rustup default 1.93
# mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a
# for mimalloc
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
rustup toolchain install nightly-2026-02-02-x86_64-unknown-linux-gnu
rustup component add rust-src --toolchain nightly-2026-02-02-x86_64-unknown-linux-gnu
# https://github.com/rust-lang/rust/issues/128808
# remove it after Cargo or rustc fix this.
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
fi
else
rustup target add $TARGET
if [[ $GUI_TARGET != '' ]]; then
rustup target add $GUI_TARGET
fi
fi
+37 -40
View File
@@ -5,11 +5,6 @@ on:
branches: ["develop", "main", "releases/**"] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -23,7 +18,6 @@ jobs:
pre_job: pre_job:
# continue-on-error: true # Uncomment once integration is finished # continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }} should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
@@ -35,25 +29,20 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
cancel_others: 'true' cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
build-mobile: build-mobile:
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
include: include:
- TARGET: aarch64-linux-android - TARGET: android
ARCH: aarch64 OS: ubuntu-22.04
- TARGET: armv7-linux-androideabi ARTIFACT_NAME: android
ARCH: armv7 runs-on: ${{ matrix.OS }}
- TARGET: i686-linux-android
ARCH: i686
- TARGET: x86_64-linux-android
ARCH: x86_64
runs-on: ubuntu-latest
env: env:
NAME: easytier NAME: easytier
TARGET: ${{ matrix.TARGET }} TARGET: ${{ matrix.TARGET }}
ARCH: ${{ matrix.ARCH }} OS: ${{ matrix.OS }}
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }} OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
needs: pre_job needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
@@ -72,41 +61,47 @@ jobs:
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
with: with:
cmdline-tools-version: 12.0 cmdline-tools-version: 11076708
packages: 'build-tools;34.0.0 ndk;26.0.10792818 platform-tools platforms;android-34 ' packages: 'build-tools;34.0.0 ndk;26.0.10792818 tools platform-tools platforms;android-34 '
- name: Setup Android Environment - name: Setup Android Environment
run: | run: |
echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" >> $GITHUB_ENV echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
- name: Prepare build environment - name: Setup Frontend Environment
uses: ./.github/actions/prepare-build uses: ./.github/actions/prepare-pnpm
with:
target: ${{ matrix.TARGET }}
gui: false
pnpm: true
pnpm-build-filter: ''
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
# The prefix cache key, this can be changed to start a new cache manually. # The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust" # default: "v0-rust"
prefix-key: "" prefix-key: ""
shared-key: "gui-registry"
cache-targets: "false"
- name: Build - name: Install rust target
run: |
bash ./.github/workflows/install_rust.sh
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add i686-linux-android
rustup target add x86_64-linux-android
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Android
run: | run: |
cd easytier-gui cd easytier-gui
pnpm tauri android build --apk --target "$ARCH" --split-per-abi pnpm tauri android build
- name: Collect artifact - name: Compress
run: | run: |
mkdir -p ./artifacts/objects/ mkdir -p ./artifacts/objects/
mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/*/release/*.apk ./artifacts/objects/ mv easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk ./artifacts/objects/
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME TAG=$GITHUB_REF_NAME
@@ -114,21 +109,23 @@ jobs:
TAG=$GITHUB_SHA TAG=$GITHUB_SHA
fi fi
mv ./artifacts/objects/* ./artifacts/ mv ./artifacts/objects/* ./artifacts
rm -rf ./artifacts/objects/ rm -rf ./artifacts/objects/
- name: Archive artifact - name: Archive artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v5
with: with:
name: easytier-mobile-android-${{ matrix.ARCH }} name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
path: | path: |
./artifacts/* ./artifacts/*
mobile-result: mobile-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ pre_job, build-mobile ] needs:
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled() - pre_job
- build-mobile
steps: steps:
- name: Mark result as failed - name: Mark result as failed
if: contains(needs.*.result, 'failure') if: needs.build-mobile.result != 'success'
run: exit 1 run: exit 1
+1 -15
View File
@@ -6,22 +6,14 @@ on:
paths: paths:
- "**/*.nix" - "**/*.nix"
- "flake.lock" - "flake.lock"
- "rust-toolchain.toml"
pull_request: pull_request:
branches: ["main", "develop"] branches: ["main", "develop"]
types: [opened, synchronize, reopened, ready_for_review]
paths: paths:
- "**/*.nix" - "**/*.nix"
- "flake.lock" - "flake.lock"
- "rust-toolchain.toml"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
check-full-shell: check-full-shell:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -34,11 +26,5 @@ jobs:
- name: Magic Nix Cache - name: Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v6 uses: DeterminateSystems/magic-nix-cache-action@v6
- name: Warm up full devShell - name: Check full devShell
run: nix develop .#full --command true run: nix develop .#full --command true
- name: Cargo check in flake environment
run: nix develop .#full --command cargo check
- name: Cargo build in flake environment
run: nix develop .#full --command cargo build
+12 -33
View File
@@ -8,13 +8,8 @@ on:
- '!*-pre' - '!*-pre'
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -25,29 +20,18 @@ defaults:
jobs: jobs:
cargo_fmt_check: cargo_fmt_check:
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: fmt check
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
gui: false
pnpm: false
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
- name: Check formatting
working-directory: ./easytier-contrib/easytier-ohrs working-directory: ./easytier-contrib/easytier-ohrs
run: cargo fmt --all -- --check run: |
bash ../../.github/workflows/install_rust.sh
rustup component add rustfmt
cargo fmt --all -- --check
pre_job: pre_job:
# continue-on-error: true # Uncomment once integration is finished # continue-on-error: true # Uncomment once integration is finished
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
# do not skip push on branch starts with releases/ # do not skip push on branch starts with releases/
@@ -60,8 +44,7 @@ jobs:
concurrent_skipping: "same_content_newer" concurrent_skipping: "same_content_newer"
skip_after_successful_duplicate: "true" skip_after_successful_duplicate: "true"
cancel_others: "true" cancel_others: "true"
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/actions/**"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
build-ohos: build-ohos:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_job needs: pre_job
@@ -73,12 +56,13 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -qq \ sudo apt-get install -y \
build-essential \ build-essential \
wget \ wget \
unzip \ unzip \
git \ git \
pkg-config curl libgl1-mesa-dev expect pkg-config curl libgl1-mesa-dev expect
sudo apt-get clean
- name: Resolve easytier version - name: Resolve easytier version
run: | run: |
@@ -150,15 +134,6 @@ jobs:
run: | run: |
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
rustup install stable
rustup default stable
rustup target add aarch64-unknown-linux-ohos
- uses: taiki-e/install-action@v2
with:
tool: ohrs
- name: Create clang wrapper script - name: Create clang wrapper script
run: | run: |
sudo mkdir -p $OHOS_NDK_HOME/native/llvm sudo mkdir -p $OHOS_NDK_HOME/native/llvm
@@ -177,7 +152,11 @@ jobs:
run: | run: |
sudo apt-get install -y llvm clang lldb lld sudo apt-get install -y llvm clang lldb lld
sudo apt-get install -y protobuf-compiler sudo apt-get install -y protobuf-compiler
bash ../../.github/workflows/install_rust.sh
source env.sh source env.sh
cargo install ohrs
rustup target add aarch64-unknown-linux-ohos
cargo update easytier
ohrs doctor ohrs doctor
ohrs build --release --arch aarch ohrs build --release --arch aarch
ohrs artifact ohrs artifact
+12 -14
View File
@@ -6,10 +6,6 @@ on:
pull_request: pull_request:
branches: [ "develop", "main" ] branches: [ "develop", "main" ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
# RUSTC_WRAPPER: "sccache" # RUSTC_WRAPPER: "sccache"
@@ -34,7 +30,7 @@ jobs:
# All of these options are optional, so you can remove them if you are happy with the defaults # All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never' concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/actions/**"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
check: check:
name: Run linters & check name: Run linters & check
@@ -48,13 +44,15 @@ jobs:
uses: ./.github/actions/prepare-build uses: ./.github/actions/prepare-build
with: with:
gui: true gui: true
pnpm: true web: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: Swatinem/rust-cache@v2
with:
components: rustfmt,clippy - name: Install rustfmt and clippy
rustflags: '' run: |
rustup component add rustfmt
rustup component add clippy
- uses: taiki-e/install-action@cargo-hack - uses: taiki-e/install-action@cargo-hack
@@ -87,7 +85,7 @@ jobs:
uses: ./.github/actions/prepare-build uses: ./.github/actions/prepare-build
with: with:
gui: true gui: true
pnpm: true web: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@@ -148,9 +146,9 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ pre_job, check, test_matrix ] needs: [ pre_job, test_matrix ]
if: needs.pre_job.result == 'success' && needs.pre_job.outputs.should_skip != 'true' && !cancelled() if: needs.pre_job.outputs.should_skip != 'true' && always()
steps: steps:
- name: Mark result as failed - name: Mark result as failed
if: contains(needs.*.result, 'failure') if: needs.test_matrix.result != 'success'
run: exit 1 run: exit 1
+3 -3
View File
@@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides
#### Required Tools #### Required Tools
- Node.js v21 or higher - Node.js v21 or higher
- pnpm v9 or higher - pnpm v9 or higher
- Rust toolchain (version 1.95) - Rust toolchain (version 1.93)
- LLVM and Clang - LLVM and Clang
- Protoc (Protocol Buffers compiler) - Protoc (Protocol Buffers compiler)
@@ -79,8 +79,8 @@ sudo apt install -y bridge-utils
2. Install dependencies: 2. Install dependencies:
```bash ```bash
# Install Rust toolchain # Install Rust toolchain
rustup install 1.95 rustup install 1.93
rustup default 1.95 rustup default 1.93
# Install project dependencies # Install project dependencies
pnpm -r install pnpm -r install
+3 -3
View File
@@ -34,7 +34,7 @@
#### 必需工具 #### 必需工具
- Node.js v21 或更高版本 - Node.js v21 或更高版本
- pnpm v9 或更高版本 - pnpm v9 或更高版本
- Rust 工具链(版本 1.95 - Rust 工具链(版本 1.93
- LLVM 和 Clang - LLVM 和 Clang
- ProtocProtocol Buffers 编译器) - ProtocProtocol Buffers 编译器)
@@ -87,8 +87,8 @@ sudo apt install -y bridge-utils
2. 安装依赖: 2. 安装依赖:
```bash ```bash
# 安装 Rust 工具链 # 安装 Rust 工具链
rustup install 1.95 rustup install 1.93
rustup default 1.95 rustup default 1.93
# 安装项目依赖 # 安装项目依赖
pnpm -r install pnpm -r install
Generated
+984 -1155
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -14,10 +14,6 @@ exclude = [
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk "easytier-contrib/easytier-ohrs", # it needs ohrs sdk
] ]
[workspace.package]
edition = "2024"
rust-version = "1.95"
[profile.dev] [profile.dev]
panic = "unwind" panic = "unwind"
debug = 2 debug = 2
@@ -1,7 +1,7 @@
[package] [package]
name = "easytier-android-jni" name = "easytier-android-jni"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition = "2021"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -1,7 +1,7 @@
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap}; use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
use jni::JNIEnv;
use jni::objects::{JClass, JObjectArray, JString}; use jni::objects::{JClass, JObjectArray, JString};
use jni::sys::{jint, jstring}; use jni::sys::{jint, jstring};
use jni::JNIEnv;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::ptr; use std::ptr;
@@ -15,7 +15,7 @@ pub struct KeyValuePair {
} }
// 声明外部 C 函数 // 声明外部 C 函数
unsafe extern "C" { extern "C" {
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int; fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
fn get_error_msg(out: *mut *const std::ffi::c_char); fn get_error_msg(out: *mut *const std::ffi::c_char);
fn free_string(s: *const std::ffi::c_char); fn free_string(s: *const std::ffi::c_char);
@@ -68,7 +68,7 @@ fn throw_exception(env: &mut JNIEnv, message: &str) {
} }
/// 设置 TUN 文件描述符 /// 设置 TUN 文件描述符
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
@@ -87,17 +87,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
unsafe { unsafe {
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd); let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
result result
} }
} }
/// 解析配置 /// 解析配置
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
@@ -115,17 +115,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
unsafe { unsafe {
let result = parse_config(config_cstr.as_ptr()); let result = parse_config(config_cstr.as_ptr());
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
result result
} }
} }
/// 运行网络实例 /// 运行网络实例
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
@@ -143,17 +143,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
unsafe { unsafe {
let result = run_network_instance(config_cstr.as_ptr()); let result = run_network_instance(config_cstr.as_ptr());
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
result result
} }
} }
/// 保持网络实例 /// 保持网络实例
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
@@ -165,10 +165,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
if instance_names.is_null() { if instance_names.is_null() {
unsafe { unsafe {
let result = retain_network_instance(ptr::null(), 0); let result = retain_network_instance(ptr::null(), 0);
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
return result; return result;
} }
@@ -187,10 +187,10 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
if array_length == 0 { if array_length == 0 {
unsafe { unsafe {
let result = retain_network_instance(ptr::null(), 0); let result = retain_network_instance(ptr::null(), 0);
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
return result; return result;
} }
@@ -234,17 +234,17 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
unsafe { unsafe {
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len()); let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
if result != 0 if result != 0 {
&& let Some(error) = get_last_error() if let Some(error) = get_last_error() {
{ throw_exception(&mut env, &error);
throw_exception(&mut env, &error); }
} }
result result
} }
} }
/// 收集网络信息 /// 收集网络信息
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
mut env: JNIEnv, mut env: JNIEnv,
_class: JClass, _class: JClass,
@@ -304,7 +304,7 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
} }
/// 获取最后的错误信息 /// 获取最后的错误信息
#[unsafe(no_mangle)] #[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError( pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
env: JNIEnv, env: JNIEnv,
_class: JClass, _class: JClass,
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "easytier-ffi" name = "easytier-ffi"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition = "2021"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
+7 -7
View File
@@ -30,7 +30,7 @@ fn set_error_msg(msg: &str) {
/// # Safety /// # Safety
/// Set the tun fd /// Set the tun fd
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn set_tun_fd( pub unsafe extern "C" fn set_tun_fd(
inst_name: *const std::ffi::c_char, inst_name: *const std::ffi::c_char,
fd: std::ffi::c_int, fd: std::ffi::c_int,
@@ -59,7 +59,7 @@ pub unsafe extern "C" fn set_tun_fd(
/// # Safety /// # Safety
/// Get the last error message /// Get the last error message
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) { pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
let msg_buf = ERROR_MSG.lock().unwrap(); let msg_buf = ERROR_MSG.lock().unwrap();
if msg_buf.is_empty() { if msg_buf.is_empty() {
@@ -74,7 +74,7 @@ pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
} }
} }
#[unsafe(no_mangle)] #[no_mangle]
pub extern "C" fn free_string(s: *const std::ffi::c_char) { pub extern "C" fn free_string(s: *const std::ffi::c_char) {
if s.is_null() { if s.is_null() {
return; return;
@@ -86,7 +86,7 @@ pub extern "C" fn free_string(s: *const std::ffi::c_char) {
/// # Safety /// # Safety
/// Parse the config /// Parse the config
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int { pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe { let cfg_str = unsafe {
assert!(!cfg_str.is_null()); assert!(!cfg_str.is_null());
@@ -105,7 +105,7 @@ pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::
/// # Safety /// # Safety
/// Run the network instance /// Run the network instance
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int { pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe { let cfg_str = unsafe {
assert!(!cfg_str.is_null()); assert!(!cfg_str.is_null());
@@ -144,7 +144,7 @@ pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char)
/// # Safety /// # Safety
/// Retain the network instance /// Retain the network instance
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn retain_network_instance( pub unsafe extern "C" fn retain_network_instance(
inst_names: *const *const std::ffi::c_char, inst_names: *const *const std::ffi::c_char,
length: usize, length: usize,
@@ -188,7 +188,7 @@ pub unsafe extern "C" fn retain_network_instance(
/// # Safety /// # Safety
/// Collect the network infos /// Collect the network infos
#[unsafe(no_mangle)] #[no_mangle]
pub unsafe extern "C" fn collect_network_infos( pub unsafe extern "C" fn collect_network_infos(
infos: *mut KeyValuePair, infos: *mut KeyValuePair,
max_length: usize, max_length: usize,
+1 -1
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "easytier-uptime" name = "easytier-uptime"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition = "2021"
[dependencies] [dependencies]
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }
@@ -1,7 +1,7 @@
use std::ops::{Div, Mul}; use std::ops::{Div, Mul};
use axum::Json;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::Json;
use sea_orm::{ use sea_orm::{
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel, QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel,
@@ -14,7 +14,7 @@ use crate::api::{
models::*, models::*,
}; };
use crate::db::entity::{self, health_records, shared_nodes}; use crate::db::entity::{self, health_records, shared_nodes};
use crate::db::{Db, operations::*}; use crate::db::{operations::*, Db};
use crate::health_checker_manager::HealthCheckerManager; use crate::health_checker_manager::HealthCheckerManager;
use axum_extra::extract::Query; use axum_extra::extract::Query;
use std::sync::Arc; use std::sync::Arc;
@@ -273,7 +273,7 @@ pub struct InstanceFilterParams {
use crate::config::AppConfig; use crate::config::AppConfig;
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::Serialize; use serde::Serialize;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -370,19 +370,19 @@ pub async fn admin_get_nodes(
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?; let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
filtered_ids = Some(ids); filtered_ids = Some(ids);
} }
if let Some(tags) = filters.tags if let Some(tags) = filters.tags {
&& !tags.is_empty() if !tags.is_empty() {
{ let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?; filtered_ids = match filtered_ids {
filtered_ids = match filtered_ids { Some(mut existing) => {
Some(mut existing) => { existing.extend(ids_any);
existing.extend(ids_any); existing.sort();
existing.sort(); existing.dedup();
existing.dedup(); Some(existing)
Some(existing) }
} None => Some(ids_any),
None => Some(ids_any), };
}; }
} }
if let Some(ids) = filtered_ids { if let Some(ids) = filtered_ids {
if ids.is_empty() { if ids.is_empty() {
@@ -1,5 +1,5 @@
use axum::Router;
use axum::routing::{delete, get, post, put}; use axum::routing::{delete, get, post, put};
use axum::Router;
use tower_http::compression::CompressionLayer; use tower_http::compression::CompressionLayer;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
@@ -1,7 +1,7 @@
use crate::db::Db;
use crate::db::entity::*; use crate::db::entity::*;
use crate::db::Db;
use sea_orm::*; use sea_orm::*;
use tokio::time::{Duration, sleep}; use tokio::time::{sleep, Duration};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
/// 数据清理策略配置 /// 数据清理策略配置
@@ -5,12 +5,12 @@ pub mod operations;
use std::fmt; use std::fmt;
use sea_orm::{ use sea_orm::{
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set, prelude::*, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
SqlxSqliteConnector, Statement, TransactionTrait as _, prelude::*, sea_query::OnConflict, QueryFilter as _, Set, SqlxSqliteConnector, Statement, TransactionTrait as _,
}; };
use sea_orm_migration::MigratorTrait as _; use sea_orm_migration::MigratorTrait as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _}; use sqlx::{migrate::MigrateDatabase as _, Sqlite, SqlitePool};
use crate::migrator; use crate::migrator;
@@ -1,8 +1,8 @@
use crate::api::CreateNodeRequest; use crate::api::CreateNodeRequest;
use crate::db::entity::*;
use crate::db::Db; use crate::db::Db;
use crate::db::HealthStats; use crate::db::HealthStats;
use crate::db::HealthStatus; use crate::db::HealthStatus;
use crate::db::entity::*;
use sea_orm::*; use sea_orm::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -19,9 +19,9 @@ use sqlx::any;
use tracing::{debug, error, info, instrument, warn}; use tracing::{debug, error, info, instrument, warn};
use crate::db::{ use crate::db::{
Db, HealthStatus,
entity::shared_nodes, entity::shared_nodes,
operations::{HealthOperations, NodeOperations}, operations::{HealthOperations, NodeOperations},
Db, HealthStatus,
}; };
pub struct HealthCheckOneNode { pub struct HealthCheckOneNode {
@@ -1,11 +1,11 @@
use std::{collections::HashSet, sync::Arc, time::Duration}; use std::{collections::HashSet, sync::Arc, time::Duration};
use anyhow::Context as _; use anyhow::Context as _;
use tokio::time::{Interval, interval}; use tokio::time::{interval, Interval};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
db::{Db, entity::shared_nodes, operations::NodeOperations}, db::{entity::shared_nodes, operations::NodeOperations, Db},
health_checker::HealthChecker, health_checker::HealthChecker,
}; };
+2 -4
View File
@@ -10,7 +10,7 @@ mod migrator;
use api::routes::create_routes; use api::routes::create_routes;
use clap::Parser; use clap::Parser;
use config::AppConfig; use config::AppConfig;
use db::{Db, operations::NodeOperations}; use db::{operations::NodeOperations, Db};
use easytier::common::log; use easytier::common::log;
use health_checker::HealthChecker; use health_checker::HealthChecker;
use health_checker_manager::HealthCheckerManager; use health_checker_manager::HealthCheckerManager;
@@ -49,9 +49,7 @@ async fn main() -> anyhow::Result<()> {
// 如果提供了管理员密码,设置环境变量 // 如果提供了管理员密码,设置环境变量
if let Some(password) = args.admin_password { if let Some(password) = args.admin_password {
unsafe { env::set_var("ADMIN_PASSWORD", password);
env::set_var("ADMIN_PASSWORD", password);
}
} }
tracing::info!( tracing::info!(
+10 -9
View File
@@ -3,7 +3,7 @@ name = "easytier-gui"
version = "2.6.0" version = "2.6.0"
description = "EasyTier GUI" description = "EasyTier GUI"
authors = ["you"] authors = ["you"]
edition.workspace = true edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -11,6 +11,15 @@ edition.workspace = true
name = "app_lib" name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }
# enable thunk-rs when compiling for x86_64 or i686 windows
[target.x86_64-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[target.i686-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[dependencies] [dependencies]
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527 # wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
@@ -57,14 +66,6 @@ libc = "0.2"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
security-framework-sys = "2.9.0" security-framework-sys = "2.9.0"
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
+5 -5
View File
@@ -1,10 +1,10 @@
use std::env;
fn main() { fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
// enable thunk-rs when target os is windows and arch is x86_64 or i686 // enable thunk-rs when target os is windows and arch is x86_64 or i686
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") { #[cfg(target_os = "windows")]
if !std::env::var("TARGET")
.unwrap_or_default()
.contains("aarch64")
{
thunk::thunk(); thunk::thunk();
} }
+1 -1
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
use super::Command; use super::Command;
use anyhow::{Result, anyhow}; use anyhow::{anyhow, Result};
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::process::{Command as StdCommand, Output}; use std::process::{Command as StdCommand, Output};
+2 -2
View File
@@ -30,10 +30,10 @@ use std::os::unix::process::ExitStatusExt;
use std::path::Path; use std::path::Path;
use std::ptr; use std::ptr;
use libc::{EINTR, SHUT_WR, fileno, wait}; use libc::{fileno, wait, EINTR, SHUT_WR};
use security_framework_sys::authorization::{ use security_framework_sys::authorization::{
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights, errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
}; };
const ENV_PATH: &str = "PATH"; const ENV_PATH: &str = "PATH";
@@ -11,11 +11,11 @@ use std::process::{ExitStatus, Output};
use winapi::shared::minwindef::{DWORD, LPVOID}; use winapi::shared::minwindef::{DWORD, LPVOID};
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken}; use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
use winapi::um::securitybaseapi::GetTokenInformation; use winapi::um::securitybaseapi::GetTokenInformation;
use winapi::um::winnt::{HANDLE, TOKEN_ELEVATION, TOKEN_QUERY, TokenElevation}; use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY};
use windows::core::{w, HSTRING, PCWSTR};
use windows::Win32::Foundation::HWND; use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Shell::ShellExecuteW; use windows::Win32::UI::Shell::ShellExecuteW;
use windows::Win32::UI::WindowsAndMessaging::SW_HIDE; use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
use windows::core::{HSTRING, PCWSTR, w};
/// The implementation of state check and elevated executing varies on each platform /// The implementation of state check and elevated executing varies on each platform
impl Command { impl Command {
+36 -36
View File
@@ -21,10 +21,10 @@ use easytier::{
instance_manager::NetworkInstanceManager, instance_manager::NetworkInstanceManager,
launcher::NetworkConfig, launcher::NetworkConfig,
rpc_service::ApiRpcServer, rpc_service::ApiRpcServer,
tunnel::TunnelListener,
tunnel::ring::RingTunnelListener, tunnel::ring::RingTunnelListener,
tunnel::tcp::TcpTunnelListener, tunnel::tcp::TcpTunnelListener,
utils::panic::setup_panic_handler, tunnel::TunnelListener,
utils::{self},
}; };
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
@@ -559,10 +559,10 @@ fn toggle_window_visibility(app: &tauri::AppHandle) {
} }
fn get_exe_path() -> String { fn get_exe_path() -> String {
if let Ok(appimage_path) = std::env::var("APPIMAGE") if let Ok(appimage_path) = std::env::var("APPIMAGE") {
&& !appimage_path.is_empty() if !appimage_path.is_empty() {
{ return appimage_path;
return appimage_path; }
} }
std::env::current_exe() std::env::current_exe()
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
@@ -596,8 +596,8 @@ mod manager {
use easytier::proto::rpc_types::controller::BaseController; use easytier::proto::rpc_types::controller::BaseController;
use easytier::rpc_service::logger::LoggerRpcService; use easytier::rpc_service::logger::LoggerRpcService;
use easytier::rpc_service::remote_client::PersistentConfig; use easytier::rpc_service::remote_client::PersistentConfig;
use easytier::tunnel::TunnelConnector;
use easytier::tunnel::ring::RingTunnelConnector; use easytier::tunnel::ring::RingTunnelConnector;
use easytier::tunnel::TunnelConnector;
use easytier::web_client::WebClientHooks; use easytier::web_client::WebClientHooks;
pub(super) struct GuiHooks { pub(super) struct GuiHooks {
@@ -979,34 +979,34 @@ mod manager {
.get_rpc_client(app.clone()) .get_rpc_client(app.clone())
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?; .ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
for id in enabled_networks { for id in enabled_networks {
if let Ok(uuid) = id.parse() if let Ok(uuid) = id.parse() {
&& !self.storage.enabled_networks.contains(&uuid) if !self.storage.enabled_networks.contains(&uuid) {
{ let config = self
let config = self .storage
.storage .network_configs
.network_configs .get(&uuid)
.get(&uuid) .map(|i| i.value().1.clone());
.map(|i| i.value().1.clone()); let Some(config) = config else {
let Some(config) = config else { continue;
continue; };
}; let toml_config = config.gen_config()?;
let toml_config = config.gen_config()?; self.pre_run_network_instance_hook(&app, &toml_config)
self.pre_run_network_instance_hook(&app, &toml_config) .await
.await .map_err(|e| anyhow::anyhow!(e))?;
.map_err(|e| anyhow::anyhow!(e))?; client
client .run_network_instance(
.run_network_instance( BaseController::default(),
BaseController::default(), RunNetworkInstanceRequest {
RunNetworkInstanceRequest { inst_id: None,
inst_id: None, config: Some(config),
config: Some(config), overwrite: false,
overwrite: false, },
}, )
) .await?;
.await?; self.post_run_network_instance_hook(&app, &uuid)
self.post_run_network_instance_hook(&app, &uuid) .await
.await .map_err(|e| anyhow::anyhow!(e))?;
.map_err(|e| anyhow::anyhow!(e))?; }
} }
} }
Ok(()) Ok(())
@@ -1120,7 +1120,7 @@ pub fn run_gui() -> std::process::ExitCode {
process::exit(0); process::exit(0);
} }
setup_panic_handler(); utils::setup_panic_handler();
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
+2 -1
View File
@@ -2,12 +2,13 @@
name = "easytier-rpc-build" name = "easytier-rpc-build"
description = "Protobuf RPC Service Generator for EasyTier" description = "Protobuf RPC Service Generator for EasyTier"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition = "2021"
homepage = "https://github.com/EasyTier/EasyTier" homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier"
authors = ["kkrainbow"] authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"] keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"] categories = ["network-programming", "command-line-utilities"]
rust-version = "1.93.0"
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"
+8 -6
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "easytier-web" name = "easytier-web"
version = "2.6.0" version = "2.6.0"
edition.workspace = true edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server." description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
[dependencies] [dependencies]
@@ -69,11 +69,13 @@ subtle = "2.6"
mimalloc = { version = "*" } mimalloc = { version = "*" }
[build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
[features] [features]
default = [] default = []
embed = ["dep:axum-embed"] embed = ["dep:axum-embed"]
# enable thunk-rs when compiling for x86_64 or i686 windows
[target.x86_64-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[target.i686-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
+5 -5
View File
@@ -1,10 +1,10 @@
use std::env;
fn main() { fn main() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
// enable thunk-rs when target os is windows and arch is x86_64 or i686 // enable thunk-rs when target os is windows and arch is x86_64 or i686
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") { #[cfg(target_os = "windows")]
if !std::env::var("TARGET")
.unwrap_or_default()
.contains("aarch64")
{
thunk::thunk(); thunk::thunk();
} }
} }
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { AutoComplete, Button, Checkbox, Dialog, Divider, InputNumber, InputText, Panel, Password, SelectButton, ToggleButton } from 'primevue'
import InputGroup from 'primevue/inputgroup' import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon' import InputGroupAddon from 'primevue/inputgroupaddon'
import { Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
import { import {
addRow, addRow,
DEFAULT_NETWORK_CONFIG, DEFAULT_NETWORK_CONFIG,
@@ -11,7 +11,6 @@ import {
} from '../types/network' } from '../types/network'
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import AclManager from './acl/AclManager.vue'
import UrlListInput from './UrlListInput.vue' import UrlListInput from './UrlListInput.vue'
const props = defineProps<{ const props = defineProps<{
@@ -489,18 +488,6 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
</div> </div>
</Panel> </Panel>
<Divider />
<Panel :header="t('acl.title')" toggleable collapsed>
<div v-if="curNetwork.acl" class="flex flex-col gap-y-2">
<AclManager v-model="curNetwork.acl" />
</div>
<div v-else class="flex justify-center p-4">
<Button :label="t('acl.enabled')"
@click="curNetwork.acl = { acl_v1: { chains: [], group: { declares: [], members: [] } } }" />
</div>
</Panel>
<div class="flex pt-6 justify-center"> <div class="flex pt-6 justify-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid" <Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)" /> @click="$emit('runNetwork', curNetwork)" />
@@ -1,218 +0,0 @@
<script setup lang="ts">
import { Button, Column, DataTable, Divider, InputText, Select, SelectButton, ToggleButton } from 'primevue'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { AclAction, AclChain, AclChainType, AclProtocol, AclRule } from '../../types/network'
import AclRuleDialog from './AclRuleDialog.vue'
const props = defineProps<{
groupNames?: string[]
}>()
const chain = defineModel<AclChain>({ required: true })
const { t } = useI18n()
watch(() => chain.value.rules, (newRules) => {
if (!newRules) return
const isSorted = newRules.every((rule, i) => i === 0 || (rule.priority || 0) <= (newRules[i - 1].priority || 0))
if (!isSorted) {
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
}
}, { deep: true, immediate: true })
const actionOptions = [
{ label: () => t('acl.allow'), value: AclAction.Allow },
{ label: () => t('acl.drop'), value: AclAction.Drop },
]
const chainTypeOptions = [
{ label: () => t('acl.inbound'), value: AclChainType.Inbound },
{ label: () => t('acl.outbound'), value: AclChainType.Outbound },
{ label: () => t('acl.forward'), value: AclChainType.Forward },
]
const editingRule = ref<AclRule | null>(null)
const editingRuleIndex = ref(-1)
const showRuleDialog = ref(false)
function getProtocolLabel(proto: AclProtocol) {
switch (proto) {
case AclProtocol.Any: return t('acl.any')
case AclProtocol.TCP: return 'TCP'
case AclProtocol.UDP: return 'UDP'
case AclProtocol.ICMP: return 'ICMP'
case AclProtocol.ICMPv6: return 'ICMPv6'
default: return t('event.Unknown')
}
}
function getActionLabel(action: AclAction) {
switch (action) {
case AclAction.Allow: return t('acl.allow')
case AclAction.Drop: return t('acl.drop')
default: return t('event.Unknown')
}
}
function addRule() {
editingRuleIndex.value = -1
editingRule.value = {
name: '',
description: '',
priority: chain.value.rules.length,
enabled: true,
protocol: AclProtocol.Any,
ports: [],
source_ips: [],
destination_ips: [],
source_ports: [],
action: AclAction.Allow,
rate_limit: 0,
burst_limit: 0,
stateful: false,
source_groups: [],
destination_groups: [],
}
showRuleDialog.value = true
}
function editRule(index: number) {
editingRuleIndex.value = index
editingRule.value = JSON.parse(JSON.stringify(chain.value.rules[index]))
showRuleDialog.value = true
}
function deleteRule(index: number) {
chain.value.rules.splice(index, 1)
}
function saveRule(rule: AclRule) {
if (editingRuleIndex.value === -1) {
chain.value.rules.push(rule)
} else {
chain.value.rules[editingRuleIndex.value] = rule
}
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
}
function onRowReorder(event: any) {
chain.value.rules = event.value
// Update priorities based on new order (higher priority at top)
chain.value.rules.forEach((rule, index) => {
rule.priority = chain.value.rules.length - index - 1
})
}
</script>
<template>
<div class="flex flex-col gap-6">
<!-- Chain Metadata Section -->
<div
class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-900 dark:border-gray-700">
<div class="flex flex-col gap-2">
<label class="font-bold text-sm">{{ t('acl.chain.name') }}</label>
<InputText v-model="chain.name" size="small" />
</div>
<div class="flex flex-col gap-2">
<label class="font-bold text-sm">{{ t('acl.rule.description') }}</label>
<InputText v-model="chain.description" size="small" />
</div>
<div class="flex items-center gap-6 col-span-full border-t pt-2 mt-2 dark:border-gray-700">
<div class="flex items-center gap-2">
<label class="font-bold text-sm">{{ t('acl.rule.enabled') }}</label>
<ToggleButton v-model="chain.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
</div>
<div class="flex items-center gap-2">
<label class="font-bold text-sm">{{ t('acl.chain.type') }}</label>
<Select v-model="chain.chain_type" :options="chainTypeOptions" :option-label="opt => opt.label()"
option-value="value" size="small" class="w-40" />
</div>
<div class="flex items-center gap-2 ml-auto">
<label class="font-bold text-sm">{{ t('acl.default_action') }}</label>
<SelectButton v-model="chain.default_action" :options="actionOptions" :option-label="opt => opt.label()"
option-value="value" :allow-empty="false" />
</div>
</div>
</div>
<div class="flex flex-row items-center gap-4 justify-between">
<h4 class="text-md font-bold">{{ t('acl.rules') }}</h4>
<Button icon="pi pi-plus" :label="t('acl.add_rule')" severity="success" size="small" @click="addRule" />
</div>
<DataTable :value="chain.rules" @row-reorder="onRowReorder" responsiveLayout="scroll">
<Column rowReorder headerStyle="width: 3rem" />
<Column field="enabled" :header="t('acl.rule.enabled')">
<template #body="{ data }">
<i class="pi" :class="data.enabled ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'"></i>
</template>
</Column>
<Column field="name" :header="t('acl.rule.name')" />
<Column :header="t('acl.match')">
<template #body="{ data }">
<div class="flex flex-col gap-2 py-1">
<div class="flex items-center gap-2">
<span
class="px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md text-[10px] font-bold uppercase tracking-wider">
{{ getProtocolLabel(data.protocol) }}
</span>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3">
<div class="flex items-center gap-1.5 min-w-0">
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Src</span>
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
<span v-for="ip in data.source_ips" :key="ip"
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
<span v-for="grp in data.source_groups" :key="grp"
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
<span v-if="data.source_ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
data.source_ports.join(',') }}</span>
<span v-if="!data.source_ips.length && !data.source_groups.length" class="text-gray-400">*</span>
</div>
</div>
<i class="pi pi-arrow-right hidden sm:block text-gray-300 text-xs"></i>
<Divider layout="horizontal" class="sm:hidden my-1" />
<div class="flex items-center gap-1.5 min-w-0">
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Dst</span>
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
<span v-for="ip in data.destination_ips" :key="ip"
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
<span v-for="grp in data.destination_groups" :key="grp"
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
<span v-if="data.ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
data.ports.join(',') }}</span>
<span v-if="!data.destination_ips.length && !data.destination_groups.length"
class="text-gray-400">*</span>
</div>
</div>
</div>
</div>
</template>
</Column>
<Column field="action" :header="t('acl.rule.action')">
<template #body="{ data }">
<span :class="data.action === AclAction.Allow ? 'text-green-600' : 'text-red-600 font-bold'">
{{ getActionLabel(data.action) }}
</span>
</template>
</Column>
<Column :header="t('web.common.edit')">
<template #body="{ index }">
<div class="flex gap-2">
<Button icon="pi pi-pencil" text rounded @click="editRule(index)" />
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteRule(index)" />
</div>
</template>
</Column>
</DataTable>
<AclRuleDialog v-if="showRuleDialog && editingRule" v-model:visible="showRuleDialog" v-model:rule="editingRule"
:group-names="props.groupNames" @save="saveRule" />
</div>
</template>
@@ -1,115 +0,0 @@
<script setup lang="ts">
import { Button, Column, DataTable, Dialog, InputText, MultiSelect, Password } from 'primevue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { GroupIdentity, GroupInfo } from '../../types/network';
const props = defineProps<{
groupNames?: string[]
}>()
const group = defineModel<GroupInfo>({ required: true })
const emit = defineEmits(['rename-group'])
const { t } = useI18n()
const editingGroup = ref<GroupIdentity | null>(null)
const editingGroupIndex = ref(-1)
const showGroupDialog = ref(false)
const oldGroupName = ref('')
function addGroup() {
editingGroupIndex.value = -1
editingGroup.value = {
group_name: '',
group_secret: '',
}
oldGroupName.value = ''
showGroupDialog.value = true
}
function editGroup(index: number) {
editingGroupIndex.value = index
editingGroup.value = JSON.parse(JSON.stringify(group.value.declares[index]))
oldGroupName.value = editingGroup.value?.group_name || ''
showGroupDialog.value = true
}
function deleteGroup(index: number) {
group.value.declares.splice(index, 1)
}
function saveGroup() {
if (!editingGroup.value) return
const newName = editingGroup.value.group_name
if (editingGroupIndex.value === -1) {
group.value.declares.push(editingGroup.value)
} else {
if (oldGroupName.value && oldGroupName.value !== newName) {
// Sync in members
group.value.members = group.value.members.map(m => m === oldGroupName.value ? newName : m)
// Notify parent to sync in rules
emit('rename-group', { oldName: oldGroupName.value, newName })
}
group.value.declares[editingGroupIndex.value] = editingGroup.value
}
showGroupDialog.value = false
}
</script>
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center">
<div class="flex flex-col">
<label class="font-bold text-lg">{{ t('acl.group.declares') }}</label>
<small class="text-gray-500">{{ t('acl.group.help') }}</small>
</div>
<Button icon="pi pi-plus" :label="t('web.common.add')" severity="success" @click="addGroup" />
</div>
<DataTable :value="group.declares" responsiveLayout="scroll">
<Column field="group_name" :header="t('acl.group.name')" />
<Column field="group_secret" :header="t('acl.group.secret')">
<template #body="{ data }">
<Password v-model="data.group_secret" :feedback="false" toggleMask readonly plain class="w-full" />
</template>
</Column>
<Column :header="t('web.common.edit')" headerStyle="width: 8rem">
<template #body="{ index }">
<div class="flex gap-2">
<Button icon="pi pi-pencil" text rounded @click="editGroup(index)" />
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteGroup(index)" />
</div>
</template>
</Column>
</DataTable>
</div>
<div class="flex flex-col gap-2">
<label class="font-bold text-lg">{{ t('acl.group.members') }}</label>
<MultiSelect v-model="group.members" :options="props.groupNames" multiple fluid filter
:placeholder="t('acl.group.members')" />
</div>
<!-- Group Identity Dialog -->
<Dialog v-model:visible="showGroupDialog" modal :header="t('acl.groups')" :style="{ width: '400px' }">
<div v-if="editingGroup" class="flex flex-col gap-4 pt-2">
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.group.name') }}</label>
<InputText v-model="editingGroup.group_name" fluid />
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.group.secret') }}</label>
<Password v-model="editingGroup.group_secret" :feedback="false" toggleMask fluid />
</div>
</div>
<template #footer>
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="showGroupDialog = false" text />
<Button :label="t('web.common.save')" icon="pi pi-save" @click="saveGroup" />
</template>
</Dialog>
</div>
</template>
@@ -1,150 +0,0 @@
<script setup lang="ts">
import { Button, Menu, Tab, TabList, TabPanel, TabPanels, Tabs } from 'primevue'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Acl, AclAction, AclChainType } from '../../types/network'
import AclChainEditor from './AclChainEditor.vue'
import AclGroupEditor from './AclGroupEditor.vue'
const acl = defineModel<Acl>({ required: true })
const { t } = useI18n()
const activeTab = ref(0)
const menu = ref()
const addMenuModel = ref([
{ label: () => t('acl.inbound'), command: () => addChain(AclChainType.Inbound) },
{ label: () => t('acl.outbound'), command: () => addChain(AclChainType.Outbound) },
{ label: () => t('acl.forward'), command: () => addChain(AclChainType.Forward) },
])
function addChain(type: AclChainType) {
if (!acl.value.acl_v1) {
acl.value.acl_v1 = { chains: [], group: { declares: [], members: [] } }
}
let defaultName = ''
switch (type) {
case AclChainType.Inbound: defaultName = 'Inbound'; break;
case AclChainType.Outbound: defaultName = 'Outbound'; break;
case AclChainType.Forward: defaultName = 'Forward'; break;
}
acl.value.acl_v1.chains.push({
name: defaultName,
chain_type: type,
description: '',
enabled: true,
rules: [],
default_action: AclAction.Allow
})
activeTab.value = acl.value.acl_v1.chains.length - 1
}
function removeChain(index: number) {
if (confirm(t('acl.delete_chain_confirm'))) {
acl.value.acl_v1?.chains.splice(index, 1)
if (activeTab.value >= (acl.value.acl_v1?.chains.length || 0)) {
activeTab.value = Math.max(0, (acl.value.acl_v1?.chains.length || 0))
}
}
}
function handleRenameGroup({ oldName, newName }: { oldName: string, newName: string }) {
if (!acl.value.acl_v1) return
acl.value.acl_v1.chains.forEach(chain => {
chain.rules.forEach(rule => {
rule.source_groups = rule.source_groups.map(g => g === oldName ? newName : g)
rule.destination_groups = rule.destination_groups.map(g => g === oldName ? newName : g)
})
})
}
const groupNames = computed(() => {
return acl.value.acl_v1?.group?.declares.map(g => g.group_name) || []
})
const tabs = computed(() => {
const chains = acl.value.acl_v1?.chains || []
const result: { type: string, label: string, index: number }[] = []
if (chains.length === 0) {
result.push({ type: 'empty', label: t('acl.chains'), index: 0 })
}
else {
chains.forEach((c, index) => {
result.push({
type: 'chain',
label: c.name || `Chain ${index}`,
index
})
})
}
result.push({ type: 'groups', label: t('acl.groups'), index: result.length })
return result
})
</script>
<template>
<div class="flex flex-col gap-4">
<Tabs v-model:value="activeTab">
<div class="flex items-center border-b border-surface-200 dark:border-surface-700">
<TabList class="flex-grow min-w-0 overflow-x-auto" style="border-bottom: none;">
<Tab v-for="tab in tabs" :key="tab.type + tab.index" :value="tab.index">
<div class="flex items-center gap-2 whitespace-nowrap">
{{ tab.label }}
<Button v-if="tab.type === 'chain'" icon="pi pi-times" severity="danger" text rounded size="small"
class="w-6 h-6 p-0" @click.stop="removeChain(tab.index)" />
</div>
</Tab>
</TabList>
<div
class="flex-shrink-0 flex items-center px-2 bg-white dark:bg-gray-900 border-l border-surface-100 dark:border-surface-800">
<Button icon="pi pi-plus" text rounded size="small" class="w-8 h-8 p-0"
@click="(event) => menu.toggle(event)" />
<Menu ref="menu" :model="addMenuModel" :popup="true" />
</div>
</div>
<TabPanels>
<TabPanel v-for="tab in tabs" :key="'panel' + tab.type + tab.index" :value="tab.index">
<!-- Empty State within TabPanel -->
<div v-if="tab.type === 'empty'"
class="py-8 flex flex-col items-center justify-center border-2 border-dashed border-surface-200 rounded-lg bg-surface-50 dark:bg-surface-900 dark:border-surface-700">
<i class="pi pi-shield text-5xl mb-4 text-primary" />
<div class="text-xl font-bold mb-2">{{ t('acl.chains') }}</div>
<p class="text-surface-500 mb-8 text-center max-w-sm px-4">{{ t('acl.help') }}</p>
<div class="flex flex-wrap gap-3 justify-center">
<Button :label="t('acl.inbound')" icon="pi pi-arrow-down-left" @click="addChain(AclChainType.Inbound)" />
<Button :label="t('acl.outbound')" icon="pi pi-arrow-up-right" @click="addChain(AclChainType.Outbound)" />
<Button :label="t('acl.forward')" icon="pi pi-directions" @click="addChain(AclChainType.Forward)" />
</div>
</div>
<!-- Rule Chains -->
<div v-if="tab.type === 'chain' && acl.acl_v1 && acl.acl_v1.chains[tab.index]" class="py-4">
<AclChainEditor v-model="acl.acl_v1.chains[tab.index]" :group-names="groupNames" />
</div>
<!-- Group Management -->
<div v-if="tab.type === 'groups'" class="py-4">
<template v-if="acl.acl_v1">
<AclGroupEditor v-if="acl.acl_v1.group" v-model="acl.acl_v1.group" :group-names="groupNames"
@rename-group="handleRenameGroup" />
<div v-else class="flex justify-center p-4">
<Button :label="t('web.common.add') + ' ' + t('acl.groups')"
@click="acl.acl_v1.group = { declares: [], members: [] }" />
</div>
</template>
<div v-else class="flex justify-center p-4">
<Button :label="t('acl.enabled')"
@click="acl.acl_v1 = { chains: [], group: { declares: [], members: [] } }" />
</div>
</div>
</TabPanel>
</TabPanels>
</Tabs>
</div>
</template>
@@ -1,150 +0,0 @@
<script setup lang="ts">
import { AutoComplete, Button, Checkbox, Dialog, InputNumber, InputText, MultiSelect, Panel, SelectButton, ToggleButton } from 'primevue';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { AclAction, AclProtocol, AclRule } from '../../types/network';
const props = defineProps<{
visible: boolean
groupNames?: string[]
}>()
const emit = defineEmits(['update:visible', 'save'])
const rule = defineModel<AclRule>('rule', { required: true })
const { t } = useI18n()
const protocolOptions = [
{ label: () => t('acl.any'), value: AclProtocol.Any },
{ label: 'TCP', value: AclProtocol.TCP },
{ label: 'UDP', value: AclProtocol.UDP },
{ label: 'ICMP', value: AclProtocol.ICMP },
{ label: 'ICMPv6', value: AclProtocol.ICMPv6 },
]
const actionOptions = [
{ label: () => t('acl.allow'), value: AclAction.Allow },
{ label: () => t('acl.drop'), value: AclAction.Drop },
]
const showPorts = computed(() => {
return rule.value.protocol === AclProtocol.TCP || rule.value.protocol === AclProtocol.UDP || rule.value.protocol === AclProtocol.Any
})
function close() {
emit('update:visible', false)
}
function save() {
emit('save', rule.value)
close()
}
// Suggestions for IP/Port AutoComplete
const genericSuggestions = ref<string[]>([])
</script>
<template>
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal :header="t('acl.edit_rule')"
:style="{ width: '90vw', maxWidth: '600px' }">
<div class="flex flex-col gap-4">
<div class="flex flex-row gap-4 items-center">
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.name') }}</label>
<InputText v-model="rule.name" fluid />
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.enabled') }}</label>
<ToggleButton v-model="rule.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.description') }}</label>
<InputText v-model="rule.description" fluid />
</div>
<div class="flex flex-row gap-4 flex-wrap">
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.action') }}</label>
<SelectButton v-model="rule.action" :options="actionOptions" :option-label="opt => opt.label()"
option-value="value" :allow-empty="false" />
</div>
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.protocol') }}</label>
<SelectButton v-model="rule.protocol" :options="protocolOptions"
:option-label="opt => typeof opt.label === 'function' ? opt.label() : opt.label" option-value="value"
:allow-empty="false" />
</div>
</div>
<Panel :header="t('acl.rules')" toggleable>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.src_ips') }}</label>
<AutoComplete v-model="rule.source_ips" multiple fluid :suggestions="genericSuggestions"
@complete="genericSuggestions = [$event.query]"
:placeholder="t('chips_placeholder', ['10.126.126.0/24'])" />
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.dst_ips') }}</label>
<AutoComplete v-model="rule.destination_ips" multiple fluid :suggestions="genericSuggestions"
@complete="genericSuggestions = [$event.query]"
:placeholder="t('chips_placeholder', ['10.126.126.2/32'])" />
</div>
<div v-if="showPorts" class="flex flex-row gap-4 flex-wrap">
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.src_ports') }}</label>
<AutoComplete v-model="rule.source_ports" multiple fluid :suggestions="genericSuggestions"
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
</div>
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.dst_ports') }}</label>
<AutoComplete v-model="rule.ports" multiple fluid :suggestions="genericSuggestions"
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
</div>
</div>
</div>
</Panel>
<Panel :header="t('advanced_settings')" toggleable collapsed>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<Checkbox v-model="rule.stateful" :binary="true" inputId="rule-stateful" />
<label for="rule-stateful" class="font-bold">{{ t('acl.rule.stateful') }}</label>
</div>
<div class="flex flex-row gap-4 flex-wrap">
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.rate_limit') }}</label>
<InputNumber v-model="rule.rate_limit" :min="0" placeholder="0 = no limit" fluid />
</div>
<div class="flex flex-col gap-2 grow">
<label class="font-bold">{{ t('acl.rule.burst_limit') }}</label>
<InputNumber v-model="rule.burst_limit" :min="0" placeholder="0 = no limit" fluid />
</div>
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.src_groups') }}</label>
<MultiSelect v-model="rule.source_groups" :options="props.groupNames" multiple fluid filter
:placeholder="t('acl.rule.src_groups')" />
</div>
<div class="flex flex-col gap-2">
<label class="font-bold">{{ t('acl.rule.dst_groups') }}</label>
<MultiSelect v-model="rule.destination_groups" :options="props.groupNames" multiple fluid filter
:placeholder="t('acl.rule.dst_groups')" />
</div>
</div>
</Panel>
</div>
<template #footer>
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="close" text />
<Button :label="t('web.common.save')" icon="pi pi-save" @click="save" />
</template>
</Dialog>
</template>
+8 -44
View File
@@ -286,6 +286,9 @@ web:
logout: 退出登录 logout: 退出登录
language: 语言 language: 语言
change_password: 修改密码 change_password: 修改密码
change_password_now: 立即修改密码
default_password_warning: 当前账号仍在使用系统默认密码。为保障安全,请部署完成后立即修改密码。
password_changed_relogin: 密码已修改,请重新登录。
device: device:
list: 设备列表 list: 设备列表
@@ -355,12 +358,16 @@ web:
delete: 删除 delete: 删除
edit: 编辑 edit: 编辑
refresh: 刷新 refresh: 刷新
add: 添加
loading: 加载中... loading: 加载中...
error: 错误 error: 错误
success: 成功 success: 成功
warning: 警告 warning: 警告
info: 提示 info: 提示
password_empty: 密码不能为空
password_min_length: 密码至少需要 8 位
password_too_weak: 密码强度不足
password_mismatch: 两次输入的密码不一致
password_strength_hint: 密码至少 8 位,且需包含大小写字母、数字、特殊字符中的至少 2 类
enable: 开启 enable: 开启
disable: 关闭 disable: 关闭
address: 地址 address: 地址
@@ -423,46 +430,3 @@ config-server:
client: client:
not_running: 无法连接至远程客户端 not_running: 无法连接至远程客户端
retry: 重试 retry: 重试
acl:
title: 访问控制
help: 访问控制列表,用于限制节点间的通信。
enabled: 启用 ACL
default_action: 默认动作
chains: 规则链
inbound: 入站
outbound: 出站
forward: 转发
rules: 规则
add_rule: 添加规则
edit_rule: 编辑规则
rule:
name: 规则名称
description: 描述
enabled: 启用
protocol: 协议
action: 动作
src_ips: 来源 IP
dst_ips: 目的 IP
src_ports: 来源端口
dst_ports: 目的端口
rate_limit: 速率限制 (pps)
burst_limit: 爆发限制
stateful: 状态追踪
src_groups: 来源组
dst_groups: 目的组
groups: 组管理
group:
declares: 声明组
members: 加入组
name: 组名
secret: 密钥
help: 在此处定义网络中的组身份,以便在规则中使用。
any: 任意
allow: 允许
drop: 丢弃
delete_chain_confirm: 确定要删除此规则链及其所有规则吗?
chain:
name: 名称
type: 类型
match: 匹配
+8 -44
View File
@@ -286,6 +286,9 @@ web:
logout: Logout logout: Logout
language: Language language: Language
change_password: Change Password change_password: Change Password
change_password_now: Change Password Now
default_password_warning: This account is still using the default password. Change it immediately after deployment to keep your instance secure.
password_changed_relogin: Password changed. Please log in again.
device: device:
list: Device List list: Device List
@@ -355,12 +358,16 @@ web:
delete: Delete delete: Delete
edit: Edit edit: Edit
refresh: Refresh refresh: Refresh
add: Add
loading: Loading... loading: Loading...
error: Error error: Error
success: Success success: Success
warning: Warning warning: Warning
info: Info info: Info
password_empty: Password cannot be empty
password_min_length: Password must be at least 8 characters long
password_too_weak: Password is too weak
password_mismatch: Passwords do not match
password_strength_hint: Password must be at least 8 characters and include at least 2 of uppercase letters, lowercase letters, numbers, or special characters
enable: Enable enable: Enable
disable: Disable disable: Disable
address: Address address: Address
@@ -423,46 +430,3 @@ config-server:
client: client:
not_running: Unable to connect to remote client. not_running: Unable to connect to remote client.
retry: Retry retry: Retry
acl:
title: Access Control (ACL)
help: Access control list to restrict communication between nodes.
enabled: Enable ACL
default_action: Default Action
chains: Rule Chains
inbound: Inbound
outbound: Outbound
forward: Forward
rules: Rules
add_rule: Add Rule
edit_rule: Edit Rule
rule:
name: Rule Name
description: Description
enabled: Enabled
protocol: Protocol
action: Action
src_ips: Source IPs
dst_ips: Destination IPs
src_ports: Source Ports
dst_ports: Destination Ports
rate_limit: Rate Limit (pps)
burst_limit: Burst Limit
stateful: Stateful
src_groups: Source Groups
dst_groups: Destination Groups
groups: Groups
group:
declares: Declared Groups
members: Node Memberships
name: Group Name
secret: Group Secret
help: Define group identities in the network to use them in rules.
any: Any
allow: Allow
drop: Drop
delete_chain_confirm: Are you sure you want to delete this rule chain and all its rules?
chain:
name: Name
type: Type
match: Match
@@ -14,74 +14,6 @@ export interface SecureModeConfig {
local_public_key?: string local_public_key?: string
} }
export enum AclProtocol {
Unspecified = 0,
TCP = 1,
UDP = 2,
ICMP = 3,
ICMPv6 = 4,
Any = 5,
}
export enum AclAction {
Noop = 0,
Allow = 1,
Drop = 2,
}
export enum AclChainType {
UnspecifiedChain = 0,
Inbound = 1,
Outbound = 2,
Forward = 3,
}
export interface AclRule {
name: string
description: string
priority: number
enabled: boolean
protocol: AclProtocol
ports: string[]
source_ips: string[]
destination_ips: string[]
source_ports: string[]
action: AclAction
rate_limit: number
burst_limit: number
stateful: boolean
source_groups: string[]
destination_groups: string[]
}
export interface AclChain {
name: string
chain_type: AclChainType
description: string
enabled: boolean
rules: AclRule[]
default_action: AclAction
}
export interface GroupIdentity {
group_name: string
group_secret: string
}
export interface GroupInfo {
declares: GroupIdentity[]
members: string[]
}
export interface AclV1 {
chains: AclChain[]
group?: GroupInfo
}
export interface Acl {
acl_v1?: AclV1
}
export interface NetworkConfig { export interface NetworkConfig {
instance_id: string instance_id: string
@@ -153,7 +85,6 @@ export interface NetworkConfig {
enable_private_mode?: boolean enable_private_mode?: boolean
port_forwards: PortForwardConfig[] port_forwards: PortForwardConfig[]
acl?: Acl
} }
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
@@ -221,15 +152,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
enable_magic_dns: false, enable_magic_dns: false,
enable_private_mode: false, enable_private_mode: false,
port_forwards: [], port_forwards: [],
acl: {
acl_v1: {
group: {
declares: [],
members: [],
},
chains: [],
},
},
} }
} }
@@ -1,17 +1,80 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { Card, Password, Button } from 'primevue'; import { Card, Password, Button } from 'primevue';
import { useToast } from 'primevue/usetoast';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import ApiClient from '../modules/api'; import ApiClient from '../modules/api';
import { clearMustChangePasswordFlag } from '../modules/auth-status';
import { validatePasswordStrength } from '../modules/password-policy';
const dialogRef = inject<any>('dialogRef'); const dialogRef = inject<any>('dialogRef');
const api = computed<ApiClient>(() => dialogRef.value.data.api); const api = computed<ApiClient>(() => dialogRef.value.data.api);
const password = ref(''); const password = ref('');
const confirmPassword = ref('');
const toast = useToast();
const router = useRouter();
const { t } = useI18n();
const passwordValidation = computed(() => validatePasswordStrength(password.value));
const passwordMatches = computed(() => password.value === confirmPassword.value);
const passwordErrorMessage = computed(() => {
if (password.value.length === 0 || passwordValidation.value.valid) {
return '';
}
return t(passwordValidation.value.reasonKey!);
});
const confirmPasswordErrorMessage = computed(() => {
if (confirmPassword.value.length === 0 || passwordMatches.value) {
return '';
}
return t('web.common.password_mismatch');
});
const canSubmit = computed(() => passwordValidation.value.valid && passwordMatches.value);
const changePassword = async () => { const changePassword = async () => {
await api.value.change_password(password.value); if (!passwordValidation.value.valid) {
dialogRef.value.close(); toast.add({
severity: 'warn',
summary: t('web.common.warning'),
detail: t(passwordValidation.value.reasonKey!),
life: 3000,
});
return;
}
if (!passwordMatches.value) {
toast.add({
severity: 'warn',
summary: t('web.common.warning'),
detail: t('web.common.password_mismatch'),
life: 3000,
});
return;
}
try {
await api.value.change_password(password.value);
toast.add({
severity: 'success',
summary: t('web.common.success'),
detail: t('web.main.password_changed_relogin'),
life: 3000,
});
clearMustChangePasswordFlag();
dialogRef.value.close();
router.push({ name: 'login' });
} catch (error) {
toast.add({
severity: 'error',
summary: t('web.common.error'),
detail: error instanceof Error ? error.message : String(error),
life: 3000,
});
}
} }
</script> </script>
@@ -19,13 +82,26 @@ const changePassword = async () => {
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<Card class="w-full max-w-md p-6"> <Card class="w-full max-w-md p-6">
<template #header> <template #header>
<h2 class="text-2xl font-semibold text-center">Change Password <h2 class="text-2xl font-semibold text-center">{{ t('web.main.change_password') }}
</h2> </h2>
</template> </template>
<template #content> <template #content>
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask /> <Password v-model="password" :placeholder="t('web.settings.new_password')" :feedback="false"
<Button @click="changePassword" label="Ok" /> toggleMask />
<Password v-model="confirmPassword" :placeholder="t('web.settings.confirm_password')"
:feedback="false" toggleMask />
<small class="text-surface-500 dark:text-surface-400">
{{ t('web.common.password_strength_hint') }}
</small>
<small v-if="passwordErrorMessage" class="text-red-500 dark:text-red-400">
{{ passwordErrorMessage }}
</small>
<small v-if="confirmPasswordErrorMessage" class="text-red-500 dark:text-red-400">
{{ confirmPasswordErrorMessage }}
</small>
<Button @click="changePassword" :label="t('web.common.confirm')"
:disabled="!canSubmit" />
</div> </div>
</template> </template>
</Card> </Card>
+60 -1
View File
@@ -7,6 +7,8 @@ import { I18nUtils } from 'easytier-frontend-lib';
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host" import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ApiClient, { Credential, RegisterData } from '../modules/api'; import ApiClient, { Credential, RegisterData } from '../modules/api';
import { setMustChangePasswordFlag } from '../modules/auth-status';
import { validatePasswordStrength } from '../modules/password-policy';
const { t } = useI18n() const { t } = useI18n()
@@ -22,8 +24,26 @@ const username = ref('');
const password = ref(''); const password = ref('');
const registerUsername = ref(''); const registerUsername = ref('');
const registerPassword = ref(''); const registerPassword = ref('');
const registerConfirmPassword = ref('');
const captcha = ref(''); const captcha = ref('');
const captchaSrc = computed(() => api.value.captcha_url()); const captchaSrc = computed(() => api.value.captcha_url());
const registerPasswordValidation = computed(() => validatePasswordStrength(registerPassword.value));
const registerPasswordsMatch = computed(() => registerPassword.value === registerConfirmPassword.value);
const registerPasswordErrorMessage = computed(() => {
if (registerPassword.value.length === 0 || registerPasswordValidation.value.valid) {
return '';
}
return t(registerPasswordValidation.value.reasonKey!);
});
const registerConfirmPasswordErrorMessage = computed(() => {
if (registerConfirmPassword.value.length === 0 || registerPasswordsMatch.value) {
return '';
}
return t('web.common.password_mismatch');
});
const canRegister = computed(() => registerPasswordValidation.value.valid && registerPasswordsMatch.value);
const onSubmit = async () => { const onSubmit = async () => {
@@ -33,6 +53,7 @@ const onSubmit = async () => {
let ret = await api.value?.login(credential); let ret = await api.value?.login(credential);
if (ret.success) { if (ret.success) {
localStorage.setItem('apiHost', btoa(apiHost.value)); localStorage.setItem('apiHost', btoa(apiHost.value));
setMustChangePasswordFlag(Boolean(ret.mustChangePassword));
router.push({ router.push({
name: 'dashboard', name: 'dashboard',
params: { apiHost: btoa(apiHost.value) }, params: { apiHost: btoa(apiHost.value) },
@@ -43,6 +64,26 @@ const onSubmit = async () => {
}; };
const onRegister = async () => { const onRegister = async () => {
if (!registerPasswordValidation.value.valid) {
toast.add({
severity: 'warn',
summary: t('web.common.warning'),
detail: t(registerPasswordValidation.value.reasonKey!),
life: 3000,
});
return;
}
if (!registerPasswordsMatch.value) {
toast.add({
severity: 'warn',
summary: t('web.common.warning'),
detail: t('web.common.password_mismatch'),
life: 3000,
});
return;
}
saveApiHost(apiHost.value); saveApiHost(apiHost.value);
const credential: Credential = { username: registerUsername.value, password: registerPassword.value }; const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value }; const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
@@ -156,6 +197,23 @@ onBeforeUnmount(() => {
}}</label> }}</label>
<Password id="register-password" v-model="registerPassword" required toggleMask <Password id="register-password" v-model="registerPassword" required toggleMask
:feedback="false" class="w-full" /> :feedback="false" class="w-full" />
<small class="text-surface-500 dark:text-surface-400">
{{ t('web.common.password_strength_hint') }}
</small>
<small v-if="registerPasswordErrorMessage" class="block text-red-500 dark:text-red-400">
{{ registerPasswordErrorMessage }}
</small>
</div>
<div class="p-field">
<label for="register-confirm-password" class="block text-sm font-medium">
{{ t('web.settings.confirm_password') }}
</label>
<Password id="register-confirm-password" v-model="registerConfirmPassword" required toggleMask
:feedback="false" class="w-full" />
<small v-if="registerConfirmPasswordErrorMessage"
class="block text-red-500 dark:text-red-400">
{{ registerConfirmPasswordErrorMessage }}
</small>
</div> </div>
<div class="p-field"> <div class="p-field">
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label> <label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
@@ -163,7 +221,8 @@ onBeforeUnmount(() => {
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" /> <img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Button :label="t('web.login.register')" type="submit" class="w-full" /> <Button :label="t('web.login.register')" type="submit" class="w-full"
:disabled="!canRegister" />
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Button :label="t('web.login.back_to_login')" type="button" class="w-full" <Button :label="t('web.login.back_to_login')" type="button" class="w-full"
@@ -1,13 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { I18nUtils } from 'easytier-frontend-lib' import { I18nUtils } from 'easytier-frontend-lib'
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue'; import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
import { Button, TieredMenu } from 'primevue'; import { Button, Message, TieredMenu } from 'primevue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useDialog } from 'primevue/usedialog'; import { useDialog } from 'primevue/usedialog';
import ChangePassword from './ChangePassword.vue'; import ChangePassword from './ChangePassword.vue';
import Icon from '../assets/easytier.png' import Icon from '../assets/easytier.png'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ApiClient from '../modules/api'; import ApiClient from '../modules/api';
import {
clearMustChangePasswordFlag,
getMustChangePasswordFlag,
setMustChangePasswordFlag,
} from '../modules/auth-status';
const { t } = useI18n() const { t } = useI18n()
const route = useRoute(); const route = useRoute();
@@ -15,6 +20,7 @@ const router = useRouter();
const api = computed<ApiClient | undefined>(() => { const api = computed<ApiClient | undefined>(() => {
try { try {
return new ApiClient(atob(route.params.apiHost as string), () => { return new ApiClient(atob(route.params.apiHost as string), () => {
clearMustChangePasswordFlag();
router.push({ name: 'login' }); router.push({ name: 'login' });
}) })
} catch (e) { } catch (e) {
@@ -23,25 +29,42 @@ const api = computed<ApiClient | undefined>(() => {
}); });
const dialog = useDialog(); const dialog = useDialog();
const mustChangePassword = ref(false);
const openChangePasswordDialog = () => {
dialog.open(ChangePassword, {
props: {
modal: true,
},
data: {
api: api.value,
}
});
};
const loadAuthStatus = async () => {
const cachedStatus = getMustChangePasswordFlag();
if (cachedStatus !== null) {
mustChangePassword.value = cachedStatus;
}
try {
const status = await api.value?.check_login_status();
mustChangePassword.value = Boolean(
status?.loggedIn && status?.mustChangePassword,
);
setMustChangePasswordFlag(mustChangePassword.value);
} catch (e) {
console.error('Failed to load auth status', e);
}
};
const userMenu = ref(); const userMenu = ref();
const userMenuItems = ref([ const userMenuItems = ref([
{ {
label: t('web.main.change_password'), label: t('web.main.change_password'),
icon: 'pi pi-key', icon: 'pi pi-key',
command: () => { command: openChangePasswordDialog,
console.log('File');
let ret = dialog.open(ChangePassword, {
props: {
modal: true,
},
data: {
api: api.value,
}
});
console.log("return", ret)
},
}, },
{ {
label: t('web.main.logout'), label: t('web.main.logout'),
@@ -52,6 +75,7 @@ const userMenuItems = ref([
} catch (e) { } catch (e) {
console.error("logout failed", e); console.error("logout failed", e);
} }
clearMustChangePasswordFlag();
router.push({ name: 'login' }); router.push({ name: 'login' });
}, },
}, },
@@ -92,6 +116,7 @@ onMounted(async () => {
// 等待 DOM 渲染完成后添加事件监听器 // 等待 DOM 渲染完成后添加事件监听器
await nextTick(); await nextTick();
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
await loadAuthStatus();
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -171,6 +196,13 @@ onUnmounted(() => {
<div class="p-4 sm:ml-64"> <div class="p-4 sm:ml-64">
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700"> <div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
<div class="grid grid-cols-1 gap-4"> <div class="grid grid-cols-1 gap-4">
<Message v-if="mustChangePassword" severity="warn" :closable="false">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<span>{{ t('web.main.default_password_warning') }}</span>
<Button size="small" icon="pi pi-key" :label="t('web.main.change_password_now')"
@click="openChangePasswordDialog" />
</div>
</Message>
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<component :is="Component" :api="api" /> <component :is="Component" :api="api" />
</RouterView> </RouterView>
+37 -14
View File
@@ -2,6 +2,8 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib'; import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
import { Md5 } from 'ts-md5'; import { Md5 } from 'ts-md5';
const hashAuthPassword = (password: string) => Md5.hashStr(password);
export interface ValidateConfigResponse { export interface ValidateConfigResponse {
toml_config: string; toml_config: string;
} }
@@ -14,6 +16,16 @@ export interface OidcConfigResponse {
export interface LoginResponse { export interface LoginResponse {
success: boolean; success: boolean;
message: string; message: string;
mustChangePassword?: boolean;
}
export interface AuthStatusResponse {
must_change_password: boolean;
}
export interface CheckLoginStatusResponse {
loggedIn: boolean;
mustChangePassword: boolean;
} }
export interface RegisterResponse { export interface RegisterResponse {
@@ -82,7 +94,6 @@ export class ApiClient {
// 添加响应拦截器 // 添加响应拦截器
this.client.interceptors.response.use((response: AxiosResponse) => { this.client.interceptors.response.use((response: AxiosResponse) => {
console.debug('Axios Response:', response);
return response.data; // 假设服务器返回的数据都在data属性中 return response.data; // 假设服务器返回的数据都在data属性中
}, (error: any) => { }, (error: any) => {
if (error.response) { if (error.response) {
@@ -108,9 +119,8 @@ export class ApiClient {
// 注册 // 注册
public async register(data: RegisterData): Promise<RegisterResponse> { public async register(data: RegisterData): Promise<RegisterResponse> {
try { try {
data.credentials.password = Md5.hashStr(data.credentials.password); data.credentials.password = hashAuthPassword(data.credentials.password);
const response = await this.client.post<RegisterResponse>('/auth/register', data); await this.client.post<RegisterResponse>('/auth/register', data);
console.log("register response:", response);
return { success: true, message: 'Register success', }; return { success: true, message: 'Register success', };
} catch (error) { } catch (error) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
@@ -123,10 +133,13 @@ export class ApiClient {
// 登录 // 登录
public async login(data: Credential): Promise<LoginResponse> { public async login(data: Credential): Promise<LoginResponse> {
try { try {
data.password = Md5.hashStr(data.password); data.password = hashAuthPassword(data.password);
const response = await this.client.post<any>('/auth/login', data); const response = await this.client.post<any, AuthStatusResponse>('/auth/login', data);
console.log("login response:", response); return {
return { success: true, message: 'Login success', }; success: true,
message: 'Login success',
mustChangePassword: response.must_change_password,
};
} catch (error) { } catch (error) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
if (error.response?.status === 401) { if (error.response?.status === 401) {
@@ -147,16 +160,26 @@ export class ApiClient {
} }
public async change_password(new_password: string) { public async change_password(new_password: string) {
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) }); await this.client.put('/auth/password', { new_password: hashAuthPassword(new_password) });
} }
public async check_login_status() { public async check_login_status(): Promise<CheckLoginStatusResponse> {
try { try {
await this.client.get('/auth/check_login_status'); const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status');
return true; return {
loggedIn: true,
mustChangePassword: response.must_change_password,
};
} catch (error) { } catch (error) {
return false; if (error instanceof AxiosError && error.response?.status === 401) {
} return {
loggedIn: false,
mustChangePassword: false,
};
}
throw error;
};
} }
public async list_session() { public async list_session() {
@@ -0,0 +1,18 @@
const MUST_CHANGE_PASSWORD_STORAGE_KEY = 'auth.mustChangePassword';
export const getMustChangePasswordFlag = (): boolean | null => {
const value = sessionStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
if (value === null) {
return null;
}
return value === 'true';
};
export const setMustChangePasswordFlag = (value: boolean) => {
sessionStorage.setItem(MUST_CHANGE_PASSWORD_STORAGE_KEY, value ? 'true' : 'false');
};
export const clearMustChangePasswordFlag = () => {
sessionStorage.removeItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
};
@@ -0,0 +1,55 @@
export type PasswordValidationReasonKey =
| 'web.common.password_empty'
| 'web.common.password_min_length'
| 'web.common.password_too_weak';
export interface PasswordValidationResult {
valid: boolean;
reasonKey?: PasswordValidationReasonKey;
}
const PASSWORD_MIN_LENGTH = 8;
export const countPasswordClasses = (password: string) => {
let count = 0;
if (/[a-z]/.test(password)) {
count += 1;
}
if (/[A-Z]/.test(password)) {
count += 1;
}
if (/\d/.test(password)) {
count += 1;
}
if (/[^A-Za-z0-9\s]/.test(password)) {
count += 1;
}
return count;
};
export const validatePasswordStrength = (password: string): PasswordValidationResult => {
if (password.trim().length === 0) {
return {
valid: false,
reasonKey: 'web.common.password_empty',
};
}
if (password.length < PASSWORD_MIN_LENGTH) {
return {
valid: false,
reasonKey: 'web.common.password_min_length',
};
}
if (countPasswordClasses(password) < 2) {
return {
valid: false,
reasonKey: 'web.common.password_too_weak',
};
}
return { valid: true };
};
+11 -18
View File
@@ -2,8 +2,8 @@ pub mod session;
pub mod storage; pub mod storage;
use std::sync::{ use std::sync::{
Arc,
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
Arc,
}; };
use dashmap::DashMap; use dashmap::DashMap;
@@ -19,11 +19,11 @@ use maxminddb::geoip2;
use session::{Location, Session}; use session::{Location, Session};
use storage::{Storage, StorageToken}; use storage::{Storage, StorageToken};
use crate::FeatureFlags;
use crate::webhook::SharedWebhookConfig; use crate::webhook::SharedWebhookConfig;
use crate::FeatureFlags;
use tokio::task::JoinSet; use tokio::task::JoinSet;
use crate::db::{Db, UserIdInDb, entity::user_running_network_configs}; use crate::db::{entity::user_running_network_configs, Db, UserIdInDb};
#[derive(rust_embed::Embed)] #[derive(rust_embed::Embed)]
#[folder = "resources/"] #[folder = "resources/"]
@@ -340,7 +340,7 @@ mod tests {
}; };
use sqlx::Executor; use sqlx::Executor;
use crate::{FeatureFlags, client_manager::ClientManager, db::Db}; use crate::{client_manager::ClientManager, db::Db, FeatureFlags};
#[tokio::test] #[tokio::test]
async fn test_client() { async fn test_client() {
@@ -379,26 +379,19 @@ mod tests {
let req = tokio::time::timeout(Duration::from_secs(12), async { let req = tokio::time::timeout(Duration::from_secs(12), async {
loop { loop {
let sessions = mgr let session = mgr
.client_sessions .client_sessions
.iter() .iter()
.map(|item| item.value().clone()) .next()
.collect::<Vec<_>>(); .map(|item| item.value().clone());
if sessions.is_empty() { let Some(session) = session else {
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
continue; continue;
} };
let mut found_req = None; let mut waiter = session.data().read().await.heartbeat_waiter();
for session in sessions { if let Ok(req) = waiter.recv().await {
if let Some(req) = session.data().read().await.req() {
found_req = Some(req);
break;
}
}
if let Some(req) = found_req {
break req; break req;
} }
tokio::time::sleep(Duration::from_millis(100)).await;
} }
}) })
.await .await
+26 -27
View File
@@ -20,11 +20,11 @@ use easytier::{
rpc_service::remote_client::{ListNetworkProps, Storage as _}, rpc_service::remote_client::{ListNetworkProps, Storage as _},
tunnel::Tunnel, tunnel::Tunnel,
}; };
use tokio::sync::{RwLock, broadcast}; use tokio::sync::{broadcast, RwLock};
use super::storage::{Storage, StorageToken, WeakRefStorage}; use super::storage::{Storage, StorageToken, WeakRefStorage};
use crate::FeatureFlags;
use crate::webhook::SharedWebhookConfig; use crate::webhook::SharedWebhookConfig;
use crate::FeatureFlags;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Location { pub struct Location {
@@ -87,30 +87,30 @@ impl SessionData {
impl Drop for SessionData { impl Drop for SessionData {
fn drop(&mut self) { fn drop(&mut self) {
if let Ok(storage) = Storage::try_from(self.storage.clone()) if let Ok(storage) = Storage::try_from(self.storage.clone()) {
&& let Some(token) = self.storage_token.as_ref() if let Some(token) = self.storage_token.as_ref() {
{ storage.remove_client(token);
storage.remove_client(token);
// Notify the webhook receiver when a node disconnects. // Notify the webhook receiver when a node disconnects.
if self.webhook_config.is_enabled() { if self.webhook_config.is_enabled() {
let webhook = self.webhook_config.clone(); let webhook = self.webhook_config.clone();
let machine_id = token.machine_id.to_string(); let machine_id = token.machine_id.to_string();
let user_id = Some(token.user_id); let user_id = Some(token.user_id);
let token_value = token.token.clone(); let token_value = token.token.clone();
let web_instance_id = webhook.web_instance_id.clone(); let web_instance_id = webhook.web_instance_id.clone();
let binding_version = self.binding_version; let binding_version = self.binding_version;
tokio::spawn(async move { tokio::spawn(async move {
webhook webhook
.notify_node_disconnected(&crate::webhook::NodeDisconnectedRequest { .notify_node_disconnected(&crate::webhook::NodeDisconnectedRequest {
machine_id, machine_id,
token: token_value, token: token_value,
user_id, user_id,
web_instance_id, web_instance_id,
binding_version, binding_version,
}) })
.await; .await;
}); });
}
} }
} }
} }
@@ -233,7 +233,6 @@ impl SessionRpcService {
let webhook_req = crate::webhook::ValidateTokenRequest { let webhook_req = crate::webhook::ValidateTokenRequest {
token: req.user_token.clone(), token: req.user_token.clone(),
machine_id: machine_id.to_string(), machine_id: machine_id.to_string(),
public_ip: data.client_url.host_str().map(str::to_string),
hostname: req.hostname.clone(), hostname: req.hostname.clone(),
version: req.easytier_version.clone(), version: req.easytier_version.clone(),
os_type: req.device_os.as_ref().map(|info| info.os_type.clone()), os_type: req.device_os.as_ref().map(|info| info.os_type.clone()),
@@ -386,7 +385,7 @@ impl WebServerService for SessionRpcService {
_: easytier::proto::web::GetFeatureRequest, _: easytier::proto::web::GetFeatureRequest,
) -> rpc_types::error::Result<easytier::proto::web::GetFeatureResponse> { ) -> rpc_types::error::Result<easytier::proto::web::GetFeatureResponse> {
Ok(easytier::proto::web::GetFeatureResponse { Ok(easytier::proto::web::GetFeatureResponse {
support_encryption: easytier::web_client::security::web_secure_tunnel_supported(), support_encryption: true,
}) })
} }
} }
+1
View File
@@ -11,6 +11,7 @@ pub struct Model {
#[sea_orm(unique)] #[sea_orm(unique)]
pub username: String, pub username: String,
pub password: String, pub password: String,
pub must_change_password: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+26 -4
View File
@@ -8,11 +8,11 @@ use easytier::{
}; };
use entity::user_running_network_configs; use entity::user_running_network_configs;
use sea_orm::{ use sea_orm::{
ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait, QueryFilter as _, Set, prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
SqlxSqliteConnector, TransactionTrait as _, prelude::Expr, sea_query::OnConflict, QueryFilter as _, Set, SqlxSqliteConnector, TransactionTrait as _,
}; };
use sea_orm_migration::MigratorTrait as _; use sea_orm_migration::MigratorTrait as _;
use sqlx::{Sqlite, SqlitePool, migrate::MigrateDatabase as _, types::chrono}; use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
use uuid::Uuid; use uuid::Uuid;
use crate::migrator; use crate::migrator;
@@ -96,6 +96,7 @@ impl Db {
let user_active = users::ActiveModel { let user_active = users::ActiveModel {
username: Set(username.to_string()), username: Set(username.to_string()),
password: Set(password_hash), password: Set(password_hash),
must_change_password: Set(false),
..Default::default() ..Default::default()
}; };
let insert_result = users::Entity::insert(user_active).exec(&txn).await?; let insert_result = users::Entity::insert(user_active).exec(&txn).await?;
@@ -280,7 +281,28 @@ mod tests {
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage}; use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
use crate::db::{Db, ListNetworkProps, entity::user_running_network_configs}; use crate::db::{
entity::{user_running_network_configs, users},
Db, ListNetworkProps,
};
#[tokio::test]
async fn created_users_default_to_not_requiring_password_change() {
let db = Db::memory_db().await;
let user = db
.create_user_and_join_users_group("created-user", "pre-hashed-password".to_string())
.await
.unwrap();
let stored = users::Entity::find_by_id(user.id)
.one(db.orm_db())
.await
.unwrap()
.unwrap();
assert!(!stored.must_change_password);
}
#[tokio::test] #[tokio::test]
async fn test_user_network_config_management() { async fn test_user_network_config_management() {
+2 -2
View File
@@ -16,8 +16,8 @@ use easytier::{
log, log,
network::{local_ipv4, local_ipv6}, network::{local_ipv4, local_ipv6},
}, },
tunnel::{TunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener}, tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, TunnelListener},
utils::panic::setup_panic_handler, utils::setup_panic_handler,
}; };
use easytier::tunnel::IpScheme; use easytier::tunnel::IpScheme;
@@ -0,0 +1,129 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
const DEFAULT_USER_PASSWORD_HASH: &str =
"$argon2i$v=19$m=16,t=2,p=1$aGVyRDBrcnRycnlaMDhkbw$449SEcv/qXf+0fnI9+fYVQ";
const DEFAULT_ADMIN_PASSWORD_HASH: &str =
"$argon2i$v=19$m=16,t=2,p=1$bW5idXl0cmY$61n+JxL4r3dwLPAEDlDdtg";
#[derive(DeriveIden)]
enum Users {
Table,
Username,
Password,
MustChangePassword,
}
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20260405_000003_add_must_change_password"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.add_column(
ColumnDef::new(Users::MustChangePassword)
.boolean()
.not_null()
.default(false),
)
.to_owned(),
)
.await?;
manager
.exec_stmt(
Query::update()
.table(Users::Table)
.value(Users::MustChangePassword, true)
.cond_where(any![
Expr::col(Users::Username)
.eq("admin")
.and(Expr::col(Users::Password).eq(DEFAULT_ADMIN_PASSWORD_HASH)),
Expr::col(Users::Username)
.eq("user")
.and(Expr::col(Users::Password).eq(DEFAULT_USER_PASSWORD_HASH)),
])
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.drop_column(Users::MustChangePassword)
.to_owned(),
)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _, SqlxSqliteConnector};
use sea_orm_migration::prelude::SchemaManager;
use sqlx::sqlite::SqlitePoolOptions;
use super::{Migration, MigrationTrait, DEFAULT_USER_PASSWORD_HASH};
use crate::db::entity::users;
async fn find_user(db: &sea_orm::DatabaseConnection, username: &str) -> users::Model {
users::Entity::find()
.filter(users::Column::Username.eq(username))
.one(db)
.await
.unwrap()
.unwrap()
}
#[tokio::test]
async fn migration_only_marks_seeded_accounts_still_using_default_passwords() {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query(
"CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)",
)
.execute(&pool)
.await
.unwrap();
let changed_admin_password = password_auth::generate_hash("already-changed");
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?), (?, ?)")
.bind("admin")
.bind(changed_admin_password)
.bind("user")
.bind(DEFAULT_USER_PASSWORD_HASH)
.execute(&pool)
.await
.unwrap();
let db = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
Migration.up(&SchemaManager::new(&db)).await.unwrap();
assert!(!find_user(&db, "admin").await.must_change_password);
assert!(find_user(&db, "user").await.must_change_password);
}
}
+2
View File
@@ -2,6 +2,7 @@ use sea_orm_migration::prelude::*;
mod m20241029_000001_init; mod m20241029_000001_init;
mod m20260403_000002_scope_network_config_unique; mod m20260403_000002_scope_network_config_unique;
mod m20260405_000003_add_must_change_password;
pub struct Migrator; pub struct Migrator;
@@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
vec![ vec![
Box::new(m20241029_000001_init::Migration), Box::new(m20241029_000001_init::Migration),
Box::new(m20260403_000002_scope_network_config_unique::Migration), Box::new(m20260403_000002_scope_network_config_unique::Migration),
Box::new(m20260405_000003_add_must_change_password::Migration),
] ]
} }
} }
+39 -28
View File
@@ -1,11 +1,10 @@
use axum::{ use axum::{
Router,
http::StatusCode, http::StatusCode,
routing::{get, post, put}, routing::{get, post, put},
Router,
}; };
use axum_login::login_required; use axum_login::login_required;
use axum_messages::Message; use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::restful::users::Backend; use crate::restful::users::Backend;
@@ -14,13 +13,13 @@ use std::sync::Arc;
use crate::FeatureFlags; use crate::FeatureFlags;
use super::{ use super::{
AppStateInner,
users::{AuthSession, Credentials}, users::{AuthSession, Credentials},
AppStateInner,
}; };
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Serialize)]
pub struct LoginResult { pub struct AuthStatusResponse {
messages: Vec<Message>, must_change_password: bool,
} }
pub fn router() -> Router<AppStateInner> { pub fn router() -> Router<AppStateInner> {
@@ -40,12 +39,15 @@ pub fn router() -> Router<AppStateInner> {
} }
mod put { mod put {
use crate::restful::{
other_error,
users::{ChangePassword, ChangePasswordError},
HttpHandleError,
};
use axum::Json; use axum::Json;
use axum_login::AuthUser; use axum_login::AuthUser;
use easytier::proto::common::Void; use easytier::proto::common::Void;
use crate::restful::{HttpHandleError, other_error, users::ChangePassword};
use super::*; use super::*;
pub async fn change_password( pub async fn change_password(
@@ -58,27 +60,33 @@ mod put {
.await .await
{ {
tracing::error!("Failed to change password: {:?}", e); tracing::error!("Failed to change password: {:?}", e);
return Err(( let (status, message) = match &e {
StatusCode::INTERNAL_SERVER_ERROR, ChangePasswordError::EmptyPassword => {
Json::from(other_error(format!("{:?}", e))), (StatusCode::BAD_REQUEST, "password cannot be empty")
)); }
ChangePasswordError::UserNotFound | ChangePasswordError::Db(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to change password",
),
};
return Err((status, Json::from(other_error(message.to_string()))));
} }
let _ = auth_session.logout().await; let _ = auth_session.logout().await;
Ok(Void::default().into()) Ok(Json(Void::default()))
} }
} }
mod post { mod post {
use axum::{Json, extract::Extension}; use axum::{extract::Extension, Json};
use easytier::proto::common::Void; use easytier::proto::common::Void;
use crate::restful::{ use crate::restful::{
HttpHandleError, captcha::extension::{axum_tower_sessions::CaptchaAxumTowerSessionStaticExt, CaptchaUtil},
captcha::extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionStaticExt},
other_error, other_error,
users::RegisterNewUser, users::RegisterNewUser,
HttpHandleError,
}; };
use super::*; use super::*;
@@ -86,7 +94,7 @@ mod post {
pub async fn login( pub async fn login(
mut auth_session: AuthSession, mut auth_session: AuthSession,
Json(creds): Json<Credentials>, Json(creds): Json<Credentials>,
) -> Result<Json<Void>, HttpHandleError> { ) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
let user = match auth_session.authenticate(creds.clone()).await { let user = match auth_session.authenticate(creds.clone()).await {
Ok(Some(user)) => user, Ok(Some(user)) => user,
Ok(None) => { Ok(None) => {
@@ -99,7 +107,7 @@ mod post {
return Err(( return Err((
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json::from(other_error(format!("{:?}", e))), Json::from(other_error(format!("{:?}", e))),
)); ))
} }
}; };
@@ -110,7 +118,9 @@ mod post {
)); ));
} }
Ok(Void::default().into()) Ok(Json(AuthStatusResponse {
must_change_password: user.db_user.must_change_password,
}))
} }
pub async fn register( pub async fn register(
@@ -150,15 +160,14 @@ mod post {
mod get { mod get {
use crate::restful::{ use crate::restful::{
HttpHandleError,
captcha::{ captcha::{
NewCaptcha as _,
builder::spec::SpecCaptcha, builder::spec::SpecCaptcha,
extension::{CaptchaUtil, axum_tower_sessions::CaptchaAxumTowerSessionExt as _}, extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil},
NewCaptcha as _,
}, },
other_error, other_error, HttpHandleError,
}; };
use axum::{Json, response::Response}; use axum::{response::Response, Json};
use easytier::proto::common::Void; use easytier::proto::common::Void;
use tower_sessions::Session; use tower_sessions::Session;
@@ -190,9 +199,11 @@ mod get {
pub async fn check_login_status( pub async fn check_login_status(
auth_session: AuthSession, auth_session: AuthSession,
) -> Result<Json<Void>, HttpHandleError> { ) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
if auth_session.user.is_some() { if let Some(user) = auth_session.user {
Ok(Json(Void::default())) Ok(Json(AuthStatusResponse {
must_change_password: user.db_user.must_change_password,
}))
} else { } else {
Err(( Err((
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
@@ -2,8 +2,8 @@ use super::super::base::randoms::Randoms;
use super::super::utils::color::Color; use super::super::utils::color::Color;
use super::super::utils::font; use super::super::utils::font;
use base64::Engine;
use base64::prelude::BASE64_STANDARD; use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use rusttype::Font; use rusttype::Font;
use std::fmt::Debug; use std::fmt::Debug;
@@ -9,14 +9,14 @@ use super::super::{CaptchaFont, NewCaptcha};
use image::{ImageBuffer, Rgba}; use image::{ImageBuffer, Rgba};
use imageproc::drawing; use imageproc::drawing;
use rand::{Rng, rngs::ThreadRng}; use rand::{rngs::ThreadRng, Rng};
use rusttype::{Font, Scale}; use rusttype::{Font, Scale};
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use std::sync::Arc; use std::sync::Arc;
mod color { mod color {
use image::Rgba; use image::Rgba;
use rand::{Rng, rngs::ThreadRng}; use rand::{rngs::ThreadRng, Rng};
pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> { pub fn gen_background_color(rng: &mut ThreadRng) -> Rgba<u8> {
let red = rng.gen_range(200..=255); let red = rng.gen_range(200..=255);
let green = rng.gen_range(200..=255); let green = rng.gen_range(200..=255);
@@ -133,7 +133,7 @@ impl<'a, 'b> CaptchaBuilder<'a, 'b> {
fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) { fn draw_line(&self, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, rng: &mut ThreadRng) {
let line_color = color::gen_line_color(rng); let line_color = color::gen_line_color(rng);
let is_h = rng.r#gen(); let is_h = rng.gen();
let (start, end) = if is_h { let (start, end) = if is_h {
let xa = rng.gen_range(0.0..(self.width as f32) / 2.0); let xa = rng.gen_range(0.0..(self.width as f32) / 2.0);
let ya = rng.gen_range(0.0..(self.height as f32)); let ya = rng.gen_range(0.0..(self.height as f32));
+6 -6
View File
@@ -8,13 +8,13 @@ mod users;
use std::{net::SocketAddr, sync::Arc}; use std::{net::SocketAddr, sync::Arc};
use axum::extract::Path; use axum::extract::Path;
use axum::http::{Request, StatusCode, header}; use axum::http::{header, Request, StatusCode};
use axum::middleware::{self as axum_mw, Next}; use axum::middleware::{self as axum_mw, Next};
use axum::response::Response; use axum::response::Response;
use axum::routing::{delete, post}; use axum::routing::{delete, post};
use axum::{Extension, Json, Router, extract::State, routing::get}; use axum::{extract::State, routing::get, Extension, Json, Router};
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer}; use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
use axum_login::{AuthManagerLayerBuilder, AuthUser, AuthzBackend, login_required}; use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
use axum_messages::MessagesManagerLayer; use axum_messages::MessagesManagerLayer;
use easytier::common::config::{ConfigLoader, TomlConfigLoader}; use easytier::common::config::{ConfigLoader, TomlConfigLoader};
use easytier::common::scoped_task::ScopedTask; use easytier::common::scoped_task::ScopedTask;
@@ -23,17 +23,17 @@ use easytier::proto::rpc_types;
use network::NetworkApi; use network::NetworkApi;
use sea_orm::DbErr; use sea_orm::DbErr;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_sessions::Expiry;
use tower_sessions::cookie::time::Duration; use tower_sessions::cookie::time::Duration;
use tower_sessions::cookie::{Key, SameSite}; use tower_sessions::cookie::{Key, SameSite};
use tower_sessions::Expiry;
use tower_sessions_sqlx_store::SqliteStore; use tower_sessions_sqlx_store::SqliteStore;
use users::{AuthSession, Backend}; use users::{AuthSession, Backend};
use crate::FeatureFlags;
use crate::client_manager::ClientManager;
use crate::client_manager::storage::StorageToken; use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager;
use crate::db::{Db, UserIdInDb}; use crate::db::{Db, UserIdInDb};
use crate::webhook::SharedWebhookConfig; use crate::webhook::SharedWebhookConfig;
use crate::FeatureFlags;
/// Embed assets for web dashboard, build frontend first /// Embed assets for web dashboard, build frontend first
#[cfg(feature = "embed")] #[cfg(feature = "embed")]
+2 -2
View File
@@ -1,7 +1,7 @@
use axum::extract::Path; use axum::extract::Path;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::{delete, post}; use axum::routing::{delete, post};
use axum::{Json, Router, extract::State, routing::get}; use axum::{extract::State, routing::get, Json, Router};
use axum_login::AuthUser; use axum_login::AuthUser;
use easytier::launcher::NetworkConfig; use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void; use easytier::proto::common::Void;
@@ -16,7 +16,7 @@ use crate::db::UserIdInDb;
use super::users::AuthSession; use super::users::AuthSession;
use super::{ use super::{
AppState, AppStateInner, Error, HttpHandleError, RpcError, convert_db_error, other_error, convert_db_error, other_error, AppState, AppStateInner, Error, HttpHandleError, RpcError,
}; };
fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) { fn convert_rpc_error(e: RpcError) -> (StatusCode, Json<Error>) {
+12 -13
View File
@@ -4,8 +4,8 @@ use std::time::Duration;
use subtle::ConstantTimeEq; use subtle::ConstantTimeEq;
use axum::Router;
use axum::routing::get; use axum::routing::get;
use axum::Router;
use openidconnect::core::{ use openidconnect::core::{
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey, CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata,
@@ -216,9 +216,7 @@ impl OidcConfig {
} = opts; } = opts;
if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() { if oidc_issuer_url.is_none() || oidc_client_id.is_none() || oidc_redirect_url.is_none() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!("--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"));
"--oidc-issuer-url, --oidc-client-id and --oidc-redirect-url are required when using OIDC authentication"
));
} }
if oidc_username_claim.trim().is_empty() { if oidc_username_claim.trim().is_empty() {
return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty")); return Err(anyhow::anyhow!("--oidc-username-claim cannot be empty"));
@@ -375,17 +373,18 @@ mod route {
) )
.into_response(); .into_response();
} }
if let Some(verifier) = pkce_verifier if let Some(verifier) = pkce_verifier {
&& let Err(e) = session if let Err(e) = session
.insert("oidc_pkce_verifier", verifier.secret().clone()) .insert("oidc_pkce_verifier", verifier.secret().clone())
.await .await
{ {
tracing::error!("Failed to store pkce_verifier in session: {:?}", e); tracing::error!("Failed to store pkce_verifier in session: {:?}", e);
return ( return (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(other_error("Session error")), Json(other_error("Session error")),
) )
.into_response(); .into_response();
}
} }
if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await { if let Err(e) = session.insert("oidc_pkce_used", pkce_enabled).await {
tracing::error!("Failed to store pkce_used in session: {:?}", e); tracing::error!("Failed to store pkce_used in session: {:?}", e);
+3 -3
View File
@@ -1,15 +1,15 @@
use axum::{ use axum::{
Json, Router,
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
routing::post, routing::post,
Json, Router,
}; };
use axum_login::AuthUser as _; use axum_login::AuthUser as _;
use easytier::proto::rpc_types::controller::BaseController; use easytier::proto::rpc_types::controller::BaseController;
use crate::db::UserIdInDb; use crate::db::UserIdInDb;
use super::{AppState, HttpHandleError, other_error}; use super::{other_error, AppState, HttpHandleError};
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize)]
pub struct ProxyRpcRequest { pub struct ProxyRpcRequest {
@@ -120,7 +120,7 @@ async fn handle_proxy_rpc_by_session(
return Err(( return Err((
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
other_error(format!("Unknown service: {}", service_name)).into(), other_error(format!("Unknown service: {}", service_name)).into(),
)); ))
} }
}; };
+128 -5
View File
@@ -12,6 +12,8 @@ use tokio::task;
use crate::db::{self, entity}; use crate::db::{self, entity};
const EMPTY_PASSWORD_MD5: &str = "d41d8cd98f00b204e9800998ecf8427e";
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct User { pub struct User {
pub(crate) db_user: entity::users::Model, pub(crate) db_user: entity::users::Model,
@@ -39,9 +41,9 @@ impl AuthUser for User {
fn session_auth_hash(&self) -> &[u8] { fn session_auth_hash(&self) -> &[u8] {
self.db_user.password.as_bytes() // We use the password hash as the auth self.db_user.password.as_bytes() // We use the password hash as the auth
// hash--what this means // hash--what this means
// is when the user changes their password the // is when the user changes their password the
// auth session becomes invalid. // auth session becomes invalid.
} }
} }
@@ -64,6 +66,18 @@ pub struct ChangePassword {
pub new_password: String, pub new_password: String,
} }
#[derive(Debug, thiserror::Error)]
pub enum ChangePasswordError {
#[error("Password cannot be empty")]
EmptyPassword,
#[error("User not found")]
UserNotFound,
#[error(transparent)]
Db(#[from] sea_orm::DbErr),
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Backend { pub struct Backend {
db: db::Db, db: db::Db,
@@ -119,7 +133,14 @@ impl Backend {
&self, &self,
id: <User as AuthUser>::Id, id: <User as AuthUser>::Id,
req: &ChangePassword, req: &ChangePassword,
) -> anyhow::Result<()> { ) -> Result<(), ChangePasswordError> {
// With the existing pre-hashed protocol the backend can only reject the
// exact empty-string digest; whitespace-only passwords must be blocked
// on the client before hashing.
if req.new_password == EMPTY_PASSWORD_MD5 {
return Err(ChangePasswordError::EmptyPassword);
}
let hashed_password = password_auth::generate_hash(req.new_password.as_str()); let hashed_password = password_auth::generate_hash(req.new_password.as_str());
use entity::users; use entity::users;
@@ -127,9 +148,10 @@ impl Backend {
let mut user = users::Entity::find_by_id(id) let mut user = users::Entity::find_by_id(id)
.one(self.db.orm_db()) .one(self.db.orm_db())
.await? .await?
.ok_or(anyhow::anyhow!("User not found"))? .ok_or(ChangePasswordError::UserNotFound)?
.into_active_model(); .into_active_model();
user.password = Set(hashed_password.clone()); user.password = Set(hashed_password.clone());
user.must_change_password = Set(false);
entity::users::Entity::update(user) entity::users::Entity::update(user)
.exec(self.db.orm_db()) .exec(self.db.orm_db())
@@ -242,6 +264,107 @@ impl AuthzBackend for Backend {
} }
} }
#[cfg(test)]
mod tests {
use axum_login::AuthnBackend;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
use super::{Backend, ChangePassword, ChangePasswordError, EMPTY_PASSWORD_MD5};
use crate::db::{entity::users, Db};
async fn find_user(db: &Db, username: &str) -> users::Model {
users::Entity::find()
.filter(users::Column::Username.eq(username))
.one(db.orm_db())
.await
.unwrap()
.unwrap()
}
#[tokio::test]
async fn seeded_default_users_require_password_change() {
let db = Db::memory_db().await;
assert!(find_user(&db, "admin").await.must_change_password);
assert!(find_user(&db, "user").await.must_change_password);
}
#[tokio::test]
async fn auto_created_user_does_not_require_password_change() {
let db = Db::memory_db().await;
db.auto_create_user("oidc-user").await.unwrap();
assert!(!find_user(&db, "oidc-user").await.must_change_password);
}
#[tokio::test]
async fn change_password_clears_must_change_password_flag() {
let db = Db::memory_db().await;
let backend = Backend::new(db.clone());
let admin = find_user(&db, "admin").await;
backend
.change_password(
admin.id,
&ChangePassword {
new_password: "f1086f68460b65771de50a970cd1242d".to_string(),
},
)
.await
.unwrap();
assert!(!find_user(&db, "admin").await.must_change_password);
}
#[tokio::test]
async fn change_password_rejects_empty_password_digest() {
let db = Db::memory_db().await;
let backend = Backend::new(db.clone());
let admin = find_user(&db, "admin").await;
let error = backend
.change_password(
admin.id,
&ChangePassword {
new_password: EMPTY_PASSWORD_MD5.to_string(),
},
)
.await
.unwrap_err();
assert!(matches!(error, ChangePasswordError::EmptyPassword));
assert!(find_user(&db, "admin").await.must_change_password);
}
#[tokio::test]
async fn can_authenticate_with_new_password_after_change() {
let db = Db::memory_db().await;
let backend = Backend::new(db.clone());
let admin = find_user(&db, "admin").await;
backend
.change_password(
admin.id,
&ChangePassword {
new_password: "f1086f68460b65771de50a970cd1242d".to_string(),
},
)
.await
.unwrap();
let authenticated = backend
.authenticate(super::Credentials {
username: "admin".to_string(),
password: "f1086f68460b65771de50a970cd1242d".to_string(),
})
.await
.unwrap();
assert!(authenticated.is_some());
}
}
// We use a type alias for convenience. // We use a type alias for convenience.
// //
// Note that we've supplied our concrete backend here. // Note that we've supplied our concrete backend here.
+1 -2
View File
@@ -1,9 +1,8 @@
use axum::{ use axum::{
Router,
extract::State, extract::State,
http::header, http::header,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing, routing, Router,
}; };
use axum_embed::ServeEmbed; use axum_embed::ServeEmbed;
use easytier::common::scoped_task::ScopedTask; use easytier::common::scoped_task::ScopedTask;
-1
View File
@@ -49,7 +49,6 @@ impl WebhookConfig {
pub struct ValidateTokenRequest { pub struct ValidateTokenRequest {
pub token: String, pub token: String,
pub machine_id: String, pub machine_id: String,
pub public_ip: Option<String>,
pub hostname: String, pub hostname: String,
pub version: String, pub version: String,
pub os_type: Option<String>, pub os_type: Option<String>,
-11
View File
@@ -1,11 +0,0 @@
disallowed-methods = [
{ path = "itertools::Itertools::map_into", reason = "Blocks underlying iterator optimizations. Use the native `.map(Into::into)` instead." },
{ path = "itertools::Itertools::map_ok", reason = "Blocks underlying iterator optimizations. Use the native `.map(|r| r.map(f))` instead." },
{ path = "itertools::Itertools::filter_ok", reason = "Blocks underlying iterator optimizations. Use a native approach, e.g., `.filter(|r| r.as_ref().map_or(true, condition))`." },
{ path = "itertools::Itertools::filter_map_ok", reason = "Blocks underlying iterator optimizations. Use native `.map()` and `.flatten()`, or extract logic into a standard `.filter_map()`." },
{ path = "itertools::Itertools::collect_vec", reason = "Non-standard idiom. Directly use the standard library's `.collect::<Vec<_>>()`." },
{ path = "itertools::Itertools::try_collect", reason = "Non-standard idiom. Standard `collect()` already supports Result/Option inversion; use `.collect::<Result<_, _>>()`." },
{ path = "itertools::Itertools::set_from", reason = "Non-standard idiom. Directly use the `.extend()` method provided by the standard library's `Extend` trait." },
{ path = "itertools::Itertools::concat", reason = "Non-standard idiom. Use native `.flatten().collect()` or a slice's `.concat()` instead." }
]
+15 -8
View File
@@ -4,11 +4,11 @@ description = "A full meshed p2p VPN, connecting all your devices in one network
homepage = "https://github.com/EasyTier/EasyTier" homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier"
version = "2.6.0" version = "2.6.0"
edition.workspace = true edition = "2021"
rust-version.workspace = true
authors = ["kkrainbow"] authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"] keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"] categories = ["network-programming", "command-line-utilities"]
rust-version = "1.93.0"
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"
@@ -50,7 +50,7 @@ time = "0.3"
toml = "0.8.12" toml = "0.8.12"
chrono = { version = "0.4.37", features = ["serde"] } chrono = { version = "0.4.37", features = ["serde"] }
delegate = "0.13.5" cfg-if = "1.0"
itertools = "0.14.0" itertools = "0.14.0"
@@ -165,6 +165,7 @@ network-interface = "2.0"
# for ospf route # for ospf route
petgraph = "0.8.1" petgraph = "0.8.1"
hashbrown = "0.15.3"
ordered_hash_map = "0.5.0" ordered_hash_map = "0.5.0"
# for wireguard # for wireguard
@@ -241,7 +242,6 @@ hickory-server = { version = "0.25.2", features = [
"resolver", "resolver",
], optional = true } ], optional = true }
bon = "3.9.1"
derive_builder = "0.20.2" derive_builder = "0.20.2"
humantime-serde = "1.1.1" humantime-serde = "1.1.1"
multimap = "0.10.1" multimap = "0.10.1"
@@ -324,14 +324,22 @@ easytier-rpc-build = { path = "../easytier-rpc-build", features = [
"internal-namespace", "internal-namespace",
] } ] }
prost-reflect-build = { version = "0.14.0" } prost-reflect-build = { version = "0.14.0" }
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
reqwest = { version = "0.12.12", features = ["blocking"] } reqwest = { version = "0.12.12", features = ["blocking"] }
zip = "4.0.0" zip = "4.0.0"
# enable thunk-rs when compiling for x86_64 or i686 windows
[target.x86_64-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
[target.i686-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
"win7",
] }
[dev-dependencies] [dev-dependencies]
serial_test = "3.0.0" serial_test = "3.0.0"
@@ -339,7 +347,6 @@ rstest = "0.25.0"
futures-util = "0.3.31" futures-util = "0.3.31"
maplit = "1.0.2" maplit = "1.0.2"
tempfile = "3.22.0" tempfile = "3.22.0"
ctor = "0.8.0"
[target.'cfg(target_os = "linux")'.dev-dependencies] [target.'cfg(target_os = "linux")'.dev-dependencies]
defguard_wireguard_rs = "0.4.2" defguard_wireguard_rs = "0.4.2"
+6 -6
View File
@@ -86,9 +86,7 @@ impl WindowsBuild {
} else { } else {
Self::download_protoc() Self::download_protoc()
}; };
unsafe { std::env::set_var("PROTOC", protoc_path);
std::env::set_var("PROTOC", protoc_path);
}
} }
} }
@@ -143,10 +141,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
// enable thunk-rs when target os is windows and arch is x86_64 or i686 // enable thunk-rs when target os is windows and arch is x86_64 or i686
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") { #[cfg(target_os = "windows")]
if !std::env::var("TARGET")
.unwrap_or_default()
.contains("aarch64")
{
thunk::thunk(); thunk::thunk();
} }
+5 -5
View File
@@ -3,6 +3,7 @@ use std::{io, mem::ManuallyDrop, net::SocketAddr, os::windows::io::AsRawSocket};
use anyhow::Context; use anyhow::Context;
use network_interface::NetworkInterfaceConfig; use network_interface::NetworkInterfaceConfig;
use windows::{ use windows::{
core::BSTR,
Win32::{ Win32::{
Foundation::{BOOL, FALSE}, Foundation::{BOOL, FALSE},
NetworkManagement::WindowsFirewall::{ NetworkManagement::WindowsFirewall::{
@@ -11,16 +12,15 @@ use windows::{
NET_FW_RULE_DIR_OUT, NET_FW_RULE_DIR_OUT,
}, },
Networking::WinSock::{ Networking::WinSock::{
IP_UNICAST_IF, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6,
SOCKET_ERROR, WSAGetLastError, WSAIoctl, htonl, setsockopt, IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR,
}, },
System::Com::{ System::Com::{
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
}, },
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement}, System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT}, System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
}, },
core::BSTR,
}; };
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> { pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
@@ -345,7 +345,7 @@ fn add_protocol_firewall_rules(
SafeArrayPutElement( SafeArrayPutElement(
interface_array, interface_array,
&index as *const _, &index as *const _ as *const i32,
&variant_interface as *const _ as *const std::ffi::c_void, &variant_interface as *const _ as *const std::ffi::c_void,
)?; )?;
+20 -20
View File
@@ -345,7 +345,7 @@ impl AclProcessor {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Sort by priority (higher priority first) // Sort by priority (higher priority first)
rules.sort_by_key(|r| std::cmp::Reverse(r.priority)); rules.sort_by(|a, b| b.priority.cmp(&a.priority));
match chain.chain_type() { match chain.chain_type() {
ChainType::Inbound => inbound_rules.extend(rules), ChainType::Inbound => inbound_rules.extend(rules),
@@ -507,7 +507,7 @@ impl AclProcessor {
matched_rule: Some(RuleId::Default), matched_rule: Some(RuleId::Default),
should_log: false, should_log: false,
log_context: Some(AclLogContext::UnsupportedChainType), log_context: Some(AclLogContext::UnsupportedChainType),
}; }
} }
}; };
@@ -679,28 +679,28 @@ impl AclProcessor {
} }
// Source port check // Source port check
if let Some(src_port) = packet_info.src_port if let Some(src_port) = packet_info.src_port {
&& !rule.src_port_ranges.is_empty() if !rule.src_port_ranges.is_empty() {
{ let matches = rule
let matches = rule .src_port_ranges
.src_port_ranges .iter()
.iter() .any(|(start, end)| src_port >= *start && src_port <= *end);
.any(|(start, end)| src_port >= *start && src_port <= *end); if !matches {
if !matches { return false;
return false; }
} }
} }
// Destination port check // Destination port check
if let Some(dst_port) = packet_info.dst_port if let Some(dst_port) = packet_info.dst_port {
&& !rule.dst_port_ranges.is_empty() if !rule.dst_port_ranges.is_empty() {
{ let matches = rule
let matches = rule .dst_port_ranges
.dst_port_ranges .iter()
.iter() .any(|(start, end)| dst_port >= *start && dst_port <= *end);
.any(|(start, end)| dst_port >= *start && dst_port <= *end); if !matches {
if !matches { return false;
return false; }
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ use zstd::bulk;
use zerocopy::{AsBytes as _, FromBytes as _}; use zerocopy::{AsBytes as _, FromBytes as _};
use crate::tunnel::packet_def::{COMPRESSOR_TAIL_SIZE, CompressorAlgo, CompressorTail, ZCPacket}; use crate::tunnel::packet_def::{CompressorAlgo, CompressorTail, ZCPacket, COMPRESSOR_TAIL_SIZE};
type Error = anyhow::Error; type Error = anyhow::Error;
+68 -69
View File
@@ -6,9 +6,10 @@ use std::{
}; };
use anyhow::Context; use anyhow::Context;
use base64::{Engine as _, prelude::BASE64_STANDARD}; use base64::{prelude::BASE64_STANDARD, Engine as _};
use clap::ValueEnum; use cfg_if::cfg_if;
use clap::builder::PossibleValue; use clap::builder::PossibleValue;
use clap::ValueEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, VariantArray}; use strum::{Display, EnumString, VariantArray};
use tokio::io::AsyncReadExt as _; use tokio::io::AsyncReadExt as _;
@@ -108,9 +109,10 @@ impl ValueEnum for EncryptionAlgorithm {
#[allow(clippy::derivable_impls)] #[allow(clippy::derivable_impls)]
impl Default for EncryptionAlgorithm { impl Default for EncryptionAlgorithm {
fn default() -> Self { fn default() -> Self {
cfg_select! { cfg_if! {
any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto") => EncryptionAlgorithm::AesGcm, if #[cfg(any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto"))] {
_ => { EncryptionAlgorithm::AesGcm
} else {
crate::common::log::warn!("no AEAD encryption algorithm is available, using INSECURE XOR"); crate::common::log::warn!("no AEAD encryption algorithm is available, using INSECURE XOR");
EncryptionAlgorithm::Xor EncryptionAlgorithm::Xor
} }
@@ -619,14 +621,14 @@ impl ConfigLoader for TomlConfigLoader {
if locked_config.proxy_network.is_none() { if locked_config.proxy_network.is_none() {
locked_config.proxy_network = Some(vec![]); locked_config.proxy_network = Some(vec![]);
} }
if let Some(mapped_cidr) = mapped_cidr.as_ref() if let Some(mapped_cidr) = mapped_cidr.as_ref() {
&& cidr.network_length() != mapped_cidr.network_length() if cidr.network_length() != mapped_cidr.network_length() {
{ return Err(anyhow::anyhow!(
return Err(anyhow::anyhow!( "Mapped CIDR must have the same network length as the original CIDR: {} != {}",
"Mapped CIDR must have the same network length as the original CIDR: {} != {}", cidr.network_length(),
cidr.network_length(), mapped_cidr.network_length()
mapped_cidr.network_length() ));
)); }
} }
// insert if no duplicate // insert if no duplicate
if !locked_config if !locked_config
@@ -879,10 +881,10 @@ impl ConfigLoader for TomlConfigLoader {
let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default(); let mut flag_map: serde_json::Map<String, serde_json::Value> = Default::default();
for (key, value) in default_flags_hashmap { for (key, value) in default_flags_hashmap {
if let Some(v) = cur_flags_hashmap.get(&key) if let Some(v) = cur_flags_hashmap.get(&key) {
&& *v != value if *v != value {
{ flag_map.insert(key, v.clone());
flag_map.insert(key, v.clone()); }
} }
} }
@@ -1087,7 +1089,6 @@ pub async fn load_config_from_file(
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use super::*;
use crate::tests::{remove_env_var, set_env_var};
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
@@ -1211,8 +1212,8 @@ proto = "tcp"
#[tokio::test] #[tokio::test]
async fn test_env_var_expansion_and_readonly_flag() { async fn test_env_var_expansion_and_readonly_flag() {
// 设置测试环境变量 // 设置测试环境变量
set_env_var("TEST_SECRET", "my-test-secret-123"); std::env::set_var("TEST_SECRET", "my-test-secret-123");
set_env_var("TEST_NETWORK", "test-network"); std::env::set_var("TEST_NETWORK", "test-network");
// 创建临时配置文件,包含环境变量占位符 // 创建临时配置文件,包含环境变量占位符
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
@@ -1252,8 +1253,8 @@ network_secret = "${TEST_SECRET}"
); );
// 清理环境变量 // 清理环境变量
remove_env_var("TEST_SECRET"); std::env::remove_var("TEST_SECRET");
remove_env_var("TEST_NETWORK"); std::env::remove_var("TEST_NETWORK");
} }
/// RPC API 安全测试(只读配置保护) /// RPC API 安全测试(只读配置保护)
@@ -1266,7 +1267,7 @@ network_secret = "${TEST_SECRET}"
/// `easytier/src/rpc_service/instance_manage.rs` 中实现 /// `easytier/src/rpc_service/instance_manage.rs` 中实现
#[tokio::test] #[tokio::test]
async fn test_readonly_config_api_protection() { async fn test_readonly_config_api_protection() {
set_env_var("API_TEST_SECRET", "secret-value"); std::env::set_var("API_TEST_SECRET", "secret-value");
// 创建包含环境变量的配置 // 创建包含环境变量的配置
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
@@ -1297,7 +1298,7 @@ network_secret = "${API_TEST_SECRET}"
"Permission flag should be set correctly" "Permission flag should be set correctly"
); );
remove_env_var("API_TEST_SECRET"); std::env::remove_var("API_TEST_SECRET");
} }
/// CLI 参数测试(--disable-env-parsing 开关) /// CLI 参数测试(--disable-env-parsing 开关)
@@ -1307,7 +1308,7 @@ network_secret = "${API_TEST_SECRET}"
/// - 配置不会被标记为只读 /// - 配置不会被标记为只读
#[tokio::test] #[tokio::test]
async fn test_disable_env_parsing_flag() { async fn test_disable_env_parsing_flag() {
set_env_var("DISABLED_TEST_VAR", "should-not-expand"); std::env::set_var("DISABLED_TEST_VAR", "should-not-expand");
// 创建包含环境变量占位符的配置 // 创建包含环境变量占位符的配置
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
@@ -1345,7 +1346,7 @@ network_secret = "${DISABLED_TEST_VAR}"
"Config should be NO_DELETE due to no config_dir, not env vars" "Config should be NO_DELETE due to no config_dir, not env vars"
); );
remove_env_var("DISABLED_TEST_VAR"); std::env::remove_var("DISABLED_TEST_VAR");
} }
/// 多实例隔离测试 /// 多实例隔离测试
@@ -1356,8 +1357,8 @@ network_secret = "${DISABLED_TEST_VAR}"
#[tokio::test] #[tokio::test]
async fn test_multiple_instances_with_different_env_vars() { async fn test_multiple_instances_with_different_env_vars() {
// 实例1:使用第一组环境变量 // 实例1:使用第一组环境变量
set_env_var("INSTANCE_SECRET", "instance1-secret"); std::env::set_var("INSTANCE_SECRET", "instance1-secret");
set_env_var("INSTANCE_NAME", "instance-one"); std::env::set_var("INSTANCE_NAME", "instance-one");
let mut temp_file1 = NamedTempFile::new().unwrap(); let mut temp_file1 = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1387,8 +1388,8 @@ network_secret = "${INSTANCE_SECRET}"
); );
// 实例2:修改环境变量后加载同一模板 // 实例2:修改环境变量后加载同一模板
set_env_var("INSTANCE_SECRET", "instance2-secret"); std::env::set_var("INSTANCE_SECRET", "instance2-secret");
set_env_var("INSTANCE_NAME", "instance-two"); std::env::set_var("INSTANCE_NAME", "instance-two");
let mut temp_file2 = NamedTempFile::new().unwrap(); let mut temp_file2 = NamedTempFile::new().unwrap();
temp_file2.write_all(config_content.as_bytes()).unwrap(); temp_file2.write_all(config_content.as_bytes()).unwrap();
@@ -1418,8 +1419,8 @@ network_secret = "${INSTANCE_SECRET}"
); );
// 清理 // 清理
remove_env_var("INSTANCE_SECRET"); std::env::remove_var("INSTANCE_SECRET");
remove_env_var("INSTANCE_NAME"); std::env::remove_var("INSTANCE_NAME");
} }
/// 实际配置字段测试(network_secret、peer.uri 等) /// 实际配置字段测试(network_secret、peer.uri 等)
@@ -1432,11 +1433,11 @@ network_secret = "${INSTANCE_SECRET}"
#[tokio::test] #[tokio::test]
async fn test_real_config_fields_expansion() { async fn test_real_config_fields_expansion() {
// 设置各种实际场景的环境变量 // 设置各种实际场景的环境变量
set_env_var("ET_SECRET", "production-secret-key"); std::env::set_var("ET_SECRET", "production-secret-key");
set_env_var("PEER_HOST", "peer.example.com"); std::env::set_var("PEER_HOST", "peer.example.com");
set_env_var("PEER_PORT", "11011"); std::env::set_var("PEER_PORT", "11011");
set_env_var("LISTEN_PORT", "11010"); std::env::set_var("LISTEN_PORT", "11010");
set_env_var("NETWORK_NAME", "prod-network"); std::env::set_var("NETWORK_NAME", "prod-network");
// 创建包含多个实际字段的完整配置 // 创建包含多个实际字段的完整配置
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
@@ -1484,11 +1485,11 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
assert!(control.is_no_delete()); assert!(control.is_no_delete());
// 清理环境变量 // 清理环境变量
remove_env_var("ET_SECRET"); std::env::remove_var("ET_SECRET");
remove_env_var("PEER_HOST"); std::env::remove_var("PEER_HOST");
remove_env_var("PEER_PORT"); std::env::remove_var("PEER_PORT");
remove_env_var("LISTEN_PORT"); std::env::remove_var("LISTEN_PORT");
remove_env_var("NETWORK_NAME"); std::env::remove_var("NETWORK_NAME");
} }
/// 带默认值的环境变量 /// 带默认值的环境变量
@@ -1498,8 +1499,8 @@ uri = "tcp://${PEER_HOST}:${PEER_PORT}"
#[tokio::test] #[tokio::test]
async fn test_env_var_with_default_value() { async fn test_env_var_with_default_value() {
// 确保变量未定义 // 确保变量未定义
remove_env_var("UNDEFINED_PORT"); std::env::remove_var("UNDEFINED_PORT");
remove_env_var("UNDEFINED_SECRET"); std::env::remove_var("UNDEFINED_SECRET");
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1540,7 +1541,7 @@ network_secret = "${UNDEFINED_SECRET:-default-secret}"
/// - 未定义的环境变量保持原样(shellexpand 的默认行为) /// - 未定义的环境变量保持原样(shellexpand 的默认行为)
#[tokio::test] #[tokio::test]
async fn test_undefined_env_var_without_default() { async fn test_undefined_env_var_without_default() {
remove_env_var("COMPLETELY_UNDEFINED"); std::env::remove_var("COMPLETELY_UNDEFINED");
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1570,8 +1571,6 @@ network_secret = "${COMPLETELY_UNDEFINED}"
// 注意:由于没有实际替换发生,控制标记不应因环境变量而设置 // 注意:由于没有实际替换发生,控制标记不应因环境变量而设置
// 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE // 但会因为其他原因(如没有 config_dir)被标记为 NO_DELETE
// 这里我们主要验证 NO_DELETE 标记的逻辑
// 由于没有 config_dir,文件会被标记为 NO_DELETE,但不是因为环境变量
assert!(control.is_no_delete()); assert!(control.is_no_delete());
} }
@@ -1583,9 +1582,9 @@ network_secret = "${COMPLETELY_UNDEFINED}"
#[tokio::test] #[tokio::test]
async fn test_boolean_type_env_vars() { async fn test_boolean_type_env_vars() {
// 设置布尔类型的环境变量 // 设置布尔类型的环境变量
set_env_var("ENABLE_DHCP", "true"); std::env::set_var("ENABLE_DHCP", "true");
set_env_var("ENABLE_ENCRYPTION", "false"); std::env::set_var("ENABLE_ENCRYPTION", "false");
set_env_var("ENABLE_IPV6", "true"); std::env::set_var("ENABLE_IPV6", "true");
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1623,9 +1622,9 @@ enable_ipv6 = ${ENABLE_IPV6}
assert!(control.is_no_delete()); assert!(control.is_no_delete());
// 清理 // 清理
remove_env_var("ENABLE_DHCP"); std::env::remove_var("ENABLE_DHCP");
remove_env_var("ENABLE_ENCRYPTION"); std::env::remove_var("ENABLE_ENCRYPTION");
remove_env_var("ENABLE_IPV6"); std::env::remove_var("ENABLE_IPV6");
} }
/// 数字类型环境变量 /// 数字类型环境变量
@@ -1636,8 +1635,8 @@ enable_ipv6 = ${ENABLE_IPV6}
#[tokio::test] #[tokio::test]
async fn test_numeric_type_env_vars() { async fn test_numeric_type_env_vars() {
// 设置数字类型的环境变量 // 设置数字类型的环境变量
set_env_var("MTU_VALUE", "1400"); std::env::set_var("MTU_VALUE", "1400");
set_env_var("THREAD_COUNT", "4"); std::env::set_var("THREAD_COUNT", "4");
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1672,8 +1671,8 @@ multi_thread_count = ${THREAD_COUNT}
assert!(control.is_no_delete()); assert!(control.is_no_delete());
// 清理 // 清理
remove_env_var("MTU_VALUE"); std::env::remove_var("MTU_VALUE");
remove_env_var("THREAD_COUNT"); std::env::remove_var("THREAD_COUNT");
} }
/// 混合类型环境变量 /// 混合类型环境变量
@@ -1685,12 +1684,12 @@ multi_thread_count = ${THREAD_COUNT}
#[tokio::test] #[tokio::test]
async fn test_mixed_type_env_vars() { async fn test_mixed_type_env_vars() {
// 设置不同类型的环境变量 // 设置不同类型的环境变量
set_env_var("MIXED_SECRET", "mixed-secret-key"); std::env::set_var("MIXED_SECRET", "mixed-secret-key");
set_env_var("MIXED_NETWORK", "production"); std::env::set_var("MIXED_NETWORK", "production");
set_env_var("MIXED_DHCP", "true"); std::env::set_var("MIXED_DHCP", "true");
set_env_var("MIXED_MTU", "1500"); std::env::set_var("MIXED_MTU", "1500");
set_env_var("MIXED_ENCRYPTION", "false"); std::env::set_var("MIXED_ENCRYPTION", "false");
set_env_var("MIXED_LISTEN_PORT", "12345"); std::env::set_var("MIXED_LISTEN_PORT", "12345");
let mut temp_file = NamedTempFile::new().unwrap(); let mut temp_file = NamedTempFile::new().unwrap();
let config_content = r#" let config_content = r#"
@@ -1742,11 +1741,11 @@ enable_encryption = ${MIXED_ENCRYPTION}
assert!(control.is_no_delete()); assert!(control.is_no_delete());
// 清理 // 清理
remove_env_var("MIXED_SECRET"); std::env::remove_var("MIXED_SECRET");
remove_env_var("MIXED_NETWORK"); std::env::remove_var("MIXED_NETWORK");
remove_env_var("MIXED_DHCP"); std::env::remove_var("MIXED_DHCP");
remove_env_var("MIXED_MTU"); std::env::remove_var("MIXED_MTU");
remove_env_var("MIXED_ENCRYPTION"); std::env::remove_var("MIXED_ENCRYPTION");
remove_env_var("MIXED_LISTEN_PORT"); std::env::remove_var("MIXED_LISTEN_PORT");
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use hickory_proto::runtime::TokioRuntimeProvider; use hickory_proto::runtime::TokioRuntimeProvider;
+20 -21
View File
@@ -42,11 +42,10 @@ pub fn expand_env_vars(text: &str) -> (String, bool) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::{remove_env_var, set_env_var};
#[test] #[test]
fn test_expand_standard_syntax() { fn test_expand_standard_syntax() {
set_env_var("TEST_VAR_STANDARD", "test_value"); std::env::set_var("TEST_VAR_STANDARD", "test_value");
let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}"); let (result, changed) = expand_env_vars("secret=${TEST_VAR_STANDARD}");
assert_eq!(result, "secret=test_value"); assert_eq!(result, "secret=test_value");
assert!(changed); assert!(changed);
@@ -54,7 +53,7 @@ mod tests {
#[test] #[test]
fn test_expand_short_syntax() { fn test_expand_short_syntax() {
set_env_var("TEST_VAR_SHORT", "short_value"); std::env::set_var("TEST_VAR_SHORT", "short_value");
let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT"); let (result, changed) = expand_env_vars("key=$TEST_VAR_SHORT");
assert_eq!(result, "key=short_value"); assert_eq!(result, "key=short_value");
assert!(changed); assert!(changed);
@@ -63,7 +62,7 @@ mod tests {
#[test] #[test]
fn test_expand_with_default() { fn test_expand_with_default() {
// 确保变量未定义 // 确保变量未定义
remove_env_var("UNDEFINED_VAR_WITH_DEFAULT"); std::env::remove_var("UNDEFINED_VAR_WITH_DEFAULT");
let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}"); let (result, changed) = expand_env_vars("port=${UNDEFINED_VAR_WITH_DEFAULT:-8080}");
assert_eq!(result, "port=8080"); assert_eq!(result, "port=8080");
assert!(changed); assert!(changed);
@@ -85,8 +84,8 @@ mod tests {
#[test] #[test]
fn test_multiple_vars() { fn test_multiple_vars() {
set_env_var("VAR1", "value1"); std::env::set_var("VAR1", "value1");
set_env_var("VAR2", "value2"); std::env::set_var("VAR2", "value2");
let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}"); let (result, changed) = expand_env_vars("${VAR1} and ${VAR2}");
assert_eq!(result, "value1 and value2"); assert_eq!(result, "value1 and value2");
assert!(changed); assert!(changed);
@@ -95,7 +94,7 @@ mod tests {
#[test] #[test]
fn test_undefined_var_without_default() { fn test_undefined_var_without_default() {
// 确保变量未定义 // 确保变量未定义
remove_env_var("COMPLETELY_UNDEFINED_VAR"); std::env::remove_var("COMPLETELY_UNDEFINED_VAR");
let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}"); let (result, changed) = expand_env_vars("value=${COMPLETELY_UNDEFINED_VAR}");
// shellexpand::env 对未定义的变量会保持原样 // shellexpand::env 对未定义的变量会保持原样
assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}"); assert_eq!(result, "value=${COMPLETELY_UNDEFINED_VAR}");
@@ -104,8 +103,8 @@ mod tests {
#[test] #[test]
fn test_complex_toml_config() { fn test_complex_toml_config() {
set_env_var("ET_SECRET", "my-secret-key"); std::env::set_var("ET_SECRET", "my-secret-key");
set_env_var("ET_PORT", "11010"); std::env::set_var("ET_PORT", "11010");
let config = r#" let config = r#"
[network_identity] [network_identity]
@@ -124,7 +123,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_escape_syntax_double_dollar() { fn test_escape_syntax_double_dollar() {
set_env_var("ESCAPED_VAR", "should_not_expand"); std::env::set_var("ESCAPED_VAR", "should_not_expand");
// shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $ // shellexpand 使用 $$ 作为转义序列,表示字面量的单个 $
// $$ 会被转义为单个 $,不会触发变量扩展 // $$ 会被转义为单个 $,不会触发变量扩展
let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}"); let (result, changed) = expand_env_vars("value=$${ESCAPED_VAR}");
@@ -134,7 +133,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_escape_syntax_backslash() { fn test_escape_syntax_backslash() {
set_env_var("ESCAPED_VAR", "should_not_expand"); std::env::set_var("ESCAPED_VAR", "should_not_expand");
// shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值> // shellexpand 中反斜杠转义的行为:\$ 会展开为 \<变量值>
// 这不是推荐的转义方式,此测试仅为记录实际行为 // 这不是推荐的转义方式,此测试仅为记录实际行为
let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}"); let (result, changed) = expand_env_vars(r"value=\${ESCAPED_VAR}");
@@ -144,7 +143,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_multiple_dollar_signs() { fn test_multiple_dollar_signs() {
set_env_var("TEST_VAR", "value"); std::env::set_var("TEST_VAR", "value");
// 测试多个连续的 $ 符号 // 测试多个连续的 $ 符号
let (result1, changed1) = expand_env_vars("$$"); let (result1, changed1) = expand_env_vars("$$");
assert_eq!(result1, "$"); assert_eq!(result1, "$");
@@ -162,7 +161,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_empty_var_value() { fn test_empty_var_value() {
set_env_var("EMPTY_VAR", ""); std::env::set_var("EMPTY_VAR", "");
let (result, changed) = expand_env_vars("value=${EMPTY_VAR}"); let (result, changed) = expand_env_vars("value=${EMPTY_VAR}");
// 变量存在但值为空 // 变量存在但值为空
assert_eq!(result, "value="); assert_eq!(result, "value=");
@@ -171,7 +170,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_default_with_special_chars() { fn test_default_with_special_chars() {
remove_env_var("UNDEFINED_SPECIAL"); std::env::remove_var("UNDEFINED_SPECIAL");
// 测试默认值包含冒号、等号、空格等特殊字符 // 测试默认值包含冒号、等号、空格等特殊字符
let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}"); let (result, changed) = expand_env_vars("url=${UNDEFINED_SPECIAL:-http://localhost:8080}");
assert_eq!(result, "url=http://localhost:8080"); assert_eq!(result, "url=http://localhost:8080");
@@ -188,9 +187,9 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_var_name_with_numbers_underscores() { fn test_var_name_with_numbers_underscores() {
set_env_var("VAR_123", "num_value"); std::env::set_var("VAR_123", "num_value");
set_env_var("_VAR", "underscore_prefix"); std::env::set_var("_VAR", "underscore_prefix");
set_env_var("VAR_", "underscore_suffix"); std::env::set_var("VAR_", "underscore_suffix");
let (result1, changed1) = expand_env_vars("${VAR_123}"); let (result1, changed1) = expand_env_vars("${VAR_123}");
assert_eq!(result1, "num_value"); assert_eq!(result1, "num_value");
@@ -215,7 +214,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
// 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理 // 注意:未闭合的 ${VAR 实际上 shellexpand 会当作普通文本处理
// 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾) // 它会尝试查找名为 "VAR" 的环境变量(到字符串末尾)
remove_env_var("VAR"); std::env::remove_var("VAR");
let (result2, _changed2) = expand_env_vars("incomplete ${VAR"); let (result2, _changed2) = expand_env_vars("incomplete ${VAR");
// 如果 VAR 未定义,shellexpand 会返回错误或保持原样 // 如果 VAR 未定义,shellexpand 会返回错误或保持原样
assert_eq!(result2, "incomplete ${VAR"); assert_eq!(result2, "incomplete ${VAR");
@@ -225,8 +224,8 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_mixed_defined_undefined_vars() { fn test_mixed_defined_undefined_vars() {
set_env_var("DEFINED_VAR", "defined"); std::env::set_var("DEFINED_VAR", "defined");
remove_env_var("UNDEFINED_VAR"); std::env::remove_var("UNDEFINED_VAR");
// 混合已定义和未定义的变量 // 混合已定义和未定义的变量
// shellexpand::env 在遇到未定义变量时会返回错误(默认行为) // shellexpand::env 在遇到未定义变量时会返回错误(默认行为)
@@ -238,7 +237,7 @@ uri = "tcp://127.0.0.1:${ET_PORT}"
#[test] #[test]
fn test_nested_braces() { fn test_nested_braces() {
set_env_var("OUTER", "outer_value"); std::env::set_var("OUTER", "outer_value");
// 嵌套的大括号是无效语法,shellexpand::env 会返回错误 // 嵌套的大括号是无效语法,shellexpand::env 会返回错误
let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}"); let (result, changed) = expand_env_vars("${OUTER} and ${{INNER}}");
// 由于语法错误,整个字符串保持不变 // 由于语法错误,整个字符串保持不变
+2 -21
View File
@@ -1,5 +1,5 @@
use std::{ use std::{
collections::{HashMap, hash_map::DefaultHasher}, collections::{hash_map::DefaultHasher, HashMap},
hash::Hasher, hash::Hasher,
net::{IpAddr, SocketAddr}, net::{IpAddr, SocketAddr},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@@ -10,11 +10,11 @@ use arc_swap::ArcSwap;
use dashmap::DashMap; use dashmap::DashMap;
use super::{ use super::{
PeerId,
config::{ConfigLoader, Flags}, config::{ConfigLoader, Flags},
netns::NetNS, netns::NetNS,
network::IPCollector, network::IPCollector,
stun::{StunInfoCollector, StunInfoCollectorTrait}, stun::{StunInfoCollector, StunInfoCollectorTrait},
PeerId,
}; };
use crate::{ use crate::{
common::{ common::{
@@ -28,7 +28,6 @@ use crate::{
common::{PeerFeatureFlag, PortForwardConfigPb}, common::{PeerFeatureFlag, PortForwardConfigPb},
peer_rpc::PeerGroupInfo, peer_rpc::PeerGroupInfo,
}, },
rpc_service::protected_port,
tunnel::matches_protocol, tunnel::matches_protocol,
}; };
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
@@ -659,7 +658,6 @@ impl GlobalCtx {
if dst_is_local_virtual_ip || dst_is_local_phy_ip { if dst_is_local_virtual_ip || dst_is_local_phy_ip {
// if is local ip, make sure the port is not one of the listening ports // if is local ip, make sure the port is not one of the listening ports
self.is_port_in_running_listeners(dst_addr.port(), is_udp) self.is_port_in_running_listeners(dst_addr.port(), is_udp)
|| (!is_udp && protected_port::is_protected_tcp_port(dst_addr.port()))
} else { } else {
false false
} }
@@ -767,23 +765,6 @@ pub mod tests {
assert!(feature_flags.is_public_server); assert!(feature_flags.is_public_server);
} }
#[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();
}
pub fn get_mock_global_ctx_with_network( pub fn get_mock_global_ctx_with_network(
network_identy: Option<NetworkIdentity>, network_identy: Option<NetworkIdentity>,
) -> ArcGlobalCtx { ) -> ArcGlobalCtx {
+3 -3
View File
@@ -1,6 +1,6 @@
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use super::{Error, IfConfiguerTrait, cidr_to_subnet_mask, run_shell_cmd}; use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
use async_trait::async_trait; use async_trait::async_trait;
use cidr::{Ipv4Inet, Ipv6Inet}; use cidr::{Ipv4Inet, Ipv6Inet};
@@ -53,8 +53,8 @@ impl IfConfiguerTrait for MacIfConfiger {
) -> Result<(), Error> { ) -> Result<(), Error> {
run_shell_cmd( run_shell_cmd(
format!( format!(
"ifconfig {} {:?}/{:?} {:?} up", "ifconfig {} {:?}/{:?} 10.8.8.8 up",
name, address, cidr_prefix, address, name, address, cidr_prefix,
) )
.as_str(), .as_str(),
) )
+2 -2
View File
@@ -119,8 +119,8 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> {
.creation_flags(CREATE_NO_WINDOW) .creation_flags(CREATE_NO_WINDOW)
.output() .output()
.await?; .await?;
stdout = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stdout.as_slice()); stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
stderr = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stderr.as_slice()); stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
+6 -6
View File
@@ -10,27 +10,27 @@ use anyhow::Context;
use async_trait::async_trait; use async_trait::async_trait;
use cidr::{IpInet, Ipv4Inet, Ipv6Inet}; use cidr::{IpInet, Ipv4Inet, Ipv6Inet};
use netlink_packet_core::{ use netlink_packet_core::{
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST, NetlinkDeserializable, NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST,
}; };
use netlink_packet_route::{ use netlink_packet_route::{
AddressFamily, RouteNetlinkMessage,
address::{AddressAttribute, AddressMessage}, address::{AddressAttribute, AddressMessage},
route::{ route::{
RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope, RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope,
RouteType, RouteType,
}, },
AddressFamily, RouteNetlinkMessage,
}; };
use netlink_sys::{Socket, SocketAddr, protocols::NETLINK_ROUTE}; use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr};
use nix::{ use nix::{
ifaddrs::getifaddrs, ifaddrs::getifaddrs,
libc::{self, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU, ifreq, ioctl}, libc::{self, ifreq, ioctl, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU},
net::if_::InterfaceFlags, net::if_::InterfaceFlags,
sys::socket::SockaddrLike as _, sys::socket::SockaddrLike as _,
}; };
use pnet::ipnetwork::ip_mask_to_prefix; use pnet::ipnetwork::ip_mask_to_prefix;
use super::{Error, IfConfiguerTrait, route::Route}; use super::{route::Route, Error, IfConfiguerTrait};
pub(crate) fn dummy_socket() -> Result<std::net::UdpSocket, Error> { pub(crate) fn dummy_socket() -> Result<std::net::UdpSocket, Error> {
Ok(std::net::UdpSocket::bind("0:0")?) Ok(std::net::UdpSocket::bind("0:0")?)
+5 -1
View File
@@ -740,6 +740,10 @@ impl InterfaceLuid {
// SAFETY: TODO // SAFETY: TODO
let ret = unsafe { SetIpInterfaceEntry(&mut row) }; let ret = unsafe { SetIpInterfaceEntry(&mut row) };
if NO_ERROR == ret { Ok(()) } else { Err(ret) } if NO_ERROR == ret {
Ok(())
} else {
Err(ret)
}
} }
} }
+11 -10
View File
@@ -10,14 +10,14 @@ use std::{
}; };
use windows_sys::Win32::{ use windows_sys::Win32::{
Foundation::NO_ERROR, Foundation::NO_ERROR,
NetworkManagement::IpHelper::{GetIfEntry, MIB_IFROW, SetIfEntry}, NetworkManagement::IpHelper::{GetIfEntry, SetIfEntry, MIB_IFROW},
System::Diagnostics::Debug::{ System::Diagnostics::Debug::{
FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS, FormatMessageW, FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS,
}, },
}; };
use winreg::{ use winreg::{
RegKey,
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE}, enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
RegKey,
}; };
use super::{Error, IfConfiguerTrait}; use super::{Error, IfConfiguerTrait};
@@ -331,7 +331,7 @@ impl RegistryManager {
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_"; r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> { pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS}; use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let profiles_key = hklm.open_subkey_with_flags( let profiles_key = hklm.open_subkey_with_flags(
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles", "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
@@ -405,7 +405,7 @@ impl RegistryManager {
} }
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> { pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS}; use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let profiles_key = hklm.open_subkey_with_flags( let profiles_key = hklm.open_subkey_with_flags(
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles", "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
@@ -448,11 +448,12 @@ impl RegistryManager {
for guid in network_key.enum_keys().map_while(Result::ok) { for guid in network_key.enum_keys().map_while(Result::ok) {
if let Ok(guid_key) = network_key.open_subkey_with_flags(&guid, KEY_READ) { if let Ok(guid_key) = network_key.open_subkey_with_flags(&guid, KEY_READ) {
// 检查 Connection/Name 是否匹配目标接口名 // 检查 Connection/Name 是否匹配目标接口名
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ) if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ) {
&& let Ok(name) = conn_key.get_value::<String, _>("Name") if let Ok(name) = conn_key.get_value::<String, _>("Name") {
&& name == interface_name if name == interface_name {
{ return Ok(guid);
return Ok(guid); }
}
} }
} }
} }
+129 -207
View File
@@ -1,18 +1,19 @@
use crate::common::config::{FileLoggerConfig, LoggingConfigLoader}; use std::io::IsTerminal as _;
use crate::common::config::LoggingConfigLoader;
use crate::common::get_logger_timer_rfc3339; use crate::common::get_logger_timer_rfc3339;
use crate::common::tracing_rolling_appender::{FileAppenderWrapper, RollingFileAppenderBase}; use crate::common::tracing_rolling_appender::{FileAppenderWrapper, RollingFileAppenderBase};
use crate::rpc_service::logger::{CURRENT_LOG_LEVEL, LOGGER_LEVEL_SENDER}; use crate::rpc_service::logger::{CURRENT_LOG_LEVEL, LOGGER_LEVEL_SENDER};
use anyhow::Context; use anyhow::Context;
use paste::paste; use paste::paste;
use std::io::IsTerminal; use regex::Regex;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing::{Level, Metadata}; use tracing::{Level, Metadata};
use tracing_subscriber::Registry; use tracing_subscriber::filter::{filter_fn, FilterExt};
use tracing_subscriber::filter::{FilterExt, filter_fn};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::fmt::layer; use tracing_subscriber::fmt::layer;
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::Registry;
use tracing_subscriber::{EnvFilter, Layer}; use tracing_subscriber::{EnvFilter, Layer};
macro_rules! __log__ { macro_rules! __log__ {
@@ -46,16 +47,18 @@ macro_rules! __log__ {
__log__!(const LOG_TARGET = "CORE"); __log__!(const LOG_TARGET = "CORE");
fn parse_env_filter(default_level: Option<LevelFilter>) -> Result<EnvFilter, anyhow::Error> { fn parse_env_filter(default_level: LevelFilter) -> Result<EnvFilter, anyhow::Error> {
let directive = match default_level { let mut filter = EnvFilter::builder()
Some(level) => level.into(), .with_default_directive(default_level.into())
None => format!("{LOG_TARGET}=info").parse()?,
};
EnvFilter::builder()
.with_default_directive(directive)
.from_env() .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()?);
}
Ok(filter)
} }
fn is_log(meta: &Metadata) -> bool { fn is_log(meta: &Metadata) -> bool {
@@ -75,6 +78,7 @@ macro_rules! log_layer {
$layer $layer
.with_file(false) .with_file(false)
.with_line_number(false) .with_line_number(false)
.with_ansi(true)
.with_filter(filter_fn(is_log)) .with_filter(filter_fn(is_log))
.boxed() .boxed()
}; };
@@ -82,68 +86,127 @@ macro_rules! log_layer {
pub fn init( pub fn init(
config: impl LoggingConfigLoader, config: impl LoggingConfigLoader,
reload: bool, need_reload: bool,
) -> Result<Option<NewFilterSender>, anyhow::Error> { ) -> Result<Option<NewFilterSender>, anyhow::Error> {
let mut layers = Vec::new(); let mut layers = Vec::new();
let console_layers = console_layers( let file_config = config.get_file_logger_config();
config let file_level = file_config
.get_console_logger_config() .level
.level .map(|s| s.parse().unwrap())
.map(|s| s.parse().unwrap()), .unwrap_or(LevelFilter::OFF);
)?;
layers.extend(console_layers);
let sender = if cfg!(not(test)) { let mut ret_sender: Option<NewFilterSender> = None;
let (file_layers, sender) = file_layers(config.get_file_logger_config(), reload)?;
layers.extend(file_layers);
sender
} else {
None
};
Registry::default() // logger to a rolling file
.with(layers) if file_level != LevelFilter::OFF || need_reload {
.try_init() let dir = file_config.dir.as_deref().unwrap_or(".");
.map(|_| sender) let file = file_config.file.as_deref().unwrap_or("easytier.log");
.map_err(Into::into) let path = std::path::Path::new(dir).join(file);
} let path_str = path.to_string_lossy().into_owned();
type BoxLayer = Box<dyn Layer<Registry> + Send + Sync>; 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();
fn console_layers(default_level: Option<LevelFilter>) -> anyhow::Result<Vec<BoxLayer>> { // Create a simple wrapper that implements MakeWriter
let mut layers = Vec::new(); let wrapper = FileAppenderWrapper::new(file_appender);
if matches!(default_level, Some(LevelFilter::OFF)) {
return Ok(layers); 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");
});
}
} }
// 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, _) = let (console_filter, _) =
tracing_subscriber::reload::Layer::new(parse_env_filter(default_level)?); tracing_subscriber::reload::Layer::new(parse_env_filter(console_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 = || { let layer = || {
layer() layer()
.compact() .compact()
.with_ansi(std::io::stderr().is_terminal())
.with_timer(get_logger_timer_rfc3339()) .with_timer(get_logger_timer_rfc3339())
.with_ansi(ansi) .with_writer(std::io::stderr)
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.with_writer(stderr)
}; };
layers.push( layers.push(
vec![ vec![
tracing_layer!(layer()), tracing_layer!(layer()),
log_layer!(layer()).with_filter(LevelFilter::WARN).boxed(), log_layer!(layer()).with_filter(LevelFilter::WARN).boxed(),
log_layer!(layer().with_writer(stdout)) log_layer!(layer().with_writer(std::io::stdout))
.with_filter(filter_fn(|metadata| *metadata.level() > Level::WARN)) .with_filter(filter_fn(|metadata| *metadata.level() > Level::WARN))
.boxed(), .boxed(),
] ]
@@ -156,164 +219,23 @@ fn console_layers(default_level: Option<LevelFilter>) -> anyhow::Result<Vec<BoxL
layers.push(console_subscriber::ConsoleLayer::builder().spawn().boxed()); layers.push(console_subscriber::ConsoleLayer::builder().spawn().boxed());
} }
Ok(layers) Registry::default().with(layers).init();
}
fn file_layers( Ok(ret_sender)
config: FileLoggerConfig,
reload: bool,
) -> anyhow::Result<(Vec<BoxLayer>, Option<NewFilterSender>)> {
let mut layers = Vec::new();
let level = config.level.map(|s| s.parse().unwrap());
if matches!(level, Some(LevelFilter::OFF)) && !reload {
return Ok((layers, None));
}
let (file_filter, file_filter_reloader) =
tracing_subscriber::reload::Layer::<_, Registry>::new(parse_env_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()));
if let Some(level) = level {
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 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");
});
Ok((layers, Some(tx)))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::common::config::FileLoggerConfig; use crate::common::config::{self};
#[ctor::ctor] async fn test_logger_reload() {
fn init() { println!("current working dir: {:?}", std::env::current_dir());
let _ = Registry::default() let config = config::LoggingConfigBuilder::default().build().unwrap();
.with(console_layers(Some(LevelFilter::WARN)).unwrap()) let s = init(&config, true).unwrap();
.try_init(); tracing::debug!("test not display debug");
} s.unwrap().send(LevelFilter::DEBUG.to_string()).unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
#[test] tracing::debug!("test display debug");
fn test_logger_reload() {
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"
);
} }
} }
+6 -6
View File
@@ -41,8 +41,8 @@ pub fn get_logger_timer<F: time::formatting::Formattable>(
tracing_subscriber::fmt::time::OffsetTime::new(local_offset, format) tracing_subscriber::fmt::time::OffsetTime::new(local_offset, format)
} }
pub fn get_logger_timer_rfc3339() pub fn get_logger_timer_rfc3339(
-> tracing_subscriber::fmt::time::OffsetTime<time::format_description::well_known::Rfc3339> { ) -> tracing_subscriber::fmt::time::OffsetTime<time::format_description::well_known::Rfc3339> {
get_logger_timer(time::format_description::well_known::Rfc3339) get_logger_timer(time::format_description::well_known::Rfc3339)
} }
@@ -117,10 +117,10 @@ pub fn get_machine_id() -> uuid::Uuid {
.unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id")); .unwrap_or_else(|_| std::path::PathBuf::from("et_machine_id"));
// try load from local file // try load from local file
if let Ok(mid) = std::fs::read_to_string(&machine_id_file) if let Ok(mid) = std::fs::read_to_string(&machine_id_file) {
&& let Ok(mid) = uuid::Uuid::parse_str(mid.trim()) if let Ok(mid) = uuid::Uuid::parse_str(mid.trim()) {
{ return mid;
return mid; }
} }
#[cfg(any( #[cfg(any(
+1 -1
View File
@@ -1,7 +1,7 @@
use futures::Future; use futures::Future;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use nix::sched::{CloneFlags, setns}; use nix::sched::{setns, CloneFlags};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::os::fd::AsFd; use std::os::fd::AsFd;
+20 -5
View File
@@ -4,25 +4,40 @@
//! For example, if task A spawned task B but is doing something else, and task B is waiting for task C to join, //! 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. //! aborting A will also abort both B and C.
use derive_more::{Deref, DerefMut, From};
use std::future::Future; use std::future::Future;
use std::ops::Deref;
use std::pin::Pin; use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
#[derive(Debug, From, Deref, DerefMut)] #[derive(Debug)]
pub struct ScopedTask<T>(JoinHandle<T>); pub struct ScopedTask<T> {
inner: JoinHandle<T>,
}
impl<T> Drop for ScopedTask<T> { impl<T> Drop for ScopedTask<T> {
fn drop(&mut self) { fn drop(&mut self) {
self.abort() self.inner.abort()
} }
} }
impl<T> Future for ScopedTask<T> { impl<T> Future for ScopedTask<T> {
type Output = <JoinHandle<T> as Future>::Output; type Output = <JoinHandle<T> as Future>::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx) 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
} }
} }
+7 -27
View File
@@ -42,8 +42,6 @@ pub enum MetricName {
TrafficControlBytesRxByInstance, TrafficControlBytesRxByInstance,
/// Traffic bytes forwarded /// Traffic bytes forwarded
TrafficBytesForwarded, TrafficBytesForwarded,
/// Control-plane traffic bytes forwarded
TrafficControlBytesForwarded,
/// Traffic bytes sent to self /// Traffic bytes sent to self
TrafficBytesSelfTx, TrafficBytesSelfTx,
/// Traffic bytes received from self /// Traffic bytes received from self
@@ -73,8 +71,6 @@ pub enum MetricName {
TrafficControlPacketsRxByInstance, TrafficControlPacketsRxByInstance,
/// Traffic packets forwarded /// Traffic packets forwarded
TrafficPacketsForwarded, TrafficPacketsForwarded,
/// Control-plane traffic packets forwarded
TrafficControlPacketsForwarded,
/// Traffic packets sent to self /// Traffic packets sent to self
TrafficPacketsSelfTx, TrafficPacketsSelfTx,
/// Traffic packets received from self /// Traffic packets received from self
@@ -121,9 +117,6 @@ impl fmt::Display for MetricName {
write!(f, "traffic_control_bytes_rx_by_instance") write!(f, "traffic_control_bytes_rx_by_instance")
} }
MetricName::TrafficBytesForwarded => write!(f, "traffic_bytes_forwarded"), MetricName::TrafficBytesForwarded => write!(f, "traffic_bytes_forwarded"),
MetricName::TrafficControlBytesForwarded => {
write!(f, "traffic_control_bytes_forwarded")
}
MetricName::TrafficBytesSelfTx => write!(f, "traffic_bytes_self_tx"), MetricName::TrafficBytesSelfTx => write!(f, "traffic_bytes_self_tx"),
MetricName::TrafficBytesSelfRx => write!(f, "traffic_bytes_self_rx"), MetricName::TrafficBytesSelfRx => write!(f, "traffic_bytes_self_rx"),
MetricName::TrafficBytesForeignForwardRx => { MetricName::TrafficBytesForeignForwardRx => {
@@ -153,9 +146,6 @@ impl fmt::Display for MetricName {
write!(f, "traffic_control_packets_rx_by_instance") write!(f, "traffic_control_packets_rx_by_instance")
} }
MetricName::TrafficPacketsForwarded => write!(f, "traffic_packets_forwarded"), MetricName::TrafficPacketsForwarded => write!(f, "traffic_packets_forwarded"),
MetricName::TrafficControlPacketsForwarded => {
write!(f, "traffic_control_packets_forwarded")
}
MetricName::TrafficPacketsSelfTx => write!(f, "traffic_packets_self_tx"), MetricName::TrafficPacketsSelfTx => write!(f, "traffic_packets_self_tx"),
MetricName::TrafficPacketsSelfRx => write!(f, "traffic_packets_self_rx"), MetricName::TrafficPacketsSelfRx => write!(f, "traffic_packets_self_rx"),
MetricName::TrafficPacketsForeignForwardRx => { MetricName::TrafficPacketsForeignForwardRx => {
@@ -384,9 +374,7 @@ impl UnsafeCounter {
/// that no other thread is accessing this counter simultaneously. /// that no other thread is accessing this counter simultaneously.
pub unsafe fn add(&self, delta: u64) { pub unsafe fn add(&self, delta: u64) {
let ptr = self.value.get(); let ptr = self.value.get();
unsafe { *ptr = (*ptr).saturating_add(delta);
*ptr = (*ptr).saturating_add(delta);
}
} }
/// Increment the counter by 1 /// Increment the counter by 1
@@ -394,9 +382,7 @@ impl UnsafeCounter {
/// This method is unsafe because it uses UnsafeCell. The caller must ensure /// This method is unsafe because it uses UnsafeCell. The caller must ensure
/// that no other thread is accessing this counter simultaneously. /// that no other thread is accessing this counter simultaneously.
pub unsafe fn inc(&self) { pub unsafe fn inc(&self) {
unsafe { self.add(1);
self.add(1);
}
} }
/// Get the current value of the counter /// Get the current value of the counter
@@ -405,7 +391,7 @@ impl UnsafeCounter {
/// that no other thread is modifying this counter simultaneously. /// that no other thread is modifying this counter simultaneously.
pub unsafe fn get(&self) -> u64 { pub unsafe fn get(&self) -> u64 {
let ptr = self.value.get(); let ptr = self.value.get();
unsafe { *ptr } *ptr
} }
/// Reset the counter to zero /// Reset the counter to zero
@@ -414,9 +400,7 @@ impl UnsafeCounter {
/// that no other thread is accessing this counter simultaneously. /// that no other thread is accessing this counter simultaneously.
pub unsafe fn reset(&self) { pub unsafe fn reset(&self) {
let ptr = self.value.get(); let ptr = self.value.get();
unsafe { *ptr = 0;
*ptr = 0;
}
} }
/// Set the counter to a specific value /// Set the counter to a specific value
@@ -425,9 +409,7 @@ impl UnsafeCounter {
/// that no other thread is accessing this counter simultaneously. /// that no other thread is accessing this counter simultaneously.
pub unsafe fn set(&self, value: u64) { pub unsafe fn set(&self, value: u64) {
let ptr = self.value.get(); let ptr = self.value.get();
unsafe { *ptr = value;
*ptr = value;
}
} }
} }
@@ -464,9 +446,7 @@ impl MetricData {
/// that no other thread is accessing this timestamp simultaneously. /// that no other thread is accessing this timestamp simultaneously.
unsafe fn touch(&self) { unsafe fn touch(&self) {
let ptr = self.last_updated.get(); let ptr = self.last_updated.get();
unsafe { *ptr = Instant::now();
*ptr = Instant::now();
}
} }
/// Get the last updated timestamp /// Get the last updated timestamp
@@ -475,7 +455,7 @@ impl MetricData {
/// that no other thread is modifying this timestamp simultaneously. /// that no other thread is modifying this timestamp simultaneously.
unsafe fn get_last_updated(&self) -> Instant { unsafe fn get_last_updated(&self) -> Instant {
let ptr = self.last_updated.get(); let ptr = self.last_updated.get();
unsafe { *ptr } *ptr
} }
} }
+31 -19
View File
@@ -11,8 +11,8 @@ use crossbeam::atomic::AtomicCell;
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
use socket2::{SockAddr, SockRef}; use socket2::{SockAddr, SockRef};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UdpSocket, lookup_host}; use tokio::net::{lookup_host, UdpSocket};
use tokio::sync::{Mutex, broadcast}; use tokio::sync::{broadcast, Mutex};
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tracing::{Instrument, Level}; use tracing::{Instrument, Level};
@@ -239,11 +239,15 @@ impl StunClient {
let mut mapped_addr = None; let mut mapped_addr = None;
for x in msg.attributes() { for x in msg.attributes() {
match x { match x {
Attribute::MappedAddress(addr) if mapped_addr.is_none() => { Attribute::MappedAddress(addr) => {
let _ = mapped_addr.insert(addr.address()); if mapped_addr.is_none() {
let _ = mapped_addr.insert(addr.address());
}
} }
Attribute::XorMappedAddress(addr) if mapped_addr.is_none() => { Attribute::XorMappedAddress(addr) => {
let _ = mapped_addr.insert(addr.address()); if mapped_addr.is_none() {
let _ = mapped_addr.insert(addr.address());
}
} }
_ => {} _ => {}
} }
@@ -255,11 +259,15 @@ impl StunClient {
let mut changed_addr = None; let mut changed_addr = None;
for x in msg.attributes() { for x in msg.attributes() {
match x { match x {
Attribute::OtherAddress(m) if changed_addr.is_none() => { Attribute::OtherAddress(m) => {
let _ = changed_addr.insert(m.address()); if changed_addr.is_none() {
let _ = changed_addr.insert(m.address());
}
} }
Attribute::ChangedAddress(m) if changed_addr.is_none() => { Attribute::ChangedAddress(m) => {
let _ = changed_addr.insert(m.address()); if changed_addr.is_none() {
let _ = changed_addr.insert(m.address());
}
} }
_ => {} _ => {}
} }
@@ -706,11 +714,15 @@ impl TcpStunClient {
let mut mapped_addr = None; let mut mapped_addr = None;
for x in msg.attributes() { for x in msg.attributes() {
match x { match x {
Attribute::MappedAddress(addr) if mapped_addr.is_none() => { Attribute::MappedAddress(addr) => {
let _ = mapped_addr.insert(addr.address()); if mapped_addr.is_none() {
let _ = mapped_addr.insert(addr.address());
}
} }
Attribute::XorMappedAddress(addr) if mapped_addr.is_none() => { Attribute::XorMappedAddress(addr) => {
let _ = mapped_addr.insert(addr.address()); if mapped_addr.is_none() {
let _ = mapped_addr.insert(addr.address());
}
} }
_ => {} _ => {}
} }
@@ -1328,7 +1340,7 @@ impl StunInfoCollectorTrait for MockStunInfoCollector {
mod tests { mod tests {
use crate::{ use crate::{
common::scoped_task::ScopedTask, common::scoped_task::ScopedTask,
tunnel::{TunnelListener, udp::UdpTunnelListener}, tunnel::{udp::UdpTunnelListener, TunnelListener},
}; };
use tokio::time::{sleep, timeout}; use tokio::time::{sleep, timeout};
@@ -1392,10 +1404,10 @@ mod tests {
loop { loop {
let ret = detector.detect_nat_type(0).await; let ret = detector.detect_nat_type(0).await;
println!("{:#?}, {:?}", ret, ret.as_ref().map(|x| x.nat_type())); println!("{:#?}, {:?}", ret, ret.as_ref().map(|x| x.nat_type()));
if let Ok(resp) = ret if let Ok(resp) = ret {
&& !resp.stun_resps.is_empty() if !resp.stun_resps.is_empty() {
{ return;
return; }
} }
sleep(Duration::from_secs(1)).await; sleep(Duration::from_secs(1)).await;
} }
+2 -2
View File
@@ -1,13 +1,13 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use bytecodec::fixnum::{U32beDecoder, U32beEncoder}; use bytecodec::fixnum::{U32beDecoder, U32beEncoder};
use stun_codec::net::{SocketAddrDecoder, SocketAddrEncoder, socket_addr_xor}; use stun_codec::net::{socket_addr_xor, SocketAddrDecoder, SocketAddrEncoder};
use stun_codec::rfc5389::attributes::{ use stun_codec::rfc5389::attributes::{
MappedAddress, Software, XorMappedAddress, XorMappedAddress2, MappedAddress, Software, XorMappedAddress, XorMappedAddress2,
}; };
use stun_codec::rfc5780::attributes::{OtherAddress, ResponseOrigin}; use stun_codec::rfc5780::attributes::{OtherAddress, ResponseOrigin};
use stun_codec::{AttributeType, Message, TransactionId, define_attribute_enums}; use stun_codec::{define_attribute_enums, AttributeType, Message, TransactionId};
use bytecodec::{ByteCount, Decode, Encode, Eos, Result, SizedEncode, TryTaggedDecode}; use bytecodec::{ByteCount, Decode, Encode, Eos, Result, SizedEncode, TryTaggedDecode};
+1 -1
View File
@@ -231,7 +231,7 @@ mod tests {
}; };
use super::*; use super::*;
use tokio::time::{Duration, sleep}; use tokio::time::{sleep, Duration};
/// Test initial state after creation /// Test initial state after creation
#[tokio::test] #[tokio::test]
@@ -1,5 +1,4 @@
use super::*; use super::*;
use anyhow::anyhow;
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct RollingConditionBase { pub struct RollingConditionBase {
@@ -58,16 +57,17 @@ impl Default for RollingConditionBase {
impl RollingCondition for RollingConditionBase { impl RollingCondition for RollingConditionBase {
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool { fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
let mut rollover = false; let mut rollover = false;
if let Some(frequency) = self.frequency_opt.as_ref() if let Some(frequency) = self.frequency_opt.as_ref() {
&& let Some(last_write) = self.last_write_opt.as_ref() if let Some(last_write) = self.last_write_opt.as_ref() {
&& frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
{ rollover = true;
rollover = true; }
}
} }
if let Some(max_size) = self.max_size_opt.as_ref() if let Some(max_size) = self.max_size_opt.as_ref() {
&& current_filesize >= *max_size if current_filesize >= *max_size {
{ rollover = true;
rollover = true; }
} }
self.last_write_opt = Some(*now); self.last_write_opt = Some(*now);
rollover rollover
@@ -136,11 +136,9 @@ impl RollingFileAppenderBaseBuilder {
/// Builds a RollingFileAppenderBase instance from the current settings. /// Builds a RollingFileAppenderBase instance from the current settings.
/// ///
/// Returns an error if the filename is empty. /// Returns an error if the filename is empty.
pub fn build(self) -> anyhow::Result<RollingFileAppenderBase> { pub fn build(self) -> Result<RollingFileAppenderBase, &'static str> {
if self.filename.is_empty() { if self.filename.is_empty() {
return Err(anyhow!( return Err("A filename is required to be set and can not be blank");
"A filename is required to be set and can not be blank"
));
} }
Ok(RollingFileAppenderBase { Ok(RollingFileAppenderBase {
condition: self.condition, condition: self.condition,
@@ -81,7 +81,11 @@ where
/// Determines the final filename, where n==0 indicates the current file /// Determines the final filename, where n==0 indicates the current file
fn filename_for(&self, n: usize) -> String { fn filename_for(&self, n: usize) -> String {
let f = self.filename.clone(); let f = self.filename.clone();
if n > 0 { format!("{}.{}", f, n) } else { f } if n > 0 {
format!("{}.{}", f, n)
} else {
f
}
} }
/// Rotates old files to make room for a new one. /// Rotates old files to make room for a new one.
@@ -141,14 +145,14 @@ where
/// Writes data using the given datetime to calculate the rolling condition /// Writes data using the given datetime to calculate the rolling condition
pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> { pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
if self.condition.should_rollover(now, self.current_filesize) if self.condition.should_rollover(now, self.current_filesize) {
&& let Err(e) = self.rollover() if let Err(e) = self.rollover() {
{ // If we can't rollover, just try to continue writing anyway
// If we can't rollover, just try to continue writing anyway // (better than missing data).
// (better than missing data). // This will likely used to implement logging, so
// This will likely used to implement logging, so // avoid using log::warn and log to stderr directly
// avoid using log::warn and log to stderr directly eprintln!("WARNING: Failed to rotate logfile {}: {}", self.filename, e);
eprintln!("WARNING: Failed to rotate logfile {}: {}", self.filename, e); }
} }
self.open_writer_if_needed()?; self.open_writer_if_needed()?;
if let Some(writer) = self.writer_opt.as_mut() { if let Some(writer) = self.writer_opt.as_mut() {
+17 -15
View File
@@ -5,16 +5,16 @@ use std::{
net::{IpAddr, Ipv6Addr, SocketAddr}, net::{IpAddr, Ipv6Addr, SocketAddr},
str::FromStr, str::FromStr,
sync::{ sync::{
Arc,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc,
}, },
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crate::{ use crate::{
common::{ common::{
PeerId, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait,
stun::StunInfoCollectorTrait, PeerId,
}, },
connector::udp_hole_punch::handle_rpc_result, connector::udp_hole_punch::handle_rpc_result,
peers::{ peers::{
@@ -31,7 +31,7 @@ use crate::{
}, },
rpc_types::controller::BaseController, rpc_types::controller::BaseController,
}, },
tunnel::{IpVersion, matches_protocol, udp::UdpTunnelConnector}, tunnel::{matches_protocol, udp::UdpTunnelConnector, IpVersion},
use_global_var, use_global_var,
}; };
@@ -39,7 +39,7 @@ use super::{
create_connector_by_url, should_background_p2p_with_peer, should_try_p2p_with_peer, create_connector_by_url, should_background_p2p_with_peer, should_try_p2p_with_peer,
udp_hole_punch, udp_hole_punch,
}; };
use crate::tunnel::{FromUrl, IpScheme, TunnelScheme, matches_scheme}; use crate::tunnel::{matches_scheme, FromUrl, IpScheme, TunnelScheme};
use anyhow::Context; use anyhow::Context;
use rand::Rng; use rand::Rng;
use socket2::Protocol; use socket2::Protocol;
@@ -769,9 +769,12 @@ mod tests {
let port = if proto == "wg" { 11040 } else { 11041 }; let port = if proto == "wg" { 11040 } else { 11041 };
if !ipv6 { if !ipv6 {
p_c.get_global_ctx().config.set_listeners(vec![ p_c.get_global_ctx().config.set_listeners(vec![format!(
format!("{}://0.0.0.0:{}", proto, port).parse().unwrap(), "{}://0.0.0.0:{}",
]); proto, port
)
.parse()
.unwrap()]);
} else { } else {
p_c.get_global_ctx() p_c.get_global_ctx()
.config .config
@@ -811,12 +814,11 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert!( assert!(data
data.dst_listener_blacklist .dst_listener_blacklist
.contains(&DstListenerUrlBlackListItem( .contains(&DstListenerUrlBlackListItem(
1, 1,
"tcp://127.0.0.1:10222".parse().unwrap() "tcp://127.0.0.1:10222".parse().unwrap()
)) )));
);
} }
} }
+9 -8
View File
@@ -3,7 +3,7 @@ use std::{net::SocketAddr, sync::Arc};
use super::{create_connector_by_url, http_connector::TunnelWithInfo}; use super::{create_connector_by_url, http_connector::TunnelWithInfo};
use crate::{ use crate::{
common::{ common::{
dns::{RESOLVER, resolve_txt_record}, dns::{resolve_txt_record, RESOLVER},
error::Error, error::Error,
global_ctx::ArcGlobalCtx, global_ctx::ArcGlobalCtx,
log, log,
@@ -14,7 +14,8 @@ use crate::{
use anyhow::Context; use anyhow::Context;
use dashmap::DashSet; use dashmap::DashSet;
use hickory_resolver::proto::rr::rdata::SRV; use hickory_resolver::proto::rr::rdata::SRV;
use rand::{Rng as _, seq::SliceRandom}; use itertools::Itertools;
use rand::{seq::SliceRandom, Rng as _};
use strum::VariantArray; use strum::VariantArray;
fn weighted_choice<T>(options: &[(T, u64)]) -> Option<&T> { fn weighted_choice<T>(options: &[(T, u64)]) -> Option<&T> {
@@ -116,7 +117,7 @@ impl DnsTunnelConnector {
let srv_domains = IpScheme::VARIANTS let srv_domains = IpScheme::VARIANTS
.iter() .iter()
.map(|s| (s, format!("_easytier._{}.{}", s, domain_name))) .map(|s| (s, format!("_easytier._{}.{}", s, domain_name)))
.collect::<Vec<_>>(); .collect_vec();
tracing::info!("build srv_domains: {:?}", srv_domains); tracing::info!("build srv_domains: {:?}", srv_domains);
let responses = Arc::new(DashSet::new()); let responses = Arc::new(DashSet::new());
let srv_lookup_tasks = srv_domains let srv_lookup_tasks = srv_domains
@@ -193,11 +194,11 @@ impl super::TunnelConnector for DnsTunnelConnector {
TunnelInfo { TunnelInfo {
local_addr: info.local_addr.clone(), local_addr: info.local_addr.clone(),
remote_addr: Some(self.addr.clone().into()), remote_addr: Some(self.addr.clone().into()),
resolved_remote_addr: info tunnel_type: format!(
.resolved_remote_addr "{}-{}",
.clone() self.addr.scheme(),
.or(info.remote_addr.clone()), info.remote_addr.unwrap_or_default()
tunnel_type: format!("{}-{}", self.addr.scheme(), info.tunnel_type), ),
}, },
))) )))
} }
+7 -9
View File
@@ -10,9 +10,9 @@ use rand::seq::SliceRandom as _;
use url::Url; use url::Url;
use crate::{ use crate::{
VERSION,
common::{error::Error, global_ctx::ArcGlobalCtx}, common::{error::Error, global_ctx::ArcGlobalCtx},
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream}, tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream},
VERSION,
}; };
use crate::proto::common::TunnelInfo; use crate::proto::common::TunnelInfo;
@@ -229,11 +229,11 @@ impl super::TunnelConnector for HttpTunnelConnector {
TunnelInfo { TunnelInfo {
local_addr: info.local_addr.clone(), local_addr: info.local_addr.clone(),
remote_addr: Some(self.addr.clone().into()), remote_addr: Some(self.addr.clone().into()),
resolved_remote_addr: info tunnel_type: format!(
.resolved_remote_addr "{:?}-{}",
.clone() self.redirect_type,
.or(info.remote_addr.clone()), info.remote_addr.unwrap_or_default()
tunnel_type: format!("{}-{}", self.addr.scheme(), info.tunnel_type), ),
}, },
))) )))
} }
@@ -257,7 +257,7 @@ mod tests {
use crate::{ use crate::{
common::global_ctx::tests::get_mock_global_ctx_with_network, common::global_ctx::tests::get_mock_global_ctx_with_network,
tunnel::{TunnelConnector, TunnelListener, tcp::TcpTunnelListener}, tunnel::{tcp::TcpTunnelListener, TunnelConnector, TunnelListener},
}; };
use super::*; use super::*;
@@ -353,8 +353,6 @@ mod tests {
let info = t.info().unwrap(); let info = t.info().unwrap();
let remote_addr = info.remote_addr.unwrap(); let remote_addr = info.remote_addr.unwrap();
assert_eq!(remote_addr, test_url.into()); assert_eq!(remote_addr, test_url.into());
let resolved_remote_addr = info.resolved_remote_addr.unwrap();
assert_eq!(resolved_remote_addr.url, "tcp://127.0.0.1:25888");
tokio::join!(task).0.unwrap(); tokio::join!(task).0.unwrap();
} }
+1 -1
View File
@@ -7,7 +7,7 @@ use dashmap::DashSet;
use tokio::{sync::mpsc, task::JoinSet, time::timeout}; use tokio::{sync::mpsc, task::JoinSet, time::timeout};
use crate::{ use crate::{
common::{PeerId, dns::socket_addrs, join_joinset_background}, common::{dns::socket_addrs, join_joinset_background, PeerId},
peers::peer_conn::PeerConnId, peers::peer_conn::PeerConnId,
proto::{ proto::{
api::instance::{ api::instance::{
+3 -5
View File
@@ -8,8 +8,8 @@ use crate::{
connector::dns_connector::DnsTunnelConnector, connector::dns_connector::DnsTunnelConnector,
proto::common::PeerFeatureFlag, proto::common::PeerFeatureFlag,
tunnel::{ tunnel::{
self, FromUrl, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme, self, ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector, FromUrl,
ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector, IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
}, },
utils::BoxExt, utils::BoxExt,
}; };
@@ -105,9 +105,7 @@ pub async fn create_connector_by_url(
IpScheme::Tcp => TcpTunnelConnector::new(url).boxed(), IpScheme::Tcp => TcpTunnelConnector::new(url).boxed(),
IpScheme::Udp => UdpTunnelConnector::new(url).boxed(), IpScheme::Udp => UdpTunnelConnector::new(url).boxed(),
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
IpScheme::Quic => { IpScheme::Quic => tunnel::quic::QuicTunnelConnector::new(url).boxed(),
tunnel::quic::QuicTunnelConnector::new(url, global_ctx.clone()).boxed()
}
#[cfg(feature = "wireguard")] #[cfg(feature = "wireguard")]
IpScheme::Wg => { IpScheme::Wg => {
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector}; use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
+20 -26
View File
@@ -9,7 +9,7 @@ use rand::Rng as _;
use tokio::task::JoinSet; use tokio::task::JoinSet;
use crate::{ use crate::{
common::{PeerId, join_joinset_background, stun::StunInfoCollectorTrait}, common::{join_joinset_background, stun::StunInfoCollectorTrait, PeerId},
connector::udp_hole_punch::BackOff, connector::udp_hole_punch::BackOff,
peers::{ peers::{
peer_manager::PeerManager, peer_manager::PeerManager,
@@ -24,8 +24,8 @@ use crate::{
rpc_types::{self, controller::BaseController}, rpc_types::{self, controller::BaseController},
}, },
tunnel::{ tunnel::{
TunnelConnector as _, TunnelListener as _,
tcp::{TcpTunnelConnector, TcpTunnelListener}, tcp::{TcpTunnelConnector, TcpTunnelListener},
TunnelConnector as _, TunnelListener as _,
}, },
}; };
@@ -719,20 +719,18 @@ mod tests {
tokio::time::sleep(Duration::from_secs(2)).await; tokio::time::sleep(Duration::from_secs(2)).await;
assert!( assert!(p_a
p_a.get_peer_map() .get_peer_map()
.list_peer_conns(p_c.my_peer_id()) .list_peer_conns(p_c.my_peer_id())
.await .await
.map(|c| c.is_empty()) .map(|c| c.is_empty())
.unwrap_or(true) .unwrap_or(true));
); assert!(p_c
assert!( .get_peer_map()
p_c.get_peer_map() .list_peer_conns(p_a.my_peer_id())
.list_peer_conns(p_a.my_peer_id()) .await
.await .map(|c| c.is_empty())
.map(|c| c.is_empty()) .unwrap_or(true));
.unwrap_or(true)
);
} }
#[tokio::test] #[tokio::test]
@@ -753,18 +751,14 @@ mod tests {
connect_peer_manager(p_b.clone(), p_c.clone()).await; connect_peer_manager(p_b.clone(), p_c.clone()).await;
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap(); wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
assert!( assert!(!collect_lazy_punch_peers(p_a.clone())
!collect_lazy_punch_peers(p_a.clone()) .await
.await .contains(&p_c.my_peer_id()));
.contains(&p_c.my_peer_id())
);
p_a.mark_recent_traffic(p_c.my_peer_id()); p_a.mark_recent_traffic(p_c.my_peer_id());
assert!( assert!(collect_lazy_punch_peers(p_a.clone())
collect_lazy_punch_peers(p_a.clone()) .await
.await .contains(&p_c.my_peer_id()));
.contains(&p_c.my_peer_id())
);
} }
} }
@@ -8,9 +8,9 @@ use anyhow::Context;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
common::{PeerId, scoped_task::ScopedTask, stun::StunInfoCollectorTrait}, common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId},
connector::udp_hole_punch::common::{ connector::udp_hole_punch::common::{
HOLE_PUNCH_PACKET_BODY_LEN, UdpHolePunchListener, try_connect_with_socket, try_connect_with_socket, UdpHolePunchListener, HOLE_PUNCH_PACKET_BODY_LEN,
}, },
connector::udp_hole_punch::handle_rpc_result, connector::udp_hole_punch::handle_rpc_result,
peers::peer_manager::PeerManager, peers::peer_manager::PeerManager,
@@ -21,7 +21,7 @@ use crate::{
}, },
rpc_types::{self, controller::BaseController}, rpc_types::{self, controller::BaseController},
}, },
tunnel::{Tunnel, udp::new_hole_punch_packet}, tunnel::{udp::new_hole_punch_packet, Tunnel},
}; };
use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray}; use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray};
@@ -340,7 +340,7 @@ impl PunchBothEasySymHoleClient {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use std::{ use std::{
sync::{Arc, atomic::AtomicU32}, sync::{atomic::AtomicU32, Arc},
time::Duration, time::Duration,
}; };
@@ -349,7 +349,7 @@ pub mod tests {
use crate::connector::udp_hole_punch::RUN_TESTING; use crate::connector::udp_hole_punch::RUN_TESTING;
use crate::{ use crate::{
connector::udp_hole_punch::{ connector::udp_hole_punch::{
UdpHolePunchConnector, tests::create_mock_peer_manager_with_mock_stun, tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector,
}, },
peers::tests::{connect_peer_manager, wait_route_appear}, peers::tests::{connect_peer_manager, wait_route_appear},
proto::common::NatType, proto::common::NatType,
@@ -8,21 +8,21 @@ use crossbeam::atomic::AtomicCell;
use dashmap::{DashMap, DashSet}; use dashmap::{DashMap, DashSet};
use rand::seq::SliceRandom as _; use rand::seq::SliceRandom as _;
use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet}; use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet};
use tracing::{Instrument, Level, instrument}; use tracing::{instrument, Instrument, Level};
use zerocopy::FromBytes as _; use zerocopy::FromBytes as _;
use crate::{ use crate::{
common::{ common::{
PeerId, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS,
stun::StunInfoCollectorTrait as _, stun::StunInfoCollectorTrait as _, PeerId,
}, },
defer, defer,
peers::peer_manager::PeerManager, peers::peer_manager::PeerManager,
proto::common::NatType, proto::common::NatType,
tunnel::{ tunnel::{
packet_def::{UDPTunnelHeader, UdpPacketType, UDP_TUNNEL_HEADER_SIZE},
udp::{new_hole_punch_packet, UdpTunnelConnector, UdpTunnelListener},
Tunnel, TunnelConnCounter, TunnelListener as _, Tunnel, TunnelConnCounter, TunnelListener as _,
packet_def::{UDP_TUNNEL_HEADER_SIZE, UDPTunnelHeader, UdpPacketType},
udp::{UdpTunnelConnector, UdpTunnelListener, new_hole_punch_packet},
}, },
}; };

Some files were not shown because too many files have changed in this diff Show More