Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d7c3179c6e | ||
![]() |
b0fd37949a | ||
![]() |
29994b663a | ||
![]() |
fc397c35c5 | ||
![]() |
0f2b214918 | ||
![]() |
fec885c427 | ||
![]() |
5a2fd4465c | ||
![]() |
83d1ecc4da | ||
![]() |
7c6daf7c56 | ||
![]() |
28fe6257be | ||
![]() |
99430983bc | ||
![]() |
d758a4958f | ||
![]() |
95b12dda5a | ||
![]() |
2675cf2d00 | ||
![]() |
72be46e8fa | ||
![]() |
c5580feb64 | ||
![]() |
7e3819be86 | ||
![]() |
f0302f2be7 | ||
![]() |
b5f60f843d | ||
![]() |
6bdfb8b01f | ||
![]() |
ef1d81a2a1 | ||
![]() |
739b4ee106 | ||
![]() |
6a038e8a88 | ||
![]() |
72ea8a9f76 | ||
![]() |
44d93648ee | ||
![]() |
75f7865769 | ||
![]() |
01e3ad99ca | ||
![]() |
3c0d85c9db | ||
![]() |
b38991a14e | ||
![]() |
465269566b | ||
![]() |
f103fc13d9 | ||
![]() |
e5917fad4e | ||
![]() |
de8c89eb03 | ||
![]() |
c142db301a | ||
![]() |
8dc8c7d9e2 | ||
![]() |
2b909e04ea | ||
![]() |
e130c3f2e4 | ||
![]() |
3ad754879f | ||
![]() |
fd2b3768e1 | ||
![]() |
67cff12c76 | ||
![]() |
c5ea7848b3 | ||
![]() |
34365a096e | ||
![]() |
d880dfbbca | ||
![]() |
b46a200f8d | ||
![]() |
81490d0662 | ||
![]() |
3d1e841cc5 | ||
![]() |
f52936a103 | ||
![]() |
23f69ce6a4 | ||
![]() |
f84ae228fc | ||
![]() |
74c716ccaa | ||
![]() |
445b02b2ca | ||
![]() |
bb17ffa9fc | ||
![]() |
389ea709ce | ||
![]() |
c2f535ead4 | ||
![]() |
0318f55322 | ||
![]() |
1f4340e82f | ||
![]() |
ed08707c98 | ||
![]() |
7397abcb94 | ||
![]() |
98d321f8ac | ||
![]() |
e78b0ef869 | ||
![]() |
8d654330ac | ||
![]() |
00d61333d3 | ||
![]() |
03b55b61e7 | ||
![]() |
745e44cc87 | ||
![]() |
24213a874a | ||
![]() |
155f8a2ba2 | ||
![]() |
568dca6f9c | ||
![]() |
673c34cf5a | ||
![]() |
2050ed78d0 | ||
![]() |
2632c44195 | ||
![]() |
5449eabf2a | ||
![]() |
dd5b00faf4 | ||
![]() |
0caec3e4da | ||
![]() |
e48e62cac0 | ||
![]() |
06ebda2e2f | ||
![]() |
53c449b9fb | ||
![]() |
51e0fac72c |
4
.github/workflows/Dockerfile
vendored
@@ -18,7 +18,7 @@ RUN mkdir -p /tmp/output; \
|
|||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata tini
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
||||||
|
|
||||||
@@ -36,4 +36,4 @@ EXPOSE 11011/tcp
|
|||||||
# wss
|
# wss
|
||||||
EXPOSE 11012/tcp
|
EXPOSE 11012/tcp
|
||||||
|
|
||||||
ENTRYPOINT ["easytier-core"]
|
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
|
||||||
|
146
.github/workflows/core.yml
vendored
@@ -31,6 +31,47 @@ jobs:
|
|||||||
skip_after_successful_duplicate: 'true'
|
skip_after_successful_duplicate: 'true'
|
||||||
cancel_others: 'true'
|
cancel_others: 'true'
|
||||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||||
|
build_web:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 21
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
name: easytier-web-dashboard
|
||||||
|
path: |
|
||||||
|
easytier-web/frontend/dist/*
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -71,10 +112,12 @@ jobs:
|
|||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
ARTIFACT_NAME: windows-x86_64
|
ARTIFACT_NAME: windows-x86_64
|
||||||
|
|
||||||
- TARGET: aarch64-pc-windows-msvc
|
- TARGET: aarch64-pc-windows-msvc
|
||||||
OS: windows-latest
|
OS: windows-latest
|
||||||
ARTIFACT_NAME: windows-arm64
|
ARTIFACT_NAME: windows-arm64
|
||||||
|
- TARGET: i686-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
ARTIFACT_NAME: windows-i686
|
||||||
|
|
||||||
- TARGET: x86_64-unknown-freebsd
|
- TARGET: x86_64-unknown-freebsd
|
||||||
OS: ubuntu-22.04
|
OS: ubuntu-22.04
|
||||||
@@ -87,7 +130,9 @@ jobs:
|
|||||||
TARGET: ${{ matrix.TARGET }}
|
TARGET: ${{ matrix.TARGET }}
|
||||||
OS: ${{ matrix.OS }}
|
OS: ${{ matrix.OS }}
|
||||||
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||||
needs: pre_job
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- build_web
|
||||||
if: needs.pre_job.outputs.should_skip != 'true'
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -96,6 +141,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Download web artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: easytier-web-dashboard
|
||||||
|
path: easytier-web/frontend/dist/
|
||||||
|
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -121,23 +172,27 @@ jobs:
|
|||||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
|
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
|
||||||
else
|
else
|
||||||
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
|
SUFFIX=.exe
|
||||||
|
fi
|
||||||
|
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
|
||||||
|
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
|
||||||
cargo build --release --verbose --target $TARGET
|
cargo build --release --verbose --target $TARGET
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||||
uses: cross-platform-actions/action@v0.23.0
|
uses: vmactions/freebsd-vm@v1
|
||||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||||
env:
|
env:
|
||||||
TARGET: ${{ matrix.TARGET }}
|
TARGET: ${{ matrix.TARGET }}
|
||||||
with:
|
with:
|
||||||
operating_system: freebsd
|
envs: TARGET
|
||||||
environment_variables: TARGET
|
release: ${{ matrix.BSD_VERSION }}
|
||||||
architecture: x86-64
|
arch: x86_64
|
||||||
version: ${{ matrix.BSD_VERSION }}
|
usesh: true
|
||||||
shell: bash
|
mem: 6144
|
||||||
memory: 5G
|
cpu: 4
|
||||||
cpu_count: 4
|
|
||||||
run: |
|
run: |
|
||||||
uname -a
|
uname -a
|
||||||
echo $SHELL
|
echo $SHELL
|
||||||
@@ -146,19 +201,21 @@ jobs:
|
|||||||
whoami
|
whoami
|
||||||
env | sort
|
env | sort
|
||||||
|
|
||||||
sudo pkg install -y git protobuf llvm-devel
|
pkg install -y git protobuf llvm-devel sudo curl
|
||||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
source $HOME/.cargo/env
|
. $HOME/.cargo/env
|
||||||
|
|
||||||
rustup set auto-self-update disable
|
rustup set auto-self-update disable
|
||||||
|
|
||||||
rustup install 1.84
|
rustup install 1.86
|
||||||
rustup default 1.84
|
rustup default 1.86
|
||||||
|
|
||||||
export CC=clang
|
export CC=clang
|
||||||
export CXX=clang++
|
export CXX=clang++
|
||||||
export CARGO_TERM_COLOR=always
|
export CARGO_TERM_COLOR=always
|
||||||
|
|
||||||
|
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
|
||||||
|
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
|
||||||
cargo build --release --verbose --target $TARGET
|
cargo build --release --verbose --target $TARGET
|
||||||
|
|
||||||
- name: Install UPX
|
- name: Install UPX
|
||||||
@@ -174,12 +231,13 @@ jobs:
|
|||||||
# windows is the only OS using a different convention for executable file name
|
# windows is the only OS using a different convention for executable file name
|
||||||
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
cp easytier/third_party/Packet.dll ./artifacts/objects/
|
cp easytier/third_party/*.dll ./artifacts/objects/
|
||||||
cp easytier/third_party/wintun.dll ./artifacts/objects/
|
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then
|
||||||
|
SUFFIX=.exe
|
||||||
|
cp easytier/third_party/i686/*.dll ./artifacts/objects/
|
||||||
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
cp easytier/third_party/arm64/Packet.dll ./artifacts/objects/
|
cp easytier/third_party/arm64/*.dll ./artifacts/objects/
|
||||||
cp easytier/third_party/arm64/wintun.dll ./artifacts/objects/
|
|
||||||
fi
|
fi
|
||||||
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
|
||||||
TAG=$GITHUB_REF_NAME
|
TAG=$GITHUB_REF_NAME
|
||||||
@@ -196,6 +254,7 @@ jobs:
|
|||||||
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
|
||||||
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
if [[ ! $TARGET =~ ^mips.*$ ]]; then
|
||||||
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
|
||||||
|
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv ./artifacts/objects/* ./artifacts/
|
mv ./artifacts/objects/* ./artifacts/
|
||||||
@@ -208,25 +267,52 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
- name: Upload OSS
|
|
||||||
if: ${{ env.OSS_BUCKET != '' }}
|
|
||||||
uses: Menci/upload-to-oss@main
|
|
||||||
with:
|
|
||||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
|
||||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
|
||||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
|
||||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
|
||||||
local-path: ./artifacts/
|
|
||||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
|
|
||||||
no-delete-remote-files: true
|
|
||||||
retry: 5
|
|
||||||
core-result:
|
core-result:
|
||||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- pre_job
|
- pre_job
|
||||||
|
- build_web
|
||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- name: Mark result as failed
|
- name: Mark result as failed
|
||||||
if: needs.build.result != 'success'
|
if: needs.build.result != 'success'
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|
||||||
|
magisk_build:
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- build_web
|
||||||
|
- build
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置
|
||||||
|
|
||||||
|
# 下载二进制文件到独立目录
|
||||||
|
- name: Download Linux aarch64 binaries
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: easytier-linux-aarch64
|
||||||
|
path: ./downloaded-binaries/ # 独立目录避免冲突
|
||||||
|
|
||||||
|
# 将二进制文件复制到 Magisk 模块目录
|
||||||
|
- name: Prepare binaries
|
||||||
|
run: |
|
||||||
|
mkdir -p ./easytier-contrib/easytier-magisk/
|
||||||
|
cp ./downloaded-binaries/easytier-core ./easytier-contrib/easytier-magisk/
|
||||||
|
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
|
||||||
|
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
|
||||||
|
|
||||||
|
|
||||||
|
# 上传生成的模块
|
||||||
|
- name: Upload Magisk Module
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Easytier-Magisk
|
||||||
|
path: |
|
||||||
|
./easytier-contrib/easytier-magisk
|
||||||
|
!./easytier-contrib/easytier-magisk/build.sh
|
||||||
|
!./easytier-contrib/easytier-magisk/magisk_update.json
|
||||||
|
if-no-files-found: error
|
12
.github/workflows/docker.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
image_tag:
|
image_tag:
|
||||||
description: 'Tag for this image build'
|
description: 'Tag for this image build'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.2.1'
|
default: 'v2.3.0'
|
||||||
required: true
|
required: true
|
||||||
mark_latest:
|
mark_latest:
|
||||||
description: 'Mark this image as latest'
|
description: 'Mark this image as latest'
|
||||||
@@ -39,6 +39,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: login github container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
id: download-artifact
|
id: download-artifact
|
||||||
uses: dawidd6/action-download-artifact@v6
|
uses: dawidd6/action-download-artifact@v6
|
||||||
@@ -58,4 +64,6 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
file: .github/workflows/Dockerfile
|
file: .github/workflows/Dockerfile
|
||||||
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
|
tags: |
|
||||||
|
easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
|
||||||
|
ghcr.io/easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
|
||||||
|
117
.github/workflows/gui.yml
vendored
@@ -63,6 +63,11 @@ jobs:
|
|||||||
GUI_TARGET: aarch64-pc-windows-msvc
|
GUI_TARGET: aarch64-pc-windows-msvc
|
||||||
ARTIFACT_NAME: windows-arm64
|
ARTIFACT_NAME: windows-arm64
|
||||||
|
|
||||||
|
- TARGET: i686-pc-windows-msvc
|
||||||
|
OS: windows-latest
|
||||||
|
GUI_TARGET: i686-pc-windows-msvc
|
||||||
|
ARTIFACT_NAME: windows-i686
|
||||||
|
|
||||||
runs-on: ${{ matrix.OS }}
|
runs-on: ${{ matrix.OS }}
|
||||||
env:
|
env:
|
||||||
NAME: easytier
|
NAME: easytier
|
||||||
@@ -73,6 +78,56 @@ jobs:
|
|||||||
needs: pre_job
|
needs: pre_job
|
||||||
if: needs.pre_job.outputs.should_skip != 'true'
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install GUI dependencies (x86 only)
|
||||||
|
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -qq libwebkit2gtk-4.1-dev \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
libgtk-3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
libssl-dev \
|
||||||
|
patchelf
|
||||||
|
|
||||||
|
- name: Install GUI cross compile (aarch64 only)
|
||||||
|
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
||||||
|
run: |
|
||||||
|
# see https://tauri.app/v1/guides/building/linux/
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
||||||
|
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||||
|
|
||||||
|
sudo dpkg --add-architecture arm64
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install aptitude
|
||||||
|
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
|
||||||
|
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
|
||||||
|
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
||||||
|
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
||||||
|
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set current ref as env variable
|
- name: Set current ref as env variable
|
||||||
@@ -124,59 +179,13 @@ jobs:
|
|||||||
# GitHub repo token to use to avoid rate limiter
|
# GitHub repo token to use to avoid rate limiter
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install GUI dependencies (x86 only)
|
|
||||||
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
|
|
||||||
run: |
|
|
||||||
sudo apt install -qq libwebkit2gtk-4.1-dev \
|
|
||||||
build-essential \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
file \
|
|
||||||
libgtk-3-dev \
|
|
||||||
librsvg2-dev \
|
|
||||||
libxdo-dev \
|
|
||||||
libssl-dev \
|
|
||||||
patchelf
|
|
||||||
|
|
||||||
- name: Install GUI cross compile (aarch64 only)
|
|
||||||
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
|
|
||||||
run: |
|
|
||||||
# see https://tauri.app/v1/guides/building/linux/
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
|
|
||||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
|
|
||||||
|
|
||||||
sudo dpkg --add-architecture arm64
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64
|
|
||||||
sudo apt-get install -y libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64
|
|
||||||
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
|
|
||||||
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
|
|
||||||
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: copy correct DLLs
|
- name: copy correct DLLs
|
||||||
if: ${{ matrix.OS == 'windows-latest' }}
|
if: ${{ matrix.OS == 'windows-latest' }}
|
||||||
run: |
|
run: |
|
||||||
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
|
||||||
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
|
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
|
||||||
|
elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then
|
||||||
|
cp ./easytier/third_party/i686/*.dll ./easytier-gui/src-tauri/
|
||||||
else
|
else
|
||||||
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
|
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
|
||||||
fi
|
fi
|
||||||
@@ -221,18 +230,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
- name: Upload OSS
|
|
||||||
if: ${{ env.OSS_BUCKET != '' }}
|
|
||||||
uses: Menci/upload-to-oss@main
|
|
||||||
with:
|
|
||||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
|
||||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
|
||||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
|
||||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
|
||||||
local-path: ./artifacts/
|
|
||||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
|
||||||
no-delete-remote-files: true
|
|
||||||
retry: 5
|
|
||||||
gui-result:
|
gui-result:
|
||||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
6
.github/workflows/install_rust.sh
vendored
@@ -36,7 +36,7 @@ if [[ $OS =~ ^ubuntu.*$ ]]; then
|
|||||||
|
|
||||||
if [ -n "$MUSL_URI" ]; then
|
if [ -n "$MUSL_URI" ]; then
|
||||||
mkdir -p ./musl_gcc
|
mkdir -p ./musl_gcc
|
||||||
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/
|
wget --inet4-only -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/
|
||||||
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
|
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
|
||||||
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
|
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
|
||||||
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/${MUSL_URI}/include/ /usr/include/musl-cross
|
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/${MUSL_URI}/include/ /usr/include/musl-cross
|
||||||
@@ -45,8 +45,8 @@ fi
|
|||||||
|
|
||||||
# see https://github.com/rust-lang/rustup/issues/3709
|
# see https://github.com/rust-lang/rustup/issues/3709
|
||||||
rustup set auto-self-update disable
|
rustup set auto-self-update disable
|
||||||
rustup install 1.84
|
rustup install 1.86
|
||||||
rustup default 1.84
|
rustup default 1.86
|
||||||
|
|
||||||
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
||||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||||
|
12
.github/workflows/mobile.yml
vendored
@@ -146,18 +146,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
./artifacts/*
|
./artifacts/*
|
||||||
|
|
||||||
- name: Upload OSS
|
|
||||||
if: ${{ env.OSS_BUCKET != '' }}
|
|
||||||
uses: Menci/upload-to-oss@main
|
|
||||||
with:
|
|
||||||
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
|
|
||||||
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
|
|
||||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
|
||||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
|
||||||
local-path: ./artifacts/
|
|
||||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
|
||||||
no-delete-remote-files: true
|
|
||||||
retry: 5
|
|
||||||
mobile-result:
|
mobile-result:
|
||||||
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
if: needs.pre_job.outputs.should_skip != 'true' && always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ on:
|
|||||||
version:
|
version:
|
||||||
description: 'Version for this release'
|
description: 'Version for this release'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.2.1'
|
default: 'v2.3.0'
|
||||||
required: true
|
required: true
|
||||||
make_latest:
|
make_latest:
|
||||||
description: 'Mark this release as latest'
|
description: 'Mark this release as latest'
|
||||||
|
31
.github/workflows/test.yml
vendored
@@ -47,11 +47,40 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup system for test
|
- name: Setup system for test
|
||||||
run: |
|
run: |
|
||||||
|
sudo modprobe br_netfilter
|
||||||
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
||||||
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
||||||
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
|
||||||
sudo ip addr add 2001:db8::2/64 dev lo
|
sudo ip addr add 2001:db8::2/64 dev lo
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 21
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: |
|
||||||
|
pnpm -r install
|
||||||
|
pnpm -r --filter "./easytier-web/*" build
|
||||||
|
|
||||||
- name: Cargo cache
|
- name: Cargo cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -62,6 +91,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose
|
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture
|
||||||
sudo chown -R $USER:$USER ./target
|
sudo chown -R $USER:$USER ./target
|
||||||
sudo chown -R $USER:$USER ~/.cargo
|
sudo chown -R $USER:$USER ~/.cargo
|
||||||
|
1495
Cargo.lock
generated
10
Cargo.toml
@@ -1,6 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"]
|
members = [
|
||||||
|
"easytier",
|
||||||
|
"easytier-gui/src-tauri",
|
||||||
|
"easytier-rpc-build",
|
||||||
|
"easytier-web",
|
||||||
|
"easytier-contrib/easytier-ffi",
|
||||||
|
]
|
||||||
default-members = ["easytier", "easytier-web"]
|
default-members = ["easytier", "easytier-web"]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
@@ -10,3 +16,5 @@ panic = "unwind"
|
|||||||
panic = "abort"
|
panic = "abort"
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
opt-level = 'z'
|
||||||
|
strip = true
|
||||||
|
38
README.md
@@ -1,14 +1,17 @@
|
|||||||
# EasyTier
|
# EasyTier
|
||||||
|
|
||||||
|
[](https://github.com/EasyTier/EasyTier/releases)
|
||||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||||
[](https://github.com/EasyTier/EasyTier/issues)
|
[](https://github.com/EasyTier/EasyTier/issues)
|
||||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||||
|
[](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
|
||||||
|
[](https://deepwiki.com/EasyTier/EasyTier)
|
||||||
|
|
||||||
[简体中文](/README_CN.md) | [English](/README.md)
|
[简体中文](/README_CN.md) | [English](/README.md)
|
||||||
|
|
||||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
**Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.**
|
||||||
|
|
||||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||||
|
|
||||||
@@ -53,7 +56,7 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
|
|||||||
|
|
||||||
4. **Install by Docker Compose**
|
4. **Install by Docker Compose**
|
||||||
|
|
||||||
Please visit the [EasyTier Official Website](https://www.easytier.cn/en/) to view the full documentation.
|
Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.
|
||||||
|
|
||||||
5. **Install by script (For Linux Only)**
|
5. **Install by script (For Linux Only)**
|
||||||
|
|
||||||
@@ -61,7 +64,36 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
|
|||||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
|
The script supports the following commands and options:
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
- `install`: Install EasyTier
|
||||||
|
- `uninstall`: Uninstall EasyTier
|
||||||
|
- `update`: Update EasyTier to the latest version
|
||||||
|
- `help`: Show help message
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--skip-folder-verify`: Skip folder verification during installation
|
||||||
|
- `--skip-folder-fix`: Skip automatic folder path fixing
|
||||||
|
- `--no-gh-proxy`: Disable GitHub proxy
|
||||||
|
- `--gh-proxy`: Set custom GitHub proxy URL (default: https://ghfast.top/)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```sh
|
||||||
|
# Show help
|
||||||
|
bash /tmp/easytier.sh help
|
||||||
|
|
||||||
|
# Install with options
|
||||||
|
bash /tmp/easytier.sh install --skip-folder-verify
|
||||||
|
bash /tmp/easytier.sh install --no-gh-proxy
|
||||||
|
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
|
||||||
|
|
||||||
|
# Update EasyTier
|
||||||
|
bash /tmp/easytier.sh update
|
||||||
|
|
||||||
|
# Uninstall EasyTier
|
||||||
|
bash /tmp/easytier.sh uninstall
|
||||||
|
```
|
||||||
|
|
||||||
6. **Install by Homebrew (For MacOS Only)**
|
6. **Install by Homebrew (For MacOS Only)**
|
||||||
|
|
||||||
|
35
README_CN.md
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
[简体中文](/README_CN.md) | [English](/README.md)
|
[简体中文](/README_CN.md) | [English](/README.md)
|
||||||
|
|
||||||
**请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。**
|
**请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
|
||||||
|
|
||||||
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
4. **通过Docker Compose安装**
|
4. **通过Docker Compose安装**
|
||||||
|
|
||||||
请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。
|
请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
|
||||||
|
|
||||||
5. **使用一键脚本安装 (仅适用于 Linux)**
|
5. **使用一键脚本安装 (仅适用于 Linux)**
|
||||||
|
|
||||||
@@ -61,7 +61,36 @@
|
|||||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||||
```
|
```
|
||||||
|
|
||||||
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
|
脚本支持以下命令和选项:
|
||||||
|
|
||||||
|
命令:
|
||||||
|
- `install`: 安装 EasyTier
|
||||||
|
- `uninstall`: 卸载 EasyTier
|
||||||
|
- `update`: 更新 EasyTier 到最新版本
|
||||||
|
- `help`: 显示帮助信息
|
||||||
|
|
||||||
|
选项:
|
||||||
|
- `--skip-folder-verify`: 跳过安装过程中的文件夹验证
|
||||||
|
- `--skip-folder-fix`: 跳过自动修复文件夹路径
|
||||||
|
- `--no-gh-proxy`: 禁用 GitHub 代理
|
||||||
|
- `--gh-proxy`: 设置自定义 GitHub 代理 URL (默认值: https://ghfast.top/)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```sh
|
||||||
|
# 查看帮助
|
||||||
|
bash /tmp/easytier.sh help
|
||||||
|
|
||||||
|
# 安装(带选项)
|
||||||
|
bash /tmp/easytier.sh install --skip-folder-verify
|
||||||
|
bash /tmp/easytier.sh install --no-gh-proxy
|
||||||
|
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
|
||||||
|
|
||||||
|
# 更新 EasyTier
|
||||||
|
bash /tmp/easytier.sh update
|
||||||
|
|
||||||
|
# 卸载 EasyTier
|
||||||
|
bash /tmp/easytier.sh uninstall
|
||||||
|
```
|
||||||
|
|
||||||
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
|
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
|
||||||
|
|
||||||
|
16
easytier-contrib/easytier-ffi/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "easytier-ffi"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
easytier = { path = "../../easytier" }
|
||||||
|
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
dashmap = "6.0"
|
||||||
|
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
159
easytier-contrib/easytier-ffi/examples/csharp.cs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
public class EasyTierFFI
|
||||||
|
{
|
||||||
|
// 导入 DLL 函数
|
||||||
|
private const string DllName = "easytier_ffi.dll";
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern int parse_config([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern int run_network_instance([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern int retain_network_instance(IntPtr instNames, int length);
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern int collect_network_infos(IntPtr infos, int maxLength);
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern void get_error_msg(out IntPtr errorMsg);
|
||||||
|
|
||||||
|
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
|
||||||
|
private static extern void free_string(IntPtr str);
|
||||||
|
|
||||||
|
// 定义 KeyValuePair 结构体
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct KeyValuePair
|
||||||
|
{
|
||||||
|
public IntPtr Key;
|
||||||
|
public IntPtr Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配置
|
||||||
|
public static void ParseConfig(string config)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(config))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Configuration string cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = parse_config(config);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
throw new Exception(GetErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动网络实例
|
||||||
|
public static void RunNetworkInstance(string config)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(config))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Configuration string cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = run_network_instance(config);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
throw new Exception(GetErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留网络实例
|
||||||
|
public static void RetainNetworkInstances(string[] instanceNames)
|
||||||
|
{
|
||||||
|
IntPtr[] namePointers = null;
|
||||||
|
IntPtr namesPtr = IntPtr.Zero;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (instanceNames != null && instanceNames.Length > 0)
|
||||||
|
{
|
||||||
|
namePointers = new IntPtr[instanceNames.Length];
|
||||||
|
for (int i = 0; i < instanceNames.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(instanceNames[i]))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Instance name cannot be null or empty.");
|
||||||
|
}
|
||||||
|
namePointers[i] = Marshal.StringToHGlobalAnsi(instanceNames[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
namesPtr = Marshal.AllocHGlobal(Marshal.SizeOf<IntPtr>() * namePointers.Length);
|
||||||
|
Marshal.Copy(namePointers, 0, namesPtr, namePointers.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = retain_network_instance(namesPtr, instanceNames?.Length ?? 0);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
throw new Exception(GetErrorMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (namePointers != null)
|
||||||
|
{
|
||||||
|
foreach (var ptr in namePointers)
|
||||||
|
{
|
||||||
|
if (ptr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namesPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(namesPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集网络信息
|
||||||
|
public static KeyValuePair<string, string>[] CollectNetworkInfos(int maxLength)
|
||||||
|
{
|
||||||
|
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf<KeyValuePair>() * maxLength);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int count = collect_network_infos(buffer, maxLength);
|
||||||
|
if (count < 0)
|
||||||
|
{
|
||||||
|
throw new Exception(GetErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new KeyValuePair<string, string>[count];
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var kv = Marshal.PtrToStructure<KeyValuePair>(buffer + i * Marshal.SizeOf<KeyValuePair>());
|
||||||
|
string key = Marshal.PtrToStringAnsi(kv.Key);
|
||||||
|
string value = Marshal.PtrToStringAnsi(kv.Value);
|
||||||
|
|
||||||
|
// 释放由 FFI 分配的字符串内存
|
||||||
|
free_string(kv.Key);
|
||||||
|
free_string(kv.Value);
|
||||||
|
|
||||||
|
result[i] = new KeyValuePair<string, string>(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取错误信息
|
||||||
|
private static string GetErrorMessage()
|
||||||
|
{
|
||||||
|
get_error_msg(out IntPtr errorMsgPtr);
|
||||||
|
if (errorMsgPtr == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
|
||||||
|
string errorMsg = Marshal.PtrToStringAnsi(errorMsgPtr);
|
||||||
|
free_string(errorMsgPtr); // 释放错误信息字符串
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
}
|
199
easytier-contrib/easytier-ffi/src/lib.rs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use easytier::{
|
||||||
|
common::config::{ConfigLoader as _, TomlConfigLoader},
|
||||||
|
launcher::NetworkInstance,
|
||||||
|
};
|
||||||
|
|
||||||
|
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||||
|
once_cell::sync::Lazy::new(DashMap::new);
|
||||||
|
|
||||||
|
static ERROR_MSG: once_cell::sync::Lazy<Mutex<Vec<u8>>> =
|
||||||
|
once_cell::sync::Lazy::new(|| Mutex::new(Vec::new()));
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct KeyValuePair {
|
||||||
|
pub key: *const std::ffi::c_char,
|
||||||
|
pub value: *const std::ffi::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_msg(msg: &str) {
|
||||||
|
let bytes = msg.as_bytes();
|
||||||
|
let mut msg_buf = ERROR_MSG.lock().unwrap();
|
||||||
|
let len = bytes.len();
|
||||||
|
msg_buf.resize(len, 0);
|
||||||
|
msg_buf[..len].copy_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
|
||||||
|
let msg_buf = ERROR_MSG.lock().unwrap();
|
||||||
|
if msg_buf.is_empty() {
|
||||||
|
unsafe {
|
||||||
|
*out = std::ptr::null();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cstr = std::ffi::CString::new(&msg_buf[..]).unwrap();
|
||||||
|
unsafe {
|
||||||
|
*out = cstr.into_raw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
|
||||||
|
if s.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
let _ = std::ffi::CString::from_raw(s as *mut std::ffi::c_char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
|
let cfg_str = unsafe {
|
||||||
|
assert!(!cfg_str.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(cfg_str)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
|
||||||
|
set_error_msg(&format!("failed to parse config: {:?}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
|
||||||
|
let cfg_str = unsafe {
|
||||||
|
assert!(!cfg_str.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(cfg_str)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
};
|
||||||
|
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
|
||||||
|
Ok(cfg) => cfg,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to parse config: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let inst_name = cfg.get_inst_name();
|
||||||
|
|
||||||
|
if INSTANCE_MAP.contains_key(&inst_name) {
|
||||||
|
set_error_msg("instance already exists");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut instance = NetworkInstance::new(cfg);
|
||||||
|
if let Err(e) = instance.start().map_err(|e| e.to_string()) {
|
||||||
|
set_error_msg(&format!("failed to start instance: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANCE_MAP.insert(inst_name, instance);
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn retain_network_instance(
|
||||||
|
inst_names: *const *const std::ffi::c_char,
|
||||||
|
length: usize,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
if length == 0 {
|
||||||
|
INSTANCE_MAP.clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inst_names = unsafe {
|
||||||
|
assert!(!inst_names.is_null());
|
||||||
|
std::slice::from_raw_parts(inst_names, length)
|
||||||
|
.iter()
|
||||||
|
.map(|&name| {
|
||||||
|
assert!(!name.is_null());
|
||||||
|
std::ffi::CStr::from_ptr(name)
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k));
|
||||||
|
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn collect_network_infos(
|
||||||
|
infos: *mut KeyValuePair,
|
||||||
|
max_length: usize,
|
||||||
|
) -> std::ffi::c_int {
|
||||||
|
if max_length == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let infos = unsafe {
|
||||||
|
assert!(!infos.is_null());
|
||||||
|
std::slice::from_raw_parts_mut(infos, max_length)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
for instance in INSTANCE_MAP.iter() {
|
||||||
|
if index >= max_length {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let key = instance.key();
|
||||||
|
let Some(value) = instance.get_running_info() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// convert value to json string
|
||||||
|
let value = match serde_json::to_string(&value) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(e) => {
|
||||||
|
set_error_msg(&format!("failed to serialize instance info: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
infos[index] = KeyValuePair {
|
||||||
|
key: std::ffi::CString::new(key.clone()).unwrap().into_raw(),
|
||||||
|
value: std::ffi::CString::new(value).unwrap().into_raw(),
|
||||||
|
};
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
index as std::ffi::c_int
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
inst_name = "test"
|
||||||
|
network = "test_network"
|
||||||
|
fdsafdsa
|
||||||
|
"#;
|
||||||
|
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||||
|
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_run_network_instance() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
inst_name = "test"
|
||||||
|
network = "test_network"
|
||||||
|
"#;
|
||||||
|
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||||
|
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
#!/sbin/sh
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Initialization
|
||||||
|
#################
|
||||||
|
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
# echo before loading util_functions
|
||||||
|
ui_print() { echo "$1"; }
|
||||||
|
|
||||||
|
require_new_magisk() {
|
||||||
|
ui_print "********************************"
|
||||||
|
ui_print " Please install Magisk v20.4+! "
|
||||||
|
ui_print "********************************"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Load util_functions.sh
|
||||||
|
#########################
|
||||||
|
|
||||||
|
OUTFD=$2
|
||||||
|
ZIPFILE=$3
|
||||||
|
|
||||||
|
mount /data 2>/dev/null
|
||||||
|
|
||||||
|
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
|
||||||
|
. /data/adb/magisk/util_functions.sh
|
||||||
|
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
|
||||||
|
|
||||||
|
install_module
|
||||||
|
exit 0
|
@@ -0,0 +1 @@
|
|||||||
|
#MAGISK
|
6
easytier-contrib/easytier-magisk/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# easytier_magisk版模块
|
||||||
|
magisk安装后重启
|
||||||
|
|
||||||
|
目录位置:/data/adb/modules/easytier_magisk
|
||||||
|
配置文件位置://data/adb/modules/easytier_magisk/config/config.conf
|
||||||
|
修改config.conf即可,修改后配置文件后去magisk app重新开关模块即可生效
|
16
easytier-contrib/easytier-magisk/action.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/data/adb/magisk/busybox sh
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
echo 'Easytier 服务停止中....'
|
||||||
|
|
||||||
|
PIDS=$(pgrep -f "^${MODDIR}/easytier-core -c ${MODDIR}/config/config.conf")
|
||||||
|
|
||||||
|
if [ -n "$PIDS" ]; then
|
||||||
|
kill $PIDS # 杀死所有匹配的进程
|
||||||
|
echo "已停止所有 Easytier 进程 (PIDs: $PIDS)"
|
||||||
|
else
|
||||||
|
echo "Easytier 服务未运行"
|
||||||
|
fi
|
||||||
|
echo '重启服务中...'
|
||||||
|
nohup sh ${MODDIR}/service.sh >> ${MODDIR}/log/start.log 2>&1 &
|
||||||
|
echo '服务已重启'
|
||||||
|
exit
|
25
easytier-contrib/easytier-magisk/build.sh
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
version=$(cat module.prop | grep 'version=' | awk -F '=' '{print $2}' | sed 's/ (.*//')
|
||||||
|
|
||||||
|
version='v'$(grep '^version =' ../../easytier/Cargo.toml | cut -d '"' -f 2)
|
||||||
|
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo "Error: 版本号不存在."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
filename="easytier_magisk_${version}.zip"
|
||||||
|
echo $version
|
||||||
|
|
||||||
|
|
||||||
|
if [ -f "./easytier-core" ] && [ -f "./easytier-cli" ] && [ -f "./easytier-web" ]; then
|
||||||
|
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
|
||||||
|
else
|
||||||
|
wget -O "easytier_last.zip" https://github.com/EasyTier/EasyTier/releases/download/"$version"/easytier-linux-aarch64-"$version".zip
|
||||||
|
unzip -o easytier_last.zip -d ./
|
||||||
|
mv ./easytier-linux-aarch64/* ./
|
||||||
|
rm -rf ./easytier_last.zip
|
||||||
|
rm -rf ./easytier-linux-aarch64
|
||||||
|
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
|
||||||
|
fi
|
37
easytier-contrib/easytier-magisk/config/config.conf
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
instance_name = "default"
|
||||||
|
dhcp = false
|
||||||
|
#ipv4="本机ip"
|
||||||
|
listeners = [
|
||||||
|
"tcp://0.0.0.0:11010",
|
||||||
|
"udp://0.0.0.0:11010",
|
||||||
|
"wg://0.0.0.0:11011",
|
||||||
|
"ws://0.0.0.0:11011/",
|
||||||
|
"wss://0.0.0.0:11012/",
|
||||||
|
]
|
||||||
|
mapped_listeners = []
|
||||||
|
exit_nodes = []
|
||||||
|
rpc_portal = "0.0.0.0:15888"
|
||||||
|
|
||||||
|
[network_identity]
|
||||||
|
network_name = "default"
|
||||||
|
network_secret = ""
|
||||||
|
|
||||||
|
[[peer]]
|
||||||
|
#uri = "协议://中转ip:端口"
|
||||||
|
|
||||||
|
[flags]
|
||||||
|
default_protocol = "tcp"
|
||||||
|
dev_name = ""
|
||||||
|
enable_encryption = true
|
||||||
|
enable_ipv6 = true
|
||||||
|
mtu = 1380
|
||||||
|
latency_first = false
|
||||||
|
enable_exit_node = false
|
||||||
|
no_tun = false
|
||||||
|
use_smoltcp = false
|
||||||
|
foreign_network_whitelist = "*"
|
||||||
|
disable_p2p = false
|
||||||
|
relay_all_peer_rpc = false
|
||||||
|
disable_udp_hole_punching = false
|
||||||
|
|
||||||
|
|
7
easytier-contrib/easytier-magisk/customize.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ui_print '安装完成'
|
||||||
|
ui_print '当前架构为' + $ARCH
|
||||||
|
ui_print '当前系统版本为' + $API
|
||||||
|
ui_print '安装目录为: /data/adb/modules/easytier_magisk'
|
||||||
|
ui_print '配置文件位置: /data/adb/modules/easytier_magisk/config/config.conf'
|
||||||
|
ui_print '修改后配置文件后在magisk app点击操作按钮即可生效'
|
||||||
|
ui_print '记得重启'
|
6
easytier-contrib/easytier-magisk/magisk_update.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "v1.0",
|
||||||
|
"versionCode": 1,
|
||||||
|
"zipUrl": "",
|
||||||
|
"changelog": ""
|
||||||
|
}
|
7
easytier-contrib/easytier-magisk/module.prop
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
id=easytier_magisk
|
||||||
|
name=easytier_magisk版
|
||||||
|
version=v2.2.4
|
||||||
|
versionCode=1
|
||||||
|
author=EasyTier
|
||||||
|
description=easytier_magisk版模块 作者:EasyTier https://github.com/EasyTier/EasyTier
|
||||||
|
updateJson=https://raw.githubusercontent.com/EasyTier/EasyTier/refs/heads/main/easytier-contrib/easytier-magisk/magisk_update.json
|
20
easytier-contrib/easytier-magisk/service.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/data/adb/magisk/busybox sh
|
||||||
|
MODDIR=${0%/*}
|
||||||
|
# MODDIR="$(dirname $(readlink -f "$0"))"
|
||||||
|
mkdir -p ${MODDIR}/log
|
||||||
|
chmod 755 ${MODDIR}/*
|
||||||
|
|
||||||
|
echo $MODDIR >> ${MODDIR}/log/start.log
|
||||||
|
|
||||||
|
echo "Easytier 服务启动" >> ${MODDIR}/log/start.log
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
nohup ${MODDIR}/easytier-core -c ${MODDIR}/config/config.conf >> ${MODDIR}/log/start.log 2>&1 &
|
||||||
|
|
||||||
|
# 检查是否启用模块
|
||||||
|
while [ ! -f ${MODDIR}/disable ]; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
PID=$(ps -ef|grep "${MODDIR}/easytier-core -c ${MODDIR}/config/config.conf" | awk '{print $2}')
|
||||||
|
kill $PID
|
||||||
|
echo "Easytier 服务停止" >> ${MODDIR}/log/start.log
|
2
easytier-contrib/easytier-magisk/uninstall.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
MODDIR=${0%/*}
|
||||||
|
rm -rf $MODDIR/*
|
@@ -113,3 +113,4 @@ event:
|
|||||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||||
|
PortForwardAdded: 端口转发添加
|
||||||
|
@@ -112,3 +112,4 @@ event:
|
|||||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||||
DhcpIpv4Changed: DhcpIpv4Changed
|
DhcpIpv4Changed: DhcpIpv4Changed
|
||||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||||
|
PortForwardAdded: PortForwardAdded
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "easytier-gui",
|
"name": "easytier-gui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.2.1",
|
"version": "2.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "4.3.3",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||||
"@tauri-apps/plugin-os": "2.0.0",
|
"@tauri-apps/plugin-os": "2.0.0",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"pinia": "^2.2.4",
|
"pinia": "^2.2.4",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "4.3.3",
|
||||||
"tauri-plugin-vpnservice-api": "workspace:*",
|
"tauri-plugin-vpnservice-api": "workspace:*",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^3.7.3",
|
"@antfu/eslint-config": "^3.7.3",
|
||||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||||
"@primevue/auto-import-resolver": "^4.1.0",
|
"@primevue/auto-import-resolver": "4.3.3",
|
||||||
"@tauri-apps/api": "2.1.0",
|
"@tauri-apps/api": "2.1.0",
|
||||||
"@tauri-apps/cli": "2.1.0",
|
"@tauri-apps/cli": "2.1.0",
|
||||||
"@types/default-gateway": "^7.2.2",
|
"@types/default-gateway": "^7.2.2",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
"eslint-plugin-format": "^0.1.2",
|
"eslint-plugin-format": "^0.1.2",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "=3.4.17",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"unplugin-auto-import": "^0.18.3",
|
"unplugin-auto-import": "^0.18.3",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
description = "EasyTier GUI"
|
description = "EasyTier GUI"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -14,6 +14,13 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||||
|
|
||||||
|
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||||
|
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
|
||||||
|
[target.i686-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
||||||
tauri = { version = "=2.0.6", features = [
|
tauri = { version = "=2.0.6", features = [
|
||||||
@@ -53,4 +60,4 @@ tauri-plugin-autostart = "2.0"
|
|||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2.0.0-rc.0"
|
tauri-plugin-single-instance = "2.2.3"
|
||||||
|
@@ -1,3 +1,12 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("aarch64")
|
||||||
|
{
|
||||||
|
thunk::thunk();
|
||||||
|
}
|
||||||
|
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@@ -17,7 +17,7 @@
|
|||||||
"createUpdaterArtifacts": false
|
"createUpdaterArtifacts": false
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "2.2.1",
|
"version": "2.3.0",
|
||||||
"identifier": "com.kkrainbow.easytier",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {},
|
"plugins": {},
|
||||||
"app": {
|
"app": {
|
||||||
|
@@ -132,6 +132,14 @@ async function onNetworkInstanceChange() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if use no tun mode, stop the vpn service
|
||||||
|
const no_tun = networkStore.isNoTunEnabled(insts[0])
|
||||||
|
if (no_tun) {
|
||||||
|
console.error('no tun mode, stop vpn service')
|
||||||
|
await doStopVpn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
|
||||||
if (!network_length) {
|
if (!network_length) {
|
||||||
network_length = 24
|
network_length = 24
|
||||||
|
@@ -50,8 +50,8 @@ async function main() {
|
|||||||
darkModeSelector: 'system',
|
darkModeSelector: 'system',
|
||||||
cssLayer: {
|
cssLayer: {
|
||||||
name: 'primevue',
|
name: 'primevue',
|
||||||
order: 'tailwind-base, primevue, tailwind-utilities'
|
order: 'tailwind-base, primevue, tailwind-utilities',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@@ -128,6 +128,13 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
}
|
}
|
||||||
this.saveAutoStartInstIdsToLocalStorage()
|
this.saveAutoStartInstIdsToLocalStorage()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isNoTunEnabled(instanceId: string): boolean {
|
||||||
|
const cfg = this.networkList.find((cfg) => cfg.instance_id === instanceId)
|
||||||
|
if (!cfg)
|
||||||
|
return false
|
||||||
|
return cfg.no_tun ?? false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -45,3 +45,11 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #0000005d;
|
background-color: #0000005d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-password {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-password>input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||||
|
|
||||||
@@ -18,13 +18,18 @@ axum = { version = "0.7", features = ["macros"] }
|
|||||||
axum-login = { version = "0.16" }
|
axum-login = { version = "0.16" }
|
||||||
password-auth = { version = "1.0.0" }
|
password-auth = { version = "1.0.0" }
|
||||||
axum-messages = "0.7.0"
|
axum-messages = "0.7.0"
|
||||||
|
axum-embed = { version = "0.1.0", optional = true }
|
||||||
tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
|
tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
|
||||||
tower-sessions = { version = "0.13.0", default-features = false, features = [
|
tower-sessions = { version = "0.13.0", default-features = false, features = [
|
||||||
"signed",
|
"signed",
|
||||||
] }
|
] }
|
||||||
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
|
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
|
||||||
sqlx = { version = "0.8", features = ["sqlite"] }
|
sqlx = { version = "0.8", features = ["sqlite"] }
|
||||||
sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
sea-orm = { version = "1.1", features = [
|
||||||
|
"sqlx-sqlite",
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
sea-orm-migration = { version = "1.1" }
|
sea-orm-migration = { version = "1.1" }
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +37,7 @@ sea-orm-migration = { version = "1.1" }
|
|||||||
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
|
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
image = {version="0.24", default-features = false, features = ["png"]}
|
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||||
rusttype = "0.9.3"
|
rusttype = "0.9.3"
|
||||||
imageproc = "0.23.0"
|
imageproc = "0.23.0"
|
||||||
|
|
||||||
@@ -55,3 +60,14 @@ uuid = { version = "1.5.0", features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
chrono = { version = "0.4.37", features = ["serde"] }
|
chrono = { version = "0.4.37", features = ["serde"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
embed = ["dep:axum-embed"]
|
||||||
|
|
||||||
|
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||||
|
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
|
||||||
|
[target.i686-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
7
easytier-web/build.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fn main() {
|
||||||
|
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET").unwrap_or_default().contains("aarch64"){
|
||||||
|
thunk::thunk();
|
||||||
|
}
|
||||||
|
}
|
@@ -18,14 +18,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "4.3.3",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
"aura": "link:@primevue\\themes\\aura",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"floating-vue": "^5.2",
|
"floating-vue": "^5.2",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "4.3.3",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"ts-md5": "^1.3.1",
|
"ts-md5": "^1.3.1",
|
||||||
"uuid": "^11.0.2",
|
"uuid": "^11.0.2",
|
||||||
@@ -40,10 +40,10 @@
|
|||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-nested": "^7.0.2",
|
"postcss-nested": "^7.0.2",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "=3.4.17",
|
||||||
"typescript": "~5.6.3",
|
"typescript": "~5.6.3",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputGroup from 'primevue/inputgroup'
|
import InputGroup from 'primevue/inputgroup'
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button } from 'primevue'
|
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
|
||||||
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
|
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
|
||||||
import { defineProps, defineEmits, ref, } from 'vue'
|
import { defineProps, defineEmits, ref, } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -120,6 +120,23 @@ function searchListenerSuggestions(e: { query: string }) {
|
|||||||
listenerSuggestions.value = ret
|
listenerSuggestions.value = ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const exitNodesSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchExitNodesSuggestions(e: { query: string }) {
|
||||||
|
const ret = []
|
||||||
|
ret.push(e.query)
|
||||||
|
exitNodesSuggestions.value = ret
|
||||||
|
}
|
||||||
|
|
||||||
|
const whitelistSuggestions = ref([''])
|
||||||
|
|
||||||
|
function searchWhitelistSuggestions(e: { query: string }) {
|
||||||
|
const ret = []
|
||||||
|
ret.push(e.query)
|
||||||
|
whitelistSuggestions.value = ret
|
||||||
|
}
|
||||||
|
|
||||||
interface BoolFlag {
|
interface BoolFlag {
|
||||||
field: keyof NetworkConfig
|
field: keyof NetworkConfig
|
||||||
help: string
|
help: string
|
||||||
@@ -133,6 +150,13 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'disable_p2p', help: 'disable_p2p_help' },
|
{ field: 'disable_p2p', help: 'disable_p2p_help' },
|
||||||
{ field: 'bind_device', help: 'bind_device_help' },
|
{ field: 'bind_device', help: 'bind_device_help' },
|
||||||
{ field: 'no_tun', help: 'no_tun_help' },
|
{ field: 'no_tun', help: 'no_tun_help' },
|
||||||
|
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
|
||||||
|
{ field: 'relay_all_peer_rpc', help: 'relay_all_peer_rpc_help' },
|
||||||
|
{ field: 'multi_thread', help: 'multi_thread_help' },
|
||||||
|
{ field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' },
|
||||||
|
{ field: 'disable_encryption', help: 'disable_encryption_help' },
|
||||||
|
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
|
||||||
|
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
|
||||||
]
|
]
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -141,7 +165,7 @@ const bool_flags: BoolFlag[] = [
|
|||||||
<div class="frontend-lib">
|
<div class="frontend-lib">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="w-10/12 self-center ">
|
<div class="w-11/12 self-center ">
|
||||||
<Panel :header="t('basic_settings')">
|
<Panel :header="t('basic_settings')">
|
||||||
<div class="flex flex-col gap-y-2">
|
<div class="flex flex-col gap-y-2">
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
@@ -174,8 +198,8 @@ const bool_flags: BoolFlag[] = [
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||||
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
<Password id="network_secret" v-model="curNetwork.network_secret"
|
||||||
aria-describedby="network_secret-help" />
|
aria-describedby="network_secret-help" toggleMask :feedback="false"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -209,7 +233,7 @@ const bool_flags: BoolFlag[] = [
|
|||||||
<label> {{ t('flags_switch') }} </label>
|
<label> {{ t('flags_switch') }} </label>
|
||||||
<div class="flex flex-row flex-wrap">
|
<div class="flex flex-row flex-wrap">
|
||||||
|
|
||||||
<div class="basis-64 flex" v-for="flag in bool_flags">
|
<div class="basis-[20rem] flex items-center" v-for="flag in bool_flags">
|
||||||
<Checkbox v-model="curNetwork[flag.field]" :input-id="flag.field" :binary="true" />
|
<Checkbox v-model="curNetwork[flag.field]" :input-id="flag.field" :binary="true" />
|
||||||
<label :for="flag.field" class="ml-2"> {{ t(flag.field) }} </label>
|
<label :for="flag.field" class="ml-2"> {{ t(flag.field) }} </label>
|
||||||
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t(flag.help)"></span>
|
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t(flag.help)"></span>
|
||||||
@@ -242,17 +266,20 @@ const bool_flags: BoolFlag[] = [
|
|||||||
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||||
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
||||||
<div class="min-w-64">
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
<InputGroup>
|
<div class="flex flex-col gap-2 basis-8/12 grow">
|
||||||
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
<InputGroup>
|
||||||
:placeholder="t('vpn_portal_client_network')" />
|
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||||
<InputGroupAddon>
|
:placeholder="t('vpn_portal_client_network')" />
|
||||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
<InputGroupAddon>
|
||||||
</InputGroupAddon>
|
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||||
</InputGroup>
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
|
</div>
|
||||||
:min="0" :max="65535" class="w-8/12" fluid />
|
<div class="flex flex-col gap-2 basis-3/12 grow">
|
||||||
|
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
|
||||||
|
:min="0" :max="65535" fluid />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +310,97 @@ const bool_flags: BoolFlag[] = [
|
|||||||
:placeholder="t('dev_name_placeholder')" />
|
:placeholder="t('dev_name_placeholder')" />
|
||||||
</div>
|
</div>
|
||||||
</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="mtu">{{ t('mtu') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center"
|
||||||
|
v-tooltip="t('mtu_help')"></span>
|
||||||
|
</div>
|
||||||
|
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help"
|
||||||
|
:format="false" :placeholder="t('mtu_placeholder')" :min="400" :max="1380" 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">
|
||||||
|
<label for="relay_network_whitelist">{{ t('relay_network_whitelist') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center"
|
||||||
|
v-tooltip="t('relay_network_whitelist_help')"></span>
|
||||||
|
</div>
|
||||||
|
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||||
|
<div v-if="curNetwork.enable_relay_network_whitelist" class="items-center flex flex-row gap-x-4">
|
||||||
|
<div class="min-w-64 w-full">
|
||||||
|
<AutoComplete id="relay_network_whitelist" v-model="curNetwork.relay_network_whitelist"
|
||||||
|
:placeholder="t('relay_network_whitelist')" class="w-full" multiple fluid
|
||||||
|
:suggestions="whitelistSuggestions" @complete="searchWhitelistSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<div class="flex">
|
||||||
|
<label for="routes">{{ t('manual_routes') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('manual_routes_help')"></span>
|
||||||
|
</div>
|
||||||
|
<ToggleButton v-model="curNetwork.enable_manual_routes" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||||
|
<div v-if="curNetwork.enable_manual_routes" class="items-center flex flex-row gap-x-4">
|
||||||
|
<div class="min-w-64 w-full">
|
||||||
|
<AutoComplete id="routes" v-model="curNetwork.routes"
|
||||||
|
:placeholder="t('chips_placeholder', ['192.168.0.0/16'])" class="w-full" multiple fluid
|
||||||
|
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<div class="flex">
|
||||||
|
<label for="socks5_port">{{ t('socks5') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('socks5_help')"></span>
|
||||||
|
</div>
|
||||||
|
<ToggleButton v-model="curNetwork.enable_socks5" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||||
|
<div v-if="curNetwork.enable_socks5" class="items-center flex flex-row gap-x-4">
|
||||||
|
<div class="min-w-64 w-full">
|
||||||
|
<InputNumber id="socks5_port" v-model="curNetwork.socks5_port" aria-describedby="rpc_port-help"
|
||||||
|
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-col gap-2 grow p-fluid">
|
||||||
|
<div class="flex">
|
||||||
|
<label for="exit_nodes">{{ t('exit_nodes') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('exit_nodes_help')"></span>
|
||||||
|
</div>
|
||||||
|
<AutoComplete id="exit_nodes" v-model="curNetwork.exit_nodes"
|
||||||
|
:placeholder="t('chips_placeholder', ['192.168.8.8'])" class="w-full" multiple fluid
|
||||||
|
:suggestions="exitNodesSuggestions" @complete="searchExitNodesSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||||
|
<div class="flex flex-col gap-2 grow p-fluid">
|
||||||
|
<div class="flex">
|
||||||
|
<label for="mapped_listeners">{{ t('mapped_listeners') }}</label>
|
||||||
|
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mapped_listeners_help')"></span>
|
||||||
|
</div>
|
||||||
|
<AutoComplete id="mapped_listeners" v-model="curNetwork.mapped_listeners"
|
||||||
|
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full"
|
||||||
|
multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
@@ -303,7 +303,8 @@ function showEventLogs() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="frontend-lib">
|
<div class="frontend-lib">
|
||||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto max-w-full">
|
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-full h-auto max-h-full"
|
||||||
|
:baseZIndex="2000">
|
||||||
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||||
<pre>{{ dialogContent }}</pre>
|
<pre>{{ dialogContent }}</pre>
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
|
@@ -75,13 +75,13 @@ latency_first: 开启延迟优先模式
|
|||||||
latency_first_help: 忽略中转跳数,选择总延迟最低的路径
|
latency_first_help: 忽略中转跳数,选择总延迟最低的路径
|
||||||
|
|
||||||
use_smoltcp: 使用用户态协议栈
|
use_smoltcp: 使用用户态协议栈
|
||||||
use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙问题导致无法子网代理。
|
use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙问题导致无法子网代理 / KCP代理。
|
||||||
|
|
||||||
enable_kcp_proxy: 启用 KCP 代理
|
enable_kcp_proxy: 启用 KCP 代理
|
||||||
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
|
||||||
|
|
||||||
disable_kcp_input: 禁用 KCP 输入
|
disable_kcp_input: 禁用 KCP 输入
|
||||||
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点无法连接到本节点。
|
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。
|
||||||
|
|
||||||
disable_p2p: 禁用 P2P
|
disable_p2p: 禁用 P2P
|
||||||
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
||||||
@@ -92,6 +92,56 @@ bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网
|
|||||||
no_tun: 无 TUN 模式
|
no_tun: 无 TUN 模式
|
||||||
no_tun_help: 不使用 TUN 网卡,适合无管理员权限时使用。本节点仅允许被访问。访问其他节点需要使用 SOCK5
|
no_tun_help: 不使用 TUN 网卡,适合无管理员权限时使用。本节点仅允许被访问。访问其他节点需要使用 SOCK5
|
||||||
|
|
||||||
|
enable_exit_node: 启用出口节点
|
||||||
|
enable_exit_node_help: 允许此节点成为出口节点
|
||||||
|
|
||||||
|
relay_all_peer_rpc: 转发RPC包
|
||||||
|
relay_all_peer_rpc_help: |
|
||||||
|
允许转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。
|
||||||
|
这可以帮助白名单外网络中的对等节点建立P2P连接。
|
||||||
|
|
||||||
|
multi_thread: 启用多线程
|
||||||
|
multi_thread_help: 使用多线程运行时
|
||||||
|
|
||||||
|
proxy_forward_by_system: 系统转发
|
||||||
|
proxy_forward_by_system_help: 通过系统内核转发子网代理数据包,禁用内置NAT
|
||||||
|
|
||||||
|
disable_encryption: 禁用加密
|
||||||
|
disable_encryption_help: 禁用对等节点通信的加密,默认为false,必须与对等节点相同
|
||||||
|
|
||||||
|
disable_udp_hole_punching: 禁用UDP打洞
|
||||||
|
disable_udp_hole_punching_help: 禁用UDP打洞功能
|
||||||
|
|
||||||
|
enable_magic_dns: 启用魔法DNS
|
||||||
|
enable_magic_dns_help: |
|
||||||
|
启用魔法DNS,允许通过EasyTier的DNS服务器访问其他节点的虚拟IPv4地址, 如 node1.et.net。
|
||||||
|
|
||||||
|
relay_network_whitelist: 网络白名单
|
||||||
|
relay_network_whitelist_help: |
|
||||||
|
仅转发白名单网络的流量,支持通配符字符串。多个网络名称间可以使用英文空格间隔。
|
||||||
|
如果该参数为空,则禁用转发。默认允许所有网络。
|
||||||
|
例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2)
|
||||||
|
|
||||||
|
manual_routes: 自定义路由
|
||||||
|
manual_routes_help: 手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16
|
||||||
|
|
||||||
|
socks5: socks5服务器
|
||||||
|
socks5_help: |
|
||||||
|
启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080
|
||||||
|
|
||||||
|
exit_nodes: 出口节点列表
|
||||||
|
exit_nodes_help: 转发所有流量的出口节点,虚拟IPv4地址,优先级由列表顺序决定
|
||||||
|
|
||||||
|
mtu: MTU
|
||||||
|
mtu_help: |
|
||||||
|
TUN设备的MTU,默认为非加密时为1380,加密时为1360。范围:400-1380
|
||||||
|
mtu_placeholder: 留空为默认值1380
|
||||||
|
|
||||||
|
mapped_listeners: 监听映射
|
||||||
|
mapped_listeners_help: |
|
||||||
|
手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。
|
||||||
|
例如:tcp://123.123.123.123:11223,可以指定多个。
|
||||||
|
|
||||||
status:
|
status:
|
||||||
version: 内核版本
|
version: 内核版本
|
||||||
local: 本机
|
local: 本机
|
||||||
@@ -136,4 +186,4 @@ event:
|
|||||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||||
|
PortForwardAdded: 端口转发添加
|
||||||
|
@@ -68,6 +68,80 @@ upload_bytes: Upload
|
|||||||
download_bytes: Download
|
download_bytes: Download
|
||||||
loss_rate: Loss Rate
|
loss_rate: Loss Rate
|
||||||
|
|
||||||
|
flags_switch: Feature Switch
|
||||||
|
|
||||||
|
latency_first: Enable Latency-First Mode
|
||||||
|
latency_first_help: Ignore hop count and select the path with the lowest total latency
|
||||||
|
|
||||||
|
use_smoltcp: Use User-Space Protocol Stack
|
||||||
|
use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating system firewalls blocking subnet or KCP proxy functionality.
|
||||||
|
|
||||||
|
enable_kcp_proxy: Enable KCP Proxy
|
||||||
|
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
|
||||||
|
|
||||||
|
disable_kcp_input: Disable KCP Input
|
||||||
|
disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP.
|
||||||
|
|
||||||
|
disable_p2p: Disable P2P
|
||||||
|
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
|
||||||
|
|
||||||
|
bind_device: Bind to Physical Device Only
|
||||||
|
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
|
||||||
|
|
||||||
|
no_tun: No TUN Mode
|
||||||
|
no_tun_help: Do not use a TUN interface, suitable for environments without administrator privileges. This node is only accessible; accessing other nodes requires SOCKS5.
|
||||||
|
|
||||||
|
enable_exit_node: Enable Exit Node
|
||||||
|
enable_exit_node_help: Allow this node to be an exit node
|
||||||
|
|
||||||
|
relay_all_peer_rpc: Relay RPC Packets
|
||||||
|
relay_all_peer_rpc_help: |
|
||||||
|
Relay all peer rpc packets, even if the peer is not in the relay network whitelist.
|
||||||
|
This can help peers not in relay network whitelist to establish p2p connection.
|
||||||
|
|
||||||
|
multi_thread: Multi Thread
|
||||||
|
multi_thread_help: Use multi-thread runtime
|
||||||
|
|
||||||
|
proxy_forward_by_system: System Forward
|
||||||
|
proxy_forward_by_system_help: Forward packet to proxy networks via system kernel, disable internal nat for network proxy
|
||||||
|
|
||||||
|
disable_encryption: Disable Encryption
|
||||||
|
disable_encryption_help: Disable encryption for peers communication, default is false, must be same with peers
|
||||||
|
|
||||||
|
disable_udp_hole_punching: Disable UDP Hole Punching
|
||||||
|
disable_udp_hole_punching_help: Disable udp hole punching
|
||||||
|
|
||||||
|
enable_magic_dns: Enable Magic DNS
|
||||||
|
enable_magic_dns_help: |
|
||||||
|
Enable magic dns, all nodes in the network can access each other by domain name, e.g.: node1.et.net.
|
||||||
|
|
||||||
|
relay_network_whitelist: Network Whitelist
|
||||||
|
relay_network_whitelist_help: |
|
||||||
|
Only forward traffic from the whitelist networks, supporting wildcard strings, multiple network names can be separated by spaces.
|
||||||
|
If this parameter is empty, forwarding is disabled. By default, all networks are allowed.
|
||||||
|
e.g.: '*' (all networks), 'def*' (networks with the prefix 'def'), 'net1 net2' (only allow net1 and net2)
|
||||||
|
|
||||||
|
manual_routes: Manual Route
|
||||||
|
manual_routes_help: |
|
||||||
|
Assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.:192.168.0.0/16
|
||||||
|
|
||||||
|
socks5: Socks5 Server
|
||||||
|
socks5_help: |
|
||||||
|
Enable socks5 server, allow socks5 client to access virtual network. format: <port>, e.g.: 1080
|
||||||
|
|
||||||
|
exit_nodes: Exit Nodes
|
||||||
|
exit_nodes_help: Exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list
|
||||||
|
|
||||||
|
mtu: MTU
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
e.g.: tcp://123.123.123.123:11223, can specify multiple.
|
||||||
|
|
||||||
status:
|
status:
|
||||||
version: Version
|
version: Version
|
||||||
local: Local
|
local: Local
|
||||||
@@ -112,3 +186,4 @@ event:
|
|||||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||||
DhcpIpv4Changed: DhcpIpv4Changed
|
DhcpIpv4Changed: DhcpIpv4Changed
|
||||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||||
|
PortForwardAdded: PortForwardAdded
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
import { Md5 } from 'ts-md5'
|
import { Md5 } from 'ts-md5'
|
||||||
import { UUID } from './utils';
|
import { UUID } from './utils';
|
||||||
|
import { NetworkConfig } from '../types/network';
|
||||||
|
|
||||||
export interface ValidateConfigResponse {
|
export interface ValidateConfigResponse {
|
||||||
toml_config: string;
|
toml_config: string;
|
||||||
@@ -37,6 +38,15 @@ export interface ListNetworkInstanceIdResponse {
|
|||||||
disabled_inst_ids: Array<UUID>,
|
disabled_inst_ids: Array<UUID>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GenerateConfigRequest {
|
||||||
|
config: NetworkConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateConfigResponse {
|
||||||
|
toml_config?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
private client: AxiosInstance;
|
private client: AxiosInstance;
|
||||||
private authFailedCb: Function | undefined;
|
private authFailedCb: Function | undefined;
|
||||||
@@ -193,6 +203,18 @@ export class ApiClient {
|
|||||||
public captcha_url() {
|
public captcha_url() {
|
||||||
return this.client.defaults.baseURL + '/auth/captcha';
|
return this.client.defaults.baseURL + '/auth/captcha';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async generate_config(config: GenerateConfigRequest): Promise<GenerateConfigResponse> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
return { error: error.response?.data };
|
||||||
|
}
|
||||||
|
return { error: 'Unknown error: ' + error };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiClient;
|
export default ApiClient;
|
@@ -1,8 +1,6 @@
|
|||||||
@import 'primeicons/primeicons.css';
|
@import 'primeicons/primeicons.css';
|
||||||
@import 'floating-vue/dist/style.css';
|
@import 'floating-vue/dist/style.css';
|
||||||
|
|
||||||
.frontend-lib {
|
|
||||||
|
|
||||||
@layer tailwind-base, primevue, tailwind-utilities;
|
@layer tailwind-base, primevue, tailwind-utilities;
|
||||||
|
|
||||||
@layer tailwind-base {
|
@layer tailwind-base {
|
||||||
@@ -51,4 +49,6 @@
|
|||||||
background-color: #0000005d;
|
background-color: #0000005d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-popper__inner {
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
@@ -42,6 +42,28 @@ export interface NetworkConfig {
|
|||||||
disable_p2p?: boolean
|
disable_p2p?: boolean
|
||||||
bind_device?: boolean
|
bind_device?: boolean
|
||||||
no_tun?: boolean
|
no_tun?: boolean
|
||||||
|
enable_exit_node?: boolean
|
||||||
|
relay_all_peer_rpc?: boolean
|
||||||
|
multi_thread?: boolean
|
||||||
|
proxy_forward_by_system?: boolean
|
||||||
|
disable_encryption?: boolean
|
||||||
|
disable_udp_hole_punching?: boolean
|
||||||
|
|
||||||
|
enable_relay_network_whitelist?: boolean
|
||||||
|
relay_network_whitelist: string[]
|
||||||
|
|
||||||
|
enable_manual_routes: boolean
|
||||||
|
routes: string[]
|
||||||
|
|
||||||
|
exit_nodes: string[]
|
||||||
|
|
||||||
|
enable_socks5?: boolean
|
||||||
|
socks5_port: number
|
||||||
|
|
||||||
|
mtu: number | null
|
||||||
|
mapped_listeners: string[]
|
||||||
|
|
||||||
|
enable_magic_dns?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||||
@@ -83,6 +105,22 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
disable_p2p: false,
|
disable_p2p: false,
|
||||||
bind_device: true,
|
bind_device: true,
|
||||||
no_tun: false,
|
no_tun: false,
|
||||||
|
enable_exit_node: false,
|
||||||
|
relay_all_peer_rpc: false,
|
||||||
|
multi_thread: true,
|
||||||
|
proxy_forward_by_system: false,
|
||||||
|
disable_encryption: false,
|
||||||
|
disable_udp_hole_punching: false,
|
||||||
|
enable_relay_network_whitelist: false,
|
||||||
|
relay_network_whitelist: [],
|
||||||
|
enable_manual_routes: false,
|
||||||
|
routes: [],
|
||||||
|
exit_nodes: [],
|
||||||
|
enable_socks5: false,
|
||||||
|
socks5_port: 1080,
|
||||||
|
mtu: null,
|
||||||
|
mapped_listeners: [],
|
||||||
|
enable_magic_dns: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,4 +267,6 @@ export enum EventType {
|
|||||||
|
|
||||||
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
|
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
|
||||||
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
||||||
|
|
||||||
|
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
|
||||||
}
|
}
|
||||||
|
@@ -9,11 +9,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "^4.2.1",
|
"@primevue/themes": "4.3.3",
|
||||||
"aura": "link:@primevue/themes/aura",
|
"aura": "link:@primevue/themes/aura",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"primevue": "^4.2.1",
|
"primevue": "4.3.3",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "4"
|
"vue-router": "4"
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "=3.4.17",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^5.4.10",
|
"vite": "^5.4.10",
|
||||||
"vite-plugin-singlefile": "^2.0.3",
|
"vite-plugin-singlefile": "^2.0.3",
|
||||||
"vue-tsc": "^2.1.10"
|
"vue-tsc": "^2.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
63
easytier-web/frontend/src/components/ConfigGenerator.vue
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import { Api } from 'easytier-frontend-lib'
|
||||||
|
import {AutoComplete, Divider} from "primevue";
|
||||||
|
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||||
|
|
||||||
|
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||||
|
|
||||||
|
|
||||||
|
const apiHost = ref<string>(getInitialApiHost())
|
||||||
|
const apiHostSuggestions = ref<Array<string>>([])
|
||||||
|
const apiHostSearch = async (event: { query: string }) => {
|
||||||
|
apiHostSuggestions.value = [];
|
||||||
|
let hosts = cleanAndLoadApiHosts();
|
||||||
|
if (event.query) {
|
||||||
|
apiHostSuggestions.value.push(event.query);
|
||||||
|
}
|
||||||
|
hosts.forEach((host) => {
|
||||||
|
apiHostSuggestions.value.push(host.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||||
|
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
|
||||||
|
|
||||||
|
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||||
|
saveApiHost(apiHost.value)
|
||||||
|
api.value?.generate_config({
|
||||||
|
config: config
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
toml_config.value = res.error;
|
||||||
|
} else if (res.toml_config) {
|
||||||
|
toml_config.value = res.toml_config;
|
||||||
|
} else {
|
||||||
|
toml_config.value = "Api server returned an unexpected response";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-center m-5">
|
||||||
|
<div class="sm:block md:flex w-full">
|
||||||
|
<div class="sm:w-full md:w-1/2 p-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-11/12 self-center ">
|
||||||
|
<label>ApiHost</label>
|
||||||
|
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||||
|
@complete="apiHostSearch" class="w-full" />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||||
|
</div>
|
||||||
|
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
|
||||||
|
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -27,7 +27,7 @@ const loadDevices = async () => {
|
|||||||
public_ip: device.client_url,
|
public_ip: device.client_url,
|
||||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
||||||
running_network_count: device.info?.running_network_instances.length,
|
running_network_count: device.info?.running_network_instances.length,
|
||||||
report_time: device.info?.report_time,
|
report_time: new Date(device.info?.report_time).toLocaleString(),
|
||||||
easytier_version: device.info?.easytier_version,
|
easytier_version: device.info?.easytier_version,
|
||||||
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
||||||
});
|
});
|
||||||
@@ -102,7 +102,7 @@ const selectedDeviceHostname = computed<string | undefined>(() => {
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||||
class="w-1/2 min-w-96">
|
:baseZIndex=1000 class="w-3/5 min-w-96">
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component }">
|
||||||
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||||
</RouterView>
|
</RouterView>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue';
|
import {Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider} from 'primevue';
|
||||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
||||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -27,6 +27,8 @@ const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
|
|||||||
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const configFile = ref();
|
||||||
|
|
||||||
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||||
|
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
@@ -158,6 +160,7 @@ const createNewNetwork = async () => {
|
|||||||
|
|
||||||
const newNetwork = () => {
|
const newNetwork = () => {
|
||||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||||
|
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
|
||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
showCreateNetworkDialog.value = true;
|
showCreateNetworkDialog.value = true;
|
||||||
}
|
}
|
||||||
@@ -207,6 +210,65 @@ const loadDeviceInfo = async () => {
|
|||||||
} as NetworkTypes.NetworkInstance;
|
} as NetworkTypes.NetworkInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportConfig = async () => {
|
||||||
|
if (!deviceId.value || !instanceId.value) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||||
|
delete ret.instance_id;
|
||||||
|
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importConfig = () => {
|
||||||
|
configFile.value.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = (event: Event) => {
|
||||||
|
const files = (event.target as HTMLInputElement).files;
|
||||||
|
const file = files ? files[0] : null;
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
let str = e.target?.result?.toString();
|
||||||
|
if(str){
|
||||||
|
const config = JSON.parse(str);
|
||||||
|
if(config === null || typeof config !== "object"){
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
Object.assign(newNetworkConfig.value, config);
|
||||||
|
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
|
||||||
|
}
|
||||||
|
configFile.value.value = null;
|
||||||
|
}
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportJsonFile = (context: string, name: string) => {
|
||||||
|
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
|
||||||
|
let link = document.createElement('a');
|
||||||
|
link.style.display = 'none';
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', name);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
||||||
@@ -226,9 +288,16 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
|
||||||
<ConfirmPopup></ConfirmPopup>
|
<ConfirmPopup></ConfirmPopup>
|
||||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
||||||
:style="{ width: '55rem' }">
|
:style="{ width: '55rem' }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="w-11/12 self-center ">
|
||||||
|
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
@@ -245,19 +314,23 @@ onUnmounted(() => {
|
|||||||
<div class="gap-x-3 flex">
|
<div class="gap-x-3 flex">
|
||||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||||
iconPos="right" />
|
iconPos="right" />
|
||||||
|
<Button @click="exportConfig" icon="pi pi-file-export" severity="help" label="Export" iconPos="right" />
|
||||||
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
||||||
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<!-- For running network, show the status -->
|
<!-- For running network, show the status -->
|
||||||
<div v-if="needShowNetworkStatus">
|
<div v-if="needShowNetworkStatus">
|
||||||
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus">
|
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus">
|
||||||
</Status>
|
</Status>
|
||||||
<center>
|
<Divider />
|
||||||
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
|
<div class="text-center">
|
||||||
</center>
|
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- For disabled network, show the config -->
|
<!-- For disabled network, show the config -->
|
||||||
|
@@ -4,6 +4,7 @@ import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { Api } from 'easytier-frontend-lib';
|
import { Api } from 'easytier-frontend-lib';
|
||||||
|
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
isRegistering: boolean;
|
isRegistering: boolean;
|
||||||
@@ -20,56 +21,6 @@ const registerPassword = ref('');
|
|||||||
const captcha = ref('');
|
const captcha = ref('');
|
||||||
const captchaSrc = computed(() => api.value.captcha_url());
|
const captchaSrc = computed(() => api.value.captcha_url());
|
||||||
|
|
||||||
interface ApiHost {
|
|
||||||
value: string;
|
|
||||||
usedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidHttpUrl = (s: string): boolean => {
|
|
||||||
let url;
|
|
||||||
|
|
||||||
try {
|
|
||||||
url = new URL(s);
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanAndLoadApiHosts = (): Array<ApiHost> => {
|
|
||||||
const maxHosts = 10;
|
|
||||||
const apiHosts = localStorage.getItem('apiHosts');
|
|
||||||
if (apiHosts) {
|
|
||||||
const hosts: Array<ApiHost> = JSON.parse(apiHosts);
|
|
||||||
// sort by usedAt
|
|
||||||
hosts.sort((a, b) => b.usedAt - a.usedAt);
|
|
||||||
|
|
||||||
// only keep the first 10
|
|
||||||
if (hosts.length > maxHosts) {
|
|
||||||
hosts.splice(maxHosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
|
||||||
return hosts;
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveApiHost = (host: string) => {
|
|
||||||
console.log('Save API Host:', host);
|
|
||||||
if (!isValidHttpUrl(host)) {
|
|
||||||
console.error('Invalid API Host:', host);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hosts = cleanAndLoadApiHosts();
|
|
||||||
const newHost: ApiHost = { value: host, usedAt: Date.now() };
|
|
||||||
hosts = hosts.filter((h) => h.value !== host);
|
|
||||||
hosts.push(newHost);
|
|
||||||
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
// Add your login logic here
|
// Add your login logic here
|
||||||
@@ -100,16 +51,6 @@ const onRegister = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialApiHost = (): string => {
|
|
||||||
const hosts = cleanAndLoadApiHosts();
|
|
||||||
if (hosts.length > 0) {
|
|
||||||
return hosts[0].value;
|
|
||||||
} else {
|
|
||||||
return defaultApiHost;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultApiHost = 'https://config-server.easytier.cn'
|
|
||||||
const apiHost = ref<string>(getInitialApiHost())
|
const apiHost = ref<string>(getInitialApiHost())
|
||||||
const apiHostSuggestions = ref<Array<string>>([])
|
const apiHostSuggestions = ref<Array<string>>([])
|
||||||
const apiHostSearch = async (event: { query: string }) => {
|
const apiHostSearch = async (event: { query: string }) => {
|
||||||
@@ -124,10 +65,7 @@ const apiHostSearch = async (event: { query: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
let hosts = cleanAndLoadApiHosts();
|
|
||||||
if (hosts.length === 0) {
|
|
||||||
saveApiHost(defaultApiHost);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.css'
|
|
||||||
import 'easytier-frontend-lib/style.css'
|
import 'easytier-frontend-lib/style.css'
|
||||||
|
import './style.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import EasytierFrontendLib from 'easytier-frontend-lib'
|
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
@@ -15,6 +15,7 @@ import DeviceManagement from './components/DeviceManagement.vue'
|
|||||||
import Dashboard from './components/Dashboard.vue'
|
import Dashboard from './components/Dashboard.vue'
|
||||||
import DialogService from 'primevue/dialogservice';
|
import DialogService from 'primevue/dialogservice';
|
||||||
import ToastService from 'primevue/toastservice';
|
import ToastService from 'primevue/toastservice';
|
||||||
|
import ConfigGenerator from './components/ConfigGenerator.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,10 @@ const routes = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/config_generator',
|
||||||
|
component: ConfigGenerator,
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
64
easytier-web/frontend/src/modules/api-host.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
const defaultApiHost = 'https://config-server.easytier.cn';
|
||||||
|
|
||||||
|
interface ApiHost {
|
||||||
|
value: string;
|
||||||
|
usedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidHttpUrl = (s: string): boolean => {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(s);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanAndLoadApiHosts = (): Array<ApiHost> => {
|
||||||
|
const maxHosts = 10;
|
||||||
|
const apiHosts = localStorage.getItem('apiHosts');
|
||||||
|
if (apiHosts) {
|
||||||
|
const hosts: Array<ApiHost> = JSON.parse(apiHosts);
|
||||||
|
// sort by usedAt
|
||||||
|
hosts.sort((a, b) => b.usedAt - a.usedAt);
|
||||||
|
|
||||||
|
// only keep the first 10
|
||||||
|
if (hosts.length > maxHosts) {
|
||||||
|
hosts.splice(maxHosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
||||||
|
return hosts;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveApiHost = (host: string) => {
|
||||||
|
console.log('Save API Host:', host);
|
||||||
|
if (!isValidHttpUrl(host)) {
|
||||||
|
console.error('Invalid API Host:', host);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hosts = cleanAndLoadApiHosts();
|
||||||
|
const newHost: ApiHost = {value: host, usedAt: Date.now()};
|
||||||
|
hosts = hosts.filter((h) => h.value !== host);
|
||||||
|
hosts.push(newHost);
|
||||||
|
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitialApiHost = (): string => {
|
||||||
|
const hosts = cleanAndLoadApiHosts();
|
||||||
|
if (hosts.length > 0) {
|
||||||
|
return hosts[0].value;
|
||||||
|
} else {
|
||||||
|
saveApiHost(defaultApiHost)
|
||||||
|
return defaultApiHost;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost}
|
@@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { viteSingleFile } from "vite-plugin-singlefile"
|
// import { viteSingleFile } from "vite-plugin-singlefile"
|
||||||
|
|
||||||
|
const WEB_BASE_URL = process.env.WEB_BASE_URL || '';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '',
|
base: WEB_BASE_URL,
|
||||||
plugins: [vue(), viteSingleFile()],
|
plugins: [vue(),/* viteSingleFile() */],
|
||||||
})
|
})
|
||||||
|
@@ -22,3 +22,9 @@ cli:
|
|||||||
api_server_port:
|
api_server_port:
|
||||||
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
|
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
|
||||||
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"
|
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"
|
||||||
|
web_server_port:
|
||||||
|
en: "The port to listen for the web dashboard server, default is same as the api server port"
|
||||||
|
zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同"
|
||||||
|
no_web:
|
||||||
|
en: "Do not run the web dashboard server"
|
||||||
|
zh-CN: "不运行 web dashboard 服务器"
|
@@ -10,7 +10,7 @@ use easytier::{
|
|||||||
use session::Session;
|
use session::Session;
|
||||||
use storage::{Storage, StorageToken};
|
use storage::{Storage, StorageToken};
|
||||||
|
|
||||||
use crate::db::Db;
|
use crate::db::{Db, UserIdInDb};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ClientManager {
|
pub struct ClientManager {
|
||||||
@@ -86,15 +86,21 @@ impl ClientManager {
|
|||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> {
|
pub fn get_session_by_machine_id(
|
||||||
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?;
|
&self,
|
||||||
|
user_id: UserIdInDb,
|
||||||
|
machine_id: &uuid::Uuid,
|
||||||
|
) -> Option<Arc<Session>> {
|
||||||
|
let c_url = self
|
||||||
|
.storage
|
||||||
|
.get_client_url_by_machine_id(user_id, machine_id)?;
|
||||||
self.client_sessions
|
self.client_sessions
|
||||||
.get(&c_url)
|
.get(&c_url)
|
||||||
.map(|item| item.value().clone())
|
.map(|item| item.value().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_machine_by_token(&self, token: String) -> Vec<url::Url> {
|
pub async fn list_machine_by_user_id(&self, user_id: UserIdInDb) -> Vec<url::Url> {
|
||||||
self.storage.list_token_clients(&token)
|
self.storage.list_user_clients(user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
|
||||||
@@ -118,6 +124,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
web_client::WebClient,
|
web_client::WebClient,
|
||||||
};
|
};
|
||||||
|
use sqlx::Executor;
|
||||||
|
|
||||||
use crate::{client_manager::ClientManager, db::Db};
|
use crate::{client_manager::ClientManager, db::Db};
|
||||||
|
|
||||||
@@ -127,8 +134,14 @@ mod tests {
|
|||||||
let mut mgr = ClientManager::new(Db::memory_db().await);
|
let mut mgr = ClientManager::new(Db::memory_db().await);
|
||||||
mgr.serve(Box::new(listener)).await.unwrap();
|
mgr.serve(Box::new(listener)).await.unwrap();
|
||||||
|
|
||||||
|
mgr.db()
|
||||||
|
.inner()
|
||||||
|
.execute("INSERT INTO users (username, password) VALUES ('test', 'test')")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
|
||||||
let _c = WebClient::new(connector, "test");
|
let _c = WebClient::new(connector, "test", "test");
|
||||||
|
|
||||||
wait_for_condition(
|
wait_for_condition(
|
||||||
|| async { mgr.client_sessions.len() == 1 },
|
|| async { mgr.client_sessions.len() == 1 },
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
use std::{fmt::Debug, str::FromStr as _, sync::Arc};
|
use std::{fmt::Debug, str::FromStr as _, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::scoped_task::ScopedTask,
|
common::scoped_task::ScopedTask,
|
||||||
proto::{
|
proto::{
|
||||||
@@ -68,6 +69,66 @@ struct SessionRpcService {
|
|||||||
data: SharedSessionData,
|
data: SharedSessionData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SessionRpcService {
|
||||||
|
async fn handle_heartbeat(
|
||||||
|
&self,
|
||||||
|
req: HeartbeatRequest,
|
||||||
|
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||||
|
let mut data = self.data.write().await;
|
||||||
|
|
||||||
|
let Ok(storage) = Storage::try_from(data.storage.clone()) else {
|
||||||
|
tracing::error!("Failed to get storage");
|
||||||
|
return Ok(HeartbeatResponse {});
|
||||||
|
};
|
||||||
|
|
||||||
|
let machine_id: uuid::Uuid =
|
||||||
|
req.machine_id
|
||||||
|
.clone()
|
||||||
|
.map(Into::into)
|
||||||
|
.ok_or(anyhow::anyhow!(
|
||||||
|
"Machine id is not set correctly, expect uuid but got: {:?}",
|
||||||
|
req.machine_id
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let user_id = storage
|
||||||
|
.db()
|
||||||
|
.get_user_id_by_token(req.user_token.clone())
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to get user id by token from db: {:?}",
|
||||||
|
req.user_token
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.ok_or(anyhow::anyhow!(
|
||||||
|
"User not found by token: {:?}",
|
||||||
|
req.user_token
|
||||||
|
))?;
|
||||||
|
|
||||||
|
if data.req.replace(req.clone()).is_none() {
|
||||||
|
assert!(data.storage_token.is_none());
|
||||||
|
data.storage_token = Some(StorageToken {
|
||||||
|
token: req.user_token.clone().into(),
|
||||||
|
client_url: data.client_url.clone(),
|
||||||
|
machine_id,
|
||||||
|
user_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(report_time) = chrono::DateTime::<chrono::Local>::from_str(&req.report_time) else {
|
||||||
|
tracing::error!("Failed to parse report time: {:?}", req.report_time);
|
||||||
|
return Ok(HeartbeatResponse {});
|
||||||
|
};
|
||||||
|
storage.update_client(
|
||||||
|
data.storage_token.as_ref().unwrap().clone(),
|
||||||
|
report_time.timestamp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = data.notifier.send(req);
|
||||||
|
Ok(HeartbeatResponse {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl WebServerService for SessionRpcService {
|
impl WebServerService for SessionRpcService {
|
||||||
type Controller = BaseController;
|
type Controller = BaseController;
|
||||||
@@ -77,34 +138,13 @@ impl WebServerService for SessionRpcService {
|
|||||||
_: BaseController,
|
_: BaseController,
|
||||||
req: HeartbeatRequest,
|
req: HeartbeatRequest,
|
||||||
) -> rpc_types::error::Result<HeartbeatResponse> {
|
) -> rpc_types::error::Result<HeartbeatResponse> {
|
||||||
let mut data = self.data.write().await;
|
let ret = self.handle_heartbeat(req).await;
|
||||||
if data.req.replace(req.clone()).is_none() {
|
if ret.is_err() {
|
||||||
assert!(data.storage_token.is_none());
|
tracing::warn!("Failed to handle heartbeat: {:?}", ret);
|
||||||
data.storage_token = Some(StorageToken {
|
// sleep for a while to avoid client busy loop
|
||||||
token: req.user_token.clone().into(),
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
client_url: data.client_url.clone(),
|
|
||||||
machine_id: req
|
|
||||||
.machine_id
|
|
||||||
.clone()
|
|
||||||
.map(Into::into)
|
|
||||||
.unwrap_or(uuid::Uuid::new_v4()),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
ret
|
||||||
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
|
|
||||||
let Ok(report_time) = chrono::DateTime::<chrono::Local>::from_str(&req.report_time)
|
|
||||||
else {
|
|
||||||
tracing::error!("Failed to parse report time: {:?}", req.report_time);
|
|
||||||
return Ok(HeartbeatResponse {});
|
|
||||||
};
|
|
||||||
storage.update_client(
|
|
||||||
data.storage_token.as_ref().unwrap().clone(),
|
|
||||||
report_time.timestamp(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = data.notifier.send(req);
|
|
||||||
Ok(HeartbeatResponse {})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ use std::sync::{Arc, Weak};
|
|||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
|
||||||
use crate::db::Db;
|
use crate::db::{Db, UserIdInDb};
|
||||||
|
|
||||||
// use this to maintain Storage
|
// use this to maintain Storage
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -10,21 +10,19 @@ pub struct StorageToken {
|
|||||||
pub token: String,
|
pub token: String,
|
||||||
pub client_url: url::Url,
|
pub client_url: url::Url,
|
||||||
pub machine_id: uuid::Uuid,
|
pub machine_id: uuid::Uuid,
|
||||||
|
pub user_id: UserIdInDb,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct ClientInfo {
|
struct ClientInfo {
|
||||||
client_url: url::Url,
|
storage_token: StorageToken,
|
||||||
machine_id: uuid::Uuid,
|
|
||||||
token: String,
|
|
||||||
report_time: i64,
|
report_time: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StorageInner {
|
pub struct StorageInner {
|
||||||
// some map for indexing
|
// some map for indexing
|
||||||
token_clients_map: DashMap<String, DashMap<uuid::Uuid, ClientInfo>>,
|
user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
|
||||||
machine_client_url_map: DashMap<uuid::Uuid, ClientInfo>,
|
|
||||||
pub db: Db,
|
pub db: Db,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,8 +41,7 @@ impl TryFrom<WeakRefStorage> for Storage {
|
|||||||
impl Storage {
|
impl Storage {
|
||||||
pub fn new(db: Db) -> Self {
|
pub fn new(db: Db) -> Self {
|
||||||
Storage(Arc::new(StorageInner {
|
Storage(Arc::new(StorageInner {
|
||||||
token_clients_map: DashMap::new(),
|
user_clients_map: DashMap::new(),
|
||||||
machine_client_url_map: DashMap::new(),
|
|
||||||
db,
|
db,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -54,17 +51,22 @@ impl Storage {
|
|||||||
machine_id: &uuid::Uuid,
|
machine_id: &uuid::Uuid,
|
||||||
client_url: &url::Url,
|
client_url: &url::Url,
|
||||||
) {
|
) {
|
||||||
map.remove_if(&machine_id, |_, v| v.client_url == *client_url);
|
map.remove_if(&machine_id, |_, v| {
|
||||||
|
v.storage_token.client_url == *client_url
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_mid_to_client_info_map(
|
fn update_mid_to_client_info_map(
|
||||||
map: &DashMap<uuid::Uuid, ClientInfo>,
|
map: &DashMap<uuid::Uuid, ClientInfo>,
|
||||||
client_info: &ClientInfo,
|
client_info: &ClientInfo,
|
||||||
) {
|
) {
|
||||||
map.entry(client_info.machine_id)
|
map.entry(client_info.storage_token.machine_id)
|
||||||
.and_modify(|e| {
|
.and_modify(|e| {
|
||||||
if e.report_time < client_info.report_time {
|
if e.report_time < client_info.report_time {
|
||||||
assert_eq!(e.machine_id, client_info.machine_id);
|
assert_eq!(
|
||||||
|
e.storage_token.machine_id,
|
||||||
|
client_info.storage_token.machine_id
|
||||||
|
);
|
||||||
*e = client_info.clone();
|
*e = client_info.clone();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -74,53 +76,51 @@ impl Storage {
|
|||||||
pub fn update_client(&self, stoken: StorageToken, report_time: i64) {
|
pub fn update_client(&self, stoken: StorageToken, report_time: i64) {
|
||||||
let inner = self
|
let inner = self
|
||||||
.0
|
.0
|
||||||
.token_clients_map
|
.user_clients_map
|
||||||
.entry(stoken.token.clone())
|
.entry(stoken.user_id)
|
||||||
.or_insert_with(DashMap::new);
|
.or_insert_with(DashMap::new);
|
||||||
|
|
||||||
let client_info = ClientInfo {
|
let client_info = ClientInfo {
|
||||||
client_url: stoken.client_url.clone(),
|
storage_token: stoken.clone(),
|
||||||
machine_id: stoken.machine_id,
|
|
||||||
token: stoken.token.clone(),
|
|
||||||
report_time,
|
report_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::update_mid_to_client_info_map(&inner, &client_info);
|
Self::update_mid_to_client_info_map(&inner, &client_info);
|
||||||
Self::update_mid_to_client_info_map(&self.0.machine_client_url_map, &client_info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_client(&self, stoken: &StorageToken) {
|
pub fn remove_client(&self, stoken: &StorageToken) {
|
||||||
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
|
self.0
|
||||||
Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url);
|
.user_clients_map
|
||||||
set.is_empty()
|
.remove_if(&stoken.user_id, |_, set| {
|
||||||
});
|
Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url);
|
||||||
|
set.is_empty()
|
||||||
Self::remove_mid_to_client_info_map(
|
});
|
||||||
&self.0.machine_client_url_map,
|
|
||||||
&stoken.machine_id,
|
|
||||||
&stoken.client_url,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn weak_ref(&self) -> WeakRefStorage {
|
pub fn weak_ref(&self) -> WeakRefStorage {
|
||||||
Arc::downgrade(&self.0)
|
Arc::downgrade(&self.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_client_url_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<url::Url> {
|
pub fn get_client_url_by_machine_id(
|
||||||
self.0
|
&self,
|
||||||
.machine_client_url_map
|
user_id: UserIdInDb,
|
||||||
.get(&machine_id)
|
machine_id: &uuid::Uuid,
|
||||||
.map(|info| info.client_url.clone())
|
) -> Option<url::Url> {
|
||||||
|
self.0.user_clients_map.get(&user_id).and_then(|info_map| {
|
||||||
|
info_map
|
||||||
|
.get(machine_id)
|
||||||
|
.map(|info| info.storage_token.client_url.clone())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> {
|
pub fn list_user_clients(&self, user_id: UserIdInDb) -> Vec<url::Url> {
|
||||||
self.0
|
self.0
|
||||||
.token_clients_map
|
.user_clients_map
|
||||||
.get(token)
|
.get(&user_id)
|
||||||
.map(|info_map| {
|
.map(|info_map| {
|
||||||
info_map
|
info_map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|info| info.value().client_url.clone())
|
.map(|info| info.value().storage_token.client_url.clone())
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
@@ -12,7 +12,7 @@ use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
|
|||||||
|
|
||||||
use crate::migrator;
|
use crate::migrator;
|
||||||
|
|
||||||
type UserIdInDb = i32;
|
pub type UserIdInDb = i32;
|
||||||
|
|
||||||
pub enum ListNetworkProps {
|
pub enum ListNetworkProps {
|
||||||
All,
|
All,
|
||||||
|
@@ -5,13 +5,14 @@ extern crate rust_i18n;
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use clap::{command, Parser};
|
use clap::Parser;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::{
|
common::{
|
||||||
config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader},
|
config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader},
|
||||||
constants::EASYTIER_VERSION,
|
constants::EASYTIER_VERSION,
|
||||||
|
error::Error,
|
||||||
},
|
},
|
||||||
tunnel::udp::UdpTunnelListener,
|
tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, TunnelListener},
|
||||||
utils::{init_logger, setup_panic_handler},
|
utils::{init_logger, setup_panic_handler},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +21,9 @@ mod db;
|
|||||||
mod migrator;
|
mod migrator;
|
||||||
mod restful;
|
mod restful;
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
mod web;
|
||||||
|
|
||||||
rust_i18n::i18n!("locales", fallback = "en");
|
rust_i18n::i18n!("locales", fallback = "en");
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -69,6 +73,32 @@ struct Cli {
|
|||||||
help = t!("cli.api_server_port").to_string(),
|
help = t!("cli.api_server_port").to_string(),
|
||||||
)]
|
)]
|
||||||
api_server_port: u16,
|
api_server_port: u16,
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short='l',
|
||||||
|
help = t!("cli.web_server_port").to_string(),
|
||||||
|
)]
|
||||||
|
web_server_port: Option<u16>,
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = t!("cli.no_web").to_string(),
|
||||||
|
default_value = "false"
|
||||||
|
)]
|
||||||
|
no_web: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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())),
|
||||||
|
_ => {
|
||||||
|
return Err(Error::InvalidUrl(l.to_string()));
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -92,25 +122,50 @@ async fn main() {
|
|||||||
// let db = db::Db::new(":memory:").await.unwrap();
|
// let db = db::Db::new(":memory:").await.unwrap();
|
||||||
let db = db::Db::new(cli.db).await.unwrap();
|
let db = db::Db::new(cli.db).await.unwrap();
|
||||||
|
|
||||||
let listener = UdpTunnelListener::new(
|
let listener = get_listener_by_url(
|
||||||
format!(
|
&format!(
|
||||||
"{}://0.0.0.0:{}",
|
"{}://0.0.0.0:{}",
|
||||||
cli.config_server_protocol, cli.config_server_port
|
cli.config_server_protocol, cli.config_server_port
|
||||||
)
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
)
|
||||||
|
.unwrap();
|
||||||
let mut mgr = client_manager::ClientManager::new(db.clone());
|
let mut mgr = client_manager::ClientManager::new(db.clone());
|
||||||
mgr.serve(listener).await.unwrap();
|
mgr.serve(listener).await.unwrap();
|
||||||
let mgr = Arc::new(mgr);
|
let mgr = Arc::new(mgr);
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
let restful_also_serve_web = !cli.no_web
|
||||||
|
&& (cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "embed"))]
|
||||||
|
let restful_also_serve_web = false;
|
||||||
|
|
||||||
let mut restful_server = restful::RestfulServer::new(
|
let mut restful_server = restful::RestfulServer::new(
|
||||||
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
|
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
|
||||||
mgr.clone(),
|
mgr.clone(),
|
||||||
db,
|
db,
|
||||||
|
restful_also_serve_web,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
restful_server.start().await.unwrap();
|
restful_server.start().await.unwrap();
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
let mut web_server = web::WebServer::new(
|
||||||
|
format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0))
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
if !cli.no_web && !restful_also_serve_web {
|
||||||
|
web_server.start().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await.unwrap();
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,14 @@ mod users;
|
|||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::routing::post;
|
||||||
use axum::{extract::State, routing::get, Json, Router};
|
use axum::{extract::State, routing::get, Json, Router};
|
||||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend};
|
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
|
use easytier::common::config::ConfigLoader;
|
||||||
use easytier::common::scoped_task::ScopedTask;
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::rpc_types;
|
use easytier::proto::rpc_types;
|
||||||
use network::NetworkApi;
|
use network::NetworkApi;
|
||||||
use sea_orm::DbErr;
|
use sea_orm::DbErr;
|
||||||
@@ -21,11 +24,16 @@ use tower_sessions::Expiry;
|
|||||||
use tower_sessions_sqlx_store::SqliteStore;
|
use tower_sessions_sqlx_store::SqliteStore;
|
||||||
use users::{AuthSession, Backend};
|
use users::{AuthSession, Backend};
|
||||||
|
|
||||||
use crate::client_manager::session::Session;
|
|
||||||
use crate::client_manager::storage::StorageToken;
|
use crate::client_manager::storage::StorageToken;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::client_manager::ClientManager;
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
|
|
||||||
|
/// Embed assets for web dashboard, build frontend first
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
#[derive(rust_embed::RustEmbed, Clone)]
|
||||||
|
#[folder = "frontend/dist/"]
|
||||||
|
struct Assets;
|
||||||
|
|
||||||
pub struct RestfulServer {
|
pub struct RestfulServer {
|
||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
client_mgr: Arc<ClientManager>,
|
client_mgr: Arc<ClientManager>,
|
||||||
@@ -35,6 +43,8 @@ pub struct RestfulServer {
|
|||||||
delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
||||||
|
|
||||||
network_api: NetworkApi,
|
network_api: NetworkApi,
|
||||||
|
|
||||||
|
enable_web_embed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppStateInner = Arc<ClientManager>;
|
type AppStateInner = Arc<ClientManager>;
|
||||||
@@ -48,6 +58,17 @@ struct GetSummaryJsonResp {
|
|||||||
device_count: u32,
|
device_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct GenerateConfigRequest {
|
||||||
|
config: NetworkConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct GenerateConfigResponse {
|
||||||
|
error: Option<String>,
|
||||||
|
toml_config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
message: String,
|
message: String,
|
||||||
@@ -73,6 +94,7 @@ impl RestfulServer {
|
|||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
client_mgr: Arc<ClientManager>,
|
client_mgr: Arc<ClientManager>,
|
||||||
db: Db,
|
db: Db,
|
||||||
|
enable_web_embed: bool,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
assert!(client_mgr.is_running());
|
assert!(client_mgr.is_running());
|
||||||
|
|
||||||
@@ -85,20 +107,10 @@ impl RestfulServer {
|
|||||||
serve_task: None,
|
serve_task: None,
|
||||||
delete_task: None,
|
delete_task: None,
|
||||||
network_api,
|
network_api,
|
||||||
|
enable_web_embed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session_by_machine_id(
|
|
||||||
client_mgr: &ClientManager,
|
|
||||||
machine_id: &uuid::Uuid,
|
|
||||||
) -> Result<Arc<Session>, HttpHandleError> {
|
|
||||||
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else {
|
|
||||||
return Err((StatusCode::NOT_FOUND, other_error("No such session").into()));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_list_all_sessions(
|
async fn handle_list_all_sessions(
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
@@ -121,9 +133,7 @@ impl RestfulServer {
|
|||||||
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
|
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
|
||||||
};
|
};
|
||||||
|
|
||||||
let machines = client_mgr
|
let machines = client_mgr.list_machine_by_user_id(user.id().clone()).await;
|
||||||
.list_machine_by_token(user.tokens[0].clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(GetSummaryJsonResp {
|
Ok(GetSummaryJsonResp {
|
||||||
device_count: machines.len() as u32,
|
device_count: machines.len() as u32,
|
||||||
@@ -131,6 +141,24 @@ impl RestfulServer {
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_generate_config(
|
||||||
|
Json(req): Json<GenerateConfigRequest>,
|
||||||
|
) -> Result<Json<GenerateConfigResponse>, HttpHandleError> {
|
||||||
|
let config = req.config.gen_config();
|
||||||
|
match config {
|
||||||
|
Ok(c) => Ok(GenerateConfigResponse {
|
||||||
|
error: None,
|
||||||
|
toml_config: Some(c.dump()),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
Err(e) => Ok(GenerateConfigResponse {
|
||||||
|
error: Some(format!("{:?}", e)),
|
||||||
|
toml_config: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
|
||||||
@@ -178,11 +206,24 @@ impl RestfulServer {
|
|||||||
.route_layer(login_required!(Backend))
|
.route_layer(login_required!(Backend))
|
||||||
.merge(auth::router())
|
.merge(auth::router())
|
||||||
.with_state(self.client_mgr.clone())
|
.with_state(self.client_mgr.clone())
|
||||||
|
.route(
|
||||||
|
"/api/v1/generate-config",
|
||||||
|
post(Self::handle_generate_config),
|
||||||
|
)
|
||||||
.layer(MessagesManagerLayer)
|
.layer(MessagesManagerLayer)
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(tower_http::cors::CorsLayer::very_permissive())
|
.layer(tower_http::cors::CorsLayer::very_permissive())
|
||||||
.layer(compression_layer);
|
.layer(compression_layer);
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
let app = if self.enable_web_embed {
|
||||||
|
use axum_embed::ServeEmbed;
|
||||||
|
let service = ServeEmbed::<Assets>::new();
|
||||||
|
app.fallback_service(service)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
};
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
});
|
});
|
||||||
|
@@ -5,7 +5,6 @@ use axum::http::StatusCode;
|
|||||||
use axum::routing::{delete, post};
|
use axum::routing::{delete, post};
|
||||||
use axum::{extract::State, routing::get, Json, Router};
|
use axum::{extract::State, routing::get, Json, Router};
|
||||||
use axum_login::AuthUser;
|
use axum_login::AuthUser;
|
||||||
use dashmap::DashSet;
|
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::common::Void;
|
use easytier::proto::common::Void;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
@@ -13,7 +12,7 @@ use easytier::proto::web::*;
|
|||||||
|
|
||||||
use crate::client_manager::session::Session;
|
use crate::client_manager::session::Session;
|
||||||
use crate::client_manager::ClientManager;
|
use crate::client_manager::ClientManager;
|
||||||
use crate::db::ListNetworkProps;
|
use crate::db::{ListNetworkProps, UserIdInDb};
|
||||||
|
|
||||||
use super::users::AuthSession;
|
use super::users::AuthSession;
|
||||||
use super::{
|
use super::{
|
||||||
@@ -81,12 +80,24 @@ impl NetworkApi {
|
|||||||
Self {}
|
Self {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_user_id(auth_session: &AuthSession) -> Result<UserIdInDb, (StatusCode, Json<Error>)> {
|
||||||
|
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
|
||||||
|
return Err((
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
other_error(format!("No user id found")).into(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
Ok(user_id)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_session_by_machine_id(
|
async fn get_session_by_machine_id(
|
||||||
auth_session: &AuthSession,
|
auth_session: &AuthSession,
|
||||||
client_mgr: &ClientManager,
|
client_mgr: &ClientManager,
|
||||||
machine_id: &uuid::Uuid,
|
machine_id: &uuid::Uuid,
|
||||||
) -> Result<Arc<Session>, HttpHandleError> {
|
) -> Result<Arc<Session>, HttpHandleError> {
|
||||||
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else {
|
let user_id = Self::get_user_id(auth_session)?;
|
||||||
|
|
||||||
|
let Some(result) = client_mgr.get_session_by_machine_id(user_id, machine_id) else {
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
other_error(format!("No such session: {}", machine_id)).into(),
|
other_error(format!("No such session: {}", machine_id)).into(),
|
||||||
@@ -289,23 +300,13 @@ impl NetworkApi {
|
|||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
State(client_mgr): AppState,
|
State(client_mgr): AppState,
|
||||||
) -> Result<Json<ListMachineJsonResp>, HttpHandleError> {
|
) -> Result<Json<ListMachineJsonResp>, HttpHandleError> {
|
||||||
let tokens = auth_session
|
let user_id = Self::get_user_id(&auth_session)?;
|
||||||
.user
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| x.tokens.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let client_urls = DashSet::new();
|
let client_urls = client_mgr.list_machine_by_user_id(user_id).await;
|
||||||
for token in tokens {
|
|
||||||
let urls = client_mgr.list_machine_by_token(token).await;
|
|
||||||
for url in urls {
|
|
||||||
client_urls.insert(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut machines = vec![];
|
let mut machines = vec![];
|
||||||
for item in client_urls.iter() {
|
for item in client_urls.iter() {
|
||||||
let client_url = item.key().clone();
|
let client_url = item.clone();
|
||||||
let session = client_mgr.get_heartbeat_requests(&client_url).await;
|
let session = client_mgr.get_heartbeat_requests(&client_url).await;
|
||||||
machines.push(ListMachineItem {
|
machines.push(ListMachineItem {
|
||||||
client_url: Some(client_url),
|
client_url: Some(client_url),
|
||||||
|
39
easytier-web/src/web/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use axum::Router;
|
||||||
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use axum_embed::ServeEmbed;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
/// Embed assets for web dashboard, build frontend first
|
||||||
|
#[derive(RustEmbed, Clone)]
|
||||||
|
#[folder = "frontend/dist/"]
|
||||||
|
struct Assets;
|
||||||
|
|
||||||
|
pub struct WebServer {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
serve_task: Option<ScopedTask<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebServer {
|
||||||
|
pub async fn new(bind_addr: SocketAddr) -> anyhow::Result<Self> {
|
||||||
|
Ok(WebServer {
|
||||||
|
bind_addr,
|
||||||
|
serve_task: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
||||||
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
let service = ServeEmbed::<Assets>::new();
|
||||||
|
let app = Router::new().fallback_service(service);
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.serve_task = Some(task.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,7 @@ name = "easytier"
|
|||||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||||
homepage = "https://github.com/EasyTier/EasyTier"
|
homepage = "https://github.com/EasyTier/EasyTier"
|
||||||
repository = "https://github.com/EasyTier/EasyTier"
|
repository = "https://github.com/EasyTier/EasyTier"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["kkrainbow"]
|
authors = ["kkrainbow"]
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||||
@@ -62,7 +62,6 @@ timedmap = "=1.0.1"
|
|||||||
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
|
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
pin-project-lite = "0.2.13"
|
pin-project-lite = "0.2.13"
|
||||||
atomicbox = "0.4.0"
|
|
||||||
tachyonix = "0.3.0"
|
tachyonix = "0.3.0"
|
||||||
|
|
||||||
quinn = { version = "0.11.0", optional = true, features = ["ring"] }
|
quinn = { version = "0.11.0", optional = true, features = ["ring"] }
|
||||||
@@ -99,7 +98,6 @@ uuid = { version = "1.5.0", features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
# for ring tunnel
|
# for ring tunnel
|
||||||
crossbeam-queue = "0.3"
|
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
|
|
||||||
# for rpc
|
# for rpc
|
||||||
@@ -126,11 +124,12 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
pnet = { version = "0.35.0", features = ["serde"] }
|
pnet = { version = "0.35.0", features = ["serde"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
clap = { version = "4.4.8", features = [
|
clap = { version = "4.5.30", features = [
|
||||||
"string",
|
"string",
|
||||||
"unicode",
|
"unicode",
|
||||||
"derive",
|
"derive",
|
||||||
"wrap_help",
|
"wrap_help",
|
||||||
|
"env",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
async-recursion = "1.0.5"
|
async-recursion = "1.0.5"
|
||||||
@@ -138,7 +137,7 @@ async-recursion = "1.0.5"
|
|||||||
network-interface = "2.0"
|
network-interface = "2.0"
|
||||||
|
|
||||||
# for ospf route
|
# for ospf route
|
||||||
petgraph = "0.6.5"
|
petgraph = "0.8.1"
|
||||||
|
|
||||||
# for wireguard
|
# for wireguard
|
||||||
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
||||||
@@ -154,13 +153,9 @@ humansize = "2.1.3"
|
|||||||
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
|
||||||
derivative = "2.2.0"
|
mimalloc-rust = { git = "https://github.com/EasyTier/mimalloc-rust", optional = true }
|
||||||
|
|
||||||
mimalloc-rust = { version = "0.2.1", optional = true }
|
|
||||||
|
|
||||||
# for mips
|
|
||||||
indexmap = { version = "~1.9.3", optional = false, features = ["std"] }
|
|
||||||
|
|
||||||
|
# mips
|
||||||
atomic-shim = "0.2.0"
|
atomic-shim = "0.2.0"
|
||||||
|
|
||||||
smoltcp = { version = "0.12.0", optional = true, default-features = false, features = [
|
smoltcp = { version = "0.12.0", optional = true, default-features = false, features = [
|
||||||
@@ -168,8 +163,14 @@ smoltcp = { version = "0.12.0", optional = true, default-features = false, featu
|
|||||||
"medium-ip",
|
"medium-ip",
|
||||||
"proto-ipv4",
|
"proto-ipv4",
|
||||||
"proto-ipv6",
|
"proto-ipv6",
|
||||||
|
"proto-ipv4-fragmentation",
|
||||||
|
"fragmentation-buffer-size-8192",
|
||||||
|
"assembler-max-segment-count-16",
|
||||||
|
"reassembly-buffer-size-8192",
|
||||||
|
"reassembly-buffer-count-16",
|
||||||
"socket-tcp",
|
"socket-tcp",
|
||||||
"socket-tcp-cubic",
|
"socket-udp",
|
||||||
|
# "socket-tcp-cubic",
|
||||||
"async",
|
"async",
|
||||||
] }
|
] }
|
||||||
parking_lot = { version = "0.12.0", optional = true }
|
parking_lot = { version = "0.12.0", optional = true }
|
||||||
@@ -182,18 +183,44 @@ sys-locale = "0.3"
|
|||||||
ringbuf = "0.4.5"
|
ringbuf = "0.4.5"
|
||||||
async-ringbuf = "0.3.1"
|
async-ringbuf = "0.3.1"
|
||||||
|
|
||||||
service-manager = {git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main"}
|
service-manager = { git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main" }
|
||||||
|
|
||||||
async-compression = { version = "0.4.17", default-features = false, features = ["zstd", "tokio"] }
|
async-compression = { version = "0.4.17", default-features = false, features = [
|
||||||
|
"zstd",
|
||||||
|
"tokio",
|
||||||
|
] }
|
||||||
|
|
||||||
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys" }
|
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys" }
|
||||||
|
|
||||||
prost-reflect = { version = "0.14.5", features = [
|
prost-reflect = { version = "0.14.5", default-features = false, features = [
|
||||||
"serde",
|
|
||||||
"derive",
|
"derive",
|
||||||
"text-format"
|
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
# for http connector
|
||||||
|
http_req = { git = "https://github.com/EasyTier/http_req.git", default-features = false, features = [
|
||||||
|
"rust-tls",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# for dns connector
|
||||||
|
hickory-resolver = "0.25.2"
|
||||||
|
hickory-proto = "0.25.2"
|
||||||
|
|
||||||
|
# for magic dns
|
||||||
|
hickory-client = "0.25.2"
|
||||||
|
hickory-server = { version = "0.25.2", features = ["resolver"] }
|
||||||
|
derive_builder = "0.20.2"
|
||||||
|
humantime-serde = "1.1.1"
|
||||||
|
multimap = "0.10.0"
|
||||||
|
version-compare = "0.2.0"
|
||||||
|
|
||||||
|
jemallocator = { version = "0.5.4", optional = true }
|
||||||
|
jemalloc-ctl = { version = "0.5.4", optional = true }
|
||||||
|
jemalloc-sys = { version = "0.5.4", features = [
|
||||||
|
"stats",
|
||||||
|
"profiling",
|
||||||
|
"unprefixed_malloc_on_supported_platforms",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
|
||||||
machine-uid = "0.5.3"
|
machine-uid = "0.5.3"
|
||||||
|
|
||||||
@@ -202,6 +229,10 @@ netlink-sys = "0.8.7"
|
|||||||
netlink-packet-route = "0.21.0"
|
netlink-packet-route = "0.21.0"
|
||||||
netlink-packet-core = { version = "0.7.0" }
|
netlink-packet-core = { version = "0.7.0" }
|
||||||
netlink-packet-utils = "0.5.2"
|
netlink-packet-utils = "0.5.2"
|
||||||
|
# for magic dns
|
||||||
|
resolv-conf = "0.7.3"
|
||||||
|
dbus = { version = "0.9.7", features = ["vendored"] }
|
||||||
|
which = "7.0.3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "0.52.0", features = [
|
windows = { version = "0.52.0", features = [
|
||||||
@@ -212,7 +243,7 @@ windows = { version = "0.52.0", features = [
|
|||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
"Win32_Networking_WinSock",
|
"Win32_Networking_WinSock",
|
||||||
"Win32_System_IO",
|
"Win32_System_IO",
|
||||||
]}
|
] }
|
||||||
encoding = "0.2"
|
encoding = "0.2"
|
||||||
winreg = "0.52"
|
winreg = "0.52"
|
||||||
windows-service = "0.7.0"
|
windows-service = "0.7.0"
|
||||||
@@ -222,18 +253,28 @@ tonic-build = "0.12"
|
|||||||
globwalk = "0.8.1"
|
globwalk = "0.8.1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
prost-build = "0.13.2"
|
prost-build = "0.13.2"
|
||||||
rpc_build = { package = "easytier-rpc-build", version = "0.1.0", features = ["internal-namespace"] }
|
rpc_build = { package = "easytier-rpc-build", version = "0.1.0", features = [
|
||||||
|
"internal-namespace",
|
||||||
|
] }
|
||||||
prost-reflect-build = { version = "0.14.0" }
|
prost-reflect-build = { version = "0.14.0" }
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
reqwest = { version = "0.11", features = ["blocking"] }
|
reqwest = { version = "0.12.12", features = ["blocking"] }
|
||||||
zip = "0.6.6"
|
zip = "0.6.6"
|
||||||
|
|
||||||
|
# enable thunk-rs when compiling for x86_64 or i686 windows
|
||||||
|
[target.x86_64-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
|
||||||
|
[target.i686-pc-windows-msvc.build-dependencies]
|
||||||
|
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3.0.0"
|
serial_test = "3.0.0"
|
||||||
rstest = "0.18.2"
|
rstest = "0.18.2"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
|
maplit = "1.0.2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||||
defguard_wireguard_rs = "0.4.2"
|
defguard_wireguard_rs = "0.4.2"
|
||||||
@@ -267,3 +308,4 @@ websocket = [
|
|||||||
]
|
]
|
||||||
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
|
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
|
||||||
socks5 = ["dep:smoltcp"]
|
socks5 = ["dep:smoltcp"]
|
||||||
|
jemalloc = ["dep:jemallocator", "dep:jemalloc-ctl", "dep:jemalloc-sys"]
|
||||||
|
@@ -71,6 +71,8 @@ impl WindowsBuild {
|
|||||||
|
|
||||||
if target.contains("x86_64") {
|
if target.contains("x86_64") {
|
||||||
println!("cargo:rustc-link-search=native=easytier/third_party/");
|
println!("cargo:rustc-link-search=native=easytier/third_party/");
|
||||||
|
} else if target.contains("i686") {
|
||||||
|
println!("cargo:rustc-link-search=native=easytier/third_party/i686/");
|
||||||
} else if target.contains("aarch64") {
|
} else if target.contains("aarch64") {
|
||||||
println!("cargo:rustc-link-search=native=easytier/third_party/arm64/");
|
println!("cargo:rustc-link-search=native=easytier/third_party/arm64/");
|
||||||
}
|
}
|
||||||
@@ -125,19 +127,29 @@ fn check_locale() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// enable thunk-rs when target os is windows and arch is x86_64 or i686
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if !std::env::var("TARGET")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("aarch64")
|
||||||
|
{
|
||||||
|
thunk::thunk();
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
WindowsBuild::check_for_win();
|
WindowsBuild::check_for_win();
|
||||||
|
|
||||||
|
let proto_files_reflect = ["src/proto/peer_rpc.proto", "src/proto/common.proto"];
|
||||||
|
|
||||||
let proto_files = [
|
let proto_files = [
|
||||||
"src/proto/peer_rpc.proto",
|
|
||||||
"src/proto/common.proto",
|
|
||||||
"src/proto/error.proto",
|
"src/proto/error.proto",
|
||||||
"src/proto/tests.proto",
|
"src/proto/tests.proto",
|
||||||
"src/proto/cli.proto",
|
"src/proto/cli.proto",
|
||||||
"src/proto/web.proto",
|
"src/proto/web.proto",
|
||||||
|
"src/proto/magic_dns.proto",
|
||||||
];
|
];
|
||||||
|
|
||||||
for proto_file in &proto_files {
|
for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) {
|
||||||
println!("cargo:rerun-if-changed={}", proto_file);
|
println!("cargo:rerun-if-changed={}", proto_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,12 +168,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
||||||
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
|
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
|
||||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||||
|
.field_attribute(".web.NetworkConfig", "#[serde(default)]")
|
||||||
.service_generator(Box::new(rpc_build::ServiceGenerator::new()))
|
.service_generator(Box::new(rpc_build::ServiceGenerator::new()))
|
||||||
.btree_map(["."]);
|
.btree_map(["."]);
|
||||||
|
|
||||||
|
config.compile_protos(&proto_files, &["src/proto/"])?;
|
||||||
|
|
||||||
prost_reflect_build::Builder::new()
|
prost_reflect_build::Builder::new()
|
||||||
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
|
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
|
||||||
.compile_protos_with_config(config, &proto_files, &["src/proto/"])?;
|
.compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?;
|
||||||
|
|
||||||
check_locale();
|
check_locale();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@@ -11,8 +11,8 @@ core_clap:
|
|||||||
完整URL:--config-server udp://127.0.0.1:22020/admin
|
完整URL:--config-server udp://127.0.0.1:22020/admin
|
||||||
仅用户名:--config-server admin,将使用官方的服务器
|
仅用户名:--config-server admin,将使用官方的服务器
|
||||||
config_file:
|
config_file:
|
||||||
en: "path to the config file, NOTE: if this is set, all other options will be ignored"
|
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
|
||||||
zh-CN: "配置文件路径,注意:如果设置了这个选项,其他所有选项都将被忽略"
|
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
|
||||||
network_name:
|
network_name:
|
||||||
en: "network name to identify this vpn network"
|
en: "network name to identify this vpn network"
|
||||||
zh-CN: "用于标识此VPN网络的网络名称"
|
zh-CN: "用于标识此VPN网络的网络名称"
|
||||||
@@ -96,12 +96,15 @@ core_clap:
|
|||||||
enable_exit_node:
|
enable_exit_node:
|
||||||
en: "allow this node to be an exit node"
|
en: "allow this node to be an exit node"
|
||||||
zh-CN: "允许此节点成为出口节点"
|
zh-CN: "允许此节点成为出口节点"
|
||||||
|
proxy_forward_by_system:
|
||||||
|
en: "forward packet to proxy networks via system kernel, disable internal nat for network proxy"
|
||||||
|
zh-CN: "通过系统内核转发子网代理数据包,禁用内置NAT"
|
||||||
no_tun:
|
no_tun:
|
||||||
en: "do not create TUN device, can use subnet proxy to access node"
|
en: "do not create TUN device, can use subnet proxy to access node"
|
||||||
zh-CN: "不创建TUN设备,可以使用子网代理访问节点"
|
zh-CN: "不创建TUN设备,可以使用子网代理访问节点"
|
||||||
use_smoltcp:
|
use_smoltcp:
|
||||||
en: "enable smoltcp stack for subnet proxy"
|
en: "enable smoltcp stack for subnet proxy and kcp proxy"
|
||||||
zh-CN: "为子网代理启用smoltcp堆栈"
|
zh-CN: "为子网代理和 KCP 代理启用smoltcp堆栈"
|
||||||
manual_routes:
|
manual_routes:
|
||||||
en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16"
|
en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16"
|
||||||
zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16"
|
zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16"
|
||||||
@@ -146,6 +149,12 @@ core_clap:
|
|||||||
disable_kcp_input:
|
disable_kcp_input:
|
||||||
en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved."
|
en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved."
|
||||||
zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。"
|
zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。"
|
||||||
|
port_forward:
|
||||||
|
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
|
||||||
|
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
|
||||||
|
accept_dns:
|
||||||
|
en: "if true, enable magic dns. with magic dns, you can access other nodes with a domain name, e.g.: <hostname>.et.net. magic dns will modify your system dns settings, enable it carefully."
|
||||||
|
zh-CN: "如果为true,则启用魔法DNS。使用魔法DNS,您可以使用域名访问其他节点,例如:<hostname>.et.net。魔法DNS将修改您的系统DNS设置,请谨慎启用。"
|
||||||
|
|
||||||
core_app:
|
core_app:
|
||||||
panic_backtrace_save:
|
panic_backtrace_save:
|
||||||
|
@@ -7,7 +7,10 @@ use std::{
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{proto::common::CompressionAlgoPb, tunnel::generate_digest_from_str};
|
use crate::{
|
||||||
|
proto::common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
||||||
|
tunnel::generate_digest_from_str,
|
||||||
|
};
|
||||||
|
|
||||||
pub type Flags = crate::proto::common::FlagsInConfig;
|
pub type Flags = crate::proto::common::FlagsInConfig;
|
||||||
|
|
||||||
@@ -20,19 +23,20 @@ pub fn gen_default_flags() -> Flags {
|
|||||||
mtu: 1380,
|
mtu: 1380,
|
||||||
latency_first: false,
|
latency_first: false,
|
||||||
enable_exit_node: false,
|
enable_exit_node: false,
|
||||||
|
proxy_forward_by_system: false,
|
||||||
no_tun: false,
|
no_tun: false,
|
||||||
use_smoltcp: false,
|
use_smoltcp: false,
|
||||||
relay_network_whitelist: "*".to_string(),
|
relay_network_whitelist: "*".to_string(),
|
||||||
disable_p2p: false,
|
disable_p2p: false,
|
||||||
relay_all_peer_rpc: false,
|
relay_all_peer_rpc: false,
|
||||||
disable_udp_hole_punching: false,
|
disable_udp_hole_punching: false,
|
||||||
ipv6_listener: "udp://[::]:0".to_string(),
|
|
||||||
multi_thread: true,
|
multi_thread: true,
|
||||||
data_compress_algo: CompressionAlgoPb::None.into(),
|
data_compress_algo: CompressionAlgoPb::None.into(),
|
||||||
bind_device: true,
|
bind_device: true,
|
||||||
enable_kcp_proxy: false,
|
enable_kcp_proxy: false,
|
||||||
disable_kcp_input: false,
|
disable_kcp_input: false,
|
||||||
disable_relay_kcp: true,
|
disable_relay_kcp: true,
|
||||||
|
accept_dns: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +77,7 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
fn get_peers(&self) -> Vec<PeerConfig>;
|
fn get_peers(&self) -> Vec<PeerConfig>;
|
||||||
fn set_peers(&self, peers: Vec<PeerConfig>);
|
fn set_peers(&self, peers: Vec<PeerConfig>);
|
||||||
|
|
||||||
fn get_listeners(&self) -> Vec<url::Url>;
|
fn get_listeners(&self) -> Option<Vec<url::Url>>;
|
||||||
fn set_listeners(&self, listeners: Vec<url::Url>);
|
fn set_listeners(&self, listeners: Vec<url::Url>);
|
||||||
|
|
||||||
fn get_mapped_listeners(&self) -> Vec<url::Url>;
|
fn get_mapped_listeners(&self) -> Vec<url::Url>;
|
||||||
@@ -97,6 +101,9 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
fn get_socks5_portal(&self) -> Option<url::Url>;
|
fn get_socks5_portal(&self) -> Option<url::Url>;
|
||||||
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
||||||
|
|
||||||
|
fn get_port_forwards(&self) -> Vec<PortForwardConfig>;
|
||||||
|
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>);
|
||||||
|
|
||||||
fn dump(&self) -> String;
|
fn dump(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +187,41 @@ pub struct VpnPortalConfig {
|
|||||||
pub wireguard_listen: SocketAddr,
|
pub wireguard_listen: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
|
pub struct PortForwardConfig {
|
||||||
|
pub bind_addr: SocketAddr,
|
||||||
|
pub dst_addr: SocketAddr,
|
||||||
|
pub proto: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PortForwardConfigPb> for PortForwardConfig {
|
||||||
|
fn from(config: PortForwardConfigPb) -> Self {
|
||||||
|
PortForwardConfig {
|
||||||
|
bind_addr: config.bind_addr.unwrap_or_default().into(),
|
||||||
|
dst_addr: config.dst_addr.unwrap_or_default().into(),
|
||||||
|
proto: match SocketType::try_from(config.socket_type) {
|
||||||
|
Ok(SocketType::Tcp) => "tcp".to_string(),
|
||||||
|
Ok(SocketType::Udp) => "udp".to_string(),
|
||||||
|
_ => "tcp".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<PortForwardConfigPb> for PortForwardConfig {
|
||||||
|
fn into(self) -> PortForwardConfigPb {
|
||||||
|
PortForwardConfigPb {
|
||||||
|
bind_addr: Some(self.bind_addr.into()),
|
||||||
|
dst_addr: Some(self.dst_addr.into()),
|
||||||
|
socket_type: match self.proto.to_lowercase().as_str() {
|
||||||
|
"tcp" => SocketType::Tcp as i32,
|
||||||
|
"udp" => SocketType::Udp as i32,
|
||||||
|
_ => SocketType::Tcp as i32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||||
struct Config {
|
struct Config {
|
||||||
netns: Option<String>,
|
netns: Option<String>,
|
||||||
@@ -207,6 +249,8 @@ struct Config {
|
|||||||
|
|
||||||
socks5_proxy: Option<url::Url>,
|
socks5_proxy: Option<url::Url>,
|
||||||
|
|
||||||
|
port_forward: Option<Vec<PortForwardConfig>>,
|
||||||
|
|
||||||
flags: Option<serde_json::Map<String, serde_json::Value>>,
|
flags: Option<serde_json::Map<String, serde_json::Value>>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -231,20 +275,23 @@ impl TomlConfigLoader {
|
|||||||
|
|
||||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||||
|
|
||||||
Ok(TomlConfigLoader {
|
let config = TomlConfigLoader {
|
||||||
config: Arc::new(Mutex::new(config)),
|
config: Arc::new(Mutex::new(config)),
|
||||||
})
|
};
|
||||||
|
|
||||||
|
let old_ns = config.get_network_identity();
|
||||||
|
config.set_network_identity(NetworkIdentity::new(
|
||||||
|
old_ns.network_name,
|
||||||
|
old_ns.network_secret.unwrap_or_default(),
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
|
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
|
||||||
let config_str = std::fs::read_to_string(config_path)
|
let config_str = std::fs::read_to_string(config_path)
|
||||||
.with_context(|| format!("failed to read config file: {:?}", config_path))?;
|
.with_context(|| format!("failed to read config file: {:?}", config_path))?;
|
||||||
let ret = Self::new_from_str(&config_str)?;
|
let ret = Self::new_from_str(&config_str)?;
|
||||||
let old_ns = ret.get_network_identity();
|
|
||||||
ret.set_network_identity(NetworkIdentity::new(
|
|
||||||
old_ns.network_name,
|
|
||||||
old_ns.network_secret.unwrap_or_default(),
|
|
||||||
));
|
|
||||||
|
|
||||||
Ok(ret)
|
Ok(ret)
|
||||||
}
|
}
|
||||||
@@ -467,13 +514,8 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().peer = Some(peers);
|
self.config.lock().unwrap().peer = Some(peers);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_listeners(&self) -> Vec<url::Url> {
|
fn get_listeners(&self) -> Option<Vec<url::Url>> {
|
||||||
self.config
|
self.config.lock().unwrap().listeners.clone()
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.listeners
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_listeners(&self, listeners: Vec<url::Url>) {
|
fn set_listeners(&self, listeners: Vec<url::Url>) {
|
||||||
@@ -534,6 +576,35 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().exit_nodes = Some(nodes);
|
self.config.lock().unwrap().exit_nodes = Some(nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>> {
|
||||||
|
self.config.lock().unwrap().routes.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
||||||
|
self.config.lock().unwrap().routes = routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_socks5_portal(&self) -> Option<url::Url> {
|
||||||
|
self.config.lock().unwrap().socks5_proxy.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
||||||
|
self.config.lock().unwrap().socks5_proxy = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_port_forwards(&self) -> Vec<PortForwardConfig> {
|
||||||
|
self.config
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.port_forward
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>) {
|
||||||
|
self.config.lock().unwrap().port_forward = Some(forwards);
|
||||||
|
}
|
||||||
|
|
||||||
fn dump(&self) -> String {
|
fn dump(&self) -> String {
|
||||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||||
let default_flags_hashmap =
|
let default_flags_hashmap =
|
||||||
@@ -558,22 +629,6 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
config.flags = Some(flag_map);
|
config.flags = Some(flag_map);
|
||||||
toml::to_string_pretty(&config).unwrap()
|
toml::to_string_pretty(&config).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>> {
|
|
||||||
self.config.lock().unwrap().routes.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
|
||||||
self.config.lock().unwrap().routes = routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_socks5_portal(&self) -> Option<url::Url> {
|
|
||||||
self.config.lock().unwrap().socks5_proxy.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
|
||||||
self.config.lock().unwrap().socks5_proxy = addr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -614,6 +669,11 @@ dir = "/tmp/easytier"
|
|||||||
|
|
||||||
[console_logger]
|
[console_logger]
|
||||||
level = "warn"
|
level = "warn"
|
||||||
|
|
||||||
|
[[port_forward]]
|
||||||
|
bind_addr = "0.0.0.0:11011"
|
||||||
|
dst_addr = "192.168.94.33:11011"
|
||||||
|
proto = "tcp"
|
||||||
"#;
|
"#;
|
||||||
let ret = TomlConfigLoader::new_from_str(config_str);
|
let ret = TomlConfigLoader::new_from_str(config_str);
|
||||||
if let Err(e) = &ret {
|
if let Err(e) = &ret {
|
||||||
@@ -634,6 +694,14 @@ level = "warn"
|
|||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vec![PortForwardConfig {
|
||||||
|
bind_addr: "0.0.0.0:11011".parse().unwrap(),
|
||||||
|
dst_addr: "192.168.94.33:11011".parse().unwrap(),
|
||||||
|
proto: "tcp".to_string(),
|
||||||
|
}],
|
||||||
|
ret.get_port_forwards()
|
||||||
|
);
|
||||||
println!("{}", ret.dump());
|
println!("{}", ret.dump());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
134
easytier/src/common/dns.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use hickory_proto::runtime::TokioRuntimeProvider;
|
||||||
|
use hickory_proto::xfer::Protocol;
|
||||||
|
use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
|
||||||
|
use hickory_resolver::name_server::{GenericConnector, TokioConnectionProvider};
|
||||||
|
use hickory_resolver::system_conf::read_system_conf;
|
||||||
|
use hickory_resolver::{Resolver, TokioResolver};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use tokio::net::lookup_host;
|
||||||
|
|
||||||
|
use super::error::Error;
|
||||||
|
|
||||||
|
pub fn get_default_resolver_config() -> ResolverConfig {
|
||||||
|
let mut default_resolve_config = ResolverConfig::new();
|
||||||
|
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||||
|
"223.5.5.5:53".parse().unwrap(),
|
||||||
|
Protocol::Udp,
|
||||||
|
));
|
||||||
|
default_resolve_config.add_name_server(NameServerConfig::new(
|
||||||
|
"180.184.1.1:53".parse().unwrap(),
|
||||||
|
Protocol::Udp,
|
||||||
|
));
|
||||||
|
default_resolve_config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static ALLOW_USE_SYSTEM_DNS_RESOLVER: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));
|
||||||
|
|
||||||
|
pub static RESOLVER: Lazy<Arc<Resolver<GenericConnector<TokioRuntimeProvider>>>> =
|
||||||
|
Lazy::new(|| {
|
||||||
|
let system_cfg = read_system_conf();
|
||||||
|
let mut cfg = get_default_resolver_config();
|
||||||
|
let mut opt = ResolverOpts::default();
|
||||||
|
if let Ok(s) = system_cfg {
|
||||||
|
for ns in s.0.name_servers() {
|
||||||
|
cfg.add_name_server(ns.clone());
|
||||||
|
}
|
||||||
|
opt = s.1;
|
||||||
|
}
|
||||||
|
opt.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
|
||||||
|
let builder = TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
|
||||||
|
.with_options(opt);
|
||||||
|
Arc::new(builder.build())
|
||||||
|
});
|
||||||
|
|
||||||
|
pub async fn resolve_txt_record(domain_name: &str) -> Result<String, Error> {
|
||||||
|
let r = RESOLVER.clone();
|
||||||
|
let response = r.txt_lookup(domain_name).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"txt_lookup failed, domain_name: {}",
|
||||||
|
domain_name.to_string()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let txt_record = response.iter().next().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"no txt record found, domain_name: {}",
|
||||||
|
domain_name.to_string()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
|
||||||
|
tracing::info!(?txt_data, ?domain_name, "get txt record");
|
||||||
|
|
||||||
|
Ok(txt_data.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn socket_addrs(
|
||||||
|
url: &url::Url,
|
||||||
|
default_port_number: impl Fn() -> Option<u16>,
|
||||||
|
) -> Result<Vec<SocketAddr>, Error> {
|
||||||
|
let host = url.host_str().ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||||
|
let port = url
|
||||||
|
.port()
|
||||||
|
.or_else(default_port_number)
|
||||||
|
.ok_or(Error::InvalidUrl(url.to_string()))?;
|
||||||
|
|
||||||
|
// if host is an ip address, return it directly
|
||||||
|
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||||
|
return Ok(vec![SocketAddr::new(ip, port)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
let socket_addr = format!("{}:{}", host, port);
|
||||||
|
match lookup_host(socket_addr).await {
|
||||||
|
Ok(a) => {
|
||||||
|
let a = a.collect();
|
||||||
|
tracing::debug!(?a, "system dns lookup done");
|
||||||
|
return Ok(a);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(?e, "system dns lookup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use hickory_resolver
|
||||||
|
let ret = RESOLVER.lookup_ip(host).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"hickory dns lookup_ip failed, host: {}, port: {}",
|
||||||
|
host, port
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(ret
|
||||||
|
.iter()
|
||||||
|
.map(|ip| SocketAddr::new(ip, port))
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::defer;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_socket_addrs() {
|
||||||
|
let url = url::Url::parse("tcp://public.easytier.cn:80").unwrap();
|
||||||
|
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
|
||||||
|
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||||
|
println!("addrs: {:?}", addrs);
|
||||||
|
|
||||||
|
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
defer!(
|
||||||
|
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
);
|
||||||
|
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
|
||||||
|
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
|
||||||
|
println!("addrs2: {:?}", addrs);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::proto::cli::PeerConnInfo;
|
use crate::proto::cli::PeerConnInfo;
|
||||||
use crate::proto::common::PeerFeatureFlag;
|
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -42,6 +42,8 @@ pub enum GlobalCtxEvent {
|
|||||||
|
|
||||||
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||||
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
||||||
|
|
||||||
|
PortForwardAdded(PortForwardConfigPb),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
|
pub type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
|
||||||
@@ -59,15 +61,16 @@ pub struct GlobalCtx {
|
|||||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||||
cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>,
|
cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>,
|
||||||
|
|
||||||
ip_collector: Arc<IPCollector>,
|
ip_collector: Mutex<Option<Arc<IPCollector>>>,
|
||||||
|
|
||||||
hostname: String,
|
hostname: Mutex<String>,
|
||||||
|
|
||||||
stun_info_collection: Box<dyn StunInfoCollectorTrait>,
|
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
||||||
|
|
||||||
running_listeners: Mutex<Vec<url::Url>>,
|
running_listeners: Mutex<Vec<url::Url>>,
|
||||||
|
|
||||||
enable_exit_node: bool,
|
enable_exit_node: bool,
|
||||||
|
proxy_forward_by_system: bool,
|
||||||
no_tun: bool,
|
no_tun: bool,
|
||||||
|
|
||||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||||
@@ -94,11 +97,12 @@ impl GlobalCtx {
|
|||||||
let net_ns = NetNS::new(config_fs.get_netns());
|
let net_ns = NetNS::new(config_fs.get_netns());
|
||||||
let hostname = config_fs.get_hostname();
|
let hostname = config_fs.get_hostname();
|
||||||
|
|
||||||
let (event_bus, _) = tokio::sync::broadcast::channel(1024);
|
let (event_bus, _) = tokio::sync::broadcast::channel(8);
|
||||||
|
|
||||||
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
|
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
|
||||||
|
|
||||||
let enable_exit_node = config_fs.get_flags().enable_exit_node;
|
let enable_exit_node = config_fs.get_flags().enable_exit_node;
|
||||||
|
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
|
||||||
let no_tun = config_fs.get_flags().no_tun;
|
let no_tun = config_fs.get_flags().no_tun;
|
||||||
|
|
||||||
let mut feature_flags = PeerFeatureFlag::default();
|
let mut feature_flags = PeerFeatureFlag::default();
|
||||||
@@ -116,15 +120,19 @@ impl GlobalCtx {
|
|||||||
cached_ipv4: AtomicCell::new(None),
|
cached_ipv4: AtomicCell::new(None),
|
||||||
cached_proxy_cidrs: AtomicCell::new(None),
|
cached_proxy_cidrs: AtomicCell::new(None),
|
||||||
|
|
||||||
ip_collector: Arc::new(IPCollector::new(net_ns, stun_info_collection.clone())),
|
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
||||||
|
net_ns,
|
||||||
|
stun_info_collection.clone(),
|
||||||
|
)))),
|
||||||
|
|
||||||
hostname,
|
hostname: Mutex::new(hostname),
|
||||||
|
|
||||||
stun_info_collection: Box::new(stun_info_collection),
|
stun_info_collection: Mutex::new(stun_info_collection),
|
||||||
|
|
||||||
running_listeners: Mutex::new(Vec::new()),
|
running_listeners: Mutex::new(Vec::new()),
|
||||||
|
|
||||||
enable_exit_node,
|
enable_exit_node,
|
||||||
|
proxy_forward_by_system,
|
||||||
no_tun,
|
no_tun,
|
||||||
|
|
||||||
feature_flags: AtomicCell::new(feature_flags),
|
feature_flags: AtomicCell::new(feature_flags),
|
||||||
@@ -136,10 +144,13 @@ impl GlobalCtx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn issue_event(&self, event: GlobalCtxEvent) {
|
pub fn issue_event(&self, event: GlobalCtxEvent) {
|
||||||
if self.event_bus.receiver_count() != 0 {
|
if let Err(e) = self.event_bus.send(event.clone()) {
|
||||||
self.event_bus.send(event).unwrap();
|
tracing::warn!(
|
||||||
} else {
|
"Failed to send event: {:?}, error: {:?}, receiver count: {}",
|
||||||
tracing::warn!("No subscriber for event: {:?}", event);
|
event,
|
||||||
|
e,
|
||||||
|
self.event_bus.receiver_count()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,26 +218,30 @@ impl GlobalCtx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ip_collector(&self) -> Arc<IPCollector> {
|
pub fn get_ip_collector(&self) -> Arc<IPCollector> {
|
||||||
self.ip_collector.clone()
|
self.ip_collector.lock().unwrap().as_ref().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hostname(&self) -> String {
|
pub fn get_hostname(&self) -> String {
|
||||||
return self.hostname.clone();
|
return self.hostname.lock().unwrap().clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stun_info_collector(&self) -> impl StunInfoCollectorTrait + '_ {
|
pub fn set_hostname(&self, hostname: String) {
|
||||||
self.stun_info_collection.as_ref()
|
*self.hostname.lock().unwrap() = hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stun_info_collector(&self) -> Arc<dyn StunInfoCollectorTrait> {
|
||||||
|
self.stun_info_collection.lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_stun_info_collector(&self, collector: Box<dyn StunInfoCollectorTrait>) {
|
pub fn replace_stun_info_collector(&self, collector: Box<dyn StunInfoCollectorTrait>) {
|
||||||
// force replace the stun_info_collection without mut and drop the old one
|
let arc_collector: Arc<dyn StunInfoCollectorTrait> = Arc::new(collector);
|
||||||
let ptr = &self.stun_info_collection as *const Box<dyn StunInfoCollectorTrait>;
|
*self.stun_info_collection.lock().unwrap() = arc_collector.clone();
|
||||||
let ptr = ptr as *mut Box<dyn StunInfoCollectorTrait>;
|
|
||||||
unsafe {
|
// rebuild the ip collector
|
||||||
std::ptr::drop_in_place(ptr);
|
*self.ip_collector.lock().unwrap() = Some(Arc::new(IPCollector::new(
|
||||||
#[allow(invalid_reference_casting)]
|
self.net_ns.clone(),
|
||||||
std::ptr::write(ptr, collector);
|
arc_collector,
|
||||||
}
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_running_listeners(&self) -> Vec<url::Url> {
|
pub fn get_running_listeners(&self) -> Vec<url::Url> {
|
||||||
@@ -273,6 +288,10 @@ impl GlobalCtx {
|
|||||||
self.enable_exit_node
|
self.enable_exit_node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn proxy_forward_by_system(&self) -> bool {
|
||||||
|
self.proxy_forward_by_system
|
||||||
|
}
|
||||||
|
|
||||||
pub fn no_tun(&self) -> bool {
|
pub fn no_tun(&self) -> bool {
|
||||||
self.no_tun
|
self.no_tun
|
||||||
}
|
}
|
||||||
@@ -288,7 +307,10 @@ impl GlobalCtx {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use crate::common::{config::TomlConfigLoader, new_peer_id};
|
use crate::{
|
||||||
|
common::{config::TomlConfigLoader, new_peer_id, stun::MockStunInfoCollector},
|
||||||
|
proto::common::NatType,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -328,7 +350,12 @@ pub mod tests {
|
|||||||
let config_fs = TomlConfigLoader::default();
|
let config_fs = TomlConfigLoader::default();
|
||||||
config_fs.set_inst_name(format!("test_{}", config_fs.get_id()));
|
config_fs.set_inst_name(format!("test_{}", config_fs.get_id()));
|
||||||
config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default()));
|
config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default()));
|
||||||
std::sync::Arc::new(GlobalCtx::new(config_fs))
|
|
||||||
|
let ctx = Arc::new(GlobalCtx::new(config_fs));
|
||||||
|
ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
|
||||||
|
udp_nat_type: NatType::Unknown,
|
||||||
|
}));
|
||||||
|
ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_mock_global_ctx() -> ArcGlobalCtx {
|
pub fn get_mock_global_ctx() -> ArcGlobalCtx {
|
||||||
|
@@ -12,13 +12,15 @@ impl IfConfiguerTrait for MacIfConfiger {
|
|||||||
name: &str,
|
name: &str,
|
||||||
address: Ipv4Addr,
|
address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
cidr_prefix: u8,
|
||||||
|
cost: Option<i32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
run_shell_cmd(
|
run_shell_cmd(
|
||||||
format!(
|
format!(
|
||||||
"route -n add {} -netmask {} -interface {} -hopcount 7",
|
"route -n add {} -netmask {} -interface {} -hopcount {}",
|
||||||
address,
|
address,
|
||||||
cidr_to_subnet_mask(cidr_prefix),
|
cidr_to_subnet_mask(cidr_prefix),
|
||||||
name
|
name,
|
||||||
|
cost.unwrap_or(7)
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
|
@@ -21,6 +21,7 @@ pub trait IfConfiguerTrait: Send + Sync {
|
|||||||
_name: &str,
|
_name: &str,
|
||||||
_address: Ipv4Addr,
|
_address: Ipv4Addr,
|
||||||
_cidr_prefix: u8,
|
_cidr_prefix: u8,
|
||||||
|
_cost: Option<i32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -125,3 +126,6 @@ pub type IfConfiger = windows::WindowsIfConfiger;
|
|||||||
target_os = "freebsd",
|
target_os = "freebsd",
|
||||||
)))]
|
)))]
|
||||||
pub type IfConfiger = DummyIfConfiger;
|
pub type IfConfiger = DummyIfConfiger;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows::RegistryManager;
|
||||||
|
@@ -350,6 +350,7 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
name: &str,
|
name: &str,
|
||||||
address: Ipv4Addr,
|
address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
cidr_prefix: u8,
|
||||||
|
cost: Option<i32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut message = RouteMessage::default();
|
let mut message = RouteMessage::default();
|
||||||
|
|
||||||
@@ -359,7 +360,9 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
|
|||||||
message.header.kind = RouteType::Unicast;
|
message.header.kind = RouteType::Unicast;
|
||||||
message.header.address_family = AddressFamily::Inet;
|
message.header.address_family = AddressFamily::Inet;
|
||||||
// metric
|
// metric
|
||||||
message.attributes.push(RouteAttribute::Priority(65535));
|
message
|
||||||
|
.attributes
|
||||||
|
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
|
||||||
// output interface
|
// output interface
|
||||||
message
|
message
|
||||||
.attributes
|
.attributes
|
||||||
@@ -550,7 +553,7 @@ mod tests {
|
|||||||
ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap();
|
ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap();
|
||||||
|
|
||||||
ifcfg
|
ifcfg
|
||||||
.add_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24)
|
.add_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24, None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
use std::net::Ipv4Addr;
|
use std::{io, net::Ipv4Addr};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use winreg::{
|
||||||
|
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
|
||||||
|
RegKey,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
|
use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
|
||||||
|
|
||||||
@@ -59,16 +63,18 @@ impl IfConfiguerTrait for WindowsIfConfiger {
|
|||||||
name: &str,
|
name: &str,
|
||||||
address: Ipv4Addr,
|
address: Ipv4Addr,
|
||||||
cidr_prefix: u8,
|
cidr_prefix: u8,
|
||||||
|
cost: Option<i32>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let Some(idx) = Self::get_interface_index(name) else {
|
let Some(idx) = Self::get_interface_index(name) else {
|
||||||
return Err(Error::NotFound);
|
return Err(Error::NotFound);
|
||||||
};
|
};
|
||||||
run_shell_cmd(
|
run_shell_cmd(
|
||||||
format!(
|
format!(
|
||||||
"route ADD {} MASK {} 10.1.1.1 IF {} METRIC 9000",
|
"route ADD {} MASK {} 10.1.1.1 IF {} METRIC {}",
|
||||||
address,
|
address,
|
||||||
cidr_to_subnet_mask(cidr_prefix),
|
cidr_to_subnet_mask(cidr_prefix),
|
||||||
idx
|
idx,
|
||||||
|
cost.unwrap_or(9000)
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
@@ -164,3 +170,220 @@ impl IfConfiguerTrait for WindowsIfConfiger {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RegistryManager;
|
||||||
|
|
||||||
|
impl RegistryManager {
|
||||||
|
pub const IPV4_TCPIP_INTERFACE_PREFIX: &str =
|
||||||
|
r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\";
|
||||||
|
pub const IPV6_TCPIP_INTERFACE_PREFIX: &str =
|
||||||
|
r"SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\";
|
||||||
|
pub const NETBT_INTERFACE_PREFIX: &str =
|
||||||
|
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
|
||||||
|
|
||||||
|
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
|
||||||
|
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||||
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
|
let profiles_key = hklm.open_subkey_with_flags(
|
||||||
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||||
|
KEY_ALL_ACCESS,
|
||||||
|
)?;
|
||||||
|
let unmanaged_key = hklm.open_subkey_with_flags(
|
||||||
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\Unmanaged",
|
||||||
|
KEY_ALL_ACCESS,
|
||||||
|
)?;
|
||||||
|
// collect subkeys to delete
|
||||||
|
let mut keys_to_delete = Vec::new();
|
||||||
|
let mut keys_to_delete_unmanaged = Vec::new();
|
||||||
|
for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) {
|
||||||
|
let subkey = profiles_key.open_subkey(&subkey_name)?;
|
||||||
|
// check if ProfileName contains "et"
|
||||||
|
match subkey.get_value::<String, _>("ProfileName") {
|
||||||
|
Ok(profile_name) => {
|
||||||
|
if profile_name.contains("et_")
|
||||||
|
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||||
|
{
|
||||||
|
keys_to_delete.push(subkey_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to read ProfileName for subkey {}: {}",
|
||||||
|
subkey_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for subkey_name in unmanaged_key.enum_keys().filter_map(Result::ok) {
|
||||||
|
let subkey = unmanaged_key.open_subkey(&subkey_name)?;
|
||||||
|
// check if ProfileName contains "et"
|
||||||
|
match subkey.get_value::<String, _>("Description") {
|
||||||
|
Ok(profile_name) => {
|
||||||
|
if profile_name.contains("et_")
|
||||||
|
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||||
|
{
|
||||||
|
keys_to_delete_unmanaged.push(subkey_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to read ProfileName for subkey {}: {}",
|
||||||
|
subkey_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete collected subkeys
|
||||||
|
if !keys_to_delete.is_empty() {
|
||||||
|
for subkey_name in keys_to_delete {
|
||||||
|
match profiles_key.delete_subkey_all(&subkey_name) {
|
||||||
|
Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name),
|
||||||
|
Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !keys_to_delete_unmanaged.is_empty() {
|
||||||
|
for subkey_name in keys_to_delete_unmanaged {
|
||||||
|
match unmanaged_key.delete_subkey_all(&subkey_name) {
|
||||||
|
Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name),
|
||||||
|
Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
|
||||||
|
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||||
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
|
let profiles_key = hklm.open_subkey_with_flags(
|
||||||
|
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||||
|
KEY_ALL_ACCESS,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) {
|
||||||
|
let subkey = profiles_key.open_subkey_with_flags(&subkey_name, KEY_ALL_ACCESS)?;
|
||||||
|
match subkey.get_value::<String, _>("ProfileName") {
|
||||||
|
Ok(profile_name) => {
|
||||||
|
if !dev_name.is_empty() && dev_name == profile_name {
|
||||||
|
match subkey.set_value("Category", &1u32) {
|
||||||
|
Ok(_) => tracing::trace!("Successfully set Category in registry"),
|
||||||
|
Err(e) => tracing::error!("Failed to set Category in registry: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to read ProfileName for subkey {}: {}",
|
||||||
|
subkey_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据接口名称查找 GUID
|
||||||
|
pub fn find_interface_guid(interface_name: &str) -> io::Result<String> {
|
||||||
|
// 注册表路径:所有网络接口的根目录
|
||||||
|
let network_key_path =
|
||||||
|
r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}";
|
||||||
|
|
||||||
|
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
|
let network_key = hklm.open_subkey_with_flags(network_key_path, KEY_READ)?;
|
||||||
|
|
||||||
|
// 遍历该路径下的所有 GUID 子键
|
||||||
|
for guid in network_key.enum_keys().map_while(Result::ok) {
|
||||||
|
if let Ok(guid_key) = network_key.open_subkey_with_flags(&guid, KEY_READ) {
|
||||||
|
// 检查 Connection/Name 是否匹配目标接口名
|
||||||
|
if let Ok(conn_key) = guid_key.open_subkey_with_flags("Connection", KEY_READ) {
|
||||||
|
if let Ok(name) = conn_key.get_value::<String, _>("Name") {
|
||||||
|
if name == interface_name {
|
||||||
|
return Ok(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到对应的接口
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"Interface not found",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开注册表键
|
||||||
|
pub fn open_interface_key(interface_guid: &str, prefix: &str) -> io::Result<RegKey> {
|
||||||
|
let path = format!(r"{}{}", prefix, interface_guid);
|
||||||
|
let hkey_local_machine = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||||
|
hkey_local_machine.open_subkey_with_flags(&path, KEY_WRITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用动态 DNS 更新
|
||||||
|
// disableDynamicUpdates sets the appropriate registry values to prevent the
|
||||||
|
// Windows DHCP client from sending dynamic DNS updates for our interface to
|
||||||
|
// AD domain controllers.
|
||||||
|
pub fn disable_dynamic_updates(interface_guid: &str) -> io::Result<()> {
|
||||||
|
let prefixes = [
|
||||||
|
Self::IPV4_TCPIP_INTERFACE_PREFIX,
|
||||||
|
Self::IPV6_TCPIP_INTERFACE_PREFIX,
|
||||||
|
];
|
||||||
|
|
||||||
|
for prefix in &prefixes {
|
||||||
|
let key = match Self::open_interface_key(interface_guid, prefix) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(e) => {
|
||||||
|
// 模拟 mute-key-not-found-if-closing 行为
|
||||||
|
if matches!(e.kind(), io::ErrorKind::NotFound) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
key.set_value("RegistrationEnabled", &0u32)?;
|
||||||
|
key.set_value("DisableDynamicUpdate", &1u32)?;
|
||||||
|
key.set_value("MaxNumberOfAddressesToRegister", &0u32)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置单个 DWORD 值到指定的注册表路径下
|
||||||
|
fn set_single_dword(
|
||||||
|
interface_guid: &str,
|
||||||
|
prefix: &str,
|
||||||
|
value_name: &str,
|
||||||
|
data: u32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let key = match Self::open_interface_key(interface_guid, prefix) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(e) => {
|
||||||
|
// 模拟 muteKeyNotFoundIfClosing 行为:忽略 Key Not Found 错误
|
||||||
|
return if matches!(e.kind(), io::ErrorKind::NotFound) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
key.set_value(value_name, &data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用 NetBIOS 名称解析请求
|
||||||
|
pub fn disable_netbios(interface_guid: &str) -> io::Result<()> {
|
||||||
|
Self::set_single_dword(
|
||||||
|
interface_guid,
|
||||||
|
Self::NETBT_INTERFACE_PREFIX,
|
||||||
|
"NetbiosOptions",
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,13 +4,14 @@ use std::{
|
|||||||
io::Write as _,
|
io::Write as _,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
use tokio::task::JoinSet;
|
use tokio::{task::JoinSet, time::timeout};
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
pub mod compressor;
|
pub mod compressor;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod defer;
|
pub mod defer;
|
||||||
|
pub mod dns;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod global_ctx;
|
pub mod global_ctx;
|
||||||
pub mod ifcfg;
|
pub mod ifcfg;
|
||||||
@@ -47,16 +48,13 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
|
|||||||
origin: String,
|
origin: String,
|
||||||
) {
|
) {
|
||||||
let js = Arc::downgrade(&js);
|
let js = Arc::downgrade(&js);
|
||||||
|
let o = origin.clone();
|
||||||
tokio::spawn(
|
tokio::spawn(
|
||||||
async move {
|
async move {
|
||||||
loop {
|
while js.strong_count() > 0 {
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
if js.weak_count() == 0 {
|
|
||||||
tracing::info!("joinset task exit");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
future::poll_fn(|cx| {
|
let fut = future::poll_fn(|cx| {
|
||||||
let Some(js) = js.upgrade() else {
|
let Some(js) = js.upgrade() else {
|
||||||
return std::task::Poll::Ready(());
|
return std::task::Poll::Ready(());
|
||||||
};
|
};
|
||||||
@@ -64,15 +62,24 @@ pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
|
|||||||
let mut js = js.lock().unwrap();
|
let mut js = js.lock().unwrap();
|
||||||
while !js.is_empty() {
|
while !js.is_empty() {
|
||||||
let ret = js.poll_join_next(cx);
|
let ret = js.poll_join_next(cx);
|
||||||
if ret.is_pending() {
|
match ret {
|
||||||
return std::task::Poll::Pending;
|
std::task::Poll::Ready(Some(_)) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::task::Poll::Ready(None) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::task::Poll::Pending => {
|
||||||
|
return std::task::Poll::Pending;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::task::Poll::Ready(())
|
std::task::Poll::Ready(())
|
||||||
})
|
});
|
||||||
.await;
|
|
||||||
|
let _ = timeout(std::time::Duration::from_secs(5), fut).await;
|
||||||
}
|
}
|
||||||
|
tracing::debug!(?o, "joinset task exit");
|
||||||
}
|
}
|
||||||
.instrument(tracing::info_span!(
|
.instrument(tracing::info_span!(
|
||||||
"join_joinset_background",
|
"join_joinset_background",
|
||||||
@@ -167,5 +174,6 @@ mod tests {
|
|||||||
drop(js);
|
drop(js);
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
assert_eq!(weak_js.weak_count(), 0);
|
assert_eq!(weak_js.weak_count(), 0);
|
||||||
|
assert_eq!(weak_js.strong_count(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -179,18 +179,16 @@ impl IPCollector {
|
|||||||
Self::do_collect_local_ip_addrs(self.net_ns.clone()).await;
|
Self::do_collect_local_ip_addrs(self.net_ns.clone()).await;
|
||||||
let net_ns = self.net_ns.clone();
|
let net_ns = self.net_ns.clone();
|
||||||
let stun_info_collector = self.stun_info_collector.clone();
|
let stun_info_collector = self.stun_info_collector.clone();
|
||||||
task.spawn(async move {
|
|
||||||
loop {
|
|
||||||
let ip_addrs = Self::do_collect_local_ip_addrs(net_ns.clone()).await;
|
|
||||||
*cached_ip_list.write().await = ip_addrs;
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(CACHED_IP_LIST_TIMEOUT_SEC))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let cached_ip_list = self.cached_ip_list.clone();
|
let cached_ip_list = self.cached_ip_list.clone();
|
||||||
task.spawn(async move {
|
task.spawn(async move {
|
||||||
|
let mut last_fetch_iface_time = std::time::Instant::now();
|
||||||
loop {
|
loop {
|
||||||
|
if last_fetch_iface_time.elapsed().as_secs() > CACHED_IP_LIST_TIMEOUT_SEC {
|
||||||
|
let ifaces = Self::do_collect_local_ip_addrs(net_ns.clone()).await;
|
||||||
|
*cached_ip_list.write().await = ifaces;
|
||||||
|
last_fetch_iface_time = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
let stun_info = stun_info_collector.get_stun_info();
|
let stun_info = stun_info_collector.get_stun_info();
|
||||||
for ip in stun_info.public_ip.iter() {
|
for ip in stun_info.public_ip.iter() {
|
||||||
let Ok(ip_addr) = ip.parse::<IpAddr>() else {
|
let Ok(ip_addr) = ip.parse::<IpAddr>() else {
|
||||||
@@ -199,14 +197,20 @@ impl IPCollector {
|
|||||||
|
|
||||||
match ip_addr {
|
match ip_addr {
|
||||||
IpAddr::V4(v) => {
|
IpAddr::V4(v) => {
|
||||||
cached_ip_list.write().await.public_ipv4 = Some(v.into())
|
cached_ip_list.write().await.public_ipv4.replace(v.into());
|
||||||
}
|
}
|
||||||
IpAddr::V6(v) => {
|
IpAddr::V6(v) => {
|
||||||
cached_ip_list.write().await.public_ipv6 = Some(v.into())
|
cached_ip_list.write().await.public_ipv6.replace(v.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"got public ip: {:?}, {:?}",
|
||||||
|
cached_ip_list.read().await.public_ipv4,
|
||||||
|
cached_ip_list.read().await.public_ipv6
|
||||||
|
);
|
||||||
|
|
||||||
let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_none() {
|
let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_none() {
|
||||||
CACHED_IP_LIST_TIMEOUT_SEC
|
CACHED_IP_LIST_TIMEOUT_SEC
|
||||||
} else {
|
} else {
|
||||||
@@ -217,10 +221,10 @@ impl IPCollector {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.cached_ip_list.read().await.deref().clone();
|
self.cached_ip_list.read().await.deref().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn collect_interfaces(net_ns: NetNS) -> Vec<NetworkInterface> {
|
pub async fn collect_interfaces(net_ns: NetNS, filter: bool) -> Vec<NetworkInterface> {
|
||||||
let _g = net_ns.guard();
|
let _g = net_ns.guard();
|
||||||
let ifaces = pnet::datalink::interfaces();
|
let ifaces = pnet::datalink::interfaces();
|
||||||
let mut ret = vec![];
|
let mut ret = vec![];
|
||||||
@@ -229,7 +233,7 @@ impl IPCollector {
|
|||||||
iface: iface.clone(),
|
iface: iface.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !f.filter_iface().await {
|
if filter && !f.filter_iface().await {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,21 +247,36 @@ impl IPCollector {
|
|||||||
async fn do_collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse {
|
async fn do_collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse {
|
||||||
let mut ret = GetIpListResponse::default();
|
let mut ret = GetIpListResponse::default();
|
||||||
|
|
||||||
let ifaces = Self::collect_interfaces(net_ns.clone()).await;
|
let ifaces = Self::collect_interfaces(net_ns.clone(), true).await;
|
||||||
let _g = net_ns.guard();
|
let _g = net_ns.guard();
|
||||||
for iface in ifaces {
|
for iface in ifaces {
|
||||||
for ip in iface.ips {
|
for ip in iface.ips {
|
||||||
let ip: std::net::IpAddr = ip.ip();
|
let ip: std::net::IpAddr = ip.ip();
|
||||||
if ip.is_loopback() || ip.is_multicast() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match ip {
|
match ip {
|
||||||
std::net::IpAddr::V4(v4) => {
|
std::net::IpAddr::V4(v4) => {
|
||||||
|
if ip.is_loopback() || ip.is_multicast() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ret.interface_ipv4s.push(v4.into());
|
ret.interface_ipv4s.push(v4.into());
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ifaces = Self::collect_interfaces(net_ns.clone(), false).await;
|
||||||
|
let _g = net_ns.guard();
|
||||||
|
for iface in ifaces {
|
||||||
|
for ip in iface.ips {
|
||||||
|
let ip: std::net::IpAddr = ip.ip();
|
||||||
|
match ip {
|
||||||
std::net::IpAddr::V6(v6) => {
|
std::net::IpAddr::V6(v6) => {
|
||||||
|
if v6.is_multicast() || v6.is_loopback() || v6.is_unicast_link_local() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ret.interface_ipv6s.push(v6.into());
|
ret.interface_ipv6s.push(v6.into());
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -20,23 +20,31 @@ use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
|||||||
|
|
||||||
use crate::common::error::Error;
|
use crate::common::error::Error;
|
||||||
|
|
||||||
|
use super::dns::resolve_txt_record;
|
||||||
use super::stun_codec_ext::*;
|
use super::stun_codec_ext::*;
|
||||||
|
|
||||||
struct HostResolverIter {
|
struct HostResolverIter {
|
||||||
hostnames: Vec<String>,
|
hostnames: Vec<String>,
|
||||||
ips: Vec<SocketAddr>,
|
ips: Vec<SocketAddr>,
|
||||||
max_ip_per_domain: u32,
|
max_ip_per_domain: u32,
|
||||||
|
use_ipv6: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostResolverIter {
|
impl HostResolverIter {
|
||||||
fn new(hostnames: Vec<String>, max_ip_per_domain: u32) -> Self {
|
fn new(hostnames: Vec<String>, max_ip_per_domain: u32, use_ipv6: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
hostnames,
|
hostnames,
|
||||||
ips: vec![],
|
ips: vec![],
|
||||||
max_ip_per_domain,
|
max_ip_per_domain,
|
||||||
|
use_ipv6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_txt_record(domain_name: &str) -> Result<Vec<String>, Error> {
|
||||||
|
let txt_data = resolve_txt_record(domain_name).await?;
|
||||||
|
Ok(txt_data.split(" ").map(|x| x.to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
#[async_recursion::async_recursion]
|
#[async_recursion::async_recursion]
|
||||||
async fn next(&mut self) -> Option<SocketAddr> {
|
async fn next(&mut self) -> Option<SocketAddr> {
|
||||||
if self.ips.is_empty() {
|
if self.ips.is_empty() {
|
||||||
@@ -51,10 +59,35 @@ impl HostResolverIter {
|
|||||||
format!("{}:3478", host)
|
format!("{}:3478", host)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if host.starts_with("txt:") {
|
||||||
|
let domain_name = host.trim_start_matches("txt:");
|
||||||
|
match Self::get_txt_record(domain_name).await {
|
||||||
|
Ok(hosts) => {
|
||||||
|
tracing::info!(
|
||||||
|
?domain_name,
|
||||||
|
?hosts,
|
||||||
|
"get txt record success when resolve stun server"
|
||||||
|
);
|
||||||
|
// insert hosts to the head of hostnames
|
||||||
|
self.hostnames.splice(0..0, hosts.into_iter());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
?domain_name,
|
||||||
|
?e,
|
||||||
|
"get txt record failed when resolve stun server"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.next().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let use_ipv6 = self.use_ipv6;
|
||||||
|
|
||||||
match lookup_host(&host).await {
|
match lookup_host(&host).await {
|
||||||
Ok(ips) => {
|
Ok(ips) => {
|
||||||
self.ips = ips
|
self.ips = ips
|
||||||
.filter(|x| x.is_ipv4())
|
.filter(|x| if use_ipv6 { x.is_ipv6() } else { x.is_ipv4() })
|
||||||
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
|
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
|
||||||
|
|
||||||
if self.ips.is_empty() {
|
if self.ips.is_empty() {
|
||||||
@@ -400,7 +433,7 @@ impl UdpNatTypeDetectResult {
|
|||||||
// find resp with distinct stun server
|
// find resp with distinct stun server
|
||||||
self.stun_resps
|
self.stun_resps
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.stun_server_addr)
|
.map(|x| x.recv_from_addr)
|
||||||
.collect::<BTreeSet<_>>()
|
.collect::<BTreeSet<_>>()
|
||||||
.len()
|
.len()
|
||||||
}
|
}
|
||||||
@@ -555,8 +588,11 @@ impl UdpNatTypeDetector {
|
|||||||
udp: Arc<UdpSocket>,
|
udp: Arc<UdpSocket>,
|
||||||
) -> Result<UdpNatTypeDetectResult, Error> {
|
) -> Result<UdpNatTypeDetectResult, Error> {
|
||||||
let mut stun_servers = vec![];
|
let mut stun_servers = vec![];
|
||||||
let mut host_resolver =
|
let mut host_resolver = HostResolverIter::new(
|
||||||
HostResolverIter::new(self.stun_server_hosts.clone(), self.max_ip_per_domain);
|
self.stun_server_hosts.clone(),
|
||||||
|
self.max_ip_per_domain,
|
||||||
|
false,
|
||||||
|
);
|
||||||
while let Some(addr) = host_resolver.next().await {
|
while let Some(addr) = host_resolver.next().await {
|
||||||
stun_servers.push(addr);
|
stun_servers.push(addr);
|
||||||
}
|
}
|
||||||
@@ -602,7 +638,9 @@ pub trait StunInfoCollectorTrait: Send + Sync {
|
|||||||
|
|
||||||
pub struct StunInfoCollector {
|
pub struct StunInfoCollector {
|
||||||
stun_servers: Arc<RwLock<Vec<String>>>,
|
stun_servers: Arc<RwLock<Vec<String>>>,
|
||||||
|
stun_servers_v6: Arc<RwLock<Vec<String>>>,
|
||||||
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
|
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
|
||||||
|
public_ipv6: Arc<AtomicCell<Option<Ipv6Addr>>>,
|
||||||
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
|
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
|
||||||
redetect_notify: Arc<tokio::sync::Notify>,
|
redetect_notify: Arc<tokio::sync::Notify>,
|
||||||
tasks: std::sync::Mutex<JoinSet<()>>,
|
tasks: std::sync::Mutex<JoinSet<()>>,
|
||||||
@@ -621,7 +659,12 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
|||||||
udp_nat_type: result.nat_type() as i32,
|
udp_nat_type: result.nat_type() as i32,
|
||||||
tcp_nat_type: 0,
|
tcp_nat_type: 0,
|
||||||
last_update_time: self.nat_test_result_time.load().timestamp(),
|
last_update_time: self.nat_test_result_time.load().timestamp(),
|
||||||
public_ip: result.public_ips().iter().map(|x| x.to_string()).collect(),
|
public_ip: result
|
||||||
|
.public_ips()
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.chain(self.public_ipv6.load().map(|x| x.to_string()))
|
||||||
|
.collect(),
|
||||||
min_port: result.min_port() as u32,
|
min_port: result.min_port() as u32,
|
||||||
max_port: result.max_port() as u32,
|
max_port: result.max_port() as u32,
|
||||||
}
|
}
|
||||||
@@ -640,7 +683,7 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
|||||||
|
|
||||||
if stun_servers.is_empty() {
|
if stun_servers.is_empty() {
|
||||||
let mut host_resolver =
|
let mut host_resolver =
|
||||||
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2);
|
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2, false);
|
||||||
while let Some(addr) = host_resolver.next().await {
|
while let Some(addr) = host_resolver.next().await {
|
||||||
stun_servers.push(addr);
|
stun_servers.push(addr);
|
||||||
if stun_servers.len() >= 2 {
|
if stun_servers.len() >= 2 {
|
||||||
@@ -680,7 +723,9 @@ impl StunInfoCollector {
|
|||||||
pub fn new(stun_servers: Vec<String>) -> Self {
|
pub fn new(stun_servers: Vec<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
||||||
|
stun_servers_v6: Arc::new(RwLock::new(Self::get_default_servers_v6())),
|
||||||
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||||
|
public_ipv6: Arc::new(AtomicCell::new(None)),
|
||||||
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
||||||
redetect_notify: Arc::new(tokio::sync::Notify::new()),
|
redetect_notify: Arc::new(tokio::sync::Notify::new()),
|
||||||
tasks: std::sync::Mutex::new(JoinSet::new()),
|
tasks: std::sync::Mutex::new(JoinSet::new()),
|
||||||
@@ -696,28 +741,45 @@ impl StunInfoCollector {
|
|||||||
// NOTICE: we may need to choose stun stun server based on geo location
|
// NOTICE: we may need to choose stun stun server based on geo location
|
||||||
// stun server cross nation may return a external ip address with high latency and loss rate
|
// stun server cross nation may return a external ip address with high latency and loss rate
|
||||||
vec![
|
vec![
|
||||||
|
"txt:stun.easytier.cn",
|
||||||
"stun.miwifi.com",
|
"stun.miwifi.com",
|
||||||
"stun.chat.bilibili.com",
|
"stun.chat.bilibili.com",
|
||||||
"stun.hitv.com",
|
"stun.hitv.com",
|
||||||
"stun.cdnbye.com",
|
|
||||||
"stun.douyucdn.cn:18000",
|
|
||||||
"fwa.lifesizecloud.com",
|
|
||||||
"global.turn.twilio.com",
|
|
||||||
"turn.cloudflare.com",
|
|
||||||
"stun.isp.net.au",
|
|
||||||
"stun.nextcloud.com",
|
|
||||||
"stun.freeswitch.org",
|
|
||||||
"stun.voip.blackberry.com",
|
|
||||||
"stunserver.stunprotocol.org",
|
|
||||||
"stun.sipnet.com",
|
|
||||||
"stun.radiojar.com",
|
|
||||||
"stun.sonetel.com",
|
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.to_string())
|
.map(|x| x.to_string())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_default_servers_v6() -> Vec<String> {
|
||||||
|
vec!["txt:stun-v6.easytier.cn"]
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_public_ipv6(servers: &Vec<String>) -> Option<Ipv6Addr> {
|
||||||
|
let mut ips = HostResolverIter::new(servers.to_vec(), 10, true);
|
||||||
|
while let Some(ip) = ips.next().await {
|
||||||
|
let Ok(udp_socket) = UdpSocket::bind(format!("[::]:0")).await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let udp = Arc::new(udp_socket);
|
||||||
|
let ret = StunClientBuilder::new(udp.clone())
|
||||||
|
.new_stun_client(ip)
|
||||||
|
.bind_request(false, false)
|
||||||
|
.await;
|
||||||
|
tracing::debug!(?ret, "finish ipv6 udp nat type detect");
|
||||||
|
match ret.map(|x| x.mapped_socket_addr.map(|x| x.ip())) {
|
||||||
|
Ok(Some(IpAddr::V6(v6))) => {
|
||||||
|
return Some(v6);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn start_stun_routine(&self) {
|
fn start_stun_routine(&self) {
|
||||||
if self.started.load(std::sync::atomic::Ordering::Relaxed) {
|
if self.started.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
@@ -784,6 +846,30 @@ impl StunInfoCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// for ipv6
|
||||||
|
let stun_servers = self.stun_servers_v6.clone();
|
||||||
|
let stored_ipv6 = self.public_ipv6.clone();
|
||||||
|
let redetect_notify = self.redetect_notify.clone();
|
||||||
|
self.tasks.lock().unwrap().spawn(async move {
|
||||||
|
loop {
|
||||||
|
let servers = stun_servers.read().unwrap().clone();
|
||||||
|
Self::get_public_ipv6(&servers)
|
||||||
|
.await
|
||||||
|
.map(|x| stored_ipv6.store(Some(x)));
|
||||||
|
|
||||||
|
let sleep_sec = if stored_ipv6.load().is_none() {
|
||||||
|
60
|
||||||
|
} else {
|
||||||
|
360
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = redetect_notify.notified() => {}
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_stun_info(&self) {
|
pub fn update_stun_info(&self) {
|
||||||
@@ -804,7 +890,7 @@ impl StunInfoCollectorTrait for MockStunInfoCollector {
|
|||||||
last_update_time: std::time::Instant::now().elapsed().as_secs() as i64,
|
last_update_time: std::time::Instant::now().elapsed().as_secs() as i64,
|
||||||
min_port: 100,
|
min_port: 100,
|
||||||
max_port: 200,
|
max_port: 200,
|
||||||
public_ip: vec!["127.0.0.1".to_string()],
|
public_ip: vec!["127.0.0.1".to_string(), "::1".to_string()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,6 +948,48 @@ mod tests {
|
|||||||
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||||
let ret = detector.detect_nat_type(0).await;
|
let ret = detector.detect_nat_type(0).await;
|
||||||
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||||
assert_eq!(ret.unwrap().nat_type(), NatType::PortRestricted);
|
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_txt_public_stun_server() {
|
||||||
|
let stun_servers = vec!["txt:stun.easytier.cn".to_string()];
|
||||||
|
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||||
|
let ret = detector.detect_nat_type(0).await;
|
||||||
|
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||||
|
assert!(!ret.unwrap().stun_resps.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_v4_stun() {
|
||||||
|
let mut udp_server = UdpTunnelListener::new("udp://0.0.0.0:55355".parse().unwrap());
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
tasks.spawn(async move {
|
||||||
|
udp_server.listen().await.unwrap();
|
||||||
|
loop {
|
||||||
|
udp_server.accept().await.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stun_servers = vec!["127.0.0.1:55355".to_string()];
|
||||||
|
|
||||||
|
let detector = UdpNatTypeDetector::new(stun_servers, 1);
|
||||||
|
let ret = detector.detect_nat_type(0).await;
|
||||||
|
println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type());
|
||||||
|
assert_eq!(ret.unwrap().nat_type(), NatType::Restricted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_v6_stun() {
|
||||||
|
let mut udp_server = UdpTunnelListener::new("udp://[::]:55355".parse().unwrap());
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
tasks.spawn(async move {
|
||||||
|
udp_server.listen().await.unwrap();
|
||||||
|
loop {
|
||||||
|
udp_server.accept().await.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let stun_servers = vec!["::1:55355".to_string()];
|
||||||
|
let ret = StunInfoCollector::get_public_ipv6(&stun_servers).await;
|
||||||
|
println!("{:#?}", ret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
// try connect peers directly, with either its public ip or lan ip
|
// try connect peers directly, with either its public ip or lan ip
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
net::SocketAddr,
|
collections::HashSet,
|
||||||
|
net::{Ipv6Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
@@ -10,28 +12,31 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
|
common::{error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait, PeerId},
|
||||||
peers::{
|
peers::{
|
||||||
peer_manager::PeerManager, peer_rpc::PeerRpcManager,
|
peer_conn::PeerConnId,
|
||||||
|
peer_manager::PeerManager,
|
||||||
|
peer_rpc::PeerRpcManager,
|
||||||
peer_rpc_service::DirectConnectorManagerRpcServer,
|
peer_rpc_service::DirectConnectorManagerRpcServer,
|
||||||
|
peer_task::{PeerTaskLauncher, PeerTaskManager},
|
||||||
},
|
},
|
||||||
proto::{
|
proto::{
|
||||||
peer_rpc::{
|
peer_rpc::{
|
||||||
DirectConnectorRpc, DirectConnectorRpcClientFactory, DirectConnectorRpcServer,
|
DirectConnectorRpc, DirectConnectorRpcClientFactory, DirectConnectorRpcServer,
|
||||||
GetIpListRequest, GetIpListResponse,
|
GetIpListRequest, GetIpListResponse, SendV6HolePunchPacketRequest,
|
||||||
},
|
},
|
||||||
rpc_types::controller::BaseController,
|
rpc_types::controller::BaseController,
|
||||||
},
|
},
|
||||||
|
tunnel::{udp::UdpTunnelConnector, IpVersion},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::proto::cli::PeerConnInfo;
|
use crate::proto::cli::PeerConnInfo;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio::{task::JoinSet, time::timeout};
|
use tokio::{net::UdpSocket, task::JoinSet, time::timeout};
|
||||||
use tracing::Instrument;
|
|
||||||
use url::Host;
|
use url::Host;
|
||||||
|
|
||||||
use super::create_connector_by_url;
|
use super::{create_connector_by_url, udp_hole_punch};
|
||||||
|
|
||||||
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
|
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
|
||||||
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
|
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
|
||||||
@@ -74,12 +79,11 @@ impl PeerManagerForDirectConnector for PeerManager {
|
|||||||
struct DstBlackListItem(PeerId, String);
|
struct DstBlackListItem(PeerId, String);
|
||||||
|
|
||||||
#[derive(Hash, Eq, PartialEq, Clone)]
|
#[derive(Hash, Eq, PartialEq, Clone)]
|
||||||
struct DstListenerUrlBlackListItem(PeerId, url::Url);
|
struct DstListenerUrlBlackListItem(PeerId, String);
|
||||||
|
|
||||||
struct DirectConnectorManagerData {
|
struct DirectConnectorManagerData {
|
||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
peer_manager: Arc<PeerManager>,
|
peer_manager: Arc<PeerManager>,
|
||||||
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
|
|
||||||
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
|
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +92,419 @@ impl DirectConnectorManagerData {
|
|||||||
Self {
|
Self {
|
||||||
global_ctx,
|
global_ctx,
|
||||||
peer_manager,
|
peer_manager,
|
||||||
dst_blacklist: timedmap::TimedMap::new(),
|
|
||||||
dst_listener_blacklist: timedmap::TimedMap::new(),
|
dst_listener_blacklist: timedmap::TimedMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remote_send_v6_hole_punch_packet(
|
||||||
|
&self,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
local_socket: &UdpSocket,
|
||||||
|
remote_url: &url::Url,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let global_ctx = self.peer_manager.get_global_ctx();
|
||||||
|
let listener_port = remote_url.port().ok_or(anyhow::anyhow!(
|
||||||
|
"failed to parse port from remote url: {}",
|
||||||
|
remote_url
|
||||||
|
))?;
|
||||||
|
let connector_ip = global_ctx
|
||||||
|
.get_stun_info_collector()
|
||||||
|
.get_stun_info()
|
||||||
|
.public_ip
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.contains(":"))
|
||||||
|
.ok_or(anyhow::anyhow!(
|
||||||
|
"failed to get public ipv6 address from stun info"
|
||||||
|
))?
|
||||||
|
.parse::<std::net::Ipv6Addr>()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to parse public ipv6 address from stun info: {:?}",
|
||||||
|
global_ctx.get_stun_info_collector().get_stun_info()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let connector_addr = SocketAddr::new(
|
||||||
|
std::net::IpAddr::V6(connector_ip),
|
||||||
|
local_socket.local_addr()?.port(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let rpc_stub = self
|
||||||
|
.peer_manager
|
||||||
|
.get_peer_rpc_mgr()
|
||||||
|
.rpc_client()
|
||||||
|
.scoped_client::<DirectConnectorRpcClientFactory<BaseController>>(
|
||||||
|
self.peer_manager.my_peer_id(),
|
||||||
|
dst_peer_id,
|
||||||
|
global_ctx.get_network_name(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rpc_stub
|
||||||
|
.send_v6_hole_punch_packet(
|
||||||
|
BaseController::default(),
|
||||||
|
SendV6HolePunchPacketRequest {
|
||||||
|
listener_port: listener_port as u32,
|
||||||
|
connector_addr: Some(connector_addr.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"do rpc, send v6 hole punch packet to peer {} at {}",
|
||||||
|
dst_peer_id, remote_url
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_to_public_ipv6(
|
||||||
|
&self,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
remote_url: &url::Url,
|
||||||
|
) -> Result<(PeerId, PeerConnId), Error> {
|
||||||
|
let local_socket = Arc::new(
|
||||||
|
UdpSocket::bind("[::]:0")
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to bind local socket for {}", remote_url))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ask remote to send v6 hole punch packet
|
||||||
|
// and no matter what the result is, continue to connect
|
||||||
|
let _ = self
|
||||||
|
.remote_send_v6_hole_punch_packet(dst_peer_id, &local_socket, &remote_url)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let udp_connector = UdpTunnelConnector::new(remote_url.clone());
|
||||||
|
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?;
|
||||||
|
|
||||||
|
// NOTICE: must add as directly connected tunnel
|
||||||
|
self.peer_manager.add_direct_tunnel(ret).await
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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(connector),
|
||||||
|
)
|
||||||
|
.await??
|
||||||
|
};
|
||||||
|
|
||||||
|
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
|
||||||
|
tracing::info!(
|
||||||
|
"connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}",
|
||||||
|
addr,
|
||||||
|
dst_peer_id,
|
||||||
|
peer_id
|
||||||
|
);
|
||||||
|
self.peer_manager
|
||||||
|
.get_peer_map()
|
||||||
|
.close_peer_conn(peer_id, &conn_id)
|
||||||
|
.await?;
|
||||||
|
return Err(Error::InvalidUrl(addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn try_connect_to_ip(
|
||||||
|
self: Arc<DirectConnectorManagerData>,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
addr: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut rand_gen = rand::rngs::OsRng::default();
|
||||||
|
let backoff_ms = vec![1000, 2000, 4000];
|
||||||
|
let mut backoff_idx = 0;
|
||||||
|
|
||||||
|
tracing::debug!(?dst_peer_id, ?addr, "try_connect_to_ip start");
|
||||||
|
|
||||||
|
self.dst_listener_blacklist.cleanup();
|
||||||
|
|
||||||
|
if self
|
||||||
|
.dst_listener_blacklist
|
||||||
|
.contains(&DstListenerUrlBlackListItem(
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
addr.clone(),
|
||||||
|
))
|
||||||
|
{
|
||||||
|
return Err(Error::UrlInBlacklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!(?dst_peer_id, ?addr, "try_connect_to_ip start one round");
|
||||||
|
let ret = self.do_try_connect_to_ip(dst_peer_id, addr.clone()).await;
|
||||||
|
tracing::debug!(?ret, ?dst_peer_id, ?addr, "try_connect_to_ip return");
|
||||||
|
if ret.is_ok() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if backoff_idx < backoff_ms.len() {
|
||||||
|
let delta = backoff_ms[backoff_idx] >> 1;
|
||||||
|
assert!(delta > 0);
|
||||||
|
assert!(delta < backoff_ms[backoff_idx]);
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(
|
||||||
|
(backoff_ms[backoff_idx] + rand_gen.gen_range(-delta..delta)) as u64,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
backoff_idx += 1;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
self.dst_listener_blacklist.insert(
|
||||||
|
DstListenerUrlBlackListItem(dst_peer_id.clone(), addr),
|
||||||
|
(),
|
||||||
|
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
|
||||||
|
);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_direct_connect_task(
|
||||||
|
self: &Arc<DirectConnectorManagerData>,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
ip_list: &GetIpListResponse,
|
||||||
|
listener: &url::Url,
|
||||||
|
tasks: &mut JoinSet<Result<(), Error>>,
|
||||||
|
) {
|
||||||
|
let Ok(mut addrs) = listener.socket_addrs(|| None) else {
|
||||||
|
tracing::error!(?listener, "failed to parse socket address from listener");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let listener_host = addrs.pop();
|
||||||
|
tracing::info!(?listener_host, ?listener, "try direct connect to peer");
|
||||||
|
match listener_host {
|
||||||
|
Some(SocketAddr::V4(s_addr)) => {
|
||||||
|
if s_addr.ip().is_unspecified() {
|
||||||
|
ip_list
|
||||||
|
.interface_ipv4s
|
||||||
|
.iter()
|
||||||
|
.chain(ip_list.public_ipv4.iter())
|
||||||
|
.for_each(|ip| {
|
||||||
|
let mut addr = (*listener).clone();
|
||||||
|
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
|
||||||
|
tasks.spawn(Self::try_connect_to_ip(
|
||||||
|
self.clone(),
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
addr.to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
tracing::error!(
|
||||||
|
?ip,
|
||||||
|
?listener,
|
||||||
|
?dst_peer_id,
|
||||||
|
"failed to set host for interface ipv4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||||
|
tasks.spawn(Self::try_connect_to_ip(
|
||||||
|
self.clone(),
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
listener.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(SocketAddr::V6(s_addr)) => {
|
||||||
|
if s_addr.ip().is_unspecified() {
|
||||||
|
// for ipv6, only try public ip
|
||||||
|
ip_list
|
||||||
|
.interface_ipv6s
|
||||||
|
.iter()
|
||||||
|
.chain(ip_list.public_ipv6.iter())
|
||||||
|
.filter_map(|x| Ipv6Addr::from_str(&x.to_string()).ok())
|
||||||
|
.filter(|x| {
|
||||||
|
TESTING.load(Ordering::Relaxed)
|
||||||
|
|| (!x.is_loopback()
|
||||||
|
&& !x.is_unspecified()
|
||||||
|
&& !x.is_unique_local()
|
||||||
|
&& !x.is_unicast_link_local()
|
||||||
|
&& !x.is_multicast())
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.iter()
|
||||||
|
.for_each(|ip| {
|
||||||
|
let mut addr = (*listener).clone();
|
||||||
|
if addr
|
||||||
|
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
tasks.spawn(Self::try_connect_to_ip(
|
||||||
|
self.clone(),
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
addr.to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
tracing::error!(
|
||||||
|
?ip,
|
||||||
|
?listener,
|
||||||
|
?dst_peer_id,
|
||||||
|
"failed to set host for public ipv6"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
||||||
|
tasks.spawn(Self::try_connect_to_ip(
|
||||||
|
self.clone(),
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
listener.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p => {
|
||||||
|
tracing::error!(?p, ?listener, "failed to parse ip version from listener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn do_try_direct_connect_internal(
|
||||||
|
self: &Arc<DirectConnectorManagerData>,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
ip_list: GetIpListResponse,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let enable_ipv6 = self.global_ctx.get_flags().enable_ipv6;
|
||||||
|
let available_listeners = ip_list
|
||||||
|
.listeners
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::<url::Url>::into)
|
||||||
|
.filter_map(|l| if l.scheme() != "ring" { Some(l) } else { None })
|
||||||
|
.filter(|l| l.port().is_some() && l.host().is_some())
|
||||||
|
.filter(|l| enable_ipv6 || !matches!(l.host().unwrap().to_owned(), Host::Ipv6(_)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
tracing::debug!(?available_listeners, "got available listeners");
|
||||||
|
|
||||||
|
if available_listeners.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("peer {} have no valid listener", dst_peer_id).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_protocol = self.global_ctx.get_flags().default_protocol;
|
||||||
|
// sort available listeners, default protocol has the highest priority, udp is second, others just random
|
||||||
|
// highest priority is in the last
|
||||||
|
let mut available_listeners = available_listeners;
|
||||||
|
available_listeners.sort_by_key(|l| {
|
||||||
|
let scheme = l.scheme();
|
||||||
|
if scheme == default_protocol {
|
||||||
|
3
|
||||||
|
} else if scheme == "udp" {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while !available_listeners.is_empty() {
|
||||||
|
let mut tasks = JoinSet::new();
|
||||||
|
let mut listener_list = vec![];
|
||||||
|
|
||||||
|
let cur_scheme = available_listeners.last().unwrap().scheme().to_owned();
|
||||||
|
while let Some(listener) = available_listeners.last() {
|
||||||
|
if listener.scheme() != cur_scheme {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("try direct connect to peer with listener: {}", listener);
|
||||||
|
self.spawn_direct_connect_task(
|
||||||
|
dst_peer_id.clone(),
|
||||||
|
&ip_list,
|
||||||
|
&listener,
|
||||||
|
&mut tasks,
|
||||||
|
);
|
||||||
|
|
||||||
|
listener_list.push(listener.clone().to_string());
|
||||||
|
available_listeners.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = tasks.join_all().await;
|
||||||
|
tracing::debug!(
|
||||||
|
?ret,
|
||||||
|
?dst_peer_id,
|
||||||
|
?cur_scheme,
|
||||||
|
?listener_list,
|
||||||
|
"all tasks finished for current scheme"
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.peer_manager.has_directly_connected_conn(dst_peer_id) {
|
||||||
|
tracing::info!(
|
||||||
|
"direct connect to peer {} success, has direct conn",
|
||||||
|
dst_peer_id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn do_try_direct_connect(
|
||||||
|
self: Arc<DirectConnectorManagerData>,
|
||||||
|
dst_peer_id: PeerId,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut backoff =
|
||||||
|
udp_hole_punch::BackOff::new(vec![1000, 2000, 2000, 5000, 5000, 10000, 30000, 60000]);
|
||||||
|
loop {
|
||||||
|
let peer_manager = self.peer_manager.clone();
|
||||||
|
tracing::debug!("try direct connect to peer: {}", dst_peer_id);
|
||||||
|
|
||||||
|
let rpc_stub = peer_manager
|
||||||
|
.get_peer_rpc_mgr()
|
||||||
|
.rpc_client()
|
||||||
|
.scoped_client::<DirectConnectorRpcClientFactory<BaseController>>(
|
||||||
|
peer_manager.my_peer_id(),
|
||||||
|
dst_peer_id,
|
||||||
|
self.global_ctx.get_network_name(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ip_list = rpc_stub
|
||||||
|
.get_ip_list(BaseController::default(), GetIpListRequest {})
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("get ip list from peer {}", dst_peer_id))?;
|
||||||
|
|
||||||
|
tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list");
|
||||||
|
|
||||||
|
let ret = self
|
||||||
|
.do_try_direct_connect_internal(dst_peer_id, ip_list)
|
||||||
|
.await;
|
||||||
|
tracing::info!(?ret, ?dst_peer_id, "do_try_direct_connect return");
|
||||||
|
|
||||||
|
if peer_manager.has_directly_connected_conn(dst_peer_id) {
|
||||||
|
tracing::info!(
|
||||||
|
"direct connect to peer {} success, has direct conn",
|
||||||
|
dst_peer_id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(backoff.next_backoff())).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for DirectConnectorManagerData {
|
impl std::fmt::Debug for DirectConnectorManagerData {
|
||||||
@@ -105,15 +518,62 @@ impl std::fmt::Debug for DirectConnectorManagerData {
|
|||||||
pub struct DirectConnectorManager {
|
pub struct DirectConnectorManager {
|
||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
data: Arc<DirectConnectorManagerData>,
|
data: Arc<DirectConnectorManagerData>,
|
||||||
|
client: PeerTaskManager<DirectConnectorLauncher>,
|
||||||
tasks: JoinSet<()>,
|
tasks: JoinSet<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct DirectConnectorLauncher(Arc<DirectConnectorManagerData>);
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl PeerTaskLauncher for DirectConnectorLauncher {
|
||||||
|
type Data = Arc<DirectConnectorManagerData>;
|
||||||
|
type CollectPeerItem = PeerId;
|
||||||
|
type TaskRet = ();
|
||||||
|
|
||||||
|
fn new_data(&self, _peer_mgr: Arc<PeerManager>) -> Self::Data {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec<Self::CollectPeerItem> {
|
||||||
|
let my_peer_id = data.peer_manager.my_peer_id();
|
||||||
|
data.peer_manager
|
||||||
|
.list_peers()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter(|peer_id| {
|
||||||
|
*peer_id != my_peer_id && !data.peer_manager.has_directly_connected_conn(*peer_id)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn launch_task(
|
||||||
|
&self,
|
||||||
|
data: &Self::Data,
|
||||||
|
item: Self::CollectPeerItem,
|
||||||
|
) -> tokio::task::JoinHandle<Result<Self::TaskRet, anyhow::Error>> {
|
||||||
|
let data = data.clone();
|
||||||
|
tokio::spawn(async move { data.do_try_direct_connect(item).await.map_err(Into::into) })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn all_task_done(&self, _data: &Self::Data) {}
|
||||||
|
|
||||||
|
fn loop_interval_ms(&self) -> u64 {
|
||||||
|
5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DirectConnectorManager {
|
impl DirectConnectorManager {
|
||||||
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Self {
|
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Self {
|
||||||
|
let data = Arc::new(DirectConnectorManagerData::new(
|
||||||
|
global_ctx.clone(),
|
||||||
|
peer_manager.clone(),
|
||||||
|
));
|
||||||
|
let client = PeerTaskManager::new(DirectConnectorLauncher(data.clone()), peer_manager);
|
||||||
Self {
|
Self {
|
||||||
global_ctx: global_ctx.clone(),
|
global_ctx,
|
||||||
data: Arc::new(DirectConnectorManagerData::new(global_ctx, peer_manager)),
|
data,
|
||||||
|
client,
|
||||||
tasks: JoinSet::new(),
|
tasks: JoinSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,321 +602,7 @@ impl DirectConnectorManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_as_client(&mut self) {
|
pub fn run_as_client(&mut self) {
|
||||||
let data = self.data.clone();
|
self.client.start();
|
||||||
let my_peer_id = self.data.peer_manager.my_peer_id();
|
|
||||||
self.tasks.spawn(
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
let peers = data.peer_manager.list_peers().await;
|
|
||||||
let mut tasks = JoinSet::new();
|
|
||||||
for peer_id in peers {
|
|
||||||
if peer_id == my_peer_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tasks.spawn(Self::do_try_direct_connect(data.clone(), peer_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(task_ret) = tasks.join_next().await {
|
|
||||||
tracing::debug!(?task_ret, ?my_peer_id, "direct connect task ret");
|
|
||||||
}
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.instrument(
|
|
||||||
tracing::info_span!("direct_connector_client", my_id = ?self.global_ctx.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_try_connect_to_ip(
|
|
||||||
data: Arc<DirectConnectorManagerData>,
|
|
||||||
dst_peer_id: PeerId,
|
|
||||||
addr: String,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
data.dst_blacklist.cleanup();
|
|
||||||
if data
|
|
||||||
.dst_blacklist
|
|
||||||
.contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone()))
|
|
||||||
{
|
|
||||||
tracing::debug!("try_connect_to_ip failed, addr in blacklist: {}", addr);
|
|
||||||
return Err(Error::UrlInBlacklist);
|
|
||||||
}
|
|
||||||
|
|
||||||
let connector = create_connector_by_url(&addr, &data.global_ctx).await?;
|
|
||||||
let (peer_id, conn_id) = timeout(
|
|
||||||
std::time::Duration::from_secs(5),
|
|
||||||
data.peer_manager.try_connect(connector),
|
|
||||||
)
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?;
|
|
||||||
|
|
||||||
if peer_id != dst_peer_id && !TESTING.load(Ordering::Relaxed) {
|
|
||||||
tracing::info!(
|
|
||||||
"connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}",
|
|
||||||
addr,
|
|
||||||
dst_peer_id,
|
|
||||||
peer_id
|
|
||||||
);
|
|
||||||
data.peer_manager
|
|
||||||
.get_peer_map()
|
|
||||||
.close_peer_conn(peer_id, &conn_id)
|
|
||||||
.await?;
|
|
||||||
return Err(Error::InvalidUrl(addr));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
async fn try_connect_to_ip(
|
|
||||||
data: Arc<DirectConnectorManagerData>,
|
|
||||||
dst_peer_id: PeerId,
|
|
||||||
addr: String,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let mut rand_gen = rand::rngs::OsRng::default();
|
|
||||||
let backoff_ms = vec![1000, 2000, 4000];
|
|
||||||
let mut backoff_idx = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let ret = Self::do_try_connect_to_ip(data.clone(), dst_peer_id, addr.clone()).await;
|
|
||||||
tracing::debug!(?ret, ?dst_peer_id, ?addr, "try_connect_to_ip return");
|
|
||||||
if matches!(ret, Err(Error::UrlInBlacklist) | Ok(_)) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if backoff_idx < backoff_ms.len() {
|
|
||||||
let delta = backoff_ms[backoff_idx] >> 1;
|
|
||||||
assert!(delta > 0);
|
|
||||||
assert!(delta < backoff_ms[backoff_idx]);
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(
|
|
||||||
(backoff_ms[backoff_idx] + rand_gen.gen_range(-delta..delta)) as u64,
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
backoff_idx += 1;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
data.dst_blacklist.insert(
|
|
||||||
DstBlackListItem(dst_peer_id.clone(), addr.clone()),
|
|
||||||
(),
|
|
||||||
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
async fn do_try_direct_connect_internal(
|
|
||||||
data: Arc<DirectConnectorManagerData>,
|
|
||||||
dst_peer_id: PeerId,
|
|
||||||
ip_list: GetIpListResponse,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
data.dst_listener_blacklist.cleanup();
|
|
||||||
|
|
||||||
let enable_ipv6 = data.global_ctx.get_flags().enable_ipv6;
|
|
||||||
let available_listeners = ip_list
|
|
||||||
.listeners
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::<url::Url>::into)
|
|
||||||
.filter_map(|l| if l.scheme() != "ring" { Some(l) } else { None })
|
|
||||||
.filter(|l| l.port().is_some() && l.host().is_some())
|
|
||||||
.filter(|l| {
|
|
||||||
!data
|
|
||||||
.dst_listener_blacklist
|
|
||||||
.contains(&DstListenerUrlBlackListItem(dst_peer_id.clone(), l.clone()))
|
|
||||||
})
|
|
||||||
.filter(|l| enable_ipv6 || !matches!(l.host().unwrap().to_owned(), Host::Ipv6(_)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
tracing::debug!(?available_listeners, "got available listeners");
|
|
||||||
|
|
||||||
let mut listener = available_listeners.get(0).ok_or(anyhow::anyhow!(
|
|
||||||
"peer {} have no valid listener",
|
|
||||||
dst_peer_id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// if have default listener, use it first
|
|
||||||
listener = available_listeners
|
|
||||||
.iter()
|
|
||||||
.find(|l| l.scheme() == data.global_ctx.get_flags().default_protocol)
|
|
||||||
.unwrap_or(listener);
|
|
||||||
|
|
||||||
let mut tasks = JoinSet::new();
|
|
||||||
|
|
||||||
let listener_host = listener.socket_addrs(|| None).unwrap().pop();
|
|
||||||
match listener_host {
|
|
||||||
Some(SocketAddr::V4(s_addr)) => {
|
|
||||||
if s_addr.ip().is_unspecified() {
|
|
||||||
ip_list.interface_ipv4s.iter().for_each(|ip| {
|
|
||||||
let mut addr = (*listener).clone();
|
|
||||||
if addr.set_host(Some(ip.to_string().as_str())).is_ok() {
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
addr.to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
tracing::error!(
|
|
||||||
?ip,
|
|
||||||
?listener,
|
|
||||||
?dst_peer_id,
|
|
||||||
"failed to set host for interface ipv4"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(public_ipv4) = ip_list.public_ipv4 {
|
|
||||||
let mut addr = (*listener).clone();
|
|
||||||
if addr
|
|
||||||
.set_host(Some(public_ipv4.to_string().as_str()))
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
addr.to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
tracing::error!(
|
|
||||||
?public_ipv4,
|
|
||||||
?listener,
|
|
||||||
?dst_peer_id,
|
|
||||||
"failed to set host for public ipv4"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
listener.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(SocketAddr::V6(s_addr)) => {
|
|
||||||
if s_addr.ip().is_unspecified() {
|
|
||||||
ip_list.interface_ipv6s.iter().for_each(|ip| {
|
|
||||||
let mut addr = (*listener).clone();
|
|
||||||
if addr
|
|
||||||
.set_host(Some(format!("[{}]", ip.to_string()).as_str()))
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
addr.to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
tracing::error!(
|
|
||||||
?ip,
|
|
||||||
?listener,
|
|
||||||
?dst_peer_id,
|
|
||||||
"failed to set host for interface ipv6"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(public_ipv6) = ip_list.public_ipv6 {
|
|
||||||
let mut addr = (*listener).clone();
|
|
||||||
if addr
|
|
||||||
.set_host(Some(format!("[{}]", public_ipv6.to_string()).as_str()))
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
addr.to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
tracing::error!(
|
|
||||||
?public_ipv6,
|
|
||||||
?listener,
|
|
||||||
?dst_peer_id,
|
|
||||||
"failed to set host for public ipv6"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !s_addr.ip().is_loopback() || TESTING.load(Ordering::Relaxed) {
|
|
||||||
tasks.spawn(Self::try_connect_to_ip(
|
|
||||||
data.clone(),
|
|
||||||
dst_peer_id.clone(),
|
|
||||||
listener.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p => {
|
|
||||||
tracing::error!(?p, ?listener, "failed to parse ip version from listener");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut has_succ = false;
|
|
||||||
while let Some(ret) = tasks.join_next().await {
|
|
||||||
match ret {
|
|
||||||
Ok(Ok(_)) => {
|
|
||||||
has_succ = true;
|
|
||||||
tracing::info!(
|
|
||||||
?dst_peer_id,
|
|
||||||
?listener,
|
|
||||||
"try direct connect to peer success"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
tracing::info!(?e, "try direct connect to peer failed");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(?e, "try direct connect to peer task join failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !has_succ {
|
|
||||||
data.dst_listener_blacklist.insert(
|
|
||||||
DstListenerUrlBlackListItem(dst_peer_id.clone(), listener.clone()),
|
|
||||||
(),
|
|
||||||
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
async fn do_try_direct_connect(
|
|
||||||
data: Arc<DirectConnectorManagerData>,
|
|
||||||
dst_peer_id: PeerId,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let peer_manager = data.peer_manager.clone();
|
|
||||||
// check if we have direct connection with dst_peer_id
|
|
||||||
if let Some(c) = peer_manager.list_peer_conns(dst_peer_id).await {
|
|
||||||
// currently if we have any type of direct connection (udp or tcp), we will not try to connect
|
|
||||||
if !c.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("try direct connect to peer: {}", dst_peer_id);
|
|
||||||
|
|
||||||
let rpc_stub = peer_manager
|
|
||||||
.get_peer_rpc_mgr()
|
|
||||||
.rpc_client()
|
|
||||||
.scoped_client::<DirectConnectorRpcClientFactory<BaseController>>(
|
|
||||||
peer_manager.my_peer_id(),
|
|
||||||
dst_peer_id,
|
|
||||||
data.global_ctx.get_network_name(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let ip_list = rpc_stub
|
|
||||||
.get_ip_list(BaseController::default(), GetIpListRequest {})
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("get ip list from peer {}", dst_peer_id))?;
|
|
||||||
|
|
||||||
tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list");
|
|
||||||
|
|
||||||
Self::do_try_direct_connect_internal(data, dst_peer_id, ip_list).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +612,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
connector::direct::{
|
connector::direct::{
|
||||||
DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem,
|
DirectConnectorManager, DirectConnectorManagerData, DstListenerUrlBlackListItem,
|
||||||
DstListenerUrlBlackListItem,
|
|
||||||
},
|
},
|
||||||
instance::listeners::ListenerManager,
|
instance::listeners::ListenerManager,
|
||||||
peers::tests::{
|
peers::tests::{
|
||||||
@@ -526,9 +671,7 @@ mod tests {
|
|||||||
#[values("tcp", "udp", "wg")] proto: &str,
|
#[values("tcp", "udp", "wg")] proto: &str,
|
||||||
#[values("true", "false")] ipv6: bool,
|
#[values("true", "false")] ipv6: bool,
|
||||||
) {
|
) {
|
||||||
if ipv6 && proto != "udp" {
|
TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let p_a = create_mock_peer_manager().await;
|
let p_a = create_mock_peer_manager().await;
|
||||||
let p_b = create_mock_peer_manager().await;
|
let p_b = create_mock_peer_manager().await;
|
||||||
@@ -538,20 +681,31 @@ mod tests {
|
|||||||
|
|
||||||
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
||||||
|
|
||||||
|
p_c.get_global_ctx()
|
||||||
|
.get_ip_collector()
|
||||||
|
.collect_ip_addrs()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
|
||||||
|
|
||||||
let mut dm_a = DirectConnectorManager::new(p_a.get_global_ctx(), p_a.clone());
|
let mut dm_a = DirectConnectorManager::new(p_a.get_global_ctx(), p_a.clone());
|
||||||
let mut dm_c = DirectConnectorManager::new(p_c.get_global_ctx(), p_c.clone());
|
let mut dm_c = DirectConnectorManager::new(p_c.get_global_ctx(), p_c.clone());
|
||||||
|
|
||||||
dm_a.run_as_client();
|
dm_a.run_as_client();
|
||||||
dm_c.run_as_server();
|
dm_c.run_as_server();
|
||||||
|
|
||||||
|
let port = if proto == "wg" { 11040 } else { 11041 };
|
||||||
if !ipv6 {
|
if !ipv6 {
|
||||||
let port = if proto == "wg" { 11040 } else { 11041 };
|
|
||||||
p_c.get_global_ctx().config.set_listeners(vec![format!(
|
p_c.get_global_ctx().config.set_listeners(vec![format!(
|
||||||
"{}://0.0.0.0:{}",
|
"{}://0.0.0.0:{}",
|
||||||
proto, port
|
proto, port
|
||||||
)
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap()]);
|
.unwrap()]);
|
||||||
|
} else {
|
||||||
|
p_c.get_global_ctx()
|
||||||
|
.config
|
||||||
|
.set_listeners(vec![format!("{}://[::]:{}", proto, port).parse().unwrap()]);
|
||||||
}
|
}
|
||||||
let mut f = p_c.get_global_ctx().config.get_flags();
|
let mut f = p_c.get_global_ctx().config.get_flags();
|
||||||
f.enable_ipv6 = ipv6;
|
f.enable_ipv6 = ipv6;
|
||||||
@@ -568,6 +722,7 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn direct_connector_scheme_blacklist() {
|
async fn direct_connector_scheme_blacklist() {
|
||||||
|
TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
let p_a = create_mock_peer_manager().await;
|
let p_a = create_mock_peer_manager().await;
|
||||||
let data = Arc::new(DirectConnectorManagerData::new(
|
let data = Arc::new(DirectConnectorManagerData::new(
|
||||||
p_a.get_global_ctx(),
|
p_a.get_global_ctx(),
|
||||||
@@ -582,7 +737,7 @@ mod tests {
|
|||||||
.interface_ipv4s
|
.interface_ipv4s
|
||||||
.push("127.0.0.1".parse::<std::net::Ipv4Addr>().unwrap().into());
|
.push("127.0.0.1".parse::<std::net::Ipv4Addr>().unwrap().into());
|
||||||
|
|
||||||
DirectConnectorManager::do_try_direct_connect_internal(data.clone(), 1, ip_list.clone())
|
data.do_try_direct_connect_internal(1, ip_list.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -592,9 +747,5 @@ mod tests {
|
|||||||
1,
|
1,
|
||||||
"tcp://127.0.0.1:10222".parse().unwrap()
|
"tcp://127.0.0.1:10222".parse().unwrap()
|
||||||
)));
|
)));
|
||||||
|
|
||||||
assert!(data
|
|
||||||
.dst_blacklist
|
|
||||||
.contains(&DstBlackListItem(1, ip_list.listeners[0].to_string())));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
256
easytier/src/connector/dns_connector.rs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{
|
||||||
|
dns::{resolve_txt_record, RESOLVER},
|
||||||
|
error::Error,
|
||||||
|
global_ctx::ArcGlobalCtx,
|
||||||
|
},
|
||||||
|
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, PROTO_PORT_OFFSET},
|
||||||
|
};
|
||||||
|
use anyhow::Context;
|
||||||
|
use dashmap::DashSet;
|
||||||
|
use hickory_resolver::proto::rr::rdata::SRV;
|
||||||
|
use rand::{seq::SliceRandom, Rng as _};
|
||||||
|
|
||||||
|
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();
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let rand_value = rng.gen_range(0..total_weight);
|
||||||
|
let mut accumulated_weight = 0;
|
||||||
|
|
||||||
|
for (item, weight) in options {
|
||||||
|
accumulated_weight += *weight;
|
||||||
|
if rand_value < accumulated_weight {
|
||||||
|
return Some(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DNSTunnelConnector {
|
||||||
|
addr: url::Url,
|
||||||
|
bind_addrs: Vec<SocketAddr>,
|
||||||
|
global_ctx: ArcGlobalCtx,
|
||||||
|
ip_version: IpVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DNSTunnelConnector {
|
||||||
|
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
||||||
|
Self {
|
||||||
|
addr,
|
||||||
|
bind_addrs: Vec::new(),
|
||||||
|
global_ctx,
|
||||||
|
ip_version: IpVersion::Both,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(ret, err)]
|
||||||
|
pub async fn handle_txt_record(
|
||||||
|
&self,
|
||||||
|
domain_name: &str,
|
||||||
|
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||||
|
let txt_data = resolve_txt_record(domain_name)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("resolve txt record failed, domain_name: {}", domain_name))?;
|
||||||
|
|
||||||
|
let candidate_urls = txt_data
|
||||||
|
.split(" ")
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.filter_map(|s| url::Url::parse(s.as_str()).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// shuffle candidate_urls and get the first one
|
||||||
|
let url = candidate_urls
|
||||||
|
.choose(&mut rand::thread_rng())
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"no valid url found, txt_data: {}, expecting an url list splitted by space",
|
||||||
|
txt_data
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let connector =
|
||||||
|
create_connector_by_url(url.as_str(), &self.global_ctx, self.ip_version).await?;
|
||||||
|
Ok(connector)
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
let connector_dst = record.target().to_utf8();
|
||||||
|
let dst_url = format!("{}://{}:{}", protocol, connector_dst, record.port());
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
dst_url.parse().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"parse dst_url failed, protocol: {}, connector_dst: {}, port: {}, dst_url: {}",
|
||||||
|
protocol,
|
||||||
|
connector_dst,
|
||||||
|
record.port(),
|
||||||
|
dst_url
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
record.priority() as _,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(ret, err)]
|
||||||
|
pub async fn handle_srv_record(
|
||||||
|
&self,
|
||||||
|
domain_name: &str,
|
||||||
|
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||||
|
tracing::info!("handle_srv_record: {}", domain_name);
|
||||||
|
|
||||||
|
let srv_domains = PROTO_PORT_OFFSET
|
||||||
|
.iter()
|
||||||
|
.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(|(srv_domain, protocol)| {
|
||||||
|
let resolver = RESOLVER.clone();
|
||||||
|
let responses = responses.clone();
|
||||||
|
async move {
|
||||||
|
let response = resolver.srv_lookup(srv_domain).await.with_context(|| {
|
||||||
|
format!("srv_lookup failed, srv_domain: {}", srv_domain.to_string())
|
||||||
|
})?;
|
||||||
|
tracing::info!(?response, ?srv_domain, "srv_lookup response");
|
||||||
|
for record in response.iter() {
|
||||||
|
let parsed_record = Self::handle_one_srv_record(record, &protocol);
|
||||||
|
tracing::info!(?parsed_record, ?srv_domain, "parsed_record");
|
||||||
|
if parsed_record.is_err() {
|
||||||
|
eprintln!(
|
||||||
|
"got invalid srv record {:?}",
|
||||||
|
parsed_record.as_ref().unwrap_err()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
responses.insert(parsed_record.unwrap());
|
||||||
|
}
|
||||||
|
Ok::<_, Error>(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let _ = futures::future::join_all(srv_lookup_tasks).await;
|
||||||
|
|
||||||
|
let srv_records = responses.iter().map(|r| r.clone()).collect::<Vec<_>>();
|
||||||
|
if srv_records.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("no srv record found").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = weighted_choice(srv_records.as_slice()).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to choose a srv record, domain_name: {}, srv_records: {:?}",
|
||||||
|
domain_name.to_string(),
|
||||||
|
srv_records
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let connector =
|
||||||
|
create_connector_by_url(url.as_str(), &self.global_ctx, self.ip_version).await?;
|
||||||
|
Ok(connector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl super::TunnelConnector for DNSTunnelConnector {
|
||||||
|
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||||
|
let mut conn = if self.addr.scheme() == "txt" {
|
||||||
|
self.handle_txt_record(self.addr.host_str().as_ref().unwrap())
|
||||||
|
.await
|
||||||
|
.with_context(|| "get txt record url failed")?
|
||||||
|
} else if self.addr.scheme() == "srv" {
|
||||||
|
self.handle_srv_record(self.addr.host_str().as_ref().unwrap())
|
||||||
|
.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();
|
||||||
|
Ok(Box::new(TunnelWithInfo::new(
|
||||||
|
t,
|
||||||
|
TunnelInfo {
|
||||||
|
local_addr: info.local_addr.clone(),
|
||||||
|
remote_addr: Some(self.addr.clone().into()),
|
||||||
|
tunnel_type: format!(
|
||||||
|
"{}-{}",
|
||||||
|
self.addr.scheme(),
|
||||||
|
info.remote_addr.unwrap_or_default()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_url(&self) -> url::Url {
|
||||||
|
self.addr.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
|
||||||
|
self.bind_addrs = addrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||||
|
self.ip_version = ip_version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
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);
|
||||||
|
connector.set_ip_version(IpVersion::V4);
|
||||||
|
for _ in 0..5 {
|
||||||
|
match connector.connect().await {
|
||||||
|
Ok(ret) => {
|
||||||
|
println!("{:?}", ret.info());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
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);
|
||||||
|
connector.set_ip_version(IpVersion::V4);
|
||||||
|
for _ in 0..5 {
|
||||||
|
match connector.connect().await {
|
||||||
|
Ok(ret) => {
|
||||||
|
println!("{:?}", ret.info());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
320
easytier/src/connector/http_connector.rs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
pin::Pin,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use http_req::request::{RedirectPolicy, Request};
|
||||||
|
use rand::seq::SliceRandom as _;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{error::Error, global_ctx::ArcGlobalCtx},
|
||||||
|
tunnel::{IpVersion, Tunnel, TunnelConnector, TunnelError, ZCPacketSink, ZCPacketStream},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::proto::common::TunnelInfo;
|
||||||
|
|
||||||
|
use super::create_connector_by_url;
|
||||||
|
|
||||||
|
pub struct TunnelWithInfo {
|
||||||
|
inner: Box<dyn Tunnel>,
|
||||||
|
info: TunnelInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TunnelWithInfo {
|
||||||
|
pub fn new(inner: Box<dyn Tunnel>, info: TunnelInfo) -> Self {
|
||||||
|
Self { inner, info }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tunnel for TunnelWithInfo {
|
||||||
|
fn split(&self) -> (Pin<Box<dyn ZCPacketStream>>, Pin<Box<dyn ZCPacketSink>>) {
|
||||||
|
self.inner.split()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(&self) -> Option<TunnelInfo> {
|
||||||
|
Some(self.info.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
|
enum HttpRedirectType {
|
||||||
|
Unknown,
|
||||||
|
// redirected url is in the path of new url
|
||||||
|
RedirectToQuery,
|
||||||
|
// redirected url is the entire new url
|
||||||
|
RedirectToUrl,
|
||||||
|
// redirected url is in the body of response
|
||||||
|
BodyUrls,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HttpTunnelConnector {
|
||||||
|
addr: url::Url,
|
||||||
|
bind_addrs: Vec<SocketAddr>,
|
||||||
|
ip_version: IpVersion,
|
||||||
|
global_ctx: ArcGlobalCtx,
|
||||||
|
redirect_type: HttpRedirectType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpTunnelConnector {
|
||||||
|
pub fn new(addr: url::Url, global_ctx: ArcGlobalCtx) -> Self {
|
||||||
|
Self {
|
||||||
|
addr,
|
||||||
|
bind_addrs: Vec::new(),
|
||||||
|
ip_version: IpVersion::Both,
|
||||||
|
global_ctx,
|
||||||
|
redirect_type: HttpRedirectType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(ret)]
|
||||||
|
async fn handle_302_redirect(
|
||||||
|
&mut self,
|
||||||
|
new_url: url::Url,
|
||||||
|
url_str: &str,
|
||||||
|
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||||
|
// the url should be in following format:
|
||||||
|
// 1: http(s)://easytier.cn/?url=tcp://10.147.22.22:11010 (scheme is http, domain is ignored, path is splitted into proto type and addr)
|
||||||
|
// 2: http(s)://tcp://10.137.22.22:11010 (connector url is appended to the scheme)
|
||||||
|
// 3: tcp://10.137.22.22:11010 (scheme is protocol type, the url is used to construct a connector directly)
|
||||||
|
tracing::info!("redirect to {}", new_url);
|
||||||
|
let url = url::Url::parse(new_url.as_str())
|
||||||
|
.with_context(|| format!("parsing redirect url failed. url: {}", new_url))?;
|
||||||
|
if url.scheme() == "http" || url.scheme() == "https" {
|
||||||
|
let mut query = new_url
|
||||||
|
.query_pairs()
|
||||||
|
.filter_map(|x| url::Url::parse(&x.1).ok())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
query.shuffle(&mut rand::thread_rng());
|
||||||
|
if !query.is_empty() {
|
||||||
|
tracing::info!("try to create connector by url: {}", query[0]);
|
||||||
|
self.redirect_type = HttpRedirectType::RedirectToQuery;
|
||||||
|
return create_connector_by_url(
|
||||||
|
&query[0].to_string(),
|
||||||
|
&self.global_ctx,
|
||||||
|
self.ip_version,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
} else if let Some(new_url) = url_str
|
||||||
|
.strip_prefix(format!("{}://", url.scheme()).as_str())
|
||||||
|
.and_then(|x| Url::parse(x).ok())
|
||||||
|
{
|
||||||
|
// stripe the scheme and create connector by url
|
||||||
|
self.redirect_type = HttpRedirectType::RedirectToUrl;
|
||||||
|
return create_connector_by_url(
|
||||||
|
new_url.as_str(),
|
||||||
|
&self.global_ctx,
|
||||||
|
self.ip_version,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
return Err(Error::InvalidUrl(format!(
|
||||||
|
"no valid connector url found in url: {}",
|
||||||
|
url
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
self.redirect_type = HttpRedirectType::RedirectToUrl;
|
||||||
|
return create_connector_by_url(new_url.as_str(), &self.global_ctx, self.ip_version)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn handle_200_success(
|
||||||
|
&mut self,
|
||||||
|
body: &String,
|
||||||
|
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||||
|
// resp body should be line of connector urls, like:
|
||||||
|
// tcp://10.1.1.1:11010
|
||||||
|
// udp://10.1.1.1:11010
|
||||||
|
let mut lines = body
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim())
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
tracing::info!("get {} lines of connector urls", lines.len());
|
||||||
|
|
||||||
|
// shuffle the lines and pick the usable one
|
||||||
|
lines.shuffle(&mut rand::thread_rng());
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let url = url::Url::parse(line);
|
||||||
|
if url.is_err() {
|
||||||
|
tracing::warn!("invalid url: {}, skip it", line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.redirect_type = HttpRedirectType::BodyUrls;
|
||||||
|
return create_connector_by_url(line, &self.global_ctx, self.ip_version).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::InvalidUrl(format!(
|
||||||
|
"no valid connector url found, response body: {}",
|
||||||
|
body
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(ret)]
|
||||||
|
pub async fn get_redirected_connector(
|
||||||
|
&mut self,
|
||||||
|
original_url: &str,
|
||||||
|
) -> Result<Box<dyn TunnelConnector>, Error> {
|
||||||
|
self.redirect_type = HttpRedirectType::Unknown;
|
||||||
|
tracing::info!("get_redirected_url: {}", original_url);
|
||||||
|
// Container for body of a response.
|
||||||
|
let body = Arc::new(RwLock::new(Vec::new()));
|
||||||
|
|
||||||
|
let original_url_clone = original_url.to_string();
|
||||||
|
let body_clone = body.clone();
|
||||||
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
|
let uri = http_req::uri::Uri::try_from(original_url_clone.as_ref())
|
||||||
|
.with_context(|| format!("parsing url failed. url: {}", original_url_clone))?;
|
||||||
|
|
||||||
|
tracing::info!("sending http request to {}", uri);
|
||||||
|
|
||||||
|
Request::new(&uri)
|
||||||
|
.redirect_policy(RedirectPolicy::Limit(0))
|
||||||
|
.timeout(std::time::Duration::from_secs(20))
|
||||||
|
.send(&mut *body_clone.write().unwrap())
|
||||||
|
.with_context(|| format!("sending http request failed. url: {}", uri))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::InvalidUrl(format!("task join error: {}", e)))??;
|
||||||
|
|
||||||
|
let body = String::from_utf8_lossy(&body.read().unwrap()).to_string();
|
||||||
|
|
||||||
|
if res.status_code().is_redirect() {
|
||||||
|
let redirect_url = res
|
||||||
|
.headers()
|
||||||
|
.get("Location")
|
||||||
|
.ok_or_else(|| Error::InvalidUrl("no redirect address found".to_string()))?;
|
||||||
|
let new_url = url::Url::parse(redirect_url.as_str())
|
||||||
|
.with_context(|| format!("parsing redirect url failed. url: {}", redirect_url))?;
|
||||||
|
return self.handle_302_redirect(new_url, &redirect_url).await;
|
||||||
|
} else if res.status_code().is_success() {
|
||||||
|
return self.handle_200_success(&body).await;
|
||||||
|
} else {
|
||||||
|
return Err(Error::InvalidUrl(format!(
|
||||||
|
"unexpected response, resp: {:?}, body: {}",
|
||||||
|
res, body,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl super::TunnelConnector for HttpTunnelConnector {
|
||||||
|
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||||
|
let mut conn = self
|
||||||
|
.get_redirected_connector(self.addr.to_string().as_str())
|
||||||
|
.await
|
||||||
|
.with_context(|| "get redirected url failed")?;
|
||||||
|
conn.set_ip_version(self.ip_version);
|
||||||
|
let t = conn.connect().await?;
|
||||||
|
let info = t.info().unwrap_or_default();
|
||||||
|
Ok(Box::new(TunnelWithInfo::new(
|
||||||
|
t,
|
||||||
|
TunnelInfo {
|
||||||
|
local_addr: info.local_addr.clone(),
|
||||||
|
remote_addr: Some(self.addr.clone().into()),
|
||||||
|
tunnel_type: format!(
|
||||||
|
"{:?}-{}",
|
||||||
|
self.redirect_type,
|
||||||
|
info.remote_addr.unwrap_or_default()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_url(&self) -> url::Url {
|
||||||
|
self.addr.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_bind_addrs(&mut self, addrs: Vec<SocketAddr>) {
|
||||||
|
self.bind_addrs = addrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_ip_version(&mut self, ip_version: IpVersion) {
|
||||||
|
self.ip_version = ip_version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tokio::{io::AsyncWriteExt as _, net::TcpListener};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::global_ctx::tests::get_mock_global_ctx,
|
||||||
|
tunnel::{tcp::TcpTunnelListener, TunnelConnector, TunnelListener},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
async fn run_http_redirect_server(port: u16, test_type: HttpRedirectType) -> Result<(), Error> {
|
||||||
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
||||||
|
let (mut stream, _) = listener.accept().await?;
|
||||||
|
|
||||||
|
match test_type {
|
||||||
|
HttpRedirectType::RedirectToQuery => {
|
||||||
|
let resp = "HTTP/1.1 301 Moved Permanently\r\nLocation: http://test.com/?url=tcp://127.0.0.1:25888\r\n\r\n";
|
||||||
|
stream.write_all(resp.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
HttpRedirectType::RedirectToUrl => {
|
||||||
|
let resp =
|
||||||
|
"HTTP/1.1 301 Moved Permanently\r\nLocation: tcp://127.0.0.1:25888\r\n\r\n";
|
||||||
|
stream.write_all(resp.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
HttpRedirectType::BodyUrls => {
|
||||||
|
let resp = "HTTP/1.1 200 OK\r\n\r\ntcp://127.0.0.1:25888";
|
||||||
|
stream.write_all(resp.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
HttpRedirectType::Unknown => {
|
||||||
|
panic!("unexpected test type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest::rstest]
|
||||||
|
#[serial_test::serial(http_redirect_test)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn http_redirect_test(
|
||||||
|
// 1. 301 redirect
|
||||||
|
// 2. 200 success with valid connector urls
|
||||||
|
#[values(
|
||||||
|
HttpRedirectType::RedirectToQuery,
|
||||||
|
HttpRedirectType::RedirectToUrl,
|
||||||
|
HttpRedirectType::BodyUrls
|
||||||
|
)]
|
||||||
|
test_type: HttpRedirectType,
|
||||||
|
) {
|
||||||
|
let http_task = tokio::spawn(run_http_redirect_server(35888, test_type));
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
let test_url: url::Url = "http://127.0.0.1:35888".parse().unwrap();
|
||||||
|
let global_ctx = get_mock_global_ctx();
|
||||||
|
let mut flags = global_ctx.config.get_flags();
|
||||||
|
flags.bind_device = false;
|
||||||
|
global_ctx.config.set_flags(flags);
|
||||||
|
let mut connector = HttpTunnelConnector::new(test_url.clone(), global_ctx.clone());
|
||||||
|
|
||||||
|
let mut listener = TcpTunnelListener::new("tcp://0.0.0.0:25888".parse().unwrap());
|
||||||
|
listener.listen().await.unwrap();
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let _conn = listener.accept().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let t = connector.connect().await.unwrap();
|
||||||
|
assert_eq!(connector.redirect_type, test_type);
|
||||||
|
let info = t.info().unwrap();
|
||||||
|
let remote_addr = info.remote_addr.unwrap();
|
||||||
|
assert_eq!(remote_addr, test_url.into());
|
||||||
|
|
||||||
|
tokio::join!(task).0.unwrap();
|
||||||
|
tokio::join!(http_task).0.unwrap().unwrap();
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,10 @@ use std::{collections::BTreeSet, sync::Arc};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use dashmap::{DashMap, DashSet};
|
use dashmap::{DashMap, DashSet};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{broadcast::Receiver, mpsc, Mutex},
|
sync::{
|
||||||
|
broadcast::{error::RecvError, Receiver},
|
||||||
|
mpsc, Mutex,
|
||||||
|
},
|
||||||
task::JoinSet,
|
task::JoinSet,
|
||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
@@ -106,7 +109,7 @@ impl ManualConnectorManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_connector_by_url(&self, url: &str) -> Result<(), Error> {
|
pub async fn add_connector_by_url(&self, url: &str) -> Result<(), Error> {
|
||||||
self.add_connector(create_connector_by_url(url, &self.global_ctx).await?);
|
self.add_connector(create_connector_by_url(url, &self.global_ctx, IpVersion::Both).await?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +182,37 @@ impl ManualConnectorManager {
|
|||||||
mut event_recv: Receiver<GlobalCtxEvent>,
|
mut event_recv: Receiver<GlobalCtxEvent>,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
let event = event_recv.recv().await.expect("event_recv got error");
|
match event_recv.recv().await {
|
||||||
Self::handle_event(&event, &data).await;
|
Ok(event) => {
|
||||||
|
Self::handle_event(&event, &data).await;
|
||||||
|
}
|
||||||
|
Err(RecvError::Lagged(n)) => {
|
||||||
|
tracing::warn!("event_recv lagged: {}, rebuild alive conn list", n);
|
||||||
|
event_recv = event_recv.resubscribe();
|
||||||
|
data.alive_conn_urls.clear();
|
||||||
|
for x in data
|
||||||
|
.peer_manager
|
||||||
|
.get_peer_map()
|
||||||
|
.get_alive_conns()
|
||||||
|
.iter()
|
||||||
|
.map(|x| {
|
||||||
|
x.tunnel
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.remote_addr
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
data.alive_conn_urls.insert(x);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(RecvError::Closed) => {
|
||||||
|
tracing::warn!("event_recv closed, exit");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +303,6 @@ impl ManualConnectorManager {
|
|||||||
|
|
||||||
async fn collect_dead_conns(data: Arc<ConnectorManagerData>) -> BTreeSet<String> {
|
async fn collect_dead_conns(data: Arc<ConnectorManagerData>) -> BTreeSet<String> {
|
||||||
Self::handle_remove_connector(data.clone());
|
Self::handle_remove_connector(data.clone());
|
||||||
|
|
||||||
let all_urls: BTreeSet<String> = data
|
let all_urls: BTreeSet<String> = data
|
||||||
.connectors
|
.connectors
|
||||||
.iter()
|
.iter()
|
||||||
@@ -293,7 +324,6 @@ impl ManualConnectorManager {
|
|||||||
ip_version: IpVersion,
|
ip_version: IpVersion,
|
||||||
) -> Result<ReconnResult, Error> {
|
) -> Result<ReconnResult, Error> {
|
||||||
let ip_collector = data.global_ctx.get_ip_collector();
|
let ip_collector = data.global_ctx.get_ip_collector();
|
||||||
let net_ns = data.net_ns.clone();
|
|
||||||
|
|
||||||
connector.lock().await.set_ip_version(ip_version);
|
connector.lock().await.set_ip_version(ip_version);
|
||||||
|
|
||||||
@@ -309,18 +339,11 @@ impl ManualConnectorManager {
|
|||||||
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
|
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
|
||||||
connector.lock().await.remote_url().clone(),
|
connector.lock().await.remote_url().clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let _g = net_ns.guard();
|
|
||||||
tracing::info!("reconnect try connect... conn: {:?}", connector);
|
tracing::info!("reconnect try connect... conn: {:?}", connector);
|
||||||
let tunnel = connector.lock().await.connect().await?;
|
let (peer_id, conn_id) = data
|
||||||
tracing::info!("reconnect get tunnel succ: {:?}", tunnel);
|
.peer_manager
|
||||||
assert_eq!(
|
.try_direct_connect(connector.lock().await.as_mut())
|
||||||
dead_url,
|
.await?;
|
||||||
tunnel.info().unwrap().remote_addr.unwrap().to_string(),
|
|
||||||
"info: {:?}",
|
|
||||||
tunnel.info()
|
|
||||||
);
|
|
||||||
let (peer_id, conn_id) = data.peer_manager.add_client_tunnel(tunnel).await?;
|
|
||||||
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
|
tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
|
||||||
Ok(ReconnResult {
|
Ok(ReconnResult {
|
||||||
dead_url,
|
dead_url,
|
||||||
@@ -339,7 +362,7 @@ impl ManualConnectorManager {
|
|||||||
let mut ip_versions = vec![];
|
let mut ip_versions = vec![];
|
||||||
let u = url::Url::parse(&dead_url)
|
let u = url::Url::parse(&dead_url)
|
||||||
.with_context(|| format!("failed to parse connector url {:?}", dead_url))?;
|
.with_context(|| format!("failed to parse connector url {:?}", dead_url))?;
|
||||||
if u.scheme() == "ring" {
|
if u.scheme() == "ring" || u.scheme() == "txt" || u.scheme() == "srv" {
|
||||||
ip_versions.push(IpVersion::Both);
|
ip_versions.push(IpVersion::Both);
|
||||||
} else {
|
} else {
|
||||||
let addrs = u.socket_addrs(|| Some(1000))?;
|
let addrs = u.socket_addrs(|| Some(1000))?;
|
||||||
@@ -365,8 +388,12 @@ impl ManualConnectorManager {
|
|||||||
"cannot get ip from url"
|
"cannot get ip from url"
|
||||||
)));
|
)));
|
||||||
for ip_version in ip_versions {
|
for ip_version in ip_versions {
|
||||||
|
let use_long_timeout = dead_url.starts_with("http")
|
||||||
|
|| dead_url.starts_with("srv")
|
||||||
|
|| dead_url.starts_with("txt");
|
||||||
let ret = timeout(
|
let ret = timeout(
|
||||||
std::time::Duration::from_secs(1),
|
// allow http connector to wait longer
|
||||||
|
std::time::Duration::from_secs(if use_long_timeout { 20 } else { 2 }),
|
||||||
Self::conn_reconnect_with_ip_version(
|
Self::conn_reconnect_with_ip_version(
|
||||||
data.clone(),
|
data.clone(),
|
||||||
dead_url.clone(),
|
dead_url.clone(),
|
||||||
|
@@ -3,6 +3,8 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use http_connector::HttpTunnelConnector;
|
||||||
|
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
use crate::tunnel::quic::QUICTunnelConnector;
|
use crate::tunnel::quic::QUICTunnelConnector;
|
||||||
#[cfg(feature = "wireguard")]
|
#[cfg(feature = "wireguard")]
|
||||||
@@ -11,7 +13,7 @@ use crate::{
|
|||||||
common::{error::Error, global_ctx::ArcGlobalCtx, network::IPCollector},
|
common::{error::Error, global_ctx::ArcGlobalCtx, network::IPCollector},
|
||||||
tunnel::{
|
tunnel::{
|
||||||
check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector,
|
check_scheme_and_get_socket_addr, ring::RingTunnelConnector, tcp::TcpTunnelConnector,
|
||||||
udp::UdpTunnelConnector, TunnelConnector,
|
udp::UdpTunnelConnector, IpVersion, TunnelConnector,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,6 +21,9 @@ pub mod direct;
|
|||||||
pub mod manual;
|
pub mod manual;
|
||||||
pub mod udp_hole_punch;
|
pub mod udp_hole_punch;
|
||||||
|
|
||||||
|
pub mod dns_connector;
|
||||||
|
pub mod http_connector;
|
||||||
|
|
||||||
async fn set_bind_addr_for_peer_connector(
|
async fn set_bind_addr_for_peer_connector(
|
||||||
connector: &mut (impl TunnelConnector + ?Sized),
|
connector: &mut (impl TunnelConnector + ?Sized),
|
||||||
is_ipv4: bool,
|
is_ipv4: bool,
|
||||||
@@ -38,8 +43,8 @@ async fn set_bind_addr_for_peer_connector(
|
|||||||
connector.set_bind_addrs(bind_addrs);
|
connector.set_bind_addrs(bind_addrs);
|
||||||
} else {
|
} else {
|
||||||
let mut bind_addrs = vec![];
|
let mut bind_addrs = vec![];
|
||||||
for ipv6 in ips.interface_ipv6s {
|
for ipv6 in ips.interface_ipv6s.iter().chain(ips.public_ipv6.iter()) {
|
||||||
let socket_addr = SocketAddrV6::new(ipv6.into(), 0, 0, 0).into();
|
let socket_addr = SocketAddrV6::new(std::net::Ipv6Addr::from(*ipv6), 0, 0, 0).into();
|
||||||
bind_addrs.push(socket_addr);
|
bind_addrs.push(socket_addr);
|
||||||
}
|
}
|
||||||
connector.set_bind_addrs(bind_addrs);
|
connector.set_bind_addrs(bind_addrs);
|
||||||
@@ -50,11 +55,13 @@ async fn set_bind_addr_for_peer_connector(
|
|||||||
pub async fn create_connector_by_url(
|
pub async fn create_connector_by_url(
|
||||||
url: &str,
|
url: &str,
|
||||||
global_ctx: &ArcGlobalCtx,
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
ip_version: IpVersion,
|
||||||
) -> Result<Box<dyn TunnelConnector + 'static>, Error> {
|
) -> Result<Box<dyn TunnelConnector + 'static>, Error> {
|
||||||
let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?;
|
let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?;
|
||||||
match url.scheme() {
|
let mut connector: Box<dyn TunnelConnector + 'static> = match url.scheme() {
|
||||||
"tcp" => {
|
"tcp" => {
|
||||||
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "tcp")?;
|
let dst_addr =
|
||||||
|
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "tcp", ip_version).await?;
|
||||||
let mut connector = TcpTunnelConnector::new(url);
|
let mut connector = TcpTunnelConnector::new(url);
|
||||||
if global_ctx.config.get_flags().bind_device {
|
if global_ctx.config.get_flags().bind_device {
|
||||||
set_bind_addr_for_peer_connector(
|
set_bind_addr_for_peer_connector(
|
||||||
@@ -64,10 +71,11 @@ pub async fn create_connector_by_url(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
"udp" => {
|
"udp" => {
|
||||||
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "udp")?;
|
let dst_addr =
|
||||||
|
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "udp", ip_version).await?;
|
||||||
let mut connector = UdpTunnelConnector::new(url);
|
let mut connector = UdpTunnelConnector::new(url);
|
||||||
if global_ctx.config.get_flags().bind_device {
|
if global_ctx.config.get_flags().bind_device {
|
||||||
set_bind_addr_for_peer_connector(
|
set_bind_addr_for_peer_connector(
|
||||||
@@ -77,16 +85,21 @@ pub async fn create_connector_by_url(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
|
}
|
||||||
|
"http" | "https" => {
|
||||||
|
let connector = HttpTunnelConnector::new(url, global_ctx.clone());
|
||||||
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
"ring" => {
|
"ring" => {
|
||||||
check_scheme_and_get_socket_addr::<uuid::Uuid>(&url, "ring")?;
|
check_scheme_and_get_socket_addr::<uuid::Uuid>(&url, "ring", IpVersion::Both).await?;
|
||||||
let connector = RingTunnelConnector::new(url);
|
let connector = RingTunnelConnector::new(url);
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
#[cfg(feature = "quic")]
|
#[cfg(feature = "quic")]
|
||||||
"quic" => {
|
"quic" => {
|
||||||
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "quic")?;
|
let dst_addr =
|
||||||
|
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "quic", ip_version).await?;
|
||||||
let mut connector = QUICTunnelConnector::new(url);
|
let mut connector = QUICTunnelConnector::new(url);
|
||||||
if global_ctx.config.get_flags().bind_device {
|
if global_ctx.config.get_flags().bind_device {
|
||||||
set_bind_addr_for_peer_connector(
|
set_bind_addr_for_peer_connector(
|
||||||
@@ -96,11 +109,12 @@ pub async fn create_connector_by_url(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
#[cfg(feature = "wireguard")]
|
#[cfg(feature = "wireguard")]
|
||||||
"wg" => {
|
"wg" => {
|
||||||
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "wg")?;
|
let dst_addr =
|
||||||
|
check_scheme_and_get_socket_addr::<SocketAddr>(&url, "wg", ip_version).await?;
|
||||||
let nid = global_ctx.get_network_identity();
|
let nid = global_ctx.get_network_identity();
|
||||||
let wg_config = WgConfig::new_from_network_identity(
|
let wg_config = WgConfig::new_from_network_identity(
|
||||||
&nid.network_name,
|
&nid.network_name,
|
||||||
@@ -115,12 +129,12 @@ pub async fn create_connector_by_url(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
#[cfg(feature = "websocket")]
|
#[cfg(feature = "websocket")]
|
||||||
"ws" | "wss" => {
|
"ws" | "wss" => {
|
||||||
use crate::tunnel::{FromUrl, IpVersion};
|
use crate::tunnel::FromUrl;
|
||||||
let dst_addr = SocketAddr::from_url(url.clone(), IpVersion::Both)?;
|
let dst_addr = SocketAddr::from_url(url.clone(), ip_version).await?;
|
||||||
let mut connector = crate::tunnel::websocket::WSTunnelConnector::new(url);
|
let mut connector = crate::tunnel::websocket::WSTunnelConnector::new(url);
|
||||||
if global_ctx.config.get_flags().bind_device {
|
if global_ctx.config.get_flags().bind_device {
|
||||||
set_bind_addr_for_peer_connector(
|
set_bind_addr_for_peer_connector(
|
||||||
@@ -130,10 +144,17 @@ pub async fn create_connector_by_url(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
return Ok(Box::new(connector));
|
Box::new(connector)
|
||||||
|
}
|
||||||
|
"txt" | "srv" => {
|
||||||
|
let connector = dns_connector::DNSTunnelConnector::new(url, global_ctx.clone());
|
||||||
|
Box::new(connector)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::InvalidUrl(url.into()));
|
return Err(Error::InvalidUrl(url.into()));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
connector.set_ip_version(ip_version);
|
||||||
|
|
||||||
|
Ok(connector)
|
||||||
}
|
}
|
||||||
|