mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-09 11:14:30 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 185b4a556b | |||
| 714897b0fd |
@@ -23,7 +23,7 @@ runs:
|
||||
if: ${{ inputs.web == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
build-filter: './easytier-web/*'
|
||||
build_filter: './easytier-web/*'
|
||||
|
||||
- name: Install GUI dependencies (Used by clippy)
|
||||
if: ${{ inputs.gui == 'true' }}
|
||||
|
||||
@@ -3,21 +3,20 @@ author: Luna
|
||||
description: 'Setup Node.js, pnpm, and install dependencies'
|
||||
|
||||
inputs:
|
||||
build-filter:
|
||||
build_filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
default: ''
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -28,7 +27,7 @@ runs:
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
@@ -39,10 +38,5 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm -r install
|
||||
if [ -n "${{ inputs.build-filter }}" ]; then
|
||||
echo "Building with filter: ${{ inputs.build-filter }}"
|
||||
pnpm -r --filter "${{ inputs.build-filter }}" build
|
||||
else
|
||||
echo "No build filter provided, building all packages"
|
||||
pnpm -r build
|
||||
fi
|
||||
echo "Building with filter: ${{ inputs.build_filter }}"
|
||||
pnpm -r --filter "${{ inputs.build_filter }}" build
|
||||
@@ -36,15 +36,38 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
build-filter: './easytier-web/*'
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
pnpm -r install
|
||||
pnpm -r --filter "./easytier-web/*" build
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-web-dashboard
|
||||
path: |
|
||||
@@ -119,7 +142,7 @@ jobs:
|
||||
- build_web
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
@@ -267,7 +290,7 @@ jobs:
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
@@ -294,7 +317,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
|
||||
uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置
|
||||
|
||||
# 下载二进制文件到独立目录
|
||||
- name: Download Linux aarch64 binaries
|
||||
@@ -314,7 +337,7 @@ jobs:
|
||||
|
||||
# 上传生成的模块
|
||||
- name: Upload Magisk Module
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Easytier-Magisk
|
||||
path: |
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.6.0'
|
||||
default: 'v2.5.0'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Validate inputs
|
||||
run: |
|
||||
|
||||
+30
-12
@@ -78,7 +78,7 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install GUI dependencies (x86 only)
|
||||
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
|
||||
@@ -119,18 +119,37 @@ jobs:
|
||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install rpm package (Linux target only)
|
||||
if: ${{ contains(matrix.TARGET, '-linux-') }}
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y rpm
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -165,7 +184,7 @@ jobs:
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ contains(matrix.TARGET, '-linux-') && contains(matrix.TARGET, 'aarch64') && '--bundles deb,rpm' || '' }}
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
@@ -183,7 +202,6 @@ jobs:
|
||||
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
|
||||
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
|
||||
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
|
||||
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
|
||||
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
|
||||
# currently only x86 appimage is supported
|
||||
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
|
||||
@@ -194,7 +212,7 @@ jobs:
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
@@ -70,8 +70,33 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: |
|
||||
pnpm -r install
|
||||
pnpm -r build
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -113,7 +138,7 @@ jobs:
|
||||
rm -rf ./artifacts/objects/
|
||||
|
||||
- name: Archive artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
path: |
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
check-full-shell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v27
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
cargo_fmt_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: fmt check
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -174,14 +174,11 @@ jobs:
|
||||
jq --arg v "$TAG_VERSION" '.name = "easytier-release" | .version = $v' oh-package.json5 > oh-package.tmp.json5 && mv oh-package.tmp.json5 oh-package.json5
|
||||
cd ..
|
||||
ohrs build --release --arch aarch
|
||||
cd dist/arm64-v8a
|
||||
mv libeasytier_ohrs.so libeasytier_release.so
|
||||
cd ../..
|
||||
ohrs artifact
|
||||
mv package.har easytier-release.har
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: easytier-ohos
|
||||
path: |
|
||||
|
||||
@@ -18,7 +18,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.6.0'
|
||||
default: 'v2.5.0'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
@@ -56,13 +56,6 @@ jobs:
|
||||
|
||||
- uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: Check Cargo.lock is up to date
|
||||
run: |
|
||||
if ! cargo metadata --format-version 1 --locked --no-deps > /dev/null; then
|
||||
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
@@ -79,7 +72,7 @@ jobs:
|
||||
needs: pre_job
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
@@ -95,7 +88,7 @@ jobs:
|
||||
- name: Archive test
|
||||
run: cargo nextest archive --archive-file tests.tar.zst --package easytier --features full
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests
|
||||
path: tests.tar.zst
|
||||
@@ -119,7 +112,7 @@ jobs:
|
||||
- name: "three_node::subnet_proxy_three_node_test"
|
||||
opts: "-E 'test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup tools for test
|
||||
run: sudo apt install bridge-utils
|
||||
@@ -151,4 +144,4 @@ jobs:
|
||||
steps:
|
||||
- name: Mark result as failed
|
||||
if: needs.test_matrix.result != 'success'
|
||||
run: exit 1
|
||||
run: exit 1
|
||||
Generated
+10
-48
@@ -2156,7 +2156,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2175,7 +2175,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"clap",
|
||||
@@ -2192,7 +2191,6 @@ dependencies = [
|
||||
"easytier-rpc-build",
|
||||
"encoding",
|
||||
"flume 0.12.0",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"gethostname 0.5.0",
|
||||
@@ -2210,7 +2208,6 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"idna 1.0.3",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
@@ -2329,7 +2326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2409,7 +2406,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2923,16 +2920,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fragile"
|
||||
version = "2.0.1"
|
||||
@@ -3783,7 +3770,7 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
"unicase",
|
||||
"webpki",
|
||||
"webpki-roots 0.26.3",
|
||||
"webpki-roots",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -3860,7 +3847,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots 0.26.3",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4332,15 +4319,6 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
@@ -5227,12 +5205,6 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.3.0"
|
||||
@@ -7165,7 +7137,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots 0.26.3",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -8488,7 +8460,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"webpki-roots 0.26.3",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9586,9 +9558,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.13.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -9599,11 +9571,10 @@ dependencies = [
|
||||
"httparse",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"simdutf8",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"webpki-roots 1.0.6",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10704,15 +10675,6 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.0"
|
||||
|
||||
@@ -11,286 +11,88 @@
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
> ✨ A simple, secure, decentralized virtual private network solution powered by Rust and Tokio
|
||||
> ✨ A simple, secure, decentralized SD-WAN solution powered by Rust and Tokio
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/config-page.png" width="300" alt="config page">
|
||||
<img src="assets/running-page.png" width="300" alt="running page">
|
||||
</p>
|
||||
🌐 **[Official Website](https://easytier.rs)** | 📚 **[Documentation](https://easytier.rs/en/)** | 🚀 **[Get Started](https://easytier.rs/en/guide/introduction.html)** | 📝 **[Download Releases](https://github.com/EasyTier/EasyTier/releases)** | 🇨🇳 **[China Site](https://easytier.cn)** | ❤️ **[Sponsor](#sponsor)**
|
||||
|
||||
📚 **[Full Documentation](https://easytier.cn/en/)** | 🖥️ **[Web Console](https://easytier.cn/web)** | 📝 **[Download Releases](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[Third Party Tools](https://easytier.cn/en/guide/installation_gui.html#third-party-graphical-interfaces)** | ❤️ **[Sponsor](#sponsor)**
|
||||
## Get Started
|
||||
|
||||
## Features
|
||||
### Install
|
||||
|
||||
### Core Features
|
||||
Linux:
|
||||
|
||||
- 🔒 **Decentralized**: Nodes are equal and independent, no centralized services required
|
||||
- 🚀 **Easy to Use**: Multiple operation methods via web, client, and command line
|
||||
- 🌍 **Cross-Platform**: Supports Win/MacOS/Linux/FreeBSD/Android and X86/ARM/MIPS architectures
|
||||
- 🔐 **Secure**: AES-GCM or WireGuard encryption, prevents man-in-the-middle attacks
|
||||
|
||||
### Advanced Capabilities
|
||||
|
||||
- 🔌 **Efficient NAT Traversal**: Supports UDP and IPv6 traversal, works with NAT4-NAT4 networks
|
||||
- 🌐 **Subnet Proxy**: Nodes can share subnets for other nodes to access
|
||||
- 🔄 **Intelligent Routing**: Latency priority and automatic route selection for best network experience
|
||||
- ⚡ **High Performance**: Zero-copy throughout the entire link, supports TCP/UDP/WSS/WG protocols
|
||||
|
||||
### Network Optimization
|
||||
|
||||
- 📊 **UDP Loss Resistance**: KCP/QUIC proxy optimizes latency and bandwidth in high packet loss environments
|
||||
- 🔧 **Web Management**: Easy configuration and monitoring through web interface
|
||||
- 🛠️ **Zero Config**: Simple deployment with statically linked executables
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 📥 Installation
|
||||
|
||||
Choose the installation method that best suits your needs:
|
||||
|
||||
Linux (Recommended):
|
||||
```bash
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
Homebrew (MacOS/Linux):
|
||||
Windows (run with administrator privileges):
|
||||
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
Homebrew (macOS/Linux):
|
||||
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
```
|
||||
|
||||
Windows (Recommended, run with administrator privileges):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
Install from source (latest development version):
|
||||
|
||||
Install via cargo (Latest development version):
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
|
||||
More installation options:
|
||||
|
||||
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
|
||||
- [CLI installation guide](https://easytier.rs/en/guide/installation.html)
|
||||
- [GUI installation guide](https://easytier.rs/en/guide/installation_gui.html)
|
||||
- [Pre-built binaries](https://github.com/EasyTier/EasyTier/releases)
|
||||
- [OpenWrt package](https://github.com/EasyTier/luci-app-easytier)
|
||||
- [One-click register service](https://easytier.rs/en/guide/network/oneclick-install-as-service.html)
|
||||
|
||||
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
|
||||
### Quick Example
|
||||
|
||||
Additional steps:
|
||||
|
||||
[One-Click Register Service](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) (Automatically start when the system boots and run in the background)
|
||||
|
||||
### 🚀 Basic Usage
|
||||
|
||||
#### Quick Networking with Shared Nodes
|
||||
|
||||
EasyTier supports quick networking using shared public nodes. When you don't have a public IP, you can use the free shared nodes provided by the EasyTier community. Nodes will automatically attempt NAT traversal and establish P2P connections. When P2P fails, data will be relayed through shared nodes.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same `--network-name` and `--network-secret` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example (Please use more complex network name to avoid conflicts):
|
||||
|
||||
1. Run on Node A:
|
||||
Join the same network from multiple nodes with a shared public node:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
# Node A
|
||||
sudo easytier-core -d --network-name demo --network-secret demo -p tcp://<SharedNodeIP>:11010
|
||||
|
||||
# Node B
|
||||
sudo easytier-core -d --network-name demo --network-secret demo -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
|
||||
2. Run on Node B:
|
||||
Use the same `--network-name` and `--network-secret` on every node to join the same network. After startup, check peers with `easytier-cli peer`, `easytier-cli route`, or `easytier-cli node`.
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>:11010
|
||||
```
|
||||
## Why EasyTier
|
||||
|
||||
After successful execution, you can check the network status using `easytier-cli`:
|
||||
- 🔒 **Decentralized**: Nodes are equal and independent, with no centralized controller required.
|
||||
- 🚀 **Easy to Use**: Use EasyTier from the web console, GUI clients, or the command line.
|
||||
- 🌍 **Cross-Platform**: Supports Windows, macOS, Linux, FreeBSD, Android, and multiple CPU architectures.
|
||||
- 🔐 **Secure**: Protects traffic with AES-GCM or WireGuard encryption.
|
||||
- 🔌 **Efficient NAT Traversal**: Supports UDP and IPv6 traversal, including NAT4-to-NAT4 scenarios.
|
||||
- 🌐 **Subnet Proxy**: Share private subnets with other nodes in the virtual network.
|
||||
- 🔄 **Intelligent Routing**: Chooses lower-latency paths automatically for a better network experience.
|
||||
- ⚡ **High Performance**: Uses zero-copy data paths and supports TCP, UDP, WS, WSS, WG, QUIC, and more.
|
||||
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ |
|
||||
```
|
||||
## Learn More
|
||||
|
||||
You can test connectivity between nodes:
|
||||
- [Introduction](https://easytier.rs/en/guide/introduction.html)
|
||||
- [Command line networking](https://easytier.rs/en/guide/networking.html)
|
||||
- [Decentralized networking](https://easytier.rs/en/guide/network/decentralized-networking.html)
|
||||
- [Networking with web console](https://easytier.rs/en/guide/network/web-console.html)
|
||||
- [WireGuard client access](https://easytier.rs/en/guide/network/use-easytier-with-wireguard-client.html)
|
||||
- [Subnet proxy (point-to-network)](https://easytier.rs/en/guide/network/point-to-networking.html)
|
||||
- [Bandwidth and latency optimization](https://easytier.rs/en/guide/network/kcp-proxy.html)
|
||||
- [Hosting public shared nodes](https://easytier.rs/en/guide/network/host-public-server.html)
|
||||
- [Third-party graphical interfaces](https://easytier.rs/en/guide/installation_gui.html#third-party-graphical-interfaces)
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
Note: If you cannot ping through, it may be that the firewall is blocking incoming traffic. Please turn off the firewall or add allow rules.
|
||||
|
||||
To improve availability, you can connect to multiple shared nodes simultaneously:
|
||||
|
||||
```bash
|
||||
# Connect to multiple shared nodes
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP1>:11010 -p udp://<SharedNodeIP2>:11010
|
||||
```
|
||||
|
||||
Once your network is set up successfully, you can easily configure it to start automatically on system boot. Refer to the [One-Click Register Service guide](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) for step-by-step instructions on registering EasyTier as a system service.
|
||||
|
||||
#### Decentralized Networking
|
||||
|
||||
EasyTier is fundamentally decentralized, with no distinction between server and client. As long as one device can communicate with any node in the virtual network, it can join the virtual network. Here's how to set up a decentralized network:
|
||||
|
||||
1. Start First Node (Node A):
|
||||
|
||||
```bash
|
||||
# Start the first node
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
After startup, this node will listen on the following ports by default:
|
||||
- TCP: 11010
|
||||
- UDP: 11010
|
||||
- WebSocket: 11011
|
||||
- WebSocket SSL: 11012
|
||||
- WireGuard: 11013
|
||||
|
||||
2. Connect Second Node (Node B):
|
||||
|
||||
```bash
|
||||
# Connect to the first node using its public IP
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
|
||||
```
|
||||
|
||||
3. Verify Connection:
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.144.144.2
|
||||
|
||||
# View connected peers
|
||||
easytier-cli peer
|
||||
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
|
||||
# View local node information
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
For more nodes to join the network, they can connect to any existing node in the network using the `-p` parameter:
|
||||
|
||||
```bash
|
||||
# Connect to any existing node using its public IP
|
||||
sudo easytier-core -i 10.144.144.3 -p udp://ANY_EXISTING_NODE_PUBLIC_IP:11010
|
||||
```
|
||||
|
||||
### 🔍 Advanced Features
|
||||
|
||||
#### Subnet Proxy
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A Public IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
To share a subnet, add the `-n` parameter when starting EasyTier:
|
||||
|
||||
```bash
|
||||
# Share subnet 10.1.1.0/24 with other nodes
|
||||
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. You can verify the subnet proxy setup:
|
||||
|
||||
1. Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
|
||||
|
||||
```bash
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. Test if you can access nodes in the proxied subnet:
|
||||
|
||||
```bash
|
||||
# Test connectivity to proxied subnet
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
#### WireGuard Integration
|
||||
|
||||
EasyTier can act as a WireGuard server, allowing any device with a WireGuard client (including iOS and Android) to access the EasyTier network. Here's an example setup:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone<br/>WireGuard Installed]]
|
||||
|
||||
subgraph Node A Public IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
1. Start EasyTier with WireGuard portal enabled:
|
||||
|
||||
```bash
|
||||
# Listen on 0.0.0.0:11013 and use 10.14.14.0/24 subnet for WireGuard clients
|
||||
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
2. Get WireGuard client configuration:
|
||||
|
||||
```bash
|
||||
# Get WireGuard client configuration
|
||||
easytier-cli vpn-portal
|
||||
```
|
||||
|
||||
3. In the output configuration:
|
||||
- Set `Interface.Address` to an available IP from the WireGuard subnet
|
||||
- Set `Peer.Endpoint` to the public IP/domain of your EasyTier node
|
||||
- Import the modified configuration into your WireGuard client
|
||||
|
||||
#### Self-Hosted Public Shared Node
|
||||
|
||||
You can run your own public shared node to help other nodes discover each other. A public shared node is just a regular EasyTier network (with same network name and secret) that other networks can connect to.
|
||||
|
||||
To run a public shared node:
|
||||
|
||||
```bash
|
||||
# No need to specify IPv4 address for public shared nodes
|
||||
sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
```
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
|
||||
### Contact Us
|
||||
## Community
|
||||
|
||||
- 💬 **[Telegram Group](https://t.me/easytier)**
|
||||
- 👥 **[QQ Group]**
|
||||
- No.1 [949700262](https://qm.qq.com/q/wFoTUChqZW)
|
||||
- No.2 [837676408](https://qm.qq.com/q/4V33DrfgHe)
|
||||
- No.3 [957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
- 👥 **QQ Groups**: [No.1 949700262](https://qm.qq.com/q/wFoTUChqZW), [No.2 837676408](https://qm.qq.com/q/4V33DrfgHe), [No.3 957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
|
||||
## License
|
||||
|
||||
@@ -306,21 +108,20 @@ CDN acceleration and security protection for this project are sponsored by Tence
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
|
||||
Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200" alt="Langlang Cloud Logo">
|
||||
</a>
|
||||
<a href="https://www.rainyun.com/NjM0NzQ1_" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200" alt="RainCloud Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
If you find EasyTier helpful, please consider sponsoring us. Software development and maintenance require a lot of time and effort, and your sponsorship will help us better maintain and improve EasyTier.
|
||||
If you find EasyTier helpful, please consider sponsoring us. Software development and maintenance require time and effort, and your sponsorship helps us keep improving EasyTier.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
<img src="assets/wechat.png" width="200" alt="WeChat sponsor QR code">
|
||||
<img src="assets/alipay.png" width="200" alt="Alipay sponsor QR code">
|
||||
</p>
|
||||
|
||||
+60
-258
@@ -11,286 +11,88 @@
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化的异地组网方案
|
||||
> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化 SD-WAN 组网方案
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/config-page.png" width="300" alt="配置页面">
|
||||
<img src="assets/running-page.png" width="300" alt="运行页面">
|
||||
</p>
|
||||
|
||||
📚 **[完整文档](https://easytier.cn)** | 🖥️ **[Web 控制台](https://easytier.cn/web)** | 📝 **[下载发布版本](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[第三方工具](https://easytier.cn/guide/installation_gui.html#%E7%AC%AC%E4%B8%89%E6%96%B9%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2)** | ❤️ **[赞助](#赞助)**
|
||||
|
||||
## 特性
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 🔒 **去中心化**:节点平等且独立,无需中心化服务
|
||||
- 🚀 **易于使用**:支持通过网页、客户端和命令行多种操作方式
|
||||
- 🌍 **跨平台**:支持 Win/MacOS/Linux/FreeBSD/Android 和 X86/ARM/MIPS 架构
|
||||
- 🔐 **安全**:AES-GCM 或 WireGuard 加密,防止中间人攻击
|
||||
|
||||
### 高级功能
|
||||
|
||||
- 🔌 **高效 NAT 穿透**:支持 UDP 和 IPv6 穿透,可在 NAT4-NAT4 网络中工作
|
||||
- 🌐 **子网代理**:节点可以共享子网供其他节点访问
|
||||
- 🔄 **智能路由**:延迟优先和自动路由选择,提供最佳网络体验
|
||||
- ⚡ **高性能**:整个链路零拷贝,支持 TCP/UDP/WSS/WG 协议
|
||||
|
||||
### 网络优化
|
||||
|
||||
- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
|
||||
- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
|
||||
- 🛠️ **零配置**:静态链接的可执行文件,简单部署
|
||||
🌐 **[官网文档](https://easytier.cn)** | 🚀 **[快速开始](https://easytier.cn/guide/introduction.html)** | 📝 **[下载发布版本](https://github.com/EasyTier/EasyTier/releases)** | 🌍 **[国际站](https://easytier.rs)** | ❤️ **[赞助](#赞助)**
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 📥 安装
|
||||
### 安装
|
||||
|
||||
选择最适合您需求的安装方式:
|
||||
Linux:
|
||||
|
||||
Linux(推荐):
|
||||
```bash
|
||||
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
|
||||
```
|
||||
|
||||
Homebrew(MacOS/Linux):
|
||||
Windows(请使用管理员权限运行):
|
||||
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
Homebrew(macOS/Linux):
|
||||
|
||||
```bash
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
```
|
||||
|
||||
Windows(推荐,请以管理员权限运行):
|
||||
```powershell
|
||||
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
|
||||
```
|
||||
|
||||
通过 cargo 安装(最新开发版本):
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
|
||||
更多安装方式:
|
||||
|
||||
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
|
||||
- [CLI 安装文档](https://easytier.cn/guide/installation.html)
|
||||
- [GUI 安装文档](https://easytier.cn/guide/installation_gui.html)
|
||||
- [下载预编译文件](https://github.com/EasyTier/EasyTier/releases)
|
||||
- [OpenWrt 插件](https://github.com/EasyTier/luci-app-easytier)
|
||||
- [一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)
|
||||
|
||||
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
|
||||
### 最小示例
|
||||
|
||||
附加步骤:
|
||||
|
||||
[一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)(系统启动时自动后台运行)
|
||||
|
||||
### 🚀 基本用法
|
||||
|
||||
#### 使用共享节点快速组网
|
||||
|
||||
EasyTier 支持使用共享节点快速组网。当您没有公网 IP 时,可以使用公共共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
|
||||
|
||||
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name` 和 `--network-secret` 参数作为网络的唯一标识符。
|
||||
|
||||
以两个节点为例(请使用更复杂的网络名称以避免冲突):
|
||||
|
||||
1. 在节点 A 上运行:
|
||||
使用共享公共节点,让多台设备加入同一个网络:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
# 节点 A
|
||||
sudo easytier-core -d --network-name demo --network-secret demo -p tcp://<共享节点IP>:11010
|
||||
|
||||
# 节点 B
|
||||
sudo easytier-core -d --network-name demo --network-secret demo -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
|
||||
2. 在节点 B 上运行:
|
||||
所有节点使用相同的 `--network-name` 和 `--network-secret` 即可加入同一个网络。启动后可通过 `easytier-cli peer`、`easytier-cli route` 或 `easytier-cli node` 查看状态。
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
|
||||
```
|
||||
## 为什么选择 EasyTier
|
||||
|
||||
执行成功后,可以使用 `easytier-cli` 检查网络状态:
|
||||
- 🔒 **去中心化**:节点平等独立,无需中心化控制器。
|
||||
- 🚀 **易于使用**:支持 Web 控制台、图形界面和命令行多种使用方式。
|
||||
- 🌍 **跨平台**:支持 Windows、macOS、Linux、FreeBSD、Android 和多种 CPU 架构。
|
||||
- 🔐 **安全**:支持 AES-GCM 或 WireGuard 加密,保护网络通信。
|
||||
- 🔌 **高效 NAT 穿透**:支持 UDP、IPv6 穿透,可打通 NAT4-NAT4 场景。
|
||||
- 🌐 **子网代理**:可将私有子网共享给虚拟网络中的其他节点访问。
|
||||
- 🔄 **智能路由**:自动选择更优链路,降低延迟并提升体验。
|
||||
- ⚡ **高性能**:全链路零拷贝,支持 TCP、UDP、WS、WSS、WG、QUIC 等协议。
|
||||
|
||||
```text
|
||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.6.0-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-70e69a38~ |
|
||||
```
|
||||
## 深入了解
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
- [简介](https://easytier.cn/guide/introduction.html)
|
||||
- [命令行组网](https://easytier.cn/guide/networking.html)
|
||||
- [去中心化组网](https://easytier.cn/guide/network/decentralized-networking.html)
|
||||
- [通过 Web 控制台组网](https://easytier.cn/guide/network/web-console.html)
|
||||
- [使用 WireGuard 客户端接入](https://easytier.cn/guide/network/use-easytier-with-wireguard-client.html)
|
||||
- [子网代理](https://easytier.cn/guide/network/point-to-networking.html)
|
||||
- [带宽与延迟优化](https://easytier.cn/guide/network/kcp-proxy.html)
|
||||
- [自建公共共享节点](https://easytier.cn/guide/network/host-public-server.html)
|
||||
- [第三方图形界面](https://easytier.cn/guide/installation_gui.html#%E7%AC%AC%E4%B8%89%E6%96%B9%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2)
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
注意:如果无法 ping 通,可能是防火墙阻止了入站流量。请关闭防火墙或添加允许规则。
|
||||
|
||||
为了提高可用性,您可以同时连接多个共享节点:
|
||||
|
||||
```bash
|
||||
# 连接多个共享节点
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<公共节点IP>:11010 -p udp://<公共节点IP>:11010
|
||||
```
|
||||
|
||||
#### 去中心化组网
|
||||
|
||||
EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
|
||||
|
||||
1. 启动第一个节点(节点 A):
|
||||
|
||||
```bash
|
||||
# 启动第一个节点
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
启动后,该节点将默认监听以下端口:
|
||||
- TCP:11010
|
||||
- UDP:11010
|
||||
- WebSocket:11011
|
||||
- WebSocket SSL:11012
|
||||
- WireGuard:11013
|
||||
|
||||
2. 连接第二个节点(节点 B):
|
||||
|
||||
```bash
|
||||
# 使用第一个节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010
|
||||
```
|
||||
|
||||
3. 验证连接:
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.144.144.2
|
||||
|
||||
# 查看已连接的对等节点
|
||||
easytier-cli peer
|
||||
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
|
||||
# 查看本地节点信息
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
更多节点要加入网络,可以使用 `-p` 参数连接到网络中的任何现有节点:
|
||||
|
||||
```bash
|
||||
# 使用任何现有节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.3 -p udp://任何现有节点的公网IP:11010
|
||||
```
|
||||
|
||||
### 🔍 高级功能
|
||||
|
||||
#### 子网代理
|
||||
|
||||
假设网络拓扑如下,节点 B 想要与其他节点共享其可访问的子网 10.1.1.0/24:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph 节点 A 公网 IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
要共享子网,在启动 EasyTier 时添加 `-n` 参数:
|
||||
|
||||
```bash
|
||||
# 与其他节点共享子网 10.1.1.0/24
|
||||
sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
子网代理信息将自动同步到虚拟网络中的每个节点,每个节点将自动配置相应的路由。您可以验证子网代理设置:
|
||||
|
||||
1. 检查路由信息是否已同步(proxy_cidrs 列显示代理的子网):
|
||||
|
||||
```bash
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. 测试是否可以访问代理子网中的节点:
|
||||
|
||||
```bash
|
||||
# 测试到代理子网的连通性
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
#### WireGuard 集成
|
||||
|
||||
EasyTier 可以作为 WireGuard 服务器,允许任何安装了 WireGuard 客户端的设备(包括 iOS 和 Android)访问 EasyTier 网络。以下是设置示例:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone<br/>已安装 WireGuard]]
|
||||
|
||||
subgraph 节点 A 公网 IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
1. 启动启用 WireGuard 门户的 EasyTier:
|
||||
|
||||
```bash
|
||||
# 在 0.0.0.0:11013 上监听,并使用 10.14.14.0/24 子网作为 WireGuard 客户端
|
||||
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
2. 获取 WireGuard 客户端配置:
|
||||
|
||||
```bash
|
||||
# 获取 WireGuard 客户端配置
|
||||
easytier-cli vpn-portal
|
||||
```
|
||||
|
||||
3. 在输出配置中:
|
||||
- 将 `Interface.Address` 设置为 WireGuard 子网中的可用 IP
|
||||
- 将 `Peer.Endpoint` 设置为您的 EasyTier 节点的公网 IP/域名
|
||||
- 将修改后的配置导入到您的 WireGuard 客户端
|
||||
|
||||
#### 自建公共共享节点
|
||||
|
||||
您可以运行自己的公共共享节点来帮助其他节点相互发现。公共共享节点只是一个普通的 EasyTier 网络(具有相同的网络名称和密钥),其他网络可以连接到它。
|
||||
|
||||
要运行公共共享节点:
|
||||
|
||||
```bash
|
||||
# 公共共享节点无需指定 IPv4 地址
|
||||
sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
```
|
||||
|
||||
网络设置成功后,您可以轻松配置它以在系统启动时自动启动。请参阅 [一键注册服务指南](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) 了解如何将 EasyTier 注册为系统服务。
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
|
||||
- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
|
||||
|
||||
### 联系我们
|
||||
## 社区
|
||||
|
||||
- 💬 **[Telegram 群组](https://t.me/easytier)**
|
||||
- 👥 **QQ 群**
|
||||
- 一群 [949700262](https://qm.qq.com/q/wFoTUChqZW)
|
||||
- 二群 [837676408](https://qm.qq.com/q/4V33DrfgHe)
|
||||
- 三群 [957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
- 👥 **QQ 群**:[一群 949700262](https://qm.qq.com/q/wFoTUChqZW)、[二群 837676408](https://qm.qq.com/q/4V33DrfgHe)、[三群 957189589](https://qm.qq.com/q/YNyTQjwlai)
|
||||
|
||||
## 许可证
|
||||
|
||||
@@ -301,25 +103,25 @@ EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
本项目的 CDN 加速和安全防护由腾讯云 EdgeOne 赞助。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200">
|
||||
</a>
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200" alt="EdgeOne Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200" alt="浪浪云 Logo">
|
||||
</a>
|
||||
<a href="https://www.rainyun.com/NjM0NzQ1_" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200" alt="雨云 Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。
|
||||
如果您觉得 EasyTier 有帮助,欢迎赞助我们。软件开发和维护需要持续投入,您的支持将帮助我们更好地维护和改进 EasyTier。
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
<img src="assets/wechat.png" width="200" alt="微信赞助二维码">
|
||||
<img src="assets/alipay.png" width="200" alt="支付宝赞助二维码">
|
||||
</p>
|
||||
|
||||
@@ -1,74 +1,43 @@
|
||||
#!/data/adb/magisk/busybox sh
|
||||
MODDIR=${0%/*}
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
IP_RULE_SCRIPT="${MODDIR}/hotspot_iprule.sh"
|
||||
|
||||
ET_STATUS=""
|
||||
REDIR_STATUS=""
|
||||
IS_RUNNING=false
|
||||
|
||||
# 确保辅助脚本有执行权限
|
||||
chmod +x "${IP_RULE_SCRIPT}" 2>/dev/null
|
||||
|
||||
# 更新 module.prop 文件中的 description
|
||||
# 更新module.prop文件中的description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
|
||||
}
|
||||
|
||||
# 判断程序启动状态
|
||||
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
IS_RUNNING=false
|
||||
ET_STATUS="主程序已关闭"
|
||||
|
||||
elif pgrep -f "${MODDIR}/easytier-core" >/dev/null; then
|
||||
IS_RUNNING=true
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
ET_STATUS="主程序正在运行(启动参数模式)"
|
||||
ET_STATUS="已关闭"
|
||||
elif pgrep -f 'easytier-core' >/dev/null; then
|
||||
if [ -f "${MODDIR}/config/command_args"]; then
|
||||
ET_STATUS="主程序已开启(启动参数模式)"
|
||||
else
|
||||
ET_STATUS="主程序正在运行(配置文件模式)"
|
||||
ET_STATUS="主程序已开启(配置文件模式)"
|
||||
fi
|
||||
|
||||
elif [ -z "$ET_STATUS" ]; then
|
||||
# 既没 disable 也没运行,说明是异常停止或未启动
|
||||
ET_STATUS="主程序启动失败或未运行"
|
||||
fi
|
||||
|
||||
# 无论主程序是否运行,都允许切换“开关文件”的状态,以便下次生效
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
rm -f "${MODDIR}/enable_IP_rule"
|
||||
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
|
||||
REDIR_STATUS="转发已禁用"
|
||||
echo "热点子网转发已禁用"
|
||||
echo "[ET-NAT] Action: IP rule disabled." >> "${MODDIR}/log.log"
|
||||
#ET_STATUS不存在说明开启模块未正常运行,不修改状态
|
||||
if [ -n "$ET_STATUS" ]; then
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
rm -f "${MODDIR}/enable_IP_rule"
|
||||
${MODDIR}/hotspot_iprule.sh del
|
||||
REDIR_STATUS="转发已禁用"
|
||||
echo "热点子网转发已禁用"
|
||||
echo "[ET-NAT] IP rule disabled." >> "${MODDIR}/log.log"
|
||||
else
|
||||
touch "${MODDIR}/enable_IP_rule"
|
||||
${MODDIR}/hotspot_iprule.sh del
|
||||
${MODDIR}/hotspot_iprule.sh add_once
|
||||
REDIR_STATUS="转发已激活"
|
||||
echo "热点子网转发已激活,热点开启后将自动将热点加入转发网络(要求已配置本地网络cidr=参数)。转发规则将随着热点开关而自动开关。该状态将保持到转发被禁用为止。"
|
||||
echo "[ET-NAT] IP rule enabled." >> "${MODDIR}/log.log"
|
||||
fi
|
||||
update_module_description "${ET_STATUS} | ${REDIR_STATUS}"
|
||||
else
|
||||
touch "${MODDIR}/enable_IP_rule"
|
||||
|
||||
if [ "$IS_RUNNING" = true ]; then
|
||||
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
|
||||
"${IP_RULE_SCRIPT}" add_once
|
||||
echo "转发规则将立即生效,无需重启"
|
||||
else
|
||||
echo "主程序未运行,转发规则将在下次启动时生效"
|
||||
fi
|
||||
|
||||
REDIR_STATUS="转发已激活"
|
||||
echo "----------------------------------"
|
||||
echo "热点子网转发已激活"
|
||||
echo "热点开启后将自动将热点加入转发网络"
|
||||
echo "需要在配置中提前配置好 cidr 参数"
|
||||
echo "----------------------------------"
|
||||
echo "[ET-NAT] Action: IP rule enabled." >> "${MODDIR}/log.log"
|
||||
echo "主程序未正常启动,请先检查配置文件"
|
||||
fi
|
||||
|
||||
sync
|
||||
update_module_description "${ET_STATUS}| ${REDIR_STATUS}"
|
||||
@@ -5,15 +5,12 @@ LATESTARTSERVICE=true
|
||||
|
||||
set_perm_recursive $MODPATH 0 0 0777 0777
|
||||
|
||||
ui_print "系统架构为:$ARCH"
|
||||
ui_print "系统 SDK 版本:$API"
|
||||
ui_print "EasyTier 安装位置:/data/adb/modules/easytier_magisk"
|
||||
ui_print "配置文件位置:/data/adb/modules/easytier_magisk/config/config.toml"
|
||||
ui_print "如需使用启动参数模式,请将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中的内容"
|
||||
ui_print "config 目录中存在 command_args 文件时,模块会自动忽略 config.toml 文件"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "注意!启动参数文件中不能存在 \" 和 ',配置文件则没有这个限制"
|
||||
ui_print "----------------------------------"
|
||||
ui_print "修改配置后无需重启设备,在 Magisk 中禁用 EasyTier 模块,等待 10 秒后重新启用即可让新配置生效"
|
||||
ui_print "点击 Magisk 中模块左下角的“操作”按钮可以禁用或激活热点子网转发,使用该功能前需要在配置中提前配置好 cidr 参数"
|
||||
ui_print "模块安装完成,重启设备生效"
|
||||
ui_print '安装完成'
|
||||
ui_print '当前架构为' + $ARCH
|
||||
ui_print '当前系统版本为' + $API
|
||||
ui_print '安装目录为: /data/adb/modules/easytier_magisk'
|
||||
ui_print '配置文件位置: /data/adb/modules/easytier_magisk/config/config.toml'
|
||||
ui_print '如果需要自定义启动参数,可将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中内容,使用自定义启动参数时会忽略配置文件'
|
||||
ui_print '修改配置文件后在magisk app禁用应用再启动即可生效'
|
||||
ui_print '点击操作按钮可启动/关闭热点子网转发,配合easytier的子网代理功能实现手机热点访问easytier网络'
|
||||
ui_print '记得重启'
|
||||
|
||||
@@ -2,111 +2,64 @@
|
||||
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_FILE="${MODDIR}/config/config.toml"
|
||||
COMMAND_ARGS="${MODDIR}/config/command_args"
|
||||
LOG_FILE="${MODDIR}/log.log"
|
||||
MODULE_PROP="${MODDIR}/module.prop"
|
||||
EASYTIER="${MODDIR}/easytier-core"
|
||||
|
||||
# 处理获取到的设备型号中可能出现的空格
|
||||
BRAND=$(getprop ro.product.brand | tr ' ' '-')
|
||||
MODEL=$(getprop ro.product.model | tr ' ' '-')
|
||||
DEVICE_HOSTNAME="${BRAND}-${MODEL}"
|
||||
REDIR_STATUS=""
|
||||
|
||||
# 更新 module.prop 文件中的 description
|
||||
# 更新module.prop文件中的description
|
||||
update_module_description() {
|
||||
local status_message=$1
|
||||
# 检查 module.prop 文件存在且 description 发生变化了再写入
|
||||
if [ -f "${MODULE_PROP}" ]; then
|
||||
local current_desc=$(grep "^description=" "${MODULE_PROP}")
|
||||
local new_desc="description=[状态] ${status_message}"
|
||||
if [ "${current_desc}" != "${new_desc}" ]; then
|
||||
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
|
||||
fi
|
||||
fi
|
||||
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
|
||||
}
|
||||
|
||||
# 检查并初始化 TUN 设备
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
REDIR_STATUS="转发已激活"
|
||||
else
|
||||
REDIR_STATUS="转发已禁用"
|
||||
fi
|
||||
|
||||
if [ ! -e /dev/net/tun ]; then
|
||||
if [ ! -d /dev/net ]; then
|
||||
mkdir -p /dev/net
|
||||
fi
|
||||
|
||||
|
||||
ln -s /dev/tun /dev/net/tun
|
||||
fi
|
||||
|
||||
while true; do
|
||||
# 获取子网转发激活状态
|
||||
if [ -f "${MODDIR}/enable_IP_rule" ]; then
|
||||
REDIR_STATUS="转发已激活"
|
||||
if ls $MODDIR | grep -q "disable"; then
|
||||
update_module_description "关闭中 | ${REDIR_STATUS}"
|
||||
if pgrep -f 'easytier-core' >/dev/null; then
|
||||
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭 ..."
|
||||
pkill easytier-core # 关闭进程
|
||||
fi
|
||||
else
|
||||
REDIR_STATUS="转发已禁用"
|
||||
fi
|
||||
if ! pgrep -f 'easytier-core' >/dev/null; then
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
update_module_description "config.toml不存在"
|
||||
sleep 3s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查模块是否被禁用
|
||||
if [ -f "${MODDIR}/disable" ]; then
|
||||
update_module_description "主程序已关闭 | ${REDIR_STATUS}"
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
echo "开关控制 $(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭"
|
||||
pkill -f "${EASYTIER}"
|
||||
fi
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查进程是否已经在运行
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${COMMAND_ARGS}" ]; then
|
||||
update_module_description "缺少配置文件或启动参数文件"
|
||||
sleep 10s
|
||||
continue
|
||||
fi
|
||||
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${COMMAND_ARGS}" ]; then
|
||||
# 启动参数模式
|
||||
CMD_CONTENT=$(tr '\r\n' ' ' < "${COMMAND_ARGS}")
|
||||
|
||||
if echo "${CMD_CONTENT}" | grep -q "\-\-hostname"; then
|
||||
FINAL_ARGS="${CMD_CONTENT}"
|
||||
else
|
||||
FINAL_ARGS="${CMD_CONTENT} --hostname ${DEVICE_HOSTNAME}"
|
||||
fi
|
||||
|
||||
TZ=Asia/Shanghai "${EASYTIER}" ${FINAL_ARGS} > "${LOG_FILE}" 2>&1 &
|
||||
STR_MODE="启动参数模式"
|
||||
|
||||
# 否则读取 config.toml 的内容作为启动参数
|
||||
else
|
||||
# 配置文件模式
|
||||
if grep -q "^[[:space:]]*hostname[[:space:]]*=" "${CONFIG_FILE}"; then
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" > "${LOG_FILE}" 2>&1 &
|
||||
else
|
||||
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" --hostname "${DEVICE_HOSTNAME}" > "${LOG_FILE}" 2>&1 &
|
||||
fi
|
||||
|
||||
STR_MODE="配置文件模式"
|
||||
fi
|
||||
|
||||
# 等待进程启动
|
||||
sleep 5s
|
||||
|
||||
# 启动后的扫尾工作
|
||||
if pgrep -f "${EASYTIER}" >/dev/null; then
|
||||
|
||||
if ! ip rule show | grep -q "lookup main"; then
|
||||
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
|
||||
if [ -f "${MODDIR}/config/command_args" ]; then
|
||||
TZ=Asia/Shanghai ${EASYTIER} $(cat ${MODDIR}/config/command_args) --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(启动参数模式) | ${REDIR_STATUS}"
|
||||
else
|
||||
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} --hostname "$(getprop ro.product.brand)-$(getprop ro.product.model)" > ${LOG_FILE} &
|
||||
sleep 5s # 等待easytier-core启动完成
|
||||
update_module_description "主程序已开启(配置文件模式) | ${REDIR_STATUS}"
|
||||
fi
|
||||
ip rule add from all lookup main
|
||||
if ! pgrep -f 'easytier-core' >/dev/null; then
|
||||
update_module_descriptio "主程序启动失败,请检查配置文件"
|
||||
fi
|
||||
else
|
||||
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在"
|
||||
fi
|
||||
|
||||
update_module_description "主程序正在运行(${STR_MODE})| ${REDIR_STATUS}"
|
||||
else
|
||||
update_module_description "主程序启动失败,请检查配置文件或启动参数"
|
||||
fi
|
||||
|
||||
sleep 10s
|
||||
done
|
||||
sleep 3s # 暂停3秒后再次执行循环
|
||||
done
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.6.0
|
||||
version=v2.5.0
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
MODDIR=${0%/*}
|
||||
pkill -f "${MODDIR}/easytier-core"
|
||||
|
||||
# 使用 ${MODDIR:?} 确保变量非空,避免执行 rm -rf /*
|
||||
rm -rf "${MODDIR:?}/"*
|
||||
pkill easytier-core # 结束 easytier-core 进程
|
||||
rm -rf $MODDIR/*
|
||||
+7
-153
@@ -1083,7 +1083,7 @@ checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -1101,7 +1101,6 @@ dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"clap",
|
||||
@@ -1116,7 +1115,6 @@ dependencies = [
|
||||
"easytier-rpc-build",
|
||||
"encoding",
|
||||
"flume",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"git-version",
|
||||
@@ -1133,7 +1131,6 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"idna",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"machine-uid",
|
||||
"multimap",
|
||||
@@ -1156,9 +1153,7 @@ dependencies = [
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"prost-wkt-types",
|
||||
"prost-types",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
"rand 0.8.5",
|
||||
@@ -1178,7 +1173,6 @@ dependencies = [
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"socket2 0.5.10",
|
||||
"strum",
|
||||
"stun_codec",
|
||||
"sys-locale",
|
||||
"tabled",
|
||||
@@ -1360,17 +1354,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
@@ -1488,16 +1471,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -2244,15 +2217,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
@@ -2860,12 +2824,6 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.0"
|
||||
@@ -3455,52 +3413,6 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497e1e938f0c09ef9cabe1d49437b4016e03e8f82fbbe5d1c62a9b61b9decae1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"inventory",
|
||||
"prost",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"typetag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-build"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07b8bf115b70a7aa5af1fd5d6e9418492e9ccb6e4785e858c938e28d132a884b"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-wkt-types"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8cdde6df0a98311c839392ca2f2f0bcecd545f86a62b4e3c6a49c336e970fe5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"prost-wkt",
|
||||
"prost-wkt-build",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
@@ -3544,9 +3456,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fastbloom",
|
||||
@@ -4224,12 +4136,6 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
@@ -4319,27 +4225,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stun_codec"
|
||||
version = "0.3.5"
|
||||
@@ -4690,9 +4575,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.13.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -4703,11 +4588,10 @@ dependencies = [
|
||||
"httparse",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"simdutf8",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"webpki-roots 1.0.2",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4939,42 +4823,12 @@ dependencies = [
|
||||
"wintun",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "typetag"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"typetag-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typetag-impl"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.6.0",
|
||||
"version": "2.5.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"core:tray:allow-set-show-menu-on-left-click",
|
||||
"core:tray:allow-set-tooltip",
|
||||
"vpnservice:allow-ping",
|
||||
"vpnservice:allow-get-vpn-status",
|
||||
"vpnservice:allow-prepare-vpn",
|
||||
"vpnservice:allow-start-vpn",
|
||||
"vpnservice:allow-stop-vpn",
|
||||
@@ -48,4 +47,4 @@
|
||||
"os:allow-platform",
|
||||
"os:allow-locale"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -206,16 +206,6 @@ async fn update_network_config_state(
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
let client_manager = get_client_manager!()?;
|
||||
if !disabled {
|
||||
let cfg = client_manager
|
||||
.handle_get_network_config(app.clone(), instance_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
client_manager
|
||||
.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.await?;
|
||||
}
|
||||
client_manager
|
||||
.handle_update_network_state(app.clone(), instance_id, disabled)
|
||||
.await
|
||||
@@ -225,10 +215,6 @@ async fn update_network_config_state(
|
||||
client_manager
|
||||
.post_stop_network_instances_hook(&app)
|
||||
.await?;
|
||||
} else {
|
||||
client_manager
|
||||
.post_run_network_instance_hook(&app, &instance_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -844,7 +830,7 @@ mod manager {
|
||||
cfg: &easytier::common::config::TomlConfigLoader,
|
||||
) -> Result<(), String> {
|
||||
let instance_id = cfg.get_id();
|
||||
app.emit("pre_run_network_instance", instance_id.to_string())
|
||||
app.emit("pre_run_network_instance", instance_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -881,21 +867,20 @@ mod manager {
|
||||
let app_clone = app.clone();
|
||||
let instance_id_clone = *instance_id;
|
||||
tokio::spawn(async move {
|
||||
let instance_id_str = instance_id_clone.to_string();
|
||||
loop {
|
||||
match event_receiver.recv().await {
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _)) => {
|
||||
let _ = app_clone.emit("dhcp_ip_changed", &instance_id_str);
|
||||
let _ = app_clone.emit("dhcp_ip_changed", instance_id_clone);
|
||||
}
|
||||
Ok(easytier::common::global_ctx::GlobalCtxEvent::ProxyCidrsUpdated(_, _)) => {
|
||||
let _ = app_clone.emit("proxy_cidrs_updated", &instance_id_str);
|
||||
let _ = app_clone.emit("proxy_cidrs_updated", instance_id_clone);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
let _ = app_clone.emit("event_lagged", &instance_id_str);
|
||||
let _ = app_clone.emit("event_lagged", instance_id_clone);
|
||||
event_receiver = event_receiver.resubscribe();
|
||||
}
|
||||
}
|
||||
@@ -907,7 +892,7 @@ mod manager {
|
||||
|
||||
self.storage.enabled_networks.insert(*instance_id);
|
||||
|
||||
app.emit("post_run_network_instance", instance_id.to_string())
|
||||
app.emit("post_run_network_instance", instance_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
@@ -986,26 +971,20 @@ mod manager {
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| i.value().1.clone());
|
||||
let Some(config) = config else {
|
||||
if config.is_none() {
|
||||
continue;
|
||||
};
|
||||
let toml_config = config.gen_config()?;
|
||||
self.pre_run_network_instance_hook(&app, &toml_config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
}
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config: Some(config),
|
||||
config,
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.post_run_network_instance_hook(&app, &uuid)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
self.storage.enabled_networks.insert(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.6.0",
|
||||
"version": "2.5.0",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
||||
Vendored
-2
@@ -93,7 +93,6 @@ declare global {
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const syncMobileVpnService: typeof import('./composables/mobile_vpn')['syncMobileVpnService']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
@@ -218,7 +217,6 @@ declare module 'vue' {
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly syncMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['syncMobileVpnService']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Event, listen } from "@tauri-apps/api/event";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { NetworkTypes } from "easytier-frontend-lib"
|
||||
import { Utils } from "easytier-frontend-lib";
|
||||
|
||||
const EVENTS = Object.freeze({
|
||||
SAVE_CONFIGS: 'save_configs',
|
||||
@@ -18,71 +17,39 @@ function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
localStorage.setItem('networkList', JSON.stringify(event.payload.map((config) => NetworkTypes.normalizeNetworkConfig(config))));
|
||||
}
|
||||
|
||||
function normalizeInstanceIdPayload(payload: unknown): string {
|
||||
if (typeof payload === 'string') {
|
||||
return payload
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object') {
|
||||
const uuid = payload as Partial<Utils.UUID>
|
||||
if (
|
||||
typeof uuid.part1 === 'number'
|
||||
&& typeof uuid.part2 === 'number'
|
||||
&& typeof uuid.part3 === 'number'
|
||||
&& typeof uuid.part4 === 'number'
|
||||
) {
|
||||
return Utils.UuidToStr(uuid as Utils.UUID)
|
||||
}
|
||||
}
|
||||
|
||||
if (payload == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const fallback = String(payload)
|
||||
return fallback === '[object Object]' ? '' : fallback
|
||||
}
|
||||
|
||||
async function onPreRunNetworkInstance(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.PRE_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
|
||||
async function onPreRunNetworkInstance(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService(instanceId);
|
||||
await prepareVpnService(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onPostRunNetworkInstance(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.POST_RUN_NETWORK_INSTANCE}', raw payload:`, event.payload, 'normalized:', instanceId)
|
||||
async function onPostRunNetworkInstance(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(event: Event<unknown>) {
|
||||
console.log(`Received event '${EVENTS.VPN_SERVICE_STOP}', raw payload:`, event.payload)
|
||||
await syncMobileVpnService();
|
||||
async function onVpnServiceStop(event: Event<string>) {
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
|
||||
async function onDhcpIpChanged(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${instanceId}`);
|
||||
async function onDhcpIpChanged(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.DHCP_IP_CHANGED}' for instance: ${event.payload}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onProxyCidrsUpdated(event: Event<unknown>) {
|
||||
const instanceId = normalizeInstanceIdPayload(event.payload)
|
||||
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${instanceId}`);
|
||||
async function onProxyCidrsUpdated(event: Event<string>) {
|
||||
console.log(`Received event '${EVENTS.PROXY_CIDRS_UPDATED}' for instance: ${event.payload}`);
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(instanceId);
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
async function onEventLagged(event: Event<unknown>) {
|
||||
async function onEventLagged(event: Event<string>) {
|
||||
if (type() === 'android') {
|
||||
await onNetworkInstanceChange(normalizeInstanceIdPayload(event.payload));
|
||||
await onNetworkInstanceChange(event.payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { Utils } from 'easytier-frontend-lib'
|
||||
import { get_vpn_status, prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
|
||||
type Route = NetworkTypes.Route
|
||||
|
||||
@@ -24,53 +24,6 @@ const curVpnStatus: vpnStatus = {
|
||||
dns: undefined,
|
||||
}
|
||||
|
||||
async function requestVpnPermission() {
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
|
||||
const granted = prepare_ret?.granted ?? true
|
||||
if (!granted) {
|
||||
console.info('vpn permission request was denied or dismissed')
|
||||
}
|
||||
|
||||
return granted
|
||||
}
|
||||
|
||||
function resetVpnConfigStatus() {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
}
|
||||
|
||||
function syncVpnStatusFromNative(status: Awaited<ReturnType<typeof get_vpn_status>>) {
|
||||
curVpnStatus.running = status?.running ?? false
|
||||
if (!curVpnStatus.running) {
|
||||
resetVpnConfigStatus()
|
||||
return
|
||||
}
|
||||
|
||||
const ipv4WithCidr = status?.ipv4Addr
|
||||
if (ipv4WithCidr?.length) {
|
||||
const [ipv4Addr, cidr] = ipv4WithCidr.split('/')
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
|
||||
const parsedCidr = Number(cidr)
|
||||
curVpnStatus.ipv4Cidr = Number.isInteger(parsedCidr) ? parsedCidr : undefined
|
||||
}
|
||||
else {
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.ipv4Cidr = undefined
|
||||
}
|
||||
|
||||
curVpnStatus.routes = [...(status?.routes ?? [])]
|
||||
curVpnStatus.dns = status?.dns ?? undefined
|
||||
}
|
||||
|
||||
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
const start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
@@ -81,19 +34,18 @@ async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function doStopVpn(force = false) {
|
||||
const wasRunning = curVpnStatus.running
|
||||
if (!force && !wasRunning) {
|
||||
async function doStopVpn() {
|
||||
if (!curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
if (wasRunning) {
|
||||
await waitVpnStatus(false, 3)
|
||||
}
|
||||
await waitVpnStatus(false, 3)
|
||||
|
||||
resetVpnConfigStatus()
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.dns = undefined
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?: string) {
|
||||
@@ -102,32 +54,19 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[], dns?
|
||||
}
|
||||
|
||||
console.log('start vpn service', ipv4Addr, cidr, routes, dns)
|
||||
const request = {
|
||||
const start_ret = await start_vpn({
|
||||
ipv4Addr: `${ipv4Addr}/${cidr}`,
|
||||
routes,
|
||||
dns,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
mtu: 1300,
|
||||
}
|
||||
|
||||
let start_ret = await start_vpn(request)
|
||||
console.log('start vpn response', JSON.stringify(start_ret))
|
||||
if (start_ret?.errorMsg === 'need_prepare') {
|
||||
const granted = await requestVpnPermission()
|
||||
if (!granted) {
|
||||
throw new Error('vpn_permission_denied')
|
||||
}
|
||||
start_ret = await start_vpn(request)
|
||||
console.log('start vpn retry response', JSON.stringify(start_ret))
|
||||
}
|
||||
|
||||
})
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.ipv4Cidr = cidr
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.dns = dns
|
||||
}
|
||||
@@ -136,16 +75,13 @@ async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
await setTunFd(payload.fd).catch((e) => {
|
||||
console.error('set tun fd failed', e)
|
||||
})
|
||||
setTunFd(payload.fd)
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
resetVpnConfigStatus()
|
||||
}
|
||||
|
||||
async function registerVpnServiceListener() {
|
||||
@@ -199,25 +135,15 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
}
|
||||
|
||||
if (!instanceId) {
|
||||
console.warn('vpn service skipped because instance id is empty')
|
||||
if (curVpnStatus.running) {
|
||||
await doStopVpn()
|
||||
}
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
const config = await getConfig(instanceId)
|
||||
console.log('vpn service loaded config', instanceId, JSON.stringify({
|
||||
no_tun: config.no_tun,
|
||||
dhcp: config.dhcp,
|
||||
enable_magic_dns: config.enable_magic_dns,
|
||||
}))
|
||||
if (config.no_tun) {
|
||||
console.log('vpn service skipped because no_tun is enabled', instanceId)
|
||||
return
|
||||
}
|
||||
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
console.warn('vpn service skipped because network info is unavailable', instanceId, curNetworkInfo?.error_msg)
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
@@ -244,39 +170,27 @@ export async function onNetworkInstanceChange(instanceId: string) {
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
|
||||
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined
|
||||
const dns = config.enable_magic_dns ? '100.100.100.101' : undefined;
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const cidrChanged = network_length !== curVpnStatus.ipv4Cidr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
const dnsChanged = dns != curVpnStatus.dns
|
||||
const configChanged = ipChanged || cidrChanged || routesChanged || dnsChanged
|
||||
const shouldStartVpn = !curVpnStatus.running
|
||||
|
||||
if (shouldStartVpn || configChanged) {
|
||||
if (ipChanged || routesChanged || dnsChanged) {
|
||||
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
if (curVpnStatus.running) {
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, network_length, routes, dns)
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && e.message === 'need_prepare') {
|
||||
console.info('vpn permission is required before starting the Android VPN service')
|
||||
return
|
||||
}
|
||||
if (e instanceof Error && e.message === 'vpn_permission_denied') {
|
||||
console.info('vpn permission request was denied or dismissed')
|
||||
return
|
||||
}
|
||||
console.error('start vpn service failed', e)
|
||||
console.error('start vpn service failed, stop all other network insts.', e)
|
||||
await runNetworkInstance(config, true); //on android config should always be saved
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,22 +202,6 @@ async function isNoTunEnabled(instanceId: string | undefined) {
|
||||
return (await getConfig(instanceId)).no_tun ?? false
|
||||
}
|
||||
|
||||
async function findRunningTunInstanceId() {
|
||||
const instanceIds = await listNetworkInstanceIds()
|
||||
const runningIds = instanceIds.running_inst_ids.map(Utils.UuidToStr)
|
||||
console.log('vpn service sync running instances', JSON.stringify(runningIds))
|
||||
|
||||
for (const instanceId of runningIds) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return instanceId
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
await registerVpnServiceListener()
|
||||
}
|
||||
@@ -312,22 +210,10 @@ export async function prepareVpnService(instanceId: string) {
|
||||
if (await isNoTunEnabled(instanceId)) {
|
||||
return
|
||||
}
|
||||
await requestVpnPermission()
|
||||
}
|
||||
|
||||
export async function syncMobileVpnService() {
|
||||
syncVpnStatusFromNative(await get_vpn_status())
|
||||
const instanceId = await findRunningTunInstanceId()
|
||||
if (instanceId) {
|
||||
console.log('vpn service sync selected instance', instanceId)
|
||||
await onNetworkInstanceChange(instanceId)
|
||||
return
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
|
||||
if (dhcpPollingTimer) {
|
||||
clearTimeout(dhcpPollingTimer)
|
||||
dhcpPollingTimer = null
|
||||
}
|
||||
|
||||
await doStopVpn(true)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { exit } from '@tauri-apps/plugin-process'
|
||||
import { I18nUtils, RemoteManagement, Utils } from "easytier-frontend-lib"
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { initMobileVpnService } from '~/composables/mobile_vpn'
|
||||
import { GUIRemoteClient } from '~/modules/api'
|
||||
|
||||
import { useToast, useConfirm } from 'primevue'
|
||||
@@ -190,25 +189,9 @@ async function initWithMode(mode: Mode) {
|
||||
clientRunning.value = await isClientRunning()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const cleanupFns: Array<() => void> = []
|
||||
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
await initMobileVpnService()
|
||||
console.error("easytier init vpn service done")
|
||||
} catch (e: any) {
|
||||
console.error("easytier init vpn service failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
cleanupFns.push(await listenGlobalEvents())
|
||||
onMounted(() => {
|
||||
currentMode.value = loadMode()
|
||||
await initWithMode(currentMode.value);
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupFns.forEach(unlisten => unlisten())
|
||||
})
|
||||
initWithMode(currentMode.value);
|
||||
});
|
||||
|
||||
useTray(true)
|
||||
@@ -364,6 +347,22 @@ async function connectRpcClient(isNormalMode: boolean, url?: string) {
|
||||
console.log("easytier rpc connection established, isNormalMode: ", isNormalMode)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
await initMobileVpnService()
|
||||
console.error("easytier init vpn service done")
|
||||
} catch (e: any) {
|
||||
console.error("easytier init vpn service failed", e)
|
||||
}
|
||||
}
|
||||
const unlisten = await listenGlobalEvents()
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
})
|
||||
|
||||
async function openConfigServerDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
configServerDialogVisible.value = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
edition = "2021"
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
|
||||
@@ -209,8 +209,7 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
|
||||
</div>
|
||||
<div class="items-center flex flex-col p-fluid gap-y-2">
|
||||
<UrlListInput id="initial_nodes" v-model="curNetwork.peer_urls" :protos="protos"
|
||||
defaultUrl="tcp://:11010" :add-label="t('add_initial_node')"
|
||||
:placeholder="t('initial_node_placeholder')" />
|
||||
:add-label="t('add_initial_node')" :placeholder="t('initial_node_placeholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,19 +305,6 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex">
|
||||
<label for="instance_recv_bps_limit">{{ t('instance_recv_bps_limit') }}</label>
|
||||
<span class="pi pi-question-circle ml-2 self-center"
|
||||
v-tooltip="t('instance_recv_bps_limit_help')"></span>
|
||||
</div>
|
||||
<InputNumber id="instance_recv_bps_limit" v-model="curNetwork.instance_recv_bps_limit"
|
||||
aria-describedby="instance_recv_bps_limit-help" :format="false"
|
||||
:placeholder="t('instance_recv_bps_limit_placeholder')" :min="1" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex">
|
||||
|
||||
@@ -15,7 +15,6 @@ const url = defineModel<string>({ required: true })
|
||||
const editing = ref(false)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const internalCompact = ref(false)
|
||||
const hostFocused = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (container.value) {
|
||||
@@ -37,86 +36,36 @@ const parseUrl = (val: string | null | undefined) => {
|
||||
const p = parseInt(portStr)
|
||||
return isNaN(p) ? (props.protos[proto] ?? 11010) : p
|
||||
}
|
||||
const parseByPattern = (input: string) => {
|
||||
const trimmed = input.trim()
|
||||
if (!trimmed) {
|
||||
return null
|
||||
}
|
||||
const match = trimmed.match(/^(\w+):\/\/(.*)$/)
|
||||
const proto = match ? match[1] : 'tcp'
|
||||
const rest = match ? match[2] : trimmed
|
||||
const authority = rest.split(/[/?#]/)[0]
|
||||
if (!authority) {
|
||||
return null
|
||||
}
|
||||
const hostAndMaybePort = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority
|
||||
if (hostAndMaybePort.startsWith('[')) {
|
||||
const ipv6End = hostAndMaybePort.indexOf(']')
|
||||
if (ipv6End > 0) {
|
||||
const host = hostAndMaybePort.slice(0, ipv6End + 1)
|
||||
const remain = hostAndMaybePort.slice(ipv6End + 1)
|
||||
const port = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : (props.protos[proto] ?? 11010)
|
||||
return { proto, host, port }
|
||||
}
|
||||
}
|
||||
const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/)
|
||||
const host = portMatch ? portMatch[1] : hostAndMaybePort
|
||||
const port = portMatch ? parseInt(portMatch[2]) : (props.protos[proto] ?? 11010)
|
||||
return { proto, host, port }
|
||||
}
|
||||
|
||||
if (!val) {
|
||||
return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 }
|
||||
}
|
||||
const parsedByPattern = parseByPattern(val)
|
||||
if (parsedByPattern) {
|
||||
return parsedByPattern
|
||||
try {
|
||||
const urlObj = new URL(val)
|
||||
const proto = urlObj.protocol.replace(':', '')
|
||||
return {
|
||||
proto: proto,
|
||||
host: urlObj.hostname,
|
||||
port: getValidPort(urlObj.port, proto)
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback for incomplete or invalid URLs
|
||||
const match = val.match(/^(\w+):\/\/(.*)$/)
|
||||
if (match) {
|
||||
const proto = match[1]
|
||||
const rest = match[2]
|
||||
const portMatch = rest.match(/:(\d+)$/)
|
||||
return {
|
||||
proto,
|
||||
host: portMatch ? rest.slice(0, portMatch.index) : rest,
|
||||
port: portMatch ? parseInt(portMatch[1]) : (props.protos[proto] ?? 11010)
|
||||
}
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: 11010 }
|
||||
}
|
||||
return { proto: 'tcp', host: '', port: 11010 }
|
||||
}
|
||||
|
||||
const internalValue = ref(parseUrl(url.value))
|
||||
const defaultHost = '0.0.0.0'
|
||||
|
||||
const buildUrlValue = (value: { proto: string, host: string, port: number }, forceDefaultHost = false) => {
|
||||
const proto = value.proto || 'tcp'
|
||||
const rawHost = (value.host ?? '').trim()
|
||||
const host = rawHost || (forceDefaultHost ? defaultHost : '')
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
let port = value.port
|
||||
if (isNaN(parseInt(port as any))) {
|
||||
port = props.protos[proto] ?? 11010
|
||||
}
|
||||
|
||||
if (props.protos[proto] === 0) {
|
||||
return `${proto}://${host}`
|
||||
}
|
||||
return `${proto}://${host}:${port}`
|
||||
}
|
||||
|
||||
const syncUrlFromInternal = (forceDefaultHost = false) => {
|
||||
const nextUrl = buildUrlValue(internalValue.value, forceDefaultHost)
|
||||
if (!nextUrl || nextUrl === url.value) {
|
||||
return
|
||||
}
|
||||
url.value = nextUrl
|
||||
}
|
||||
|
||||
const onHostBlur = () => {
|
||||
hostFocused.value = false
|
||||
syncUrlFromInternal(true)
|
||||
}
|
||||
|
||||
const onHostFocus = () => {
|
||||
hostFocused.value = true
|
||||
}
|
||||
|
||||
const onDialogConfirm = () => {
|
||||
syncUrlFromInternal(true)
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
const isNoPortProto = computed(() => {
|
||||
return props.protos[internalValue.value.proto] === 0
|
||||
@@ -124,22 +73,28 @@ const isNoPortProto = computed(() => {
|
||||
|
||||
// Sync from external
|
||||
watch(() => url.value, (newVal) => {
|
||||
if (hostFocused.value) {
|
||||
return
|
||||
}
|
||||
const parsed = parseUrl(newVal)
|
||||
const internalHost = internalValue.value.host ?? ''
|
||||
const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost)
|
||||
if (parsed.proto !== internalValue.value.proto ||
|
||||
!sameHost ||
|
||||
parsed.host !== internalValue.value.host ||
|
||||
parsed.port !== internalValue.value.port) {
|
||||
internalValue.value = parsed
|
||||
}
|
||||
})
|
||||
|
||||
// Sync to external
|
||||
watch(internalValue, () => {
|
||||
syncUrlFromInternal(false)
|
||||
watch(internalValue, (newVal) => {
|
||||
const proto = newVal.proto || 'tcp'
|
||||
const host = newVal.host || '0.0.0.0'
|
||||
let port = newVal.port
|
||||
if (isNaN(parseInt(port as any))) {
|
||||
port = props.protos[proto] ?? 11010
|
||||
}
|
||||
|
||||
if (props.protos[proto] === 0) {
|
||||
url.value = `${proto}://${host}`
|
||||
} else {
|
||||
url.value = `${proto}://${host}:${port}`
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
const protoOptions = computed(() => Object.keys(props.protos))
|
||||
@@ -173,8 +128,7 @@ const onProtoChange = (newProto: string) => {
|
||||
<AutoComplete :model-value="internalValue.proto" :suggestions="filteredProtos" dropdown
|
||||
class="max-w-32 proto-autocomplete-in-group" @complete="searchProtos"
|
||||
@update:model-value="onProtoChange" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="grow" />
|
||||
<template v-if="!isNoPortProto">
|
||||
<InputGroupAddon>
|
||||
<span style="font-weight: bold">:</span>
|
||||
@@ -202,8 +156,7 @@ const onProtoChange = (newProto: string) => {
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label>{{ t('web.common.address') || 'Address' }}</label>
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full"
|
||||
@focus="onHostFocus" @blur="onHostBlur" />
|
||||
<InputText v-model="internalValue.host" :placeholder="placeholder || '0.0.0.0'" class="w-full" />
|
||||
</div>
|
||||
<div v-if="!isNoPortProto" class="flex flex-col gap-2">
|
||||
<label>{{ t('port') }}</label>
|
||||
@@ -211,7 +164,7 @@ const onProtoChange = (newProto: string) => {
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.confirm') || 'Done'" icon="pi pi-check" @click="onDialogConfirm"
|
||||
<Button :label="t('web.common.confirm') || 'Done'" icon="pi pi-check" @click="editing = false"
|
||||
autofocus />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
@@ -10,7 +10,7 @@ initial_nodes_help: |
|
||||
• 留空 = 节点独立启动,等别人来连,或你后续手动连。
|
||||
• 无论直接还是间接连通(通过其他节点搭桥),都能组网互通。
|
||||
初始节点可以用自己的,也可以用别人分享的。
|
||||
initial_node_placeholder: 例如:node.example.com
|
||||
initial_node_placeholder: 例如:tcp://node.example.com:11010
|
||||
virtual_ipv4: 虚拟IPv4地址
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: 网络名称
|
||||
@@ -117,7 +117,7 @@ disable_quic_input: 禁用 QUIC 输入
|
||||
disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的节点仍然使用 TCP 连接到本节点。
|
||||
|
||||
disable_p2p: 禁用 P2P
|
||||
disable_p2p_help: 禁用普通自动 P2P。开启 need-p2p 的节点仍可与当前节点建立 P2P。
|
||||
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
||||
|
||||
p2p_only: 仅 P2P
|
||||
p2p_only_help: 仅与已经建立P2P连接的对等节点通信,不通过其他节点中转。
|
||||
@@ -196,12 +196,6 @@ mtu_help: |
|
||||
TUN设备的MTU,默认为非加密时为1380,加密时为1360。范围:400-1380
|
||||
mtu_placeholder: 留空为默认值1380
|
||||
|
||||
instance_recv_bps_limit: 实例接收限速
|
||||
instance_recv_bps_limit_help: |
|
||||
限制当前实例整体入站流量的总接收速率,单位为字节每秒。
|
||||
留空表示不限速。
|
||||
instance_recv_bps_limit_placeholder: 留空表示不限速
|
||||
|
||||
mapped_listeners: 监听映射
|
||||
mapped_listeners_help: |
|
||||
手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。
|
||||
@@ -286,9 +280,6 @@ web:
|
||||
logout: 退出登录
|
||||
language: 语言
|
||||
change_password: 修改密码
|
||||
change_password_now: 立即修改密码
|
||||
default_password_warning: 当前账号仍在使用系统默认密码。为保障安全,请部署完成后立即修改密码。
|
||||
password_changed_relogin: 密码已修改,请重新登录。
|
||||
|
||||
device:
|
||||
list: 设备列表
|
||||
@@ -363,11 +354,6 @@ web:
|
||||
success: 成功
|
||||
warning: 警告
|
||||
info: 提示
|
||||
password_empty: 密码不能为空
|
||||
password_min_length: 密码至少需要 8 位
|
||||
password_too_weak: 密码强度不足
|
||||
password_mismatch: 两次输入的密码不一致
|
||||
password_strength_hint: 密码至少 8 位,且需包含大小写字母、数字、特殊字符中的至少 2 类
|
||||
enable: 开启
|
||||
disable: 关闭
|
||||
address: 地址
|
||||
|
||||
@@ -10,7 +10,7 @@ initial_nodes_help: |
|
||||
• Leaving it empty = the node starts alone until others connect to it, or you connect it later yourself.
|
||||
• Direct or indirect connectivity, including through relay nodes, can form one network.
|
||||
Initial nodes can be your own nodes or ones shared by others.
|
||||
initial_node_placeholder: "Example: node.example.com"
|
||||
initial_node_placeholder: "Example: tcp://node.example.com:11010"
|
||||
virtual_ipv4: Virtual IPv4
|
||||
virtual_ipv4_dhcp: DHCP
|
||||
network_name: Network Name
|
||||
@@ -116,7 +116,7 @@ disable_quic_input: Disable QUIC Input
|
||||
disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC proxy enabled continue to connect using TCP.
|
||||
|
||||
disable_p2p: Disable P2P
|
||||
disable_p2p_help: Disable ordinary automatic P2P. Nodes with need-p2p enabled can still establish P2P with this node.
|
||||
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
|
||||
|
||||
p2p_only: P2P Only
|
||||
p2p_only_help: Only communicate with peers that have already established P2P connections, do not relay through other nodes.
|
||||
@@ -196,12 +196,6 @@ mtu_help: |
|
||||
MTU of the TUN device, default is 1380 for non-encryption, 1360 for encryption. Range:400-1380
|
||||
mtu_placeholder: Leave blank as default value 1380
|
||||
|
||||
instance_recv_bps_limit: Instance Receive Limit
|
||||
instance_recv_bps_limit_help: |
|
||||
Limit the total receive bandwidth for the whole instance. Unit: bytes per second.
|
||||
Leave blank for no limit.
|
||||
instance_recv_bps_limit_placeholder: Leave blank for no limit
|
||||
|
||||
mapped_listeners: Map Listeners
|
||||
mapped_listeners_help: |
|
||||
Manually specify the public address of the listener, other nodes can use this address to connect to this node.
|
||||
@@ -286,9 +280,6 @@ web:
|
||||
logout: Logout
|
||||
language: Language
|
||||
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:
|
||||
list: Device List
|
||||
@@ -363,11 +354,6 @@ web:
|
||||
success: Success
|
||||
warning: Warning
|
||||
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
|
||||
disable: Disable
|
||||
address: Address
|
||||
|
||||
@@ -78,7 +78,6 @@ export interface NetworkConfig {
|
||||
socks5_port: number
|
||||
|
||||
mtu: number | null
|
||||
instance_recv_bps_limit: number | null
|
||||
mapped_listeners: string[]
|
||||
|
||||
enable_magic_dns?: boolean
|
||||
@@ -147,7 +146,6 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
enable_socks5: false,
|
||||
socks5_port: 1080,
|
||||
mtu: null,
|
||||
instance_recv_bps_limit: null,
|
||||
mapped_listeners: [],
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
|
||||
@@ -1,80 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
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 { clearMustChangePasswordFlag } from '../modules/auth-status';
|
||||
import { validatePasswordStrength } from '../modules/password-policy';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
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 () => {
|
||||
if (!passwordValidation.value.valid) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: t('web.common.warning'),
|
||||
detail: t(passwordValidation.value.reasonKey!),
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!passwordMatches.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: t('web.common.warning'),
|
||||
detail: t('web.common.password_mismatch'),
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.value.change_password(password.value);
|
||||
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,
|
||||
});
|
||||
}
|
||||
await api.value.change_password(password.value);
|
||||
dialogRef.value.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,28 +19,15 @@ const changePassword = async () => {
|
||||
<div class="flex items-center justify-center">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">{{ t('web.main.change_password') }}
|
||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<Password v-model="password" :placeholder="t('web.settings.new_password')" :feedback="false"
|
||||
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" />
|
||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
||||
<Button @click="changePassword" label="Ok" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -7,8 +7,6 @@ import { I18nUtils } from 'easytier-frontend-lib';
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||
import { setMustChangePasswordFlag } from '../modules/auth-status';
|
||||
import { validatePasswordStrength } from '../modules/password-policy';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -24,26 +22,8 @@ const username = ref('');
|
||||
const password = ref('');
|
||||
const registerUsername = ref('');
|
||||
const registerPassword = ref('');
|
||||
const registerConfirmPassword = ref('');
|
||||
const captcha = ref('');
|
||||
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 () => {
|
||||
@@ -53,7 +33,6 @@ const onSubmit = async () => {
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
setMustChangePasswordFlag(Boolean(ret.mustChangePassword));
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
params: { apiHost: btoa(apiHost.value) },
|
||||
@@ -64,26 +43,6 @@ const onSubmit = 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);
|
||||
const credential: Credential = { username: registerUsername.value, password: registerPassword.value };
|
||||
const registerReq: RegisterData = { credentials: credential, captcha: captcha.value };
|
||||
@@ -197,23 +156,6 @@ onBeforeUnmount(() => {
|
||||
}}</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
: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 class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
|
||||
@@ -221,8 +163,7 @@ onBeforeUnmount(() => {
|
||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :label="t('web.login.register')" type="submit" class="w-full"
|
||||
:disabled="!canRegister" />
|
||||
<Button :label="t('web.login.register')" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :label="t('web.login.back_to_login')" type="button" class="w-full"
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||
import { Button, Message, TieredMenu } from 'primevue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
import {
|
||||
clearMustChangePasswordFlag,
|
||||
getMustChangePasswordFlag,
|
||||
setMustChangePasswordFlag,
|
||||
} from '../modules/auth-status';
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
@@ -20,7 +15,6 @@ const router = useRouter();
|
||||
const api = computed<ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||
clearMustChangePasswordFlag();
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -29,42 +23,25 @@ const api = computed<ApiClient | undefined>(() => {
|
||||
});
|
||||
|
||||
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 userMenuItems = ref([
|
||||
{
|
||||
label: t('web.main.change_password'),
|
||||
icon: 'pi pi-key',
|
||||
command: openChangePasswordDialog,
|
||||
command: () => {
|
||||
console.log('File');
|
||||
let ret = dialog.open(ChangePassword, {
|
||||
props: {
|
||||
modal: true,
|
||||
},
|
||||
data: {
|
||||
api: api.value,
|
||||
}
|
||||
});
|
||||
|
||||
console.log("return", ret)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('web.main.logout'),
|
||||
@@ -75,7 +52,6 @@ const userMenuItems = ref([
|
||||
} catch (e) {
|
||||
console.error("logout failed", e);
|
||||
}
|
||||
clearMustChangePasswordFlag();
|
||||
router.push({ name: 'login' });
|
||||
},
|
||||
},
|
||||
@@ -116,7 +92,6 @@ onMounted(async () => {
|
||||
// 等待 DOM 渲染完成后添加事件监听器
|
||||
await nextTick();
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
await loadAuthStatus();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -196,13 +171,6 @@ onUnmounted(() => {
|
||||
<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="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 }">
|
||||
<component :is="Component" :api="api" />
|
||||
</RouterView>
|
||||
|
||||
@@ -2,8 +2,6 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
|
||||
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
const hashAuthPassword = (password: string) => Md5.hashStr(password);
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
@@ -16,16 +14,6 @@ export interface OidcConfigResponse {
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
mustChangePassword?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthStatusResponse {
|
||||
must_change_password: boolean;
|
||||
}
|
||||
|
||||
export interface CheckLoginStatusResponse {
|
||||
loggedIn: boolean;
|
||||
mustChangePassword: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
@@ -94,6 +82,7 @@ export class ApiClient {
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
@@ -119,8 +108,9 @@ export class ApiClient {
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = hashAuthPassword(data.credentials.password);
|
||||
await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -133,13 +123,10 @@ export class ApiClient {
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = hashAuthPassword(data.password);
|
||||
const response = await this.client.post<any, AuthStatusResponse>('/auth/login', data);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login success',
|
||||
mustChangePassword: response.must_change_password,
|
||||
};
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
@@ -160,26 +147,16 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: hashAuthPassword(new_password) });
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status(): Promise<CheckLoginStatusResponse> {
|
||||
public async check_login_status() {
|
||||
try {
|
||||
const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status');
|
||||
return {
|
||||
loggedIn: true,
|
||||
mustChangePassword: response.must_change_password,
|
||||
};
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response?.status === 401) {
|
||||
return {
|
||||
loggedIn: false,
|
||||
mustChangePassword: false,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
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 };
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{collections::HashSet, fmt::Debug, str::FromStr as _, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use easytier::{
|
||||
@@ -42,7 +37,6 @@ pub struct SessionData {
|
||||
|
||||
storage_token: Option<StorageToken>,
|
||||
binding_version: Option<u64>,
|
||||
applied_config_revision: Option<String>,
|
||||
notifier: broadcast::Sender<HeartbeatRequest>,
|
||||
req: Option<HeartbeatRequest>,
|
||||
location: Option<Location>,
|
||||
@@ -65,7 +59,6 @@ impl SessionData {
|
||||
client_url,
|
||||
storage_token: None,
|
||||
binding_version: None,
|
||||
applied_config_revision: None,
|
||||
notifier: tx,
|
||||
req: None,
|
||||
location,
|
||||
@@ -124,16 +117,37 @@ struct SessionRpcService {
|
||||
}
|
||||
|
||||
impl SessionRpcService {
|
||||
fn normalize_network_config(
|
||||
mut network_config: serde_json::Value,
|
||||
inst_id: uuid::Uuid,
|
||||
) -> anyhow::Result<NetworkConfig> {
|
||||
async fn persist_webhook_network_config(
|
||||
storage: &Storage,
|
||||
user_id: i32,
|
||||
machine_id: uuid::Uuid,
|
||||
network_config: serde_json::Value,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut network_config = network_config;
|
||||
let network_name = network_config
|
||||
.get("network_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|v| !v.is_empty())
|
||||
.ok_or_else(|| anyhow::anyhow!("webhook response missing network_name"))?
|
||||
.to_string();
|
||||
let existing_configs = storage
|
||||
.db()
|
||||
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
||||
let inst_id = existing_configs
|
||||
.iter()
|
||||
.find_map(|cfg| {
|
||||
let value = serde_json::from_str::<serde_json::Value>(&cfg.network_config).ok()?;
|
||||
let cfg_network_name = value.get("network_name")?.as_str()?;
|
||||
if cfg_network_name == network_name {
|
||||
uuid::Uuid::parse_str(&cfg.network_instance_id).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(uuid::Uuid::new_v4);
|
||||
|
||||
let config_obj = network_config
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("webhook network_config must be a JSON object"))?;
|
||||
@@ -143,66 +157,14 @@ impl SessionRpcService {
|
||||
);
|
||||
config_obj
|
||||
.entry("instance_name".to_string())
|
||||
.or_insert_with(|| serde_json::Value::String(network_name));
|
||||
.or_insert_with(|| serde_json::Value::String(network_name.clone()));
|
||||
|
||||
Ok(serde_json::from_value::<NetworkConfig>(network_config)?)
|
||||
}
|
||||
|
||||
async fn reconcile_managed_network_configs(
|
||||
storage: &Storage,
|
||||
user_id: i32,
|
||||
machine_id: uuid::Uuid,
|
||||
desired_configs: Vec<crate::webhook::ManagedNetworkConfig>,
|
||||
) -> anyhow::Result<()> {
|
||||
let existing_configs = storage
|
||||
let config = serde_json::from_value::<NetworkConfig>(network_config)?;
|
||||
storage
|
||||
.db()
|
||||
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
||||
.insert_or_update_user_network_config((user_id, machine_id), inst_id, config)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to list existing network configs: {:?}", e))?;
|
||||
let existing_ids = existing_configs
|
||||
.iter()
|
||||
.filter_map(|cfg| uuid::Uuid::parse_str(&cfg.network_instance_id).ok())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut desired_ids = HashSet::with_capacity(desired_configs.len());
|
||||
let mut normalized = HashMap::with_capacity(desired_configs.len());
|
||||
for desired in desired_configs {
|
||||
let inst_id = uuid::Uuid::parse_str(&desired.instance_id).with_context(|| {
|
||||
format!(
|
||||
"invalid desired managed instance id: {}",
|
||||
desired.instance_id
|
||||
)
|
||||
})?;
|
||||
let config = Self::normalize_network_config(desired.network_config, inst_id)?;
|
||||
desired_ids.insert(inst_id);
|
||||
normalized.insert(inst_id, config);
|
||||
}
|
||||
|
||||
for (inst_id, config) in normalized {
|
||||
storage
|
||||
.db()
|
||||
.insert_or_update_user_network_config((user_id, machine_id), inst_id, config)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to persist managed network config {}: {:?}",
|
||||
inst_id,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let stale_ids = existing_ids
|
||||
.difference(&desired_ids)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
if !stale_ids.is_empty() {
|
||||
storage
|
||||
.db()
|
||||
.delete_network_configs((user_id, machine_id), &stale_ids)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("failed to delete stale network configs: {:?}", e))?;
|
||||
}
|
||||
.map_err(|e| anyhow::anyhow!("failed to persist webhook network config: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -223,13 +185,10 @@ impl SessionRpcService {
|
||||
req.machine_id
|
||||
))?;
|
||||
|
||||
let (
|
||||
user_id,
|
||||
webhook_managed_network_configs,
|
||||
webhook_config_revision,
|
||||
webhook_validated,
|
||||
binding_version,
|
||||
) = if data.webhook_config.is_enabled() {
|
||||
let (user_id, webhook_network_config, webhook_validated, binding_version) = if data
|
||||
.webhook_config
|
||||
.is_enabled()
|
||||
{
|
||||
let webhook_req = crate::webhook::ValidateTokenRequest {
|
||||
token: req.user_token.clone(),
|
||||
machine_id: machine_id.to_string(),
|
||||
@@ -264,8 +223,7 @@ impl SessionRpcService {
|
||||
};
|
||||
(
|
||||
user_id,
|
||||
resp.managed_network_configs,
|
||||
resp.config_revision,
|
||||
resp.network_config,
|
||||
true,
|
||||
Some(resp.binding_version),
|
||||
)
|
||||
@@ -299,21 +257,21 @@ impl SessionRpcService {
|
||||
);
|
||||
}
|
||||
};
|
||||
(user_id, Vec::new(), String::new(), false, None)
|
||||
(user_id, None, false, None)
|
||||
};
|
||||
|
||||
if webhook_validated
|
||||
&& data.applied_config_revision.as_deref() != Some(webhook_config_revision.as_str())
|
||||
{
|
||||
Self::reconcile_managed_network_configs(
|
||||
&storage,
|
||||
user_id,
|
||||
machine_id,
|
||||
webhook_managed_network_configs,
|
||||
if webhook_validated {
|
||||
if let Some(network_config) = webhook_network_config {
|
||||
Self::persist_webhook_network_config(&storage, user_id, machine_id, network_config)
|
||||
.await
|
||||
.map_err(rpc_types::error::Error::from)?;
|
||||
}
|
||||
} else if webhook_network_config.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unexpected webhook network_config for non-webhook token {:?}",
|
||||
req.user_token
|
||||
)
|
||||
.await
|
||||
.map_err(rpc_types::error::Error::from)?;
|
||||
data.applied_config_revision = Some(webhook_config_revision);
|
||||
.into());
|
||||
}
|
||||
|
||||
if data.req.replace(req.clone()).is_none() {
|
||||
@@ -453,7 +411,6 @@ impl Session {
|
||||
rpc_client: SessionRpcClient,
|
||||
) {
|
||||
let mut cleaned_web_managed_instances = false;
|
||||
let mut last_desired_inst_ids: Option<HashSet<String>> = None;
|
||||
loop {
|
||||
heartbeat_waiter = heartbeat_waiter.resubscribe();
|
||||
let req = heartbeat_waiter.recv().await;
|
||||
@@ -510,15 +467,8 @@ impl Session {
|
||||
};
|
||||
|
||||
let mut has_failed = false;
|
||||
let should_be_alive_inst_ids = local_configs
|
||||
.iter()
|
||||
.map(|cfg| cfg.network_instance_id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
let desired_changed = last_desired_inst_ids
|
||||
.as_ref()
|
||||
.is_none_or(|last| last != &should_be_alive_inst_ids);
|
||||
|
||||
if !cleaned_web_managed_instances || desired_changed {
|
||||
if !cleaned_web_managed_instances {
|
||||
let all_local_configs = match storage
|
||||
.db
|
||||
.list_network_configs((user_id, machine_id.into()), ListNetworkProps::All)
|
||||
@@ -536,6 +486,11 @@ impl Session {
|
||||
.map(|cfg| cfg.network_instance_id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let should_be_alive_inst_ids = local_configs
|
||||
.iter()
|
||||
.map(|cfg| cfg.network_instance_id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let should_delete_ids = running_inst_ids
|
||||
.iter()
|
||||
.chain(all_inst_ids.iter())
|
||||
@@ -564,7 +519,6 @@ impl Session {
|
||||
|
||||
if !has_failed {
|
||||
cleaned_web_managed_instances = true;
|
||||
last_desired_inst_ids = Some(should_be_alive_inst_ids.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +549,8 @@ impl Session {
|
||||
}
|
||||
|
||||
if !has_failed {
|
||||
last_desired_inst_ids = Some(should_be_alive_inst_ids);
|
||||
tracing::info!(?req, "All network instances are running");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,103 +585,3 @@ impl Session {
|
||||
self.data.read().await.req()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use easytier::rpc_service::remote_client::{ListNetworkProps, Storage as _};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{super::storage::Storage, *};
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconcile_managed_network_configs_upserts_and_deletes_exact_set() {
|
||||
let storage = Storage::new(crate::db::Db::memory_db().await);
|
||||
let user_id = storage
|
||||
.db()
|
||||
.auto_create_user("webhook-user")
|
||||
.await
|
||||
.unwrap()
|
||||
.id;
|
||||
let machine_id = uuid::Uuid::new_v4();
|
||||
let keep_id = uuid::Uuid::new_v4();
|
||||
let stale_id = uuid::Uuid::new_v4();
|
||||
let new_id = uuid::Uuid::new_v4();
|
||||
|
||||
storage
|
||||
.db()
|
||||
.insert_or_update_user_network_config(
|
||||
(user_id, machine_id),
|
||||
keep_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("old-name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
storage
|
||||
.db()
|
||||
.insert_or_update_user_network_config(
|
||||
(user_id, machine_id),
|
||||
stale_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("stale".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SessionRpcService::reconcile_managed_network_configs(
|
||||
&storage,
|
||||
user_id,
|
||||
machine_id,
|
||||
vec![
|
||||
crate::webhook::ManagedNetworkConfig {
|
||||
instance_id: keep_id.to_string(),
|
||||
network_config: json!({
|
||||
"instance_id": keep_id.to_string(),
|
||||
"network_name": "updated-name"
|
||||
}),
|
||||
},
|
||||
crate::webhook::ManagedNetworkConfig {
|
||||
instance_id: new_id.to_string(),
|
||||
network_config: json!({
|
||||
"instance_id": new_id.to_string(),
|
||||
"network_name": "new-name"
|
||||
}),
|
||||
},
|
||||
],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let configs = storage
|
||||
.db()
|
||||
.list_network_configs((user_id, machine_id), ListNetworkProps::All)
|
||||
.await
|
||||
.unwrap();
|
||||
let config_ids = configs
|
||||
.iter()
|
||||
.map(|cfg| cfg.network_instance_id.clone())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(configs.len(), 2);
|
||||
assert!(config_ids.contains(&keep_id.to_string()));
|
||||
assert!(config_ids.contains(&new_id.to_string()));
|
||||
assert!(!config_ids.contains(&stale_id.to_string()));
|
||||
|
||||
let updated_keep = storage
|
||||
.db()
|
||||
.get_network_config((user_id, machine_id), &keep_id.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let updated_keep_config: NetworkConfig =
|
||||
serde_json::from_str(&updated_keep.network_config).unwrap();
|
||||
assert_eq!(
|
||||
updated_keep_config.network_name.as_deref(),
|
||||
Some("updated-name")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct Model {
|
||||
#[sea_orm(unique)]
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub must_change_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
+10
-94
@@ -96,7 +96,6 @@ impl Db {
|
||||
let user_active = users::ActiveModel {
|
||||
username: Set(username.to_string()),
|
||||
password: Set(password_hash),
|
||||
must_change_password: Set(false),
|
||||
..Default::default()
|
||||
};
|
||||
let insert_result = users::Entity::insert(user_active).exec(&txn).await?;
|
||||
@@ -155,17 +154,13 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
use entity::user_running_network_configs as urnc;
|
||||
|
||||
let on_conflict = OnConflict::columns([
|
||||
urnc::Column::UserId,
|
||||
urnc::Column::DeviceId,
|
||||
urnc::Column::NetworkInstanceId,
|
||||
])
|
||||
.update_columns([
|
||||
urnc::Column::NetworkConfig,
|
||||
urnc::Column::Disabled,
|
||||
urnc::Column::UpdateTime,
|
||||
])
|
||||
.to_owned();
|
||||
let on_conflict = OnConflict::column(urnc::Column::NetworkInstanceId)
|
||||
.update_columns([
|
||||
urnc::Column::NetworkConfig,
|
||||
urnc::Column::Disabled,
|
||||
urnc::Column::UpdateTime,
|
||||
])
|
||||
.to_owned();
|
||||
let insert_m = urnc::ActiveModel {
|
||||
user_id: sea_orm::Set(user_id),
|
||||
device_id: sea_orm::Set(device_id.to_string()),
|
||||
@@ -189,14 +184,13 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
async fn delete_network_configs(
|
||||
&self,
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
(user_id, _): (UserIdInDb, Uuid),
|
||||
network_inst_ids: &[Uuid],
|
||||
) -> Result<(), DbErr> {
|
||||
use entity::user_running_network_configs as urnc;
|
||||
|
||||
urnc::Entity::delete_many()
|
||||
.filter(urnc::Column::UserId.eq(user_id))
|
||||
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||
.filter(
|
||||
urnc::Column::NetworkInstanceId
|
||||
.is_in(network_inst_ids.iter().map(|id| id.to_string())),
|
||||
@@ -209,7 +203,7 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
async fn update_network_config_state(
|
||||
&self,
|
||||
(user_id, device_id): (UserIdInDb, Uuid),
|
||||
(user_id, _): (UserIdInDb, Uuid),
|
||||
network_inst_id: Uuid,
|
||||
disabled: bool,
|
||||
) -> Result<(), DbErr> {
|
||||
@@ -217,7 +211,6 @@ impl Storage<(UserIdInDb, Uuid), user_running_network_configs::Model, DbErr> for
|
||||
|
||||
urnc::Entity::update_many()
|
||||
.filter(urnc::Column::UserId.eq(user_id))
|
||||
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
|
||||
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
|
||||
.col_expr(urnc::Column::Disabled, Expr::value(disabled))
|
||||
.col_expr(
|
||||
@@ -281,28 +274,7 @@ mod tests {
|
||||
use easytier::{proto::api::manage::NetworkConfig, rpc_service::remote_client::Storage};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
|
||||
|
||||
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);
|
||||
}
|
||||
use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_management() {
|
||||
@@ -369,60 +341,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(result3.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_network_config_same_instance_id_is_scoped_by_device() {
|
||||
let db = Db::memory_db().await;
|
||||
let user_id = db.auto_create_user("user-1").await.unwrap().id;
|
||||
let device1 = uuid::Uuid::new_v4();
|
||||
let device2 = uuid::Uuid::new_v4();
|
||||
let inst_id = uuid::Uuid::new_v4();
|
||||
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device1),
|
||||
inst_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("cfg-1".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert_or_update_user_network_config(
|
||||
(user_id, device2),
|
||||
inst_id,
|
||||
NetworkConfig {
|
||||
network_name: Some("cfg-2".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let first = db
|
||||
.get_network_config((user_id, device1), &inst_id.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let second = db
|
||||
.get_network_config((user_id, device2), &inst_id.to_string())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(first.user_id, user_id);
|
||||
assert_eq!(first.device_id, device1.to_string());
|
||||
assert_eq!(second.user_id, user_id);
|
||||
assert_eq!(second.device_id, device2.to_string());
|
||||
|
||||
let device1_configs = db
|
||||
.list_network_configs((user_id, device1), ListNetworkProps::All)
|
||||
.await
|
||||
.unwrap();
|
||||
let device2_configs = db
|
||||
.list_network_configs((user_id, device2), ListNetworkProps::All)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(device1_configs.len(), 1);
|
||||
assert_eq!(device2_configs.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-25
@@ -7,7 +7,7 @@ use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use easytier::tunnel::websocket::WsTunnelListener;
|
||||
use easytier::tunnel::websocket::WSTunnelListener;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader},
|
||||
@@ -20,8 +20,6 @@ use easytier::{
|
||||
utils::setup_panic_handler,
|
||||
};
|
||||
|
||||
use easytier::tunnel::IpScheme;
|
||||
use easytier::utils::BoxExt;
|
||||
use mimalloc::MiMalloc;
|
||||
|
||||
mod client_manager;
|
||||
@@ -194,12 +192,14 @@ impl LoggingConfigLoader for &Cli {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_listener_by_url(scheme: IpScheme, l: &url::Url) -> Option<Box<dyn TunnelListener>> {
|
||||
Some(match scheme {
|
||||
IpScheme::Tcp => TcpTunnelListener::new(l.clone()).boxed(),
|
||||
IpScheme::Udp => UdpTunnelListener::new(l.clone()).boxed(),
|
||||
IpScheme::Ws => WsTunnelListener::new(l.clone()).boxed(),
|
||||
_ => return None,
|
||||
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
|
||||
Ok(match l.scheme() {
|
||||
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
|
||||
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
|
||||
"ws" => Box::new(WSTunnelListener::new(l.clone())),
|
||||
_ => {
|
||||
return Err(Error::InvalidUrl(l.to_string()));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,23 +213,15 @@ async fn get_dual_stack_listener(
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let scheme = protocol
|
||||
.parse()
|
||||
.map_err(|_| Error::InvalidUrl(protocol.to_string()))?;
|
||||
let v6_listener =
|
||||
if local_ipv6().await.is_ok() && matches!(scheme, IpScheme::Tcp | IpScheme::Udp) {
|
||||
get_listener_by_url(
|
||||
scheme,
|
||||
&format!("{protocol}://[::]:{port}").parse().unwrap(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let is_protocol_support_dual_stack =
|
||||
protocol.trim().to_lowercase() == "tcp" || protocol.trim().to_lowercase() == "udp";
|
||||
let v6_listener = if is_protocol_support_dual_stack && local_ipv6().await.is_ok() {
|
||||
get_listener_by_url(&format!("{}://[::0]:{}", protocol, port).parse().unwrap()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let v4_listener = if local_ipv4().await.is_ok() {
|
||||
get_listener_by_url(
|
||||
scheme,
|
||||
&format!("{protocol}://0.0.0.0:{port}").parse().unwrap(),
|
||||
)
|
||||
get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260403_000002_scope_network_config_unique"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_new (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_new RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_running_network_configs_scope_inst
|
||||
ON user_running_network_configs(user_id, device_id, network_instance_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
db.execute_unprepared(
|
||||
r#"
|
||||
CREATE TABLE user_running_network_configs_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
network_instance_id TEXT NOT NULL UNIQUE,
|
||||
network_config TEXT NOT NULL,
|
||||
disabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL,
|
||||
CONSTRAINT fk_user_running_network_configs_user_id_to_users_id
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO user_running_network_configs_old (
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
device_id,
|
||||
network_instance_id,
|
||||
network_config,
|
||||
disabled,
|
||||
create_time,
|
||||
update_time
|
||||
FROM user_running_network_configs;
|
||||
|
||||
DROP TABLE user_running_network_configs;
|
||||
ALTER TABLE user_running_network_configs_old RENAME TO user_running_network_configs;
|
||||
|
||||
CREATE INDEX idx_user_running_network_configs_user_id
|
||||
ON user_running_network_configs(user_id);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
const DEFAULT_USER_PASSWORD_HASH: &str =
|
||||
"$argon2i$v=19$m=16,t=2,p=1$aGVyRDBrcnRycnlaMDhkbw$449SEcv/qXf+0fnI9+fYVQ";
|
||||
const DEFAULT_ADMIN_PASSWORD_HASH: &str =
|
||||
"$argon2i$v=19$m=16,t=2,p=1$bW5idXl0cmY$61n+JxL4r3dwLPAEDlDdtg";
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Users {
|
||||
Table,
|
||||
Username,
|
||||
Password,
|
||||
MustChangePassword,
|
||||
}
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260405_000003_add_must_change_password"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Users::Table)
|
||||
.add_column(
|
||||
ColumnDef::new(Users::MustChangePassword)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.exec_stmt(
|
||||
Query::update()
|
||||
.table(Users::Table)
|
||||
.value(Users::MustChangePassword, true)
|
||||
.cond_where(any![
|
||||
Expr::col(Users::Username)
|
||||
.eq("admin")
|
||||
.and(Expr::col(Users::Password).eq(DEFAULT_ADMIN_PASSWORD_HASH)),
|
||||
Expr::col(Users::Username)
|
||||
.eq("user")
|
||||
.and(Expr::col(Users::Password).eq(DEFAULT_USER_PASSWORD_HASH)),
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Users::Table)
|
||||
.drop_column(Users::MustChangePassword)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _, SqlxSqliteConnector};
|
||||
use sea_orm_migration::prelude::SchemaManager;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
|
||||
use super::{Migration, MigrationTrait, DEFAULT_USER_PASSWORD_HASH};
|
||||
use crate::db::entity::users;
|
||||
|
||||
async fn find_user(db: &sea_orm::DatabaseConnection, username: &str) -> users::Model {
|
||||
users::Entity::find()
|
||||
.filter(users::Column::Username.eq(username))
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migration_only_marks_seeded_accounts_still_using_default_passwords() {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL
|
||||
)",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let changed_admin_password = password_auth::generate_hash("already-changed");
|
||||
|
||||
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?), (?, ?)")
|
||||
.bind("admin")
|
||||
.bind(changed_admin_password)
|
||||
.bind("user")
|
||||
.bind(DEFAULT_USER_PASSWORD_HASH)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let db = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
|
||||
Migration.up(&SchemaManager::new(&db)).await.unwrap();
|
||||
|
||||
assert!(!find_user(&db, "admin").await.must_change_password);
|
||||
assert!(find_user(&db, "user").await.must_change_password);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20241029_000001_init;
|
||||
mod m20260403_000002_scope_network_config_unique;
|
||||
mod m20260405_000003_add_must_change_password;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20241029_000001_init::Migration),
|
||||
Box::new(m20260403_000002_scope_network_config_unique::Migration),
|
||||
Box::new(m20260405_000003_add_must_change_password::Migration),
|
||||
]
|
||||
vec![Box::new(m20241029_000001_init::Migration)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use axum_login::login_required;
|
||||
use serde::Serialize;
|
||||
use axum_messages::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::restful::users::Backend;
|
||||
|
||||
@@ -17,9 +18,9 @@ use super::{
|
||||
AppStateInner,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthStatusResponse {
|
||||
must_change_password: bool,
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct LoginResult {
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppStateInner> {
|
||||
@@ -39,15 +40,12 @@ pub fn router() -> Router<AppStateInner> {
|
||||
}
|
||||
|
||||
mod put {
|
||||
use crate::restful::{
|
||||
other_error,
|
||||
users::{ChangePassword, ChangePasswordError},
|
||||
HttpHandleError,
|
||||
};
|
||||
use axum::Json;
|
||||
use axum_login::AuthUser;
|
||||
use easytier::proto::common::Void;
|
||||
|
||||
use crate::restful::{other_error, users::ChangePassword, HttpHandleError};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub async fn change_password(
|
||||
@@ -60,21 +58,15 @@ mod put {
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to change password: {:?}", e);
|
||||
let (status, message) = match &e {
|
||||
ChangePasswordError::EmptyPassword => {
|
||||
(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()))));
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json::from(other_error(format!("{:?}", e))),
|
||||
));
|
||||
}
|
||||
|
||||
let _ = auth_session.logout().await;
|
||||
|
||||
Ok(Json(Void::default()))
|
||||
Ok(Void::default().into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +86,7 @@ mod post {
|
||||
pub async fn login(
|
||||
mut auth_session: AuthSession,
|
||||
Json(creds): Json<Credentials>,
|
||||
) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
let user = match auth_session.authenticate(creds.clone()).await {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => {
|
||||
@@ -118,9 +110,7 @@ mod post {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Json(AuthStatusResponse {
|
||||
must_change_password: user.db_user.must_change_password,
|
||||
}))
|
||||
Ok(Void::default().into())
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
@@ -199,11 +189,9 @@ mod get {
|
||||
|
||||
pub async fn check_login_status(
|
||||
auth_session: AuthSession,
|
||||
) -> Result<Json<AuthStatusResponse>, HttpHandleError> {
|
||||
if let Some(user) = auth_session.user {
|
||||
Ok(Json(AuthStatusResponse {
|
||||
must_change_password: user.db_user.must_change_password,
|
||||
}))
|
||||
) -> Result<Json<Void>, HttpHandleError> {
|
||||
if auth_session.user.is_some() {
|
||||
Ok(Json(Void::default()))
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
|
||||
@@ -12,8 +12,6 @@ use tokio::task;
|
||||
|
||||
use crate::db::{self, entity};
|
||||
|
||||
const EMPTY_PASSWORD_MD5: &str = "d41d8cd98f00b204e9800998ecf8427e";
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub(crate) db_user: entity::users::Model,
|
||||
@@ -66,18 +64,6 @@ pub struct ChangePassword {
|
||||
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)]
|
||||
pub struct Backend {
|
||||
db: db::Db,
|
||||
@@ -133,14 +119,7 @@ impl Backend {
|
||||
&self,
|
||||
id: <User as AuthUser>::Id,
|
||||
req: &ChangePassword,
|
||||
) -> 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);
|
||||
}
|
||||
|
||||
) -> anyhow::Result<()> {
|
||||
let hashed_password = password_auth::generate_hash(req.new_password.as_str());
|
||||
|
||||
use entity::users;
|
||||
@@ -148,10 +127,9 @@ impl Backend {
|
||||
let mut user = users::Entity::find_by_id(id)
|
||||
.one(self.db.orm_db())
|
||||
.await?
|
||||
.ok_or(ChangePasswordError::UserNotFound)?
|
||||
.ok_or(anyhow::anyhow!("User not found"))?
|
||||
.into_active_model();
|
||||
user.password = Set(hashed_password.clone());
|
||||
user.must_change_password = Set(false);
|
||||
|
||||
entity::users::Entity::update(user)
|
||||
.exec(self.db.orm_db())
|
||||
@@ -264,107 +242,6 @@ 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.
|
||||
//
|
||||
// Note that we've supplied our concrete backend here.
|
||||
|
||||
@@ -65,14 +65,7 @@ pub struct ValidateTokenResponse {
|
||||
pub pre_approved: bool,
|
||||
#[serde(default)]
|
||||
pub binding_version: u64,
|
||||
pub managed_network_configs: Vec<ManagedNetworkConfig>,
|
||||
pub config_revision: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ManagedNetworkConfig {
|
||||
pub instance_id: String,
|
||||
pub network_config: serde_json::Value,
|
||||
pub network_config: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
+5
-9
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.0"
|
||||
version = "2.5.0"
|
||||
edition = "2021"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
@@ -37,7 +37,7 @@ tracing-subscriber = { version = "0.3", features = [
|
||||
"time",
|
||||
] }
|
||||
derivative = "2.2.0"
|
||||
derive_more = { version = "2.1.1", features = ["full"] }
|
||||
derive_more = {version = "2.1.1", features = ["full"]}
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
indoc = "2.0.7"
|
||||
regex = "1.8"
|
||||
@@ -50,8 +50,6 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
cfg-if = "1.0"
|
||||
|
||||
itertools = "0.14.0"
|
||||
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
@@ -81,12 +79,12 @@ quinn = { version = "0.11.8", optional = true, features = ["ring"] }
|
||||
quinn-plaintext = { version = "0.3.0", optional = true }
|
||||
|
||||
rustls = { version = "0.23.0", features = [
|
||||
"ring", "tls12"
|
||||
"ring","tls12"
|
||||
], default-features = false, optional = true }
|
||||
rcgen = { version = "0.12.1", optional = true }
|
||||
|
||||
# for websocket
|
||||
tokio-websockets = { version = "0.13.2", optional = true, features = [
|
||||
tokio-websockets = { version = "0.8", optional = true, features = [
|
||||
"rustls-webpki-roots",
|
||||
"client",
|
||||
"server",
|
||||
@@ -96,7 +94,6 @@ tokio-websockets = { version = "0.13.2", optional = true, features = [
|
||||
http = { version = "1", default-features = false, features = [
|
||||
"std",
|
||||
], optional = true }
|
||||
forwarded-header-value = { version = "0.1.1", optional = true }
|
||||
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
||||
|
||||
# for tap device
|
||||
@@ -252,6 +249,7 @@ shellexpand = "3.1.1"
|
||||
|
||||
# for fake tcp
|
||||
flume = { version = "0.12", optional = true }
|
||||
cfg-if = "1.0"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||
machine-uid = "0.5.3"
|
||||
@@ -314,7 +312,6 @@ jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||
], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cfg_aliases = "0.2.1"
|
||||
tonic-build = "0.12"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
@@ -390,7 +387,6 @@ tun = ["dep:tun"]
|
||||
websocket = [
|
||||
"dep:tokio-websockets",
|
||||
"dep:http",
|
||||
"dep:forwarded-header-value",
|
||||
"dep:tokio-rustls",
|
||||
"dep:rustls",
|
||||
"dep:rcgen",
|
||||
|
||||
+2
-13
@@ -1,9 +1,9 @@
|
||||
use cfg_aliases::cfg_aliases;
|
||||
use prost_wkt_build::{FileDescriptorSet, Message as _};
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::io::Cursor;
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use prost_wkt_build::{FileDescriptorSet, Message as _};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
struct WindowsBuild {}
|
||||
|
||||
@@ -130,17 +130,6 @@ fn check_locale() {
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
cfg_aliases! {
|
||||
mobile: {
|
||||
any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||
#[cfg(target_os = "windows")]
|
||||
if !std::env::var("TARGET")
|
||||
|
||||
@@ -152,8 +152,8 @@ core_clap:
|
||||
如果该参数为空,则禁用转发。默认允许所有网络。
|
||||
例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2)"
|
||||
disable_p2p:
|
||||
en: "disable ordinary automatic p2p; still establish p2p with peers marked as need-p2p, and other peers should not proactively connect to this node"
|
||||
zh-CN: "禁用普通自动P2P;仍会与标记为 need-p2p 的节点建立P2P连接,其他节点不应主动与当前节点建立P2P"
|
||||
en: "disable p2p communication, will only relay packets with peers specified by --peers"
|
||||
zh-CN: "禁用P2P通信,只通过--peers指定的节点转发数据包"
|
||||
p2p_only:
|
||||
en: "only communicate with peers that already establish p2p connection"
|
||||
zh-CN: "仅与已经建立P2P连接的对等节点通信"
|
||||
@@ -212,14 +212,11 @@ core_clap:
|
||||
en: "specify the top-level domain zone for magic DNS. if not provided, defaults to the value from dns_server module (et.net.). only used when accept_dns is true."
|
||||
zh-CN: "指定魔法DNS的顶级域名区域。如果未提供,默认使用dns_server模块中的值(et.net.)。仅在accept_dns为true时使用。"
|
||||
private_mode:
|
||||
en: "if true, foreign networks are only allowed when this node can verify they use the same network secret, or when a foreign credential node is already trusted via admin-issued credential propagation; different or missing secrets are otherwise rejected."
|
||||
zh-CN: "如果为true,则仅允许两类 foreign network 接入:本节点能验证其使用相同 network secret 的节点,或已通过 foreign network 管理节点传播而被信任的 credential 节点;否则 secret 不同或缺失时会被拒绝。"
|
||||
en: "if true, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node."
|
||||
zh-CN: "如果为true,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转"
|
||||
foreign_relay_bps_limit:
|
||||
en: "the maximum bps limit for foreign network relay, default is no limit. unit: BPS (bytes per second)"
|
||||
zh-CN: "作为共享节点时,限制非本地网络的流量转发速率,默认无限制,单位 BPS (字节每秒)"
|
||||
instance_recv_bps_limit:
|
||||
en: "the maximum total receive bps limit for this instance, default is no limit. unit: BPS (bytes per second)"
|
||||
zh-CN: "限制当前网络实例整体入站流量的总接收速率,默认无限制,单位 BPS (字节每秒)"
|
||||
tcp_whitelist:
|
||||
en: "tcp port whitelist. Supports single ports (80) and ranges (8000-9000)"
|
||||
zh-CN: "TCP 端口白名单。支持单个端口(80)和范围(8000-9000)"
|
||||
|
||||
@@ -69,7 +69,6 @@ pub fn gen_default_flags() -> Flags {
|
||||
|
||||
quic_listen_port: u32::MAX,
|
||||
need_p2p: false,
|
||||
instance_recv_bps_limit: u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashMap},
|
||||
hash::Hasher,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
@@ -9,6 +10,21 @@ use std::{
|
||||
use arc_swap::ArcSwap;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use crate::common::config::ProxyNetworkConfig;
|
||||
use crate::common::shrink_dashmap;
|
||||
use crate::common::stats_manager::StatsManager;
|
||||
use crate::common::token_bucket::TokenBucketManager;
|
||||
use crate::peers::acl_filter::AclFilter;
|
||||
use crate::peers::credential_manager::CredentialManager;
|
||||
use crate::proto::acl::GroupIdentity;
|
||||
use crate::proto::api::config::InstanceConfigPatch;
|
||||
use crate::proto::api::instance::PeerConnInfo;
|
||||
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
|
||||
use crate::proto::peer_rpc::PeerGroupInfo;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
use super::{
|
||||
config::{ConfigLoader, Flags},
|
||||
netns::NetNS,
|
||||
@@ -16,24 +32,6 @@ use super::{
|
||||
stun::{StunInfoCollector, StunInfoCollectorTrait},
|
||||
PeerId,
|
||||
};
|
||||
use crate::{
|
||||
common::{
|
||||
config::ProxyNetworkConfig, shrink_dashmap, stats_manager::StatsManager,
|
||||
token_bucket::TokenBucketManager,
|
||||
},
|
||||
peers::{acl_filter::AclFilter, credential_manager::CredentialManager},
|
||||
proto::{
|
||||
acl::GroupIdentity,
|
||||
api::{config::InstanceConfigPatch, instance::PeerConnInfo},
|
||||
common::{PeerFeatureFlag, PortForwardConfigPb},
|
||||
peer_rpc::PeerGroupInfo,
|
||||
},
|
||||
tunnel::matches_protocol,
|
||||
};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use socket2::Protocol;
|
||||
|
||||
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
|
||||
|
||||
@@ -244,7 +242,6 @@ impl GlobalCtx {
|
||||
feature_flags.quic_input = !flags.disable_quic_input;
|
||||
feature_flags.no_relay_quic = flags.disable_relay_quic;
|
||||
feature_flags.need_p2p = flags.need_p2p;
|
||||
feature_flags.disable_p2p = flags.disable_p2p;
|
||||
feature_flags
|
||||
}
|
||||
|
||||
@@ -628,11 +625,15 @@ impl GlobalCtx {
|
||||
}
|
||||
|
||||
fn is_port_in_running_listeners(&self, port: u16, is_udp: bool) -> bool {
|
||||
let check_proto = |listener_proto: &str| {
|
||||
let listener_is_udp = matches!(listener_proto, "udp" | "wg");
|
||||
listener_is_udp == is_udp
|
||||
};
|
||||
self.running_listeners
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|x| x.port() == Some(port) && matches_protocol!(x, Protocol::UDP) == is_udp)
|
||||
.any(|x| x.port() == Some(port) && check_proto(x.scheme()))
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret, skip(self))]
|
||||
@@ -744,13 +745,12 @@ pub mod tests {
|
||||
feature_flags.is_public_server = true;
|
||||
global_ctx.set_feature_flags(feature_flags);
|
||||
|
||||
let mut flags = global_ctx.get_flags().clone();
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.disable_kcp_input = true;
|
||||
flags.disable_relay_kcp = true;
|
||||
flags.disable_quic_input = true;
|
||||
flags.disable_relay_quic = true;
|
||||
flags.need_p2p = true;
|
||||
flags.disable_p2p = true;
|
||||
global_ctx.set_flags(flags);
|
||||
|
||||
let feature_flags = global_ctx.get_feature_flags();
|
||||
@@ -759,7 +759,6 @@ pub mod tests {
|
||||
assert!(!feature_flags.quic_input);
|
||||
assert!(feature_flags.no_relay_quic);
|
||||
assert!(feature_flags.need_p2p);
|
||||
assert!(feature_flags.disable_p2p);
|
||||
assert!(feature_flags.support_conn_list_sync);
|
||||
assert!(feature_flags.avoid_relay_data);
|
||||
assert!(feature_flags.is_public_server);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
use std::{net::IpAddr, ops::Deref, sync::Arc};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use network_interface::{
|
||||
Addr as SystemAddr, NetworkInterface as SystemNetworkInterface, NetworkInterfaceConfig,
|
||||
};
|
||||
use pnet::datalink::NetworkInterface;
|
||||
#[cfg(target_os = "windows")]
|
||||
use pnet::{ipnetwork::IpNetwork, util::MacAddr};
|
||||
use tokio::{
|
||||
sync::{Mutex, RwLock},
|
||||
task::JoinSet,
|
||||
@@ -270,9 +264,6 @@ impl IPCollector {
|
||||
|
||||
pub async fn collect_interfaces(net_ns: NetNS, filter: bool) -> Vec<NetworkInterface> {
|
||||
let _g = net_ns.guard();
|
||||
#[cfg(target_os = "windows")]
|
||||
let ifaces = Self::collect_interfaces_windows();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let ifaces = pnet::datalink::interfaces();
|
||||
let mut ret = vec![];
|
||||
for iface in ifaces {
|
||||
@@ -290,86 +281,6 @@ impl IPCollector {
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn collect_interfaces_windows() -> Vec<NetworkInterface> {
|
||||
match SystemNetworkInterface::show() {
|
||||
Ok(ifaces) => ifaces
|
||||
.into_iter()
|
||||
.map(Self::convert_windows_interface)
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
?e,
|
||||
"failed to enumerate interfaces via network-interface, falling back to pnet"
|
||||
);
|
||||
match std::panic::catch_unwind(pnet::datalink::interfaces) {
|
||||
Ok(ifaces) => ifaces,
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
"failed to enumerate interfaces via both network-interface and pnet"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn convert_windows_interface(iface: SystemNetworkInterface) -> NetworkInterface {
|
||||
let mac = iface.mac_addr.as_deref().and_then(|mac| {
|
||||
mac.parse::<MacAddr>()
|
||||
.map_err(|e| {
|
||||
tracing::debug!(iface = %iface.name, mac, ?e, "failed to parse interface mac")
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let ips = iface
|
||||
.addr
|
||||
.into_iter()
|
||||
.filter_map(Self::convert_windows_interface_addr)
|
||||
.collect();
|
||||
|
||||
NetworkInterface {
|
||||
name: iface.name,
|
||||
description: String::new(),
|
||||
index: iface.index,
|
||||
mac,
|
||||
ips,
|
||||
// pnet does not populate Windows flags either, so keep the existing semantics.
|
||||
flags: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn convert_windows_interface_addr(addr: SystemAddr) -> Option<IpNetwork> {
|
||||
match addr {
|
||||
SystemAddr::V4(addr) => {
|
||||
let netmask = addr
|
||||
.netmask
|
||||
.map(IpAddr::V4)
|
||||
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::new(255, 255, 255, 255)));
|
||||
IpNetwork::with_netmask(IpAddr::V4(addr.ip), netmask)
|
||||
.map_err(|e| {
|
||||
tracing::debug!(ip = %addr.ip, ?addr.netmask, ?e, "failed to convert ipv4")
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
SystemAddr::V6(addr) => {
|
||||
let netmask = addr
|
||||
.netmask
|
||||
.map(IpAddr::V6)
|
||||
.unwrap_or(IpAddr::V6(std::net::Ipv6Addr::from(u128::MAX)));
|
||||
IpNetwork::with_netmask(IpAddr::V6(addr.ip), netmask)
|
||||
.map_err(|e| {
|
||||
tracing::debug!(ip = %addr.ip, ?addr.netmask, ?e, "failed to convert ipv6")
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(net_ns))]
|
||||
async fn do_collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse {
|
||||
let mut ret = GetIpListResponse::default();
|
||||
|
||||
@@ -581,9 +581,9 @@ impl StatsManager {
|
||||
break;
|
||||
};
|
||||
|
||||
counters.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
// Remove entries that haven't been updated for 3 minutes
|
||||
counters.retain(|_, metric_data: &mut Arc<MetricData>| unsafe {
|
||||
metric_data.get_last_updated() > cutoff_time
|
||||
});
|
||||
counters.shrink_to_fit();
|
||||
}
|
||||
@@ -900,33 +900,6 @@ mod tests {
|
||||
assert_eq!(counter2.get(), 25);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleanup_keeps_metrics_with_live_handles() {
|
||||
let stats = StatsManager::new();
|
||||
let counter = stats.get_simple_counter(MetricName::TrafficBytesForwarded);
|
||||
counter.set(1);
|
||||
|
||||
let cutoff_time = Instant::now().checked_add(Duration::from_secs(1)).unwrap();
|
||||
stats
|
||||
.counters
|
||||
.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
});
|
||||
|
||||
assert_eq!(stats.metric_count(), 1);
|
||||
assert_eq!(stats.get_all_metrics().len(), 1);
|
||||
|
||||
drop(counter);
|
||||
stats
|
||||
.counters
|
||||
.retain(|_, metric_data: &mut Arc<MetricData>| {
|
||||
Arc::strong_count(metric_data) > 1
|
||||
|| unsafe { metric_data.get_last_updated() > cutoff_time }
|
||||
});
|
||||
assert_eq!(stats.metric_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stats_rpc_data_structures() {
|
||||
// Test GetStatsRequest
|
||||
|
||||
@@ -31,20 +31,19 @@ use crate::{
|
||||
},
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::{matches_protocol, udp::UdpTunnelConnector, IpVersion},
|
||||
tunnel::{udp::UdpTunnelConnector, IpVersion},
|
||||
use_global_var,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use rand::Rng;
|
||||
use tokio::{net::UdpSocket, task::JoinSet, time::timeout};
|
||||
use url::Host;
|
||||
|
||||
use super::{
|
||||
create_connector_by_url, should_background_p2p_with_peer, should_try_p2p_with_peer,
|
||||
udp_hole_punch,
|
||||
};
|
||||
use crate::tunnel::{matches_scheme, FromUrl, IpScheme, TunnelScheme};
|
||||
use anyhow::Context;
|
||||
use rand::Rng;
|
||||
use socket2::Protocol;
|
||||
use tokio::{net::UdpSocket, task::JoinSet, time::timeout};
|
||||
use url::Host;
|
||||
|
||||
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
|
||||
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
|
||||
@@ -62,8 +61,7 @@ impl PeerManagerForDirectConnector for PeerManager {
|
||||
async fn list_peers(&self) -> Vec<PeerId> {
|
||||
let mut ret = vec![];
|
||||
let allow_public_server = use_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER);
|
||||
let flags = self.get_global_ctx().get_flags();
|
||||
let lazy_p2p = flags.lazy_p2p;
|
||||
let lazy_p2p = self.get_global_ctx().get_flags().lazy_p2p;
|
||||
let now = Instant::now();
|
||||
|
||||
let routes = self.list_routes().await;
|
||||
@@ -72,15 +70,10 @@ impl PeerManagerForDirectConnector for PeerManager {
|
||||
route.feature_flag.as_ref(),
|
||||
allow_public_server,
|
||||
lazy_p2p,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(
|
||||
route.feature_flag.as_ref(),
|
||||
allow_public_server,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
) && self.has_recent_traffic(route.peer_id, now);
|
||||
let dynamic_allowed =
|
||||
should_try_p2p_with_peer(route.feature_flag.as_ref(), allow_public_server)
|
||||
&& self.has_recent_traffic(route.peer_id, now);
|
||||
if static_allowed || dynamic_allowed {
|
||||
ret.push(route.peer_id);
|
||||
}
|
||||
@@ -196,7 +189,9 @@ impl DirectConnectorManagerData {
|
||||
.await;
|
||||
|
||||
let udp_connector = UdpTunnelConnector::new(remote_url.clone());
|
||||
let remote_addr = SocketAddr::from_url(remote_url.clone(), IpVersion::V6).await?;
|
||||
let remote_addr =
|
||||
super::check_scheme_and_get_socket_addr::<SocketAddr>(remote_url, "udp", IpVersion::V6)
|
||||
.await?;
|
||||
let ret = udp_connector
|
||||
.try_connect_with_socket(local_socket, remote_addr)
|
||||
.await?;
|
||||
@@ -210,19 +205,18 @@ impl DirectConnectorManagerData {
|
||||
async fn do_try_connect_to_ip(&self, dst_peer_id: PeerId, addr: String) -> Result<(), Error> {
|
||||
let connector = create_connector_by_url(&addr, &self.global_ctx, IpVersion::Both).await?;
|
||||
let remote_url = connector.remote_url();
|
||||
let (peer_id, conn_id) = if matches_scheme!(remote_url, TunnelScheme::Ip(IpScheme::Udp))
|
||||
&& matches!(remote_url.host(), Some(Host::Ipv6(_)))
|
||||
{
|
||||
self.connect_to_public_ipv6(dst_peer_id, &remote_url)
|
||||
.await?
|
||||
} else {
|
||||
timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
self.peer_manager
|
||||
.try_direct_connect_with_peer_id_hint(connector, Some(dst_peer_id)),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
let (peer_id, conn_id) =
|
||||
if remote_url.scheme() == "udp" && matches!(remote_url.host(), Some(Host::Ipv6(_))) {
|
||||
self.connect_to_public_ipv6(dst_peer_id, &remote_url)
|
||||
.await?
|
||||
} else {
|
||||
timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
self.peer_manager
|
||||
.try_direct_connect_with_peer_id_hint(connector, Some(dst_peer_id)),
|
||||
)
|
||||
.await??
|
||||
};
|
||||
|
||||
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
|
||||
tracing::info!(
|
||||
@@ -312,7 +306,7 @@ impl DirectConnectorManagerData {
|
||||
let listener_host = addrs.pop();
|
||||
tracing::info!(?listener_host, ?listener, "try direct connect to peer");
|
||||
|
||||
let is_udp = matches_protocol!(listener, Protocol::UDP);
|
||||
let is_udp = matches!(listener.scheme(), "udp" | "wg");
|
||||
// Snapshot running listeners once; used for cheap port pre-checks before the
|
||||
// expensive should_deny_proxy call (which binds a socket per IP) in the
|
||||
// unspecified-address expansion loops below.
|
||||
@@ -320,7 +314,7 @@ impl DirectConnectorManagerData {
|
||||
let port_has_local_listener = |port: u16| -> bool {
|
||||
local_listeners
|
||||
.iter()
|
||||
.any(|l| l.port() == Some(port) && matches_protocol!(l, Protocol::UDP) == is_udp)
|
||||
.any(|l| l.port() == Some(port) && (matches!(l.scheme(), "udp" | "wg") == is_udp))
|
||||
};
|
||||
|
||||
match listener_host {
|
||||
@@ -656,6 +650,10 @@ impl DirectConnectorManager {
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
if self.global_ctx.get_flags().disable_p2p {
|
||||
return;
|
||||
}
|
||||
|
||||
self.run_as_server();
|
||||
self.run_as_client();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use super::{create_connector_by_url, http_connector::TunnelWithInfo};
|
||||
use crate::{
|
||||
common::{
|
||||
dns::{resolve_txt_record, RESOLVER},
|
||||
@@ -8,15 +7,16 @@ use crate::{
|
||||
global_ctx::ArcGlobalCtx,
|
||||
log,
|
||||
},
|
||||
proto::common::TunnelInfo,
|
||||
tunnel::{IpScheme, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelScheme},
|
||||
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use hickory_resolver::proto::rr::rdata::SRV;
|
||||
use itertools::Itertools;
|
||||
use rand::{seq::SliceRandom, Rng as _};
|
||||
use strum::VariantArray;
|
||||
|
||||
use crate::proto::common::TunnelInfo;
|
||||
|
||||
use super::{create_connector_by_url, http_connector::TunnelWithInfo};
|
||||
|
||||
fn weighted_choice<T>(options: &[(T, u64)]) -> Option<&T> {
|
||||
let total_weight = options.iter().map(|(_, weight)| *weight).sum();
|
||||
@@ -35,18 +35,16 @@ fn weighted_choice<T>(options: &[(T, u64)]) -> Option<&T> {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DnsTunnelConnector {
|
||||
scheme: TunnelScheme,
|
||||
pub struct DNSTunnelConnector {
|
||||
addr: url::Url,
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
ip_version: IpVersion,
|
||||
}
|
||||
|
||||
impl DnsTunnelConnector {
|
||||
impl DNSTunnelConnector {
|
||||
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
||||
Self {
|
||||
scheme: (&addr).try_into().unwrap(),
|
||||
addr,
|
||||
bind_addrs: Vec::new(),
|
||||
global_ctx,
|
||||
@@ -84,7 +82,7 @@ impl DnsTunnelConnector {
|
||||
Ok(connector)
|
||||
}
|
||||
|
||||
fn handle_one_srv_record(record: &SRV, protocol: IpScheme) -> Result<(url::Url, u64), Error> {
|
||||
fn handle_one_srv_record(record: &SRV, protocol: &str) -> Result<(url::Url, u64), Error> {
|
||||
// port must be non-zero
|
||||
if record.port() == 0 {
|
||||
return Err(anyhow::anyhow!("port must be non-zero").into());
|
||||
@@ -114,15 +112,15 @@ impl DnsTunnelConnector {
|
||||
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||
tracing::info!("handle_srv_record: {}", domain_name);
|
||||
|
||||
let srv_domains = IpScheme::VARIANTS
|
||||
let srv_domains = PROTO_PORT_OFFSET
|
||||
.iter()
|
||||
.map(|s| (s, format!("_easytier._{}.{}", s, domain_name)))
|
||||
.collect_vec();
|
||||
.map(|(p, _)| (format!("_easytier._{}.{}", p, domain_name), *p)) // _easytier._udp.{domain_name}
|
||||
.collect::<Vec<_>>();
|
||||
tracing::info!("build srv_domains: {:?}", srv_domains);
|
||||
let responses = Arc::new(DashSet::new());
|
||||
let srv_lookup_tasks = srv_domains
|
||||
.iter()
|
||||
.map(|(protocol, srv_domain)| {
|
||||
.map(|(srv_domain, protocol)| {
|
||||
let resolver = RESOLVER.clone();
|
||||
let responses = responses.clone();
|
||||
async move {
|
||||
@@ -131,7 +129,7 @@ impl DnsTunnelConnector {
|
||||
})?;
|
||||
tracing::info!(?response, ?srv_domain, "srv_lookup response");
|
||||
for record in response.iter() {
|
||||
let parsed_record = Self::handle_one_srv_record(record, **protocol);
|
||||
let parsed_record = Self::handle_one_srv_record(record, protocol);
|
||||
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
|
||||
if let Err(e) = &parsed_record {
|
||||
log::warn!("got invalid srv record {:?}", e);
|
||||
@@ -164,28 +162,32 @@ impl DnsTunnelConnector {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::TunnelConnector for DnsTunnelConnector {
|
||||
impl super::TunnelConnector for DNSTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let mut conn = match self.scheme {
|
||||
TunnelScheme::Txt => self
|
||||
.handle_txt_record(
|
||||
self.addr
|
||||
.host_str()
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("host should not be empty in txt url"))?,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "get txt record url failed")?,
|
||||
TunnelScheme::Srv => self
|
||||
.handle_srv_record(
|
||||
self.addr
|
||||
.host_str()
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("host should not be empty in srv url"))?,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "get srv record url failed")?,
|
||||
_ => return Err(anyhow::anyhow!("unsupported dns scheme: {:?}", self.scheme).into()),
|
||||
let mut conn = if self.addr.scheme() == "txt" {
|
||||
self.handle_txt_record(
|
||||
self.addr
|
||||
.host_str()
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("host should not be empty in txt url"))?,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "get txt record url failed")?
|
||||
} else if self.addr.scheme() == "srv" {
|
||||
self.handle_srv_record(
|
||||
self.addr
|
||||
.host_str()
|
||||
.as_ref()
|
||||
.ok_or(anyhow::anyhow!("host should not be empty in srv url"))?,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "get srv record url failed")?
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unsupported dns scheme: {}, expecting txt or srv",
|
||||
self.addr.scheme()
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let t = conn.connect().await?;
|
||||
let info = t.info().unwrap_or_default();
|
||||
@@ -225,7 +227,7 @@ mod tests {
|
||||
async fn test_txt() {
|
||||
let url = "txt://txt.easytier.cn";
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let mut connector = DnsTunnelConnector::new(url.parse().unwrap(), global_ctx);
|
||||
let mut connector = DNSTunnelConnector::new(url.parse().unwrap(), global_ctx);
|
||||
connector.set_ip_version(IpVersion::V4);
|
||||
for _ in 0..5 {
|
||||
match connector.connect().await {
|
||||
@@ -244,7 +246,7 @@ mod tests {
|
||||
async fn test_srv() {
|
||||
let url = "srv://easytier.cn";
|
||||
let global_ctx = get_mock_global_ctx();
|
||||
let mut connector = DnsTunnelConnector::new(url.parse().unwrap(), global_ctx);
|
||||
let mut connector = DNSTunnelConnector::new(url.parse().unwrap(), global_ctx);
|
||||
connector.set_ip_version(IpVersion::V4);
|
||||
for _ in 0..5 {
|
||||
match connector.connect().await {
|
||||
|
||||
+124
-144
@@ -3,17 +3,24 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use http_connector::HttpTunnelConnector;
|
||||
|
||||
#[cfg(feature = "faketcp")]
|
||||
use crate::tunnel::fake_tcp::FakeTcpTunnelConnector;
|
||||
#[cfg(feature = "quic")]
|
||||
use crate::tunnel::quic::QUICTunnelConnector;
|
||||
#[cfg(unix)]
|
||||
use crate::tunnel::unix::UnixSocketTunnelConnector;
|
||||
#[cfg(feature = "wireguard")]
|
||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, idn, network::IPCollector},
|
||||
connector::dns_connector::DnsTunnelConnector,
|
||||
proto::common::PeerFeatureFlag,
|
||||
tunnel::{
|
||||
self, ring::RingTunnelConnector, tcp::TcpTunnelConnector, udp::UdpTunnelConnector, FromUrl,
|
||||
IpScheme, IpVersion, TunnelConnector, TunnelError, TunnelScheme,
|
||||
check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector,
|
||||
udp::UdpTunnelConnector, IpVersion, TunnelConnector,
|
||||
},
|
||||
utils::BoxExt,
|
||||
};
|
||||
use http_connector::HttpTunnelConnector;
|
||||
|
||||
pub mod direct;
|
||||
pub mod manual;
|
||||
@@ -26,31 +33,19 @@ pub mod http_connector;
|
||||
pub(crate) fn should_try_p2p_with_peer(
|
||||
feature_flag: Option<&PeerFeatureFlag>,
|
||||
allow_public_server: bool,
|
||||
local_disable_p2p: bool,
|
||||
local_need_p2p: bool,
|
||||
) -> bool {
|
||||
feature_flag
|
||||
.map(|flag| {
|
||||
(allow_public_server || !flag.is_public_server)
|
||||
&& (!local_disable_p2p || flag.need_p2p)
|
||||
&& (!flag.disable_p2p || local_need_p2p)
|
||||
})
|
||||
.unwrap_or(!local_disable_p2p)
|
||||
.map(|flag| allow_public_server || !flag.is_public_server)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub(crate) fn should_background_p2p_with_peer(
|
||||
feature_flag: Option<&PeerFeatureFlag>,
|
||||
allow_public_server: bool,
|
||||
lazy_p2p: bool,
|
||||
local_disable_p2p: bool,
|
||||
local_need_p2p: bool,
|
||||
) -> bool {
|
||||
should_try_p2p_with_peer(
|
||||
feature_flag,
|
||||
allow_public_server,
|
||||
local_disable_p2p,
|
||||
local_need_p2p,
|
||||
) && (!lazy_p2p || feature_flag.map(|flag| flag.need_p2p).unwrap_or(false))
|
||||
should_try_p2p_with_peer(feature_flag, allow_public_server)
|
||||
&& (!lazy_p2p || feature_flag.map(|flag| flag.need_p2p).unwrap_or(false))
|
||||
}
|
||||
|
||||
async fn set_bind_addr_for_peer_connector(
|
||||
@@ -95,34 +90,11 @@ pub async fn create_connector_by_url(
|
||||
) -> Result<Box<dyn TunnelConnector + 'static>, Error> {
|
||||
let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?;
|
||||
let url = idn::convert_idn_to_ascii(url)?;
|
||||
let scheme = (&url)
|
||||
.try_into()
|
||||
.map_err(|_| TunnelError::InvalidProtocol(url.scheme().to_owned()))?;
|
||||
let mut connector: Box<dyn TunnelConnector + 'static> = match scheme {
|
||||
TunnelScheme::Ip(scheme) => {
|
||||
let dst_addr = SocketAddr::from_url(url.clone(), ip_version).await?;
|
||||
let mut connector: Box<dyn TunnelConnector> = match scheme {
|
||||
IpScheme::Tcp => TcpTunnelConnector::new(url).boxed(),
|
||||
IpScheme::Udp => UdpTunnelConnector::new(url).boxed(),
|
||||
#[cfg(feature = "quic")]
|
||||
IpScheme::Quic => tunnel::quic::QuicTunnelConnector::new(url).boxed(),
|
||||
#[cfg(feature = "wireguard")]
|
||||
IpScheme::Wg => {
|
||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelConnector};
|
||||
let nid = global_ctx.get_network_identity();
|
||||
let wg_config = WgConfig::new_from_network_identity(
|
||||
&nid.network_name,
|
||||
&nid.network_secret.unwrap_or_default(),
|
||||
);
|
||||
WgTunnelConnector::new(url, wg_config).boxed()
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
IpScheme::Ws | IpScheme::Wss => {
|
||||
tunnel::websocket::WsTunnelConnector::new(url).boxed()
|
||||
}
|
||||
#[cfg(feature = "faketcp")]
|
||||
IpScheme::FakeTcp => tunnel::fake_tcp::FakeTcpTunnelConnector::new(url).boxed(),
|
||||
};
|
||||
let mut connector: Box<dyn TunnelConnector + 'static> = match url.scheme() {
|
||||
"tcp" => {
|
||||
let dst_addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "tcp", ip_version).await?;
|
||||
let mut connector = TcpTunnelConnector::new(url);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
@@ -131,22 +103,113 @@ pub async fn create_connector_by_url(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
connector
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
TunnelScheme::Unix => tunnel::unix::UnixSocketTunnelConnector::new(url).boxed(),
|
||||
TunnelScheme::Http | TunnelScheme::Https => {
|
||||
HttpTunnelConnector::new(url, global_ctx.clone()).boxed()
|
||||
"udp" => {
|
||||
let dst_addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "udp", ip_version).await?;
|
||||
let mut connector = UdpTunnelConnector::new(url);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Box::new(connector)
|
||||
}
|
||||
TunnelScheme::Ring => RingTunnelConnector::new(url).boxed(),
|
||||
TunnelScheme::Txt | TunnelScheme::Srv => {
|
||||
"http" | "https" => {
|
||||
let connector = HttpTunnelConnector::new(url, global_ctx.clone());
|
||||
Box::new(connector)
|
||||
}
|
||||
"ring" => {
|
||||
check_scheme_and_get_socket_addr::<uuid::Uuid>(&url, "ring", IpVersion::Both).await?;
|
||||
let connector = RingTunnelConnector::new(url);
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(feature = "quic")]
|
||||
"quic" => {
|
||||
let dst_addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "quic", ip_version).await?;
|
||||
let mut connector = QUICTunnelConnector::new(url);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(feature = "wireguard")]
|
||||
"wg" => {
|
||||
let dst_addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "wg", ip_version).await?;
|
||||
let nid = global_ctx.get_network_identity();
|
||||
let wg_config = WgConfig::new_from_network_identity(
|
||||
&nid.network_name,
|
||||
&nid.network_secret.unwrap_or_default(),
|
||||
);
|
||||
let mut connector = WgTunnelConnector::new(url, wg_config);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(feature = "websocket")]
|
||||
"ws" | "wss" => {
|
||||
use crate::tunnel::FromUrl;
|
||||
let dst_addr = SocketAddr::from_url(url.clone(), ip_version).await?;
|
||||
let mut connector = crate::tunnel::websocket::WSTunnelConnector::new(url);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Box::new(connector)
|
||||
}
|
||||
"txt" | "srv" => {
|
||||
if url.host_str().is_none() {
|
||||
return Err(Error::InvalidUrl(format!(
|
||||
"host should not be empty in txt or srv url: {}",
|
||||
url
|
||||
)));
|
||||
}
|
||||
DnsTunnelConnector::new(url, global_ctx.clone()).boxed()
|
||||
let connector = dns_connector::DNSTunnelConnector::new(url, global_ctx.clone());
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(feature = "faketcp")]
|
||||
"faketcp" => {
|
||||
let dst_addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "faketcp", ip_version).await?;
|
||||
let mut connector = FakeTcpTunnelConnector::new(url);
|
||||
if global_ctx.config.get_flags().bind_device {
|
||||
set_bind_addr_for_peer_connector(
|
||||
&mut connector,
|
||||
dst_addr.is_ipv4(),
|
||||
&global_ctx.get_ip_collector(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Box::new(connector)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
"unix" => {
|
||||
let connector = UnixSocketTunnelConnector::new(url);
|
||||
Box::new(connector)
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::InvalidUrl(url.into()));
|
||||
}
|
||||
};
|
||||
connector.set_ip_version(ip_version);
|
||||
@@ -174,23 +237,17 @@ mod tests {
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&no_need_p2p),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_background_p2p_with_peer(
|
||||
Some(&no_need_p2p),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
true
|
||||
));
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&need_p2p),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
@@ -201,93 +258,16 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
true,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_try_p2p_with_peer(Some(&public_server), false));
|
||||
assert!(should_try_p2p_with_peer(Some(&public_server), true));
|
||||
assert!(!should_background_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_p2p_only_allows_need_p2p_exceptions() {
|
||||
let normal_peer = PeerFeatureFlag::default();
|
||||
let need_peer = PeerFeatureFlag {
|
||||
need_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
let disable_peer = PeerFeatureFlag {
|
||||
disable_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
let disable_need_peer = PeerFeatureFlag {
|
||||
disable_p2p: true,
|
||||
need_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&normal_peer),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(None, false, false, false));
|
||||
assert!(!should_try_p2p_with_peer(None, false, true, false));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&normal_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&need_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&disable_peer),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&disable_peer),
|
||||
false,
|
||||
false,
|
||||
true
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&disable_need_peer),
|
||||
false,
|
||||
true,
|
||||
true
|
||||
));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&disable_need_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
@@ -420,8 +420,7 @@ impl PeerTaskLauncher for TcpHolePunchPeerTaskLauncher {
|
||||
#[tracing::instrument(skip(self, data))]
|
||||
async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec<Self::CollectPeerItem> {
|
||||
let global_ctx = data.peer_mgr.get_global_ctx();
|
||||
let flags = global_ctx.get_flags();
|
||||
let lazy_p2p = flags.lazy_p2p;
|
||||
let lazy_p2p = global_ctx.get_flags().lazy_p2p;
|
||||
let my_tcp_nat_type = NatType::try_from(
|
||||
global_ctx
|
||||
.get_stun_info_collector()
|
||||
@@ -444,19 +443,10 @@ impl PeerTaskLauncher for TcpHolePunchPeerTaskLauncher {
|
||||
|
||||
let mut peers_to_connect = Vec::new();
|
||||
for route in data.peer_mgr.list_routes().await.iter() {
|
||||
let static_allowed = should_background_p2p_with_peer(
|
||||
route.feature_flag.as_ref(),
|
||||
false,
|
||||
lazy_p2p,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(
|
||||
route.feature_flag.as_ref(),
|
||||
false,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
) && data.peer_mgr.has_recent_traffic(route.peer_id, now);
|
||||
let static_allowed =
|
||||
should_background_p2p_with_peer(route.feature_flag.as_ref(), false, lazy_p2p);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(route.feature_flag.as_ref(), false)
|
||||
&& data.peer_mgr.has_recent_traffic(route.peer_id, now);
|
||||
if !static_allowed && !dynamic_allowed {
|
||||
continue;
|
||||
}
|
||||
@@ -564,9 +554,10 @@ impl TcpHolePunchConnector {
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
let flags = self.peer_mgr.get_global_ctx().get_flags();
|
||||
if flags.disable_tcp_hole_punching {
|
||||
if flags.disable_p2p || flags.disable_tcp_hole_punching {
|
||||
tracing::debug!(
|
||||
"tcp hole punch disabled by disable_tcp_hole_punching(={});",
|
||||
"tcp hole punch disabled by disable_p2p(={}) or disable_tcp_hole_punching(={});",
|
||||
flags.disable_p2p,
|
||||
flags.disable_tcp_hole_punching
|
||||
);
|
||||
return Ok(());
|
||||
|
||||
@@ -428,8 +428,7 @@ impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher {
|
||||
}
|
||||
|
||||
let my_peer_id = data.peer_mgr.my_peer_id();
|
||||
let flags = data.peer_mgr.get_global_ctx().get_flags();
|
||||
let lazy_p2p = flags.lazy_p2p;
|
||||
let lazy_p2p = data.peer_mgr.get_global_ctx().get_flags().lazy_p2p;
|
||||
let now = Instant::now();
|
||||
|
||||
data.blacklist.cleanup();
|
||||
@@ -439,19 +438,10 @@ impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher {
|
||||
// 2. peers is full cone (any restricted type);
|
||||
// 3. peers not in blacklist;
|
||||
for route in data.peer_mgr.list_routes().await.iter() {
|
||||
let static_allowed = should_background_p2p_with_peer(
|
||||
route.feature_flag.as_ref(),
|
||||
false,
|
||||
lazy_p2p,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(
|
||||
route.feature_flag.as_ref(),
|
||||
false,
|
||||
flags.disable_p2p,
|
||||
flags.need_p2p,
|
||||
) && data.peer_mgr.has_recent_traffic(route.peer_id, now);
|
||||
let static_allowed =
|
||||
should_background_p2p_with_peer(route.feature_flag.as_ref(), false, lazy_p2p);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(route.feature_flag.as_ref(), false)
|
||||
&& data.peer_mgr.has_recent_traffic(route.peer_id, now);
|
||||
if !static_allowed && !dynamic_allowed {
|
||||
continue;
|
||||
}
|
||||
@@ -575,6 +565,9 @@ impl UdpHolePunchConnector {
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
|
||||
if global_ctx.get_flags().disable_p2p {
|
||||
return Ok(());
|
||||
}
|
||||
if global_ctx.get_flags().disable_udp_hole_punching {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
+17
-24
@@ -22,6 +22,7 @@ use crate::{
|
||||
launcher::add_proxy_network_to_config,
|
||||
proto::common::{CompressionAlgoPb, SecureModeConfig},
|
||||
rpc_service::ApiRpcServer,
|
||||
tunnel::PROTO_PORT_OFFSET,
|
||||
utils::setup_panic_handler,
|
||||
web_client, ShellType,
|
||||
};
|
||||
@@ -29,10 +30,8 @@ use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use clap::{CommandFactory, Parser};
|
||||
use rust_i18n::t;
|
||||
use strum::VariantArray;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::tunnel::IpScheme;
|
||||
#[cfg(feature = "jemalloc-prof")]
|
||||
use jemalloc_ctl::{epoch, stats, Access as _, AsName as _};
|
||||
|
||||
@@ -561,13 +560,6 @@ struct NetworkOptions {
|
||||
)]
|
||||
foreign_relay_bps_limit: Option<u64>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "ET_INSTANCE_RECV_BPS_LIMIT",
|
||||
help = t!("core_clap.instance_recv_bps_limit").to_string(),
|
||||
)]
|
||||
instance_recv_bps_limit: Option<u64>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_delimiter = ',',
|
||||
@@ -743,12 +735,8 @@ impl Cli {
|
||||
let mut listeners: Vec<String> = Vec::new();
|
||||
if origin_listeners.len() == 1 {
|
||||
if let Ok(port) = origin_listeners[0].parse::<u16>() {
|
||||
for proto in IpScheme::VARIANTS {
|
||||
listeners.push(format!(
|
||||
"{}://0.0.0.0:{}",
|
||||
proto,
|
||||
port + proto.port_offset()
|
||||
));
|
||||
for (proto, offset) in PROTO_PORT_OFFSET {
|
||||
listeners.push(format!("{}://0.0.0.0:{}", proto, port + *offset));
|
||||
}
|
||||
return Ok(listeners);
|
||||
}
|
||||
@@ -763,15 +751,20 @@ impl Cli {
|
||||
panic!("failed to parse listener: {}", l);
|
||||
}
|
||||
} else {
|
||||
let scheme: IpScheme = proto_port[0].parse()?;
|
||||
let Some((proto, offset)) = PROTO_PORT_OFFSET
|
||||
.iter()
|
||||
.find(|(proto, _)| *proto == proto_port[0])
|
||||
else {
|
||||
return Err(anyhow::anyhow!("unknown protocol: {}", proto_port[0]));
|
||||
};
|
||||
|
||||
let port = if proto_port.len() == 2 {
|
||||
proto_port[1].parse::<u16>().unwrap()
|
||||
} else {
|
||||
11010 + scheme.port_offset()
|
||||
11010 + offset
|
||||
};
|
||||
|
||||
listeners.push(format!("{}://0.0.0.0:{}", scheme, port));
|
||||
listeners.push(format!("{}://0.0.0.0:{}", proto, port));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,9 +1060,6 @@ impl NetworkOptions {
|
||||
f.foreign_relay_bps_limit = self
|
||||
.foreign_relay_bps_limit
|
||||
.unwrap_or(f.foreign_relay_bps_limit);
|
||||
f.instance_recv_bps_limit = self
|
||||
.instance_recv_bps_limit
|
||||
.unwrap_or(f.instance_recv_bps_limit);
|
||||
f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count);
|
||||
f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp);
|
||||
f.disable_relay_quic = self.disable_relay_quic.unwrap_or(f.disable_relay_quic);
|
||||
@@ -1134,7 +1124,8 @@ impl LoggingConfigLoader for &LoggingOptions {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> {
|
||||
use crate::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
|
||||
use winreg::{enums::*, RegKey};
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?;
|
||||
@@ -1214,9 +1205,11 @@ fn parse_cli() -> Cli {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Notify;
|
||||
use windows_service::{service::*, service_control_handler::*};
|
||||
use windows_service::service::*;
|
||||
use windows_service::service_control_handler::*;
|
||||
|
||||
_ = win_service_set_work_dir(&arg[0]);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ use easytier::{
|
||||
rpc_impl::standalone::StandAloneClient,
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::{tcp::TcpTunnelConnector, TunnelScheme},
|
||||
tunnel::tcp::TcpTunnelConnector,
|
||||
utils::{cost_to_str, PeerRoutePair},
|
||||
};
|
||||
|
||||
@@ -192,6 +192,8 @@ struct PeerArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum PeerSubCommand {
|
||||
Add,
|
||||
Remove,
|
||||
List,
|
||||
ListForeign {
|
||||
#[arg(
|
||||
@@ -230,16 +232,8 @@ struct ConnectorArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum ConnectorSubCommand {
|
||||
/// Add a connector
|
||||
Add {
|
||||
#[arg(help = "connector url, e.g., tcp://1.2.3.4:11010")]
|
||||
url: String,
|
||||
},
|
||||
/// Remove a connector
|
||||
Remove {
|
||||
#[arg(help = "connector url, e.g., tcp://1.2.3.4:11010")]
|
||||
url: String,
|
||||
},
|
||||
Add,
|
||||
Remove,
|
||||
List,
|
||||
}
|
||||
|
||||
@@ -1158,59 +1152,14 @@ impl<'a> CommandHandler<'a> {
|
||||
.prometheus_text)
|
||||
}
|
||||
|
||||
fn connector_validate_url(url: &str) -> Result<url::Url, Error> {
|
||||
let url = url::Url::parse(url).map_err(|e| anyhow::anyhow!("invalid url ({url}): {e}"))?;
|
||||
TunnelScheme::try_from(&url).map_err(|_| {
|
||||
anyhow::anyhow!("unsupported scheme \"{}\" in url ({url})", url.scheme())
|
||||
})?;
|
||||
Ok(url)
|
||||
#[allow(dead_code)]
|
||||
fn handle_peer_add(&self, _args: PeerArgs) {
|
||||
println!("add peer");
|
||||
}
|
||||
|
||||
async fn apply_connector_modify(
|
||||
&self,
|
||||
url: &str,
|
||||
action: ConfigPatchAction,
|
||||
) -> Result<(), Error> {
|
||||
let url = match action {
|
||||
ConfigPatchAction::Add => Self::connector_validate_url(url)?,
|
||||
ConfigPatchAction::Remove => {
|
||||
url::Url::parse(url).map_err(|e| anyhow::anyhow!("invalid url ({url}): {e}"))?
|
||||
}
|
||||
ConfigPatchAction::Clear => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unsupported connector patch action: {:?}",
|
||||
action
|
||||
));
|
||||
}
|
||||
};
|
||||
let client = self.get_config_client().await?;
|
||||
let request = PatchConfigRequest {
|
||||
instance: Some(self.instance_selector.clone()),
|
||||
patch: Some(InstanceConfigPatch {
|
||||
connectors: vec![UrlPatch {
|
||||
action: action.into(),
|
||||
url: Some(url.into()),
|
||||
}],
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
let _response = client
|
||||
.patch_config(BaseController::default(), request)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connector_modify(
|
||||
&self,
|
||||
url: &str,
|
||||
action: ConfigPatchAction,
|
||||
) -> Result<(), Error> {
|
||||
let url = url.to_string();
|
||||
self.apply_to_instances(|handler| {
|
||||
let url = url.clone();
|
||||
Box::pin(async move { handler.apply_connector_modify(&url, action).await })
|
||||
})
|
||||
.await
|
||||
#[allow(dead_code)]
|
||||
fn handle_peer_remove(&self, _args: PeerArgs) {
|
||||
println!("remove peer");
|
||||
}
|
||||
|
||||
async fn handle_peer_list(&self) -> Result<(), Error> {
|
||||
@@ -2623,6 +2572,12 @@ async fn main() -> Result<(), Error> {
|
||||
|
||||
match cli.sub_command {
|
||||
SubCommand::Peer(peer_args) => match &peer_args.sub_command {
|
||||
Some(PeerSubCommand::Add) => {
|
||||
println!("add peer");
|
||||
}
|
||||
Some(PeerSubCommand::Remove) => {
|
||||
println!("remove peer");
|
||||
}
|
||||
Some(PeerSubCommand::List) => {
|
||||
handler.handle_peer_list().await?;
|
||||
}
|
||||
@@ -2637,17 +2592,11 @@ async fn main() -> Result<(), Error> {
|
||||
}
|
||||
},
|
||||
SubCommand::Connector(conn_args) => match conn_args.sub_command {
|
||||
Some(ConnectorSubCommand::Add { url }) => {
|
||||
handler
|
||||
.handle_connector_modify(&url, ConfigPatchAction::Add)
|
||||
.await?;
|
||||
println!("connector add applied to selected instance(s): {url}");
|
||||
Some(ConnectorSubCommand::Add) => {
|
||||
println!("add connector");
|
||||
}
|
||||
Some(ConnectorSubCommand::Remove { url }) => {
|
||||
handler
|
||||
.handle_connector_modify(&url, ConfigPatchAction::Remove)
|
||||
.await?;
|
||||
println!("connector remove applied to selected instance(s): {url}");
|
||||
Some(ConnectorSubCommand::Remove) => {
|
||||
println!("remove connector");
|
||||
}
|
||||
Some(ConnectorSubCommand::List) => {
|
||||
handler.handle_connector_list().await?;
|
||||
|
||||
@@ -808,7 +808,14 @@ impl Instance {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(all(not(mobile), feature = "tun"))]
|
||||
#[cfg(all(
|
||||
not(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
)),
|
||||
feature = "tun"
|
||||
))]
|
||||
{
|
||||
let mut new_nic_ctx = NicCtx::new(
|
||||
global_ctx_c.clone(),
|
||||
@@ -849,7 +856,14 @@ impl Instance {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(not(mobile), feature = "tun"))]
|
||||
#[cfg(all(
|
||||
not(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
)),
|
||||
feature = "tun"
|
||||
))]
|
||||
fn check_for_static_ip(&self, first_round_output: oneshot::Sender<Result<(), Error>>) {
|
||||
let ipv4_addr = self.global_ctx.get_ipv4();
|
||||
let ipv6_addr = self.global_ctx.get_ipv6();
|
||||
@@ -937,7 +951,11 @@ impl Instance {
|
||||
{
|
||||
Self::clear_nic_ctx(self.nic_ctx.clone(), self.peer_packet_receiver.clone()).await;
|
||||
|
||||
#[cfg(not(mobile))]
|
||||
#[cfg(not(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
)))]
|
||||
if !self.global_ctx.config.get_flags().no_tun {
|
||||
let (output_tx, output_rx) = oneshot::channel();
|
||||
self.check_for_static_ip(output_tx);
|
||||
@@ -1457,7 +1475,11 @@ impl Instance {
|
||||
self.peer_packet_receiver.clone()
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
pub async fn setup_nic_ctx_for_mobile(
|
||||
nic_ctx: ArcNicCtx,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
@@ -9,6 +9,12 @@ use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[cfg(feature = "faketcp")]
|
||||
use crate::tunnel::fake_tcp::FakeTcpTunnelListener;
|
||||
#[cfg(feature = "quic")]
|
||||
use crate::tunnel::quic::QUICTunnelListener;
|
||||
#[cfg(feature = "wireguard")]
|
||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelListener};
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
@@ -17,42 +23,44 @@ use crate::{
|
||||
},
|
||||
peers::peer_manager::PeerManager,
|
||||
tunnel::{
|
||||
self, ring::RingTunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener, IpScheme,
|
||||
Tunnel, TunnelListener, TunnelScheme,
|
||||
ring::RingTunnelListener, tcp::TcpTunnelListener, udp::UdpTunnelListener, Tunnel,
|
||||
TunnelListener,
|
||||
},
|
||||
utils::BoxExt,
|
||||
};
|
||||
|
||||
pub fn create_listener_by_url(
|
||||
pub fn get_listener_by_url(
|
||||
l: &url::Url,
|
||||
#[allow(unused_variables)] ctx: ArcGlobalCtx,
|
||||
_ctx: ArcGlobalCtx,
|
||||
) -> Result<Box<dyn TunnelListener>, Error> {
|
||||
Ok(match l.try_into()? {
|
||||
TunnelScheme::Ip(scheme) => match scheme {
|
||||
IpScheme::Tcp => TcpTunnelListener::new(l.clone()).boxed(),
|
||||
IpScheme::Udp => UdpTunnelListener::new(l.clone()).boxed(),
|
||||
#[cfg(feature = "wireguard")]
|
||||
IpScheme::Wg => {
|
||||
use crate::tunnel::wireguard::{WgConfig, WgTunnelListener};
|
||||
let nid = ctx.get_network_identity();
|
||||
let wg_config = WgConfig::new_from_network_identity(
|
||||
&nid.network_name,
|
||||
&nid.network_secret.unwrap_or_default(),
|
||||
);
|
||||
WgTunnelListener::new(l.clone(), wg_config).boxed()
|
||||
}
|
||||
#[cfg(feature = "quic")]
|
||||
IpScheme::Quic => tunnel::quic::QuicTunnelListener::new(l.clone()).boxed(),
|
||||
#[cfg(feature = "websocket")]
|
||||
IpScheme::Ws | IpScheme::Wss => {
|
||||
tunnel::websocket::WsTunnelListener::new(l.clone()).boxed()
|
||||
}
|
||||
#[cfg(feature = "faketcp")]
|
||||
IpScheme::FakeTcp => tunnel::fake_tcp::FakeTcpTunnelListener::new(l.clone()).boxed(),
|
||||
},
|
||||
Ok(match l.scheme() {
|
||||
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
|
||||
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
|
||||
#[cfg(feature = "wireguard")]
|
||||
"wg" => {
|
||||
let nid = _ctx.get_network_identity();
|
||||
let wg_config = WgConfig::new_from_network_identity(
|
||||
&nid.network_name,
|
||||
&nid.network_secret.unwrap_or_default(),
|
||||
);
|
||||
Box::new(WgTunnelListener::new(l.clone(), wg_config))
|
||||
}
|
||||
#[cfg(feature = "quic")]
|
||||
"quic" => Box::new(QUICTunnelListener::new(l.clone())),
|
||||
#[cfg(feature = "websocket")]
|
||||
"ws" | "wss" => {
|
||||
use crate::tunnel::websocket::WSTunnelListener;
|
||||
Box::new(WSTunnelListener::new(l.clone()))
|
||||
}
|
||||
#[cfg(feature = "faketcp")]
|
||||
"faketcp" => Box::new(FakeTcpTunnelListener::new(l.clone())),
|
||||
#[cfg(unix)]
|
||||
TunnelScheme::Unix => tunnel::unix::UnixSocketTunnelListener::new(l.clone()).boxed(),
|
||||
_ => return Err(Error::InvalidUrl(l.to_string())),
|
||||
"unix" => {
|
||||
use crate::tunnel::unix::UnixSocketTunnelListener;
|
||||
Box::new(UnixSocketTunnelListener::new(l.clone()))
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::InvalidUrl(l.to_string()));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,7 +133,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
|
||||
for l in self.global_ctx.config.get_listener_uris().iter() {
|
||||
let l = l.clone();
|
||||
let Ok(_) = create_listener_by_url(&l, self.global_ctx.clone()) else {
|
||||
let Ok(_) = get_listener_by_url(&l, self.global_ctx.clone()) else {
|
||||
let msg = format!("failed to get listener by url: {}, maybe not supported", l);
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::ListenerAddFailed(l.clone(), msg));
|
||||
@@ -135,7 +143,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
|
||||
let listener = l.clone();
|
||||
self.add_listener(
|
||||
move || create_listener_by_url(&listener, ctx.clone()).unwrap(),
|
||||
move || get_listener_by_url(&listener, ctx.clone()).unwrap(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
@@ -152,7 +160,7 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
.with_context(|| format!("failed to set ipv6 host for listener: {}", l))?;
|
||||
let ctx = self.global_ctx.clone();
|
||||
self.add_listener(
|
||||
move || create_listener_by_url(&ipv6_listener, ctx.clone()).unwrap(),
|
||||
move || get_listener_by_url(&ipv6_listener, ctx.clone()).unwrap(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -353,6 +361,10 @@ mod tests {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelListener for MockListener {
|
||||
fn local_url(&self) -> url::Url {
|
||||
"mock://".parse().unwrap()
|
||||
}
|
||||
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
self.counter.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
@@ -362,10 +374,6 @@ mod tests {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Err(TunnelError::BufferFull)
|
||||
}
|
||||
|
||||
fn local_url(&self) -> url::Url {
|
||||
"mock://".parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MockListener {
|
||||
|
||||
@@ -575,7 +575,11 @@ impl VirtualNic {
|
||||
Ok(tun::create(&config)?)
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
pub async fn create_dev_for_mobile(
|
||||
&mut self,
|
||||
tun_fd: std::os::fd::RawFd,
|
||||
@@ -1171,7 +1175,11 @@ impl NicCtx {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
pub async fn run_for_mobile(&mut self, tun_fd: std::os::fd::RawFd) -> Result<(), Error> {
|
||||
let tunnel = {
|
||||
let mut nic = self.nic.lock().await;
|
||||
|
||||
@@ -227,17 +227,11 @@ impl NetworkInstanceManager {
|
||||
}
|
||||
|
||||
pub fn set_tun_fd(&self, instance_id: &uuid::Uuid, fd: i32) -> Result<(), anyhow::Error> {
|
||||
let sender = self
|
||||
let mut instance = self
|
||||
.instance_map
|
||||
.get(instance_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("instance not found"))?
|
||||
.get_tun_fd_sender()
|
||||
.ok_or_else(|| anyhow::anyhow!("tun fd sender not found"))?;
|
||||
|
||||
sender
|
||||
.try_send(Some(fd))
|
||||
.map_err(|e| anyhow::anyhow!("failed to send tun fd: {}", e))?;
|
||||
|
||||
.get_mut(instance_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("instance not found"))?;
|
||||
instance.set_tun_fd(fd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+30
-14
@@ -93,7 +93,11 @@ impl EasyTierLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
async fn run_routine_for_mobile(
|
||||
instance: &Instance,
|
||||
data: &EasyTierData,
|
||||
@@ -152,7 +156,11 @@ impl EasyTierLauncher {
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(mobile)]
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
any(target_os = "ios", all(target_os = "macos", feature = "macos-ne")),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
Self::run_routine_for_mobile(&instance, &data, &mut tasks).await;
|
||||
|
||||
instance.run().await?;
|
||||
@@ -395,6 +403,12 @@ impl NetworkInstance {
|
||||
self.config.get_network_identity().network_name
|
||||
}
|
||||
|
||||
pub fn set_tun_fd(&mut self, tun_fd: i32) {
|
||||
if let Some(launcher) = self.launcher.as_ref() {
|
||||
let _ = launcher.data.tun_fd.0.blocking_send(Some(tun_fd));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tun_fd_sender(&self) -> Option<mpsc::Sender<TunFd>> {
|
||||
self.launcher
|
||||
.as_ref()
|
||||
@@ -559,9 +573,8 @@ impl NetworkConfig {
|
||||
peer_public_key: None,
|
||||
});
|
||||
}
|
||||
if !peers.is_empty() {
|
||||
cfg.set_peers(peers);
|
||||
}
|
||||
|
||||
cfg.set_peers(peers);
|
||||
}
|
||||
NetworkingMethod::Standalone => {}
|
||||
}
|
||||
@@ -813,10 +826,6 @@ impl NetworkConfig {
|
||||
flags.mtu = mtu as u32;
|
||||
}
|
||||
|
||||
if let Some(instance_recv_bps_limit) = self.instance_recv_bps_limit {
|
||||
flags.instance_recv_bps_limit = instance_recv_bps_limit;
|
||||
}
|
||||
|
||||
if let Some(enable_private_mode) = self.enable_private_mode {
|
||||
flags.private_mode = enable_private_mode;
|
||||
}
|
||||
@@ -861,9 +870,18 @@ impl NetworkConfig {
|
||||
}
|
||||
|
||||
let peers = config.get_peers();
|
||||
result.networking_method = Some(NetworkingMethod::Manual as i32);
|
||||
if !peers.is_empty() {
|
||||
result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect();
|
||||
match peers.len() {
|
||||
1 => {
|
||||
result.networking_method = Some(NetworkingMethod::PublicServer as i32);
|
||||
result.public_server_url = Some(peers[0].uri.to_string());
|
||||
}
|
||||
0 => {
|
||||
result.networking_method = Some(NetworkingMethod::Standalone as i32);
|
||||
}
|
||||
_ => {
|
||||
result.networking_method = Some(NetworkingMethod::Manual as i32);
|
||||
result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect();
|
||||
}
|
||||
}
|
||||
|
||||
result.listener_urls = config
|
||||
@@ -960,8 +978,6 @@ impl NetworkConfig {
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
result.mtu = Some(flags.mtu as i32);
|
||||
result.instance_recv_bps_limit =
|
||||
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
||||
result.enable_private_mode = Some(flags.private_mode);
|
||||
|
||||
if flags.relay_network_whitelist == "*" {
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
common::{config::EncryptionAlgorithm, log},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "wireguard")]
|
||||
@@ -60,11 +61,8 @@ impl Encryptor for NullCipher {
|
||||
pub fn create_encryptor(
|
||||
algorithm: &str,
|
||||
key_128: [u8; 16],
|
||||
#[allow(unused_variables)] key_256: [u8; 32],
|
||||
key_256: [u8; 32],
|
||||
) -> Arc<dyn Encryptor> {
|
||||
#[cfg(any(feature = "aes-gcm", feature = "wireguard", feature = "openssl-crypto"))]
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
let algorithm = match EncryptionAlgorithm::try_from(algorithm) {
|
||||
Ok(algorithm) => algorithm,
|
||||
Err(_) => {
|
||||
@@ -77,7 +75,6 @@ pub fn create_encryptor(
|
||||
default
|
||||
}
|
||||
};
|
||||
|
||||
match algorithm {
|
||||
EncryptionAlgorithm::Xor => Arc::new(xor::XorCipher::new(&key_128)),
|
||||
|
||||
|
||||
@@ -730,44 +730,16 @@ impl ForeignNetworkManager {
|
||||
matches!(identity_type, PeerIdentityType::Admin)
|
||||
}
|
||||
|
||||
fn credential_pubkey_is_trusted(
|
||||
global_ctx: &ArcGlobalCtx,
|
||||
network_name: &str,
|
||||
remote_static_pubkey: &[u8],
|
||||
) -> bool {
|
||||
remote_static_pubkey.len() == 32
|
||||
&& global_ctx.is_pubkey_trusted_with_source(
|
||||
remote_static_pubkey,
|
||||
network_name,
|
||||
TrustedKeySource::OspfCredential,
|
||||
)
|
||||
}
|
||||
|
||||
fn is_credential_pubkey_trusted(
|
||||
async fn is_credential_pubkey_trusted(
|
||||
entry: &ForeignNetworkEntry,
|
||||
remote_static_pubkey: &[u8],
|
||||
) -> bool {
|
||||
Self::credential_pubkey_is_trusted(
|
||||
&entry.global_ctx,
|
||||
&entry.network.network_name,
|
||||
remote_static_pubkey,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_existing_credential_pubkey_trusted(
|
||||
&self,
|
||||
network_name: &str,
|
||||
remote_static_pubkey: &[u8],
|
||||
) -> bool {
|
||||
self.data
|
||||
.get_network_entry(network_name)
|
||||
.is_some_and(|entry| {
|
||||
Self::credential_pubkey_is_trusted(
|
||||
&entry.global_ctx,
|
||||
&entry.network.network_name,
|
||||
remote_static_pubkey,
|
||||
)
|
||||
})
|
||||
remote_static_pubkey.len() == 32
|
||||
&& entry.global_ctx.is_pubkey_trusted_with_source(
|
||||
remote_static_pubkey,
|
||||
&entry.network.network_name,
|
||||
TrustedKeySource::OspfCredential,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_trusted_key_items(entry: &ForeignNetworkEntry) -> Vec<TrustedKeyInfoPb> {
|
||||
@@ -867,7 +839,8 @@ impl ForeignNetworkManager {
|
||||
let same_identity = entry.network == peer_network;
|
||||
let peer_identity_type = peer_conn.get_peer_identity_type();
|
||||
let credential_peer_trusted = peer_digest_empty
|
||||
&& Self::is_credential_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey);
|
||||
&& Self::is_credential_pubkey_trusted(&entry, &conn_info.noise_remote_static_pubkey)
|
||||
.await;
|
||||
let credential_identity_mismatch = credential_peer_trusted
|
||||
&& Self::should_reject_credential_trust_path(peer_identity_type);
|
||||
|
||||
@@ -1510,9 +1483,7 @@ pub mod tests {
|
||||
)]),
|
||||
&foreign_network.network_name,
|
||||
);
|
||||
assert!(!ForeignNetworkManager::is_credential_pubkey_trusted(
|
||||
&entry, &pubkey
|
||||
));
|
||||
assert!(!ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await);
|
||||
|
||||
entry.global_ctx.update_trusted_keys(
|
||||
HashMap::from([(
|
||||
@@ -1524,9 +1495,7 @@ pub mod tests {
|
||||
)]),
|
||||
&foreign_network.network_name,
|
||||
);
|
||||
assert!(ForeignNetworkManager::is_credential_pubkey_trusted(
|
||||
&entry, &pubkey
|
||||
));
|
||||
assert!(ForeignNetworkManager::is_credential_pubkey_trusted(&entry, &pubkey).await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1365,17 +1365,6 @@ impl PeerConn {
|
||||
&format!("{}:recv", conn_info_for_instrument.network_name),
|
||||
limiter_config.into(),
|
||||
))
|
||||
} else if self.global_ctx.get_flags().instance_recv_bps_limit != u64::MAX {
|
||||
let limiter_config = LimiterConfig {
|
||||
burst_rate: None,
|
||||
bps: Some(self.global_ctx.get_flags().instance_recv_bps_limit),
|
||||
fill_duration_ms: None,
|
||||
};
|
||||
Some(
|
||||
self.global_ctx
|
||||
.token_bucket_manager()
|
||||
.get_or_create("instance:recv", limiter_config.into()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1483,40 +1472,6 @@ impl PeerConn {
|
||||
ret
|
||||
}
|
||||
|
||||
fn network_secret_digest_is_empty(network: &NetworkIdentity) -> bool {
|
||||
network
|
||||
.network_secret_digest
|
||||
.as_ref()
|
||||
.is_none_or(|digest| digest.iter().all(|byte| *byte == 0))
|
||||
}
|
||||
|
||||
fn matches_local_secret_proof(&self) -> bool {
|
||||
let Some(secret_proof) = self
|
||||
.noise_handshake_result
|
||||
.as_ref()
|
||||
.and_then(|noise| noise.client_secret_proof.as_ref())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.global_ctx
|
||||
.get_secret_proof(&secret_proof.challenge)
|
||||
.is_some_and(|mac| mac.verify_slice(&secret_proof.proof).is_ok())
|
||||
}
|
||||
|
||||
pub(crate) fn matches_local_network_secret(&self) -> bool {
|
||||
if self.matches_local_secret_proof() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let my_identity = self.global_ctx.get_network_identity();
|
||||
let peer_identity = self.get_network_identity();
|
||||
|
||||
!Self::network_secret_digest_is_empty(&my_identity)
|
||||
&& !Self::network_secret_digest_is_empty(&peer_identity)
|
||||
&& my_identity.network_secret_digest == peer_identity.network_secret_digest
|
||||
}
|
||||
|
||||
pub fn get_close_notifier(&self) -> Arc<PeerConnCloseNotify> {
|
||||
self.close_event_notifier.clone()
|
||||
}
|
||||
|
||||
@@ -697,6 +697,12 @@ impl PeerManager {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.global_ctx.config.get_flags().private_mode {
|
||||
return Err(Error::SecretKeyError(
|
||||
"private mode is turned on, network identity not match".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut peer_id = self
|
||||
.foreign_network_manager
|
||||
.get_network_peer_id(network_name);
|
||||
@@ -718,31 +724,11 @@ impl PeerManager {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let peer_identity = conn.get_network_identity();
|
||||
let peer_network_name = peer_identity.network_name.clone();
|
||||
let my_identity = self.global_ctx.get_network_identity();
|
||||
let is_local_network = peer_network_name == my_identity.network_name;
|
||||
let trusted_foreign_credential =
|
||||
matches!(conn.get_peer_identity_type(), PeerIdentityType::Credential)
|
||||
&& self
|
||||
.foreign_network_manager
|
||||
.is_existing_credential_pubkey_trusted(
|
||||
&peer_network_name,
|
||||
&conn.get_conn_info().noise_remote_static_pubkey,
|
||||
);
|
||||
let foreign_network_allowed =
|
||||
conn.matches_local_network_secret() || trusted_foreign_credential;
|
||||
|
||||
if !is_local_network && self.global_ctx.get_flags().private_mode && !foreign_network_allowed
|
||||
{
|
||||
return Err(Error::SecretKeyError(
|
||||
"private mode is turned on, foreign network secret mismatch".to_string(),
|
||||
));
|
||||
}
|
||||
let peer_network_name = conn.get_network_identity().network_name.clone();
|
||||
|
||||
conn.set_is_hole_punched(!is_directly_connected);
|
||||
|
||||
if is_local_network {
|
||||
if peer_network_name == self.global_ctx.get_network_identity().network_name {
|
||||
self.add_new_peer_conn(conn).await?;
|
||||
} else {
|
||||
self.foreign_network_manager.add_peer_conn(conn).await?;
|
||||
@@ -2003,7 +1989,7 @@ mod tests {
|
||||
create_connector_by_url, direct::PeerManagerForDirectConnector,
|
||||
udp_hole_punch::tests::create_mock_peer_manager_with_mock_stun,
|
||||
},
|
||||
instance::listeners::create_listener_by_url,
|
||||
instance::listeners::get_listener_by_url,
|
||||
peers::{
|
||||
create_packet_recv_chan,
|
||||
peer_conn::tests::set_secure_mode_cfg,
|
||||
@@ -2783,7 +2769,7 @@ mod tests {
|
||||
let peer_mgr_c = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
register_service(&peer_mgr_c.peer_rpc_mgr, "", 0, "hello c");
|
||||
|
||||
let mut listener1 = create_listener_by_url(
|
||||
let mut listener1 = get_listener_by_url(
|
||||
&format!("{}://0.0.0.0:31013", proto1).parse().unwrap(),
|
||||
peer_mgr_b.get_global_ctx(),
|
||||
)
|
||||
@@ -2802,7 +2788,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut listener2 = create_listener_by_url(
|
||||
let mut listener2 = get_listener_by_url(
|
||||
&format!("{}://0.0.0.0:31014", proto2).parse().unwrap(),
|
||||
peer_mgr_c.get_global_ctx(),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ use crate::{
|
||||
stats_manager::{LabelSet, LabelType, MetricName},
|
||||
PeerId,
|
||||
},
|
||||
proto::api::instance::TrustedKeySourcePb,
|
||||
tunnel::{
|
||||
common::tests::wait_for_condition,
|
||||
packet_def::{PacketType, ZCPacket},
|
||||
@@ -64,92 +63,6 @@ pub async fn create_mock_peer_manager_secure(
|
||||
peer_mgr
|
||||
}
|
||||
|
||||
fn set_private_mode(peer_mgr: &PeerManager, enabled: bool) {
|
||||
let global_ctx = peer_mgr.get_global_ctx();
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.private_mode = enabled;
|
||||
global_ctx.set_flags(flags);
|
||||
}
|
||||
|
||||
async fn connect_client_and_server(
|
||||
client: Arc<PeerManager>,
|
||||
server: Arc<PeerManager>,
|
||||
) -> (Result<(), Error>, Result<(), Error>) {
|
||||
let (client_ring, server_ring) = create_ring_tunnel_pair();
|
||||
tokio::join!(
|
||||
{
|
||||
let client = client.clone();
|
||||
async move {
|
||||
client.add_client_tunnel(client_ring, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
{
|
||||
let server = server.clone();
|
||||
async move { server.add_tunnel_as_server(server_ring, true).await }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn wait_for_foreign_network(server: Arc<PeerManager>, network_name: &'static str) {
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let server = server.clone();
|
||||
async move {
|
||||
server
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks()
|
||||
.await
|
||||
.foreign_networks
|
||||
.contains_key(network_name)
|
||||
}
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_foreign_network_peer_count_at_least(
|
||||
server: Arc<PeerManager>,
|
||||
network_name: &'static str,
|
||||
min_peer_count: usize,
|
||||
) {
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let server = server.clone();
|
||||
async move {
|
||||
server
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks()
|
||||
.await
|
||||
.foreign_networks
|
||||
.get(network_name)
|
||||
.map(|entry| entry.peers.len() >= min_peer_count)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_public_peers_empty(client: Arc<PeerManager>) {
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
client
|
||||
.get_foreign_network_client()
|
||||
.list_public_peers()
|
||||
.await
|
||||
.is_empty()
|
||||
}
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn connect_peer_manager(client: Arc<PeerManager>, server: Arc<PeerManager>) {
|
||||
let (a_ring, b_ring) = create_ring_tunnel_pair();
|
||||
let a_mgr_copy = client;
|
||||
@@ -292,145 +205,6 @@ async fn relay_peer_map_secure_session_decrypt() {
|
||||
assert_eq!(packet.payload(), b"relay-hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_mode_allows_foreign_network_with_same_secret() {
|
||||
let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await;
|
||||
let client =
|
||||
create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await;
|
||||
set_private_mode(&server, true);
|
||||
|
||||
let (client_ret, server_ret) = connect_client_and_server(client, server.clone()).await;
|
||||
|
||||
assert!(client_ret.is_ok(), "client should connect in private mode");
|
||||
assert!(
|
||||
server_ret.is_ok(),
|
||||
"server should accept foreign network with matching secret: {:?}",
|
||||
server_ret
|
||||
);
|
||||
wait_for_foreign_network(server, "tenant-a").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_mode_rejects_foreign_network_with_different_secret() {
|
||||
let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await;
|
||||
let client = create_mock_peer_manager_secure("tenant-a".to_string(), "other".to_string()).await;
|
||||
set_private_mode(&server, true);
|
||||
|
||||
let (client_ret, server_ret) = connect_client_and_server(client.clone(), server.clone()).await;
|
||||
|
||||
assert!(
|
||||
server_ret.is_err(),
|
||||
"server should reject foreign network with mismatched secret in private mode"
|
||||
);
|
||||
let _ = client_ret;
|
||||
wait_for_public_peers_empty(client).await;
|
||||
assert!(server
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks()
|
||||
.await
|
||||
.foreign_networks
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_mode_allows_trusted_foreign_credential() {
|
||||
let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await;
|
||||
let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await;
|
||||
set_private_mode(&server, true);
|
||||
|
||||
let (_cred_id, cred_secret) = admin
|
||||
.get_global_ctx()
|
||||
.get_credential_manager()
|
||||
.generate_credential(vec![], false, vec![], Duration::from_secs(3600));
|
||||
|
||||
let privkey_bytes: [u8; 32] = base64::engine::general_purpose::STANDARD
|
||||
.decode(&cred_secret)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let private = x25519_dalek::StaticSecret::from(privkey_bytes);
|
||||
let public = x25519_dalek::PublicKey::from(&private);
|
||||
let credential = create_mock_peer_manager_credential("tenant-a".to_string(), &private).await;
|
||||
|
||||
connect_peer_manager(admin.clone(), server.clone()).await;
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let server = server.clone();
|
||||
let pubkey = public.as_bytes().to_vec();
|
||||
async move {
|
||||
server
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks_with_options(true)
|
||||
.await
|
||||
.foreign_networks
|
||||
.get("tenant-a")
|
||||
.map(|entry| {
|
||||
entry.trusted_keys.iter().any(|trusted_key| {
|
||||
trusted_key.pubkey == pubkey
|
||||
&& trusted_key.source == TrustedKeySourcePb::OspfCredential as i32
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (client_ret, server_ret) = connect_client_and_server(credential, server.clone()).await;
|
||||
|
||||
assert!(
|
||||
client_ret.is_ok(),
|
||||
"trusted foreign credential client should connect in private mode"
|
||||
);
|
||||
assert!(
|
||||
server_ret.is_ok(),
|
||||
"server should allow trusted foreign credential in private mode: {:?}",
|
||||
server_ret
|
||||
);
|
||||
wait_for_foreign_network_peer_count_at_least(server, "tenant-a", 2).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn private_mode_rejects_untrusted_foreign_credential() {
|
||||
let server = create_mock_peer_manager_secure("public".to_string(), "shared".to_string()).await;
|
||||
let admin = create_mock_peer_manager_secure("tenant-a".to_string(), "shared".to_string()).await;
|
||||
set_private_mode(&server, true);
|
||||
|
||||
let random_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
let unknown_credential =
|
||||
create_mock_peer_manager_credential("tenant-a".to_string(), &random_private).await;
|
||||
|
||||
connect_peer_manager(admin.clone(), server.clone()).await;
|
||||
wait_for_foreign_network(server.clone(), "tenant-a").await;
|
||||
|
||||
let (client_ret, server_ret) =
|
||||
connect_client_and_server(unknown_credential, server.clone()).await;
|
||||
|
||||
let _ = client_ret;
|
||||
assert!(
|
||||
server_ret.is_err(),
|
||||
"server should reject untrusted foreign credential in private mode"
|
||||
);
|
||||
wait_for_condition(
|
||||
|| {
|
||||
let server = server.clone();
|
||||
async move {
|
||||
server
|
||||
.get_foreign_network_manager()
|
||||
.list_foreign_networks()
|
||||
.await
|
||||
.foreign_networks
|
||||
.get("tenant-a")
|
||||
.map(|entry| entry.peers.len() == 1)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_peer_map_retry_backoff_and_evict() {
|
||||
let (s, _r) = create_packet_recv_chan();
|
||||
|
||||
@@ -135,18 +135,17 @@ pub mod instance {
|
||||
}
|
||||
|
||||
pub fn get_loss_rate(&self) -> Option<f64> {
|
||||
let mut ret = 0.0;
|
||||
let p = self.peer.as_ref()?;
|
||||
let default_conn_id = p.default_conn_id.map(|id| id.to_string());
|
||||
let mut ret = None;
|
||||
for conn in p.conns.iter() {
|
||||
if default_conn_id == Some(conn.conn_id.to_string()) {
|
||||
return Some(conn.loss_rate as f64);
|
||||
}
|
||||
|
||||
ret.get_or_insert(conn.loss_rate as f64);
|
||||
ret += conn.loss_rate;
|
||||
}
|
||||
|
||||
ret
|
||||
if ret == 0.0 {
|
||||
None
|
||||
} else {
|
||||
Some(ret as f64)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_tunnel_ipv6(tunnel_info: &super::super::common::TunnelInfo) -> bool {
|
||||
@@ -267,7 +266,6 @@ mod tests {
|
||||
use bytes::Bytes;
|
||||
use prost::Message;
|
||||
|
||||
use super::instance::{PeerConnInfo, PeerInfo, PeerRoutePair};
|
||||
use super::manage::{
|
||||
ListNetworkInstanceRequest, ListNetworkInstanceResponse, WebClientService,
|
||||
WebClientServiceClient, WebClientServiceDescriptor, WebClientServiceMethodDescriptor,
|
||||
@@ -357,56 +355,4 @@ mod tests {
|
||||
.await;
|
||||
assert!(ret.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_route_pair_loss_rate_uses_default_conn() {
|
||||
let default_conn_id = uuid::Uuid::new_v4();
|
||||
let pair = PeerRoutePair {
|
||||
peer: Some(PeerInfo {
|
||||
default_conn_id: Some(default_conn_id.into()),
|
||||
conns: vec![
|
||||
PeerConnInfo {
|
||||
conn_id: uuid::Uuid::new_v4().to_string(),
|
||||
loss_rate: 0.8,
|
||||
..Default::default()
|
||||
},
|
||||
PeerConnInfo {
|
||||
conn_id: default_conn_id.to_string(),
|
||||
loss_rate: 0.4,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(pair
|
||||
.get_loss_rate()
|
||||
.is_some_and(|loss_rate| (loss_rate - 0.4).abs() < 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_route_pair_loss_rate_falls_back_to_first_conn() {
|
||||
let pair = PeerRoutePair {
|
||||
peer: Some(PeerInfo {
|
||||
conns: vec![
|
||||
PeerConnInfo {
|
||||
conn_id: uuid::Uuid::new_v4().to_string(),
|
||||
loss_rate: 0.0,
|
||||
..Default::default()
|
||||
},
|
||||
PeerConnInfo {
|
||||
conn_id: uuid::Uuid::new_v4().to_string(),
|
||||
loss_rate: 0.7,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(pair.get_loss_rate(), Some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ message NetworkConfig {
|
||||
optional string credential_file = 57;
|
||||
optional bool lazy_p2p = 58;
|
||||
optional bool need_p2p = 59;
|
||||
optional uint64 instance_recv_bps_limit = 60;
|
||||
}
|
||||
|
||||
message PortForwardConfig {
|
||||
|
||||
@@ -73,7 +73,6 @@ message FlagsInConfig {
|
||||
|
||||
bool lazy_p2p = 37;
|
||||
bool need_p2p = 38;
|
||||
uint64 instance_recv_bps_limit = 39;
|
||||
}
|
||||
|
||||
message RpcDescriptor {
|
||||
@@ -222,7 +221,6 @@ message PeerFeatureFlag {
|
||||
bool no_relay_quic = 7;
|
||||
bool is_credential_peer = 8;
|
||||
bool need_p2p = 9;
|
||||
bool disable_p2p = 10;
|
||||
}
|
||||
|
||||
enum SocketType {
|
||||
|
||||
@@ -156,14 +156,14 @@ async fn init_three_node_ex_with_inst3<F: Fn(TomlConfigLoader) -> TomlConfigLoad
|
||||
#[cfg(feature = "websocket")]
|
||||
inst1
|
||||
.get_conn_manager()
|
||||
.add_connector(crate::tunnel::websocket::WsTunnelConnector::new(
|
||||
.add_connector(crate::tunnel::websocket::WSTunnelConnector::new(
|
||||
"ws://10.1.1.2:11011".parse().unwrap(),
|
||||
));
|
||||
} else if proto == "wss" {
|
||||
#[cfg(feature = "websocket")]
|
||||
inst1
|
||||
.get_conn_manager()
|
||||
.add_connector(crate::tunnel::websocket::WsTunnelConnector::new(
|
||||
.add_connector(crate::tunnel::websocket::WSTunnelConnector::new(
|
||||
"wss://10.1.1.2:11012".parse().unwrap(),
|
||||
));
|
||||
}
|
||||
@@ -1535,48 +1535,6 @@ pub async fn relay_bps_limit_test(#[values(100, 200, 400, 800)] bps_limit: u64)
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[serial_test::serial]
|
||||
#[tokio::test]
|
||||
pub async fn instance_recv_bps_limit_test(#[values(100, 800)] bps_limit: u64) {
|
||||
let insts = init_three_node_ex(
|
||||
"tcp",
|
||||
|cfg| {
|
||||
if cfg.get_inst_name() == "inst2" {
|
||||
let mut f = cfg.get_flags();
|
||||
f.instance_recv_bps_limit = bps_limit * 1024;
|
||||
cfg.set_flags(f);
|
||||
}
|
||||
cfg
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let tcp_listener = TcpTunnelListener::new("tcp://0.0.0.0:22223".parse().unwrap());
|
||||
let tcp_connector = TcpTunnelConnector::new("tcp://10.144.144.3:22223".parse().unwrap());
|
||||
|
||||
let bps = _tunnel_bench_netns(
|
||||
tcp_listener,
|
||||
tcp_connector,
|
||||
NetNS::new(Some("net_c".into())),
|
||||
NetNS::new(Some("net_a".into())),
|
||||
)
|
||||
.await;
|
||||
|
||||
println!("bps: {}", bps);
|
||||
|
||||
let bps = bps as u64 / 1024;
|
||||
assert!(
|
||||
bps >= bps_limit - 50 && bps <= bps_limit + 50,
|
||||
"bps: {}, bps_limit: {}",
|
||||
bps,
|
||||
bps_limit
|
||||
);
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
async fn assert_try_direct_connect_err<C>(inst: &Instance, connector: C)
|
||||
where
|
||||
C: crate::tunnel::TunnelConnector + std::fmt::Debug,
|
||||
@@ -2565,73 +2523,6 @@ pub async fn need_p2p_overrides_lazy_p2p() {
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn disable_p2p_still_connects_to_need_p2p_peers() {
|
||||
let insts = init_lazy_p2p_three_node_ex("udp", |cfg| {
|
||||
let mut flags = cfg.get_flags();
|
||||
if cfg.get_inst_name() == "inst1" {
|
||||
flags.disable_p2p = true;
|
||||
}
|
||||
if cfg.get_inst_name() == "inst3" {
|
||||
flags.need_p2p = true;
|
||||
}
|
||||
cfg.set_flags(flags);
|
||||
cfg
|
||||
})
|
||||
.await;
|
||||
|
||||
let inst3_peer_id = insts[2].peer_id();
|
||||
wait_route_cost(&insts[0], inst3_peer_id, 2, Duration::from_secs(5)).await;
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
insts[0]
|
||||
.get_peer_manager()
|
||||
.get_peer_map()
|
||||
.has_peer(inst3_peer_id)
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
wait_route_cost(&insts[0], inst3_peer_id, 1, Duration::from_secs(10)).await;
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn ordinary_nodes_do_not_proactively_connect_to_disable_p2p_peers() {
|
||||
let insts = init_lazy_p2p_three_node_ex("udp", |cfg| {
|
||||
if cfg.get_inst_name() == "inst3" {
|
||||
let mut flags = cfg.get_flags();
|
||||
flags.disable_p2p = true;
|
||||
cfg.set_flags(flags);
|
||||
}
|
||||
cfg
|
||||
})
|
||||
.await;
|
||||
|
||||
let inst3_peer_id = insts[2].peer_id();
|
||||
wait_route_cost(&insts[0], inst3_peer_id, 2, Duration::from_secs(5)).await;
|
||||
assert!(
|
||||
ping_test("net_a", "10.144.144.3", None).await,
|
||||
"relay traffic to disable-p2p peers should still succeed"
|
||||
);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
assert!(
|
||||
!insts[0]
|
||||
.get_peer_manager()
|
||||
.get_peer_map()
|
||||
.has_peer(inst3_peer_id),
|
||||
"ordinary nodes should not proactively establish p2p with disable-p2p peers"
|
||||
);
|
||||
wait_route_cost(&insts[0], inst3_peer_id, 2, Duration::from_secs(3)).await;
|
||||
|
||||
drop_insts(insts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn lazy_p2p_warms_up_before_p2p_only_send() {
|
||||
|
||||
@@ -2,28 +2,20 @@ mod netfilter;
|
||||
mod packet;
|
||||
mod stack;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use futures::{Sink, Stream};
|
||||
use network_interface::NetworkInterfaceConfig;
|
||||
use pnet::util::MacAddr;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context as TaskContext, Poll},
|
||||
};
|
||||
use tokio::{io::AsyncReadExt, net::TcpStream, sync::Mutex};
|
||||
use std::net::{IpAddr, Ipv4Addr, UdpSocket};
|
||||
use std::sync::Arc;
|
||||
use std::{net::SocketAddr, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
common::scoped_task::ScopedTask,
|
||||
tunnel::{
|
||||
common::TunnelWrapper,
|
||||
fake_tcp::netfilter::create_tun,
|
||||
packet_def::{ZCPacket, ZCPacketType, PEER_MANAGER_HEADER_SIZE, TCP_TUNNEL_HEADER_SIZE},
|
||||
FromUrl, IpVersion, SinkError, SinkItem, StreamItem, Tunnel, TunnelConnector, TunnelError,
|
||||
TunnelInfo, TunnelListener,
|
||||
},
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
use pnet::datalink;
|
||||
use pnet::util::MacAddr;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::common::scoped_task::ScopedTask;
|
||||
use crate::tunnel::fake_tcp::netfilter::create_tun;
|
||||
use crate::tunnel::{common::TunnelWrapper, Tunnel, TunnelError, TunnelInfo, TunnelListener};
|
||||
|
||||
use futures::Future;
|
||||
|
||||
@@ -42,18 +34,11 @@ impl IpToIfNameCache {
|
||||
|
||||
fn reload_ip_to_ifname(&self) {
|
||||
self.ip_to_ifname.clear();
|
||||
let Ok(interfaces) = network_interface::NetworkInterface::show() else {
|
||||
tracing::warn!("failed to enumerate interfaces when reloading faketcp ip cache");
|
||||
return;
|
||||
};
|
||||
let interfaces = datalink::interfaces();
|
||||
for iface in interfaces {
|
||||
let mac = iface.mac_addr.as_deref().and_then(|mac| {
|
||||
mac.parse::<MacAddr>().map_err(|e| {
|
||||
tracing::debug!(iface = %iface.name, mac, ?e, "failed to parse interface mac")
|
||||
}).ok()
|
||||
});
|
||||
for ip in iface.addr.iter() {
|
||||
self.ip_to_ifname.insert(ip.ip(), (iface.name.clone(), mac));
|
||||
for ip in iface.ips.iter() {
|
||||
self.ip_to_ifname
|
||||
.insert(ip.ip(), (iface.name.clone(), iface.mac));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +200,12 @@ struct AcceptResult {
|
||||
impl TunnelListener for FakeTcpTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
let port = self.addr.port().unwrap_or(0);
|
||||
let bind_addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let bind_addr = crate::tunnel::check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&self.addr,
|
||||
"faketcp",
|
||||
crate::tunnel::IpVersion::Both,
|
||||
)
|
||||
.await?;
|
||||
let os_listener = tokio::net::TcpListener::bind(bind_addr).await?;
|
||||
tracing::info!(port, "FakeTcpTunnelListener listening");
|
||||
self.os_listener = Some(os_listener);
|
||||
@@ -309,9 +299,14 @@ fn get_local_ip_for_destination(destination: IpAddr) -> Option<IpAddr> {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for FakeTcpTunnelConnector {
|
||||
impl crate::tunnel::TunnelConnector for FakeTcpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let remote_addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let remote_addr = crate::tunnel::check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&self.addr,
|
||||
"faketcp",
|
||||
crate::tunnel::IpVersion::Both,
|
||||
)
|
||||
.await?;
|
||||
let local_ip = get_local_ip_for_destination(remote_addr.ip())
|
||||
.ok_or(TunnelError::InternalError("Failed to get local ip".into()))?;
|
||||
|
||||
@@ -385,6 +380,13 @@ impl TunnelConnector for FakeTcpTunnelConnector {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::tunnel::packet_def::{
|
||||
ZCPacket, ZCPacketType, PEER_MANAGER_HEADER_SIZE, TCP_TUNNEL_HEADER_SIZE,
|
||||
};
|
||||
use crate::tunnel::{SinkError, SinkItem, StreamItem};
|
||||
use futures::{Sink, Stream};
|
||||
use std::task::{Context as TaskContext, Poll};
|
||||
|
||||
type RecvFut = Pin<Box<dyn Future<Output = Option<(BytesMut, usize)>> + Send + Sync>>;
|
||||
|
||||
enum FakeTcpStreamState {
|
||||
|
||||
@@ -221,8 +221,7 @@ fn get_or_create_worker(interface_name: &str) -> io::Result<Arc<InterfaceWorker>
|
||||
|
||||
// But creation is rare.
|
||||
// Let's find interface first.
|
||||
let interfaces = std::panic::catch_unwind(datalink::interfaces)
|
||||
.map_err(|_| io::Error::other("failed to enumerate network interfaces: pnet panicked"))?;
|
||||
let interfaces = datalink::interfaces();
|
||||
let interface = interfaces
|
||||
.into_iter()
|
||||
.find(|iface| iface.name == interface_name)
|
||||
|
||||
+52
-140
@@ -1,19 +1,16 @@
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher, hash::Hasher, net::SocketAddr, pin::Pin, sync::Arc,
|
||||
};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
use std::{net::SocketAddr, pin::Pin, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
common::{dns::socket_addrs, error::Error},
|
||||
proto::common::TunnelInfo,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use derive_more::{From, TryInto};
|
||||
use futures::{Sink, Stream};
|
||||
use socket2::Protocol;
|
||||
use std::fmt::Debug;
|
||||
use strum::{Display, EnumString, VariantArray};
|
||||
|
||||
use tokio::time::error::Elapsed;
|
||||
|
||||
use crate::common::dns::socket_addrs;
|
||||
use crate::proto::common::TunnelInfo;
|
||||
|
||||
use self::packet_def::ZCPacket;
|
||||
|
||||
pub mod buf;
|
||||
@@ -26,6 +23,15 @@ pub mod stats;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
|
||||
pub const PROTO_PORT_OFFSET: &[(&str, u16)] = &[
|
||||
("tcp", 0),
|
||||
("udp", 0),
|
||||
("wg", 1),
|
||||
("ws", 1),
|
||||
("wss", 2),
|
||||
("faketcp", 3),
|
||||
];
|
||||
|
||||
#[cfg(feature = "faketcp")]
|
||||
pub mod fake_tcp;
|
||||
|
||||
@@ -187,23 +193,45 @@ pub(crate) trait FromUrl {
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub(crate) async fn check_scheme_and_get_socket_addr<T>(
|
||||
url: &url::Url,
|
||||
scheme: &str,
|
||||
ip_version: IpVersion,
|
||||
) -> Result<T, TunnelError>
|
||||
where
|
||||
T: FromUrl,
|
||||
{
|
||||
if url.scheme() != scheme {
|
||||
return Err(TunnelError::InvalidProtocol(url.scheme().to_string()));
|
||||
}
|
||||
|
||||
T::from_url(url.clone(), ip_version).await
|
||||
}
|
||||
|
||||
fn default_port(scheme: &str) -> Option<u16> {
|
||||
match scheme {
|
||||
"tcp" => Some(11010),
|
||||
"udp" => Some(11010),
|
||||
"ws" => Some(80),
|
||||
"wss" => Some(443),
|
||||
"faketcp" => Some(11013),
|
||||
"quic" => Some(11012),
|
||||
"wg" => Some(11011),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FromUrl for SocketAddr {
|
||||
async fn from_url(url: url::Url, ip_version: IpVersion) -> Result<Self, TunnelError> {
|
||||
let addrs = socket_addrs(&url, || {
|
||||
(&url)
|
||||
.try_into()
|
||||
.ok()
|
||||
.and_then(|s: TunnelScheme| s.try_into().ok())
|
||||
.map(IpScheme::default_port)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
TunnelError::InvalidAddr(format!(
|
||||
"failed to resolve socket addr, url: {}, error: {}",
|
||||
url, e
|
||||
))
|
||||
})?;
|
||||
let addrs = socket_addrs(&url, || default_port(url.scheme()))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
TunnelError::InvalidAddr(format!(
|
||||
"failed to resolve socket addr, url: {}, error: {}",
|
||||
url, e
|
||||
))
|
||||
})?;
|
||||
tracing::debug!(?addrs, ?ip_version, ?url, "convert url to socket addrs");
|
||||
let addrs = addrs
|
||||
.into_iter()
|
||||
@@ -277,119 +305,3 @@ pub fn generate_digest_from_str(str1: &str, str2: &str, digest: &mut [u8]) {
|
||||
hasher.write(&digest[..(i + 1) * 8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct IpSchemeAttributes {
|
||||
protocol: Protocol,
|
||||
port_offset: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Display, EnumString, VariantArray)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum IpScheme {
|
||||
Tcp,
|
||||
Udp,
|
||||
#[cfg(feature = "wireguard")]
|
||||
Wg,
|
||||
#[cfg(feature = "quic")]
|
||||
Quic,
|
||||
#[cfg(feature = "websocket")]
|
||||
Ws,
|
||||
#[cfg(feature = "websocket")]
|
||||
Wss,
|
||||
#[cfg(feature = "faketcp")]
|
||||
FakeTcp,
|
||||
}
|
||||
|
||||
impl IpScheme {
|
||||
const fn attributes(self) -> IpSchemeAttributes {
|
||||
let (protocol, port_offset) = match self {
|
||||
Self::Tcp => (Protocol::TCP, 0),
|
||||
Self::Udp => (Protocol::UDP, 0),
|
||||
#[cfg(feature = "wireguard")]
|
||||
Self::Wg => (Protocol::UDP, 1),
|
||||
#[cfg(feature = "quic")]
|
||||
Self::Quic => (Protocol::UDP, 2),
|
||||
#[cfg(feature = "websocket")]
|
||||
Self::Ws => (Protocol::TCP, 1),
|
||||
#[cfg(feature = "websocket")]
|
||||
Self::Wss => (Protocol::TCP, 2),
|
||||
#[cfg(feature = "faketcp")]
|
||||
Self::FakeTcp => (Protocol::TCP, 3),
|
||||
};
|
||||
IpSchemeAttributes {
|
||||
protocol,
|
||||
port_offset,
|
||||
}
|
||||
}
|
||||
pub const fn protocol(self) -> Protocol {
|
||||
self.attributes().protocol
|
||||
}
|
||||
|
||||
pub const fn port_offset(self) -> u16 {
|
||||
self.attributes().port_offset
|
||||
}
|
||||
|
||||
pub const fn default_port(self) -> u16 {
|
||||
match self {
|
||||
#[cfg(feature = "websocket")]
|
||||
Self::Ws => 80,
|
||||
#[cfg(feature = "websocket")]
|
||||
Self::Wss => 443,
|
||||
_ => 11010 + self.port_offset(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, EnumString, From, TryInto)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum TunnelScheme {
|
||||
#[strum(disabled)]
|
||||
Ip(IpScheme),
|
||||
#[cfg(unix)]
|
||||
Unix,
|
||||
// Only for connector
|
||||
Http,
|
||||
Https,
|
||||
Ring,
|
||||
Txt,
|
||||
Srv,
|
||||
}
|
||||
|
||||
impl TryFrom<&url::Url> for TunnelScheme {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &url::Url) -> Result<Self, Self::Error> {
|
||||
let scheme = value.scheme();
|
||||
scheme.parse().or_else(|_| {
|
||||
Ok(TunnelScheme::Ip(
|
||||
scheme
|
||||
.parse()
|
||||
.map_err(|_| Error::InvalidUrl(value.to_string()))?,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! __matches_scheme__ {
|
||||
($url:expr, $( $pattern:pat_param )|+ ) => {
|
||||
matches!($crate::tunnel::TunnelScheme::try_from(($url).as_ref()), Ok($( $pattern )|+))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use __matches_scheme__ as matches_scheme;
|
||||
|
||||
pub fn get_protocol_by_url(l: &url::Url) -> Result<Protocol, Error> {
|
||||
let TunnelScheme::Ip(scheme) = l.try_into()? else {
|
||||
return Err(Error::InvalidUrl(l.to_string()));
|
||||
};
|
||||
Ok(scheme.protocol())
|
||||
}
|
||||
|
||||
macro_rules! __matches_protocol__ {
|
||||
($url:expr, $( $pattern:pat_param )|+ ) => {
|
||||
matches!($crate::tunnel::get_protocol_by_url($url), Ok($( $pattern )|+))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use __matches_protocol__ as matches_protocol;
|
||||
|
||||
+34
-26
@@ -8,16 +8,20 @@ use std::{
|
||||
|
||||
use crate::tunnel::{
|
||||
common::{setup_sokcet2, FramedReader, FramedWriter, TunnelWrapper},
|
||||
FromUrl, TunnelInfo,
|
||||
TunnelInfo,
|
||||
};
|
||||
use anyhow::Context;
|
||||
|
||||
use super::{IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener};
|
||||
use quinn::{
|
||||
congestion::BbrConfig, udp::RecvMeta, AsyncUdpSocket, ClientConfig, Connection, Endpoint,
|
||||
EndpointConfig, ServerConfig, TransportConfig, UdpPoller,
|
||||
};
|
||||
|
||||
use super::{
|
||||
check_scheme_and_get_socket_addr, IpVersion, Tunnel, TunnelConnector, TunnelError,
|
||||
TunnelListener,
|
||||
};
|
||||
|
||||
pub fn transport_config() -> Arc<TransportConfig> {
|
||||
let mut config = TransportConfig::default();
|
||||
|
||||
@@ -141,14 +145,14 @@ impl Drop for ConnWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuicTunnelListener {
|
||||
pub struct QUICTunnelListener {
|
||||
addr: url::Url,
|
||||
endpoint: Option<Endpoint>,
|
||||
}
|
||||
|
||||
impl QuicTunnelListener {
|
||||
impl QUICTunnelListener {
|
||||
pub fn new(addr: url::Url) -> Self {
|
||||
QuicTunnelListener {
|
||||
QUICTunnelListener {
|
||||
addr,
|
||||
endpoint: None,
|
||||
}
|
||||
@@ -186,9 +190,11 @@ impl QuicTunnelListener {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelListener for QuicTunnelListener {
|
||||
impl TunnelListener for QUICTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "quic", IpVersion::Both)
|
||||
.await?;
|
||||
let endpoint = make_server_endpoint(addr)
|
||||
.map_err(|e| anyhow::anyhow!("make server endpoint error: {:?}", e))?;
|
||||
self.endpoint = Some(endpoint);
|
||||
@@ -217,15 +223,15 @@ impl TunnelListener for QuicTunnelListener {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuicTunnelConnector {
|
||||
pub struct QUICTunnelConnector {
|
||||
addr: url::Url,
|
||||
endpoint: Option<Endpoint>,
|
||||
ip_version: IpVersion,
|
||||
}
|
||||
|
||||
impl QuicTunnelConnector {
|
||||
impl QUICTunnelConnector {
|
||||
pub fn new(addr: url::Url) -> Self {
|
||||
QuicTunnelConnector {
|
||||
QUICTunnelConnector {
|
||||
addr,
|
||||
endpoint: None,
|
||||
ip_version: IpVersion::Both,
|
||||
@@ -234,9 +240,11 @@ impl QuicTunnelConnector {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for QuicTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
impl TunnelConnector for QUICTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "quic", self.ip_version)
|
||||
.await?;
|
||||
if addr.port() == 0 {
|
||||
return Err(TunnelError::InvalidAddr(format!(
|
||||
"invalid remote QUIC port 0 in url: {} (port 0 is not a valid QUIC port)",
|
||||
@@ -310,36 +318,36 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn quic_pingpong() {
|
||||
let listener = QuicTunnelListener::new("quic://0.0.0.0:21011".parse().unwrap());
|
||||
let connector = QuicTunnelConnector::new("quic://127.0.0.1:21011".parse().unwrap());
|
||||
let listener = QUICTunnelListener::new("quic://0.0.0.0:21011".parse().unwrap());
|
||||
let connector = QUICTunnelConnector::new("quic://127.0.0.1:21011".parse().unwrap());
|
||||
_tunnel_pingpong(listener, connector).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn quic_bench() {
|
||||
let listener = QuicTunnelListener::new("quic://0.0.0.0:21012".parse().unwrap());
|
||||
let connector = QuicTunnelConnector::new("quic://127.0.0.1:21012".parse().unwrap());
|
||||
let listener = QUICTunnelListener::new("quic://0.0.0.0:21012".parse().unwrap());
|
||||
let connector = QUICTunnelConnector::new("quic://127.0.0.1:21012".parse().unwrap());
|
||||
_tunnel_bench(listener, connector).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ipv6_pingpong() {
|
||||
let listener = QuicTunnelListener::new("quic://[::1]:31015".parse().unwrap());
|
||||
let connector = QuicTunnelConnector::new("quic://[::1]:31015".parse().unwrap());
|
||||
let listener = QUICTunnelListener::new("quic://[::1]:31015".parse().unwrap());
|
||||
let connector = QUICTunnelConnector::new("quic://[::1]:31015".parse().unwrap());
|
||||
_tunnel_pingpong(listener, connector).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ipv6_domain_pingpong() {
|
||||
let listener = QuicTunnelListener::new("quic://[::1]:31016".parse().unwrap());
|
||||
let listener = QUICTunnelListener::new("quic://[::1]:31016".parse().unwrap());
|
||||
let mut connector =
|
||||
QuicTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap());
|
||||
QUICTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap());
|
||||
connector.set_ip_version(IpVersion::V6);
|
||||
_tunnel_pingpong(listener, connector).await;
|
||||
|
||||
let listener = QuicTunnelListener::new("quic://127.0.0.1:31016".parse().unwrap());
|
||||
let listener = QUICTunnelListener::new("quic://127.0.0.1:31016".parse().unwrap());
|
||||
let mut connector =
|
||||
QuicTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap());
|
||||
QUICTunnelConnector::new("quic://test.easytier.top:31016".parse().unwrap());
|
||||
connector.set_ip_version(IpVersion::V4);
|
||||
_tunnel_pingpong(listener, connector).await;
|
||||
}
|
||||
@@ -347,13 +355,13 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_alloc_port() {
|
||||
// v4
|
||||
let mut listener = QuicTunnelListener::new("quic://0.0.0.0:0".parse().unwrap());
|
||||
let mut listener = QUICTunnelListener::new("quic://0.0.0.0:0".parse().unwrap());
|
||||
listener.listen().await.unwrap();
|
||||
let port = listener.local_url().port().unwrap();
|
||||
assert!(port > 0);
|
||||
|
||||
// v6
|
||||
let mut listener = QuicTunnelListener::new("quic://[::]:0".parse().unwrap());
|
||||
let mut listener = QUICTunnelListener::new("quic://[::]:0".parse().unwrap());
|
||||
listener.listen().await.unwrap();
|
||||
let port = listener.local_url().port().unwrap();
|
||||
assert!(port > 0);
|
||||
@@ -361,7 +369,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn quic_connector_reject_port_zero() {
|
||||
let mut connector = QuicTunnelConnector::new("quic://127.0.0.1:0".parse().unwrap());
|
||||
let mut connector = QUICTunnelConnector::new("quic://127.0.0.1:0".parse().unwrap());
|
||||
let err = connector.connect().await.unwrap_err().to_string();
|
||||
assert!(err.contains("port 0"), "unexpected error: {}", err);
|
||||
}
|
||||
|
||||
+28
-17
@@ -1,5 +1,3 @@
|
||||
use async_ringbuf::{traits::*, AsyncHeapCons, AsyncHeapProd, AsyncHeapRb};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
@@ -7,6 +5,9 @@ use std::{
|
||||
task::{ready, Poll},
|
||||
};
|
||||
|
||||
use async_ringbuf::{traits::*, AsyncHeapCons, AsyncHeapProd, AsyncHeapRb};
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -15,15 +16,15 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::tunnel::{FromUrl, IpVersion, SinkError, SinkItem};
|
||||
use crate::tunnel::{SinkError, SinkItem};
|
||||
|
||||
use super::{
|
||||
build_url_from_socket_addr, common::TunnelWrapper, StreamItem, Tunnel, TunnelConnector,
|
||||
TunnelError, TunnelInfo, TunnelListener,
|
||||
build_url_from_socket_addr, check_scheme_and_get_socket_addr, common::TunnelWrapper,
|
||||
StreamItem, Tunnel, TunnelConnector, TunnelError, TunnelInfo, TunnelListener,
|
||||
};
|
||||
|
||||
pub static RING_TUNNEL_CAP: usize = 128;
|
||||
static RING_TUNNEL_RESERVED_CAP: usize = 4;
|
||||
static RING_TUNNEL_RESERVERD_CAP: usize = 4;
|
||||
|
||||
type RingLock = parking_lot::Mutex<()>;
|
||||
|
||||
@@ -43,7 +44,7 @@ impl RingTunnel {
|
||||
|
||||
pub fn new(cap: usize) -> Self {
|
||||
let id = Uuid::new_v4();
|
||||
let ring_impl = AsyncHeapRb::new(std::cmp::max(RING_TUNNEL_RESERVED_CAP * 2, cap));
|
||||
let ring_impl = AsyncHeapRb::new(std::cmp::max(RING_TUNNEL_RESERVERD_CAP * 2, cap));
|
||||
let (ring_prod_impl, ring_cons_impl) = ring_impl.split();
|
||||
Self {
|
||||
id,
|
||||
@@ -119,7 +120,7 @@ impl RingSink {
|
||||
|
||||
pub fn try_send(&mut self, item: RingItem) -> Result<(), RingItem> {
|
||||
let base = self.ring_prod_impl.base();
|
||||
if base.occupied_len() >= base.capacity().get() - RING_TUNNEL_RESERVED_CAP {
|
||||
if base.occupied_len() >= base.capacity().get() - RING_TUNNEL_RESERVERD_CAP {
|
||||
return Err(item);
|
||||
}
|
||||
self.ring_prod_impl.try_push(item)
|
||||
@@ -187,7 +188,7 @@ static CONNECTION_MAP: Lazy<Arc<std::sync::Mutex<ConnectionMap>>> =
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RingTunnelListener {
|
||||
listener_addr: url::Url,
|
||||
listerner_addr: url::Url,
|
||||
conn_sender: UnboundedSender<Arc<Connection>>,
|
||||
conn_receiver: UnboundedReceiver<Arc<Connection>>,
|
||||
|
||||
@@ -198,7 +199,7 @@ impl RingTunnelListener {
|
||||
pub fn new(key: url::Url) -> Self {
|
||||
let (conn_sender, conn_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
RingTunnelListener {
|
||||
listener_addr: key,
|
||||
listerner_addr: key,
|
||||
conn_sender,
|
||||
conn_receiver,
|
||||
key_in_conn_map: None,
|
||||
@@ -231,15 +232,20 @@ fn get_tunnel_for_server(conn: Arc<Connection>) -> impl Tunnel {
|
||||
}
|
||||
|
||||
impl RingTunnelListener {
|
||||
async fn get_addr(&self) -> Result<Uuid, TunnelError> {
|
||||
Uuid::from_url(self.listener_addr.clone(), IpVersion::Both).await
|
||||
async fn get_addr(&self) -> Result<uuid::Uuid, TunnelError> {
|
||||
check_scheme_and_get_socket_addr::<Uuid>(
|
||||
&self.listerner_addr,
|
||||
"ring",
|
||||
super::IpVersion::Both,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TunnelListener for RingTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
tracing::info!("listen new conn of key: {}", self.listener_addr);
|
||||
tracing::info!("listen new conn of key: {}", self.listerner_addr);
|
||||
let addr = self.get_addr().await?;
|
||||
CONNECTION_MAP
|
||||
.lock()
|
||||
@@ -250,11 +256,11 @@ impl TunnelListener for RingTunnelListener {
|
||||
}
|
||||
|
||||
async fn accept(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
tracing::info!("waiting accept new conn of key: {}", self.listener_addr);
|
||||
tracing::info!("waiting accept new conn of key: {}", self.listerner_addr);
|
||||
let my_addr = self.get_addr().await?;
|
||||
if let Some(conn) = self.conn_receiver.recv().await {
|
||||
if conn.server.id == my_addr {
|
||||
tracing::info!("accept new conn of key: {}", self.listener_addr);
|
||||
tracing::info!("accept new conn of key: {}", self.listerner_addr);
|
||||
return Ok(Box::new(get_tunnel_for_server(conn)));
|
||||
} else {
|
||||
tracing::error!(?conn.server.id, ?my_addr, "got new conn with wrong id");
|
||||
@@ -270,7 +276,7 @@ impl TunnelListener for RingTunnelListener {
|
||||
}
|
||||
|
||||
fn local_url(&self) -> url::Url {
|
||||
self.listener_addr.clone()
|
||||
self.listerner_addr.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +301,12 @@ impl RingTunnelConnector {
|
||||
#[async_trait]
|
||||
impl TunnelConnector for RingTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
let remote_addr = Uuid::from_url(self.remote_addr.clone(), IpVersion::Both).await?;
|
||||
let remote_addr = check_scheme_and_get_socket_addr::<Uuid>(
|
||||
&self.remote_addr,
|
||||
"ring",
|
||||
super::IpVersion::Both,
|
||||
)
|
||||
.await?;
|
||||
let entry = CONNECTION_MAP
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use super::{FromUrl, TunnelInfo};
|
||||
use crate::tunnel::common::setup_sokcet2;
|
||||
use async_trait::async_trait;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use tokio::net::{TcpListener, TcpSocket, TcpStream};
|
||||
|
||||
use super::TunnelInfo;
|
||||
use crate::tunnel::common::setup_sokcet2;
|
||||
|
||||
use super::{
|
||||
check_scheme_and_get_socket_addr,
|
||||
common::{wait_for_connect_futures, FramedReader, FramedWriter, TunnelWrapper},
|
||||
IpVersion, Tunnel, TunnelError, TunnelListener,
|
||||
};
|
||||
@@ -56,7 +58,9 @@ impl TcpTunnelListener {
|
||||
impl TunnelListener for TcpTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
self.listener = None;
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "tcp", IpVersion::Both)
|
||||
.await?;
|
||||
|
||||
let socket2_socket = socket2::Socket::new(
|
||||
socket2::Domain::for_address(addr),
|
||||
@@ -185,8 +189,10 @@ impl TcpTunnelConnector {
|
||||
|
||||
#[async_trait]
|
||||
impl super::TunnelConnector for TcpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "tcp", self.ip_version)
|
||||
.await?;
|
||||
if self.bind_addrs.is_empty() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
|
||||
+31
-18
@@ -21,13 +21,7 @@ use tokio::{
|
||||
|
||||
use tracing::{instrument, Instrument};
|
||||
|
||||
use super::{
|
||||
common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures},
|
||||
packet_def::{UDPTunnelHeader, V6HolePunchPacket, UDP_TUNNEL_HEADER_SIZE},
|
||||
ring::{RingSink, RingStream},
|
||||
FromUrl, IpVersion, Tunnel, TunnelConnCounter, TunnelError, TunnelInfo, TunnelListener,
|
||||
TunnelUrl,
|
||||
};
|
||||
use super::{packet_def::V6HolePunchPacket, TunnelInfo};
|
||||
use crate::{
|
||||
common::{join_joinset_background, scoped_task::ScopedTask, shrink_dashmap},
|
||||
tunnel::{
|
||||
@@ -38,6 +32,13 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures},
|
||||
packet_def::{UDPTunnelHeader, UDP_TUNNEL_HEADER_SIZE},
|
||||
ring::{RingSink, RingStream},
|
||||
IpVersion, Tunnel, TunnelConnCounter, TunnelError, TunnelListener, TunnelUrl,
|
||||
};
|
||||
|
||||
pub const UDP_DATA_MTU: usize = 2000;
|
||||
|
||||
type UdpCloseEventSender = UnboundedSender<(SocketAddr, Option<TunnelError>)>;
|
||||
@@ -148,11 +149,11 @@ async fn respond_stun_packet(
|
||||
req_buf: Vec<u8>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
use crate::common::stun_codec_ext::*;
|
||||
use bytecodec::{DecodeExt as _, EncodeExt as _};
|
||||
use stun_codec::{
|
||||
rfc5389::{attributes::XorMappedAddress, methods::BINDING},
|
||||
Message, MessageClass, MessageDecoder, MessageEncoder,
|
||||
};
|
||||
use bytecodec::DecodeExt as _;
|
||||
use bytecodec::EncodeExt as _;
|
||||
use stun_codec::rfc5389::attributes::XorMappedAddress;
|
||||
use stun_codec::rfc5389::methods::BINDING;
|
||||
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
||||
|
||||
let mut decoder = MessageDecoder::<Attribute>::new();
|
||||
let req_msg = decoder
|
||||
@@ -531,8 +532,13 @@ impl UdpTunnelListener {
|
||||
|
||||
#[async_trait]
|
||||
impl TunnelListener for UdpTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
async fn listen(&mut self) -> Result<(), super::TunnelError> {
|
||||
let addr = super::check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&self.addr,
|
||||
"udp",
|
||||
IpVersion::Both,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let socket2_socket = socket2::Socket::new(
|
||||
socket2::Domain::for_address(addr),
|
||||
@@ -845,8 +851,13 @@ impl UdpTunnelConnector {
|
||||
|
||||
#[async_trait]
|
||||
impl super::TunnelConnector for UdpTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
async fn connect(&mut self) -> Result<Box<dyn super::Tunnel>, super::TunnelError> {
|
||||
let addr = super::check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&self.addr,
|
||||
"udp",
|
||||
self.ip_version,
|
||||
)
|
||||
.await?;
|
||||
if self.bind_addrs.is_empty() || addr.is_ipv6() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
} else {
|
||||
@@ -878,6 +889,7 @@ mod tests {
|
||||
use crate::{
|
||||
common::global_ctx::tests::get_mock_global_ctx,
|
||||
tunnel::{
|
||||
check_scheme_and_get_socket_addr,
|
||||
common::{
|
||||
get_interface_name_by_ip,
|
||||
tests::{_tunnel_bench, _tunnel_echo_server, _tunnel_pingpong, wait_for_condition},
|
||||
@@ -1022,8 +1034,9 @@ mod tests {
|
||||
|
||||
for ip in ips {
|
||||
println!("bind to ip: {}, {:?}", ip, bind_dev);
|
||||
let addr = SocketAddr::from_url(
|
||||
format!("udp://{}:11111", ip).parse().unwrap(),
|
||||
let addr = check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&format!("udp://{}:11111", ip).parse().unwrap(),
|
||||
"udp",
|
||||
IpVersion::Both,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context;
|
||||
use bytes::BytesMut;
|
||||
use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpSocket, TcpStream},
|
||||
time::timeout,
|
||||
};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_websockets::{ClientBuilder, Limits, MaybeTlsStream, Message};
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
use super::TunnelInfo;
|
||||
use crate::tunnel::insecure_tls::get_insecure_tls_client_config;
|
||||
|
||||
use super::{
|
||||
common::{setup_sokcet2, wait_for_connect_futures, TunnelWrapper},
|
||||
insecure_tls::{get_insecure_tls_cert, init_crypto_provider},
|
||||
packet_def::{ZCPacket, ZCPacketType},
|
||||
FromUrl, IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener,
|
||||
};
|
||||
use crate::{proto::common::TunnelInfo, tunnel::insecure_tls::get_insecure_tls_client_config};
|
||||
use anyhow::Context;
|
||||
use bytes::BytesMut;
|
||||
use forwarded_header_value::ForwardedHeaderValue;
|
||||
use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
|
||||
use pnet::ipnetwork::IpNetwork;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpSocket, TcpStream},
|
||||
time::timeout,
|
||||
};
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_util::either::Either;
|
||||
use tokio_websockets::{ClientBuilder, Limits, MaybeTlsStream, Message, ServerBuilder};
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
fn is_wss(addr: &url::Url) -> Result<bool, TunnelError> {
|
||||
match addr.scheme() {
|
||||
@@ -62,104 +59,67 @@ async fn map_from_ws_message(
|
||||
)))
|
||||
}
|
||||
|
||||
static TRUSTED_PROXIES: LazyLock<Vec<IpNetwork>> = LazyLock::new(|| {
|
||||
[
|
||||
"127.0.0.0/8", // IPV4 Loopback
|
||||
"10.0.0.0/8", // IPV4 Private Networks
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"::1/128", // IPV6 Loopback
|
||||
"fc00::/7", // IPV6 Private network
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| s.parse().unwrap())
|
||||
.collect()
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WsTunnelListener {
|
||||
pub struct WSTunnelListener {
|
||||
addr: url::Url,
|
||||
listener: Option<TcpListener>,
|
||||
}
|
||||
|
||||
impl WsTunnelListener {
|
||||
impl WSTunnelListener {
|
||||
pub fn new(addr: url::Url) -> Self {
|
||||
WsTunnelListener {
|
||||
WSTunnelListener {
|
||||
addr,
|
||||
listener: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_accept(&self, stream: TcpStream) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let peer_addr = stream.peer_addr()?;
|
||||
let mut remote_addr =
|
||||
super::build_url_from_socket_addr(&peer_addr.to_string(), self.addr.scheme());
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: self.addr.scheme().to_owned(),
|
||||
local_addr: Some(self.local_url().into()),
|
||||
remote_addr: Some(
|
||||
super::build_url_from_socket_addr(
|
||||
&stream.peer_addr()?.to_string(),
|
||||
self.addr.scheme().to_string().as_str(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
|
||||
let stream = if is_wss(&self.addr)? {
|
||||
let server_bulder = tokio_websockets::ServerBuilder::new().limits(Limits::unlimited());
|
||||
|
||||
let ret: Box<dyn Tunnel> = if is_wss(&self.addr)? {
|
||||
init_crypto_provider();
|
||||
let (certs, key) = get_insecure_tls_cert();
|
||||
let config = rustls::ServerConfig::builder()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.with_context(|| "Failed to create server config")?;
|
||||
let acceptor = TlsAcceptor::from(Arc::new(config));
|
||||
|
||||
let stream = TlsAcceptor::from(Arc::new(config)).accept(stream).await?;
|
||||
Either::Left(stream)
|
||||
let stream = acceptor.accept(stream).await?;
|
||||
let (write, read) = server_bulder.accept(stream).await?.split();
|
||||
|
||||
Box::new(TunnelWrapper::new(
|
||||
read.filter_map(map_from_ws_message),
|
||||
write.with(sink_from_zc_packet),
|
||||
Some(info),
|
||||
))
|
||||
} else {
|
||||
Either::Right(stream)
|
||||
let (write, read) = server_bulder.accept(stream).await?.split();
|
||||
Box::new(TunnelWrapper::new(
|
||||
read.filter_map(map_from_ws_message),
|
||||
write.with(sink_from_zc_packet),
|
||||
Some(info),
|
||||
))
|
||||
};
|
||||
|
||||
let (request, stream) = ServerBuilder::new()
|
||||
.limits(Limits::unlimited())
|
||||
.accept(stream)
|
||||
.await?;
|
||||
|
||||
if TRUSTED_PROXIES
|
||||
.iter()
|
||||
.any(|net| net.contains(peer_addr.ip()))
|
||||
{
|
||||
if let Some(forwarded) = request
|
||||
.headers()
|
||||
.get("Forwarded")
|
||||
.and_then(|f| f.to_str().ok())
|
||||
.and_then(|f| ForwardedHeaderValue::from_forwarded(f).ok())
|
||||
.or_else(|| {
|
||||
request
|
||||
.headers()
|
||||
.get("X-Forwarded-For")
|
||||
.and_then(|f| f.to_str().ok())
|
||||
.and_then(|f| ForwardedHeaderValue::from_x_forwarded_for(f).ok())
|
||||
})
|
||||
{
|
||||
if let Some(ip) = forwarded.remotest_forwarded_for_ip() {
|
||||
remote_addr.set_host(Some(&ip.to_string())).map_err(|_| {
|
||||
TunnelError::InvalidAddr(format!("invalid forwarded ip {}", ip))
|
||||
})?;
|
||||
remote_addr
|
||||
.query_pairs_mut()
|
||||
.append_pair("proxy", &peer_addr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (write, read) = stream.split();
|
||||
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: self.addr.scheme().to_owned(),
|
||||
local_addr: Some(self.local_url().into()),
|
||||
remote_addr: Some(remote_addr.into()),
|
||||
};
|
||||
|
||||
Ok(Box::new(TunnelWrapper::new(
|
||||
read.filter_map(map_from_ws_message),
|
||||
write.with(sink_from_zc_packet),
|
||||
Some(info),
|
||||
)))
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelListener for WsTunnelListener {
|
||||
impl TunnelListener for WSTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
let socket2_socket = socket2::Socket::new(
|
||||
@@ -199,16 +159,16 @@ impl TunnelListener for WsTunnelListener {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WsTunnelConnector {
|
||||
pub struct WSTunnelConnector {
|
||||
addr: url::Url,
|
||||
ip_version: IpVersion,
|
||||
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
}
|
||||
|
||||
impl WsTunnelConnector {
|
||||
impl WSTunnelConnector {
|
||||
pub fn new(addr: url::Url) -> Self {
|
||||
WsTunnelConnector {
|
||||
WSTunnelConnector {
|
||||
addr,
|
||||
ip_version: IpVersion::Both,
|
||||
|
||||
@@ -307,8 +267,8 @@ impl WsTunnelConnector {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TunnelConnector for WsTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
impl TunnelConnector for WSTunnelConnector {
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, super::TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
if self.bind_addrs.is_empty() || addr.is_ipv6() {
|
||||
self.connect_with_default_bind(addr).await
|
||||
@@ -332,17 +292,17 @@ impl TunnelConnector for WsTunnelConnector {
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::tunnel::common::tests::_tunnel_pingpong;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use crate::tunnel::websocket::{WSTunnelConnector, WSTunnelListener};
|
||||
use crate::tunnel::{TunnelConnector, TunnelListener};
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn ws_pingpong(#[values("ws", "wss")] proto: &str) {
|
||||
let listener = WsTunnelListener::new(format!("{}://0.0.0.0:25556", proto).parse().unwrap());
|
||||
let listener = WSTunnelListener::new(format!("{}://0.0.0.0:25556", proto).parse().unwrap());
|
||||
let connector =
|
||||
WsTunnelConnector::new(format!("{}://127.0.0.1:25556", proto).parse().unwrap());
|
||||
WSTunnelConnector::new(format!("{}://127.0.0.1:25556", proto).parse().unwrap());
|
||||
_tunnel_pingpong(listener, connector).await
|
||||
}
|
||||
|
||||
@@ -350,9 +310,9 @@ pub mod tests {
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn ws_pingpong_bind(#[values("ws", "wss")] proto: &str) {
|
||||
let listener = WsTunnelListener::new(format!("{}://0.0.0.0:25557", proto).parse().unwrap());
|
||||
let listener = WSTunnelListener::new(format!("{}://0.0.0.0:25557", proto).parse().unwrap());
|
||||
let mut connector =
|
||||
WsTunnelConnector::new(format!("{}://127.0.0.1:25557", proto).parse().unwrap());
|
||||
WSTunnelConnector::new(format!("{}://127.0.0.1:25557", proto).parse().unwrap());
|
||||
connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]);
|
||||
_tunnel_pingpong(listener, connector).await
|
||||
}
|
||||
@@ -371,73 +331,18 @@ pub mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn ws_accept_wss() {
|
||||
let mut listener = WsTunnelListener::new("wss://0.0.0.0:25558".parse().unwrap());
|
||||
let mut listener = WSTunnelListener::new("wss://0.0.0.0:25558".parse().unwrap());
|
||||
listener.listen().await.unwrap();
|
||||
let j = tokio::spawn(async move {
|
||||
let _ = listener.accept().await;
|
||||
});
|
||||
|
||||
let mut connector = WsTunnelConnector::new("ws://127.0.0.1:25558".parse().unwrap());
|
||||
let mut connector = WSTunnelConnector::new("ws://127.0.0.1:25558".parse().unwrap());
|
||||
connector.connect().await.unwrap_err();
|
||||
|
||||
let mut connector = WsTunnelConnector::new("wss://127.0.0.1:25558".parse().unwrap());
|
||||
let mut connector = WSTunnelConnector::new("wss://127.0.0.1:25558".parse().unwrap());
|
||||
connector.connect().await.unwrap();
|
||||
|
||||
j.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ws_forwarded() {
|
||||
let mut listener = WsTunnelListener::new("ws://127.0.0.1:25559".parse().unwrap());
|
||||
listener.listen().await.unwrap();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let tunnel = listener.accept().await.unwrap();
|
||||
|
||||
let remote_addr = tunnel
|
||||
.info()
|
||||
.unwrap()
|
||||
.remote_addr
|
||||
.unwrap()
|
||||
.url
|
||||
.parse::<url::Url>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(remote_addr.host_str().unwrap(), "203.0.113.5");
|
||||
let proxy_addr = remote_addr
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "proxy")
|
||||
.map(|(_, v)| v.into_owned())
|
||||
.unwrap();
|
||||
assert_eq!(proxy_addr, "127.0.0.1:25560");
|
||||
|
||||
tunnel
|
||||
});
|
||||
|
||||
let socket = TcpSocket::new_v4().unwrap();
|
||||
socket.bind("127.0.0.1:25560".parse().unwrap()).unwrap();
|
||||
let mut stream = socket
|
||||
.connect("127.0.0.1:25559".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let handshake = "GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:25559\r\n\
|
||||
Upgrade: websocket\r\n\
|
||||
Connection: Upgrade\r\n\
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
X-Forwarded-For: 203.0.113.5, 192.168.1.1\r\n\
|
||||
\r\n";
|
||||
|
||||
stream.write_all(handshake.as_bytes()).await.unwrap();
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let bytes_read = stream.read(&mut buf).await.unwrap();
|
||||
let response = String::from_utf8_lossy(&buf[..bytes_read]);
|
||||
|
||||
assert!(response.contains("101 Switching Protocols"));
|
||||
|
||||
let _tunnel = server_task.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,7 @@ use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
|
||||
use rand::RngCore;
|
||||
use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet};
|
||||
|
||||
use super::{
|
||||
common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures},
|
||||
generate_digest_from_str,
|
||||
packet_def::{ZCPacketType, PEER_MANAGER_HEADER_SIZE},
|
||||
ring::create_ring_tunnel_pair,
|
||||
FromUrl, IpVersion, Tunnel, TunnelError, TunnelInfo, TunnelListener, TunnelUrl, ZCPacketSink,
|
||||
ZCPacketStream,
|
||||
};
|
||||
use super::TunnelInfo;
|
||||
use crate::{
|
||||
common::shrink_dashmap,
|
||||
tunnel::{
|
||||
@@ -37,6 +30,15 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
check_scheme_and_get_socket_addr,
|
||||
common::{setup_sokcet2, setup_sokcet2_ext, wait_for_connect_futures},
|
||||
generate_digest_from_str,
|
||||
packet_def::{ZCPacketType, PEER_MANAGER_HEADER_SIZE},
|
||||
ring::create_ring_tunnel_pair,
|
||||
IpVersion, Tunnel, TunnelError, TunnelListener, TunnelUrl, ZCPacketSink, ZCPacketStream,
|
||||
};
|
||||
|
||||
const MAX_PACKET: usize = 2048;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -200,10 +202,7 @@ impl WgPeerData {
|
||||
match self.udp.send_to(packet, self.endpoint).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}",
|
||||
e
|
||||
);
|
||||
tracing::error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -215,10 +214,7 @@ impl WgPeerData {
|
||||
match self.udp.send_to(packet, self.endpoint).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}",
|
||||
e
|
||||
);
|
||||
tracing::error!("Failed to send decapsulation-instructed packet to WireGuard endpoint: {:?}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -554,8 +550,10 @@ impl WgTunnelListener {
|
||||
|
||||
#[async_trait]
|
||||
impl TunnelListener for WgTunnelListener {
|
||||
async fn listen(&mut self) -> Result<(), TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), IpVersion::Both).await?;
|
||||
async fn listen(&mut self) -> Result<(), super::TunnelError> {
|
||||
let addr =
|
||||
check_scheme_and_get_socket_addr::<SocketAddr>(&self.addr, "wg", IpVersion::Both)
|
||||
.await?;
|
||||
let socket2_socket = socket2::Socket::new(
|
||||
socket2::Domain::for_address(addr),
|
||||
socket2::Type::DGRAM,
|
||||
@@ -707,8 +705,13 @@ impl WgTunnelConnector {
|
||||
#[async_trait]
|
||||
impl super::TunnelConnector for WgTunnelConnector {
|
||||
#[tracing::instrument]
|
||||
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let addr = SocketAddr::from_url(self.addr.clone(), self.ip_version).await?;
|
||||
async fn connect(&mut self) -> Result<Box<dyn super::Tunnel>, super::TunnelError> {
|
||||
let addr = super::check_scheme_and_get_socket_addr::<SocketAddr>(
|
||||
&self.addr,
|
||||
"wg",
|
||||
self.ip_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if addr.is_ipv6() {
|
||||
return self.connect_with_ipv6(addr).await;
|
||||
|
||||
@@ -36,7 +36,8 @@ thread_local! {
|
||||
}
|
||||
|
||||
pub fn setup_panic_handler() {
|
||||
use std::{backtrace, io::Write};
|
||||
use std::backtrace;
|
||||
use std::io::Write;
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let mut stderr = std::io::stderr();
|
||||
let sep = format!("{}\n", "=======".repeat(10));
|
||||
@@ -140,11 +141,3 @@ pub fn weak_upgrade<T>(weak: &std::sync::Weak<T>) -> anyhow::Result<std::sync::A
|
||||
weak.upgrade()
|
||||
.ok_or_else(|| anyhow::anyhow!("{} not available", std::any::type_name::<T>()))
|
||||
}
|
||||
|
||||
pub trait BoxExt: Sized {
|
||||
fn boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BoxExt for T {}
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
Copies binaries to the install directory and updates the system PATH.
|
||||
|
||||
.PARAMETER Version
|
||||
Target version: "latest", "stable", or a specific tag like "v2.6.0".
|
||||
Target version: "latest", "stable", or a specific tag like "v2.5.0".
|
||||
Default: "latest"
|
||||
|
||||
.PARAMETER InstallDir
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.EXAMPLE
|
||||
.\install.ps1
|
||||
.\install.ps1 -Version v2.6.0
|
||||
.\install.ps1 -Version v2.5.0
|
||||
.\install.ps1 -InstallDir "C:\EasyTier"
|
||||
|
||||
.NOTES
|
||||
|
||||
@@ -14,9 +14,6 @@ class TauriVpnService : VpnService() {
|
||||
companion object {
|
||||
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
|
||||
@JvmField var self: TauriVpnService? = null
|
||||
@JvmField var ipv4Addr: String? = null
|
||||
@JvmField var routes: Array<String> = emptyArray()
|
||||
@JvmField var dns: String? = null
|
||||
|
||||
const val IPV4_ADDR = "IPV4_ADDR"
|
||||
const val ROUTES = "ROUTES"
|
||||
@@ -30,9 +27,6 @@ class TauriVpnService : VpnService() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
println("vpn on start command ${intent?.getExtras()} $intent")
|
||||
var args = intent?.getExtras()
|
||||
ipv4Addr = args?.getString(IPV4_ADDR)
|
||||
routes = args?.getStringArray(ROUTES) ?: emptyArray()
|
||||
dns = args?.getString(DNS)
|
||||
|
||||
vpnInterface = createVpnInterface(args)
|
||||
println("vpn created ${vpnInterface.fd}")
|
||||
@@ -69,13 +63,6 @@ class TauriVpnService : VpnService() {
|
||||
triggerCallback("vpn_service_stop", JSObject())
|
||||
vpnInterface.close()
|
||||
}
|
||||
clearStatus()
|
||||
}
|
||||
|
||||
private fun clearStatus() {
|
||||
ipv4Addr = null
|
||||
routes = emptyArray()
|
||||
dns = null
|
||||
}
|
||||
|
||||
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
|
||||
|
||||
@@ -3,9 +3,7 @@ package com.plugin.vpnservice
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import androidx.activity.result.ActivityResult
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.ActivityCallback
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
@@ -50,70 +48,46 @@ class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun prepareVpn(invoke: Invoke) {
|
||||
activity.runOnUiThread {
|
||||
println("prepare vpn in plugin")
|
||||
val it = VpnService.prepare(activity)
|
||||
if (it != null) {
|
||||
startActivityForResult(invoke, it, "onPrepareVpnResult")
|
||||
return@runOnUiThread
|
||||
}
|
||||
val ret = JSObject()
|
||||
ret.put("granted", true)
|
||||
invoke.resolve(ret)
|
||||
println("prepare vpn in plugin")
|
||||
val it = VpnService.prepare(activity)
|
||||
var ret = JSObject()
|
||||
if (it != null) {
|
||||
activity.startActivityForResult(it, 0x0f)
|
||||
ret.put("errorMsg", "again")
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
fun onPrepareVpnResult(invoke: Invoke, result: ActivityResult) {
|
||||
val ret = JSObject()
|
||||
ret.put("granted", result.resultCode == Activity.RESULT_OK)
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun startVpn(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(StartVpnArgs::class.java)
|
||||
activity.runOnUiThread {
|
||||
println("start vpn in plugin, args: $args")
|
||||
println("start vpn in plugin, args: $args")
|
||||
|
||||
TauriVpnService.self?.onRevoke()
|
||||
TauriVpnService.self?.onRevoke()
|
||||
|
||||
val it = VpnService.prepare(activity)
|
||||
val ret = JSObject()
|
||||
if (it != null) {
|
||||
ret.put("errorMsg", "need_prepare")
|
||||
} else {
|
||||
val intent = Intent(activity, TauriVpnService::class.java)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||
intent.putExtra(TauriVpnService.MTU, args.mtu)
|
||||
val it = VpnService.prepare(activity)
|
||||
var ret = JSObject()
|
||||
if (it != null) {
|
||||
ret.put("errorMsg", "need_prepare")
|
||||
} else {
|
||||
var intent = Intent(activity, TauriVpnService::class.java)
|
||||
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
|
||||
intent.putExtra(TauriVpnService.ROUTES, args.routes)
|
||||
intent.putExtra(TauriVpnService.DNS, args.dns)
|
||||
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
|
||||
intent.putExtra(TauriVpnService.MTU, args.mtu)
|
||||
|
||||
activity.startService(intent)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
activity.startService(intent)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun stopVpn(invoke: Invoke) {
|
||||
activity.runOnUiThread {
|
||||
println("stop vpn in plugin")
|
||||
TauriVpnService.self?.onRevoke()
|
||||
activity.stopService(Intent(activity, TauriVpnService::class.java))
|
||||
println("stop vpn in plugin end")
|
||||
invoke.resolve(JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun getVpnStatus(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
ret.put("running", TauriVpnService.self != null)
|
||||
ret.put("ipv4Addr", TauriVpnService.ipv4Addr)
|
||||
ret.put("routes", TauriVpnService.routes)
|
||||
ret.put("dns", TauriVpnService.dns)
|
||||
invoke.resolve(ret)
|
||||
println("stop vpn in plugin")
|
||||
TauriVpnService.self?.onRevoke()
|
||||
activity.stopService(Intent(activity, TauriVpnService::class.java))
|
||||
println("stop vpn in plugin end")
|
||||
invoke.resolve(JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ const COMMANDS: &[&str] = &[
|
||||
"prepare_vpn",
|
||||
"start_vpn",
|
||||
"stop_vpn",
|
||||
"get_vpn_status",
|
||||
"registerListener",
|
||||
];
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export async function ping(value: string): Promise<string | null> {
|
||||
|
||||
export interface InvokeResponse {
|
||||
errorMsg?: string;
|
||||
granted?: boolean;
|
||||
}
|
||||
|
||||
export interface StartVpnRequest {
|
||||
@@ -21,13 +20,6 @@ export interface StartVpnRequest {
|
||||
mtu?: number;
|
||||
}
|
||||
|
||||
export interface VpnStatusResponse {
|
||||
running: boolean;
|
||||
ipv4Addr?: string;
|
||||
routes?: string[];
|
||||
dns?: string;
|
||||
}
|
||||
|
||||
export async function prepare_vpn(): Promise<InvokeResponse | null> {
|
||||
return await invoke<InvokeResponse>('plugin:vpnservice|prepare_vpn', {})
|
||||
}
|
||||
@@ -41,7 +33,3 @@ export async function start_vpn(request: StartVpnRequest): Promise<InvokeRespons
|
||||
export async function stop_vpn(): Promise<InvokeResponse | null> {
|
||||
return await invoke<InvokeResponse>('plugin:vpnservice|stop_vpn', {})
|
||||
}
|
||||
|
||||
export async function get_vpn_status(): Promise<VpnStatusResponse | null> {
|
||||
return await invoke<VpnStatusResponse>('plugin:vpnservice|get_vpn_status', {})
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ class ExamplePlugin: Plugin {
|
||||
let args = try invoke.parseArgs(PingArgs.self)
|
||||
invoke.resolve(["value": args.value ?? ""])
|
||||
}
|
||||
|
||||
@objc public func getVpnStatus(_ invoke: Invoke) {
|
||||
invoke.resolve(["running": false])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_vpnservice")
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-vpn-status"
|
||||
description = "Enables the get_vpn_status command without any pre-configured scope."
|
||||
commands.allow = ["get_vpn_status"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-vpn-status"
|
||||
description = "Denies the get_vpn_status command without any pre-configured scope."
|
||||
commands.deny = ["get_vpn_status"]
|
||||
@@ -16,32 +16,6 @@ Default permissions for the plugin
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpnservice:allow-get-vpn-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_vpn_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`vpnservice:deny-get-vpn-status`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_vpn_status command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
@@ -294,18 +294,6 @@
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the get_vpn_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-vpn-status",
|
||||
"markdownDescription": "Enables the get_vpn_status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_vpn_status command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-vpn-status",
|
||||
"markdownDescription": "Denies the get_vpn_status command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the ping command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
@@ -51,10 +51,4 @@ impl<R: Runtime> Vpnservice<R> {
|
||||
.run_mobile_plugin("stop_vpn", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_vpn_status(&self, payload: VoidRequest) -> crate::Result<VpnStatus> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_vpn_status", payload)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,3 @@ pub struct StartVpnRequest {
|
||||
pub struct Status {
|
||||
pub error_msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VpnStatus {
|
||||
pub running: bool,
|
||||
pub ipv4_addr: Option<String>,
|
||||
pub routes: Option<Vec<String>>,
|
||||
pub dns: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user