mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-11-02 12:54:03 +08:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dca25db86 | ||
|
|
d87a440c04 | ||
|
|
55efd62798 | ||
|
|
70a41275c1 | ||
|
|
dd941681ce | ||
|
|
9824d0adaa | ||
|
|
d2291628e0 | ||
|
|
7ab8cad1af | ||
|
|
2c017e0fc5 | ||
|
|
d9453589ac | ||
|
|
e344372616 | ||
|
|
63821e56bc | ||
|
|
1be64223c8 | ||
|
|
a08a8e7f4c | ||
|
|
b31996230d | ||
|
|
1e836501a8 | ||
|
|
d4e59ffc40 | ||
|
|
37ceb77bf6 | ||
|
|
ba3da97ad4 | ||
|
|
984ed8f6cf | ||
|
|
c7895963e4 | ||
|
|
a0ece6ad4d | ||
|
|
d0a3a40a0f | ||
|
|
ff5ee8a05e | ||
|
|
a50bcf3087 | ||
|
|
e0b364d3e2 | ||
|
|
2496cf51c3 | ||
|
|
7b4a01e7fb | ||
|
|
3f9a1d8f2e | ||
|
|
0b927bcc91 | ||
|
|
92397bf7b6 | ||
|
|
d1e2e1db2b | ||
|
|
783ba50c9e | ||
|
|
aca9a0e35b | ||
|
|
fb8d262554 | ||
|
|
bd60cfc2a0 | ||
|
|
06afd221d5 | ||
|
|
0171fb35a4 | ||
|
|
99c47813c3 | ||
|
|
82f5dfd569 | ||
|
|
6d7edcd486 | ||
|
|
9f273dc887 | ||
|
|
ac9cfa5040 | ||
|
|
1b03223537 | ||
|
|
0467b0a3dc | ||
|
|
ba75167238 | ||
|
|
51e7daa26f | ||
|
|
2ff653cc6f | ||
|
|
cfe4d080d5 | ||
|
|
9b28ecde8e | ||
|
|
096ed39d23 | ||
|
|
6ea3adcef8 | ||
|
|
4342be29d7 | ||
|
|
1609c97574 | ||
|
|
f07b3ee9c6 | ||
|
|
2058dbc470 | ||
|
|
6964fb71fc | ||
|
|
a8bb4ee7e5 | ||
|
|
3fcd74ce4e | ||
|
|
2b7ff0efc5 | ||
|
|
5833541a6e | ||
|
|
54c6418f97 | ||
|
|
fc9aac42b4 | ||
|
|
89b43684d8 | ||
|
|
31b26222d3 | ||
|
|
e4df03053e | ||
|
|
833e7eca22 | ||
|
|
b7d85ad2ff | ||
|
|
8793560e12 | ||
|
|
58e0e48d59 | ||
|
|
ad4cbbea6d | ||
|
|
db660ee3b1 | ||
|
|
ae54a872ce | ||
|
|
2aa686f7ad | ||
|
|
ce10bf5e60 | ||
|
|
28ae9c447a | ||
|
|
ff6da9bbec | ||
|
|
198c239399 | ||
|
|
0fbbea963f | ||
|
|
51165c54f5 | ||
|
|
f14875aa3f | ||
|
|
6391dceb62 | ||
|
|
29806b899a | ||
|
|
d63a3c01e4 | ||
|
|
b6fb7ac962 | ||
|
|
1d22fdc972 | ||
|
|
d135dd5a6f | ||
|
|
7cae63cb17 | ||
|
|
232165eff3 | ||
|
|
2bc4dd8c53 |
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 🐞 问题报告 / Bug Report
|
||||
title: '[bug] '
|
||||
description: 报告一个问题 / Report a bug
|
||||
labels: ['type: bug', 'status: needs triage']
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 在提交问题之前 / First of all
|
||||
1. 请先搜索有关此问题的 [现有问题](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue)。
|
||||
1. Please search for [existing issues](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue) about this problem first.
|
||||
2. 请确保所使用的 Easytier 版本都是最新的。
|
||||
2. Make sure that all Easytier versions are up-to-date.
|
||||
3. 请确保这是 EasyTier 的问题,而不是你正在使用的其他内容引起的问题。
|
||||
3. Make sure it's an issue with EasyTier and not something else you are using.
|
||||
4. 请记得遵守我们的社区准则并保持友好态度。
|
||||
4. Remember to follow our community guidelines and be friendly.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 描述问题 / Describe the bug
|
||||
description: 对 bug 的明确描述。如果条件允许,请包括屏幕截图。 / A clear description of what the bug is. Include screenshots if applicable.
|
||||
placeholder: 问题描述 / Bug description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤 / Reproduction
|
||||
description: 能够重现行为的步骤或指向能够复现的存储库链接。 / A link to a reproduction repo or steps to reproduce the behaviour.
|
||||
placeholder: |
|
||||
请提供一个最小化的复现示例或复现步骤,请参考这个指南 https://stackoverflow.com/help/minimal-reproducible-example
|
||||
Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example
|
||||
为什么需要重现(问题)?请参阅这篇文章 https://antfu.me/posts/why-reproductions-are-required
|
||||
Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 预期结果 / Expected behavior
|
||||
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 额外上下文 / Additional context
|
||||
description: 在这里添加关于问题的任何其他上下文。 / Add any other context about the problem here.
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright 2024-present Easytier Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: 💡 新功能请求 / Feature Request
|
||||
title: '[feat] '
|
||||
description: 提出一个想法 / Suggest an idea
|
||||
labels: ['type: feature request']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 描述问题 / Describe the problem
|
||||
description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
|
||||
placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "描述您想要的解决方案 / Describe the solution you'd like"
|
||||
description: 明确说明您希望做出的改变 / A clear description of what change you would like
|
||||
placeholder: '我希望... / I would like to...'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 替代方案 / Alternatives considered
|
||||
description: "您考虑过的任何替代解决方案 / Any alternative solutions you've considered"
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 额外上下文 / Additional context
|
||||
description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
|
||||
4
.github/workflows/Dockerfile
vendored
4
.github/workflows/Dockerfile
vendored
@@ -18,9 +18,13 @@ RUN mkdir -p /tmp/output; \
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
||||
|
||||
# users can use "-e TZ=xxx" to adjust it
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
# tcp
|
||||
EXPOSE 11010/tcp
|
||||
# udp
|
||||
|
||||
65
.github/workflows/core.yml
vendored
65
.github/workflows/core.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier Core
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
# do not skip push on branch starts with releases/
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||
build:
|
||||
strategy:
|
||||
@@ -70,6 +72,11 @@ jobs:
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
NAME: easytier
|
||||
@@ -81,9 +88,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
@@ -93,9 +100,6 @@ jobs:
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
@@ -103,13 +107,52 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Core & Cli
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
|
||||
else
|
||||
cargo build --release --verbose --target $TARGET
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: cross-platform-actions/action@v0.23.0
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
with:
|
||||
operating_system: freebsd
|
||||
environment_variables: TARGET
|
||||
architecture: x86-64
|
||||
version: ${{ matrix.BSD_VERSION }}
|
||||
shell: bash
|
||||
memory: 5G
|
||||
cpu_count: 4
|
||||
run: |
|
||||
uname -a
|
||||
echo $SHELL
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
env | sort
|
||||
|
||||
sudo pkg install -y git protobuf
|
||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.77
|
||||
rustup default 1.77
|
||||
|
||||
export CC=clang
|
||||
export CXX=clang++
|
||||
export CARGO_TERM_COLOR=always
|
||||
|
||||
cargo build --release --verbose --target $TARGET
|
||||
|
||||
- name: Install UPX
|
||||
if: ${{ matrix.OS != 'macos-latest' }}
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
@@ -132,7 +175,7 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
fi
|
||||
@@ -159,7 +202,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
core-result:
|
||||
|
||||
57
.github/workflows/gui.yml
vendored
57
.github/workflows/gui.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier GUI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
@@ -69,6 +70,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
@@ -118,33 +123,31 @@ jobs:
|
||||
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=amd64] http://archive.ubuntu.com/ubuntu/ noble main restricted" | sudo tee /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-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
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security universe" | sudo tee -a /etc/apt/sources.list
|
||||
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security multiverse" | sudo tee -a /etc/apt/sources.list
|
||||
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo apt-get update && sudo apt-get upgrade -y
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
sudo apt install libwebkit2gtk-4.1-dev:arm64
|
||||
sudo apt install libssl-dev: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"
|
||||
|
||||
@@ -197,7 +200,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/gui
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
gui-result:
|
||||
|
||||
7
.github/workflows/install_rust.sh
vendored
7
.github/workflows/install_rust.sh
vendored
@@ -72,6 +72,13 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
|
||||
rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
||||
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
||||
|
||||
# https://github.com/rust-lang/rust/issues/128808
|
||||
# remove it after Cargo or rustc fix this.
|
||||
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
|
||||
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
|
||||
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
|
||||
fi
|
||||
else
|
||||
rustup target add $TARGET
|
||||
if [[ $GUI_TARGET != '' ]]; then
|
||||
|
||||
13
.github/workflows/mobile.yml
vendored
13
.github/workflows/mobile.yml
vendored
@@ -2,7 +2,7 @@ name: EasyTier Mobile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
|
||||
@@ -20,14 +20,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
@@ -48,6 +49,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set current ref as env variable
|
||||
run: |
|
||||
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
@@ -150,7 +155,7 @@ jobs:
|
||||
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
|
||||
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
|
||||
local-path: ./artifacts/
|
||||
remote-path: /easytier-releases/${{ github.sha }}/mobile
|
||||
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
|
||||
no-delete-remote-files: true
|
||||
retry: 5
|
||||
mobile-result:
|
||||
|
||||
92
.github/workflows/release.yml
vendored
Normal file
92
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: EasyTier Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
core_run_id:
|
||||
description: 'The run id of EasyTier-Core Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498549
|
||||
required: true
|
||||
gui_run_id:
|
||||
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498557
|
||||
required: true
|
||||
mobile_run_id:
|
||||
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498555
|
||||
required: true
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.0.3'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
type: boolean
|
||||
default: true
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: contains('["KKRainbow"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.core_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Zip release assets
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
mkdir zipped_assets
|
||||
|
||||
find release_assets_nozip -type f -exec mv {} zipped_assets \;
|
||||
ls -l -R ./zipped_assets
|
||||
|
||||
cd release_assets
|
||||
ls -l -R ./
|
||||
chmod -R 755 .
|
||||
for x in `ls`; do
|
||||
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
|
||||
done
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ inputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
./zipped_assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
1223
Cargo.lock
generated
1223
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,4 +10,3 @@ panic = "unwind"
|
||||
panic = "abort"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
@@ -4,84 +4,23 @@
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"name": "gui",
|
||||
"path": "easytier-gui"
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"path": "easytier"
|
||||
},
|
||||
{
|
||||
"name": "vpnservice",
|
||||
"path": "tauri-plugin-vpnservice"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"i18n-ally.sourceLanguage": "cn",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sortKeys": true,
|
||||
// Disable the default formatter
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "style/eol-last",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
],
|
||||
"eslint.validate": [
|
||||
"code-workspace",
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"gql",
|
||||
"graphql"
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"easytier-gui/locales"
|
||||
]
|
||||
}
|
||||
}
|
||||
388
README.md
388
README.md
@@ -1,28 +1,28 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
[简体中文](/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://www.easytier.top/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.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-5.png" width="300">
|
||||
<img src="assets/image-4.png" width="300">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
|
||||
- **Safe**: Use WireGuard protocol to encrypt data.
|
||||
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows, will support IOS and Android in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows/Android, will support IOS in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
|
||||
- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
|
||||
- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
|
||||
@@ -32,159 +32,195 @@
|
||||
- **IPv6 Support**: Supports networking using IPv6.
|
||||
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **Install from source code**
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
3. **Install from source code**
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **Install by Docker Compose**
|
||||
|
||||
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
|
||||
```sh
|
||||
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
|
||||
|
||||
6. **Install by Homebrew (For MacOS Only)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
Successful execution of the command will print the following.
|
||||
|
||||
|
||||

|
||||
|
||||
2. Execute on Node B
|
||||
|
||||
2. Execute on Node B
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
The two nodes should connect successfully and be able to communicate within the virtual subnet
|
||||
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
|
||||
Use easytier-cli to view node information in the subnet
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### Subnet Proxy (Point-to-Network) Configuration
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
Then the startup parameters for Node B's easytier are (new -n parameter)
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### Subnet Proxy (Point-to-Network) Configuration
|
||||
|
||||
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
|
||||
nodea <--> nodeb <-.-> id1
|
||||
|
||||
```
|
||||
|
||||
Then the startup parameters for Node B's easytier are (new -n parameter)
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
```
|
||||
|
||||
Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
2. Test whether Node A can access nodes under the proxied subnet
|
||||
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://easytier.public.kkrainbow.top:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
Node B executes
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
|
||||
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.top:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
Node B executes
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
|
||||
EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
|
||||
|
||||
Assuming the network topology is as follows:
|
||||
|
||||
@@ -210,14 +246,14 @@ To enable an iPhone to access the EasyTier network through Node A, the following
|
||||
|
||||
Include the --vpn-portal parameter in the easytier-core command on Node A to specify the port that the WireGuard service listens on and the subnet used by the WireGuard network.
|
||||
|
||||
```
|
||||
```sh
|
||||
# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
|
||||
|
||||
```
|
||||
```sh
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
@@ -241,45 +277,53 @@ connected_clients:
|
||||
|
||||
Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
|
||||
|
||||
# Self-Hosted Public Server
|
||||
### Self-Hosted Public Server
|
||||
|
||||
Each node can act as a relay node for other users' networks. Simply start EasyTier without any parameters.
|
||||
|
||||
### Configurations
|
||||
|
||||
You can use ``easytier-core --help`` to view all configuration items
|
||||
|
||||
|
||||
# Roadmap
|
||||
|
||||
- [ ] Improve documentation and user guides.
|
||||
- [ ] Support features such as encryption, TCP hole punching, etc.
|
||||
- [ ] Support Android, IOS and other mobile platforms.
|
||||
- [ ] Support Web configuration management.
|
||||
|
||||
# Community and Contribution
|
||||
|
||||
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
|
||||
|
||||
# Related Projects and Resources
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
# License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
# Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
Every virtual network (with same network name and secret) can act as a public server cluster. Nodes of other network can connect to arbitrary nodes in public server cluster to discover each other without public IP.
|
||||
|
||||
# Sponsor
|
||||
Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
|
||||
|
||||
You can also join the official public server cluster with following command:
|
||||
|
||||
```
|
||||
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
|
||||
### Configurations
|
||||
|
||||
You can use ``easytier-core --help`` to view all configuration items
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Improve documentation and user guides.
|
||||
- [ ] Support features such as encryption, TCP hole punching, etc.
|
||||
- [ ] Support iOS.
|
||||
- [ ] Support Web configuration management.
|
||||
|
||||
## Community and Contribution
|
||||
|
||||
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Related Projects and Resources
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
|
||||
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
|
||||
|
||||
## License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
## Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
|
||||
## Sponsor
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
|
||||
144
README_CN.md
144
README_CN.md
@@ -22,7 +22,7 @@
|
||||
- **去中心化**:无需依赖中心化服务,节点平等且独立。
|
||||
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
|
||||
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
|
||||
- **跨平台**:支持 MacOS/Linux/Windows,未来将支持 IOS 和 Android。可执行文件静态链接,部署简单。
|
||||
- **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接,部署简单。
|
||||
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
|
||||
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
|
||||
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
|
||||
@@ -39,15 +39,35 @@
|
||||
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
|
||||
|
||||
2. **通过 crates.io 安装**
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **通过源码安装**
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **通过Docker Compose安装**
|
||||
|
||||
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。
|
||||
|
||||
5. **使用一键脚本安装 (仅适用于 Linux)**
|
||||
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
|
||||
|
||||
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -75,34 +95,48 @@ nodea <-----> nodeb
|
||||
```
|
||||
|
||||
1. 在节点 A 上执行:
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
|
||||
2. 在节点 B 执行
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. 测试联通性
|
||||
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -110,11 +144,11 @@ nodea <-----> nodeb
|
||||
|
||||
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
|
||||
|
||||
```
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
其中 `--peers ` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
|
||||
---
|
||||
|
||||
@@ -149,35 +183,36 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
|
||||
|
||||
1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||

|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. 测试节点 A 是否可访问被代理子网下的节点
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 无公网IP组网
|
||||
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://easytier.public.kkrainbow.top:11010``。
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.top:11010``。
|
||||
|
||||
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
|
||||
|
||||
以双节点为例,节点 A 执行:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
节点 B 执行
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
|
||||
@@ -212,14 +247,14 @@ ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
|
||||
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
|
||||
|
||||
```
|
||||
```sh
|
||||
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
|
||||
|
||||
```
|
||||
```sh
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
@@ -247,43 +282,50 @@ connected_clients:
|
||||
|
||||
### 自建公共中转服务器
|
||||
|
||||
每个节点都可作为其他用户网络的中转节点。不带任何参数直接启动 EasyTier 即可。
|
||||
每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此。
|
||||
|
||||
运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
|
||||
|
||||
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
|
||||
|
||||
```
|
||||
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010
|
||||
```
|
||||
|
||||
### 其他配置
|
||||
|
||||
可使用 ``easytier-core --help`` 查看全部配置项
|
||||
|
||||
|
||||
# 路线图
|
||||
## 路线图
|
||||
|
||||
- [ ] 完善文档和用户指南。
|
||||
- [ ] 支持 TCP 打洞等特性。
|
||||
- [ ] 支持 Android、IOS 等移动平台。
|
||||
- [ ] 支持 iOS。
|
||||
- [ ] 支持 Web 配置管理。
|
||||
|
||||
# 社区和贡献
|
||||
## 社区和贡献
|
||||
|
||||
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
|
||||
|
||||
# 相关项目和资源
|
||||
## 相关项目和资源
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
|
||||
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
|
||||
|
||||
# 许可证
|
||||
## 许可证
|
||||
|
||||
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
|
||||
|
||||
# 联系方式
|
||||
## 联系方式
|
||||
|
||||
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- QQ 群: 949700262
|
||||
- Telegram:https://t.me/easytier
|
||||
|
||||
# 赞助
|
||||
## 赞助
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
|
||||
BIN
assets/image-10.png
Normal file
BIN
assets/image-10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
82
easytier-gui/.vscode/settings.json
vendored
82
easytier-gui/.vscode/settings.json
vendored
@@ -1,5 +1,81 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
"cSpell.words": [
|
||||
"easytier",
|
||||
"Vite",
|
||||
"vueuse",
|
||||
"pinia",
|
||||
"demi",
|
||||
"antfu",
|
||||
"iconify",
|
||||
"intlify",
|
||||
"vitejs",
|
||||
"unplugin",
|
||||
"pnpm"
|
||||
],
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"editor.formatOnSave": false,
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{
|
||||
"rule": "style/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "format/*",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-indent",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spacing",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-spaces",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-order",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-dangle",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*-newline",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*quotes",
|
||||
"severity": "off"
|
||||
},
|
||||
{
|
||||
"rule": "*semi",
|
||||
"severity": "off"
|
||||
}
|
||||
],
|
||||
// The following is optional.
|
||||
// It's better to put under project setting `.vscode/settings.json`
|
||||
// to avoid conflicts with working with different eslint configs
|
||||
// that does not support all formats.
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ npm install -g pnpm
|
||||
### For Desktop (Win/Mac/Linux)
|
||||
|
||||
```
|
||||
cd ../tauri-plugin-vpnservice
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
cd ../easytier-gui
|
||||
pnpm install
|
||||
pnpm tauri build
|
||||
```
|
||||
@@ -34,7 +39,6 @@ rustup target add aarch64-linux-android
|
||||
install java 20
|
||||
```
|
||||
|
||||
|
||||
Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts)
|
||||
|
||||
See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail .
|
||||
@@ -43,4 +47,4 @@ See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/comp
|
||||
pnpm install
|
||||
pnpm tauri android init
|
||||
pnpm tauri android build
|
||||
```
|
||||
```
|
||||
|
||||
@@ -13,6 +13,7 @@ proxy_cidrs: 子网代理CIDR
|
||||
enable_vpn_portal: 启用VPN门户
|
||||
vpn_portal_listen_port: 监听端口
|
||||
vpn_portal_client_network: 客户端子网
|
||||
dev_name: TUN接口名称
|
||||
advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
@@ -45,11 +46,13 @@ enable_auto_launch: 开启开机自启
|
||||
exit: 退出
|
||||
chips_placeholder: 例如: {0}, 按回车添加
|
||||
hostname_placeholder: '留空默认为主机名: {0}'
|
||||
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||
off_text: 点击关闭
|
||||
on_text: 点击开启
|
||||
show_config: 显示配置
|
||||
close: 关闭
|
||||
|
||||
use_latency_first: 延迟优先模式
|
||||
my_node_info: 当前节点信息
|
||||
peer_count: 已连接
|
||||
upload: 上传
|
||||
@@ -66,6 +69,12 @@ upload_bytes: 上传
|
||||
download_bytes: 下载
|
||||
loss_rate: 丢包率
|
||||
|
||||
status:
|
||||
version: 内核版本
|
||||
local: 本机
|
||||
server: 服务器
|
||||
relay: 中继
|
||||
|
||||
run_network: 运行网络
|
||||
stop_network: 停止网络
|
||||
network_running: 运行中
|
||||
@@ -75,3 +84,32 @@ dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中
|
||||
tray:
|
||||
show: 显示 / 隐藏
|
||||
exit: 退出
|
||||
|
||||
about:
|
||||
title: 关于
|
||||
version: 版本
|
||||
author: 作者
|
||||
homepage: 主页
|
||||
license: 许可证
|
||||
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||
check_update: 检查更新
|
||||
|
||||
event:
|
||||
Unknown: 未知
|
||||
TunDeviceReady: Tun设备就绪
|
||||
TunDeviceError: Tun设备错误
|
||||
PeerAdded: 对端添加
|
||||
PeerRemoved: 对端移除
|
||||
PeerConnAdded: 对端连接添加
|
||||
PeerConnRemoved: 对端连接移除
|
||||
ListenerAdded: 监听器添加
|
||||
ListenerAddFailed: 监听器添加失败
|
||||
ListenerAcceptFailed: 监听器接受连接失败
|
||||
ConnectionAccepted: 连接已接受
|
||||
ConnectionError: 连接错误
|
||||
Connecting: 正在连接
|
||||
ConnectError: 连接错误
|
||||
VpnPortalClientConnected: VPN门户客户端已连接
|
||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
|
||||
@@ -13,6 +13,7 @@ proxy_cidrs: Subnet Proxy CIDRs
|
||||
enable_vpn_portal: Enable VPN Portal
|
||||
vpn_portal_listen_port: VPN Portal Listen Port
|
||||
vpn_portal_client_network: Client Sub Network
|
||||
dev_name: TUN interface name
|
||||
advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
@@ -43,9 +44,10 @@ logging_copy_dir: Copy Log Path
|
||||
disable_auto_launch: Disable Launch on Reboot
|
||||
enable_auto_launch: Enable Launch on Reboot
|
||||
exit: Exit
|
||||
|
||||
use_latency_first: Latency First Mode
|
||||
chips_placeholder: 'e.g: {0}, press Enter to add'
|
||||
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||
off_text: Press to disable
|
||||
on_text: Press to enable
|
||||
show_config: Show Config
|
||||
@@ -66,6 +68,12 @@ upload_bytes: Upload
|
||||
download_bytes: Download
|
||||
loss_rate: Loss Rate
|
||||
|
||||
status:
|
||||
version: Version
|
||||
local: Local
|
||||
server: Server
|
||||
relay: Relay
|
||||
|
||||
run_network: Run Network
|
||||
stop_network: Stop Network
|
||||
network_running: running
|
||||
@@ -75,3 +83,32 @@ dhcp_experimental_warning: Experimental warning! if there is an IP conflict in t
|
||||
tray:
|
||||
show: Show / Hide
|
||||
exit: Exit
|
||||
|
||||
about:
|
||||
title: About
|
||||
version: Version
|
||||
author: Author
|
||||
homepage: Homepage
|
||||
license: License
|
||||
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
|
||||
check_update: Check Update
|
||||
|
||||
event:
|
||||
Unknown: Unknown
|
||||
TunDeviceReady: TunDeviceReady
|
||||
TunDeviceError: TunDeviceError
|
||||
PeerAdded: PeerAdded
|
||||
PeerRemoved: PeerRemoved
|
||||
PeerConnAdded: PeerConnAdded
|
||||
PeerConnRemoved: PeerConnRemoved
|
||||
ListenerAdded: ListenerAdded
|
||||
ListenerAddFailed: ListenerAddFailed
|
||||
ListenerAcceptFailed: ListenerAcceptFailed
|
||||
ConnectionAccepted: ConnectionAccepted
|
||||
ConnectionError: ConnectionError
|
||||
Connecting: Connecting
|
||||
ConnectError: ConnectError
|
||||
VpnPortalClientConnected: VpnPortalClientConnected
|
||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "1.2.1",
|
||||
"version": "2.0.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,47 +12,52 @@
|
||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.0.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
|
||||
"aura": "link:@primevue/themes/aura",
|
||||
"pinia": "^2.2.1",
|
||||
"@primevue/themes": "^4.1.0",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"aura": "link:@primevue\\themes\\aura",
|
||||
"ip-num": "1.5.1",
|
||||
"pinia": "^2.2.4",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.4",
|
||||
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
|
||||
"vue": "^3.4.36",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3"
|
||||
"primevue": "^4.1.0",
|
||||
"tauri-plugin-vpnservice-api": "link:..\\tauri-plugin-vpnservice",
|
||||
"vue": "^3.5.11",
|
||||
"vue-i18n": "^10.0.4",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.24.1",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@primevue/auto-import-resolver": "^4.0.4",
|
||||
"@antfu/eslint-config": "^3.7.3",
|
||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||
"@primevue/auto-import-resolver": "^4.1.0",
|
||||
"@tauri-apps/api": "2.0.0-rc.0",
|
||||
"@tauri-apps/cli": "2.0.0-rc.1",
|
||||
"@types/node": "^20.14.14",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vue-macros/volar": "^0.19.1",
|
||||
"@tauri-apps/cli": "2.0.0-rc.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue-macros/volar": "0.30.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"unplugin-vue-macros": "^2.11.4",
|
||||
"internal-ip": "^8.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"unplugin-vue-macros": "^2.12.3",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"unplugin-vue-router": "^0.8.8",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.3.5",
|
||||
"vite-plugin-vue-devtools": "^7.3.7",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
}
|
||||
"vue-i18n": "^10.0.0",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4"
|
||||
}
|
||||
|
||||
4460
easytier-gui/pnpm-lock.yaml
generated
4460
easytier-gui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
[target]
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "1.2.1"
|
||||
version = "2.0.3"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -35,7 +35,6 @@ dashmap = "6.0"
|
||||
privilege = "0.3"
|
||||
gethostname = "0.5"
|
||||
|
||||
auto-launch = "0.5.0"
|
||||
dunce = "1.0.4"
|
||||
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
@@ -44,8 +43,12 @@ tauri-plugin-clipboard-manager = "2.0.0-rc"
|
||||
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
|
||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-autostart = "2.0.0-rc"
|
||||
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.0.0-rc.0"
|
||||
|
||||
@@ -1,34 +1,3 @@
|
||||
fn main() {
|
||||
if !cfg!(debug_assertions) && cfg!(target_os = "windows") {
|
||||
let mut windows = tauri_build::WindowsAttributes::new();
|
||||
windows = windows.app_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
|
||||
.expect("failed to run build script");
|
||||
} else {
|
||||
tauri_build::build();
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "migrated",
|
||||
"description": "permissions that were migrated from v1",
|
||||
"local": true,
|
||||
@@ -13,6 +14,7 @@
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-title",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
@@ -24,7 +26,6 @@
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"clipboard-manager:default",
|
||||
"core:tray:default",
|
||||
"core:tray:allow-new",
|
||||
"core:tray:allow-set-menu",
|
||||
"core:tray:allow-set-title",
|
||||
@@ -44,6 +45,10 @@
|
||||
"os:allow-arch",
|
||||
"os:allow-hostname",
|
||||
"os:allow-platform",
|
||||
"os:allow-locale"
|
||||
"os:allow-locale",
|
||||
"autostart:default",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-is-enabled"
|
||||
]
|
||||
}
|
||||
@@ -4,12 +4,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use auto_launch::AutoLaunchBuilder;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{
|
||||
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
ConfigLoader, FileLoggerConfig, Flags, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
VpnPortalConfig,
|
||||
},
|
||||
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
|
||||
@@ -19,6 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use tauri::Manager as _;
|
||||
|
||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
enum NetworkingMethod {
|
||||
@@ -42,6 +41,7 @@ struct NetworkConfig {
|
||||
|
||||
dhcp: bool,
|
||||
virtual_ipv4: String,
|
||||
network_length: i32,
|
||||
hostname: Option<String>,
|
||||
network_name: String,
|
||||
network_secret: String,
|
||||
@@ -61,6 +61,9 @@ struct NetworkConfig {
|
||||
|
||||
listener_urls: Vec<String>,
|
||||
rpc_port: i32,
|
||||
latency_first: bool,
|
||||
|
||||
dev_name: String,
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
@@ -81,9 +84,15 @@ impl NetworkConfig {
|
||||
|
||||
if !self.dhcp {
|
||||
if self.virtual_ipv4.len() > 0 {
|
||||
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
|
||||
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
|
||||
})?))
|
||||
let ip = format!("{}/{}", self.virtual_ipv4, self.network_length)
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse ipv4 inet address: {}, {}",
|
||||
self.virtual_ipv4, self.network_length
|
||||
)
|
||||
})?;
|
||||
cfg.set_ipv4(Some(ip));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +146,7 @@ impl NetworkConfig {
|
||||
}
|
||||
|
||||
cfg.set_rpc_portal(
|
||||
format!("127.0.0.1:{}", self.rpc_port)
|
||||
format!("0.0.0.0:{}", self.rpc_port)
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
|
||||
);
|
||||
@@ -161,7 +170,10 @@ impl NetworkConfig {
|
||||
})?,
|
||||
});
|
||||
}
|
||||
|
||||
let mut flags = Flags::default();
|
||||
flags.latency_first = self.latency_first;
|
||||
flags.dev_name = self.dev_name.clone();
|
||||
cfg.set_flags(flags);
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
@@ -172,6 +184,18 @@ static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||
once_cell::sync::Lazy::new(Default::default);
|
||||
|
||||
#[tauri::command]
|
||||
fn easytier_version() -> Result<String, String> {
|
||||
Ok(easytier::VERSION.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_autostart() -> Result<bool, String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
println!("{:?}", args);
|
||||
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
|
||||
}
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
@@ -224,11 +248,6 @@ fn get_os_hostname() -> Result<String, String> {
|
||||
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
|
||||
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_logging_level(level: String) -> Result<(), String> {
|
||||
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
||||
@@ -262,82 +281,19 @@ fn check_sudo() -> bool {
|
||||
use std::env::current_exe;
|
||||
let is_elevated = privilege::user::privileged();
|
||||
if !is_elevated {
|
||||
let Ok(my_exe) = current_exe() else {
|
||||
let Ok(exe) = current_exe() else {
|
||||
return true;
|
||||
};
|
||||
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
||||
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut elevated_cmd = privilege::runas::Command::new(exe);
|
||||
if args.contains(&AUTOSTART_ARG.to_owned()) {
|
||||
elevated_cmd.arg(AUTOSTART_ARG);
|
||||
}
|
||||
let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
|
||||
}
|
||||
is_elevated
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, _enable: bool) -> Result<bool, anyhow::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
|
||||
use std::env::current_exe;
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_name = app_exe
|
||||
.file_stem()
|
||||
.and_then(|f| f.to_str())
|
||||
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
|
||||
|
||||
let app_path = app_exe
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!("failed to get app_path"))?
|
||||
.to_string();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
|
||||
// use the /Applications/easytier-gui.app
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
let path = path.parent()?.parent()?.parent()?;
|
||||
let extension = path.extension()?.to_str()?;
|
||||
match extension == "app" {
|
||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||
false => None,
|
||||
}
|
||||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
let appimage = _app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
.build()
|
||||
.with_context(|| "failed to build auto launch")?;
|
||||
|
||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||
// 避免重复设置登录项
|
||||
let _ = auto.disable();
|
||||
auto.enable()
|
||||
.with_context(|| "failed to enable auto launch")?
|
||||
} else if !enable {
|
||||
let _ = auto.disable();
|
||||
}
|
||||
|
||||
let enabled = auto.is_enabled()?;
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -345,12 +301,41 @@ pub fn run() {
|
||||
use std::process;
|
||||
process::exit(0);
|
||||
}
|
||||
tauri::Builder::default()
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
utils::setup_panic_handler();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
builder = builder.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![AUTOSTART_ARG]),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
app.webview_windows()
|
||||
.values()
|
||||
.next()
|
||||
.expect("Sorry, no window found")
|
||||
.set_focus()
|
||||
.expect("Can't Bring Window to Focus");
|
||||
}));
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_vpnservice::init())
|
||||
.plugin(tauri_plugin_vpnservice::init());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
// for logging config
|
||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||
@@ -382,7 +367,7 @@ pub fn run() {
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
})
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../icons/icon.png"
|
||||
))?)
|
||||
.icon_as_template(false)
|
||||
@@ -396,9 +381,10 @@ pub fn run() {
|
||||
retain_network_instance,
|
||||
collect_network_infos,
|
||||
get_os_hostname,
|
||||
set_auto_launch_status,
|
||||
set_logging_level,
|
||||
set_tun_fd
|
||||
set_tun_fd,
|
||||
is_autostart,
|
||||
easytier_version
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "1.2.1",
|
||||
"version": "2.0.3",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import pkg from '~/../package.json'
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await getCurrentWindow().setTitle(`Easytier GUI: v${pkg.version}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
40
easytier-gui/src/auto-imports.d.ts
vendored
40
easytier-gui/src/auto-imports.d.ts
vendored
@@ -3,6 +3,7 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
@@ -20,15 +21,18 @@ declare global {
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const event2human: typeof import('./composables/utils')['event2human']
|
||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
|
||||
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
||||
const h: typeof import('vue')['h']
|
||||
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isAutostart: typeof import('./composables/network')['isAutostart']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
@@ -41,10 +45,12 @@ declare global {
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
|
||||
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
@@ -56,6 +62,7 @@ declare global {
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
|
||||
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
|
||||
const provide: typeof import('vue')['provide']
|
||||
@@ -77,6 +84,7 @@ declare global {
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
@@ -87,11 +95,14 @@ declare global {
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router/auto')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTray: typeof import('./composables/tray')['useTray']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
@@ -101,7 +112,7 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
@@ -120,22 +131,22 @@ declare module 'vue' {
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly definePage: UnwrapRef<typeof import('unplugin-vue-router/runtime')['definePage']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
|
||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -143,10 +154,12 @@ declare module 'vue' {
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly num2ipv4: UnwrapRef<typeof import('./composables/utils')['num2ipv4']>
|
||||
readonly num2ipv6: UnwrapRef<typeof import('./composables/utils')['num2ipv6']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
@@ -158,6 +171,7 @@ declare module 'vue' {
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
@@ -168,7 +182,6 @@ declare module 'vue' {
|
||||
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
@@ -189,11 +202,14 @@ declare module 'vue' {
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
|
||||
27
easytier-gui/src/components/About.vue
Normal file
27
easytier-gui/src/components/About.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { getEasytierVersion } from '~/composables/network'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const etVersion = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
etVersion.value = await getEasytierVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card>
|
||||
<template #title>
|
||||
Easytier - {{ t('about.version') }}: {{ etVersion }}
|
||||
</template>
|
||||
<template #content>
|
||||
<p class="mb-1">
|
||||
{{ t('about.description') }}
|
||||
</p>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
</style>
|
||||
@@ -2,10 +2,8 @@
|
||||
import InputGroup from 'primevue/inputgroup'
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||
import { getOsHostname } from '~/composables/network'
|
||||
import { NetworkingMethod } from '~/types/network'
|
||||
const { t } = useI18n()
|
||||
|
||||
import { ping } from 'tauri-plugin-vpnservice-api'
|
||||
import { NetworkingMethod } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
configInvalid?: boolean
|
||||
@@ -14,6 +12,8 @@ const props = defineProps<{
|
||||
|
||||
defineEmits(['runNetwork'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networking_methods = ref([
|
||||
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
|
||||
{ value: NetworkingMethod.Manual, label: () => t('manual') },
|
||||
@@ -32,24 +32,27 @@ const curNetwork = computed(() => {
|
||||
return networkStore.curNetwork
|
||||
})
|
||||
|
||||
const protos:{ [proto: string] : number; } = {'tcp': 11010, 'udp': 11010, 'wg':11011, 'ws': 11011, 'wss': 11012}
|
||||
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
|
||||
|
||||
function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
const query = e.query
|
||||
let ret = []
|
||||
const ret = []
|
||||
// if query match "^\w+:.*", then no proto prefix
|
||||
if (query.match(/^\w+:.*/)) {
|
||||
// if query is a valid url, then add to suggestions
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(query)
|
||||
ret.push(query)
|
||||
} catch (e) {}
|
||||
} else {
|
||||
for (let proto in protos) {
|
||||
let item = proto + '://' + query
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
else {
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://${query}`
|
||||
// if query match ":\d+$", then no port suffix
|
||||
if (!query.match(/:\d+$/)) {
|
||||
item += ':' + protos[proto]
|
||||
item += `:${protos[proto]}`
|
||||
}
|
||||
ret.push(item)
|
||||
}
|
||||
@@ -58,45 +61,59 @@ function searchUrlSuggestions(e: { query: string }): string[] {
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
const publicServerSuggestions = ref([''])
|
||||
|
||||
const searchPresetPublicServers = (e: { query: string }) => {
|
||||
const presetPublicServers = [
|
||||
'tcp://easytier.public.kkrainbow.top:11010',
|
||||
]
|
||||
function searchPresetPublicServers(e: { query: string }) {
|
||||
const presetPublicServers = [
|
||||
'tcp://public.easytier.top:11010',
|
||||
]
|
||||
|
||||
let query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter((item) => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
const query = e.query
|
||||
// if query is sub string of presetPublicServers, add to suggestions
|
||||
let ret = presetPublicServers.filter(item => item.includes(query))
|
||||
// add additional suggestions
|
||||
if (query.length > 0) {
|
||||
ret = ret.concat(searchUrlSuggestions(e))
|
||||
}
|
||||
|
||||
publicServerSuggestions.value = ret
|
||||
publicServerSuggestions.value = ret
|
||||
}
|
||||
|
||||
const peerSuggestions = ref([''])
|
||||
|
||||
const searchPeerSuggestions = (e: { query: string }) => {
|
||||
function searchPeerSuggestions(e: { query: string }) {
|
||||
peerSuggestions.value = searchUrlSuggestions(e)
|
||||
}
|
||||
|
||||
const inetSuggestions = ref([''])
|
||||
|
||||
function searchInetSuggestions(e: { query: string }) {
|
||||
if (e.query.search('/') >= 0) {
|
||||
inetSuggestions.value = [e.query]
|
||||
} else {
|
||||
const ret = []
|
||||
for (let i = 0; i < 32; i++) {
|
||||
ret.push(`${e.query}/${i}`)
|
||||
}
|
||||
inetSuggestions.value = ret
|
||||
}
|
||||
}
|
||||
|
||||
const listenerSuggestions = ref([''])
|
||||
|
||||
const searchListenerSuggestiong = (e: { query: string }) => {
|
||||
let ret = []
|
||||
function searchListenerSuggestiong(e: { query: string }) {
|
||||
const ret = []
|
||||
|
||||
for (let proto in protos) {
|
||||
let item = proto + '://0.0.0.0:';
|
||||
for (const proto in protos) {
|
||||
let item = `${proto}://0.0.0.0:`
|
||||
// if query is a number, use it as port
|
||||
if (e.query.match(/^\d+$/)) {
|
||||
item += e.query
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
item += protos[proto]
|
||||
}
|
||||
|
||||
|
||||
if (item.includes(e.query)) {
|
||||
ret.push(item)
|
||||
}
|
||||
@@ -112,7 +129,7 @@ const searchListenerSuggestiong = (e: { query: string }) => {
|
||||
function validateHostname() {
|
||||
if (curNetwork.value.hostname) {
|
||||
// eslint no-useless-escape
|
||||
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-zA-Z0-9\-]*/g, '')
|
||||
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-z0-9\-]*/gi, '')
|
||||
if (name.length > 32)
|
||||
name = name.substring(0, 32)
|
||||
|
||||
@@ -125,18 +142,12 @@ const osHostname = ref<string>('')
|
||||
|
||||
onMounted(async () => {
|
||||
osHostname.value = await getOsHostname()
|
||||
osHostname.value = await ping('ffdklsajflkdsjl') || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-column h-full">
|
||||
<div class="flex flex-column">
|
||||
<div class="w-10/12 self-center ">
|
||||
<Message severity="warn">
|
||||
{{ t('dhcp_experimental_warning') }}
|
||||
</Message>
|
||||
</div>
|
||||
<div class="w-10/12 self-center ">
|
||||
<Panel :header="t('basic_settings')">
|
||||
<div class="flex flex-column gap-y-2">
|
||||
@@ -151,11 +162,14 @@ onMounted(async () => {
|
||||
</label>
|
||||
</div>
|
||||
<InputGroup>
|
||||
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||
aria-describedby="virtual_ipv4-help" />
|
||||
<InputText
|
||||
id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
|
||||
aria-describedby="virtual_ipv4-help"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<span>/24</span>
|
||||
<span>/</span>
|
||||
</InputGroupAddon>
|
||||
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp" inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid class="max-w-20"/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,23 +181,29 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||
<InputText id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby=" network_secret-help" />
|
||||
<InputText
|
||||
id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby="network_secret-help"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="nm">{{ t('networking_method') }}</label>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value"></SelectButton>
|
||||
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value" />
|
||||
<div class="items-center flex flex-row p-fluid gap-x-1">
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
<AutoComplete
|
||||
v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
|
||||
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"/>
|
||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"
|
||||
/>
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" :suggestions="publicServerSuggestions"
|
||||
:virtualScrollerOptions="{ itemSize: 38 }" class="grow" dropdown @complete="searchPresetPublicServers" :completeOnFocus="true"
|
||||
v-model="curNetwork.public_server_url"/>
|
||||
<AutoComplete
|
||||
v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" v-model="curNetwork.public_server_url"
|
||||
:suggestions="publicServerSuggestions" :virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
|
||||
@complete="searchPresetPublicServers"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,67 +214,103 @@ onMounted(async () => {
|
||||
|
||||
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||
<div class="flex flex-column gap-y-2">
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<div class="flex align-items-center">
|
||||
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" />
|
||||
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="hostname">{{ t('hostname') }}</label>
|
||||
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
|
||||
<InputText
|
||||
id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
|
||||
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="username">{{ t('proxy_cidrs') }}</label>
|
||||
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
|
||||
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
|
||||
<AutoComplete
|
||||
id="subnet-proxy"
|
||||
v-model="curNetwork.proxy_cidrs" :placeholder="t('chips_placeholder', ['10.0.0.0/24'])"
|
||||
class="w-full" multiple fluid :suggestions="inetSuggestions" @complete="searchInetSuggestions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap ">
|
||||
<div class="flex flex-column gap-2 grow">
|
||||
<label for="username">VPN Portal</label>
|
||||
<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"/>
|
||||
<div class="items-center flex flex-row gap-x-4" v-if="curNetwork.enable_vpn_portal">
|
||||
<div class="min-w-64">
|
||||
<InputGroup>
|
||||
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
|
||||
:placeholder="t('vpn_portal_client_network')" />
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<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"
|
||||
/>
|
||||
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
|
||||
<div class="min-w-64">
|
||||
<InputGroup>
|
||||
<InputText
|
||||
v-model="curNetwork.vpn_portal_client_network_addr"
|
||||
:placeholder="t('vpn_portal_client_network')"
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
|
||||
:format="false" :min="0" :max="65535" class="w-8" fluid/>
|
||||
</div>
|
||||
<InputNumber
|
||||
v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
|
||||
:format="false" :min="0" :max="65535" class="w-8" fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 grow p-fluid">
|
||||
<label for="listener_urls">{{ t('listener_urls') }}</label>
|
||||
<AutoComplete id="listener_urls" :suggestions="listenerSuggestions"
|
||||
class="w-full" dropdown @complete="searchListenerSuggestiong" :completeOnFocus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
|
||||
v-model="curNetwork.listener_urls" multiple/>
|
||||
<AutoComplete
|
||||
id="listener_urls" v-model="curNetwork.listener_urls"
|
||||
:suggestions="listenerSuggestions" class="w-full" dropdown :complete-on-focus="true"
|
||||
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
|
||||
multiple @complete="searchListenerSuggestiong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="rpc_port">{{ t('rpc_port') }}</label>
|
||||
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
|
||||
:format="false" :min="0" :max="65535" />
|
||||
<InputNumber
|
||||
id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
|
||||
:format="false" :min="0" :max="65535"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-column gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
<InputText
|
||||
id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
|
||||
:placeholder="t('dev_name_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)" />
|
||||
<Button
|
||||
:label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||
@click="$emit('runNetwork', curNetwork)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
32
easytier-gui/src/components/HumanEvent.vue
Normal file
32
easytier-gui/src/components/HumanEvent.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { EventType } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
event: {
|
||||
[key: string]: any
|
||||
}
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const eventKey = computed(() => {
|
||||
const key = Object.keys(props.event)[0]
|
||||
return Object.keys(EventType).includes(key) ? key : 'Unknown'
|
||||
})
|
||||
|
||||
const eventValue = computed(() => {
|
||||
const value = props.event[eventKey.value]
|
||||
return typeof value === 'object' ? value : value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset :legend="t(`event.${eventKey}`)">
|
||||
<template v-if="eventKey !== 'Unknown'">
|
||||
<div v-if="event.DhcpIpv4Changed">
|
||||
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
|
||||
</div>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</template>
|
||||
<pre v-else>{{ eventValue }}</pre>
|
||||
</Fieldset>
|
||||
</template>
|
||||
@@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { NodeInfo } from '~/types/network'
|
||||
const { t } = useI18n()
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||
import type { NodeInfo, PeerRoutePair } from '~/types/network'
|
||||
|
||||
const props = defineProps<{
|
||||
instanceId?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
const curNetwork = computed(() => {
|
||||
@@ -24,8 +27,16 @@ const curNetworkInst = computed(() => {
|
||||
})
|
||||
|
||||
const peerRouteInfos = computed(() => {
|
||||
if (curNetworkInst.value)
|
||||
return curNetworkInst.value.detail?.peer_route_pairs || []
|
||||
if (curNetworkInst.value) {
|
||||
const my_node_info = curNetworkInst.value.detail?.my_node_info
|
||||
return [{
|
||||
route: {
|
||||
ipv4_addr: my_node_info?.virtual_ipv4,
|
||||
hostname: my_node_info?.hostname,
|
||||
version: my_node_info?.version,
|
||||
},
|
||||
}, ...(curNetworkInst.value.detail?.peer_route_pairs || [])]
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
@@ -33,8 +44,9 @@ const peerRouteInfos = computed(() => {
|
||||
function routeCost(info: any) {
|
||||
if (info.route) {
|
||||
const cost = info.route.cost
|
||||
return cost === 1 ? 'p2p' : `relay(${cost})`
|
||||
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
|
||||
}
|
||||
|
||||
return '?'
|
||||
}
|
||||
|
||||
@@ -73,29 +85,40 @@ function humanFileSize(bytes: number, si = false, dp = 1) {
|
||||
return `${bytes.toFixed(dp)} ${units[u]}`
|
||||
}
|
||||
|
||||
function latencyMs(info: any) {
|
||||
function latencyMs(info: PeerRoutePair) {
|
||||
let lat_us_sum = statsCommon(info, 'stats.latency_us')
|
||||
if (lat_us_sum === undefined)
|
||||
return ''
|
||||
lat_us_sum = lat_us_sum / 1000 / info.peer.conns.length
|
||||
lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
|
||||
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
|
||||
}
|
||||
|
||||
function txBytes(info: any) {
|
||||
function txBytes(info: PeerRoutePair) {
|
||||
const tx = statsCommon(info, 'stats.tx_bytes')
|
||||
return tx ? humanFileSize(tx) : ''
|
||||
}
|
||||
|
||||
function rxBytes(info: any) {
|
||||
function rxBytes(info: PeerRoutePair) {
|
||||
const rx = statsCommon(info, 'stats.rx_bytes')
|
||||
return rx ? humanFileSize(rx) : ''
|
||||
}
|
||||
|
||||
function lossRate(info: any) {
|
||||
function lossRate(info: PeerRoutePair) {
|
||||
const lossRate = statsCommon(info, 'loss_rate')
|
||||
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
|
||||
}
|
||||
|
||||
function version(info: PeerRoutePair) {
|
||||
return info.route.version === '' ? 'unknown' : info.route.version
|
||||
}
|
||||
|
||||
function ipFormat(info: PeerRoutePair) {
|
||||
const ip = info.route.ipv4_addr
|
||||
if (typeof ip === 'string')
|
||||
return ip
|
||||
return ip ? `${num2ipv4(ip.address)}/${ip.network_length}` : ''
|
||||
}
|
||||
|
||||
const myNodeInfo = computed(() => {
|
||||
if (!curNetworkInst.value)
|
||||
return {} as NodeInfo
|
||||
@@ -117,8 +140,16 @@ const myNodeInfoChips = computed(() => {
|
||||
if (!my_node_info)
|
||||
return chips
|
||||
|
||||
// virtual ipv4
|
||||
// TUN Device Name
|
||||
const dev_name = curNetworkInst.value.detail?.dev_name
|
||||
if (dev_name) {
|
||||
chips.push({
|
||||
label: `TUN Device Name: ${dev_name}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
// virtual ipv4
|
||||
chips.push({
|
||||
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
|
||||
icon: '',
|
||||
@@ -128,7 +159,7 @@ const myNodeInfoChips = computed(() => {
|
||||
const local_ipv4s = my_node_info.ips?.interface_ipv4s
|
||||
for (const [idx, ip] of local_ipv4s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv4 ${idx}: ${ip}`,
|
||||
label: `Local IPv4 ${idx}: ${num2ipv4(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -137,7 +168,7 @@ const myNodeInfoChips = computed(() => {
|
||||
const local_ipv6s = my_node_info.ips?.interface_ipv6s
|
||||
for (const [idx, ip] of local_ipv6s?.entries()) {
|
||||
chips.push({
|
||||
label: `Local IPv6 ${idx}: ${ip}`,
|
||||
label: `Local IPv6 ${idx}: ${num2ipv6(ip)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -146,7 +177,19 @@ const myNodeInfoChips = computed(() => {
|
||||
const public_ip = my_node_info.ips?.public_ipv4
|
||||
if (public_ip) {
|
||||
chips.push({
|
||||
label: `Public IP: ${public_ip}`,
|
||||
label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
|
||||
const public_ipv6 = my_node_info.ips?.public_ipv6
|
||||
if (public_ipv6) {
|
||||
chips.push({
|
||||
label: `Public IPv6: ${IPv6.fromBigInt((BigInt(public_ipv6.part1) << BigInt(96))
|
||||
+ (BigInt(public_ipv6.part2) << BigInt(64))
|
||||
+ (BigInt(public_ipv6.part3) << BigInt(32))
|
||||
+ BigInt(public_ipv6.part4),
|
||||
)}`,
|
||||
icon: '',
|
||||
} as Chip)
|
||||
}
|
||||
@@ -171,6 +214,8 @@ const myNodeInfoChips = computed(() => {
|
||||
PortRestricted = 5,
|
||||
Symmetric = 6,
|
||||
SymUdpFirewall = 7,
|
||||
SymmetricEasyInc = 8,
|
||||
SymmetricEasyDec = 9,
|
||||
};
|
||||
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
|
||||
if (udpNatType !== undefined) {
|
||||
@@ -183,6 +228,8 @@ const myNodeInfoChips = computed(() => {
|
||||
[NatType.PortRestricted]: 'Port Restricted',
|
||||
[NatType.Symmetric]: 'Symmetric',
|
||||
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
|
||||
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
|
||||
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
|
||||
}
|
||||
|
||||
chips.push({
|
||||
@@ -273,16 +320,18 @@ function showEventLogs() {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 400px">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<Button type="button" :label="t('close')" @click="dialogVisible = false" />
|
||||
</div>
|
||||
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto">
|
||||
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
|
||||
<pre>{{ dialogContent }}</pre>
|
||||
</ScrollPanel>
|
||||
<Timeline v-else :value="dialogContent">
|
||||
<template #opposite="slotProps">
|
||||
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item[0])) }}</small>
|
||||
</template>
|
||||
<template #content="slotProps">
|
||||
<HumanEvent :event="slotProps.item[1]" />
|
||||
</template>
|
||||
</Timeline>
|
||||
</Dialog>
|
||||
|
||||
<Card v-if="curNetworkInst?.error_msg">
|
||||
@@ -365,17 +414,46 @@ function showEventLogs() {
|
||||
{{ t('peer_info') }}
|
||||
</template>
|
||||
<template #content>
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-style="width: 100%">
|
||||
<Column field="route.ipv4_addr" style="width: 100px;" :header="t('virtual_ipv4')" />
|
||||
<Column field="route.hostname" style="max-width: 250px;" :header="t('hostname')" />
|
||||
<Column :field="routeCost" style="width: 100px;" :header="t('route_cost')" />
|
||||
<Column :field="latencyMs" style="width: 80px;" :header="t('latency')" />
|
||||
<Column :field="txBytes" style="width: 80px;" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" style="width: 80px;" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" style="width: 100px;" :header="t('loss_rate')" />
|
||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
|
||||
<Column :header="t('hostname')">
|
||||
<template #body="slotProps">
|
||||
<div
|
||||
v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
|
||||
v-tooltip="slotProps.data.route.hostname"
|
||||
>
|
||||
{{
|
||||
slotProps.data.route.hostname }}
|
||||
</div>
|
||||
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
|
||||
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
|
||||
{{ t('status.server') }}
|
||||
</Tag>
|
||||
<Tag v-if="slotProps.data.route.no_relay_data" severity="warn" value="Warn">
|
||||
{{ t('status.relay') }}
|
||||
</Tag>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column :field="routeCost" :header="t('route_cost')" />
|
||||
<Column :field="latencyMs" :header="t('latency')" />
|
||||
<Column :field="txBytes" :header="t('upload_bytes')" />
|
||||
<Column :field="rxBytes" :header="t('download_bytes')" />
|
||||
<Column :field="lossRate" :header="t('loss_rate')" />
|
||||
<Column :header="t('status.version')">
|
||||
<template #body="slotProps">
|
||||
<span>{{ version(slotProps.data) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.p-timeline :deep(.p-timeline-event-opposite) {
|
||||
@apply flex-none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,183 +1,184 @@
|
||||
import { addPluginListener } from '@tauri-apps/api/core';
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api';
|
||||
import { Route } from '~/types/network';
|
||||
import { addPluginListener } from '@tauri-apps/api/core'
|
||||
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
|
||||
import type { Route } from '~/types/network'
|
||||
|
||||
const networkStore = useNetworkStore()
|
||||
|
||||
interface vpnStatus {
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
running: boolean
|
||||
ipv4Addr: string | null | undefined
|
||||
ipv4Cidr: number | null | undefined
|
||||
routes: string[]
|
||||
}
|
||||
|
||||
var curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
routes: []
|
||||
const curVpnStatus: vpnStatus = {
|
||||
running: false,
|
||||
ipv4Addr: undefined,
|
||||
ipv4Cidr: undefined,
|
||||
routes: [],
|
||||
}
|
||||
|
||||
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
|
||||
let start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
if (Date.now() - start_time > timeout_sec * 1000) {
|
||||
throw new Error('wait vpn status timeout')
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
const start_time = Date.now()
|
||||
while (curVpnStatus.running !== target_status) {
|
||||
if (Date.now() - start_time > timeout_sec * 1000) {
|
||||
throw new Error('wait vpn status timeout')
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
}
|
||||
}
|
||||
|
||||
async function doStopVpn() {
|
||||
if (!curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
let stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
await waitVpnStatus(false, 3)
|
||||
if (!curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
console.log('stop vpn')
|
||||
const stop_ret = await stop_vpn()
|
||||
console.log('stop vpn', JSON.stringify((stop_ret)))
|
||||
await waitVpnStatus(false, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
curVpnStatus.ipv4Addr = undefined
|
||||
curVpnStatus.routes = []
|
||||
}
|
||||
|
||||
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
if (curVpnStatus.running) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('start vpn')
|
||||
let start_ret = await start_vpn({
|
||||
"ipv4Addr": ipv4Addr + '/' + cidr,
|
||||
"routes": routes,
|
||||
"disallowedApplications": ["com.kkrainbow.easytier"],
|
||||
"mtu": 1300,
|
||||
});
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
console.log('start vpn')
|
||||
const start_ret = await start_vpn({
|
||||
ipv4Addr: `${ipv4Addr}`,
|
||||
routes,
|
||||
disallowedApplications: ['com.kkrainbow.easytier'],
|
||||
mtu: 1300,
|
||||
})
|
||||
if (start_ret?.errorMsg?.length) {
|
||||
throw new Error(start_ret.errorMsg)
|
||||
}
|
||||
await waitVpnStatus(true, 3)
|
||||
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.routes = routes
|
||||
curVpnStatus.ipv4Addr = ipv4Addr
|
||||
curVpnStatus.routes = routes
|
||||
}
|
||||
|
||||
async function onVpnServiceStart(payload: any) {
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||
}
|
||||
console.log('vpn service start', JSON.stringify(payload))
|
||||
curVpnStatus.running = true
|
||||
if (payload.fd) {
|
||||
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
|
||||
}
|
||||
}
|
||||
|
||||
async function onVpnServiceStop(payload: any) {
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
console.log('vpn service stop', JSON.stringify(payload))
|
||||
curVpnStatus.running = false
|
||||
}
|
||||
|
||||
async function registerVpnServiceListener() {
|
||||
console.log('register vpn service listener')
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_start',
|
||||
onVpnServiceStart
|
||||
)
|
||||
console.log('register vpn service listener')
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_start',
|
||||
onVpnServiceStart,
|
||||
)
|
||||
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_stop',
|
||||
onVpnServiceStop
|
||||
)
|
||||
await addPluginListener(
|
||||
'vpnservice',
|
||||
'vpn_service_stop',
|
||||
onVpnServiceStop,
|
||||
)
|
||||
}
|
||||
|
||||
function getRoutesForVpn(routes: Route[]): string[] {
|
||||
if (!routes) {
|
||||
return []
|
||||
}
|
||||
if (!routes) {
|
||||
return []
|
||||
}
|
||||
|
||||
let ret = []
|
||||
for (let r of routes) {
|
||||
for (let cidr of r.proxy_cidrs) {
|
||||
if (cidr.indexOf('/') === -1) {
|
||||
cidr += '/32'
|
||||
}
|
||||
ret.push(cidr)
|
||||
}
|
||||
const ret = []
|
||||
for (const r of routes) {
|
||||
for (let cidr of r.proxy_cidrs) {
|
||||
if (!cidr.includes('/')) {
|
||||
cidr += '/32'
|
||||
}
|
||||
ret.push(cidr)
|
||||
}
|
||||
}
|
||||
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
// sort and dedup
|
||||
return Array.from(new Set(ret)).sort()
|
||||
}
|
||||
|
||||
async function onNetworkInstanceChange() {
|
||||
let insts = networkStore.networkInstanceIds
|
||||
if (!insts) {
|
||||
await doStopVpn()
|
||||
return
|
||||
const insts = networkStore.networkInstanceIds
|
||||
if (!insts) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||
|
||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
|
||||
if (ipChanged || routesChanged) {
|
||||
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
const curNetworkInfo = networkStore.networkInfos[insts[0]]
|
||||
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
try {
|
||||
await doStartVpn(virtual_ip, 24, routes)
|
||||
}
|
||||
|
||||
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
|
||||
if (!virtual_ip || !virtual_ip.length) {
|
||||
await doStopVpn()
|
||||
return
|
||||
}
|
||||
|
||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
||||
|
||||
var ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||
var routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||
|
||||
if (ipChanged || routesChanged) {
|
||||
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
|
||||
try {
|
||||
await doStopVpn()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
await doStartVpn(virtual_ip, 24, routes)
|
||||
} catch (e) {
|
||||
console.error("start vpn failed, clear all network insts.", e)
|
||||
networkStore.clearNetworkInstances()
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
}
|
||||
return
|
||||
catch (e) {
|
||||
console.error('start vpn failed, clear all network insts.', e)
|
||||
networkStore.clearNetworkInstances()
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function watchNetworkInstance() {
|
||||
var subscribe_running = false
|
||||
networkStore.$subscribe(async () => {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
} catch (_) {
|
||||
}
|
||||
subscribe_running = false
|
||||
})
|
||||
let subscribe_running = false
|
||||
networkStore.$subscribe(async () => {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
}
|
||||
catch (_) {
|
||||
}
|
||||
subscribe_running = false
|
||||
})
|
||||
}
|
||||
|
||||
export async function initMobileVpnService() {
|
||||
await registerVpnServiceListener()
|
||||
await watchNetworkInstance()
|
||||
await registerVpnServiceListener()
|
||||
await watchNetworkInstance()
|
||||
}
|
||||
|
||||
export async function prepareVpnService() {
|
||||
console.log('prepare vpn')
|
||||
let prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
console.log('prepare vpn')
|
||||
const prepare_ret = await prepare_vpn()
|
||||
console.log('prepare vpn', JSON.stringify((prepare_ret)))
|
||||
if (prepare_ret?.errorMsg?.length) {
|
||||
throw new Error(prepare_ret.errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network'
|
||||
|
||||
@@ -22,8 +22,8 @@ export async function getOsHostname() {
|
||||
return await invoke<string>('get_os_hostname')
|
||||
}
|
||||
|
||||
export async function setAutoLaunchStatus(enable: boolean) {
|
||||
return await invoke<boolean>('set_auto_launch_status', { enable })
|
||||
export async function isAutostart() {
|
||||
return await invoke<boolean>('is_autostart')
|
||||
}
|
||||
|
||||
export async function setLoggingLevel(level: string) {
|
||||
@@ -33,3 +33,7 @@ export async function setLoggingLevel(level: string) {
|
||||
export async function setTunFd(instanceId: string, fd: number) {
|
||||
return await invoke('set_tun_fd', { instanceId, fd })
|
||||
}
|
||||
|
||||
export async function getEasytierVersion() {
|
||||
return await invoke<string>('easytier_version')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import pkg from '~/../package.json'
|
||||
|
||||
const DEFAULT_TRAY_NAME = 'main'
|
||||
@@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
|
||||
async function toggleVisibility() {
|
||||
if (await getCurrentWindow().isVisible()) {
|
||||
await getCurrentWindow().hide()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
await getCurrentWindow().show()
|
||||
await getCurrentWindow().setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
export async function useTray(init: boolean = false) {
|
||||
let tray;
|
||||
let tray
|
||||
try {
|
||||
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
|
||||
if (!tray) {
|
||||
@@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
|
||||
}),
|
||||
action: async () => {
|
||||
toggleVisibility()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Error while creating tray icon:', error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (init) {
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}`)
|
||||
tray.setMenuOnLeftClick(false);
|
||||
tray.setMenuOnLeftClick(false)
|
||||
tray.setMenu(await Menu.new({
|
||||
id: 'main',
|
||||
items: await generateMenuItem(),
|
||||
@@ -59,7 +61,7 @@ export async function generateMenuItem() {
|
||||
|
||||
export async function MenuItemExit(text: string) {
|
||||
return await PredefinedMenuItem.new({
|
||||
text: text,
|
||||
text,
|
||||
item: 'Quit',
|
||||
})
|
||||
}
|
||||
@@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
|
||||
id: 'show',
|
||||
text,
|
||||
action: async () => {
|
||||
await toggleVisibility();
|
||||
await toggleVisibility()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
const menu = await Menu.new({
|
||||
id: 'main',
|
||||
items: items || await generateMenuItem(),
|
||||
@@ -86,15 +89,17 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
|
||||
|
||||
export async function setTrayRunState(isRunning: boolean = false) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
|
||||
}
|
||||
|
||||
export async function setTrayTooltip(tooltip: string) {
|
||||
if (tooltip) {
|
||||
const tray = await useTray()
|
||||
if (!tray) return
|
||||
if (!tray)
|
||||
return
|
||||
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
easytier-gui/src/composables/utils.ts
Normal file
15
easytier-gui/src/composables/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IPv4, IPv6 } from 'ip-num/IPNumber'
|
||||
import type { Ipv4Addr, Ipv6Addr } from '~/types/network'
|
||||
|
||||
export function num2ipv4(ip: Ipv4Addr) {
|
||||
return IPv4.fromNumber(ip.addr)
|
||||
}
|
||||
|
||||
export function num2ipv6(ip: Ipv6Addr) {
|
||||
return IPv6.fromBigInt(
|
||||
(BigInt(ip.part1) << BigInt(96))
|
||||
+ (BigInt(ip.part2) << BigInt(64))
|
||||
+ (BigInt(ip.part3) << BigInt(32))
|
||||
+ BigInt(ip.part4),
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import App from '~/App.vue'
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router/auto'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import App from '~/App.vue'
|
||||
import { i18n, loadLanguageAsync } from '~/modules/i18n'
|
||||
|
||||
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||
import '~/styles.css'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import 'primeicons/primeicons.css'
|
||||
import 'primeflex/primeflex.css'
|
||||
import { i18n, loadLanguageAsync } from '~/modules/i18n'
|
||||
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
@@ -18,8 +18,9 @@ if (import.meta.env.PROD) {
|
||||
event.key === 'F5'
|
||||
|| (event.ctrlKey && event.key === 'r')
|
||||
|| (event.metaKey && event.key === 'r')
|
||||
)
|
||||
) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('contextmenu', (event) => {
|
||||
@@ -35,7 +36,7 @@ async function main() {
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
extendRoutes: routes => setupLayouts(routes),
|
||||
routes,
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
@@ -45,11 +46,12 @@ async function main() {
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: false
|
||||
}
|
||||
}})
|
||||
prefix: 'p',
|
||||
darkModeSelector: 'system',
|
||||
cssLayer: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
app.use(ToastService)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { setAutoLaunchStatus } from "~/composables/network"
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
const ret = await setAutoLaunchStatus(enable)
|
||||
localStorage.setItem('auto_launch', JSON.stringify(ret))
|
||||
return ret
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (target_enable) {
|
||||
await enable()
|
||||
}
|
||||
else {
|
||||
// 消除没有配置自启动时进行关闭操作报错
|
||||
try {
|
||||
await disable()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||
return isEnabled()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoLaunchStatusAsync(): boolean {
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Locale } from 'vue-i18n'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { Locale } from 'vue-i18n'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { exit } from '@tauri-apps/plugin-process';
|
||||
import Config from '~/components/Config.vue'
|
||||
import Status from '~/components/Status.vue'
|
||||
|
||||
import type { NetworkConfig } from '~/types/network'
|
||||
import { loadLanguageAsync } from '~/modules/i18n'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { loadRunningInstanceIdsFromLocalStorage } from '~/stores/network'
|
||||
import { setLoggingLevel } from '~/composables/network'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { appLogDir } from '@tauri-apps/api/path'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { useTray } from '~/composables/tray';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Config from '~/components/Config.vue'
|
||||
|
||||
import Status from '~/components/Status.vue'
|
||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { loadLanguageAsync } from '~/modules/i18n'
|
||||
import { type NetworkConfig, NetworkingMethod } from '~/types/network'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const visible = ref(false)
|
||||
const aboutVisible = ref(false)
|
||||
const tomlConfig = ref('')
|
||||
|
||||
useTray(true)
|
||||
@@ -71,7 +72,6 @@ function addNewNetwork() {
|
||||
|
||||
networkStore.$subscribe(async () => {
|
||||
networkStore.saveToLocalStorage()
|
||||
networkStore.saveRunningInstanceIdsToLocalStorage()
|
||||
try {
|
||||
await parseNetworkConfig(networkStore.curNetwork)
|
||||
messageBarSeverity.value = Severity.None
|
||||
@@ -86,7 +86,8 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
if (type() === 'android') {
|
||||
await prepareVpnService()
|
||||
networkStore.clearNetworkInstances()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
}
|
||||
|
||||
@@ -95,6 +96,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
|
||||
try {
|
||||
await runNetworkInstance(cfg)
|
||||
networkStore.addAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
// console.error(e)
|
||||
@@ -109,6 +111,7 @@ async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
cb()
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.removeAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
|
||||
async function updateNetworkInfos() {
|
||||
@@ -120,10 +123,13 @@ onMounted(async () => {
|
||||
intervalId = window.setInterval(async () => {
|
||||
await updateNetworkInfos()
|
||||
}, 500)
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show'))
|
||||
])
|
||||
|
||||
window.setTimeout(async () => {
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show')),
|
||||
])
|
||||
}, 1000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
|
||||
@@ -142,7 +148,7 @@ const setting_menu_items = ref([
|
||||
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show'))
|
||||
await MenuItemShow(t('tray.show')),
|
||||
])
|
||||
},
|
||||
},
|
||||
@@ -158,10 +164,10 @@ const setting_menu_items = ref([
|
||||
icon: 'pi pi-file',
|
||||
items: (function () {
|
||||
const levels = ['off', 'warn', 'info', 'debug', 'trace']
|
||||
let items = []
|
||||
for (let level of levels) {
|
||||
const items = []
|
||||
for (const level of levels) {
|
||||
items.push({
|
||||
label: () => t("logging_level_" + level) + (current_log_level === level ? ' ✓' : ''),
|
||||
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
|
||||
command: async () => {
|
||||
current_log_level = level
|
||||
await setLoggingLevel(level)
|
||||
@@ -175,7 +181,7 @@ const setting_menu_items = ref([
|
||||
label: () => t('logging_open_dir'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: async () => {
|
||||
console.log("open log dir", await appLogDir())
|
||||
// console.log('open log dir', await appLogDir())
|
||||
await open(await appLogDir())
|
||||
},
|
||||
})
|
||||
@@ -187,7 +193,14 @@ const setting_menu_items = ref([
|
||||
},
|
||||
})
|
||||
return items
|
||||
})()
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: () => t('about.title'),
|
||||
icon: 'pi pi-at',
|
||||
command: async () => {
|
||||
aboutVisible.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => t('exit'),
|
||||
@@ -202,18 +215,22 @@ function toggle_setting_menu(event: any) {
|
||||
setting_menu.value.toggle(event)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
networkStore.loadFromLocalStorage()
|
||||
if (getAutoLaunchStatus()) {
|
||||
let prev_running_ids = loadRunningInstanceIdsFromLocalStorage()
|
||||
for (let id of prev_running_ids) {
|
||||
let cfg = networkStore.networkList.find((item) => item.instance_id === id)
|
||||
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
|
||||
getCurrentWindow().hide()
|
||||
const autoStartIds = networkStore.autoStartInstIds
|
||||
for (const id of autoStartIds) {
|
||||
const cfg = networkStore.networkList.find(item => item.instance_id === id)
|
||||
if (cfg) {
|
||||
networkStore.addNetworkInstance(cfg.instance_id)
|
||||
await runNetworkInstance(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
await initMobileVpnService()
|
||||
}
|
||||
@@ -222,7 +239,6 @@ onMounted(async () => {
|
||||
function isRunning(id: string) {
|
||||
return networkStore.networkInstanceIds.includes(id)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -237,11 +253,15 @@ function isRunning(id: string) {
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex justify-content-end gap-2">
|
||||
<div class="flex gap-2 justify-content-end">
|
||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
</Dialog>
|
||||
|
||||
<div>
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
@@ -252,30 +272,45 @@ function isRunning(id: string) {
|
||||
|
||||
<template #center>
|
||||
<div class="min-w-40">
|
||||
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||
:placeholder="t('select_network')" class="w-full">
|
||||
<Dropdown
|
||||
v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||
:placeholder="t('select_network')" class="w-full"
|
||||
>
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-start content-center">
|
||||
<div class="mr-3 flex-column">
|
||||
<span>{{ slotProps.value.network_name }}</span>
|
||||
</div>
|
||||
<Tag class="my-auto" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
<Tag
|
||||
class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="slotProps">
|
||||
<div class="flex flex-col items-start content-center">
|
||||
<div class="flex flex-col items-start content-center max-w-full">
|
||||
<div class="flex">
|
||||
<div class="mr-3">
|
||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
||||
</div>
|
||||
<Tag class="my-auto" :severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||
<Tag
|
||||
class="my-auto leading-3"
|
||||
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')"
|
||||
/>
|
||||
</div>
|
||||
<div>{{ slotProps.option.public_server_url }}</div>
|
||||
<div
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')">
|
||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||
v-if="slotProps.option.networking_method !== NetworkingMethod.Standalone"
|
||||
class="max-w-full overflow-hidden text-ellipsis"
|
||||
>
|
||||
{{ slotProps.option.networking_method === NetworkingMethod.Manual
|
||||
? slotProps.option.peer_urls.join(', ')
|
||||
: slotProps.option.public_server_url }}
|
||||
</div>
|
||||
<div
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')"
|
||||
>
|
||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -285,8 +320,10 @@ function isRunning(id: string) {
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||
<Button
|
||||
icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu"
|
||||
/>
|
||||
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
@@ -295,21 +332,29 @@ function isRunning(id: string) {
|
||||
<Panel class="h-full overflow-y-auto">
|
||||
<Stepper :value="activeStep">
|
||||
<StepList value="1">
|
||||
<Step value="1">{{ t('config_network') }}</Step>
|
||||
<Step value="2">{{ t('running') }}</Step>
|
||||
<Step value="1">
|
||||
{{ t('config_network') }}
|
||||
</Step>
|
||||
<Step value="2">
|
||||
{{ t('running') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels value="1">
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||
@run-network="runNetworkCb($event, () => activateCallback('2'))" />
|
||||
<Config
|
||||
:instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
|
||||
@run-network="runNetworkCb($event, () => activateCallback('2'))"
|
||||
/>
|
||||
</StepPanel>
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
|
||||
<div class="flex flex-column">
|
||||
<Status :instance-id="networkStore.curNetworkId" />
|
||||
</div>
|
||||
<div class="flex pt-4 justify-content-center">
|
||||
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
|
||||
<Button
|
||||
:label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
|
||||
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))"
|
||||
/>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
@@ -349,6 +394,10 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.p-select-overlay {
|
||||
max-width: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.p-tabview-panel {
|
||||
|
||||
@@ -14,6 +14,8 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
instances: {} as Record<string, NetworkInstance>,
|
||||
|
||||
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
|
||||
|
||||
autoStartInstIds: [] as string[],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,7 +76,6 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
this.instances[instanceId].error_msg = info.error_msg || ''
|
||||
this.instances[instanceId].detail = info
|
||||
}
|
||||
this.saveRunningInstanceIdsToLocalStorage()
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
@@ -92,27 +93,44 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
|
||||
this.networkList = networkList
|
||||
this.curNetwork = this.networkList[0]
|
||||
|
||||
this.loadAutoStartInstIdsFromLocalStorage()
|
||||
},
|
||||
|
||||
saveToLocalStorage() {
|
||||
localStorage.setItem('networkList', JSON.stringify(this.networkList))
|
||||
},
|
||||
|
||||
saveRunningInstanceIdsToLocalStorage() {
|
||||
let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running)
|
||||
localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids))
|
||||
}
|
||||
saveAutoStartInstIdsToLocalStorage() {
|
||||
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
|
||||
},
|
||||
|
||||
loadAutoStartInstIdsFromLocalStorage() {
|
||||
try {
|
||||
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
this.autoStartInstIds = []
|
||||
}
|
||||
},
|
||||
|
||||
addAutoStartInstId(instanceId: string) {
|
||||
if (!this.autoStartInstIds.includes(instanceId)) {
|
||||
this.autoStartInstIds.push(instanceId)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
|
||||
removeAutoStartInstId(instanceId: string) {
|
||||
const idx = this.autoStartInstIds.indexOf(instanceId)
|
||||
if (idx !== -1) {
|
||||
this.autoStartInstIds.splice(idx, 1)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))
|
||||
|
||||
export function loadRunningInstanceIdsFromLocalStorage(): string[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('runningInstanceIds') || '[]')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: white;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
2
easytier-gui/src/typed-router.d.ts
vendored
2
easytier-gui/src/typed-router.d.ts
vendored
@@ -12,7 +12,7 @@ declare module 'vue-router/auto-routes' {
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'unplugin-vue-router/types'
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NetworkConfig {
|
||||
|
||||
dhcp: boolean
|
||||
virtual_ipv4: string
|
||||
network_length: number,
|
||||
hostname?: string
|
||||
network_name: string
|
||||
network_secret: string
|
||||
@@ -31,6 +32,9 @@ export interface NetworkConfig {
|
||||
|
||||
listener_urls: string[]
|
||||
rpc_port: number
|
||||
latency_first: boolean
|
||||
|
||||
dev_name: string
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -39,12 +43,13 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
|
||||
dhcp: true,
|
||||
virtual_ipv4: '',
|
||||
network_length: 24,
|
||||
network_name: 'easytier',
|
||||
network_secret: '',
|
||||
|
||||
networking_method: NetworkingMethod.PublicServer,
|
||||
|
||||
public_server_url: 'tcp://easytier.public.kkrainbow.top:11010',
|
||||
public_server_url: 'tcp://public.easytier.top:11010',
|
||||
peer_urls: [],
|
||||
|
||||
proxy_cidrs: [],
|
||||
@@ -62,6 +67,8 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
'wg://0.0.0.0:11011',
|
||||
],
|
||||
rpc_port: 0,
|
||||
latency_first: true,
|
||||
dev_name: '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +82,7 @@ export interface NetworkInstance {
|
||||
}
|
||||
|
||||
export interface NetworkInstanceRunningInfo {
|
||||
dev_name: string
|
||||
my_node_info: NodeInfo
|
||||
events: Record<string, any>
|
||||
node_info: NodeInfo
|
||||
@@ -85,13 +93,26 @@ export interface NetworkInstanceRunningInfo {
|
||||
error_msg?: string
|
||||
}
|
||||
|
||||
export interface Ipv4Addr {
|
||||
addr: number
|
||||
}
|
||||
|
||||
export interface Ipv6Addr {
|
||||
part1: number
|
||||
part2: number
|
||||
part3: number
|
||||
part4: number
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
virtual_ipv4: string
|
||||
hostname: string
|
||||
version: string
|
||||
ips: {
|
||||
public_ipv4: string
|
||||
interface_ipv4s: string[]
|
||||
public_ipv6: string
|
||||
interface_ipv6s: string[]
|
||||
public_ipv4: Ipv4Addr
|
||||
interface_ipv4s: Ipv4Addr[]
|
||||
public_ipv6: Ipv6Addr
|
||||
interface_ipv6s: Ipv6Addr[]
|
||||
listeners: {
|
||||
serialization: string
|
||||
scheme_end: number
|
||||
@@ -118,13 +139,17 @@ export interface StunInfo {
|
||||
|
||||
export interface Route {
|
||||
peer_id: number
|
||||
ipv4_addr: string
|
||||
ipv4_addr: {
|
||||
address: Ipv4Addr
|
||||
network_length: number
|
||||
} | string | null
|
||||
next_hop_peer_id: number
|
||||
cost: number
|
||||
proxy_cidrs: string[]
|
||||
hostname: string
|
||||
stun_info?: StunInfo
|
||||
inst_id: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface PeerInfo {
|
||||
@@ -135,6 +160,7 @@ export interface PeerInfo {
|
||||
export interface PeerConnInfo {
|
||||
conn_id: string
|
||||
my_peer_id: number
|
||||
is_client: boolean
|
||||
peer_id: number
|
||||
features: string[]
|
||||
tunnel?: TunnelInfo
|
||||
@@ -160,3 +186,28 @@ export interface PeerConnStats {
|
||||
tx_packets: number
|
||||
latency_us: number
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
TunDeviceReady = 'TunDeviceReady', // string
|
||||
TunDeviceError = 'TunDeviceError', // string
|
||||
|
||||
PeerAdded = 'PeerAdded', // number
|
||||
PeerRemoved = 'PeerRemoved', // number
|
||||
PeerConnAdded = 'PeerConnAdded', // PeerConnInfo
|
||||
PeerConnRemoved = 'PeerConnRemoved', // PeerConnInfo
|
||||
|
||||
ListenerAdded = 'ListenerAdded', // any
|
||||
ListenerAddFailed = 'ListenerAddFailed', // any, string
|
||||
ListenerAcceptFailed = 'ListenerAcceptFailed', // any, string
|
||||
ConnectionAccepted = 'ConnectionAccepted', // string, string
|
||||
ConnectionError = 'ConnectionError', // string, string, string
|
||||
|
||||
Connecting = 'Connecting', // any
|
||||
ConnectError = 'ConnectError', // string, string, string
|
||||
|
||||
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
||||
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
||||
|
||||
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
|
||||
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import process from 'node:process'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import { internalIpV4Sync } from 'internal-ip'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
@@ -87,15 +91,18 @@ export default defineConfig(async () => ({
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
host: '10.147.223.128',
|
||||
host: host || false,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
hmr: {
|
||||
host: "10.147.223.128",
|
||||
protocol: "ws",
|
||||
},
|
||||
hmr: host
|
||||
? {
|
||||
protocol: 'ws',
|
||||
host: internalIpV4Sync(),
|
||||
port: 1430,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -3,12 +3,12 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "1.2.1"
|
||||
version = "2.0.3"
|
||||
edition = "2021"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming", "command-line-utilities"]
|
||||
rust-version = "1.75"
|
||||
rust-version = "1.77.0"
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -29,6 +29,8 @@ path = "src/lib.rs"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
git-version = "0.3.9"
|
||||
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"env-filter",
|
||||
@@ -49,7 +51,7 @@ futures = { version = "0.3", features = ["bilock", "unstable"] }
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-util = { version = "0.7.9", features = ["codec", "net"] }
|
||||
tokio-util = { version = "0.7.9", features = ["codec", "net", "io"] }
|
||||
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
@@ -101,14 +103,10 @@ uuid = { version = "1.5.0", features = [
|
||||
crossbeam-queue = "0.3"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# for packet
|
||||
postcard = { "version" = "1.0.8", features = ["alloc"] }
|
||||
|
||||
# for rpc
|
||||
tonic = "0.12"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
anyhow = "1.0"
|
||||
tarpc = { version = "0.32", features = ["tokio1", "serde1"] }
|
||||
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
percent-encoding = "2.3.1"
|
||||
@@ -127,6 +125,7 @@ rand = "0.8.5"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
pnet = { version = "0.35.0", features = ["serde"] }
|
||||
serde_json = "1"
|
||||
|
||||
clap = { version = "4.4.8", features = [
|
||||
"string",
|
||||
@@ -142,7 +141,10 @@ network-interface = "2.0"
|
||||
# for ospf route
|
||||
petgraph = "0.6.5"
|
||||
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.0", optional = true } # for encryption
|
||||
# for wireguard
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
||||
|
||||
# for encryption
|
||||
ring = { version = "0.17", optional = true }
|
||||
bitflags = "2.5"
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
@@ -177,6 +179,9 @@ wildmatch = "2.3.4"
|
||||
rust-i18n = "3"
|
||||
sys-locale = "0.3"
|
||||
|
||||
ringbuf = "0.4.5"
|
||||
async-ringbuf = "0.3.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Networking_WinSock",
|
||||
@@ -191,6 +196,8 @@ winreg = "0.52"
|
||||
tonic-build = "0.12"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
prost-build = "0.13.2"
|
||||
rpc_build = { path = "src/proto/rpc_build" }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
@@ -200,13 +207,15 @@ zip = "0.6.6"
|
||||
[dev-dependencies]
|
||||
serial_test = "3.0.0"
|
||||
rstest = "0.18.2"
|
||||
futures-util = "0.3.30"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
tokio-socks = "0.5.2"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun"]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"]
|
||||
full = [
|
||||
"quic",
|
||||
"websocket",
|
||||
@@ -215,9 +224,9 @@ full = [
|
||||
"aes-gcm",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"socks5",
|
||||
]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard"]
|
||||
bsd = ["aes-gcm", "mimalloc", "smoltcp"]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
|
||||
mimalloc = ["dep:mimalloc-rust"]
|
||||
@@ -231,3 +240,4 @@ websocket = [
|
||||
"dep:rcgen",
|
||||
]
|
||||
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
|
||||
socks5 = ["dep:smoltcp"]
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{copy, Cursor},
|
||||
path::PathBuf,
|
||||
};
|
||||
use std::{env, io::Cursor, path::PathBuf};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
struct WindowsBuild {}
|
||||
@@ -46,8 +41,8 @@ impl WindowsBuild {
|
||||
|
||||
fn download_protoc() -> PathBuf {
|
||||
println!("cargo:info=use exist protoc: {:?}", "k");
|
||||
let out_dir = Self::get_cargo_target_dir().unwrap();
|
||||
let fname = out_dir.join("protoc");
|
||||
let out_dir = Self::get_cargo_target_dir().unwrap().join("protobuf");
|
||||
let fname = out_dir.join("bin/protoc.exe");
|
||||
if fname.exists() {
|
||||
println!("cargo:info=use exist protoc: {:?}", fname);
|
||||
return fname;
|
||||
@@ -65,10 +60,7 @@ impl WindowsBuild {
|
||||
.map(zip::ZipArchive::new)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let protoc_zipped_file = content.by_name("bin/protoc.exe").unwrap();
|
||||
let mut content = protoc_zipped_file;
|
||||
|
||||
copy(&mut content, &mut File::create(&fname).unwrap()).unwrap();
|
||||
content.extract(out_dir).unwrap();
|
||||
|
||||
fname
|
||||
}
|
||||
@@ -129,14 +121,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "windows")]
|
||||
WindowsBuild::check_for_win();
|
||||
|
||||
tonic_build::configure()
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute("cli.DirectConnectedPeerInfo", "#[derive(Hash)]")
|
||||
.type_attribute("cli.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
||||
let proto_files = [
|
||||
"src/proto/peer_rpc.proto",
|
||||
"src/proto/common.proto",
|
||||
"src/proto/error.proto",
|
||||
"src/proto/tests.proto",
|
||||
"src/proto/cli.proto",
|
||||
];
|
||||
|
||||
for proto_file in &proto_files {
|
||||
println!("cargo:rerun-if-changed={}", proto_file);
|
||||
}
|
||||
|
||||
prost_build::Config::new()
|
||||
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.type_attribute(
|
||||
"peer_rpc.GetIpListResponse",
|
||||
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||
)
|
||||
.type_attribute("peer_rpc.DirectConnectedPeerInfo", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
|
||||
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
|
||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||
.service_generator(Box::new(rpc_build::ServiceGenerator::new()))
|
||||
.btree_map(&["."])
|
||||
.compile(&["proto/cli.proto"], &["proto/"])
|
||||
.compile_protos(&proto_files, &["src/proto/"])
|
||||
.unwrap();
|
||||
// tonic_build::compile_protos("proto/cli.proto")?;
|
||||
|
||||
check_locale();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,5 +97,26 @@ core_clap:
|
||||
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"
|
||||
relay_network_whitelist:
|
||||
en: "only relay traffic of whitelisted networks, input is a wildcard string, e.g.: '*' (all networks), 'def*' (network prefixed with def), can specify multiple networks disable relay if arg is empty. default is allowing all networks"
|
||||
zh-CN: "仅转发白名单网络的流量,输入是通配符字符串,例如:'*'(所有网络),'def*'(以def为前缀的网络),可以指定多个网络。如果参数为空,则禁用转发。默认允许所有网络"
|
||||
en: |+
|
||||
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)"
|
||||
zh-CN: |+
|
||||
仅转发白名单网络的流量,支持通配符字符串。多个网络名称间可以使用英文空格间隔。
|
||||
如果该参数为空,则禁用转发。默认允许所有网络。
|
||||
例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2)"
|
||||
disable_p2p:
|
||||
en: "disable p2p communication, will only relay packets with peers specified by --peers"
|
||||
zh-CN: "禁用P2P通信,只通过--peers指定的节点转发数据包"
|
||||
disable_udp_hole_punching:
|
||||
en: "disable udp hole punching"
|
||||
zh-CN: "禁用UDP打洞功能"
|
||||
relay_all_peer_rpc:
|
||||
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
socks5:
|
||||
en: "enable socks5 server, allow socks5 client to access virtual network. format: <port>, e.g.: 1080"
|
||||
zh-CN: "启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080"
|
||||
ipv6_listener:
|
||||
en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port"
|
||||
zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听"
|
||||
@@ -23,8 +23,8 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_netns(&self) -> Option<String>;
|
||||
fn set_netns(&self, ns: Option<String>);
|
||||
|
||||
fn get_ipv4(&self) -> Option<std::net::Ipv4Addr>;
|
||||
fn set_ipv4(&self, addr: Option<std::net::Ipv4Addr>);
|
||||
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet>;
|
||||
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>);
|
||||
|
||||
fn get_dhcp(&self) -> bool;
|
||||
fn set_dhcp(&self, dhcp: bool);
|
||||
@@ -64,12 +64,15 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>>;
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>);
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url>;
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
||||
|
||||
fn dump(&self) -> String;
|
||||
}
|
||||
|
||||
pub type NetworkSecretDigest = [u8; 32];
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, Eq, Hash)]
|
||||
pub struct NetworkIdentity {
|
||||
pub network_name: String,
|
||||
pub network_secret: Option<String>,
|
||||
@@ -171,6 +174,14 @@ pub struct Flags {
|
||||
pub use_smoltcp: bool,
|
||||
#[derivative(Default(value = "\"*\".to_string()"))]
|
||||
pub foreign_network_whitelist: String,
|
||||
#[derivative(Default(value = "false"))]
|
||||
pub disable_p2p: bool,
|
||||
#[derivative(Default(value = "false"))]
|
||||
pub relay_all_peer_rpc: bool,
|
||||
#[derivative(Default(value = "false"))]
|
||||
pub disable_udp_hole_punching: bool,
|
||||
#[derivative(Default(value = "\"udp://[::]:0\".to_string()"))]
|
||||
pub ipv6_listener: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -197,7 +208,12 @@ struct Config {
|
||||
|
||||
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
||||
|
||||
flags: Option<Flags>,
|
||||
socks5_proxy: Option<url::Url>,
|
||||
|
||||
flags: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
|
||||
#[serde(skip)]
|
||||
flags_struct: Option<Flags>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -213,13 +229,15 @@ impl Default for TomlConfigLoader {
|
||||
|
||||
impl TomlConfigLoader {
|
||||
pub fn new_from_str(config_str: &str) -> Result<Self, anyhow::Error> {
|
||||
let config = toml::de::from_str::<Config>(config_str).with_context(|| {
|
||||
let mut config = toml::de::from_str::<Config>(config_str).with_context(|| {
|
||||
format!(
|
||||
"failed to parse config file: {}\n{}",
|
||||
config_str, config_str
|
||||
)
|
||||
})?;
|
||||
|
||||
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
|
||||
|
||||
Ok(TomlConfigLoader {
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
})
|
||||
@@ -237,6 +255,24 @@ impl TomlConfigLoader {
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn gen_flags(mut flags_hashmap: serde_json::Map<String, serde_json::Value>) -> Flags {
|
||||
let default_flags_json = serde_json::to_string(&Flags::default()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&default_flags_json)
|
||||
.unwrap();
|
||||
|
||||
let mut merged_hashmap = serde_json::Map::new();
|
||||
for (key, value) in default_flags_hashmap {
|
||||
if let Some(v) = flags_hashmap.remove(&key) {
|
||||
merged_hashmap.insert(key, v);
|
||||
} else {
|
||||
merged_hashmap.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_value(serde_json::Value::Object(merged_hashmap)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigLoader for TomlConfigLoader {
|
||||
@@ -288,16 +324,23 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().netns = ns;
|
||||
}
|
||||
|
||||
fn get_ipv4(&self) -> Option<std::net::Ipv4Addr> {
|
||||
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
|
||||
let locked_config = self.config.lock().unwrap();
|
||||
locked_config
|
||||
.ipv4
|
||||
.as_ref()
|
||||
.map(|s| s.parse().ok())
|
||||
.flatten()
|
||||
.map(|c: cidr::Ipv4Inet| {
|
||||
if c.network_length() == 32 {
|
||||
cidr::Ipv4Inet::new(c.address(), 24).unwrap()
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_ipv4(&self, addr: Option<std::net::Ipv4Addr>) {
|
||||
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
|
||||
self.config.lock().unwrap().ipv4 = if let Some(addr) = addr {
|
||||
Some(addr.to_string())
|
||||
} else {
|
||||
@@ -463,13 +506,13 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.flags
|
||||
.flags_struct
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_flags(&self, flags: Flags) {
|
||||
self.config.lock().unwrap().flags = Some(flags);
|
||||
self.config.lock().unwrap().flags_struct = Some(flags);
|
||||
}
|
||||
|
||||
fn get_exit_nodes(&self) -> Vec<Ipv4Addr> {
|
||||
@@ -496,6 +539,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
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)]
|
||||
@@ -546,7 +597,7 @@ level = "warn"
|
||||
assert!(ret.is_ok());
|
||||
|
||||
let ret = ret.unwrap();
|
||||
assert_eq!("10.144.144.10", ret.get_ipv4().unwrap().to_string());
|
||||
assert_eq!("10.144.144.10/24", ret.get_ipv4().unwrap().to_string());
|
||||
|
||||
assert_eq!(
|
||||
vec!["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"],
|
||||
|
||||
@@ -21,4 +21,13 @@ macro_rules! set_global_var {
|
||||
|
||||
define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000);
|
||||
|
||||
define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10);
|
||||
|
||||
pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2;
|
||||
|
||||
pub const EASYTIER_VERSION: &str = git_version::git_version!(
|
||||
args = ["--abbrev=8", "--always", "--dirty=~"],
|
||||
prefix = concat!(env!("CARGO_PKG_VERSION"), "-"),
|
||||
suffix = "",
|
||||
fallback = env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
@@ -31,8 +31,6 @@ pub enum Error {
|
||||
// RpcListenError(String),
|
||||
#[error("Rpc connect error: {0}")]
|
||||
RpcConnectError(String),
|
||||
#[error("Rpc error: {0}")]
|
||||
RpcClientError(#[from] tarpc::client::RpcError),
|
||||
#[error("Timeout error: {0}")]
|
||||
Timeout(#[from] tokio::time::error::Elapsed),
|
||||
#[error("url in blacklist")]
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::rpc::PeerConnInfo;
|
||||
use crate::proto::cli::PeerConnInfo;
|
||||
use crate::proto::common::PeerFeatureFlag;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
|
||||
use super::{
|
||||
@@ -39,8 +40,8 @@ pub enum GlobalCtxEvent {
|
||||
VpnPortalClientConnected(String, String), // (portal, client ip)
|
||||
VpnPortalClientDisconnected(String, String), // (portal, client ip)
|
||||
|
||||
DhcpIpv4Changed(Option<std::net::Ipv4Addr>, Option<std::net::Ipv4Addr>), // (old, new)
|
||||
DhcpIpv4Conflicted(Option<std::net::Ipv4Addr>),
|
||||
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
|
||||
}
|
||||
|
||||
type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
|
||||
@@ -55,7 +56,7 @@ pub struct GlobalCtx {
|
||||
|
||||
event_bus: EventBus,
|
||||
|
||||
cached_ipv4: AtomicCell<Option<std::net::Ipv4Addr>>,
|
||||
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
|
||||
cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>,
|
||||
|
||||
ip_collector: Arc<IPCollector>,
|
||||
@@ -68,6 +69,8 @@ pub struct GlobalCtx {
|
||||
|
||||
enable_exit_node: bool,
|
||||
no_tun: bool,
|
||||
|
||||
feature_flags: AtomicCell<PeerFeatureFlag>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GlobalCtx {
|
||||
@@ -91,7 +94,7 @@ impl GlobalCtx {
|
||||
let net_ns = NetNS::new(config_fs.get_netns());
|
||||
let hostname = config_fs.get_hostname();
|
||||
|
||||
let (event_bus, _) = tokio::sync::broadcast::channel(100);
|
||||
let (event_bus, _) = tokio::sync::broadcast::channel(1024);
|
||||
|
||||
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
|
||||
|
||||
@@ -119,6 +122,8 @@ impl GlobalCtx {
|
||||
|
||||
enable_exit_node,
|
||||
no_tun,
|
||||
|
||||
feature_flags: AtomicCell::new(PeerFeatureFlag::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +139,7 @@ impl GlobalCtx {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ipv4(&self) -> Option<std::net::Ipv4Addr> {
|
||||
pub fn get_ipv4(&self) -> Option<cidr::Ipv4Inet> {
|
||||
if let Some(ret) = self.cached_ipv4.load() {
|
||||
return Some(ret);
|
||||
}
|
||||
@@ -143,7 +148,7 @@ impl GlobalCtx {
|
||||
return addr;
|
||||
}
|
||||
|
||||
pub fn set_ipv4(&self, addr: Option<std::net::Ipv4Addr>) {
|
||||
pub fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>) {
|
||||
self.config.set_ipv4(addr);
|
||||
self.cached_ipv4.store(None);
|
||||
}
|
||||
@@ -179,6 +184,10 @@ impl GlobalCtx {
|
||||
self.config.get_network_identity()
|
||||
}
|
||||
|
||||
pub fn get_network_name(&self) -> String {
|
||||
self.get_network_identity().network_name
|
||||
}
|
||||
|
||||
pub fn get_ip_collector(&self) -> Arc<IPCollector> {
|
||||
self.ip_collector.clone()
|
||||
}
|
||||
@@ -191,7 +200,6 @@ impl GlobalCtx {
|
||||
self.stun_info_collection.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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 ptr = &self.stun_info_collection as *const Box<dyn StunInfoCollectorTrait>;
|
||||
@@ -219,6 +227,10 @@ impl GlobalCtx {
|
||||
self.config.get_flags()
|
||||
}
|
||||
|
||||
pub fn set_flags(&self, flags: Flags) {
|
||||
self.config.set_flags(flags);
|
||||
}
|
||||
|
||||
pub fn get_128_key(&self) -> [u8; 16] {
|
||||
let mut key = [0u8; 16];
|
||||
let secret = self
|
||||
@@ -243,6 +255,14 @@ impl GlobalCtx {
|
||||
pub fn no_tun(&self) -> bool {
|
||||
self.no_tun
|
||||
}
|
||||
|
||||
pub fn get_feature_flags(&self) -> PeerFeatureFlag {
|
||||
self.feature_flags.load()
|
||||
}
|
||||
|
||||
pub fn set_feature_flags(&self, flags: PeerFeatureFlag) {
|
||||
self.feature_flags.store(flags);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -384,6 +384,10 @@ impl IfConfiguerTrait for WindowsIfConfiger {
|
||||
}
|
||||
|
||||
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
|
||||
let _ = run_shell_cmd(
|
||||
format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(),
|
||||
)
|
||||
.await;
|
||||
run_shell_cmd(
|
||||
format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(),
|
||||
)
|
||||
@@ -395,7 +399,7 @@ pub struct DummyIfConfiger {}
|
||||
#[async_trait]
|
||||
impl IfConfiguerTrait for DummyIfConfiger {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub type IfConfiger = MacIfConfiger;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -404,5 +408,10 @@ pub type IfConfiger = LinuxIfConfiger;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub type IfConfiger = WindowsIfConfiger;
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
#[cfg(not(any(
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
pub type IfConfiger = DummyIfConfiger;
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod global_ctx;
|
||||
pub mod ifcfg;
|
||||
pub mod netns;
|
||||
pub mod network;
|
||||
pub mod scoped_task;
|
||||
pub mod stun;
|
||||
pub mod stun_codec_ext;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::{net::IpAddr, ops::Deref, sync::Arc};
|
||||
|
||||
use crate::rpc::peer::GetIpListResponse;
|
||||
use pnet::datalink::NetworkInterface;
|
||||
use tokio::{
|
||||
sync::{Mutex, RwLock},
|
||||
task::JoinSet,
|
||||
};
|
||||
|
||||
use crate::proto::peer_rpc::GetIpListResponse;
|
||||
|
||||
use super::{netns::NetNS, stun::StunInfoCollectorTrait};
|
||||
|
||||
pub const CACHED_IP_LIST_TIMEOUT_SEC: u64 = 60;
|
||||
@@ -60,7 +61,9 @@ impl InterfaceFilter {
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
impl InterfaceFilter {
|
||||
async fn is_interface_physical(interface_name: &str) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn is_interface_physical(&self) -> bool {
|
||||
let interface_name = &self.iface.name;
|
||||
let output = tokio::process::Command::new("networksetup")
|
||||
.args(&["-listallhardwareports"])
|
||||
.output()
|
||||
@@ -87,11 +90,17 @@ impl InterfaceFilter {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
async fn is_interface_physical(&self) -> bool {
|
||||
// if mac addr is not zero, then it's physical interface
|
||||
self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn filter_iface(&self) -> bool {
|
||||
!self.iface.is_point_to_point()
|
||||
&& !self.iface.is_loopback()
|
||||
&& self.iface.is_up()
|
||||
&& Self::is_interface_physical(&self.iface.name).await
|
||||
&& self.is_interface_physical().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +164,7 @@ pub struct IPCollector {
|
||||
impl IPCollector {
|
||||
pub fn new<T: StunInfoCollectorTrait + 'static>(net_ns: NetNS, stun_info_collector: T) -> Self {
|
||||
Self {
|
||||
cached_ip_list: Arc::new(RwLock::new(GetIpListResponse::new())),
|
||||
cached_ip_list: Arc::new(RwLock::new(GetIpListResponse::default())),
|
||||
collect_ip_task: Mutex::new(JoinSet::new()),
|
||||
net_ns,
|
||||
stun_info_collector: Arc::new(Box::new(stun_info_collector)),
|
||||
@@ -187,14 +196,18 @@ impl IPCollector {
|
||||
let Ok(ip_addr) = ip.parse::<IpAddr>() else {
|
||||
continue;
|
||||
};
|
||||
if ip_addr.is_ipv4() {
|
||||
cached_ip_list.write().await.public_ipv4 = ip.clone();
|
||||
} else {
|
||||
cached_ip_list.write().await.public_ipv6 = ip.clone();
|
||||
|
||||
match ip_addr {
|
||||
IpAddr::V4(v) => {
|
||||
cached_ip_list.write().await.public_ipv4 = Some(v.into())
|
||||
}
|
||||
IpAddr::V6(v) => {
|
||||
cached_ip_list.write().await.public_ipv6 = Some(v.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_empty() {
|
||||
let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_none() {
|
||||
CACHED_IP_LIST_TIMEOUT_SEC
|
||||
} else {
|
||||
3
|
||||
@@ -228,7 +241,7 @@ impl IPCollector {
|
||||
|
||||
#[tracing::instrument(skip(net_ns))]
|
||||
async fn do_collect_local_ip_addrs(net_ns: NetNS) -> GetIpListResponse {
|
||||
let mut ret = crate::rpc::peer::GetIpListResponse::new();
|
||||
let mut ret = GetIpListResponse::default();
|
||||
|
||||
let ifaces = Self::collect_interfaces(net_ns.clone()).await;
|
||||
let _g = net_ns.guard();
|
||||
@@ -238,25 +251,28 @@ impl IPCollector {
|
||||
if ip.is_loopback() || ip.is_multicast() {
|
||||
continue;
|
||||
}
|
||||
if ip.is_ipv4() {
|
||||
ret.interface_ipv4s.push(ip.to_string());
|
||||
} else if ip.is_ipv6() {
|
||||
ret.interface_ipv6s.push(ip.to_string());
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
ret.interface_ipv4s.push(v4.into());
|
||||
}
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
ret.interface_ipv6s.push(v6.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(v4_addr) = local_ipv4().await {
|
||||
tracing::trace!("got local ipv4: {}", v4_addr);
|
||||
if !ret.interface_ipv4s.contains(&v4_addr.to_string()) {
|
||||
ret.interface_ipv4s.push(v4_addr.to_string());
|
||||
if !ret.interface_ipv4s.contains(&v4_addr.into()) {
|
||||
ret.interface_ipv4s.push(v4_addr.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(v6_addr) = local_ipv6().await {
|
||||
tracing::trace!("got local ipv6: {}", v6_addr);
|
||||
if !ret.interface_ipv6s.contains(&v6_addr.to_string()) {
|
||||
ret.interface_ipv6s.push(v6_addr.to_string());
|
||||
if !ret.interface_ipv6s.contains(&v6_addr.into()) {
|
||||
ret.interface_ipv6s.push(v6_addr.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
easytier/src/common/scoped_task.rs
Normal file
134
easytier/src/common/scoped_task.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! This crate provides a wrapper type of Tokio's JoinHandle: `ScopedTask`, which aborts the task when it's dropped.
|
||||
//! `ScopedTask` can still be awaited to join the child-task, and abort-on-drop will still trigger while it is being awaited.
|
||||
//!
|
||||
//! For example, if task A spawned task B but is doing something else, and task B is waiting for task C to join,
|
||||
//! aborting A will also abort both B and C.
|
||||
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScopedTask<T> {
|
||||
inner: JoinHandle<T>,
|
||||
}
|
||||
|
||||
impl<T> Drop for ScopedTask<T> {
|
||||
fn drop(&mut self) {
|
||||
self.inner.abort()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ScopedTask<T> {
|
||||
type Output = <JoinHandle<T> as Future>::Output;
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.inner).poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<JoinHandle<T>> for ScopedTask<T> {
|
||||
fn from(inner: JoinHandle<T>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for ScopedTask<T> {
|
||||
type Target = JoinHandle<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ScopedTask;
|
||||
use futures_util::future::pending;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::task::yield_now;
|
||||
|
||||
struct Sentry(Arc<RwLock<bool>>);
|
||||
impl Drop for Sentry {
|
||||
fn drop(&mut self) {
|
||||
*self.0.write().unwrap() = true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_while_not_waiting_for_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
drop(task);
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drop_while_waiting_for_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let handle = tokio::spawn(async move {
|
||||
ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}))
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
handle.abort();
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_drop_only_join() {
|
||||
assert_eq!(
|
||||
ScopedTask::from(tokio::spawn(async {
|
||||
yield_now().await;
|
||||
5
|
||||
}))
|
||||
.await
|
||||
.unwrap(),
|
||||
5
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manually_abort_before_drop() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
task.abort();
|
||||
yield_now().await;
|
||||
assert!(*dropped.read().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manually_abort_then_join() {
|
||||
let dropped = Arc::new(RwLock::new(false));
|
||||
let sentry = Sentry(dropped.clone());
|
||||
let task = ScopedTask::from(tokio::spawn(async move {
|
||||
let _sentry = sentry;
|
||||
pending::<()>().await
|
||||
}));
|
||||
yield_now().await;
|
||||
assert!(!*dropped.read().unwrap());
|
||||
task.abort();
|
||||
yield_now().await;
|
||||
assert!(task.await.is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::rpc::{NatType, StunInfo};
|
||||
use crate::proto::common::{NatType, StunInfo};
|
||||
use anyhow::Context;
|
||||
use chrono::Local;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
@@ -55,6 +56,8 @@ impl HostResolverIter {
|
||||
self.ips = ips
|
||||
.filter(|x| x.is_ipv4())
|
||||
.choose_multiple(&mut rand::thread_rng(), self.max_ip_per_domain as usize);
|
||||
|
||||
if self.ips.is_empty() {return self.next().await;}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(?host, ?e, "lookup host for stun failed");
|
||||
@@ -161,7 +164,7 @@ impl StunClient {
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::debug!(b = ?&udp_buf[..len], ?tids, ?remote_addr, ?stun_host, "recv stun response, msg: {:#?}", msg);
|
||||
tracing::trace!(b = ?&udp_buf[..len], ?tids, ?remote_addr, ?stun_host, "recv stun response, msg: {:#?}", msg);
|
||||
|
||||
if msg.class() != MessageClass::SuccessResponse
|
||||
|| msg.method() != BINDING
|
||||
@@ -216,7 +219,7 @@ impl StunClient {
|
||||
changed_addr
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret, err, level = Level::DEBUG)]
|
||||
#[tracing::instrument(ret, level = Level::TRACE)]
|
||||
pub async fn bind_request(
|
||||
self,
|
||||
change_ip: bool,
|
||||
@@ -243,7 +246,7 @@ impl StunClient {
|
||||
.encode_into_bytes(message.clone())
|
||||
.with_context(|| "encode stun message")?;
|
||||
tids.push(tid as u128);
|
||||
tracing::debug!(?message, ?msg, tid, "send stun request");
|
||||
tracing::trace!(?message, ?msg, tid, "send stun request");
|
||||
self.socket
|
||||
.send_to(msg.as_slice().into(), &stun_host)
|
||||
.await?;
|
||||
@@ -276,7 +279,7 @@ impl StunClient {
|
||||
latency_us: now.elapsed().as_micros() as u32,
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
tracing::trace!(
|
||||
?stun_host,
|
||||
?recv_addr,
|
||||
?changed_socket_addr,
|
||||
@@ -303,14 +306,14 @@ impl StunClientBuilder {
|
||||
task_set.spawn(
|
||||
async move {
|
||||
let mut buf = [0; 1620];
|
||||
tracing::info!("start stun packet listener");
|
||||
tracing::trace!("start stun packet listener");
|
||||
loop {
|
||||
let Ok((len, addr)) = udp_clone.recv_from(&mut buf).await else {
|
||||
tracing::error!("udp recv_from error");
|
||||
break;
|
||||
};
|
||||
let data = buf[..len].to_vec();
|
||||
tracing::debug!(?addr, ?data, "recv udp stun packet");
|
||||
tracing::trace!(?addr, ?data, "recv udp stun packet");
|
||||
let _ = stun_packet_sender_clone.send(StunPacket { data, addr });
|
||||
}
|
||||
}
|
||||
@@ -342,6 +345,8 @@ impl StunClientBuilder {
|
||||
pub struct UdpNatTypeDetectResult {
|
||||
source_addr: SocketAddr,
|
||||
stun_resps: Vec<BindRequestResponse>,
|
||||
// if we are easy symmetric nat, we need to test with another port to check inc or dec
|
||||
extra_bind_test: Option<BindRequestResponse>,
|
||||
}
|
||||
|
||||
impl UdpNatTypeDetectResult {
|
||||
@@ -349,6 +354,7 @@ impl UdpNatTypeDetectResult {
|
||||
Self {
|
||||
source_addr,
|
||||
stun_resps,
|
||||
extra_bind_test: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +411,7 @@ impl UdpNatTypeDetectResult {
|
||||
.filter_map(|x| x.mapped_socket_addr)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len();
|
||||
mapped_addr_count < self.stun_server_count()
|
||||
mapped_addr_count == 1
|
||||
}
|
||||
|
||||
pub fn nat_type(&self) -> NatType {
|
||||
@@ -428,7 +434,32 @@ impl UdpNatTypeDetectResult {
|
||||
return NatType::PortRestricted;
|
||||
}
|
||||
} else if !self.stun_resps.is_empty() {
|
||||
return NatType::Symmetric;
|
||||
if self.public_ips().len() != 1
|
||||
|| self.usable_stun_resp_count() <= 1
|
||||
|| self.max_port() - self.min_port() > 15
|
||||
|| self.extra_bind_test.is_none()
|
||||
|| self
|
||||
.extra_bind_test
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.mapped_socket_addr
|
||||
.is_none()
|
||||
{
|
||||
return NatType::Symmetric;
|
||||
} else {
|
||||
let extra_bind_test = self.extra_bind_test.as_ref().unwrap();
|
||||
let extra_port = extra_bind_test.mapped_socket_addr.unwrap().port();
|
||||
|
||||
let max_port_diff = extra_port.saturating_sub(self.max_port());
|
||||
let min_port_diff = self.min_port().saturating_sub(extra_port);
|
||||
if max_port_diff != 0 && max_port_diff < 100 {
|
||||
return NatType::SymmetricEasyInc;
|
||||
} else if min_port_diff != 0 && min_port_diff < 100 {
|
||||
return NatType::SymmetricEasyDec;
|
||||
} else {
|
||||
return NatType::Symmetric;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return NatType::Unknown;
|
||||
}
|
||||
@@ -476,6 +507,13 @@ impl UdpNatTypeDetectResult {
|
||||
.max()
|
||||
.unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
pub fn usable_stun_resp_count(&self) -> usize {
|
||||
self.stun_resps
|
||||
.iter()
|
||||
.filter(|x| x.mapped_socket_addr.is_some())
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpNatTypeDetector {
|
||||
@@ -491,6 +529,19 @@ impl UdpNatTypeDetector {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_extra_bind_result(
|
||||
&self,
|
||||
source_port: u16,
|
||||
stun_server: SocketAddr,
|
||||
) -> Result<BindRequestResponse, Error> {
|
||||
let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?);
|
||||
let client_builder = StunClientBuilder::new(udp.clone());
|
||||
client_builder
|
||||
.new_stun_client(stun_server)
|
||||
.bind_request(false, false)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn detect_nat_type(&self, source_port: u16) -> Result<UdpNatTypeDetectResult, Error> {
|
||||
let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?);
|
||||
self.detect_nat_type_with_socket(udp).await
|
||||
@@ -552,12 +603,15 @@ pub struct StunInfoCollector {
|
||||
udp_nat_test_result: Arc<RwLock<Option<UdpNatTypeDetectResult>>>,
|
||||
nat_test_result_time: Arc<AtomicCell<chrono::DateTime<Local>>>,
|
||||
redetect_notify: Arc<tokio::sync::Notify>,
|
||||
tasks: JoinSet<()>,
|
||||
tasks: std::sync::Mutex<JoinSet<()>>,
|
||||
started: AtomicBool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
self.start_stun_routine();
|
||||
|
||||
let Some(result) = self.udp_nat_test_result.read().unwrap().clone() else {
|
||||
return Default::default();
|
||||
};
|
||||
@@ -572,13 +626,30 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, local_port: u16) -> Result<SocketAddr, Error> {
|
||||
let stun_servers = self
|
||||
self.start_stun_routine();
|
||||
|
||||
let mut stun_servers = self
|
||||
.udp_nat_test_result
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.map(|x| x.collect_available_stun_server())
|
||||
.ok_or(Error::NotFound)?;
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
if stun_servers.is_empty() {
|
||||
let mut host_resolver =
|
||||
HostResolverIter::new(self.stun_servers.read().unwrap().clone(), 2);
|
||||
while let Some(addr) = host_resolver.next().await {
|
||||
stun_servers.push(addr);
|
||||
if stun_servers.len() >= 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stun_servers.is_empty() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let udp = Arc::new(UdpSocket::bind(format!("0.0.0.0:{}", local_port)).await?);
|
||||
let mut client_builder = StunClientBuilder::new(udp.clone());
|
||||
@@ -605,17 +676,14 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
||||
|
||||
impl StunInfoCollector {
|
||||
pub fn new(stun_servers: Vec<String>) -> Self {
|
||||
let mut ret = Self {
|
||||
Self {
|
||||
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
||||
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
||||
redetect_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
tasks: JoinSet::new(),
|
||||
};
|
||||
|
||||
ret.start_stun_routine();
|
||||
|
||||
ret
|
||||
tasks: std::sync::Mutex::new(JoinSet::new()),
|
||||
started: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_default_servers() -> Self {
|
||||
@@ -627,9 +695,9 @@ impl StunInfoCollector {
|
||||
// stun server cross nation may return a external ip address with high latency and loss rate
|
||||
vec![
|
||||
"stun.miwifi.com",
|
||||
"stun.cdnbye.com",
|
||||
"stun.hitv.com",
|
||||
"stun.chat.bilibili.com",
|
||||
"stun.hitv.com",
|
||||
"stun.cdnbye.com",
|
||||
"stun.douyucdn.cn:18000",
|
||||
"fwa.lifesizecloud.com",
|
||||
"global.turn.twilio.com",
|
||||
@@ -648,12 +716,18 @@ impl StunInfoCollector {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn start_stun_routine(&mut self) {
|
||||
fn start_stun_routine(&self) {
|
||||
if self.started.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
self.started
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let stun_servers = self.stun_servers.clone();
|
||||
let udp_nat_test_result = self.udp_nat_test_result.clone();
|
||||
let udp_test_time = self.nat_test_result_time.clone();
|
||||
let redetect_notify = self.redetect_notify.clone();
|
||||
self.tasks.spawn(async move {
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
let servers = stun_servers.read().unwrap().clone();
|
||||
// use first three and random choose one from the rest
|
||||
@@ -664,38 +738,41 @@ impl StunInfoCollector {
|
||||
.map(|x| x.to_string())
|
||||
.collect();
|
||||
let detector = UdpNatTypeDetector::new(servers, 1);
|
||||
let ret = detector.detect_nat_type(0).await;
|
||||
let mut ret = detector.detect_nat_type(0).await;
|
||||
tracing::debug!(?ret, "finish udp nat type detect");
|
||||
|
||||
let mut nat_type = NatType::Unknown;
|
||||
let sleep_sec = match &ret {
|
||||
Ok(resp) => {
|
||||
*udp_nat_test_result.write().unwrap() = Some(resp.clone());
|
||||
udp_test_time.store(Local::now());
|
||||
nat_type = resp.nat_type();
|
||||
if nat_type == NatType::Unknown {
|
||||
15
|
||||
} else {
|
||||
600
|
||||
}
|
||||
}
|
||||
_ => 15,
|
||||
};
|
||||
if let Ok(resp) = &ret {
|
||||
tracing::debug!(?resp, "got udp nat type detect result");
|
||||
nat_type = resp.nat_type();
|
||||
}
|
||||
|
||||
// if nat type is symmtric, detect with another port to gather more info
|
||||
if nat_type == NatType::Symmetric {
|
||||
let old_resp = ret.unwrap();
|
||||
let old_local_port = old_resp.local_addr().port();
|
||||
let new_port = if old_local_port >= 65535 {
|
||||
old_local_port - 1
|
||||
} else {
|
||||
old_local_port + 1
|
||||
};
|
||||
let ret = detector.detect_nat_type(new_port).await;
|
||||
tracing::debug!(?ret, "finish udp nat type detect with another port");
|
||||
if let Ok(resp) = ret {
|
||||
udp_nat_test_result.write().unwrap().as_mut().map(|x| {
|
||||
x.extend_result(resp);
|
||||
});
|
||||
let old_resp = ret.as_mut().unwrap();
|
||||
tracing::debug!(?old_resp, "start get extra bind result");
|
||||
let available_stun_servers = old_resp.collect_available_stun_server();
|
||||
for server in available_stun_servers.iter() {
|
||||
let ret = detector
|
||||
.get_extra_bind_result(0, *server)
|
||||
.await
|
||||
.with_context(|| "get extra bind result failed");
|
||||
tracing::debug!(?ret, "finish udp nat type detect with another port");
|
||||
if let Ok(resp) = ret {
|
||||
old_resp.extra_bind_test = Some(resp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sleep_sec = 10;
|
||||
if let Ok(resp) = &ret {
|
||||
udp_test_time.store(Local::now());
|
||||
*udp_nat_test_result.write().unwrap() = Some(resp.clone());
|
||||
if nat_type != NatType::Unknown
|
||||
&& (nat_type != NatType::Symmetric || resp.extra_bind_test.is_some())
|
||||
{
|
||||
sleep_sec = 600
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +789,31 @@ impl StunInfoCollector {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockStunInfoCollector {
|
||||
pub udp_nat_type: NatType,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl StunInfoCollectorTrait for MockStunInfoCollector {
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
StunInfo {
|
||||
udp_nat_type: self.udp_nat_type as i32,
|
||||
tcp_nat_type: NatType::Unknown as i32,
|
||||
last_update_time: std::time::Instant::now().elapsed().as_secs() as i64,
|
||||
min_port: 100,
|
||||
max_port: 200,
|
||||
public_ip: vec!["127.0.0.1".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, mut port: u16) -> Result<std::net::SocketAddr, Error> {
|
||||
if port == 0 {
|
||||
port = 40144;
|
||||
}
|
||||
Ok(format!("127.0.0.1:{}", port).parse().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
// try connect peers directly, with either its public ip or lan ip
|
||||
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
|
||||
peers::{peer_manager::PeerManager, peer_rpc::PeerRpcManager},
|
||||
peers::{
|
||||
peer_manager::PeerManager, peer_rpc::PeerRpcManager,
|
||||
peer_rpc_service::DirectConnectorManagerRpcServer,
|
||||
},
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
DirectConnectorRpc, DirectConnectorRpcClientFactory, DirectConnectorRpcServer,
|
||||
GetIpListRequest, GetIpListResponse,
|
||||
},
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::rpc::{peer::GetIpListResponse, PeerConnInfo};
|
||||
use crate::proto::cli::PeerConnInfo;
|
||||
use anyhow::Context;
|
||||
use rand::Rng;
|
||||
use tokio::{task::JoinSet, time::timeout};
|
||||
use tracing::Instrument;
|
||||
use url::Host;
|
||||
@@ -17,11 +29,6 @@ use super::create_connector_by_url;
|
||||
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
|
||||
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 300;
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait DirectConnectorRpc {
|
||||
async fn get_ip_list() -> GetIpListResponse;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait PeerManagerForDirectConnector {
|
||||
async fn list_peers(&self) -> Vec<PeerId>;
|
||||
@@ -35,7 +42,10 @@ impl PeerManagerForDirectConnector for PeerManager {
|
||||
let mut ret = vec![];
|
||||
|
||||
let routes = self.list_routes().await;
|
||||
for r in routes.iter() {
|
||||
for r in routes
|
||||
.iter()
|
||||
.filter(|r| r.feature_flag.map(|r| !r.is_public_server).unwrap_or(true))
|
||||
{
|
||||
ret.push(r.peer_id);
|
||||
}
|
||||
|
||||
@@ -51,38 +61,17 @@ impl PeerManagerForDirectConnector for PeerManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DirectConnectorManagerRpcServer {
|
||||
// TODO: this only cache for one src peer, should make it global
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
|
||||
async fn get_ip_list(self, _: tarpc::context::Context) -> GetIpListResponse {
|
||||
let mut ret = self.global_ctx.get_ip_collector().collect_ip_addrs().await;
|
||||
ret.listeners = self.global_ctx.get_running_listeners();
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectConnectorManagerRpcServer {
|
||||
pub fn new(global_ctx: ArcGlobalCtx) -> Self {
|
||||
Self { global_ctx }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone)]
|
||||
struct DstBlackListItem(PeerId, String);
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone)]
|
||||
struct DstSchemeBlackListItem(PeerId, String);
|
||||
struct DstListenerUrlBlackListItem(PeerId, url::Url);
|
||||
|
||||
struct DirectConnectorManagerData {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
|
||||
dst_sceme_blacklist: timedmap::TimedMap<DstSchemeBlackListItem, ()>,
|
||||
dst_listener_blacklist: timedmap::TimedMap<DstListenerUrlBlackListItem, ()>,
|
||||
}
|
||||
|
||||
impl DirectConnectorManagerData {
|
||||
@@ -91,7 +80,7 @@ impl DirectConnectorManagerData {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
dst_blacklist: timedmap::TimedMap::new(),
|
||||
dst_sceme_blacklist: timedmap::TimedMap::new(),
|
||||
dst_listener_blacklist: timedmap::TimedMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,15 +110,26 @@ impl DirectConnectorManager {
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
if self.global_ctx.get_flags().disable_p2p {
|
||||
return;
|
||||
}
|
||||
|
||||
self.run_as_server();
|
||||
self.run_as_client();
|
||||
}
|
||||
|
||||
pub fn run_as_server(&mut self) {
|
||||
self.data.peer_manager.get_peer_rpc_mgr().run_service(
|
||||
DIRECT_CONNECTOR_SERVICE_ID,
|
||||
DirectConnectorManagerRpcServer::new(self.global_ctx.clone()).serve(),
|
||||
);
|
||||
self.data
|
||||
.peer_manager
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.register(
|
||||
DirectConnectorRpcServer::new(DirectConnectorManagerRpcServer::new(
|
||||
self.global_ctx.clone(),
|
||||
)),
|
||||
&self.data.global_ctx.get_network_name(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn run_as_client(&mut self) {
|
||||
@@ -148,7 +148,7 @@ impl DirectConnectorManager {
|
||||
}
|
||||
|
||||
while let Some(task_ret) = tasks.join_next().await {
|
||||
tracing::trace!(?task_ret, "direct connect task ret");
|
||||
tracing::debug!(?task_ret, ?my_peer_id, "direct connect task ret");
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
@@ -169,7 +169,7 @@ impl DirectConnectorManager {
|
||||
.dst_blacklist
|
||||
.contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone()))
|
||||
{
|
||||
tracing::trace!("try_connect_to_ip failed, addr in blacklist: {}", addr);
|
||||
tracing::debug!("try_connect_to_ip failed, addr in blacklist: {}", addr);
|
||||
return Err(Error::UrlInBlacklist);
|
||||
}
|
||||
|
||||
@@ -204,24 +204,38 @@ impl DirectConnectorManager {
|
||||
dst_peer_id: PeerId,
|
||||
addr: String,
|
||||
) -> Result<(), Error> {
|
||||
let ret = Self::do_try_connect_to_ip(data.clone(), dst_peer_id, addr.clone()).await;
|
||||
if let Err(e) = ret {
|
||||
if !matches!(e, Error::UrlInBlacklist) {
|
||||
tracing::info!(
|
||||
"try_connect_to_ip failed: {:?}, peer_id: {}",
|
||||
e,
|
||||
dst_peer_id
|
||||
);
|
||||
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;
|
||||
}
|
||||
return Err(e);
|
||||
} else {
|
||||
tracing::info!("try_connect_to_ip success, peer_id: {}", dst_peer_id);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,21 +245,25 @@ impl DirectConnectorManager {
|
||||
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
|
||||
.iter()
|
||||
.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_sceme_blacklist.contains(&DstSchemeBlackListItem(
|
||||
dst_peer_id.clone(),
|
||||
l.scheme().to_string(),
|
||||
))
|
||||
!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
|
||||
@@ -264,46 +282,84 @@ impl DirectConnectorManager {
|
||||
Some(SocketAddr::V4(_)) => {
|
||||
ip_list.interface_ipv4s.iter().for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(ip.as_str())).is_ok() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(ip_list.public_ipv4.as_str())).is_ok() {
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SocketAddr::V6(_)) => {
|
||||
ip_list.interface_ipv6s.iter().for_each(|ip| {
|
||||
let mut addr = (*listener).clone();
|
||||
if addr.set_host(Some(format!("[{}]", ip).as_str())).is_ok() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let mut addr = (*listener).clone();
|
||||
if addr
|
||||
.set_host(Some(format!("[{}]", ip_list.public_ipv6).as_str()))
|
||||
.is_ok()
|
||||
{
|
||||
tasks.spawn(Self::try_connect_to_ip(
|
||||
data.clone(),
|
||||
dst_peer_id.clone(),
|
||||
addr.to_string(),
|
||||
));
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
p => {
|
||||
@@ -313,16 +369,28 @@ impl DirectConnectorManager {
|
||||
|
||||
let mut has_succ = false;
|
||||
while let Some(ret) = tasks.join_next().await {
|
||||
if let Err(e) = ret {
|
||||
tracing::error!("join direct connect task failed: {:?}", e);
|
||||
} else if let Ok(Ok(_)) = ret {
|
||||
has_succ = true;
|
||||
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_sceme_blacklist.insert(
|
||||
DstSchemeBlackListItem(dst_peer_id.clone(), listener.scheme().to_string()),
|
||||
data.dst_listener_blacklist.insert(
|
||||
DstListenerUrlBlackListItem(dst_peer_id.clone(), listener.clone()),
|
||||
(),
|
||||
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
|
||||
);
|
||||
@@ -345,18 +413,23 @@ impl DirectConnectorManager {
|
||||
}
|
||||
}
|
||||
|
||||
tracing::trace!("try direct connect to peer: {}", dst_peer_id);
|
||||
tracing::debug!("try direct connect to peer: {}", dst_peer_id);
|
||||
|
||||
let ip_list = peer_manager
|
||||
let rpc_stub = peer_manager
|
||||
.get_peer_rpc_mgr()
|
||||
.do_client_rpc_scoped(1, dst_peer_id, |c| async {
|
||||
let client =
|
||||
DirectConnectorRpcClient::new(tarpc::client::Config::default(), c).spawn();
|
||||
let ip_list = client.get_ip_list(tarpc::context::current()).await;
|
||||
tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list");
|
||||
ip_list
|
||||
})
|
||||
.await?;
|
||||
.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
|
||||
}
|
||||
@@ -369,14 +442,14 @@ mod tests {
|
||||
use crate::{
|
||||
connector::direct::{
|
||||
DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem,
|
||||
DstSchemeBlackListItem,
|
||||
DstListenerUrlBlackListItem,
|
||||
},
|
||||
instance::listeners::ListenerManager,
|
||||
peers::tests::{
|
||||
connect_peer_manager, create_mock_peer_manager, wait_route_appear,
|
||||
wait_route_appear_with_cost,
|
||||
},
|
||||
rpc::peer::GetIpListResponse,
|
||||
proto::peer_rpc::GetIpListResponse,
|
||||
};
|
||||
|
||||
#[rstest::rstest]
|
||||
@@ -432,20 +505,25 @@ mod tests {
|
||||
p_a.get_global_ctx(),
|
||||
p_a.clone(),
|
||||
));
|
||||
let mut ip_list = GetIpListResponse::new();
|
||||
let mut ip_list = GetIpListResponse::default();
|
||||
ip_list
|
||||
.listeners
|
||||
.push("tcp://127.0.0.1:10222".parse().unwrap());
|
||||
|
||||
ip_list.interface_ipv4s.push("127.0.0.1".to_string());
|
||||
ip_list
|
||||
.interface_ipv4s
|
||||
.push("127.0.0.1".parse::<std::net::Ipv4Addr>().unwrap().into());
|
||||
|
||||
DirectConnectorManager::do_try_direct_connect_internal(data.clone(), 1, ip_list.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(data
|
||||
.dst_sceme_blacklist
|
||||
.contains(&DstSchemeBlackListItem(1, "tcp".into())));
|
||||
.dst_listener_blacklist
|
||||
.contains(&DstListenerUrlBlackListItem(
|
||||
1,
|
||||
"tcp://127.0.0.1:10222".parse().unwrap()
|
||||
)));
|
||||
|
||||
assert!(data
|
||||
.dst_blacklist
|
||||
|
||||
@@ -11,7 +11,12 @@ use tokio::{
|
||||
use crate::{
|
||||
common::PeerId,
|
||||
peers::peer_conn::PeerConnId,
|
||||
rpc as easytier_rpc,
|
||||
proto::{
|
||||
cli::{
|
||||
ConnectorManageAction, ListConnectorResponse, ManageConnectorResponse, PeerConnInfo,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{IpVersion, TunnelConnector},
|
||||
};
|
||||
|
||||
@@ -23,9 +28,9 @@ use crate::{
|
||||
},
|
||||
connector::set_bind_addr_for_peer_connector,
|
||||
peers::peer_manager::PeerManager,
|
||||
rpc::{
|
||||
connector_manage_rpc_server::ConnectorManageRpc, Connector, ConnectorStatus,
|
||||
ListConnectorRequest, ManageConnectorRequest,
|
||||
proto::cli::{
|
||||
Connector, ConnectorManageRpc, ConnectorStatus, ListConnectorRequest,
|
||||
ManageConnectorRequest,
|
||||
},
|
||||
use_global_var,
|
||||
};
|
||||
@@ -46,7 +51,7 @@ struct ConnectorManagerData {
|
||||
connectors: ConnectorMap,
|
||||
reconnecting: DashSet<String>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
alive_conn_urls: Arc<Mutex<BTreeSet<String>>>,
|
||||
alive_conn_urls: Arc<DashSet<String>>,
|
||||
// user removed connector urls
|
||||
removed_conn_urls: Arc<DashSet<String>>,
|
||||
net_ns: NetNS,
|
||||
@@ -71,7 +76,7 @@ impl ManualConnectorManager {
|
||||
connectors,
|
||||
reconnecting: DashSet::new(),
|
||||
peer_manager,
|
||||
alive_conn_urls: Arc::new(Mutex::new(BTreeSet::new())),
|
||||
alive_conn_urls: Arc::new(DashSet::new()),
|
||||
removed_conn_urls: Arc::new(DashSet::new()),
|
||||
net_ns: global_ctx.net_ns.clone(),
|
||||
global_ctx,
|
||||
@@ -80,7 +85,11 @@ impl ManualConnectorManager {
|
||||
};
|
||||
|
||||
ret.tasks
|
||||
.spawn(Self::conn_mgr_routine(ret.data.clone(), event_subscriber));
|
||||
.spawn(Self::conn_mgr_reconn_routine(ret.data.clone()));
|
||||
ret.tasks.spawn(Self::conn_mgr_handle_event_routine(
|
||||
ret.data.clone(),
|
||||
event_subscriber,
|
||||
));
|
||||
|
||||
ret
|
||||
}
|
||||
@@ -101,12 +110,18 @@ impl ManualConnectorManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_connector(&self, url: &str) -> Result<(), Error> {
|
||||
pub async fn remove_connector(&self, url: url::Url) -> Result<(), Error> {
|
||||
tracing::info!("remove_connector: {}", url);
|
||||
if !self.list_connectors().await.iter().any(|x| x.url == url) {
|
||||
let url = url.into();
|
||||
if !self
|
||||
.list_connectors()
|
||||
.await
|
||||
.iter()
|
||||
.any(|x| x.url.as_ref() == Some(&url))
|
||||
{
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
self.data.removed_conn_urls.insert(url.into());
|
||||
self.data.removed_conn_urls.insert(url.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -133,7 +148,7 @@ impl ManualConnectorManager {
|
||||
ret.insert(
|
||||
0,
|
||||
Connector {
|
||||
url: conn_url,
|
||||
url: Some(conn_url.parse().unwrap()),
|
||||
status: status.into(),
|
||||
},
|
||||
);
|
||||
@@ -150,7 +165,7 @@ impl ManualConnectorManager {
|
||||
ret.insert(
|
||||
0,
|
||||
Connector {
|
||||
url: conn_url,
|
||||
url: Some(conn_url.parse().unwrap()),
|
||||
status: ConnectorStatus::Connecting.into(),
|
||||
},
|
||||
);
|
||||
@@ -159,10 +174,17 @@ impl ManualConnectorManager {
|
||||
ret
|
||||
}
|
||||
|
||||
async fn conn_mgr_routine(
|
||||
async fn conn_mgr_handle_event_routine(
|
||||
data: Arc<ConnectorManagerData>,
|
||||
mut event_recv: Receiver<GlobalCtxEvent>,
|
||||
) {
|
||||
loop {
|
||||
let event = event_recv.recv().await.expect("event_recv got error");
|
||||
Self::handle_event(&event, &data).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn conn_mgr_reconn_routine(data: Arc<ConnectorManagerData>) {
|
||||
tracing::warn!("conn_mgr_routine started");
|
||||
let mut reconn_interval = tokio::time::interval(std::time::Duration::from_millis(
|
||||
use_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS),
|
||||
@@ -171,15 +193,6 @@ impl ManualConnectorManager {
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = event_recv.recv() => {
|
||||
if let Ok(event) = event {
|
||||
Self::handle_event(&event, data.clone()).await;
|
||||
} else {
|
||||
tracing::warn!(?event, "event_recv got error");
|
||||
panic!("event_recv got error, err: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
_ = reconn_interval.tick() => {
|
||||
let dead_urls = Self::collect_dead_conns(data.clone()).await;
|
||||
if dead_urls.is_empty() {
|
||||
@@ -210,17 +223,24 @@ impl ManualConnectorManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(event: &GlobalCtxEvent, data: Arc<ConnectorManagerData>) {
|
||||
async fn handle_event(event: &GlobalCtxEvent, data: &ConnectorManagerData) {
|
||||
let need_add_alive = |conn_info: &PeerConnInfo| conn_info.is_client;
|
||||
match event {
|
||||
GlobalCtxEvent::PeerConnAdded(conn_info) => {
|
||||
if !need_add_alive(conn_info) {
|
||||
return;
|
||||
}
|
||||
let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone();
|
||||
data.alive_conn_urls.lock().await.insert(addr);
|
||||
data.alive_conn_urls.insert(addr.unwrap().to_string());
|
||||
tracing::warn!("peer conn added: {:?}", conn_info);
|
||||
}
|
||||
|
||||
GlobalCtxEvent::PeerConnRemoved(conn_info) => {
|
||||
if !need_add_alive(conn_info) {
|
||||
return;
|
||||
}
|
||||
let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone();
|
||||
data.alive_conn_urls.lock().await.remove(&addr);
|
||||
data.alive_conn_urls.remove(&addr.unwrap().to_string());
|
||||
tracing::warn!("peer conn removed: {:?}", conn_info);
|
||||
}
|
||||
|
||||
@@ -252,13 +272,18 @@ impl ManualConnectorManager {
|
||||
async fn collect_dead_conns(data: Arc<ConnectorManagerData>) -> BTreeSet<String> {
|
||||
Self::handle_remove_connector(data.clone());
|
||||
|
||||
let curr_alive = data.alive_conn_urls.lock().await.clone();
|
||||
let all_urls: BTreeSet<String> = data
|
||||
.connectors
|
||||
.iter()
|
||||
.map(|x| x.key().clone().into())
|
||||
.collect();
|
||||
&all_urls - &curr_alive
|
||||
let mut ret = BTreeSet::new();
|
||||
for url in all_urls.iter() {
|
||||
if !data.alive_conn_urls.contains(url) {
|
||||
ret.insert(url.clone());
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
async fn conn_reconnect_with_ip_version(
|
||||
@@ -289,7 +314,7 @@ impl ManualConnectorManager {
|
||||
tracing::info!("reconnect get tunnel succ: {:?}", tunnel);
|
||||
assert_eq!(
|
||||
dead_url,
|
||||
tunnel.info().unwrap().remote_addr,
|
||||
tunnel.info().unwrap().remote_addr.unwrap().to_string(),
|
||||
"info: {:?}",
|
||||
tunnel.info()
|
||||
);
|
||||
@@ -371,45 +396,43 @@ impl ManualConnectorManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectorManagerRpcService(pub Arc<ManualConnectorManager>);
|
||||
|
||||
#[tonic::async_trait]
|
||||
#[async_trait::async_trait]
|
||||
impl ConnectorManageRpc for ConnectorManagerRpcService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn list_connector(
|
||||
&self,
|
||||
_request: tonic::Request<ListConnectorRequest>,
|
||||
) -> Result<tonic::Response<easytier_rpc::ListConnectorResponse>, tonic::Status> {
|
||||
let mut ret = easytier_rpc::ListConnectorResponse::default();
|
||||
_: BaseController,
|
||||
_request: ListConnectorRequest,
|
||||
) -> Result<ListConnectorResponse, rpc_types::error::Error> {
|
||||
let mut ret = ListConnectorResponse::default();
|
||||
let connectors = self.0.list_connectors().await;
|
||||
ret.connectors = connectors;
|
||||
Ok(tonic::Response::new(ret))
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn manage_connector(
|
||||
&self,
|
||||
request: tonic::Request<ManageConnectorRequest>,
|
||||
) -> Result<tonic::Response<easytier_rpc::ManageConnectorResponse>, tonic::Status> {
|
||||
let req = request.into_inner();
|
||||
let url = url::Url::parse(&req.url)
|
||||
.map_err(|_| tonic::Status::invalid_argument("invalid url"))?;
|
||||
if req.action == easytier_rpc::ConnectorManageAction::Remove as i32 {
|
||||
self.0.remove_connector(url.path()).await.map_err(|e| {
|
||||
tonic::Status::invalid_argument(format!("remove connector failed: {:?}", e))
|
||||
})?;
|
||||
return Ok(tonic::Response::new(
|
||||
easytier_rpc::ManageConnectorResponse::default(),
|
||||
));
|
||||
_: BaseController,
|
||||
req: ManageConnectorRequest,
|
||||
) -> Result<ManageConnectorResponse, rpc_types::error::Error> {
|
||||
let url: url::Url = req.url.ok_or(anyhow::anyhow!("url is empty"))?.into();
|
||||
if req.action == ConnectorManageAction::Remove as i32 {
|
||||
self.0
|
||||
.remove_connector(url.clone())
|
||||
.await
|
||||
.with_context(|| format!("remove connector failed: {:?}", url))?;
|
||||
return Ok(ManageConnectorResponse::default());
|
||||
} else {
|
||||
self.0
|
||||
.add_connector_by_url(url.as_str())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::invalid_argument(format!("add connector failed: {:?}", e))
|
||||
})?;
|
||||
.with_context(|| format!("add connector failed: {:?}", url))?;
|
||||
}
|
||||
Ok(tonic::Response::new(
|
||||
easytier_rpc::ManageConnectorResponse::default(),
|
||||
))
|
||||
Ok(ManageConnectorResponse::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ async fn set_bind_addr_for_peer_connector(
|
||||
if is_ipv4 {
|
||||
let mut bind_addrs = vec![];
|
||||
for ipv4 in ips.interface_ipv4s {
|
||||
let socket_addr = SocketAddrV4::new(ipv4.parse().unwrap(), 0).into();
|
||||
let socket_addr = SocketAddrV4::new(ipv4.into(), 0).into();
|
||||
bind_addrs.push(socket_addr);
|
||||
}
|
||||
connector.set_bind_addrs(bind_addrs);
|
||||
} else {
|
||||
let mut bind_addrs = vec![];
|
||||
for ipv6 in ips.interface_ipv6s {
|
||||
let socket_addr = SocketAddrV6::new(ipv6.parse().unwrap(), 0, 0, 0).into();
|
||||
let socket_addr = SocketAddrV6::new(ipv6.into(), 0, 0, 0).into();
|
||||
bind_addrs.push(socket_addr);
|
||||
}
|
||||
connector.set_bind_addrs(bind_addrs);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
399
easytier/src/connector/udp_hole_punch/both_easy_sym.rs
Normal file
399
easytier/src/connector/udp_hole_punch/both_easy_sym.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr, SocketAddrV4},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId},
|
||||
connector::udp_hole_punch::common::{
|
||||
try_connect_with_socket, UdpHolePunchListener, HOLE_PUNCH_PACKET_BODY_LEN,
|
||||
},
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
SendPunchPacketBothEasySymRequest, SendPunchPacketBothEasySymResponse,
|
||||
UdpHolePunchRpcClientFactory,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{udp::new_hole_punch_packet, Tunnel},
|
||||
};
|
||||
|
||||
use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray};
|
||||
|
||||
const UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM: usize = 25;
|
||||
const DST_PORT_OFFSET: u16 = 20;
|
||||
const REMOTE_WAIT_TIME_MS: u64 = 5000;
|
||||
|
||||
pub(crate) struct PunchBothEasySymHoleServer {
|
||||
common: Arc<PunchHoleServerCommon>,
|
||||
task: Mutex<Option<ScopedTask<()>>>,
|
||||
}
|
||||
|
||||
impl PunchBothEasySymHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> Self {
|
||||
Self {
|
||||
common,
|
||||
task: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
// hard sym means public port is random and cannot be predicted
|
||||
#[tracing::instrument(skip(self), ret, err)]
|
||||
pub(crate) async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketBothEasySymRequest,
|
||||
) -> Result<SendPunchPacketBothEasySymResponse, rpc_types::error::Error> {
|
||||
tracing::info!("send_punch_packet_both_easy_sym start");
|
||||
let busy_resp = Ok(SendPunchPacketBothEasySymResponse {
|
||||
is_busy: true,
|
||||
..Default::default()
|
||||
});
|
||||
let Ok(mut locked_task) = self.task.try_lock() else {
|
||||
return busy_resp;
|
||||
};
|
||||
if locked_task.is_some() && !locked_task.as_ref().unwrap().is_finished() {
|
||||
return busy_resp;
|
||||
}
|
||||
|
||||
let global_ctx = self.common.get_global_ctx();
|
||||
let cur_mapped_addr = global_ctx
|
||||
.get_stun_info_collector()
|
||||
.get_udp_port_mapping(0)
|
||||
.await
|
||||
.with_context(|| "failed to get udp port mapping")?;
|
||||
|
||||
tracing::info!("send_punch_packet_hard_sym start");
|
||||
let socket_count = request.udp_socket_count as usize;
|
||||
let public_ips = request
|
||||
.public_ip
|
||||
.ok_or(anyhow::anyhow!("public_ip is required"))?;
|
||||
let transaction_id = request.transaction_id;
|
||||
|
||||
let udp_array =
|
||||
UdpSocketArray::new(socket_count, self.common.get_global_ctx().net_ns.clone());
|
||||
udp_array.start().await?;
|
||||
udp_array.add_intreast_tid(transaction_id);
|
||||
let peer_mgr = self.common.get_peer_mgr();
|
||||
|
||||
let punch_packet =
|
||||
new_hole_punch_packet(transaction_id, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
|
||||
let mut punched = vec![];
|
||||
let common = self.common.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let mut listeners = Vec::new();
|
||||
let start_time = Instant::now();
|
||||
let wait_time_ms = request.wait_time_ms.min(8000);
|
||||
while start_time.elapsed() < Duration::from_millis(wait_time_ms as u64) {
|
||||
if let Err(e) = udp_array
|
||||
.send_with_all(
|
||||
&punch_packet,
|
||||
SocketAddr::V4(SocketAddrV4::new(
|
||||
public_ips.into(),
|
||||
request.dst_port_num as u16,
|
||||
)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(?e, "failed to send hole punch packet");
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if let Some(s) = udp_array.try_fetch_punched_socket(transaction_id) {
|
||||
tracing::info!(?s, ?transaction_id, "got punched socket in both easy sym");
|
||||
assert!(Arc::strong_count(&s.socket) == 1);
|
||||
let Some(port) = s.socket.local_addr().ok().map(|addr| addr.port()) else {
|
||||
tracing::warn!("failed to get local addr from punched socket");
|
||||
continue;
|
||||
};
|
||||
let remote_addr = s.remote_addr;
|
||||
drop(s);
|
||||
|
||||
let listener =
|
||||
match UdpHolePunchListener::new_ext(peer_mgr.clone(), false, Some(port))
|
||||
.await
|
||||
{
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
tracing::warn!(?e, "failed to create listener");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
punched.push((listener.get_socket().await, remote_addr));
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
// if any listener is punched, we can break the loop
|
||||
for l in &listeners {
|
||||
if l.get_conn_count().await > 0 {
|
||||
tracing::info!(?l, "got punched listener");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !punched.is_empty() {
|
||||
tracing::debug!(?punched, "got punched socket and keep sending punch packet");
|
||||
}
|
||||
|
||||
for p in &punched {
|
||||
let (socket, remote_addr) = p;
|
||||
let send_remote_ret = socket.send_to(&punch_packet, remote_addr).await;
|
||||
tracing::debug!(
|
||||
?send_remote_ret,
|
||||
?socket,
|
||||
"send hole punch packet to punched remote"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for l in listeners {
|
||||
if l.get_conn_count().await > 0 {
|
||||
common.add_listener(l).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*locked_task = Some(task.into());
|
||||
return Ok(SendPunchPacketBothEasySymResponse {
|
||||
is_busy: false,
|
||||
base_mapped_addr: Some(cur_mapped_addr.into()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PunchBothEasySymHoleClient {
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
impl PunchBothEasySymHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
Self { peer_mgr }
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret)]
|
||||
pub(crate) async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
my_nat_info: UdpNatType,
|
||||
peer_nat_info: UdpNatType,
|
||||
is_busy: &mut bool,
|
||||
) -> Result<Option<Box<dyn Tunnel>>, anyhow::Error> {
|
||||
*is_busy = false;
|
||||
|
||||
let udp_array = UdpSocketArray::new(
|
||||
UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM,
|
||||
self.peer_mgr.get_global_ctx().net_ns.clone(),
|
||||
);
|
||||
udp_array.start().await?;
|
||||
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
let cur_mapped_addr = global_ctx
|
||||
.get_stun_info_collector()
|
||||
.get_udp_port_mapping(0)
|
||||
.await
|
||||
.with_context(|| "failed to get udp port mapping")?;
|
||||
let my_public_ip = match cur_mapped_addr.ip() {
|
||||
IpAddr::V4(v4) => v4,
|
||||
_ => {
|
||||
anyhow::bail!("ipv6 is not supported");
|
||||
}
|
||||
};
|
||||
let me_is_incremental = my_nat_info
|
||||
.get_inc_of_easy_sym()
|
||||
.ok_or(anyhow::anyhow!("me_is_incremental is required"))?;
|
||||
let peer_is_incremental = peer_nat_info
|
||||
.get_inc_of_easy_sym()
|
||||
.ok_or(anyhow::anyhow!("peer_is_incremental is required"))?;
|
||||
|
||||
let rpc_stub = self
|
||||
.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_client()
|
||||
.scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
self.peer_mgr.my_peer_id(),
|
||||
dst_peer_id,
|
||||
global_ctx.get_network_name(),
|
||||
);
|
||||
|
||||
let tid = rand::random();
|
||||
udp_array.add_intreast_tid(tid);
|
||||
|
||||
let remote_ret = rpc_stub
|
||||
.send_punch_packet_both_easy_sym(
|
||||
BaseController {
|
||||
timeout_ms: 2000,
|
||||
..Default::default()
|
||||
},
|
||||
SendPunchPacketBothEasySymRequest {
|
||||
transaction_id: tid,
|
||||
public_ip: Some(my_public_ip.into()),
|
||||
dst_port_num: if me_is_incremental {
|
||||
cur_mapped_addr.port().saturating_add(DST_PORT_OFFSET)
|
||||
} else {
|
||||
cur_mapped_addr.port().saturating_sub(DST_PORT_OFFSET)
|
||||
} as u32,
|
||||
udp_socket_count: UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM as u32,
|
||||
wait_time_ms: REMOTE_WAIT_TIME_MS as u32,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if remote_ret.is_busy {
|
||||
*is_busy = true;
|
||||
anyhow::bail!("remote is busy");
|
||||
}
|
||||
|
||||
let mut remote_mapped_addr = remote_ret
|
||||
.base_mapped_addr
|
||||
.ok_or(anyhow::anyhow!("remote_mapped_addr is required"))?;
|
||||
|
||||
let now = Instant::now();
|
||||
remote_mapped_addr.port = if peer_is_incremental {
|
||||
remote_mapped_addr
|
||||
.port
|
||||
.saturating_add(DST_PORT_OFFSET as u32)
|
||||
} else {
|
||||
remote_mapped_addr
|
||||
.port
|
||||
.saturating_sub(DST_PORT_OFFSET as u32)
|
||||
};
|
||||
tracing::debug!(
|
||||
?remote_mapped_addr,
|
||||
?remote_ret,
|
||||
"start send hole punch packet for both easy sym"
|
||||
);
|
||||
|
||||
while now.elapsed().as_millis() < (REMOTE_WAIT_TIME_MS + 1000).into() {
|
||||
udp_array
|
||||
.send_with_all(
|
||||
&new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(),
|
||||
remote_mapped_addr.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::trace!(
|
||||
?remote_mapped_addr,
|
||||
?tid,
|
||||
"no punched socket found, send some more hole punch packets"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
?socket,
|
||||
?remote_mapped_addr,
|
||||
?tid,
|
||||
"got punched socket in both easy sym"
|
||||
);
|
||||
|
||||
for _ in 0..2 {
|
||||
match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into())
|
||||
.await
|
||||
{
|
||||
Ok(tunnel) => {
|
||||
return Ok(Some(tunnel));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
udp_array.add_new_socket(socket.socket).await?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::{
|
||||
sync::{atomic::AtomicU32, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::connector::udp_hole_punch::RUN_TESTING;
|
||||
use crate::{
|
||||
connector::udp_hole_punch::{
|
||||
tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector,
|
||||
},
|
||||
peers::tests::{connect_peer_manager, wait_route_appear},
|
||||
proto::common::NatType,
|
||||
tunnel::common::tests::wait_for_condition,
|
||||
};
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(hole_punch)]
|
||||
async fn hole_punching_easy_sym(#[values("true", "false")] is_inc: bool) {
|
||||
RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let p_a = create_mock_peer_manager_with_mock_stun(if is_inc {
|
||||
NatType::SymmetricEasyInc
|
||||
} else {
|
||||
NatType::SymmetricEasyDec
|
||||
})
|
||||
.await;
|
||||
let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
let p_c = create_mock_peer_manager_with_mock_stun(if !is_inc {
|
||||
NatType::SymmetricEasyInc
|
||||
} else {
|
||||
NatType::SymmetricEasyDec
|
||||
})
|
||||
.await;
|
||||
connect_peer_manager(p_a.clone(), p_b.clone()).await;
|
||||
connect_peer_manager(p_b.clone(), p_c.clone()).await;
|
||||
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
||||
|
||||
let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone());
|
||||
let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone());
|
||||
|
||||
hole_punching_a.run().await.unwrap();
|
||||
hole_punching_c.run().await.unwrap();
|
||||
|
||||
// 144 + DST_PORT_OFFSET = 164
|
||||
let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40164").await.unwrap());
|
||||
// 144 - DST_PORT_OFFSET = 124
|
||||
let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40124").await.unwrap());
|
||||
let udps = vec![udp1, udp2];
|
||||
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// all these sockets should receive hole punching packet
|
||||
for udp in udps.iter().map(Arc::clone) {
|
||||
let counter = counter.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = [0u8; 1024];
|
||||
let (len, addr) = udp.recv_from(&mut buf).await.unwrap();
|
||||
println!(
|
||||
"got predictable punch packet, {:?} {:?} {:?}",
|
||||
len,
|
||||
addr,
|
||||
udp.local_addr()
|
||||
);
|
||||
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
|
||||
hole_punching_a.client.run_immediately().await;
|
||||
let udp_len = udps.len();
|
||||
wait_for_condition(
|
||||
|| async { counter.load(std::sync::atomic::Ordering::Relaxed) == udp_len as u32 },
|
||||
Duration::from_secs(30),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
589
easytier/src/connector/udp_hole_punch/common.rs
Normal file
589
easytier/src/connector/udp_hole_punch/common.rs
Normal file
@@ -0,0 +1,589 @@
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use rand::seq::SliceRandom as _;
|
||||
use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet};
|
||||
use tracing::{instrument, Instrument, Level};
|
||||
use zerocopy::FromBytes as _;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error, global_ctx::ArcGlobalCtx, join_joinset_background, netns::NetNS,
|
||||
stun::StunInfoCollectorTrait as _, PeerId,
|
||||
},
|
||||
defer,
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::common::NatType,
|
||||
tunnel::{
|
||||
packet_def::{UDPTunnelHeader, UdpPacketType, UDP_TUNNEL_HEADER_SIZE},
|
||||
udp::{new_hole_punch_packet, UdpTunnelConnector, UdpTunnelListener},
|
||||
Tunnel, TunnelConnCounter, TunnelListener as _,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) const HOLE_PUNCH_PACKET_BODY_LEN: u16 = 16;
|
||||
|
||||
fn generate_shuffled_port_vec() -> Vec<u16> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut port_vec: Vec<u16> = (1..=65535).collect();
|
||||
port_vec.shuffle(&mut rng);
|
||||
port_vec
|
||||
}
|
||||
|
||||
pub(crate) enum UdpPunchClientMethod {
|
||||
None,
|
||||
ConeToCone,
|
||||
SymToCone,
|
||||
EasySymToEasySym,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum UdpNatType {
|
||||
Unknown,
|
||||
Open(NatType),
|
||||
Cone(NatType),
|
||||
// bool means if it is incremental
|
||||
EasySymmetric(NatType, bool),
|
||||
HardSymmetric(NatType),
|
||||
}
|
||||
|
||||
impl From<NatType> for UdpNatType {
|
||||
fn from(nat_type: NatType) -> Self {
|
||||
match nat_type {
|
||||
NatType::Unknown => UdpNatType::Unknown,
|
||||
NatType::NoPat | NatType::OpenInternet => UdpNatType::Open(nat_type),
|
||||
NatType::FullCone | NatType::Restricted | NatType::PortRestricted => {
|
||||
UdpNatType::Cone(nat_type)
|
||||
}
|
||||
NatType::Symmetric | NatType::SymUdpFirewall => UdpNatType::HardSymmetric(nat_type),
|
||||
NatType::SymmetricEasyInc => UdpNatType::EasySymmetric(nat_type, true),
|
||||
NatType::SymmetricEasyDec => UdpNatType::EasySymmetric(nat_type, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NatType> for UdpNatType {
|
||||
fn into(self) -> NatType {
|
||||
match self {
|
||||
UdpNatType::Unknown => NatType::Unknown,
|
||||
UdpNatType::Open(nat_type) => nat_type,
|
||||
UdpNatType::Cone(nat_type) => nat_type,
|
||||
UdpNatType::EasySymmetric(nat_type, _) => nat_type,
|
||||
UdpNatType::HardSymmetric(nat_type) => nat_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpNatType {
|
||||
pub(crate) fn is_open(&self) -> bool {
|
||||
matches!(self, UdpNatType::Open(_))
|
||||
}
|
||||
|
||||
pub(crate) fn is_unknown(&self) -> bool {
|
||||
matches!(self, UdpNatType::Unknown)
|
||||
}
|
||||
|
||||
pub(crate) fn is_sym(&self) -> bool {
|
||||
self.is_hard_sym() || self.is_easy_sym()
|
||||
}
|
||||
|
||||
pub(crate) fn is_hard_sym(&self) -> bool {
|
||||
matches!(self, UdpNatType::HardSymmetric(_))
|
||||
}
|
||||
|
||||
pub(crate) fn is_easy_sym(&self) -> bool {
|
||||
matches!(self, UdpNatType::EasySymmetric(_, _))
|
||||
}
|
||||
|
||||
pub(crate) fn is_cone(&self) -> bool {
|
||||
matches!(self, UdpNatType::Cone(_))
|
||||
}
|
||||
|
||||
pub(crate) fn get_inc_of_easy_sym(&self) -> Option<bool> {
|
||||
match self {
|
||||
UdpNatType::EasySymmetric(_, inc) => Some(*inc),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_punch_hole_method(&self, other: Self) -> UdpPunchClientMethod {
|
||||
if other.is_unknown() {
|
||||
if self.is_sym() {
|
||||
return UdpPunchClientMethod::SymToCone;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_unknown() {
|
||||
if other.is_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_open() || other.is_open() {
|
||||
// open nat does not need to punch hole
|
||||
return UdpPunchClientMethod::None;
|
||||
}
|
||||
|
||||
if self.is_cone() {
|
||||
if other.is_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
} else if self.is_easy_sym() {
|
||||
if other.is_hard_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else if other.is_easy_sym() {
|
||||
return UdpPunchClientMethod::EasySymToEasySym;
|
||||
} else {
|
||||
return UdpPunchClientMethod::SymToCone;
|
||||
}
|
||||
} else if self.is_hard_sym() {
|
||||
if other.is_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else {
|
||||
return UdpPunchClientMethod::SymToCone;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!("invalid nat type");
|
||||
}
|
||||
|
||||
pub(crate) fn can_punch_hole_as_client(
|
||||
&self,
|
||||
other: Self,
|
||||
my_peer_id: PeerId,
|
||||
dst_peer_id: PeerId,
|
||||
) -> bool {
|
||||
match self.get_punch_hole_method(other) {
|
||||
UdpPunchClientMethod::None => false,
|
||||
UdpPunchClientMethod::ConeToCone | UdpPunchClientMethod::SymToCone => true,
|
||||
UdpPunchClientMethod::EasySymToEasySym => my_peer_id < dst_peer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PunchedUdpSocket {
|
||||
pub(crate) socket: Arc<UdpSocket>,
|
||||
pub(crate) tid: u32,
|
||||
pub(crate) remote_addr: SocketAddr,
|
||||
}
|
||||
|
||||
// used for symmetric hole punching, binding to multiple ports to increase the chance of success
|
||||
pub(crate) struct UdpSocketArray {
|
||||
sockets: Arc<DashMap<SocketAddr, Arc<UdpSocket>>>,
|
||||
max_socket_count: usize,
|
||||
net_ns: NetNS,
|
||||
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
intreast_tids: Arc<DashSet<u32>>,
|
||||
tid_to_socket: Arc<DashMap<u32, Vec<PunchedUdpSocket>>>,
|
||||
}
|
||||
|
||||
impl UdpSocketArray {
|
||||
pub fn new(max_socket_count: usize, net_ns: NetNS) -> Self {
|
||||
let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new()));
|
||||
join_joinset_background(tasks.clone(), "UdpSocketArray".to_owned());
|
||||
|
||||
Self {
|
||||
sockets: Arc::new(DashMap::new()),
|
||||
max_socket_count,
|
||||
net_ns,
|
||||
tasks,
|
||||
|
||||
intreast_tids: Arc::new(DashSet::new()),
|
||||
tid_to_socket: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn started(&self) -> bool {
|
||||
!self.sockets.is_empty()
|
||||
}
|
||||
|
||||
pub async fn add_new_socket(&self, socket: Arc<UdpSocket>) -> Result<(), anyhow::Error> {
|
||||
let socket_map = self.sockets.clone();
|
||||
let local_addr = socket.local_addr()?;
|
||||
let intreast_tids = self.intreast_tids.clone();
|
||||
let tid_to_socket = self.tid_to_socket.clone();
|
||||
socket_map.insert(local_addr, socket.clone());
|
||||
self.tasks.lock().unwrap().spawn(
|
||||
async move {
|
||||
defer!(socket_map.remove(&local_addr););
|
||||
let mut buf = [0u8; UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize];
|
||||
tracing::trace!(?local_addr, "udp socket added");
|
||||
loop {
|
||||
let Ok((len, addr)) = socket.recv_from(&mut buf).await else {
|
||||
break;
|
||||
};
|
||||
|
||||
tracing::debug!(?len, ?addr, "got raw packet");
|
||||
|
||||
if len != UDP_TUNNEL_HEADER_SIZE + HOLE_PUNCH_PACKET_BODY_LEN as usize {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(p) = UDPTunnelHeader::ref_from_prefix(&buf) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let tid = p.conn_id.get();
|
||||
let valid = p.msg_type == UdpPacketType::HolePunch as u8
|
||||
&& p.len.get() == HOLE_PUNCH_PACKET_BODY_LEN;
|
||||
tracing::debug!(?p, ?addr, ?tid, ?valid, ?p, "got udp hole punch packet");
|
||||
|
||||
if !valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
if intreast_tids.contains(&tid) {
|
||||
tracing::info!(?addr, ?tid, "got hole punching packet with intreast tid");
|
||||
tid_to_socket
|
||||
.entry(tid)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(PunchedUdpSocket {
|
||||
socket: socket.clone(),
|
||||
tid,
|
||||
remote_addr: addr,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
tracing::debug!(?local_addr, "udp socket recv loop end");
|
||||
}
|
||||
.instrument(tracing::info_span!("udp array socket recv loop")),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn start(&self) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("starting udp socket array");
|
||||
|
||||
while self.sockets.len() < self.max_socket_count {
|
||||
let socket = {
|
||||
let _g = self.net_ns.guard();
|
||||
Arc::new(UdpSocket::bind("0.0.0.0:0").await?)
|
||||
};
|
||||
|
||||
self.add_new_socket(socket).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn send_with_all(&self, data: &[u8], addr: SocketAddr) -> Result<(), anyhow::Error> {
|
||||
tracing::info!(?addr, "sending hole punching packet");
|
||||
|
||||
for socket in self.sockets.iter() {
|
||||
let socket = socket.value();
|
||||
socket.send_to(data, addr).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(ret(level = Level::DEBUG))]
|
||||
pub fn try_fetch_punched_socket(&self, tid: u32) -> Option<PunchedUdpSocket> {
|
||||
tracing::debug!(?tid, "try fetch punched socket");
|
||||
self.tid_to_socket.get_mut(&tid)?.value_mut().pop()
|
||||
}
|
||||
|
||||
pub fn add_intreast_tid(&self, tid: u32) {
|
||||
self.intreast_tids.insert(tid);
|
||||
}
|
||||
|
||||
pub fn remove_intreast_tid(&self, tid: u32) {
|
||||
self.intreast_tids.remove(&tid);
|
||||
self.tid_to_socket.remove(&tid);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UdpSocketArray {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UdpSocketArray")
|
||||
.field("sockets", &self.sockets.len())
|
||||
.field("max_socket_count", &self.max_socket_count)
|
||||
.field("started", &self.started())
|
||||
.field("intreast_tids", &self.intreast_tids.len())
|
||||
.field("tid_to_socket", &self.tid_to_socket.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UdpHolePunchListener {
|
||||
socket: Arc<UdpSocket>,
|
||||
tasks: JoinSet<()>,
|
||||
running: Arc<AtomicCell<bool>>,
|
||||
mapped_addr: SocketAddr,
|
||||
conn_counter: Arc<Box<dyn TunnelConnCounter>>,
|
||||
|
||||
listen_time: std::time::Instant,
|
||||
last_select_time: AtomicCell<std::time::Instant>,
|
||||
last_active_time: Arc<AtomicCell<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl UdpHolePunchListener {
|
||||
async fn get_avail_port() -> Result<u16, Error> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
Ok(socket.local_addr()?.port())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn new(peer_mgr: Arc<PeerManager>) -> Result<Self, Error> {
|
||||
Self::new_ext(peer_mgr, true, None).await
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn new_ext(
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
with_mapped_addr: bool,
|
||||
port: Option<u16>,
|
||||
) -> Result<Self, Error> {
|
||||
let port = port.unwrap_or(Self::get_avail_port().await?);
|
||||
let listen_url = format!("udp://0.0.0.0:{}", port);
|
||||
|
||||
let mapped_addr = if with_mapped_addr {
|
||||
let gctx = peer_mgr.get_global_ctx();
|
||||
let stun_info_collect = gctx.get_stun_info_collector();
|
||||
stun_info_collect.get_udp_port_mapping(port).await?
|
||||
} else {
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port))
|
||||
};
|
||||
|
||||
let mut listener = UdpTunnelListener::new(listen_url.parse().unwrap());
|
||||
|
||||
{
|
||||
let _g = peer_mgr.get_global_ctx().net_ns.guard();
|
||||
listener.listen().await?;
|
||||
}
|
||||
let socket = listener.get_socket().unwrap();
|
||||
|
||||
let running = Arc::new(AtomicCell::new(true));
|
||||
let running_clone = running.clone();
|
||||
|
||||
let conn_counter = listener.get_conn_counter();
|
||||
let mut tasks = JoinSet::new();
|
||||
|
||||
tasks.spawn(async move {
|
||||
while let Ok(conn) = listener.accept().await {
|
||||
tracing::warn!(?conn, "udp hole punching listener got peer connection");
|
||||
let peer_mgr = peer_mgr.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await {
|
||||
tracing::error!(
|
||||
?e,
|
||||
"failed to add tunnel as server in hole punch listener"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
running_clone.store(false);
|
||||
});
|
||||
|
||||
let last_active_time = Arc::new(AtomicCell::new(std::time::Instant::now()));
|
||||
let conn_counter_clone = conn_counter.clone();
|
||||
let last_active_time_clone = last_active_time.clone();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
if conn_counter_clone.get().unwrap_or(0) != 0 {
|
||||
last_active_time_clone.store(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tracing::warn!(?mapped_addr, ?socket, "udp hole punching listener started");
|
||||
|
||||
Ok(Self {
|
||||
tasks,
|
||||
socket,
|
||||
running,
|
||||
mapped_addr,
|
||||
conn_counter,
|
||||
|
||||
listen_time: std::time::Instant::now(),
|
||||
last_select_time: AtomicCell::new(std::time::Instant::now()),
|
||||
last_active_time,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_socket(&self) -> Arc<UdpSocket> {
|
||||
self.last_select_time.store(std::time::Instant::now());
|
||||
self.socket.clone()
|
||||
}
|
||||
|
||||
pub async fn get_conn_count(&self) -> usize {
|
||||
self.conn_counter.get().unwrap_or(0) as usize
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PunchHoleServerCommon {
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
|
||||
listeners: Arc<Mutex<Vec<UdpHolePunchListener>>>,
|
||||
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
}
|
||||
|
||||
impl PunchHoleServerCommon {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new()));
|
||||
join_joinset_background(tasks.clone(), "PunchHoleServerCommon".to_owned());
|
||||
|
||||
let listeners = Arc::new(Mutex::new(Vec::<UdpHolePunchListener>::new()));
|
||||
|
||||
let l = listeners.clone();
|
||||
tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
{
|
||||
// remove listener that is not active for 40 seconds but keep listeners that are selected less than 30 seconds
|
||||
l.lock().await.retain(|listener| {
|
||||
listener.last_active_time.load().elapsed().as_secs() < 40
|
||||
|| listener.last_select_time.load().elapsed().as_secs() < 30
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
peer_mgr,
|
||||
|
||||
listeners,
|
||||
tasks,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn add_listener(&self, listener: UdpHolePunchListener) {
|
||||
self.listeners.lock().await.push(listener);
|
||||
}
|
||||
|
||||
pub(crate) async fn find_listener(&self, addr: &SocketAddr) -> Option<Arc<UdpSocket>> {
|
||||
let all_listener_sockets = self.listeners.lock().await;
|
||||
|
||||
let listener = all_listener_sockets
|
||||
.iter()
|
||||
.find(|listener| listener.mapped_addr == *addr && listener.running.load())?;
|
||||
|
||||
Some(listener.get_socket().await)
|
||||
}
|
||||
|
||||
pub(crate) async fn my_udp_nat_type(&self) -> i32 {
|
||||
self.peer_mgr
|
||||
.get_global_ctx()
|
||||
.get_stun_info_collector()
|
||||
.get_stun_info()
|
||||
.udp_nat_type
|
||||
}
|
||||
|
||||
pub(crate) async fn select_listener(
|
||||
&self,
|
||||
use_new_listener: bool,
|
||||
) -> Option<(Arc<UdpSocket>, SocketAddr)> {
|
||||
let all_listener_sockets = &self.listeners;
|
||||
|
||||
let mut use_last = false;
|
||||
if all_listener_sockets.lock().await.len() < 16 || use_new_listener {
|
||||
tracing::warn!("creating new udp hole punching listener");
|
||||
all_listener_sockets.lock().await.push(
|
||||
UdpHolePunchListener::new(self.peer_mgr.clone())
|
||||
.await
|
||||
.ok()?,
|
||||
);
|
||||
use_last = true;
|
||||
}
|
||||
|
||||
let mut locked = all_listener_sockets.lock().await;
|
||||
|
||||
let listener = if use_last {
|
||||
locked.last_mut()?
|
||||
} else {
|
||||
// use the listener that is active most recently
|
||||
locked
|
||||
.iter_mut()
|
||||
.max_by_key(|listener| listener.last_active_time.load())?
|
||||
};
|
||||
|
||||
if listener.mapped_addr.ip().is_unspecified() {
|
||||
tracing::info!("listener mapped addr is unspecified, trying to get mapped addr");
|
||||
listener.mapped_addr = self
|
||||
.get_global_ctx()
|
||||
.get_stun_info_collector()
|
||||
.get_udp_port_mapping(listener.mapped_addr.port())
|
||||
.await
|
||||
.ok()?;
|
||||
}
|
||||
|
||||
Some((listener.get_socket().await, listener.mapped_addr))
|
||||
}
|
||||
|
||||
pub(crate) fn get_joinset(&self) -> Arc<std::sync::Mutex<JoinSet<()>>> {
|
||||
self.tasks.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn get_global_ctx(&self) -> ArcGlobalCtx {
|
||||
self.peer_mgr.get_global_ctx()
|
||||
}
|
||||
|
||||
pub(crate) fn get_peer_mgr(&self) -> Arc<PeerManager> {
|
||||
self.peer_mgr.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(err, ret(level=Level::DEBUG), skip(ports))]
|
||||
pub(crate) async fn send_symmetric_hole_punch_packet(
|
||||
ports: &Vec<u16>,
|
||||
udp: Arc<UdpSocket>,
|
||||
transaction_id: u32,
|
||||
public_ips: &Vec<Ipv4Addr>,
|
||||
port_start_idx: usize,
|
||||
max_packets: usize,
|
||||
) -> Result<usize, Error> {
|
||||
tracing::debug!("sending hard symmetric hole punching packet");
|
||||
let mut sent_packets = 0;
|
||||
let mut cur_port_idx = port_start_idx;
|
||||
while sent_packets < max_packets {
|
||||
let port = ports[cur_port_idx % ports.len()];
|
||||
for pub_ip in public_ips {
|
||||
let addr = SocketAddr::V4(SocketAddrV4::new(*pub_ip, port));
|
||||
let packet = new_hole_punch_packet(transaction_id, HOLE_PUNCH_PACKET_BODY_LEN);
|
||||
udp.send_to(&packet.into_bytes(), addr).await?;
|
||||
sent_packets += 1;
|
||||
}
|
||||
cur_port_idx = cur_port_idx.wrapping_add(1);
|
||||
tokio::time::sleep(Duration::from_millis(3)).await;
|
||||
}
|
||||
Ok(cur_port_idx % ports.len())
|
||||
}
|
||||
|
||||
pub(crate) async fn try_connect_with_socket(
|
||||
socket: Arc<UdpSocket>,
|
||||
remote_mapped_addr: SocketAddr,
|
||||
) -> Result<Box<dyn Tunnel>, Error> {
|
||||
let connector = UdpTunnelConnector::new(
|
||||
format!(
|
||||
"udp://{}:{}",
|
||||
remote_mapped_addr.ip(),
|
||||
remote_mapped_addr.port()
|
||||
)
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
connector
|
||||
.try_connect_with_socket(socket, remote_mapped_addr)
|
||||
.await
|
||||
.map_err(|e| Error::from(e))
|
||||
}
|
||||
264
easytier/src/connector/udp_hole_punch/cone.rs
Normal file
264
easytier/src/connector/udp_hole_punch/cone.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::{
|
||||
common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId},
|
||||
connector::udp_hole_punch::common::{
|
||||
try_connect_with_socket, UdpSocketArray, HOLE_PUNCH_PACKET_BODY_LEN,
|
||||
},
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::{
|
||||
common::Void,
|
||||
peer_rpc::{
|
||||
SelectPunchListenerRequest, SendPunchPacketConeRequest, UdpHolePunchRpcClientFactory,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{udp::new_hole_punch_packet, Tunnel},
|
||||
};
|
||||
|
||||
use super::common::PunchHoleServerCommon;
|
||||
|
||||
pub(crate) struct PunchConeHoleServer {
|
||||
common: Arc<PunchHoleServerCommon>,
|
||||
}
|
||||
|
||||
impl PunchConeHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> Self {
|
||||
Self { common }
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), ret, err)]
|
||||
pub(crate) async fn send_punch_packet_cone(
|
||||
&self,
|
||||
_: BaseController,
|
||||
request: SendPunchPacketConeRequest,
|
||||
) -> Result<Void, rpc_types::error::Error> {
|
||||
let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone request missing listener_mapped_addr"
|
||||
))?;
|
||||
let listener_addr = std::net::SocketAddr::from(listener_addr);
|
||||
let listener = self
|
||||
.common
|
||||
.find_listener(&listener_addr)
|
||||
.await
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone failed to find listener"
|
||||
))?;
|
||||
|
||||
let dest_addr = request.dest_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone request missing dest_addr"
|
||||
))?;
|
||||
let dest_addr = std::net::SocketAddr::from(dest_addr);
|
||||
let dest_ip = dest_addr.ip();
|
||||
if dest_ip.is_unspecified() || dest_ip.is_multicast() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone dest_ip is malformed, {:?}",
|
||||
request
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
for _ in 0..request.packet_batch_count {
|
||||
tracing::info!(?request, "sending hole punching packet");
|
||||
|
||||
for _ in 0..request.packet_count_per_batch {
|
||||
let udp_packet =
|
||||
new_hole_punch_packet(request.transaction_id, HOLE_PUNCH_PACKET_BODY_LEN);
|
||||
if let Err(e) = listener.send_to(&udp_packet.into_bytes(), &dest_addr).await {
|
||||
tracing::error!(?e, "failed to send hole punch packet to dest addr");
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(request.packet_interval_ms as u64)).await;
|
||||
}
|
||||
|
||||
Ok(Void::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PunchConeHoleClient {
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
impl PunchConeHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
Self { peer_mgr }
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(crate) async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Result<Option<Box<dyn Tunnel>>, anyhow::Error> {
|
||||
tracing::info!(?dst_peer_id, "start hole punching");
|
||||
let tid = rand::random();
|
||||
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
let udp_array = UdpSocketArray::new(1, global_ctx.net_ns.clone());
|
||||
let local_socket = {
|
||||
let _g = self.peer_mgr.get_global_ctx().net_ns.guard();
|
||||
Arc::new(UdpSocket::bind("0.0.0.0:0").await?)
|
||||
};
|
||||
|
||||
let local_addr = local_socket
|
||||
.local_addr()
|
||||
.with_context(|| anyhow::anyhow!("failed to get local port from udp array"))?;
|
||||
let local_port = local_addr.port();
|
||||
|
||||
drop(local_socket);
|
||||
let local_mapped_addr = global_ctx
|
||||
.get_stun_info_collector()
|
||||
.get_udp_port_mapping(local_port)
|
||||
.await
|
||||
.with_context(|| "failed to get udp port mapping")?;
|
||||
|
||||
let local_socket = {
|
||||
let _g = self.peer_mgr.get_global_ctx().net_ns.guard();
|
||||
Arc::new(UdpSocket::bind(local_addr).await?)
|
||||
};
|
||||
|
||||
// client -> server: tell server the mapped port, server will return the mapped address of listening port.
|
||||
let rpc_stub = self
|
||||
.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_client()
|
||||
.scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
self.peer_mgr.my_peer_id(),
|
||||
dst_peer_id,
|
||||
global_ctx.get_network_name(),
|
||||
);
|
||||
|
||||
let resp = rpc_stub
|
||||
.select_punch_listener(
|
||||
BaseController::default(),
|
||||
SelectPunchListenerRequest { force_new: false },
|
||||
)
|
||||
.await
|
||||
.with_context(|| "failed to select punch listener")?;
|
||||
let remote_mapped_addr = resp.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"select_punch_listener response missing listener_mapped_addr"
|
||||
))?;
|
||||
|
||||
tracing::debug!(
|
||||
?local_mapped_addr,
|
||||
?remote_mapped_addr,
|
||||
"hole punch got remote listener"
|
||||
);
|
||||
|
||||
udp_array.add_new_socket(local_socket).await?;
|
||||
udp_array.add_intreast_tid(tid);
|
||||
let send_from_local = || async {
|
||||
udp_array
|
||||
.send_with_all(
|
||||
&new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(),
|
||||
remote_mapped_addr.clone().into(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "failed to send hole punch packet from local")
|
||||
};
|
||||
|
||||
send_from_local().await?;
|
||||
|
||||
let scoped_punch_task: ScopedTask<()> = tokio::spawn(async move {
|
||||
if let Err(e) = rpc_stub
|
||||
.send_punch_packet_cone(
|
||||
BaseController {
|
||||
timeout_ms: 4000,
|
||||
..Default::default()
|
||||
},
|
||||
SendPunchPacketConeRequest {
|
||||
listener_mapped_addr: Some(remote_mapped_addr.into()),
|
||||
dest_addr: Some(local_mapped_addr.into()),
|
||||
transaction_id: tid,
|
||||
packet_count_per_batch: 2,
|
||||
packet_batch_count: 5,
|
||||
packet_interval_ms: 400,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(?e, "failed to call remote send punch packet");
|
||||
}
|
||||
})
|
||||
.into();
|
||||
|
||||
// server: will send some punching resps, total 10 packets.
|
||||
// client: use the socket to create UdpTunnel with UdpTunnelConnector
|
||||
// NOTICE: UdpTunnelConnector will ignore the punching resp packet sent by remote.
|
||||
let mut finish_time: Option<Instant> = None;
|
||||
while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
if finish_time.is_none() && (*scoped_punch_task).is_finished() {
|
||||
finish_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::debug!("no punched socket found, send some more hole punch packets");
|
||||
send_from_local().await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::debug!(?socket, ?tid, "punched socket found, try connect with it");
|
||||
|
||||
for _ in 0..2 {
|
||||
match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into())
|
||||
.await
|
||||
{
|
||||
Ok(tunnel) => {
|
||||
tracing::info!(?tunnel, "hole punched");
|
||||
return Ok(Some(tunnel));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
use crate::{
|
||||
connector::udp_hole_punch::{
|
||||
tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector,
|
||||
},
|
||||
peers::tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost},
|
||||
proto::common::NatType,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn hole_punching_cone() {
|
||||
let p_a = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await;
|
||||
let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
let p_c = create_mock_peer_manager_with_mock_stun(NatType::Restricted).await;
|
||||
connect_peer_manager(p_a.clone(), p_b.clone()).await;
|
||||
connect_peer_manager(p_b.clone(), p_c.clone()).await;
|
||||
|
||||
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
||||
|
||||
println!("{:?}", p_a.list_routes().await);
|
||||
|
||||
let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone());
|
||||
let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone());
|
||||
|
||||
hole_punching_a.run_as_client().await.unwrap();
|
||||
hole_punching_c.run_as_server().await.unwrap();
|
||||
|
||||
hole_punching_a.client.run_immediately().await;
|
||||
|
||||
wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1))
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{:?}", p_a.list_routes().await);
|
||||
}
|
||||
}
|
||||
559
easytier/src/connector/udp_hole_punch/mod.rs
Normal file
559
easytier/src/connector/udp_hole_punch/mod.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use anyhow::{Context, Error};
|
||||
use both_easy_sym::{PunchBothEasySymHoleClient, PunchBothEasySymHoleServer};
|
||||
use common::{PunchHoleServerCommon, UdpNatType, UdpPunchClientMethod};
|
||||
use cone::{PunchConeHoleClient, PunchConeHoleServer};
|
||||
use dashmap::DashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use sym_to_cone::{PunchSymToConeHoleClient, PunchSymToConeHoleServer};
|
||||
use tokio::{sync::Mutex, task::JoinHandle};
|
||||
|
||||
use crate::{
|
||||
common::{stun::StunInfoCollectorTrait, PeerId},
|
||||
connector::direct::PeerManagerForDirectConnector,
|
||||
peers::{
|
||||
peer_manager::PeerManager,
|
||||
peer_task::{PeerTaskLauncher, PeerTaskManager},
|
||||
},
|
||||
proto::{
|
||||
common::{NatType, Void},
|
||||
peer_rpc::{
|
||||
SelectPunchListenerRequest, SelectPunchListenerResponse,
|
||||
SendPunchPacketBothEasySymRequest, SendPunchPacketBothEasySymResponse,
|
||||
SendPunchPacketConeRequest, SendPunchPacketEasySymRequest,
|
||||
SendPunchPacketHardSymRequest, SendPunchPacketHardSymResponse, UdpHolePunchRpc,
|
||||
UdpHolePunchRpcServer,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::Tunnel,
|
||||
};
|
||||
|
||||
pub(crate) mod both_easy_sym;
|
||||
pub(crate) mod common;
|
||||
pub(crate) mod cone;
|
||||
pub(crate) mod sym_to_cone;
|
||||
|
||||
// sym punch should be serialized
|
||||
static SYM_PUNCH_LOCK: Lazy<DashMap<PeerId, Arc<Mutex<()>>>> = Lazy::new(|| DashMap::new());
|
||||
static RUN_TESTING: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(false));
|
||||
|
||||
fn get_sym_punch_lock(peer_id: PeerId) -> Arc<Mutex<()>> {
|
||||
SYM_PUNCH_LOCK
|
||||
.entry(peer_id)
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.value()
|
||||
.clone()
|
||||
}
|
||||
|
||||
struct UdpHolePunchServer {
|
||||
common: Arc<PunchHoleServerCommon>,
|
||||
cone_server: PunchConeHoleServer,
|
||||
sym_to_cone_server: PunchSymToConeHoleServer,
|
||||
both_easy_sym_server: PunchBothEasySymHoleServer,
|
||||
}
|
||||
|
||||
impl UdpHolePunchServer {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>) -> Arc<Self> {
|
||||
let common = Arc::new(PunchHoleServerCommon::new(peer_mgr.clone()));
|
||||
let cone_server = PunchConeHoleServer::new(common.clone());
|
||||
let sym_to_cone_server = PunchSymToConeHoleServer::new(common.clone());
|
||||
let both_easy_sym_server = PunchBothEasySymHoleServer::new(common.clone());
|
||||
|
||||
Arc::new(Self {
|
||||
common,
|
||||
cone_server,
|
||||
sym_to_cone_server,
|
||||
both_easy_sym_server,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl UdpHolePunchRpc for UdpHolePunchServer {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
input: SelectPunchListenerRequest,
|
||||
) -> rpc_types::error::Result<SelectPunchListenerResponse> {
|
||||
let (_, addr) = self
|
||||
.common
|
||||
.select_listener(input.force_new)
|
||||
.await
|
||||
.ok_or(anyhow::anyhow!("no listener available"))?;
|
||||
|
||||
Ok(SelectPunchListenerResponse {
|
||||
listener_mapped_addr: Some(addr.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// send packet to one remote_addr, used by nat1-3 to nat1-3
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
ctrl: Self::Controller,
|
||||
input: SendPunchPacketConeRequest,
|
||||
) -> rpc_types::error::Result<Void> {
|
||||
self.cone_server.send_punch_packet_cone(ctrl, input).await
|
||||
}
|
||||
|
||||
/// send packet to multiple remote_addr (birthday attack), used by nat4 to nat1-3
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
input: SendPunchPacketHardSymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketHardSymResponse> {
|
||||
let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id())
|
||||
.try_lock_owned()
|
||||
.with_context(|| "sym punch lock is busy")?;
|
||||
self.sym_to_cone_server
|
||||
.send_punch_packet_hard_sym(input)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
input: SendPunchPacketEasySymRequest,
|
||||
) -> rpc_types::error::Result<Void> {
|
||||
let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id())
|
||||
.try_lock_owned()
|
||||
.with_context(|| "sym punch lock is busy")?;
|
||||
self.sym_to_cone_server
|
||||
.send_punch_packet_easy_sym(input)
|
||||
.await
|
||||
.map(|_| Void {})
|
||||
}
|
||||
|
||||
/// nat4 to nat4 (both predictably)
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
_ctrl: Self::Controller,
|
||||
input: SendPunchPacketBothEasySymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketBothEasySymResponse> {
|
||||
let _locked = get_sym_punch_lock(self.common.get_peer_mgr().my_peer_id())
|
||||
.try_lock_owned()
|
||||
.with_context(|| "sym punch lock is busy")?;
|
||||
self.both_easy_sym_server
|
||||
.send_punch_packet_both_easy_sym(input)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BackOff {
|
||||
backoffs_ms: Vec<u64>,
|
||||
current_idx: usize,
|
||||
}
|
||||
|
||||
impl BackOff {
|
||||
pub fn new(backoffs_ms: Vec<u64>) -> Self {
|
||||
Self {
|
||||
backoffs_ms,
|
||||
current_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_backoff(&mut self) -> u64 {
|
||||
let backoff = self.backoffs_ms[self.current_idx];
|
||||
self.current_idx = (self.current_idx + 1).min(self.backoffs_ms.len() - 1);
|
||||
backoff
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) {
|
||||
self.current_idx = self.current_idx.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub async fn sleep_for_next_backoff(&mut self) {
|
||||
let backoff = self.next_backoff();
|
||||
if backoff > 0 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(backoff)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpHoePunchConnectorData {
|
||||
cone_client: PunchConeHoleClient,
|
||||
sym_to_cone_client: PunchSymToConeHoleClient,
|
||||
both_easy_sym_client: PunchBothEasySymHoleClient,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
impl UdpHoePunchConnectorData {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>) -> Arc<Self> {
|
||||
let cone_client = PunchConeHoleClient::new(peer_mgr.clone());
|
||||
let sym_to_cone_client = PunchSymToConeHoleClient::new(peer_mgr.clone());
|
||||
let both_easy_sym_client = PunchBothEasySymHoleClient::new(peer_mgr.clone());
|
||||
|
||||
Arc::new(Self {
|
||||
cone_client,
|
||||
sym_to_cone_client,
|
||||
both_easy_sym_client,
|
||||
peer_mgr,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn handle_punch_result(
|
||||
self: &Self,
|
||||
ret: Result<Option<Box<dyn Tunnel>>, Error>,
|
||||
backoff: Option<&mut BackOff>,
|
||||
round: Option<&mut u32>,
|
||||
) -> bool {
|
||||
let op = |rollback: bool| {
|
||||
if rollback {
|
||||
if let Some(backoff) = backoff {
|
||||
backoff.rollback();
|
||||
}
|
||||
if let Some(round) = round {
|
||||
*round = round.saturating_sub(1);
|
||||
}
|
||||
} else {
|
||||
if let Some(round) = round {
|
||||
*round += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match ret {
|
||||
Ok(Some(tunnel)) => {
|
||||
tracing::info!(?tunnel, "hole punching get tunnel success");
|
||||
|
||||
if let Err(e) = self.peer_mgr.add_client_tunnel(tunnel).await {
|
||||
tracing::warn!(?e, "add client tunnel failed");
|
||||
op(true);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::info!("hole punching failed, no punch tunnel");
|
||||
op(false);
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::info!(?e, "hole punching failed");
|
||||
op(true);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn cone_to_cone(self: Arc<Self>, task_info: PunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000]);
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
let ret = self
|
||||
.cone_client
|
||||
.do_hole_punching(task_info.dst_peer_id)
|
||||
.await;
|
||||
|
||||
if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), None)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn sym_to_cone(self: Arc<Self>, task_info: PunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]);
|
||||
let mut round = 0;
|
||||
let mut port_idx = rand::random();
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
// always try cone first
|
||||
if !RUN_TESTING.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let ret = self
|
||||
.cone_client
|
||||
.do_hole_punching(task_info.dst_peer_id)
|
||||
.await;
|
||||
if self.handle_punch_result(ret, None, None).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ret = {
|
||||
let _lock = get_sym_punch_lock(self.peer_mgr.my_peer_id())
|
||||
.lock_owned()
|
||||
.await;
|
||||
self.sym_to_cone_client
|
||||
.do_hole_punching(
|
||||
task_info.dst_peer_id,
|
||||
round,
|
||||
&mut port_idx,
|
||||
task_info.my_nat_type,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), Some(&mut round))
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn both_easy_sym(self: Arc<Self>, task_info: PunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]);
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
// always try cone first
|
||||
if !RUN_TESTING.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let ret = self
|
||||
.cone_client
|
||||
.do_hole_punching(task_info.dst_peer_id)
|
||||
.await;
|
||||
if self.handle_punch_result(ret, None, None).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut is_busy = false;
|
||||
|
||||
let ret = {
|
||||
let _lock = get_sym_punch_lock(self.peer_mgr.my_peer_id())
|
||||
.lock_owned()
|
||||
.await;
|
||||
self.both_easy_sym_client
|
||||
.do_hole_punching(
|
||||
task_info.dst_peer_id,
|
||||
task_info.my_nat_type,
|
||||
task_info.dst_nat_type,
|
||||
&mut is_busy,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
if is_busy {
|
||||
backoff.rollback();
|
||||
} else if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), None)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct UdpHolePunchPeerTaskLauncher {}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
struct PunchTaskInfo {
|
||||
dst_peer_id: PeerId,
|
||||
dst_nat_type: UdpNatType,
|
||||
my_nat_type: UdpNatType,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerTaskLauncher for UdpHolePunchPeerTaskLauncher {
|
||||
type Data = Arc<UdpHoePunchConnectorData>;
|
||||
type CollectPeerItem = PunchTaskInfo;
|
||||
type TaskRet = ();
|
||||
|
||||
fn new_data(&self, peer_mgr: Arc<PeerManager>) -> Self::Data {
|
||||
UdpHoePunchConnectorData::new(peer_mgr)
|
||||
}
|
||||
|
||||
async fn collect_peers_need_task(&self, data: &Self::Data) -> Vec<Self::CollectPeerItem> {
|
||||
let my_nat_type = data
|
||||
.peer_mgr
|
||||
.get_global_ctx()
|
||||
.get_stun_info_collector()
|
||||
.get_stun_info()
|
||||
.udp_nat_type;
|
||||
let my_nat_type: UdpNatType = NatType::try_from(my_nat_type)
|
||||
.unwrap_or(NatType::Unknown)
|
||||
.into();
|
||||
if !my_nat_type.is_sym() {
|
||||
data.sym_to_cone_client.clear_udp_array().await;
|
||||
}
|
||||
|
||||
let mut peers_to_connect: Vec<Self::CollectPeerItem> = Vec::new();
|
||||
// do not do anything if:
|
||||
// 1. our nat type is OpenInternet or NoPat, which means we can wait other peers to connect us
|
||||
// notice that if we are unknown, we treat ourselves as cone
|
||||
if my_nat_type.is_open() {
|
||||
return peers_to_connect;
|
||||
}
|
||||
|
||||
let my_peer_id = data.peer_mgr.my_peer_id();
|
||||
|
||||
// collect peer list from peer manager and do some filter:
|
||||
// 1. peers without direct conns;
|
||||
// 2. peers is full cone (any restricted type);
|
||||
for route in data.peer_mgr.list_routes().await.iter() {
|
||||
if route
|
||||
.feature_flag
|
||||
.map(|x| x.is_public_server)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let peer_nat_type = route
|
||||
.stun_info
|
||||
.as_ref()
|
||||
.map(|x| x.udp_nat_type)
|
||||
.unwrap_or(0);
|
||||
let Ok(peer_nat_type) = NatType::try_from(peer_nat_type) else {
|
||||
continue;
|
||||
};
|
||||
let peer_nat_type = peer_nat_type.into();
|
||||
|
||||
let peer_id: PeerId = route.peer_id;
|
||||
let conns = data.peer_mgr.list_peer_conns(peer_id).await;
|
||||
if conns.is_some() && conns.unwrap().len() > 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !my_nat_type.can_punch_hole_as_client(peer_nat_type, my_peer_id, peer_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
?peer_id,
|
||||
?peer_nat_type,
|
||||
?my_nat_type,
|
||||
"found peer to do hole punching"
|
||||
);
|
||||
|
||||
peers_to_connect.push(PunchTaskInfo {
|
||||
dst_peer_id: peer_id,
|
||||
dst_nat_type: peer_nat_type,
|
||||
my_nat_type,
|
||||
});
|
||||
}
|
||||
|
||||
peers_to_connect
|
||||
}
|
||||
|
||||
async fn launch_task(
|
||||
&self,
|
||||
data: &Self::Data,
|
||||
item: Self::CollectPeerItem,
|
||||
) -> JoinHandle<Result<Self::TaskRet, Error>> {
|
||||
let data = data.clone();
|
||||
let punch_method = item.my_nat_type.get_punch_hole_method(item.dst_nat_type);
|
||||
match punch_method {
|
||||
UdpPunchClientMethod::ConeToCone => tokio::spawn(data.cone_to_cone(item)),
|
||||
UdpPunchClientMethod::SymToCone => tokio::spawn(data.sym_to_cone(item)),
|
||||
UdpPunchClientMethod::EasySymToEasySym => tokio::spawn(data.both_easy_sym(item)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn all_task_done(&self, data: &Self::Data) {
|
||||
data.sym_to_cone_client.clear_udp_array().await;
|
||||
}
|
||||
|
||||
fn loop_interval_ms(&self) -> u64 {
|
||||
5000
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpHolePunchConnector {
|
||||
server: Arc<UdpHolePunchServer>,
|
||||
client: PeerTaskManager<UdpHolePunchPeerTaskLauncher>,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
}
|
||||
|
||||
// Currently support:
|
||||
// Symmetric -> Full Cone
|
||||
// Any Type of Full Cone -> Any Type of Full Cone
|
||||
|
||||
// if same level of full cone, node with smaller peer_id will be the initiator
|
||||
// if different level of full cone, node with more strict level will be the initiator
|
||||
|
||||
impl UdpHolePunchConnector {
|
||||
pub fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
Self {
|
||||
server: UdpHolePunchServer::new(peer_mgr.clone()),
|
||||
client: PeerTaskManager::new(UdpHolePunchPeerTaskLauncher {}, peer_mgr.clone()),
|
||||
peer_mgr,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_as_client(&mut self) -> Result<(), Error> {
|
||||
self.client.start();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_as_server(&mut self) -> Result<(), Error> {
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.register(
|
||||
UdpHolePunchRpcServer::new(self.server.clone()),
|
||||
&self.peer_mgr.get_global_ctx().get_network_name(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
|
||||
if global_ctx.get_flags().disable_p2p {
|
||||
return Ok(());
|
||||
}
|
||||
if global_ctx.get_flags().disable_udp_hole_punching {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.run_as_client().await?;
|
||||
self.run_as_server().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::stun::MockStunInfoCollector;
|
||||
use crate::proto::common::NatType;
|
||||
|
||||
use crate::peers::{peer_manager::PeerManager, tests::create_mock_peer_manager};
|
||||
|
||||
pub fn replace_stun_info_collector(peer_mgr: Arc<PeerManager>, udp_nat_type: NatType) {
|
||||
let collector = Box::new(MockStunInfoCollector { udp_nat_type });
|
||||
peer_mgr
|
||||
.get_global_ctx()
|
||||
.replace_stun_info_collector(collector);
|
||||
}
|
||||
|
||||
pub async fn create_mock_peer_manager_with_mock_stun(
|
||||
udp_nat_type: NatType,
|
||||
) -> Arc<PeerManager> {
|
||||
let p_a = create_mock_peer_manager().await;
|
||||
replace_stun_info_collector(p_a.clone(), udp_nat_type);
|
||||
p_a
|
||||
}
|
||||
}
|
||||
589
easytier/src/connector/udp_hole_punch/sym_to_cone.rs
Normal file
589
easytier/src/connector/udp_hole_punch/sym_to_cone.rs
Normal file
@@ -0,0 +1,589 @@
|
||||
use std::{
|
||||
net::Ipv4Addr,
|
||||
ops::{Div, Mul},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use tokio::{net::UdpSocket, sync::RwLock};
|
||||
use tracing::Level;
|
||||
|
||||
use crate::{
|
||||
common::{scoped_task::ScopedTask, stun::StunInfoCollectorTrait, PeerId},
|
||||
connector::udp_hole_punch::common::{
|
||||
send_symmetric_hole_punch_packet, try_connect_with_socket, HOLE_PUNCH_PACKET_BODY_LEN,
|
||||
},
|
||||
defer,
|
||||
peers::peer_manager::PeerManager,
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
SelectPunchListenerRequest, SendPunchPacketEasySymRequest,
|
||||
SendPunchPacketHardSymRequest, SendPunchPacketHardSymResponse,
|
||||
UdpHolePunchRpcClientFactory,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
tunnel::{udp::new_hole_punch_packet, Tunnel},
|
||||
};
|
||||
|
||||
use super::common::{PunchHoleServerCommon, UdpNatType, UdpSocketArray};
|
||||
|
||||
const UDP_ARRAY_SIZE_FOR_HARD_SYM: usize = 84;
|
||||
|
||||
pub(crate) struct PunchSymToConeHoleServer {
|
||||
common: Arc<PunchHoleServerCommon>,
|
||||
|
||||
shuffled_port_vec: Arc<Vec<u16>>,
|
||||
}
|
||||
|
||||
impl PunchSymToConeHoleServer {
|
||||
pub(crate) fn new(common: Arc<PunchHoleServerCommon>) -> Self {
|
||||
let mut shuffled_port_vec: Vec<u16> = (1..=65535).collect();
|
||||
shuffled_port_vec.shuffle(&mut rand::thread_rng());
|
||||
|
||||
Self {
|
||||
common,
|
||||
shuffled_port_vec: Arc::new(shuffled_port_vec),
|
||||
}
|
||||
}
|
||||
|
||||
// hard sym means public port is random and cannot be predicted
|
||||
#[tracing::instrument(skip(self), ret)]
|
||||
pub(crate) async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketEasySymRequest,
|
||||
) -> Result<(), rpc_types::error::Error> {
|
||||
tracing::info!("send_punch_packet_easy_sym start");
|
||||
|
||||
let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_easy_sym request missing listener_addr"
|
||||
))?;
|
||||
let listener_addr = std::net::SocketAddr::from(listener_addr);
|
||||
let listener = self
|
||||
.common
|
||||
.find_listener(&listener_addr)
|
||||
.await
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_easy_sym failed to find listener"
|
||||
))?;
|
||||
|
||||
let public_ips = request
|
||||
.public_ips
|
||||
.into_iter()
|
||||
.map(|ip| std::net::Ipv4Addr::from(ip))
|
||||
.collect::<Vec<_>>();
|
||||
if public_ips.len() == 0 {
|
||||
tracing::warn!("send_punch_packet_easy_sym got zero len public ip");
|
||||
return Err(
|
||||
anyhow::anyhow!("send_punch_packet_easy_sym got zero len public ip").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let transaction_id = request.transaction_id;
|
||||
let base_port_num = request.base_port_num;
|
||||
let max_port_num = request.max_port_num.max(1);
|
||||
let is_incremental = request.is_incremental;
|
||||
|
||||
let port_start = if is_incremental {
|
||||
base_port_num.saturating_add(1)
|
||||
} else {
|
||||
base_port_num.saturating_sub(max_port_num)
|
||||
};
|
||||
|
||||
let port_end = if is_incremental {
|
||||
base_port_num.saturating_add(max_port_num)
|
||||
} else {
|
||||
base_port_num.saturating_sub(1)
|
||||
};
|
||||
|
||||
if port_end <= port_start {
|
||||
return Err(anyhow::anyhow!("send_punch_packet_easy_sym invalid port range").into());
|
||||
}
|
||||
|
||||
let ports = (port_start..=port_end)
|
||||
.map(|x| x as u16)
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!(
|
||||
?ports,
|
||||
?public_ips,
|
||||
"send_punch_packet_easy_sym send to ports"
|
||||
);
|
||||
send_symmetric_hole_punch_packet(
|
||||
&ports,
|
||||
listener,
|
||||
transaction_id,
|
||||
&public_ips,
|
||||
0,
|
||||
ports.len(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "failed to send symmetric hole punch packet")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// hard sym means public port is random and cannot be predicted
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(crate) async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
request: SendPunchPacketHardSymRequest,
|
||||
) -> Result<SendPunchPacketHardSymResponse, rpc_types::error::Error> {
|
||||
tracing::info!("try_punch_symmetric start");
|
||||
|
||||
let listener_addr = request.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"try_punch_symmetric request missing listener_addr"
|
||||
))?;
|
||||
let listener_addr = std::net::SocketAddr::from(listener_addr);
|
||||
let listener = self
|
||||
.common
|
||||
.find_listener(&listener_addr)
|
||||
.await
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone failed to find listener"
|
||||
))?;
|
||||
|
||||
let public_ips = request
|
||||
.public_ips
|
||||
.into_iter()
|
||||
.map(|ip| std::net::Ipv4Addr::from(ip))
|
||||
.collect::<Vec<_>>();
|
||||
if public_ips.len() == 0 {
|
||||
tracing::warn!("try_punch_symmetric got zero len public ip");
|
||||
return Err(anyhow::anyhow!("try_punch_symmetric got zero len public ip").into());
|
||||
}
|
||||
|
||||
let transaction_id = request.transaction_id;
|
||||
let last_port_index = request.port_index as usize;
|
||||
|
||||
let round = std::cmp::max(request.round, 1);
|
||||
|
||||
// send max k1 packets if we are predicting the dst port
|
||||
let max_k1: u32 = 180;
|
||||
// send max k2 packets if we are sending to random port
|
||||
let mut max_k2: u32 = rand::thread_rng().gen_range(600..800);
|
||||
if round > 2 {
|
||||
max_k2 = max_k2.mul(2).div(round).max(max_k1);
|
||||
}
|
||||
|
||||
let next_port_index = send_symmetric_hole_punch_packet(
|
||||
&self.shuffled_port_vec,
|
||||
listener.clone(),
|
||||
transaction_id,
|
||||
&public_ips,
|
||||
last_port_index,
|
||||
max_k2 as usize,
|
||||
)
|
||||
.await
|
||||
.with_context(|| "failed to send symmetric hole punch packet randomly")?;
|
||||
|
||||
return Ok(SendPunchPacketHardSymResponse {
|
||||
next_port_index: next_port_index as u32,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PunchSymToConeHoleClient {
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
udp_array: RwLock<Option<Arc<UdpSocketArray>>>,
|
||||
try_direct_connect: AtomicBool,
|
||||
punch_predicablely: AtomicBool,
|
||||
punch_randomly: AtomicBool,
|
||||
}
|
||||
|
||||
impl PunchSymToConeHoleClient {
|
||||
pub(crate) fn new(peer_mgr: Arc<PeerManager>) -> Self {
|
||||
Self {
|
||||
peer_mgr,
|
||||
udp_array: RwLock::new(None),
|
||||
try_direct_connect: AtomicBool::new(true),
|
||||
punch_predicablely: AtomicBool::new(true),
|
||||
punch_randomly: AtomicBool::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare_udp_array(&self) -> Result<Arc<UdpSocketArray>, anyhow::Error> {
|
||||
let rlocked = self.udp_array.read().await;
|
||||
if let Some(udp_array) = rlocked.clone() {
|
||||
return Ok(udp_array);
|
||||
}
|
||||
|
||||
drop(rlocked);
|
||||
let mut wlocked = self.udp_array.write().await;
|
||||
if let Some(udp_array) = wlocked.clone() {
|
||||
return Ok(udp_array);
|
||||
}
|
||||
|
||||
let udp_array = Arc::new(UdpSocketArray::new(
|
||||
UDP_ARRAY_SIZE_FOR_HARD_SYM,
|
||||
self.peer_mgr.get_global_ctx().net_ns.clone(),
|
||||
));
|
||||
udp_array.start().await?;
|
||||
wlocked.replace(udp_array.clone());
|
||||
Ok(udp_array)
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_udp_array(&self) {
|
||||
let mut wlocked = self.udp_array.write().await;
|
||||
wlocked.take();
|
||||
}
|
||||
|
||||
async fn get_base_port_for_easy_sym(&self, my_nat_info: UdpNatType) -> Option<u16> {
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
if my_nat_info.is_easy_sym() {
|
||||
match global_ctx
|
||||
.get_stun_info_collector()
|
||||
.get_udp_port_mapping(0)
|
||||
.await
|
||||
{
|
||||
Ok(addr) => Some(addr.port()),
|
||||
ret => {
|
||||
tracing::warn!(?ret, "failed to get udp port mapping for easy sym");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(err(level = Level::ERROR), skip(self))]
|
||||
pub(crate) async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
round: u32,
|
||||
last_port_idx: &mut usize,
|
||||
my_nat_info: UdpNatType,
|
||||
) -> Result<Option<Box<dyn Tunnel>>, anyhow::Error> {
|
||||
let udp_array = self.prepare_udp_array().await?;
|
||||
let global_ctx = self.peer_mgr.get_global_ctx();
|
||||
|
||||
let rpc_stub = self
|
||||
.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_client()
|
||||
.scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
self.peer_mgr.my_peer_id(),
|
||||
dst_peer_id,
|
||||
global_ctx.get_network_name(),
|
||||
);
|
||||
|
||||
let resp = rpc_stub
|
||||
.select_punch_listener(
|
||||
BaseController::default(),
|
||||
SelectPunchListenerRequest { force_new: false },
|
||||
)
|
||||
.await
|
||||
.with_context(|| "failed to select punch listener")?;
|
||||
let remote_mapped_addr = resp.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"select_punch_listener response missing listener_mapped_addr"
|
||||
))?;
|
||||
|
||||
// try direct connect first
|
||||
if self.try_direct_connect.load(Ordering::Relaxed) {
|
||||
if let Ok(tunnel) = try_connect_with_socket(
|
||||
Arc::new(UdpSocket::bind("0.0.0.0:0").await?),
|
||||
remote_mapped_addr.into(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(Some(tunnel));
|
||||
}
|
||||
}
|
||||
|
||||
let stun_info = global_ctx.get_stun_info_collector().get_stun_info();
|
||||
let public_ips: Vec<Ipv4Addr> = stun_info
|
||||
.public_ip
|
||||
.iter()
|
||||
.map(|x| x.parse().unwrap())
|
||||
.collect();
|
||||
if public_ips.is_empty() {
|
||||
return Err(anyhow::anyhow!("failed to get public ips"));
|
||||
}
|
||||
|
||||
let tid = rand::thread_rng().gen();
|
||||
let packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
|
||||
udp_array.add_intreast_tid(tid);
|
||||
defer! { udp_array.remove_intreast_tid(tid);}
|
||||
udp_array
|
||||
.send_with_all(&packet, remote_mapped_addr.into())
|
||||
.await?;
|
||||
|
||||
let port_index = *last_port_idx as u32;
|
||||
let base_port_for_easy_sym = self.get_base_port_for_easy_sym(my_nat_info).await;
|
||||
let punch_random = self.punch_randomly.load(Ordering::Relaxed);
|
||||
let punch_predicable = self.punch_predicablely.load(Ordering::Relaxed);
|
||||
let scoped_punch_task: ScopedTask<Option<u32>> = tokio::spawn(async move {
|
||||
if punch_predicable {
|
||||
if let Some(inc) = my_nat_info.get_inc_of_easy_sym() {
|
||||
let req = SendPunchPacketEasySymRequest {
|
||||
listener_mapped_addr: remote_mapped_addr.clone().into(),
|
||||
public_ips: public_ips.clone().into_iter().map(|x| x.into()).collect(),
|
||||
transaction_id: tid,
|
||||
base_port_num: base_port_for_easy_sym.unwrap() as u32,
|
||||
max_port_num: 50,
|
||||
is_incremental: inc,
|
||||
};
|
||||
tracing::debug!(?req, "send punch packet for easy sym start");
|
||||
let ret = rpc_stub
|
||||
.send_punch_packet_easy_sym(
|
||||
BaseController {
|
||||
timeout_ms: 4000,
|
||||
trace_id: 0,
|
||||
},
|
||||
req,
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(?ret, "send punch packet for easy sym return");
|
||||
}
|
||||
}
|
||||
|
||||
if punch_random {
|
||||
let req = SendPunchPacketHardSymRequest {
|
||||
listener_mapped_addr: remote_mapped_addr.clone().into(),
|
||||
public_ips: public_ips.clone().into_iter().map(|x| x.into()).collect(),
|
||||
transaction_id: tid,
|
||||
round,
|
||||
port_index,
|
||||
};
|
||||
tracing::debug!(?req, "send punch packet for hard sym start");
|
||||
match rpc_stub
|
||||
.send_punch_packet_hard_sym(
|
||||
BaseController {
|
||||
timeout_ms: 4000,
|
||||
trace_id: 0,
|
||||
},
|
||||
req,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to send punch packet for hard sym");
|
||||
return None;
|
||||
}
|
||||
Ok(resp) => return Some(resp.next_port_index),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.into();
|
||||
|
||||
// no matter what the result is, we should check if we received any hole punching packet
|
||||
let mut ret_tunnel: Option<Box<dyn Tunnel>> = None;
|
||||
let mut finish_time: Option<Instant> = None;
|
||||
while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
if finish_time.is_none() && (*scoped_punch_task).is_finished() {
|
||||
finish_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::debug!("no punched socket found, wait for more time");
|
||||
continue;
|
||||
};
|
||||
|
||||
// if hole punched but tunnel creation failed, need to retry entire process.
|
||||
match try_connect_with_socket(socket.socket.clone(), remote_mapped_addr.into()).await {
|
||||
Ok(tunnel) => {
|
||||
ret_tunnel.replace(tunnel);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
udp_array.add_new_socket(socket.socket).await?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let punch_task_result = scoped_punch_task.await;
|
||||
tracing::debug!(?punch_task_result, ?ret_tunnel, "punch task got result");
|
||||
|
||||
if let Ok(Some(next_port_idx)) = punch_task_result {
|
||||
*last_port_idx = next_port_idx as usize;
|
||||
} else {
|
||||
*last_port_idx = rand::random();
|
||||
}
|
||||
|
||||
Ok(ret_tunnel)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::{
|
||||
sync::{atomic::AtomicU32, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::{
|
||||
connector::udp_hole_punch::{
|
||||
tests::create_mock_peer_manager_with_mock_stun, UdpHolePunchConnector, RUN_TESTING,
|
||||
},
|
||||
peers::tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost},
|
||||
proto::common::NatType,
|
||||
tunnel::common::tests::wait_for_condition,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(hole_punch)]
|
||||
async fn hole_punching_symmetric_only_random() {
|
||||
RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let p_a = create_mock_peer_manager_with_mock_stun(NatType::Symmetric).await;
|
||||
let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
connect_peer_manager(p_a.clone(), p_b.clone()).await;
|
||||
connect_peer_manager(p_b.clone(), p_c.clone()).await;
|
||||
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
||||
|
||||
let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone());
|
||||
let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone());
|
||||
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.try_direct_connect
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.punch_predicablely
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
hole_punching_a.run().await.unwrap();
|
||||
hole_punching_c.run().await.unwrap();
|
||||
|
||||
hole_punching_a.client.run_immediately().await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.udp_array
|
||||
.read()
|
||||
.await
|
||||
.is_some()
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
wait_route_appear_with_cost(p_a.clone(), p_c.my_peer_id(), Some(1))
|
||||
.await
|
||||
.is_ok()
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
println!("{:?}", p_a.list_routes().await);
|
||||
|
||||
wait_for_condition(
|
||||
|| async {
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.udp_array
|
||||
.read()
|
||||
.await
|
||||
.is_none()
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial(hole_punch)]
|
||||
async fn hole_punching_symmetric_only_predict(#[values("true", "false")] is_inc: bool) {
|
||||
RUN_TESTING.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let p_a = create_mock_peer_manager_with_mock_stun(if is_inc {
|
||||
NatType::SymmetricEasyInc
|
||||
} else {
|
||||
NatType::SymmetricEasyDec
|
||||
})
|
||||
.await;
|
||||
let p_b = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await;
|
||||
connect_peer_manager(p_a.clone(), p_b.clone()).await;
|
||||
connect_peer_manager(p_b.clone(), p_c.clone()).await;
|
||||
wait_route_appear(p_a.clone(), p_c.clone()).await.unwrap();
|
||||
|
||||
let mut hole_punching_a = UdpHolePunchConnector::new(p_a.clone());
|
||||
let mut hole_punching_c = UdpHolePunchConnector::new(p_c.clone());
|
||||
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.try_direct_connect
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
hole_punching_a
|
||||
.client
|
||||
.data()
|
||||
.sym_to_cone_client
|
||||
.punch_randomly
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
hole_punching_a.run().await.unwrap();
|
||||
hole_punching_c.run().await.unwrap();
|
||||
|
||||
let udps = if is_inc {
|
||||
let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40147").await.unwrap());
|
||||
let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40194").await.unwrap());
|
||||
vec![udp1, udp2]
|
||||
} else {
|
||||
let udp1 = Arc::new(UdpSocket::bind("0.0.0.0:40141").await.unwrap());
|
||||
let udp2 = Arc::new(UdpSocket::bind("0.0.0.0:40100").await.unwrap());
|
||||
vec![udp1, udp2]
|
||||
};
|
||||
// let udp_dec = Arc::new(UdpSocket::bind("0.0.0.0:40140").await.unwrap());
|
||||
// let udp_dec2 = Arc::new(UdpSocket::bind("0.0.0.0:40050").await.unwrap());
|
||||
|
||||
let counter = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// all these sockets should receive hole punching packet
|
||||
for udp in udps.iter().map(Arc::clone) {
|
||||
let counter = counter.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut buf = [0u8; 1024];
|
||||
let (len, addr) = udp.recv_from(&mut buf).await.unwrap();
|
||||
println!(
|
||||
"got predictable punch packet, {:?} {:?} {:?}",
|
||||
len,
|
||||
addr,
|
||||
udp.local_addr()
|
||||
);
|
||||
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
|
||||
hole_punching_a.client.run_immediately().await;
|
||||
|
||||
let udp_len = udps.len();
|
||||
wait_for_condition(
|
||||
|| async { counter.load(std::sync::atomic::Ordering::Relaxed) == udp_len as u32 },
|
||||
Duration::from_secs(30),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,36 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{net::SocketAddr, time::Duration, vec};
|
||||
use std::{net::SocketAddr, sync::Mutex, time::Duration, vec};
|
||||
|
||||
use anyhow::{Context, Ok};
|
||||
use clap::{command, Args, Parser, Subcommand};
|
||||
use common::stun::StunInfoCollectorTrait;
|
||||
use rpc::vpn_portal_rpc_client::VpnPortalRpcClient;
|
||||
use common::{constants::EASYTIER_VERSION, stun::StunInfoCollectorTrait};
|
||||
use proto::{
|
||||
common::NatType,
|
||||
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
||||
rpc_impl::standalone::StandAloneClient,
|
||||
rpc_types::controller::BaseController,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use tunnel::tcp::TcpTunnelConnector;
|
||||
use utils::{list_peer_route_pair, PeerRoutePair};
|
||||
|
||||
mod arch;
|
||||
mod common;
|
||||
mod rpc;
|
||||
mod proto;
|
||||
mod tunnel;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
common::stun::StunInfoCollector,
|
||||
rpc::{
|
||||
connector_manage_rpc_client::ConnectorManageRpcClient,
|
||||
peer_center_rpc_client::PeerCenterRpcClient, peer_manage_rpc_client::PeerManageRpcClient,
|
||||
*,
|
||||
},
|
||||
proto::cli::*,
|
||||
utils::{cost_to_str, float_to_str},
|
||||
};
|
||||
use humansize::format_size;
|
||||
use tabled::settings::Style;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "easytier-cli", author, version, about, long_about = None)]
|
||||
#[command(name = "easytier-cli", author, version = EASYTIER_VERSION, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// the instance name
|
||||
#[arg(short = 'p', long, default_value = "127.0.0.1:15888")]
|
||||
@@ -48,6 +51,7 @@ enum SubCommand {
|
||||
Route(RouteArgs),
|
||||
PeerCenter,
|
||||
VpnPortal,
|
||||
Node(NodeArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -68,6 +72,7 @@ enum PeerSubCommand {
|
||||
Remove,
|
||||
List(PeerListArgs),
|
||||
ListForeign,
|
||||
ListGlobalForeign,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -101,56 +106,90 @@ enum ConnectorSubCommand {
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum Error {
|
||||
#[error("tonic transport error")]
|
||||
TonicTransportError(#[from] tonic::transport::Error),
|
||||
#[error("tonic rpc error")]
|
||||
TonicRpcError(#[from] tonic::Status),
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum NodeSubCommand {
|
||||
Info,
|
||||
Config,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct NodeArgs {
|
||||
#[command(subcommand)]
|
||||
sub_command: Option<NodeSubCommand>,
|
||||
}
|
||||
|
||||
type Error = anyhow::Error;
|
||||
|
||||
struct CommandHandler {
|
||||
addr: String,
|
||||
client: Mutex<RpcClient>,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
type RpcClient = StandAloneClient<TcpTunnelConnector>;
|
||||
|
||||
impl CommandHandler {
|
||||
async fn get_peer_manager_client(
|
||||
&self,
|
||||
) -> Result<PeerManageRpcClient<tonic::transport::Channel>, Error> {
|
||||
Ok(PeerManageRpcClient::connect(self.addr.clone()).await?)
|
||||
) -> Result<Box<dyn PeerManageRpc<Controller = BaseController>>, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.scoped_client::<PeerManageRpcClientFactory<BaseController>>("".to_string())
|
||||
.await
|
||||
.with_context(|| "failed to get peer manager client")?)
|
||||
}
|
||||
|
||||
async fn get_connector_manager_client(
|
||||
&self,
|
||||
) -> Result<ConnectorManageRpcClient<tonic::transport::Channel>, Error> {
|
||||
Ok(ConnectorManageRpcClient::connect(self.addr.clone()).await?)
|
||||
) -> Result<Box<dyn ConnectorManageRpc<Controller = BaseController>>, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.scoped_client::<ConnectorManageRpcClientFactory<BaseController>>("".to_string())
|
||||
.await
|
||||
.with_context(|| "failed to get connector manager client")?)
|
||||
}
|
||||
|
||||
async fn get_peer_center_client(
|
||||
&self,
|
||||
) -> Result<PeerCenterRpcClient<tonic::transport::Channel>, Error> {
|
||||
Ok(PeerCenterRpcClient::connect(self.addr.clone()).await?)
|
||||
) -> Result<Box<dyn PeerCenterRpc<Controller = BaseController>>, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.scoped_client::<PeerCenterRpcClientFactory<BaseController>>("".to_string())
|
||||
.await
|
||||
.with_context(|| "failed to get peer center client")?)
|
||||
}
|
||||
|
||||
async fn get_vpn_portal_client(
|
||||
&self,
|
||||
) -> Result<VpnPortalRpcClient<tonic::transport::Channel>, Error> {
|
||||
Ok(VpnPortalRpcClient::connect(self.addr.clone()).await?)
|
||||
) -> Result<Box<dyn VpnPortalRpc<Controller = BaseController>>, Error> {
|
||||
Ok(self
|
||||
.client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.scoped_client::<VpnPortalRpcClientFactory<BaseController>>("".to_string())
|
||||
.await
|
||||
.with_context(|| "failed to get vpn portal client")?)
|
||||
}
|
||||
|
||||
async fn list_peers(&self) -> Result<ListPeerResponse, Error> {
|
||||
let mut client = self.get_peer_manager_client().await?;
|
||||
let request = tonic::Request::new(ListPeerRequest::default());
|
||||
let response = client.list_peer(request).await?;
|
||||
Ok(response.into_inner())
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListPeerRequest::default();
|
||||
let response = client.list_peer(BaseController::default(), request).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn list_routes(&self) -> Result<ListRouteResponse, Error> {
|
||||
let mut client = self.get_peer_manager_client().await?;
|
||||
let request = tonic::Request::new(ListRouteRequest::default());
|
||||
let response = client.list_route(request).await?;
|
||||
Ok(response.into_inner())
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListRouteRequest::default();
|
||||
let response = client
|
||||
.list_route(BaseController::default(), request)
|
||||
.await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn list_peer_route_pair(&self) -> Result<Vec<PeerRoutePair>, Error> {
|
||||
@@ -182,12 +221,18 @@ impl CommandHandler {
|
||||
tunnel_proto: String,
|
||||
nat_type: String,
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl From<PeerRoutePair> for PeerTableItem {
|
||||
fn from(p: PeerRoutePair) -> Self {
|
||||
PeerTableItem {
|
||||
ipv4: p.route.ipv4_addr.clone(),
|
||||
ipv4: p
|
||||
.route
|
||||
.ipv4_addr
|
||||
.clone()
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_default(),
|
||||
hostname: p.route.hostname.clone(),
|
||||
cost: cost_to_str(p.route.cost),
|
||||
lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3),
|
||||
@@ -197,6 +242,33 @@ impl CommandHandler {
|
||||
tunnel_proto: p.get_conn_protos().unwrap_or(vec![]).join(",").to_string(),
|
||||
nat_type: p.get_udp_nat_type(),
|
||||
id: p.route.peer_id.to_string(),
|
||||
version: if p.route.version.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
p.route.version.to_string()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeInfo> for PeerTableItem {
|
||||
fn from(p: NodeInfo) -> Self {
|
||||
PeerTableItem {
|
||||
ipv4: p.ipv4_addr.clone(),
|
||||
hostname: p.hostname.clone(),
|
||||
cost: "Local".to_string(),
|
||||
lat_ms: "-".to_string(),
|
||||
loss_rate: "-".to_string(),
|
||||
rx_bytes: "-".to_string(),
|
||||
tx_bytes: "-".to_string(),
|
||||
tunnel_proto: "-".to_string(),
|
||||
nat_type: if let Some(info) = p.stun_info {
|
||||
info.udp_nat_type().as_str_name().to_string()
|
||||
} else {
|
||||
"Unknown".to_string()
|
||||
},
|
||||
id: p.peer_id.to_string(),
|
||||
version: p.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,6 +280,14 @@ impl CommandHandler {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
items.push(node_info.into());
|
||||
|
||||
for p in peer_routes {
|
||||
items.push(p.into());
|
||||
}
|
||||
@@ -221,18 +301,22 @@ impl CommandHandler {
|
||||
}
|
||||
|
||||
async fn handle_route_dump(&self) -> Result<(), Error> {
|
||||
let mut client = self.get_peer_manager_client().await?;
|
||||
let request = tonic::Request::new(DumpRouteRequest::default());
|
||||
let response = client.dump_route(request).await?;
|
||||
println!("response: {}", response.into_inner().result);
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = DumpRouteRequest::default();
|
||||
let response = client
|
||||
.dump_route(BaseController::default(), request)
|
||||
.await?;
|
||||
println!("response: {}", response.result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_foreign_network_list(&self) -> Result<(), Error> {
|
||||
let mut client = self.get_peer_manager_client().await?;
|
||||
let request = tonic::Request::new(ListForeignNetworkRequest::default());
|
||||
let response = client.list_foreign_network(request).await?;
|
||||
let network_map = response.into_inner();
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListForeignNetworkRequest::default();
|
||||
let response = client
|
||||
.list_foreign_network(BaseController::default(), request)
|
||||
.await?;
|
||||
let network_map = response;
|
||||
if self.verbose {
|
||||
println!("{:#?}", network_map);
|
||||
return Ok(());
|
||||
@@ -251,7 +335,7 @@ impl CommandHandler {
|
||||
"remote_addr: {}, rx_bytes: {}, tx_bytes: {}, latency_us: {}",
|
||||
conn.tunnel
|
||||
.as_ref()
|
||||
.map(|t| t.remote_addr.clone())
|
||||
.map(|t| t.remote_addr.clone().unwrap_or_default())
|
||||
.unwrap_or_default(),
|
||||
conn.stats.as_ref().map(|s| s.rx_bytes).unwrap_or_default(),
|
||||
conn.stats.as_ref().map(|s| s.tx_bytes).unwrap_or_default(),
|
||||
@@ -268,6 +352,30 @@ impl CommandHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_global_foreign_network_list(&self) -> Result<(), Error> {
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListGlobalForeignNetworkRequest::default();
|
||||
let response = client
|
||||
.list_global_foreign_network(BaseController::default(), request)
|
||||
.await?;
|
||||
if self.verbose {
|
||||
println!("{:#?}", response);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for (k, v) in response.foreign_networks.iter() {
|
||||
println!("Peer ID: {}", k);
|
||||
for n in v.foreign_networks.iter() {
|
||||
println!(
|
||||
" Network Name: {}, Last Updated: {}, Version: {}, PeerIds: {:?}",
|
||||
n.network_name, n.last_updated, n.version, n.peer_ids
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_route_list(&self) -> Result<(), Error> {
|
||||
#[derive(tabled::Tabled)]
|
||||
struct RouteTableItem {
|
||||
@@ -278,9 +386,27 @@ impl CommandHandler {
|
||||
next_hop_hostname: String,
|
||||
next_hop_lat: f64,
|
||||
cost: i32,
|
||||
version: String,
|
||||
}
|
||||
|
||||
let mut items: Vec<RouteTableItem> = vec![];
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
|
||||
items.push(RouteTableItem {
|
||||
ipv4: node_info.ipv4_addr.clone(),
|
||||
hostname: node_info.hostname.clone(),
|
||||
proxy_cidrs: node_info.proxy_cidrs.join(", "),
|
||||
next_hop_ipv4: "-".to_string(),
|
||||
next_hop_hostname: "Local".to_string(),
|
||||
next_hop_lat: 0.0,
|
||||
cost: 0,
|
||||
version: node_info.version.clone(),
|
||||
});
|
||||
let peer_routes = self.list_peer_route_pair().await?;
|
||||
for p in peer_routes.iter() {
|
||||
let Some(next_hop_pair) = peer_routes
|
||||
@@ -292,23 +418,48 @@ impl CommandHandler {
|
||||
|
||||
if p.route.cost == 1 {
|
||||
items.push(RouteTableItem {
|
||||
ipv4: p.route.ipv4_addr.clone(),
|
||||
ipv4: p
|
||||
.route
|
||||
.ipv4_addr
|
||||
.clone()
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_default(),
|
||||
hostname: p.route.hostname.clone(),
|
||||
proxy_cidrs: p.route.proxy_cidrs.clone().join(",").to_string(),
|
||||
next_hop_ipv4: "DIRECT".to_string(),
|
||||
next_hop_hostname: "".to_string(),
|
||||
next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0),
|
||||
cost: p.route.cost,
|
||||
version: if p.route.version.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
p.route.version.to_string()
|
||||
},
|
||||
});
|
||||
} else {
|
||||
items.push(RouteTableItem {
|
||||
ipv4: p.route.ipv4_addr.clone(),
|
||||
ipv4: p
|
||||
.route
|
||||
.ipv4_addr
|
||||
.clone()
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_default(),
|
||||
hostname: p.route.hostname.clone(),
|
||||
proxy_cidrs: p.route.proxy_cidrs.clone().join(",").to_string(),
|
||||
next_hop_ipv4: next_hop_pair.route.ipv4_addr.clone(),
|
||||
next_hop_ipv4: next_hop_pair
|
||||
.route
|
||||
.ipv4_addr
|
||||
.clone()
|
||||
.map(|ip| ip.to_string())
|
||||
.unwrap_or_default(),
|
||||
next_hop_hostname: next_hop_pair.route.hostname.clone(),
|
||||
next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0),
|
||||
cost: p.route.cost,
|
||||
version: if p.route.version.is_empty() {
|
||||
"unknown".to_string()
|
||||
} else {
|
||||
p.route.version.to_string()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -322,10 +473,12 @@ impl CommandHandler {
|
||||
}
|
||||
|
||||
async fn handle_connector_list(&self) -> Result<(), Error> {
|
||||
let mut client = self.get_connector_manager_client().await?;
|
||||
let request = tonic::Request::new(ListConnectorRequest::default());
|
||||
let response = client.list_connector(request).await?;
|
||||
println!("response: {:#?}", response.into_inner());
|
||||
let client = self.get_connector_manager_client().await?;
|
||||
let request = ListConnectorRequest::default();
|
||||
let response = client
|
||||
.list_connector(BaseController::default(), request)
|
||||
.await?;
|
||||
println!("response: {:#?}", response);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -334,8 +487,13 @@ impl CommandHandler {
|
||||
#[tracing::instrument]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
let client = RpcClient::new(TcpTunnelConnector::new(
|
||||
format!("tcp://{}:{}", cli.rpc_portal.ip(), cli.rpc_portal.port())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
));
|
||||
let handler = CommandHandler {
|
||||
addr: format!("http://{}:{}", cli.rpc_portal.ip(), cli.rpc_portal.port()),
|
||||
client: Mutex::new(client),
|
||||
verbose: cli.verbose,
|
||||
};
|
||||
|
||||
@@ -357,6 +515,9 @@ async fn main() -> Result<(), Error> {
|
||||
Some(PeerSubCommand::ListForeign) => {
|
||||
handler.handle_foreign_network_list().await?;
|
||||
}
|
||||
Some(PeerSubCommand::ListGlobalForeign) => {
|
||||
handler.handle_global_foreign_network_list().await?;
|
||||
}
|
||||
None => {
|
||||
handler.handle_peer_list(&peer_args).await?;
|
||||
}
|
||||
@@ -380,7 +541,7 @@ async fn main() -> Result<(), Error> {
|
||||
Some(RouteSubCommand::Dump) => handler.handle_route_dump().await?,
|
||||
},
|
||||
SubCommand::Stun => {
|
||||
timeout(Duration::from_secs(5), async move {
|
||||
timeout(Duration::from_secs(25), async move {
|
||||
let collector = StunInfoCollector::new_with_default_servers();
|
||||
loop {
|
||||
let ret = collector.get_stun_info();
|
||||
@@ -395,11 +556,13 @@ async fn main() -> Result<(), Error> {
|
||||
.unwrap();
|
||||
}
|
||||
SubCommand::PeerCenter => {
|
||||
let mut peer_center_client = handler.get_peer_center_client().await?;
|
||||
let peer_center_client = handler.get_peer_center_client().await?;
|
||||
let resp = peer_center_client
|
||||
.get_global_peer_map(GetGlobalPeerMapRequest::default())
|
||||
.await?
|
||||
.into_inner();
|
||||
.get_global_peer_map(
|
||||
BaseController::default(),
|
||||
GetGlobalPeerMapRequest::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[derive(tabled::Tabled)]
|
||||
struct PeerCenterTableItem {
|
||||
@@ -429,11 +592,13 @@ async fn main() -> Result<(), Error> {
|
||||
);
|
||||
}
|
||||
SubCommand::VpnPortal => {
|
||||
let mut vpn_portal_client = handler.get_vpn_portal_client().await?;
|
||||
let vpn_portal_client = handler.get_vpn_portal_client().await?;
|
||||
let resp = vpn_portal_client
|
||||
.get_vpn_portal_info(GetVpnPortalInfoRequest::default())
|
||||
.get_vpn_portal_info(
|
||||
BaseController::default(),
|
||||
GetVpnPortalInfoRequest::default(),
|
||||
)
|
||||
.await?
|
||||
.into_inner()
|
||||
.vpn_portal_info
|
||||
.unwrap_or_default();
|
||||
println!("portal_name: {}", resp.vpn_type);
|
||||
@@ -447,6 +612,44 @@ async fn main() -> Result<(), Error> {
|
||||
);
|
||||
println!("connected_clients:\n{:#?}", resp.connected_clients);
|
||||
}
|
||||
SubCommand::Node(sub_cmd) => {
|
||||
let client = handler.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(BaseController::default(), ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
match sub_cmd.sub_command {
|
||||
Some(NodeSubCommand::Info) | None => {
|
||||
let stun_info = node_info.stun_info.clone().unwrap_or_default();
|
||||
|
||||
let mut builder = tabled::builder::Builder::default();
|
||||
builder.push_record(vec!["Virtual IP", node_info.ipv4_addr.as_str()]);
|
||||
builder.push_record(vec!["Hostname", node_info.hostname.as_str()]);
|
||||
builder.push_record(vec![
|
||||
"Proxy CIDRs",
|
||||
node_info.proxy_cidrs.join(", ").as_str(),
|
||||
]);
|
||||
builder.push_record(vec!["Peer ID", node_info.peer_id.to_string().as_str()]);
|
||||
builder.push_record(vec!["Public IP", stun_info.public_ip.join(", ").as_str()]);
|
||||
builder.push_record(vec![
|
||||
"UDP Stun Type",
|
||||
format!("{:?}", stun_info.udp_nat_type()).as_str(),
|
||||
]);
|
||||
for (idx, l) in node_info.listeners.iter().enumerate() {
|
||||
if l.starts_with("ring") {
|
||||
continue;
|
||||
}
|
||||
builder.push_record(vec![format!("Listener {}", idx).as_str(), l]);
|
||||
}
|
||||
|
||||
println!("{}", builder.build().with(Style::modern()).to_string());
|
||||
}
|
||||
Some(NodeSubCommand::Config) => {
|
||||
println!("{}", node_info.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
mod tests;
|
||||
|
||||
use std::{
|
||||
backtrace,
|
||||
io::Write as _,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
@@ -23,16 +21,18 @@ mod gateway;
|
||||
mod instance;
|
||||
mod peer_center;
|
||||
mod peers;
|
||||
mod rpc;
|
||||
mod proto;
|
||||
mod tunnel;
|
||||
mod utils;
|
||||
mod vpn_portal;
|
||||
|
||||
use common::config::{
|
||||
ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig, VpnPortalConfig,
|
||||
use common::{
|
||||
config::{ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig, VpnPortalConfig},
|
||||
constants::EASYTIER_VERSION,
|
||||
};
|
||||
use instance::instance::Instance;
|
||||
use tokio::net::TcpSocket;
|
||||
use utils::setup_panic_handler;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
@@ -50,7 +50,7 @@ use mimalloc_rust::*;
|
||||
static GLOBAL_MIMALLOC: GlobalMiMalloc = GlobalMiMalloc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "easytier-core", author, version, about, long_about = None)]
|
||||
#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
short,
|
||||
@@ -259,20 +259,53 @@ struct Cli {
|
||||
num_args = 0..
|
||||
)]
|
||||
relay_network_whitelist: Option<Vec<String>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.disable_p2p").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
disable_p2p: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.disable_udp_hole_punching").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
disable_udp_hole_punching: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.relay_all_peer_rpc").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
relay_all_peer_rpc: bool,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.socks5").to_string()
|
||||
)]
|
||||
socks5: Option<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.ipv6_listener").to_string()
|
||||
)]
|
||||
ipv6_listener: Option<String>,
|
||||
}
|
||||
|
||||
rust_i18n::i18n!("locales");
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
|
||||
impl Cli {
|
||||
fn parse_listeners(&self) -> Vec<String> {
|
||||
println!("parsing listeners: {:?}", self.listeners);
|
||||
fn parse_listeners(no_listener: bool, listeners: Vec<String>) -> Vec<String> {
|
||||
let proto_port_offset = vec![("tcp", 0), ("udp", 0), ("wg", 1), ("ws", 1), ("wss", 2)];
|
||||
|
||||
if self.no_listener || self.listeners.is_empty() {
|
||||
if no_listener || listeners.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let origin_listners = self.listeners.clone();
|
||||
let origin_listners = listeners;
|
||||
let mut listeners: Vec<String> = Vec::new();
|
||||
if origin_listners.len() == 1 {
|
||||
if let Ok(port) = origin_listners[0].parse::<u16>() {
|
||||
@@ -313,12 +346,12 @@ impl Cli {
|
||||
}
|
||||
|
||||
fn check_tcp_available(port: u16) -> Option<SocketAddr> {
|
||||
let s = format!("127.0.0.1:{}", port).parse::<SocketAddr>().unwrap();
|
||||
let s = format!("0.0.0.0:{}", port).parse::<SocketAddr>().unwrap();
|
||||
TcpSocket::new_v4().unwrap().bind(s).map(|_| s).ok()
|
||||
}
|
||||
|
||||
fn parse_rpc_portal(&self) -> SocketAddr {
|
||||
if let Ok(port) = self.rpc_portal.parse::<u16>() {
|
||||
fn parse_rpc_portal(rpc_portal: String) -> SocketAddr {
|
||||
if let Ok(port) = rpc_portal.parse::<u16>() {
|
||||
if port == 0 {
|
||||
// check tcp 15888 first
|
||||
for i in 15888..15900 {
|
||||
@@ -326,12 +359,12 @@ impl Cli {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return "127.0.0.1:0".parse().unwrap();
|
||||
return "0.0.0.0:0".parse().unwrap();
|
||||
}
|
||||
return format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
return format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
}
|
||||
|
||||
self.rpc_portal.parse().unwrap()
|
||||
rpc_portal.parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,25 +382,18 @@ impl From<Cli> for TomlConfigLoader {
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
|
||||
cfg.set_inst_name(cli.instance_name.clone());
|
||||
cfg.set_hostname(cli.hostname);
|
||||
|
||||
cfg.set_hostname(cli.hostname.clone());
|
||||
|
||||
cfg.set_network_identity(NetworkIdentity::new(
|
||||
cli.network_name.clone(),
|
||||
cli.network_secret.clone(),
|
||||
));
|
||||
cfg.set_network_identity(NetworkIdentity::new(cli.network_name, cli.network_secret));
|
||||
|
||||
cfg.set_dhcp(cli.dhcp);
|
||||
|
||||
if !cli.dhcp {
|
||||
if let Some(ipv4) = &cli.ipv4 {
|
||||
cfg.set_ipv4(Some(
|
||||
ipv4.parse()
|
||||
.with_context(|| format!("failed to parse ipv4 address: {}", ipv4))
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
if let Some(ipv4) = &cli.ipv4 {
|
||||
cfg.set_ipv4(Some(
|
||||
ipv4.parse()
|
||||
.with_context(|| format!("failed to parse ipv4 address: {}", ipv4))
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
cfg.set_peers(
|
||||
@@ -383,7 +409,7 @@ impl From<Cli> for TomlConfigLoader {
|
||||
);
|
||||
|
||||
cfg.set_listeners(
|
||||
cli.parse_listeners()
|
||||
Cli::parse_listeners(cli.no_listener, cli.listeners)
|
||||
.into_iter()
|
||||
.map(|s| s.parse().unwrap())
|
||||
.collect(),
|
||||
@@ -397,21 +423,15 @@ impl From<Cli> for TomlConfigLoader {
|
||||
);
|
||||
}
|
||||
|
||||
cfg.set_rpc_portal(cli.parse_rpc_portal());
|
||||
cfg.set_rpc_portal(Cli::parse_rpc_portal(cli.rpc_portal));
|
||||
|
||||
if cli.external_node.is_some() {
|
||||
if let Some(external_nodes) = cli.external_node {
|
||||
let mut old_peers = cfg.get_peers();
|
||||
old_peers.push(PeerConfig {
|
||||
uri: cli
|
||||
.external_node
|
||||
.clone()
|
||||
.unwrap()
|
||||
uri: external_nodes
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse external node uri: {}",
|
||||
cli.external_node.unwrap()
|
||||
)
|
||||
format!("failed to parse external node uri: {}", external_nodes)
|
||||
})
|
||||
.unwrap(),
|
||||
});
|
||||
@@ -420,7 +440,7 @@ impl From<Cli> for TomlConfigLoader {
|
||||
|
||||
if cli.console_log_level.is_some() {
|
||||
cfg.set_console_logger_config(ConsoleLoggerConfig {
|
||||
level: cli.console_log_level.clone(),
|
||||
level: cli.console_log_level,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -432,18 +452,12 @@ impl From<Cli> for TomlConfigLoader {
|
||||
});
|
||||
}
|
||||
|
||||
if cli.vpn_portal.is_some() {
|
||||
let url: url::Url = cli
|
||||
.vpn_portal
|
||||
.clone()
|
||||
.unwrap()
|
||||
cfg.set_inst_name(cli.instance_name);
|
||||
|
||||
if let Some(vpn_portal) = cli.vpn_portal {
|
||||
let url: url::Url = vpn_portal
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse vpn portal url: {}",
|
||||
cli.vpn_portal.unwrap()
|
||||
)
|
||||
})
|
||||
.with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))
|
||||
.unwrap();
|
||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||
client_cidr: url.path()[1..]
|
||||
@@ -464,11 +478,9 @@ impl From<Cli> for TomlConfigLoader {
|
||||
});
|
||||
}
|
||||
|
||||
if cli.manual_routes.is_some() {
|
||||
if let Some(manual_routes) = cli.manual_routes {
|
||||
cfg.set_routes(Some(
|
||||
cli.manual_routes
|
||||
.clone()
|
||||
.unwrap()
|
||||
manual_routes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
s.parse()
|
||||
@@ -479,6 +491,15 @@ impl From<Cli> for TomlConfigLoader {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
if let Some(socks5_proxy) = cli.socks5 {
|
||||
cfg.set_socks5_portal(Some(
|
||||
format!("socks5://0.0.0.0:{}", socks5_proxy)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut f = cfg.get_flags();
|
||||
if cli.default_protocol.is_some() {
|
||||
f.default_protocol = cli.default_protocol.as_ref().unwrap().clone();
|
||||
@@ -496,6 +517,14 @@ impl From<Cli> for TomlConfigLoader {
|
||||
if let Some(wl) = cli.relay_network_whitelist {
|
||||
f.foreign_network_whitelist = wl.join(" ");
|
||||
}
|
||||
f.disable_p2p = cli.disable_p2p;
|
||||
f.relay_all_peer_rpc = cli.relay_all_peer_rpc;
|
||||
if let Some(ipv6_listener) = cli.ipv6_listener {
|
||||
f.ipv6_listener = ipv6_listener
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener))
|
||||
.unwrap();
|
||||
}
|
||||
cfg.set_flags(f);
|
||||
|
||||
cfg.set_exit_nodes(cli.exit_nodes.clone());
|
||||
@@ -512,23 +541,13 @@ fn print_event(msg: String) {
|
||||
);
|
||||
}
|
||||
|
||||
fn peer_conn_info_to_string(p: crate::rpc::PeerConnInfo) -> String {
|
||||
fn peer_conn_info_to_string(p: crate::proto::cli::PeerConnInfo) -> String {
|
||||
format!(
|
||||
"my_peer_id: {}, dst_peer_id: {}, tunnel_info: {:?}",
|
||||
p.my_peer_id, p.peer_id, p.tunnel
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_panic_handler() {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let backtrace = backtrace::Backtrace::force_capture();
|
||||
println!("panic occurred: {:?}", info);
|
||||
let _ = std::fs::File::create("easytier-panic.log")
|
||||
.and_then(|mut f| f.write_all(format!("{:?}\n{:#?}", info, backtrace).as_bytes()));
|
||||
std::process::exit(1);
|
||||
}));
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn async_main(cli: Cli) {
|
||||
let cfg: TomlConfigLoader = cli.into();
|
||||
|
||||
21
easytier/src/gateway/fast_socks5/LICENSE
Normal file
21
easytier/src/gateway/fast_socks5/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jonathan Dizdarevic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
easytier/src/gateway/fast_socks5/README.md
Normal file
1
easytier/src/gateway/fast_socks5/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Code is modified from https://github.com/dizda/fast-socks5
|
||||
314
easytier/src/gateway/fast_socks5/mod.rs
Normal file
314
easytier/src/gateway/fast_socks5/mod.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
//! Fast SOCKS5 client/server implementation written in Rust async/.await (with tokio).
|
||||
//!
|
||||
//! This library is maintained by [anyip.io](https://anyip.io/) a residential and mobile socks5 proxy provider.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - An `async`/`.await` [SOCKS5](https://tools.ietf.org/html/rfc1928) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4 Client](https://www.openssh.com/txt/socks4.protocol) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4a Client](https://www.openssh.com/txt/socks4a.protocol) implementation.
|
||||
//! - No **unsafe** code
|
||||
//! - Built on-top of `tokio` library
|
||||
//! - Ultra lightweight and scalable
|
||||
//! - No system dependencies
|
||||
//! - Cross-platform
|
||||
//! - Authentication methods:
|
||||
//! - No-Auth method
|
||||
//! - Username/Password auth method
|
||||
//! - Custom auth methods can be implemented via the Authentication Trait
|
||||
//! - Credentials returned on authentication success
|
||||
//! - All SOCKS5 RFC errors (replies) should be mapped
|
||||
//! - `AsyncRead + AsyncWrite` traits are implemented on Socks5Stream & Socks5Socket
|
||||
//! - `IPv4`, `IPv6`, and `Domains` types are supported
|
||||
//! - Config helper for Socks5Server
|
||||
//! - Helpers to run a Socks5Server à la *"std's TcpStream"* via `incoming.next().await`
|
||||
//! - Examples come with real cases commands scenarios
|
||||
//! - Can disable `DNS resolving`
|
||||
//! - Can skip the authentication/handshake process, which will directly handle command's request (useful to save useless round-trips in a current authenticated environment)
|
||||
//! - Can disable command execution (useful if you just want to forward the request to a different server)
|
||||
//!
|
||||
//!
|
||||
//! ## Install
|
||||
//!
|
||||
//! Open in [crates.io](https://crates.io/crates/fast-socks5).
|
||||
//!
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Please check [`examples`](https://github.com/dizda/fast-socks5/tree/master/examples) directory.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod server;
|
||||
pub mod util;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use util::target_addr::read_address;
|
||||
use util::target_addr::TargetAddr;
|
||||
use util::target_addr::ToTargetAddr;
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::read_exact;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub mod consts {
|
||||
pub const SOCKS5_VERSION: u8 = 0x05;
|
||||
|
||||
pub const SOCKS5_AUTH_METHOD_NONE: u8 = 0x00;
|
||||
pub const SOCKS5_AUTH_METHOD_GSSAPI: u8 = 0x01;
|
||||
pub const SOCKS5_AUTH_METHOD_PASSWORD: u8 = 0x02;
|
||||
pub const SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE: u8 = 0xff;
|
||||
|
||||
pub const SOCKS5_CMD_TCP_CONNECT: u8 = 0x01;
|
||||
pub const SOCKS5_CMD_TCP_BIND: u8 = 0x02;
|
||||
pub const SOCKS5_CMD_UDP_ASSOCIATE: u8 = 0x03;
|
||||
|
||||
pub const SOCKS5_ADDR_TYPE_IPV4: u8 = 0x01;
|
||||
pub const SOCKS5_ADDR_TYPE_DOMAIN_NAME: u8 = 0x03;
|
||||
pub const SOCKS5_ADDR_TYPE_IPV6: u8 = 0x04;
|
||||
|
||||
pub const SOCKS5_REPLY_SUCCEEDED: u8 = 0x00;
|
||||
pub const SOCKS5_REPLY_GENERAL_FAILURE: u8 = 0x01;
|
||||
pub const SOCKS5_REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02;
|
||||
pub const SOCKS5_REPLY_NETWORK_UNREACHABLE: u8 = 0x03;
|
||||
pub const SOCKS5_REPLY_HOST_UNREACHABLE: u8 = 0x04;
|
||||
pub const SOCKS5_REPLY_CONNECTION_REFUSED: u8 = 0x05;
|
||||
pub const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06;
|
||||
pub const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
|
||||
pub const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Socks5Command {
|
||||
TCPConnect,
|
||||
TCPBind,
|
||||
UDPAssociate,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Socks5Command {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Socks5Command::TCPConnect => consts::SOCKS5_CMD_TCP_CONNECT,
|
||||
Socks5Command::TCPBind => consts::SOCKS5_CMD_TCP_BIND,
|
||||
Socks5Command::UDPAssociate => consts::SOCKS5_CMD_UDP_ASSOCIATE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn from_u8(code: u8) -> Option<Socks5Command> {
|
||||
match code {
|
||||
consts::SOCKS5_CMD_TCP_CONNECT => Some(Socks5Command::TCPConnect),
|
||||
consts::SOCKS5_CMD_TCP_BIND => Some(Socks5Command::TCPBind),
|
||||
consts::SOCKS5_CMD_UDP_ASSOCIATE => Some(Socks5Command::UDPAssociate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AuthenticationMethod {
|
||||
None,
|
||||
Password { username: String, password: String },
|
||||
}
|
||||
|
||||
impl AuthenticationMethod {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
AuthenticationMethod::None => consts::SOCKS5_AUTH_METHOD_NONE,
|
||||
AuthenticationMethod::Password {..} =>
|
||||
consts::SOCKS5_AUTH_METHOD_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn from_u8(code: u8) -> Option<AuthenticationMethod> {
|
||||
match code {
|
||||
consts::SOCKS5_AUTH_METHOD_NONE => Some(AuthenticationMethod::None),
|
||||
consts::SOCKS5_AUTH_METHOD_PASSWORD => Some(AuthenticationMethod::Password { username: "test".to_string(), password: "test".to_string()}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthenticationMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AuthenticationMethod::None => f.write_str("AuthenticationMethod::None"),
|
||||
AuthenticationMethod::Password { .. } => f.write_str("AuthenticationMethod::Password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//impl Vec<AuthenticationMethod> {
|
||||
// pub fn as_bytes(&self) -> &[u8] {
|
||||
// self.iter().map(|l| l.as_u8()).collect()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<&[AuthenticationMethod]> for &[u8] {
|
||||
// fn from(_: Vec<AuthenticationMethod>) -> Self {
|
||||
// &[0x00]
|
||||
// }
|
||||
//}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SocksError {
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("the data for key `{0}` is not available")]
|
||||
Redaction(String),
|
||||
#[error("invalid header (expected {expected:?}, found {found:?})")]
|
||||
InvalidHeader { expected: String, found: String },
|
||||
|
||||
#[error("Auth method unacceptable `{0:?}`.")]
|
||||
AuthMethodUnacceptable(Vec<u8>),
|
||||
#[error("Unsupported SOCKS version `{0}`.")]
|
||||
UnsupportedSocksVersion(u8),
|
||||
#[error("Domain exceeded max sequence length")]
|
||||
ExceededMaxDomainLen(usize),
|
||||
#[error("Authentication failed `{0}`")]
|
||||
AuthenticationFailed(String),
|
||||
#[error("Authentication rejected `{0}`")]
|
||||
AuthenticationRejected(String),
|
||||
|
||||
#[error("Error with reply: {0}.")]
|
||||
ReplyError(#[from] ReplyError),
|
||||
|
||||
#[error("Argument input error: `{0}`.")]
|
||||
ArgumentInputError(&'static str),
|
||||
|
||||
// #[error("Other: `{0}`.")]
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T, E = SocksError> = core::result::Result<T, E>;
|
||||
|
||||
/// SOCKS5 reply code
|
||||
#[derive(Error, Debug, Copy, Clone)]
|
||||
pub enum ReplyError {
|
||||
#[error("Succeeded")]
|
||||
Succeeded,
|
||||
#[error("General failure")]
|
||||
GeneralFailure,
|
||||
#[error("Connection not allowed by ruleset")]
|
||||
ConnectionNotAllowed,
|
||||
#[error("Network unreachable")]
|
||||
NetworkUnreachable,
|
||||
#[error("Host unreachable")]
|
||||
HostUnreachable,
|
||||
#[error("Connection refused")]
|
||||
ConnectionRefused,
|
||||
#[error("Connection timeout")]
|
||||
ConnectionTimeout,
|
||||
#[error("TTL expired")]
|
||||
TtlExpired,
|
||||
#[error("Command not supported")]
|
||||
CommandNotSupported,
|
||||
#[error("Address type not supported")]
|
||||
AddressTypeNotSupported,
|
||||
// OtherReply(u8),
|
||||
}
|
||||
|
||||
impl ReplyError {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
pub fn as_u8(self) -> u8 {
|
||||
match self {
|
||||
ReplyError::Succeeded => consts::SOCKS5_REPLY_SUCCEEDED,
|
||||
ReplyError::GeneralFailure => consts::SOCKS5_REPLY_GENERAL_FAILURE,
|
||||
ReplyError::ConnectionNotAllowed => consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
|
||||
ReplyError::NetworkUnreachable => consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
|
||||
ReplyError::HostUnreachable => consts::SOCKS5_REPLY_HOST_UNREACHABLE,
|
||||
ReplyError::ConnectionRefused => consts::SOCKS5_REPLY_CONNECTION_REFUSED,
|
||||
ReplyError::ConnectionTimeout => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||
ReplyError::TtlExpired => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||
ReplyError::CommandNotSupported => consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
|
||||
ReplyError::AddressTypeNotSupported => consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
|
||||
// ReplyError::OtherReply(c) => c,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
pub fn from_u8(code: u8) -> ReplyError {
|
||||
match code {
|
||||
consts::SOCKS5_REPLY_SUCCEEDED => ReplyError::Succeeded,
|
||||
consts::SOCKS5_REPLY_GENERAL_FAILURE => ReplyError::GeneralFailure,
|
||||
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED => ReplyError::ConnectionNotAllowed,
|
||||
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE => ReplyError::NetworkUnreachable,
|
||||
consts::SOCKS5_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable,
|
||||
consts::SOCKS5_REPLY_CONNECTION_REFUSED => ReplyError::ConnectionRefused,
|
||||
consts::SOCKS5_REPLY_TTL_EXPIRED => ReplyError::TtlExpired,
|
||||
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED => ReplyError::CommandNotSupported,
|
||||
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED => ReplyError::AddressTypeNotSupported,
|
||||
// _ => ReplyError::OtherReply(code),
|
||||
_ => unreachable!("ReplyError code unsupported."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate UDP header
|
||||
///
|
||||
/// # UDP Request header structure.
|
||||
/// ```text
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
/// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
/// | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
///
|
||||
/// The fields in the UDP request header are:
|
||||
///
|
||||
/// o RSV Reserved X'0000'
|
||||
/// o FRAG Current fragment number
|
||||
/// o ATYP address type of following addresses:
|
||||
/// o IP V4 address: X'01'
|
||||
/// o DOMAINNAME: X'03'
|
||||
/// o IP V6 address: X'04'
|
||||
/// o DST.ADDR desired destination address
|
||||
/// o DST.PORT desired destination port
|
||||
/// o DATA user data
|
||||
/// ```
|
||||
pub fn new_udp_header<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>> {
|
||||
let mut header = vec![
|
||||
0, 0, // RSV
|
||||
0, // FRAG
|
||||
];
|
||||
header.append(&mut target_addr.to_target_addr()?.to_be_bytes()?);
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Parse data from UDP client on raw buffer, return (frag, target_addr, payload).
|
||||
pub async fn parse_udp_request<'a>(mut req: &'a [u8]) -> Result<(u8, TargetAddr, &'a [u8])> {
|
||||
let rsv = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
if !rsv.eq(&[0u8; 2]) {
|
||||
return Err(ReplyError::GeneralFailure.into());
|
||||
}
|
||||
|
||||
let [frag, atyp] = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
let target_addr = read_address(&mut req, atyp).await.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
Ok((frag, target_addr, req))
|
||||
}
|
||||
842
easytier/src/gateway/fast_socks5/server.rs
Normal file
842
easytier/src/gateway/fast_socks5/server.rs
Normal file
@@ -0,0 +1,842 @@
|
||||
use super::new_udp_header;
|
||||
use super::parse_udp_request;
|
||||
use super::read_exact;
|
||||
use super::util::stream::tcp_connect_with_timeout;
|
||||
use super::util::target_addr::{read_address, TargetAddr};
|
||||
use super::Socks5Command;
|
||||
use super::{consts, AuthenticationMethod, ReplyError, Result, SocksError};
|
||||
use anyhow::Context;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::try_join;
|
||||
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config<A: Authentication = DenyAuthentication> {
|
||||
/// Timeout of the command request
|
||||
request_timeout: u64,
|
||||
/// Avoid useless roundtrips if we don't need the Authentication layer
|
||||
skip_auth: bool,
|
||||
/// Enable dns-resolving
|
||||
dns_resolve: bool,
|
||||
/// Enable command execution
|
||||
execute_command: bool,
|
||||
/// Enable UDP support
|
||||
allow_udp: bool,
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 1-2 auth methods (no auth) rather than 3 (with auth)
|
||||
allow_no_auth: bool,
|
||||
/// Contains the authentication trait to use the user against with
|
||||
auth: Option<Arc<A>>,
|
||||
}
|
||||
|
||||
impl<A: Authentication> Default for Config<A> {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
request_timeout: 10,
|
||||
skip_auth: false,
|
||||
dns_resolve: true,
|
||||
execute_command: true,
|
||||
allow_udp: false,
|
||||
allow_no_auth: false,
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this trait to handle a custom authentication on your end.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Authentication: Send + Sync {
|
||||
type Item;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item>;
|
||||
}
|
||||
|
||||
/// Basic user/pass auth method provided.
|
||||
pub struct SimpleUserPassword {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The struct returned when the user has successfully authenticated
|
||||
pub struct AuthSucceeded {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// This is an example to auth via simple credentials.
|
||||
/// If the auth succeed, we return the username authenticated with, for further uses.
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for SimpleUserPassword {
|
||||
type Item = AuthSucceeded;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
if let Some((username, password)) = credentials {
|
||||
// Client has supplied credentials
|
||||
if username == self.username && password == self.password {
|
||||
// Some() will allow the authentication and the credentials
|
||||
// will be forwarded to the socket
|
||||
Some(AuthSucceeded { username })
|
||||
} else {
|
||||
// Credentials incorrect, we deny the auth
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// The client hasn't supplied any credentials, which only happens
|
||||
// when `Config::allow_no_auth()` is set as `true`
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will simply return Option::None, which denies the authentication
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct DenyAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for DenyAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// While this one will always allow the user in.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct AcceptAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for AcceptAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Authentication> Config<A> {
|
||||
/// How much time it should wait until the request timeout.
|
||||
pub fn set_request_timeout(&mut self, n: u64) -> &mut Self {
|
||||
self.request_timeout = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Skip the entire auth/handshake part, which means the server will directly wait for
|
||||
/// the command request.
|
||||
pub fn set_skip_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.skip_auth = value;
|
||||
self.auth = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable authentication
|
||||
/// 'static lifetime for Authentication avoid us to use `dyn Authentication`
|
||||
/// and set the Arc before calling the function.
|
||||
pub fn with_authentication<T: Authentication + 'static>(self, authentication: T) -> Config<T> {
|
||||
Config {
|
||||
request_timeout: self.request_timeout,
|
||||
skip_auth: self.skip_auth,
|
||||
dns_resolve: self.dns_resolve,
|
||||
execute_command: self.execute_command,
|
||||
allow_udp: self.allow_udp,
|
||||
allow_no_auth: self.allow_no_auth,
|
||||
auth: Some(Arc::new(authentication)),
|
||||
}
|
||||
}
|
||||
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 2 auth methods rather than 3 (with auth)
|
||||
pub fn set_allow_no_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_no_auth = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to execute commands
|
||||
pub fn set_execute_command(&mut self, value: bool) -> &mut Self {
|
||||
self.execute_command = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Will the server perform dns resolve
|
||||
pub fn set_dns_resolve(&mut self, value: bool) -> &mut Self {
|
||||
self.dns_resolve = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to allow udp traffic
|
||||
pub fn set_udp_support(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_udp = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AsyncTcpConnector {
|
||||
type S: AsyncRead + AsyncWrite + Unpin + Send + Sync;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<Self::S>;
|
||||
}
|
||||
|
||||
pub struct DefaultTcpConnector {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for DefaultTcpConnector {
|
||||
type S = TcpStream;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<TcpStream> {
|
||||
tcp_connect_with_timeout(addr, timeout_s).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap TcpStream and contains Socks5 protocol implementation.
|
||||
pub struct Socks5Socket<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
{
|
||||
inner: T,
|
||||
config: Arc<Config<A>>,
|
||||
auth: AuthenticationMethod,
|
||||
target_addr: Option<TargetAddr>,
|
||||
cmd: Option<Socks5Command>,
|
||||
/// Socket address which will be used in the reply message.
|
||||
reply_ip: Option<IpAddr>,
|
||||
/// If the client has been authenticated, that's where we store his credentials
|
||||
/// to be accessed from the socket
|
||||
credentials: Option<A::Item>,
|
||||
tcp_connector: C,
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
Socks5Socket<T, A, C>
|
||||
{
|
||||
pub fn new(socket: T, config: Arc<Config<A>>, tcp_connector: C) -> Self {
|
||||
Socks5Socket {
|
||||
inner: socket,
|
||||
config,
|
||||
auth: AuthenticationMethod::None,
|
||||
target_addr: None,
|
||||
cmd: None,
|
||||
reply_ip: None,
|
||||
credentials: None,
|
||||
tcp_connector,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the bind IP address in Socks5Reply.
|
||||
///
|
||||
/// Only the inner socket owner knows the correct reply bind addr, so leave this field to be
|
||||
/// populated. For those strict clients, users can use this function to set the correct IP
|
||||
/// address.
|
||||
///
|
||||
/// Most popular SOCKS5 clients [1] [2] ignore BND.ADDR and BND.PORT the reply of command
|
||||
/// CONNECT, but this field could be useful in some other command, such as UDP ASSOCIATE.
|
||||
///
|
||||
/// [1]: https://github.com/chromium/chromium/blob/bd2c7a8b65ec42d806277dd30f138a673dec233a/net/socket/socks5_client_socket.cc#L481
|
||||
/// [2]: https://github.com/curl/curl/blob/d15692ebbad5e9cfb871b0f7f51a73e43762cee2/lib/socks.c#L978
|
||||
pub fn set_reply_ip(&mut self, addr: IpAddr) {
|
||||
self.reply_ip = Some(addr);
|
||||
}
|
||||
|
||||
/// Process clients SOCKS requests
|
||||
/// This is the entry point where a whole request is processed.
|
||||
pub async fn upgrade_to_socks5(mut self) -> Result<Socks5Socket<T, A, C>> {
|
||||
trace!("upgrading to socks5...");
|
||||
|
||||
// Handshake
|
||||
if !self.config.skip_auth {
|
||||
let methods = self.get_methods().await?;
|
||||
|
||||
let auth_method = self.can_accept_method(methods).await?;
|
||||
|
||||
if self.config.auth.is_some() {
|
||||
let credentials = self.authenticate(auth_method).await?;
|
||||
self.credentials = Some(credentials);
|
||||
}
|
||||
} else {
|
||||
debug!("skipping auth");
|
||||
}
|
||||
|
||||
match self.request().await {
|
||||
Ok(_) => {}
|
||||
Err(SocksError::ReplyError(e)) => {
|
||||
// If a reply error has been returned, we send it to the client
|
||||
self.reply_error(&e).await?;
|
||||
return Err(e.into()); // propagate the error to end this connection's task
|
||||
}
|
||||
// if any other errors has been detected, we simply end connection's task
|
||||
Err(d) => return Err(d),
|
||||
};
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Consumes the `Socks5Socket`, returning the wrapped stream.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Read the authentication method provided by the client.
|
||||
/// A client send a list of methods that he supports, he could send
|
||||
///
|
||||
/// - 0: Non auth
|
||||
/// - 2: Auth with username/password
|
||||
///
|
||||
/// Altogether, then the server choose to use of of these,
|
||||
/// or deny the handshake (thus the connection).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```text
|
||||
/// {SOCKS Version, methods-length}
|
||||
/// eg. (non-auth) {5, 2}
|
||||
/// eg. (auth) {5, 3}
|
||||
/// ```
|
||||
///
|
||||
async fn get_methods(&mut self) -> Result<Vec<u8>> {
|
||||
trace!("Socks5Socket: get_methods()");
|
||||
// read the first 2 bytes which contains the SOCKS version and the methods len()
|
||||
let [version, methods_len] =
|
||||
read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?;
|
||||
debug!(
|
||||
"Handshake headers: [version: {version}, methods len: {len}]",
|
||||
version = version,
|
||||
len = methods_len,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
// {METHODS available from the client}
|
||||
// eg. (non-auth) {0, 1}
|
||||
// eg. (auth) {0, 1, 2}
|
||||
let methods = read_exact!(self.inner, vec![0u8; methods_len as usize])
|
||||
.context("Can't get methods.")?;
|
||||
debug!("methods supported sent by the client: {:?}", &methods);
|
||||
|
||||
// Return methods available
|
||||
Ok(methods)
|
||||
}
|
||||
|
||||
/// Decide to whether or not, accept the authentication method.
|
||||
/// Don't forget that the methods list sent by the client, contains one or more methods.
|
||||
///
|
||||
/// # Request
|
||||
///
|
||||
/// Client send an array of 3 entries: [0, 1, 2]
|
||||
/// ```text
|
||||
/// {SOCKS Version, Authentication chosen}
|
||||
/// eg. (non-auth) {5, 0}
|
||||
/// eg. (GSSAPI) {5, 1}
|
||||
/// eg. (auth) {5, 2}
|
||||
/// ```
|
||||
///
|
||||
/// # Response
|
||||
/// ```text
|
||||
/// eg. (accept non-auth) {5, 0x00}
|
||||
/// eg. (non-acceptable) {5, 0xff}
|
||||
/// ```
|
||||
///
|
||||
async fn can_accept_method(&mut self, client_methods: Vec<u8>) -> Result<u8> {
|
||||
let method_supported;
|
||||
|
||||
if let Some(_auth) = self.config.auth.as_ref() {
|
||||
if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) {
|
||||
// can auth with password
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD;
|
||||
} else {
|
||||
// client hasn't provided a password
|
||||
if self.config.allow_no_auth {
|
||||
// but we allow no auth, for ip whitelisting
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
} else {
|
||||
// we don't allow no auth, so we deny the entry
|
||||
debug!("Don't support this auth method, reply with (0xff)");
|
||||
self.inner
|
||||
.write_all(&[
|
||||
consts::SOCKS5_VERSION,
|
||||
consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE,
|
||||
])
|
||||
.await
|
||||
.context("Can't reply with method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthMethodUnacceptable(client_methods));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Reply with method {} ({})",
|
||||
AuthenticationMethod::from_u8(method_supported).context("Method not supported")?,
|
||||
method_supported
|
||||
);
|
||||
self.inner
|
||||
.write(&[consts::SOCKS5_VERSION, method_supported])
|
||||
.await
|
||||
.context("Can't reply with method auth-none")?;
|
||||
Ok(method_supported)
|
||||
}
|
||||
|
||||
async fn read_username_password(socket: &mut T) -> Result<(String, String)> {
|
||||
trace!("Socks5Socket: authenticate()");
|
||||
let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?;
|
||||
debug!(
|
||||
"Auth: [version: {version}, user len: {len}]",
|
||||
version = version,
|
||||
len = user_len,
|
||||
);
|
||||
|
||||
if user_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Username malformed ({} chars)",
|
||||
user_len
|
||||
)));
|
||||
}
|
||||
|
||||
let username =
|
||||
read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?;
|
||||
debug!("username bytes: {:?}", &username);
|
||||
|
||||
let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?;
|
||||
debug!("Auth: [pass len: {len}]", len = pass_len,);
|
||||
|
||||
if pass_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Password malformed ({} chars)",
|
||||
pass_len
|
||||
)));
|
||||
}
|
||||
|
||||
let password =
|
||||
read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?;
|
||||
debug!("password bytes: {:?}", &password);
|
||||
|
||||
let username = String::from_utf8(username).context("Failed to convert username")?;
|
||||
let password = String::from_utf8(password).context("Failed to convert password")?;
|
||||
|
||||
Ok((username, password))
|
||||
}
|
||||
|
||||
/// Only called if
|
||||
/// - this server has `Authentication` trait implemented.
|
||||
/// - and the client supports authentication via username/password
|
||||
/// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true`
|
||||
async fn authenticate(&mut self, auth_method: u8) -> Result<A::Item> {
|
||||
let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
let credentials = Self::read_username_password(&mut self.inner).await?;
|
||||
Some(credentials)
|
||||
} else {
|
||||
// the client hasn't provided any credentials, the function auth.authenticate()
|
||||
// will then check None, according to other parameters provided by the trait
|
||||
// such as IP, etc.
|
||||
None
|
||||
};
|
||||
|
||||
let auth = self.config.auth.as_ref().context("No auth module")?;
|
||||
|
||||
if let Some(credentials) = auth.authenticate(credentials).await {
|
||||
if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
// only the password way expect to write a response at this moment
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED])
|
||||
.await
|
||||
.context("Can't reply auth success")?;
|
||||
}
|
||||
|
||||
info!("User logged successfully.");
|
||||
|
||||
return Ok(credentials);
|
||||
} else {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE])
|
||||
.await
|
||||
.context("Can't reply with auth method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthenticationRejected(format!(
|
||||
"Authentication, rejected."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to principally cover ReplyError types for both functions read & execute request.
|
||||
async fn request(&mut self) -> Result<()> {
|
||||
self.read_command().await?;
|
||||
|
||||
if self.config.dns_resolve {
|
||||
self.resolve_dns().await?;
|
||||
} else {
|
||||
debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.")
|
||||
}
|
||||
|
||||
if self.config.execute_command {
|
||||
self.execute_command().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reply error to the client with the reply code according to the RFC.
|
||||
async fn reply_error(&mut self, error: &ReplyError) -> Result<()> {
|
||||
let reply = new_reply(error, "0.0.0.0:0".parse().unwrap());
|
||||
debug!("reply error to be written: {:?}", &reply);
|
||||
|
||||
self.inner
|
||||
.write(&reply)
|
||||
.await
|
||||
.context("Can't write the reply!")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decide to whether or not, accept the authentication method.
|
||||
/// Don't forget that the methods list sent by the client, contains one or more methods.
|
||||
///
|
||||
/// # Request
|
||||
/// ```text
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// | 1 | 1 | 1 | 1 | Variable | 2 |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// ```
|
||||
///
|
||||
/// It the request is correct, it should returns a ['SocketAddr'].
|
||||
///
|
||||
async fn read_command(&mut self) -> Result<()> {
|
||||
let [version, cmd, rsv, address_type] =
|
||||
read_exact!(self.inner, [0u8; 4]).context("Malformed request")?;
|
||||
debug!(
|
||||
"Request: [version: {version}, command: {cmd}, rev: {rsv}, address_type: {address_type}]",
|
||||
version = version,
|
||||
cmd = cmd,
|
||||
rsv = rsv,
|
||||
address_type = address_type,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
match Socks5Command::from_u8(cmd) {
|
||||
None => return Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPConnect => {
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::UDPAssociate => {
|
||||
if !self.config.allow_udp {
|
||||
return Err(ReplyError::CommandNotSupported.into());
|
||||
}
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::TCPBind => return Err(ReplyError::CommandNotSupported.into()),
|
||||
},
|
||||
}
|
||||
|
||||
// Guess address type
|
||||
let target_addr = read_address(&mut self.inner, address_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
self.target_addr = Some(target_addr);
|
||||
|
||||
debug!("Request target is {}", self.target_addr.as_ref().unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is public, it can be call manually on your own-willing
|
||||
/// if config flag has been turned off: `Config::dns_resolve == false`.
|
||||
pub async fn resolve_dns(&mut self) -> Result<()> {
|
||||
trace!("resolving dns");
|
||||
if let Some(target_addr) = self.target_addr.take() {
|
||||
// decide whether we have to resolve DNS or not
|
||||
self.target_addr = match target_addr {
|
||||
TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?),
|
||||
TargetAddr::Ip(_) => Some(target_addr),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the socks5 command that the client wants.
|
||||
async fn execute_command(&mut self) -> Result<()> {
|
||||
match &self.cmd {
|
||||
None => Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPBind => Err(ReplyError::CommandNotSupported.into()),
|
||||
Socks5Command::TCPConnect => return self.execute_command_connect().await,
|
||||
Socks5Command::UDPAssociate => {
|
||||
if self.config.allow_udp {
|
||||
return self.execute_command_udp_assoc().await;
|
||||
} else {
|
||||
Err(ReplyError::CommandNotSupported.into())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the target address that the client wants,
|
||||
/// then forward the data between them (client <=> target address).
|
||||
async fn execute_command_connect(&mut self) -> Result<()> {
|
||||
// async-std's ToSocketAddrs doesn't supports external trait implementation
|
||||
// @see https://github.com/async-rs/async-std/issues/539
|
||||
let addr = self
|
||||
.target_addr
|
||||
.as_ref()
|
||||
.context("target_addr empty")?
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
// TCP connect with timeout, to avoid memory leak for connection that takes forever
|
||||
let outbound = self
|
||||
.tcp_connector
|
||||
.tcp_connect(addr, self.config.request_timeout)
|
||||
.await?;
|
||||
|
||||
debug!("Connected to remote destination");
|
||||
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer(&mut self.inner, outbound).await
|
||||
}
|
||||
|
||||
/// Bind to a random UDP port, wait for the traffic from
|
||||
/// the client, and then forward the data to the remote addr.
|
||||
async fn execute_command_udp_assoc(&mut self) -> Result<()> {
|
||||
// The DST.ADDR and DST.PORT fields contain the address and port that
|
||||
// the client expects to use to send UDP datagrams on for the
|
||||
// association. The server MAY use this information to limit access
|
||||
// to the association.
|
||||
// @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928.
|
||||
//
|
||||
// We do NOT limit the access from the client currently in this implementation.
|
||||
let _not_used = self.target_addr.as_ref();
|
||||
|
||||
// Listen with UDP6 socket, so the client can connect to it with either
|
||||
// IPv4 or IPv6.
|
||||
let peer_sock = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
// Respect the pre-populated reply IP address.
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(
|
||||
self.reply_ip.context("invalid reply ip")?,
|
||||
peer_sock.local_addr()?.port(),
|
||||
),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer_udp(peer_sock).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn target_addr(&self) -> Option<&TargetAddr> {
|
||||
self.target_addr.as_ref()
|
||||
}
|
||||
|
||||
pub fn auth(&self) -> &AuthenticationMethod {
|
||||
&self.auth
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> &Option<Socks5Command> {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
/// Borrow the credentials of the user has authenticated with
|
||||
pub fn get_credentials(&self) -> Option<&<<A as Authentication>::Item as Deref>::Target>
|
||||
where
|
||||
<A as Authentication>::Item: Deref,
|
||||
{
|
||||
self.credentials.as_deref()
|
||||
}
|
||||
|
||||
/// Get the credentials of the user has authenticated with
|
||||
pub fn take_credentials(&mut self) -> Option<A::Item> {
|
||||
self.credentials.take()
|
||||
}
|
||||
|
||||
pub fn tcp_connector(&self) -> &C {
|
||||
&self.tcp_connector
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy data between two peers
|
||||
/// Using 2 different generators, because they could be different structs with same traits.
|
||||
async fn transfer<I, O>(mut inbound: I, mut outbound: O) -> Result<()>
|
||||
where
|
||||
I: AsyncRead + AsyncWrite + Unpin,
|
||||
O: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await {
|
||||
Ok(res) => info!("transfer closed ({}, {})", res.0, res.1),
|
||||
Err(err) => error!("transfer error: {:?}", err),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, client_addr) = inbound.recv_from(&mut buf).await?;
|
||||
debug!("Server recieve udp from {}", client_addr);
|
||||
inbound.connect(client_addr).await?;
|
||||
|
||||
let (frag, target_addr, data) = parse_udp_request(&buf[..size]).await?;
|
||||
|
||||
if frag != 0 {
|
||||
debug!("Discard UDP frag packets sliently.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Server forward to packet to {}", target_addr);
|
||||
let mut target_addr = target_addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
target_addr.set_ip(match target_addr.ip() {
|
||||
std::net::IpAddr::V4(v4) => std::net::IpAddr::V6(v4.to_ipv6_mapped()),
|
||||
v6 @ std::net::IpAddr::V6(_) => v6,
|
||||
});
|
||||
outbound.send_to(data, target_addr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, remote_addr) = outbound.recv_from(&mut buf).await?;
|
||||
debug!("Recieve packet from {}", remote_addr);
|
||||
|
||||
let mut data = new_udp_header(remote_addr)?;
|
||||
data.extend_from_slice(&buf[..size]);
|
||||
inbound.send(&data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_udp(inbound: UdpSocket) -> Result<()> {
|
||||
let outbound = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
let req_fut = handle_udp_request(&inbound, &outbound);
|
||||
let res_fut = handle_udp_response(&inbound, &outbound);
|
||||
match try_join!(req_fut, res_fut) {
|
||||
Ok(_) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Fixes the issue "cannot borrow data in dereference of `Pin<&mut >` as mutable"
|
||||
//
|
||||
// cf. https://users.rust-lang.org/t/take-in-impl-future-cannot-borrow-data-in-a-dereference-of-pin/52042
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> Unpin for Socks5Socket<T, A, S> where
|
||||
T: AsyncRead + AsyncWrite + Unpin
|
||||
{
|
||||
}
|
||||
|
||||
/// Allow us to read directly from the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncRead for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(context, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow us to write directly into the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncWrite for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(context, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(context)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate reply code according to the RFC.
|
||||
fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec<u8> {
|
||||
let (addr_type, mut ip_oct, mut port) = match sock_addr {
|
||||
SocketAddr::V4(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
SocketAddr::V6(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut reply = vec![
|
||||
consts::SOCKS5_VERSION,
|
||||
error.as_u8(), // transform the error into byte code
|
||||
0x00, // reserved
|
||||
addr_type, // address type (ipv4, v6, domain)
|
||||
];
|
||||
reply.append(&mut ip_oct);
|
||||
reply.append(&mut port);
|
||||
|
||||
reply
|
||||
}
|
||||
2
easytier/src/gateway/fast_socks5/util/mod.rs
Normal file
2
easytier/src/gateway/fast_socks5/util/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod stream;
|
||||
pub mod target_addr;
|
||||
65
easytier/src/gateway/fast_socks5/util/stream.rs
Normal file
65
easytier/src/gateway/fast_socks5/util/stream.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::time::Duration;
|
||||
use tokio::io::ErrorKind as IOErrorKind;
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::gateway::fast_socks5::{ReplyError, Result};
|
||||
|
||||
/// Easy to destructure bytes buffers by naming each fields:
|
||||
///
|
||||
/// # Examples (before)
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut buf = [0u8; 2];
|
||||
/// stream.read_exact(&mut buf).await?;
|
||||
/// let [version, method_len] = buf;
|
||||
///
|
||||
/// assert_eq!(version, 0x05);
|
||||
/// ```
|
||||
///
|
||||
/// # Examples (after)
|
||||
///
|
||||
/// ```ignore
|
||||
/// let [version, method_len] = read_exact!(stream, [0u8; 2]);
|
||||
///
|
||||
/// assert_eq!(version, 0x05);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! read_exact {
|
||||
($stream: expr, $array: expr) => {{
|
||||
let mut x = $array;
|
||||
// $stream
|
||||
// .read_exact(&mut x)
|
||||
// .await
|
||||
// .map_err(|_| io_err("lol"))?;
|
||||
$stream.read_exact(&mut x).await.map(|_| x)
|
||||
}};
|
||||
}
|
||||
|
||||
pub async fn tcp_connect_with_timeout<T>(addr: T, request_timeout_s: u64) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
let fut = tcp_connect(addr);
|
||||
match timeout(Duration::from_secs(request_timeout_s), fut).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ReplyError::ConnectionTimeout.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tcp_connect<T>(addr: T) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
match TcpStream::connect(addr).await {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => match e.kind() {
|
||||
// Match other TCP errors with ReplyError
|
||||
IOErrorKind::ConnectionRefused => Err(ReplyError::ConnectionRefused.into()),
|
||||
IOErrorKind::ConnectionAborted => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::ConnectionReset => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::NotConnected => Err(ReplyError::NetworkUnreachable.into()),
|
||||
_ => Err(e.into()), // #[error("General failure")] ?
|
||||
},
|
||||
}
|
||||
}
|
||||
244
easytier/src/gateway/fast_socks5/util/target_addr.rs
Normal file
244
easytier/src/gateway/fast_socks5/util/target_addr.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use crate::gateway::fast_socks5::consts;
|
||||
use crate::gateway::fast_socks5::consts::SOCKS5_ADDR_TYPE_IPV4;
|
||||
use crate::gateway::fast_socks5::SocksError;
|
||||
use crate::read_exact;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::vec::IntoIter;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// SOCKS5 reply code
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AddrError {
|
||||
#[error("DNS Resolution failed")]
|
||||
DNSResolutionFailed,
|
||||
#[error("Can't read IPv4")]
|
||||
IPv4Unreadable,
|
||||
#[error("Can't read IPv6")]
|
||||
IPv6Unreadable,
|
||||
#[error("Can't read port number")]
|
||||
PortNumberUnreadable,
|
||||
#[error("Can't read domain len")]
|
||||
DomainLenUnreadable,
|
||||
#[error("Can't read Domain content")]
|
||||
DomainContentUnreadable,
|
||||
#[error("Malformed UTF-8")]
|
||||
Utf8,
|
||||
#[error("Unknown address type")]
|
||||
IncorrectAddressType,
|
||||
#[error("{0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A description of a connection target.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum TargetAddr {
|
||||
/// Connect to an IP address.
|
||||
Ip(SocketAddr),
|
||||
/// Connect to a fully qualified domain name.
|
||||
///
|
||||
/// The domain name will be passed along to the proxy server and DNS lookup
|
||||
/// will happen there.
|
||||
Domain(String, u16),
|
||||
}
|
||||
|
||||
impl TargetAddr {
|
||||
pub async fn resolve_dns(self) -> anyhow::Result<TargetAddr> {
|
||||
match self {
|
||||
TargetAddr::Ip(ip) => Ok(TargetAddr::Ip(ip)),
|
||||
TargetAddr::Domain(domain, port) => {
|
||||
debug!("Attempt to DNS resolve the domain {}...", &domain);
|
||||
|
||||
let socket_addr = lookup_host((&domain[..], port))
|
||||
.await
|
||||
.context(AddrError::DNSResolutionFailed)?
|
||||
.next()
|
||||
.ok_or(AddrError::Custom(
|
||||
"Can't fetch DNS to the domain.".to_string(),
|
||||
))?;
|
||||
debug!("domain name resolved to {}", socket_addr);
|
||||
|
||||
// has been converted to an ip
|
||||
Ok(TargetAddr::Ip(socket_addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ip(&self) -> bool {
|
||||
match self {
|
||||
TargetAddr::Ip(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_domain(&self) -> bool {
|
||||
!self.is_ip()
|
||||
}
|
||||
|
||||
pub fn to_be_bytes(&self) -> anyhow::Result<Vec<u8>> {
|
||||
let mut buf = vec![];
|
||||
match self {
|
||||
TargetAddr::Ip(SocketAddr::V4(addr)) => {
|
||||
debug!("TargetAddr::IpV4");
|
||||
|
||||
buf.extend_from_slice(&[SOCKS5_ADDR_TYPE_IPV4]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Ip(SocketAddr::V6(addr)) => {
|
||||
debug!("TargetAddr::IpV6");
|
||||
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_IPV6]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Domain(ref domain, port) => {
|
||||
debug!("TargetAddr::Domain");
|
||||
if domain.len() > u8::max_value() as usize {
|
||||
return Err(SocksError::ExceededMaxDomainLen(domain.len()).into());
|
||||
}
|
||||
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]);
|
||||
buf.extend_from_slice(domain.as_bytes()); // domain content
|
||||
buf.extend_from_slice(&port.to_be_bytes());
|
||||
// port content (.to_be_bytes() convert from u16 to u8 type)
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// async-std ToSocketAddrs doesn't supports external trait implementation
|
||||
// @see https://github.com/async-rs/async-std/issues/539
|
||||
impl std::net::ToSocketAddrs for TargetAddr {
|
||||
type Iter = IntoIter<SocketAddr>;
|
||||
|
||||
fn to_socket_addrs(&self) -> io::Result<IntoIter<SocketAddr>> {
|
||||
match *self {
|
||||
TargetAddr::Ip(addr) => Ok(vec![addr].into_iter()),
|
||||
TargetAddr::Domain(_, _) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Domain name has to be explicitly resolved, please use TargetAddr::resolve_dns().",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetAddr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
TargetAddr::Ip(ref addr) => write!(f, "{}", addr),
|
||||
TargetAddr::Domain(ref addr, ref port) => write!(f, "{}:{}", addr, port),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for objects that can be converted to `TargetAddr`.
|
||||
pub trait ToTargetAddr {
|
||||
/// Converts the value of `self` to a `TargetAddr`.
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr>;
|
||||
}
|
||||
|
||||
impl<'a> ToTargetAddr for (&'a str, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
// try to parse as an IP first
|
||||
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
|
||||
return (addr, self.1).to_target_addr();
|
||||
}
|
||||
|
||||
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
|
||||
return (addr, self.1).to_target_addr();
|
||||
}
|
||||
|
||||
Ok(TargetAddr::Domain(self.0.to_owned(), self.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddr {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
Ok(TargetAddr::Ip(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV4 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V4(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV6 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V6(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv4Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddrV4::new(self.0, self.1).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Addr {
|
||||
V4([u8; 4]),
|
||||
V6([u8; 16]),
|
||||
Domain(String), // Vec<[u8]> or Box<[u8]> or String ?
|
||||
}
|
||||
|
||||
/// This function is used by the client & the server
|
||||
pub async fn read_address<T: AsyncRead + Unpin>(
|
||||
stream: &mut T,
|
||||
atyp: u8,
|
||||
) -> anyhow::Result<TargetAddr> {
|
||||
let addr = match atyp {
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4 => {
|
||||
debug!("Address type `IPv4`");
|
||||
Addr::V4(read_exact!(stream, [0u8; 4]).context(AddrError::IPv4Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6 => {
|
||||
debug!("Address type `IPv6`");
|
||||
Addr::V6(read_exact!(stream, [0u8; 16]).context(AddrError::IPv6Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
|
||||
debug!("Address type `domain`");
|
||||
let len = read_exact!(stream, [0]).context(AddrError::DomainLenUnreadable)?[0];
|
||||
let domain = read_exact!(stream, vec![0u8; len as usize])
|
||||
.context(AddrError::DomainContentUnreadable)?;
|
||||
// make sure the bytes are correct utf8 string
|
||||
let domain = String::from_utf8(domain).context(AddrError::Utf8)?;
|
||||
|
||||
Addr::Domain(domain)
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!(AddrError::IncorrectAddressType)),
|
||||
};
|
||||
|
||||
// Find port number
|
||||
let port = read_exact!(stream, [0u8; 2]).context(AddrError::PortNumberUnreadable)?;
|
||||
// Convert (u8 * 2) into u16
|
||||
let port = (port[0] as u16) << 8 | port[1] as u16;
|
||||
|
||||
// Merge ADDRESS + PORT into a TargetAddr
|
||||
let addr: TargetAddr = match addr {
|
||||
Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port).to_target_addr()?,
|
||||
Addr::V6(x) => (Ipv6Addr::from(x), port).to_target_addr()?,
|
||||
Addr::Domain(domain) => TargetAddr::Domain(domain, port),
|
||||
};
|
||||
|
||||
Ok(addr)
|
||||
}
|
||||
@@ -358,7 +358,12 @@ impl IcmpProxy {
|
||||
if !self.cidr_set.contains_v4(ipv4.get_destination())
|
||||
&& !is_exit_node
|
||||
&& !(self.global_ctx.no_tun()
|
||||
&& Some(ipv4.get_destination()) == self.global_ctx.get_ipv4())
|
||||
&& Some(ipv4.get_destination())
|
||||
== self
|
||||
.global_ctx
|
||||
.get_ipv4()
|
||||
.as_ref()
|
||||
.map(cidr::Ipv4Inet::address))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -382,7 +387,14 @@ impl IcmpProxy {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.global_ctx.no_tun() && Some(ipv4.get_destination()) == self.global_ctx.get_ipv4() {
|
||||
if self.global_ctx.no_tun()
|
||||
&& Some(ipv4.get_destination())
|
||||
== self
|
||||
.global_ctx
|
||||
.get_ipv4()
|
||||
.as_ref()
|
||||
.map(cidr::Ipv4Inet::address)
|
||||
{
|
||||
self.send_icmp_reply_to_peer(
|
||||
&ipv4.get_destination(),
|
||||
&ipv4.get_source(),
|
||||
|
||||
@@ -9,6 +9,12 @@ pub mod tcp_proxy;
|
||||
#[cfg(feature = "smoltcp")]
|
||||
pub mod tokio_smoltcp;
|
||||
pub mod udp_proxy;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod fast_socks5;
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod socks5;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CidrSet {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
418
easytier/src/gateway/socks5.rs
Normal file
418
easytier/src/gateway/socks5.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gateway::{
|
||||
fast_socks5::{
|
||||
server::{
|
||||
AcceptAuthentication, AsyncTcpConnector, Config, SimpleUserPassword, Socks5Socket,
|
||||
},
|
||||
util::stream::tcp_connect_with_timeout,
|
||||
},
|
||||
tokio_smoltcp::TcpStream,
|
||||
},
|
||||
tunnel::packet_def::PacketType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use pnet::packet::{ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, Packet};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
select,
|
||||
};
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{mpsc, Mutex},
|
||||
task::JoinSet,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::GlobalCtx},
|
||||
gateway::tokio_smoltcp::{channel_device, Net, NetConfig},
|
||||
peers::{peer_manager::PeerManager, PeerPacketFilter},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
|
||||
enum SocksTcpStream {
|
||||
TcpStream(tokio::net::TcpStream),
|
||||
SmolTcpStream(TcpStream),
|
||||
}
|
||||
|
||||
impl AsyncRead for SocksTcpStream {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_read(cx, buf)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for SocksTcpStream {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_write(cx, buf)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_write(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => std::pin::Pin::new(stream).poll_flush(cx),
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_flush(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_shutdown(cx)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||
struct Socks5Entry {
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
}
|
||||
|
||||
type Socks5EntrySet = Arc<DashSet<Socks5Entry>>;
|
||||
|
||||
struct Socks5ServerNet {
|
||||
ipv4_addr: cidr::Ipv4Inet,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
smoltcp_net: Arc<Net>,
|
||||
forward_tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
impl Socks5ServerNet {
|
||||
pub fn new(
|
||||
ipv4_addr: cidr::Ipv4Inet,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
entries: Socks5EntrySet,
|
||||
) -> Self {
|
||||
let mut forward_tasks = JoinSet::new();
|
||||
let mut cap = smoltcp::phy::DeviceCapabilities::default();
|
||||
cap.max_transmission_unit = 1280;
|
||||
cap.medium = smoltcp::phy::Medium::Ip;
|
||||
let (dev, stack_sink, mut stack_stream) = channel_device::ChannelDevice::new(cap);
|
||||
|
||||
let packet_recv = packet_recv.clone();
|
||||
forward_tasks.spawn(async move {
|
||||
let mut smoltcp_stack_receiver = packet_recv.lock().await;
|
||||
while let Some(packet) = smoltcp_stack_receiver.recv().await {
|
||||
tracing::trace!(?packet, "receive from peer send to smoltcp packet");
|
||||
if let Err(e) = stack_sink.send(Ok(packet.payload().to_vec())).await {
|
||||
tracing::error!("send to smoltcp stack failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack sink exited");
|
||||
panic!("smoltcp stack sink exited");
|
||||
});
|
||||
|
||||
forward_tasks.spawn(async move {
|
||||
while let Some(data) = stack_stream.recv().await {
|
||||
tracing::trace!(
|
||||
?data,
|
||||
"receive from smoltcp stack and send to peer mgr packet"
|
||||
);
|
||||
let Some(ipv4) = Ipv4Packet::new(&data) else {
|
||||
tracing::error!(?data, "smoltcp stack stream get non ipv4 packet");
|
||||
continue;
|
||||
};
|
||||
|
||||
let dst = ipv4.get_destination();
|
||||
let packet = ZCPacket::new_with_payload(&data);
|
||||
if let Err(e) = peer_manager.send_msg_ipv4(packet, dst).await {
|
||||
tracing::error!("send to peer failed in smoltcp sender: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack stream exited");
|
||||
panic!("smoltcp stack stream exited");
|
||||
});
|
||||
|
||||
let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip);
|
||||
let net = Net::new(
|
||||
dev,
|
||||
NetConfig::new(
|
||||
interface_config,
|
||||
format!("{}/{}", ipv4_addr.address(), ipv4_addr.network_length())
|
||||
.parse()
|
||||
.unwrap(),
|
||||
vec![format!("{}", ipv4_addr.address()).parse().unwrap()],
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
ipv4_addr,
|
||||
auth,
|
||||
|
||||
smoltcp_net: Arc::new(net),
|
||||
forward_tasks: Arc::new(std::sync::Mutex::new(forward_tasks)),
|
||||
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tcp_stream(&self, stream: tokio::net::TcpStream) {
|
||||
let mut config = Config::<AcceptAuthentication>::default();
|
||||
config.set_request_timeout(10);
|
||||
config.set_skip_auth(false);
|
||||
config.set_allow_no_auth(true);
|
||||
|
||||
struct SmolTcpConnector(
|
||||
Arc<Net>,
|
||||
Socks5EntrySet,
|
||||
std::sync::Mutex<Option<Socks5Entry>>,
|
||||
);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for SmolTcpConnector {
|
||||
type S = SocksTcpStream;
|
||||
|
||||
async fn tcp_connect(
|
||||
&self,
|
||||
addr: SocketAddr,
|
||||
timeout_s: u64,
|
||||
) -> crate::gateway::fast_socks5::Result<SocksTcpStream> {
|
||||
let local_addr = self.0.get_address();
|
||||
let port = self.0.get_port();
|
||||
|
||||
let entry = Socks5Entry {
|
||||
src: SocketAddr::new(local_addr, port),
|
||||
dst: addr,
|
||||
};
|
||||
*self.2.lock().unwrap() = Some(entry.clone());
|
||||
self.1.insert(entry);
|
||||
|
||||
if addr.ip() == local_addr {
|
||||
let modified_addr =
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), addr.port());
|
||||
|
||||
Ok(SocksTcpStream::TcpStream(
|
||||
tcp_connect_with_timeout(modified_addr, timeout_s).await?,
|
||||
))
|
||||
} else {
|
||||
let remote_socket = timeout(
|
||||
Duration::from_secs(timeout_s),
|
||||
self.0.tcp_connect(addr, port),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "connect to remote timeout")?;
|
||||
|
||||
Ok(SocksTcpStream::SmolTcpStream(remote_socket.map_err(
|
||||
|e| super::fast_socks5::SocksError::Other(e.into()),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmolTcpConnector {
|
||||
fn drop(&mut self) {
|
||||
if let Some(entry) = self.2.lock().unwrap().take() {
|
||||
self.1.remove(&entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let socket = Socks5Socket::new(
|
||||
stream,
|
||||
Arc::new(config),
|
||||
SmolTcpConnector(
|
||||
self.smoltcp_net.clone(),
|
||||
self.entries.clone(),
|
||||
std::sync::Mutex::new(None),
|
||||
),
|
||||
);
|
||||
|
||||
self.forward_tasks.lock().unwrap().spawn(async move {
|
||||
match socket.upgrade_to_socks5().await {
|
||||
Ok(_) => {
|
||||
tracing::info!("socks5 handle success");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("socks5 handshake failed: {:?}", e);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Socks5Server {
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
packet_sender: mpsc::Sender<ZCPacket>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
|
||||
net: Arc<Mutex<Option<Socks5ServerNet>>>,
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerPacketFilter for Socks5Server {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
let hdr = packet.peer_manager_header().unwrap();
|
||||
if hdr.packet_type != PacketType::Data as u8 {
|
||||
return Some(packet);
|
||||
};
|
||||
|
||||
let payload_bytes = packet.payload();
|
||||
|
||||
let ipv4 = Ipv4Packet::new(payload_bytes).unwrap();
|
||||
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap();
|
||||
let entry = Socks5Entry {
|
||||
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
|
||||
src: SocketAddr::new(ipv4.get_destination().into(), tcp_packet.get_destination()),
|
||||
};
|
||||
|
||||
if !self.entries.contains(&entry) {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let _ = self.packet_sender.try_send(packet).ok();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Socks5Server {
|
||||
pub fn new(
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
) -> Arc<Self> {
|
||||
let (packet_sender, packet_recv) = mpsc::channel(1024);
|
||||
Arc::new(Self {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
auth,
|
||||
|
||||
tasks: Arc::new(Mutex::new(JoinSet::new())),
|
||||
packet_recv: Arc::new(Mutex::new(packet_recv)),
|
||||
packet_sender,
|
||||
|
||||
net: Arc::new(Mutex::new(None)),
|
||||
entries: Arc::new(DashSet::new()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_net_update_task(self: &Arc<Self>) {
|
||||
let net = self.net.clone();
|
||||
let global_ctx = self.global_ctx.clone();
|
||||
let peer_manager = self.peer_manager.clone();
|
||||
let packet_recv = self.packet_recv.clone();
|
||||
let entries = self.entries.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
let mut prev_ipv4 = None;
|
||||
loop {
|
||||
let mut event_recv = global_ctx.subscribe();
|
||||
|
||||
let cur_ipv4 = global_ctx.get_ipv4();
|
||||
if prev_ipv4 != cur_ipv4 {
|
||||
prev_ipv4 = cur_ipv4;
|
||||
entries.clear();
|
||||
|
||||
if cur_ipv4.is_none() {
|
||||
let _ = net.lock().await.take();
|
||||
} else {
|
||||
net.lock().await.replace(Socks5ServerNet::new(
|
||||
cur_ipv4.unwrap(),
|
||||
None,
|
||||
peer_manager.clone(),
|
||||
packet_recv.clone(),
|
||||
entries.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
select! {
|
||||
_ = event_recv.recv() => {}
|
||||
_ = tokio::time::sleep(Duration::from_secs(120)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn run(self: &Arc<Self>) -> Result<(), Error> {
|
||||
let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bind_addr = format!(
|
||||
"{}:{}",
|
||||
proxy_url.host_str().unwrap(),
|
||||
proxy_url.port().unwrap()
|
||||
);
|
||||
|
||||
let listener = {
|
||||
let _g = self.global_ctx.net_ns.guard();
|
||||
TcpListener::bind(bind_addr.parse::<SocketAddr>().unwrap()).await?
|
||||
};
|
||||
|
||||
self.peer_manager
|
||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||
.await;
|
||||
|
||||
self.run_net_update_task().await;
|
||||
|
||||
let net = self.net.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((socket, _addr)) => {
|
||||
tracing::info!("accept a new connection, {:?}", socket);
|
||||
if let Some(net) = net.lock().await.as_ref() {
|
||||
net.handle_tcp_stream(socket);
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::error!("accept error = {:?}", err),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use cidr::Ipv4Inet;
|
||||
use core::panic;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
@@ -97,10 +98,55 @@ impl ProxyTcpStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smoltcp")]
|
||||
struct SmolTcpListener {
|
||||
listener_task: JoinSet<()>,
|
||||
listen_count: usize,
|
||||
|
||||
stream_rx: mpsc::UnboundedReceiver<Result<(tokio_smoltcp::TcpStream, SocketAddr)>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smoltcp")]
|
||||
impl SmolTcpListener {
|
||||
pub async fn new(net: Arc<Mutex<Option<Net>>>, listen_count: usize) -> Self {
|
||||
let mut tasks = JoinSet::new();
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let locked_net = net.lock().await;
|
||||
for _ in 0..listen_count {
|
||||
let mut tcp = locked_net
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.tcp_bind("0.0.0.0:8899".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let tx = tx.clone();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
tx.send(tcp.accept().await.map_err(|e| {
|
||||
anyhow::anyhow!("smol tcp listener accept failed: {:?}", e).into()
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
listener_task: tasks,
|
||||
listen_count,
|
||||
stream_rx: rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept(&mut self) -> Result<(tokio_smoltcp::TcpStream, SocketAddr)> {
|
||||
self.stream_rx.recv().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyTcpListener {
|
||||
KernelTcpListener(TcpListener),
|
||||
#[cfg(feature = "smoltcp")]
|
||||
SmolTcpListener(tokio_smoltcp::TcpListener),
|
||||
SmolTcpListener(SmolTcpListener),
|
||||
}
|
||||
|
||||
impl ProxyTcpListener {
|
||||
@@ -375,8 +421,8 @@ impl TcpProxy {
|
||||
),
|
||||
);
|
||||
net.set_any_ip(true);
|
||||
let tcp = net.tcp_bind("0.0.0.0:8899".parse().unwrap()).await?;
|
||||
self.smoltcp_net.lock().await.replace(net);
|
||||
let tcp = SmolTcpListener::new(self.smoltcp_net.clone(), 64).await;
|
||||
|
||||
self.enable_smoltcp
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
@@ -481,7 +527,8 @@ impl TcpProxy {
|
||||
tracing::warn!("set_nodelay failed, ignore it: {:?}", e);
|
||||
}
|
||||
|
||||
let nat_dst = if Some(nat_entry.dst.ip()) == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip))
|
||||
let nat_dst = if Some(nat_entry.dst.ip())
|
||||
== global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address()))
|
||||
{
|
||||
format!("127.0.0.1:{}", nat_entry.dst.port())
|
||||
.parse()
|
||||
@@ -546,7 +593,10 @@ impl TcpProxy {
|
||||
{
|
||||
Some(Ipv4Addr::new(192, 88, 99, 254))
|
||||
} else {
|
||||
self.global_ctx.get_ipv4()
|
||||
self.global_ctx
|
||||
.get_ipv4()
|
||||
.as_ref()
|
||||
.map(cidr::Ipv4Inet::address)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +626,8 @@ impl TcpProxy {
|
||||
if !self.cidr_set.contains_v4(ipv4.get_destination())
|
||||
&& !is_exit_node
|
||||
&& !(self.global_ctx.no_tun()
|
||||
&& Some(ipv4.get_destination()) == self.global_ctx.get_ipv4())
|
||||
&& Some(ipv4.get_destination())
|
||||
== self.global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicU16, Ordering},
|
||||
Arc,
|
||||
@@ -134,7 +134,10 @@ impl Net {
|
||||
fut,
|
||||
)
|
||||
}
|
||||
fn get_port(&self) -> u16 {
|
||||
pub fn get_address(&self) -> IpAddr {
|
||||
self.ip_addr.address().into()
|
||||
}
|
||||
pub fn get_port(&self) -> u16 {
|
||||
self.from_port
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
|
||||
Some(if x > 60000 { 10000 } else { x + 1 })
|
||||
@@ -147,10 +150,10 @@ impl Net {
|
||||
TcpListener::new(self.reactor.clone(), addr.into()).await
|
||||
}
|
||||
/// Opens a TCP connection to a remote host.
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr) -> io::Result<TcpStream> {
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr, local_port: u16) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(
|
||||
self.reactor.clone(),
|
||||
(self.ip_addr.address(), self.get_port()).into(),
|
||||
(self.ip_addr.address(), local_port).into(),
|
||||
addr.into(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cidr::Ipv4Inet;
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use pnet::packet::{
|
||||
ip::IpNextHeaderProtocols,
|
||||
@@ -11,12 +13,10 @@ use pnet::packet::{
|
||||
udp::{self, MutableUdpPacket},
|
||||
Packet,
|
||||
};
|
||||
use tachyonix::{channel, Receiver, Sender, TrySendError};
|
||||
use tokio::{
|
||||
net::UdpSocket,
|
||||
sync::{
|
||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex,
|
||||
},
|
||||
sync::Mutex,
|
||||
task::{JoinHandle, JoinSet},
|
||||
time::timeout,
|
||||
};
|
||||
@@ -49,6 +49,7 @@ struct UdpNatEntry {
|
||||
forward_task: Mutex<Option<JoinHandle<()>>>,
|
||||
stopped: AtomicBool,
|
||||
start_time: std::time::Instant,
|
||||
last_active_time: AtomicCell<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl UdpNatEntry {
|
||||
@@ -72,6 +73,7 @@ impl UdpNatEntry {
|
||||
forward_task: Mutex::new(None),
|
||||
stopped: AtomicBool::new(false),
|
||||
start_time: std::time::Instant::now(),
|
||||
last_active_time: AtomicCell::new(std::time::Instant::now()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ impl UdpNatEntry {
|
||||
|
||||
async fn compose_ipv4_packet(
|
||||
self: &Arc<Self>,
|
||||
packet_sender: &mut UnboundedSender<ZCPacket>,
|
||||
packet_sender: &mut Sender<ZCPacket>,
|
||||
buf: &mut [u8],
|
||||
src_v4: &SocketAddrV4,
|
||||
payload_len: usize,
|
||||
@@ -119,11 +121,13 @@ impl UdpNatEntry {
|
||||
p.fill_peer_manager_hdr(self.my_peer_id, self.src_peer_id, PacketType::Data as u8);
|
||||
p.mut_peer_manager_header().unwrap().set_no_proxy(true);
|
||||
|
||||
if let Err(e) = packet_sender.send(p) {
|
||||
tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e);
|
||||
return Err(Error::AnyhowError(e.into()));
|
||||
match packet_sender.try_send(p) {
|
||||
Err(TrySendError::Closed(e)) => {
|
||||
tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e);
|
||||
Err(Error::Unknown)
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -132,7 +136,7 @@ impl UdpNatEntry {
|
||||
|
||||
async fn forward_task(
|
||||
self: Arc<Self>,
|
||||
mut packet_sender: UnboundedSender<ZCPacket>,
|
||||
mut packet_sender: Sender<ZCPacket>,
|
||||
virtual_ipv4: Ipv4Addr,
|
||||
) {
|
||||
let mut buf = [0u8; 65536];
|
||||
@@ -141,7 +145,7 @@ impl UdpNatEntry {
|
||||
|
||||
loop {
|
||||
let (len, src_socket) = match timeout(
|
||||
Duration::from_secs(30),
|
||||
Duration::from_secs(120),
|
||||
self.socket.recv_from(&mut udp_body),
|
||||
)
|
||||
.await
|
||||
@@ -167,6 +171,8 @@ impl UdpNatEntry {
|
||||
continue;
|
||||
};
|
||||
|
||||
self.mark_active();
|
||||
|
||||
if src_v4.ip().is_loopback() {
|
||||
src_v4.set_ip(virtual_ipv4);
|
||||
}
|
||||
@@ -189,6 +195,14 @@ impl UdpNatEntry {
|
||||
|
||||
self.stop();
|
||||
}
|
||||
|
||||
fn mark_active(&self) {
|
||||
self.last_active_time.store(std::time::Instant::now());
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.last_active_time.load().elapsed().as_secs() < 180
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -200,8 +214,8 @@ pub struct UdpProxy {
|
||||
|
||||
nat_table: Arc<DashMap<UdpNatKey, Arc<UdpNatEntry>>>,
|
||||
|
||||
sender: UnboundedSender<ZCPacket>,
|
||||
receiver: Mutex<Option<UnboundedReceiver<ZCPacket>>>,
|
||||
sender: Sender<ZCPacket>,
|
||||
receiver: Mutex<Option<Receiver<ZCPacket>>>,
|
||||
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
|
||||
@@ -232,7 +246,8 @@ impl UdpProxy {
|
||||
if !self.cidr_set.contains_v4(ipv4.get_destination())
|
||||
&& !is_exit_node
|
||||
&& !(self.global_ctx.no_tun()
|
||||
&& Some(ipv4.get_destination()) == self.global_ctx.get_ipv4())
|
||||
&& Some(ipv4.get_destination())
|
||||
== self.global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -283,12 +298,16 @@ impl UdpProxy {
|
||||
.replace(tokio::spawn(UdpNatEntry::forward_task(
|
||||
nat_entry.clone(),
|
||||
self.sender.clone(),
|
||||
self.global_ctx.get_ipv4()?,
|
||||
self.global_ctx.get_ipv4().map(|x| x.address())?,
|
||||
)));
|
||||
}
|
||||
|
||||
nat_entry.mark_active();
|
||||
|
||||
// TODO: should it be async.
|
||||
let dst_socket = if Some(ipv4.get_destination()) == self.global_ctx.get_ipv4() {
|
||||
let dst_socket = if Some(ipv4.get_destination())
|
||||
== self.global_ctx.get_ipv4().as_ref().map(Ipv4Inet::address)
|
||||
{
|
||||
format!("127.0.0.1:{}", udp_packet.get_destination())
|
||||
.parse()
|
||||
.unwrap()
|
||||
@@ -335,7 +354,7 @@ impl UdpProxy {
|
||||
peer_manager: Arc<PeerManager>,
|
||||
) -> Result<Arc<Self>, Error> {
|
||||
let cidr_set = CidrSet::new(global_ctx.clone());
|
||||
let (sender, receiver) = unbounded_channel();
|
||||
let (sender, receiver) = channel(1024);
|
||||
let ret = Self {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
@@ -360,7 +379,7 @@ impl UdpProxy {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
nat_table.retain(|_, v| {
|
||||
if v.start_time.elapsed().as_secs() > 120 {
|
||||
if !v.is_active() {
|
||||
tracing::info!(?v, "udp nat table entry removed");
|
||||
v.stop();
|
||||
false
|
||||
@@ -383,7 +402,7 @@ impl UdpProxy {
|
||||
let mut receiver = self.receiver.lock().await.take().unwrap();
|
||||
let peer_manager = self.peer_manager.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
while let Some(msg) = receiver.recv().await {
|
||||
while let Ok(msg) = receiver.recv().await {
|
||||
let to_peer_id: PeerId = msg.peer_manager_header().unwrap().to_peer_id.get();
|
||||
tracing::trace!(?msg, ?to_peer_id, "udp nat packet response send");
|
||||
let ret = peer_manager.send_msg(msg, to_peer_id).await;
|
||||
|
||||
@@ -8,8 +8,6 @@ use anyhow::Context;
|
||||
use cidr::Ipv4Inet;
|
||||
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
use tonic::transport::server::TcpIncoming;
|
||||
use tonic::transport::Server;
|
||||
|
||||
use crate::common::config::ConfigLoader;
|
||||
use crate::common::error::Error;
|
||||
@@ -26,12 +24,20 @@ use crate::peers::peer_conn::PeerConnId;
|
||||
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
|
||||
use crate::peers::rpc_service::PeerManagerRpcService;
|
||||
use crate::peers::PacketRecvChanReceiver;
|
||||
use crate::rpc::vpn_portal_rpc_server::VpnPortalRpc;
|
||||
use crate::rpc::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo};
|
||||
use crate::proto::cli::VpnPortalRpc;
|
||||
use crate::proto::cli::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo};
|
||||
use crate::proto::peer_rpc::PeerCenterRpcServer;
|
||||
use crate::proto::rpc_impl::standalone::StandAloneServer;
|
||||
use crate::proto::rpc_types;
|
||||
use crate::proto::rpc_types::controller::BaseController;
|
||||
use crate::tunnel::tcp::TcpTunnelListener;
|
||||
use crate::vpn_portal::{self, VpnPortal};
|
||||
|
||||
use super::listeners::ListenerManager;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
use crate::gateway::socks5::Socks5Server;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IpProxy {
|
||||
tcp_proxy: Arc<TcpProxy>,
|
||||
@@ -101,8 +107,6 @@ pub struct Instance {
|
||||
|
||||
nic_ctx: ArcNicCtx,
|
||||
|
||||
tasks: JoinSet<()>,
|
||||
|
||||
peer_packet_receiver: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
listener_manager: Arc<Mutex<ListenerManager<PeerManager>>>,
|
||||
@@ -116,6 +120,11 @@ pub struct Instance {
|
||||
|
||||
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server: Arc<Socks5Server>,
|
||||
|
||||
rpc_server: Option<StandAloneServer<TcpTunnelListener>>,
|
||||
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
@@ -152,7 +161,7 @@ impl Instance {
|
||||
DirectConnectorManager::new(global_ctx.clone(), peer_manager.clone());
|
||||
direct_conn_manager.run();
|
||||
|
||||
let udp_hole_puncher = UdpHolePunchConnector::new(global_ctx.clone(), peer_manager.clone());
|
||||
let udp_hole_puncher = UdpHolePunchConnector::new(peer_manager.clone());
|
||||
|
||||
let peer_center = Arc::new(PeerCenterInstance::new(peer_manager.clone()));
|
||||
|
||||
@@ -161,6 +170,15 @@ impl Instance {
|
||||
#[cfg(not(feature = "wireguard"))]
|
||||
let vpn_portal_inst = vpn_portal::NullVpnPortal;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None);
|
||||
|
||||
let rpc_server = global_ctx.config.get_rpc_portal().and_then(|s| {
|
||||
Some(StandAloneServer::new(TcpTunnelListener::new(
|
||||
format!("tcp://{}", s).parse().unwrap(),
|
||||
)))
|
||||
});
|
||||
|
||||
Instance {
|
||||
inst_name: global_ctx.inst_name.clone(),
|
||||
id,
|
||||
@@ -168,7 +186,6 @@ impl Instance {
|
||||
peer_packet_receiver: Arc::new(Mutex::new(peer_packet_receiver)),
|
||||
nic_ctx: Arc::new(Mutex::new(None)),
|
||||
|
||||
tasks: JoinSet::new(),
|
||||
peer_manager,
|
||||
listener_manager,
|
||||
conn_manager,
|
||||
@@ -181,6 +198,11 @@ impl Instance {
|
||||
|
||||
vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))),
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server,
|
||||
|
||||
rpc_server,
|
||||
|
||||
global_ctx,
|
||||
}
|
||||
}
|
||||
@@ -248,19 +270,11 @@ impl Instance {
|
||||
|
||||
let mut used_ipv4 = HashSet::new();
|
||||
for route in routes {
|
||||
if route.ipv4_addr.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(peer_ipv4_addr) = route.ipv4_addr.parse::<Ipv4Addr>() else {
|
||||
let Some(peer_ipv4_addr) = route.ipv4_addr else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(peer_ipv4_addr) = Ipv4Inet::new(peer_ipv4_addr, 24) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
used_ipv4.insert(peer_ipv4_addr);
|
||||
used_ipv4.insert(peer_ipv4_addr.into());
|
||||
}
|
||||
|
||||
let dhcp_inet = used_ipv4.iter().next().unwrap_or(&default_ipv4_addr);
|
||||
@@ -282,7 +296,7 @@ impl Instance {
|
||||
continue;
|
||||
}
|
||||
|
||||
let last_ip = current_dhcp_ip.as_ref().map(Ipv4Inet::address);
|
||||
let last_ip = current_dhcp_ip.clone();
|
||||
tracing::debug!(
|
||||
?current_dhcp_ip,
|
||||
?candidate_ipv4_addr,
|
||||
@@ -294,11 +308,9 @@ impl Instance {
|
||||
if let Some(ip) = candidate_ipv4_addr {
|
||||
if global_ctx_c.no_tun() {
|
||||
current_dhcp_ip = Some(ip);
|
||||
global_ctx_c.set_ipv4(Some(ip.address()));
|
||||
global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Changed(
|
||||
last_ip,
|
||||
Some(ip.address()),
|
||||
));
|
||||
global_ctx_c.set_ipv4(Some(ip));
|
||||
global_ctx_c
|
||||
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip)));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -309,7 +321,7 @@ impl Instance {
|
||||
&peer_manager_c,
|
||||
_peer_packet_receiver.clone(),
|
||||
);
|
||||
if let Err(e) = new_nic_ctx.run(ip.address()).await {
|
||||
if let Err(e) = new_nic_ctx.run(ip).await {
|
||||
tracing::error!(
|
||||
?current_dhcp_ip,
|
||||
?candidate_ipv4_addr,
|
||||
@@ -323,9 +335,8 @@ impl Instance {
|
||||
}
|
||||
|
||||
current_dhcp_ip = Some(ip);
|
||||
global_ctx_c.set_ipv4(Some(ip.address()));
|
||||
global_ctx_c
|
||||
.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip.address())));
|
||||
global_ctx_c.set_ipv4(Some(ip));
|
||||
global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Changed(last_ip, Some(ip)));
|
||||
} else {
|
||||
current_dhcp_ip = None;
|
||||
global_ctx_c.set_ipv4(None);
|
||||
@@ -363,7 +374,7 @@ impl Instance {
|
||||
self.check_dhcp_ip_conflict();
|
||||
}
|
||||
|
||||
self.run_rpc_server()?;
|
||||
self.run_rpc_server().await?;
|
||||
|
||||
// run after tun device created, so listener can bind to tun device, which may be required by win 10
|
||||
self.ip_proxy = Some(IpProxy::new(
|
||||
@@ -387,6 +398,9 @@ impl Instance {
|
||||
self.run_vpn_portal().await?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
self.socks5_server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -426,11 +440,8 @@ impl Instance {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn wait(&mut self) {
|
||||
while let Some(ret) = self.tasks.join_next().await {
|
||||
tracing::info!("task finished: {:?}", ret);
|
||||
ret.unwrap();
|
||||
}
|
||||
pub async fn wait(&self) {
|
||||
self.peer_manager.wait().await;
|
||||
}
|
||||
|
||||
pub fn id(&self) -> uuid::Uuid {
|
||||
@@ -441,24 +452,28 @@ impl Instance {
|
||||
self.peer_manager.my_peer_id()
|
||||
}
|
||||
|
||||
fn get_vpn_portal_rpc_service(&self) -> impl VpnPortalRpc {
|
||||
fn get_vpn_portal_rpc_service(&self) -> impl VpnPortalRpc<Controller = BaseController> + Clone {
|
||||
#[derive(Clone)]
|
||||
struct VpnPortalRpcService {
|
||||
peer_mgr: Weak<PeerManager>,
|
||||
vpn_portal: Weak<Mutex<Box<dyn VpnPortal>>>,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
#[async_trait::async_trait]
|
||||
impl VpnPortalRpc for VpnPortalRpcService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn get_vpn_portal_info(
|
||||
&self,
|
||||
_request: tonic::Request<GetVpnPortalInfoRequest>,
|
||||
) -> Result<tonic::Response<GetVpnPortalInfoResponse>, tonic::Status> {
|
||||
_: BaseController,
|
||||
_request: GetVpnPortalInfoRequest,
|
||||
) -> Result<GetVpnPortalInfoResponse, rpc_types::error::Error> {
|
||||
let Some(vpn_portal) = self.vpn_portal.upgrade() else {
|
||||
return Err(tonic::Status::unavailable("vpn portal not available"));
|
||||
return Err(anyhow::anyhow!("vpn portal not available").into());
|
||||
};
|
||||
|
||||
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
|
||||
return Err(tonic::Status::unavailable("peer manager not available"));
|
||||
return Err(anyhow::anyhow!("peer manager not available").into());
|
||||
};
|
||||
|
||||
let vpn_portal = vpn_portal.lock().await;
|
||||
@@ -470,7 +485,7 @@ impl Instance {
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(tonic::Response::new(ret))
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,46 +495,36 @@ impl Instance {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_rpc_server(&mut self) -> Result<(), Error> {
|
||||
let Some(addr) = self.global_ctx.config.get_rpc_portal() else {
|
||||
async fn run_rpc_server(&mut self) -> Result<(), Error> {
|
||||
let Some(_) = self.global_ctx.config.get_rpc_portal() else {
|
||||
tracing::info!("rpc server not enabled, because rpc_portal is not set.");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
use crate::proto::cli::*;
|
||||
|
||||
let peer_mgr = self.peer_manager.clone();
|
||||
let conn_manager = self.conn_manager.clone();
|
||||
let net_ns = self.global_ctx.net_ns.clone();
|
||||
let peer_center = self.peer_center.clone();
|
||||
let vpn_portal_rpc = self.get_vpn_portal_rpc_service();
|
||||
|
||||
let incoming = TcpIncoming::new(addr, true, None)
|
||||
.map_err(|e| anyhow::anyhow!("create rpc server failed. addr: {}, err: {}", addr, e))?;
|
||||
self.tasks.spawn(async move {
|
||||
let _g = net_ns.guard();
|
||||
Server::builder()
|
||||
.add_service(
|
||||
crate::rpc::peer_manage_rpc_server::PeerManageRpcServer::new(
|
||||
PeerManagerRpcService::new(peer_mgr),
|
||||
),
|
||||
)
|
||||
.add_service(
|
||||
crate::rpc::connector_manage_rpc_server::ConnectorManageRpcServer::new(
|
||||
ConnectorManagerRpcService(conn_manager.clone()),
|
||||
),
|
||||
)
|
||||
.add_service(
|
||||
crate::rpc::peer_center_rpc_server::PeerCenterRpcServer::new(
|
||||
peer_center.get_rpc_service(),
|
||||
),
|
||||
)
|
||||
.add_service(crate::rpc::vpn_portal_rpc_server::VpnPortalRpcServer::new(
|
||||
vpn_portal_rpc,
|
||||
))
|
||||
.serve_with_incoming(incoming)
|
||||
.await
|
||||
.with_context(|| format!("rpc server failed. addr: {}", addr))
|
||||
.unwrap();
|
||||
});
|
||||
Ok(())
|
||||
let s = self.rpc_server.as_mut().unwrap();
|
||||
s.registry().register(
|
||||
PeerManageRpcServer::new(PeerManagerRpcService::new(peer_mgr)),
|
||||
"",
|
||||
);
|
||||
s.registry().register(
|
||||
ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)),
|
||||
"",
|
||||
);
|
||||
|
||||
s.registry()
|
||||
.register(PeerCenterRpcServer::new(peer_center.get_rpc_service()), "");
|
||||
s.registry()
|
||||
.register(VpnPortalRpcServer::new(vpn_portal_rpc), "");
|
||||
|
||||
let _g = self.global_ctx.net_ns.guard();
|
||||
Ok(s.serve().await.with_context(|| "rpc server start failed")?)
|
||||
}
|
||||
|
||||
pub fn get_global_ctx(&self) -> ArcGlobalCtx {
|
||||
|
||||
@@ -111,9 +111,10 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
}
|
||||
|
||||
if self.global_ctx.config.get_flags().enable_ipv6 {
|
||||
let ipv6_listener = self.global_ctx.config.get_flags().ipv6_listener.clone();
|
||||
let _ = self
|
||||
.add_listener(
|
||||
UdpTunnelListener::new("udp://[::]:0".parse().unwrap()),
|
||||
UdpTunnelListener::new(ipv6_listener.parse().unwrap()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
@@ -159,8 +160,16 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
|
||||
let tunnel_info = ret.info().unwrap();
|
||||
global_ctx.issue_event(GlobalCtxEvent::ConnectionAccepted(
|
||||
tunnel_info.local_addr.clone(),
|
||||
tunnel_info.remote_addr.clone(),
|
||||
tunnel_info
|
||||
.local_addr
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
tunnel_info
|
||||
.remote_addr
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
));
|
||||
tracing::info!(ret = ?ret, "conn accepted");
|
||||
let peer_manager = peer_manager.clone();
|
||||
@@ -169,8 +178,8 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
|
||||
let server_ret = peer_manager.handle_tunnel(ret).await;
|
||||
if let Err(e) = &server_ret {
|
||||
global_ctx.issue_event(GlobalCtxEvent::ConnectionError(
|
||||
tunnel_info.local_addr,
|
||||
tunnel_info.remote_addr,
|
||||
tunnel_info.local_addr.unwrap_or_default().to_string(),
|
||||
tunnel_info.remote_addr.unwrap_or_default().to_string(),
|
||||
e.to_string(),
|
||||
));
|
||||
tracing::error!(error = ?e, "handle conn error");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
pub mod instance;
|
||||
pub mod listeners;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod tun_codec;
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod virtual_nic;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use byteorder::{NativeEndian, NetworkEndian, WriteBytesExt};
|
||||
use tokio_util::bytes::{BufMut, Bytes, BytesMut};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
/// A packet protocol IP version
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
enum PacketProtocol {
|
||||
#[default]
|
||||
IPv4,
|
||||
IPv6,
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
// Note: the protocol in the packet information header is platform dependent.
|
||||
impl PacketProtocol {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16),
|
||||
PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16),
|
||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"neither an IPv4 nor IPv6 packet",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
|
||||
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
|
||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"neither an IPv4 nor IPv6 packet",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TunPacketBuffer {
|
||||
Bytes(Bytes),
|
||||
BytesMut(BytesMut),
|
||||
}
|
||||
|
||||
impl From<TunPacketBuffer> for Bytes {
|
||||
fn from(buf: TunPacketBuffer) -> Self {
|
||||
match buf {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes,
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.freeze(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for TunPacketBuffer {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes.as_ref(),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tun Packet to be sent or received on the TUN interface.
|
||||
#[derive(Debug)]
|
||||
pub struct TunPacket(PacketProtocol, TunPacketBuffer);
|
||||
|
||||
/// Infer the protocol based on the first nibble in the packet buffer.
|
||||
fn infer_proto(buf: &[u8]) -> PacketProtocol {
|
||||
match buf[0] >> 4 {
|
||||
4 => PacketProtocol::IPv4,
|
||||
6 => PacketProtocol::IPv6,
|
||||
p => PacketProtocol::Other(p),
|
||||
}
|
||||
}
|
||||
|
||||
impl TunPacket {
|
||||
/// Create a new `TunPacket` based on a byte slice.
|
||||
pub fn new(buffer: TunPacketBuffer) -> TunPacket {
|
||||
let proto = infer_proto(buffer.as_ref());
|
||||
TunPacket(proto, buffer)
|
||||
}
|
||||
|
||||
/// Return this packet's bytes.
|
||||
pub fn get_bytes(&self) -> &[u8] {
|
||||
match &self.1 {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes.as_ref(),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
match self.1 {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes,
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.freeze(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes_mut(self) -> BytesMut {
|
||||
match self.1 {
|
||||
TunPacketBuffer::Bytes(_) => panic!("cannot into_bytes_mut from bytes"),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A TunPacket Encoder/Decoder.
|
||||
pub struct TunPacketCodec(bool, i32);
|
||||
|
||||
impl TunPacketCodec {
|
||||
/// Create a new `TunPacketCodec` specifying whether the underlying
|
||||
/// tunnel Device has enabled the packet information header.
|
||||
pub fn new(pi: bool, mtu: i32) -> TunPacketCodec {
|
||||
TunPacketCodec(pi, mtu)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for TunPacketCodec {
|
||||
type Item = TunPacket;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if buf.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut pkt = buf.split_to(buf.len());
|
||||
|
||||
// reserve enough space for the next packet
|
||||
if self.0 {
|
||||
buf.reserve(self.1 as usize + 4);
|
||||
} else {
|
||||
buf.reserve(self.1 as usize);
|
||||
}
|
||||
|
||||
// if the packet information is enabled we have to ignore the first 4 bytes
|
||||
if self.0 {
|
||||
let _ = pkt.split_to(4);
|
||||
}
|
||||
|
||||
let proto = infer_proto(pkt.as_ref());
|
||||
Ok(Some(TunPacket(proto, TunPacketBuffer::BytesMut(pkt))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<TunPacket> for TunPacketCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, item: TunPacket, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
dst.reserve(item.get_bytes().len() + 4);
|
||||
match item {
|
||||
TunPacket(proto, bytes) if self.0 => {
|
||||
// build the packet information header comprising of 2 u16
|
||||
// fields: flags and protocol.
|
||||
let mut buf = Vec::<u8>::with_capacity(4);
|
||||
|
||||
// flags is always 0
|
||||
buf.write_u16::<NativeEndian>(0)?;
|
||||
// write the protocol as network byte order
|
||||
buf.write_u16::<NetworkEndian>(proto.into_pi_field()?)?;
|
||||
|
||||
dst.put_slice(&buf);
|
||||
dst.put(Bytes::from(bytes));
|
||||
}
|
||||
TunPacket(_, bytes) => dst.put(Bytes::from(bytes)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ impl PacketProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
@@ -242,8 +242,9 @@ pub struct VirtualNic {
|
||||
ifname: Option<String>,
|
||||
ifcfg: Box<dyn IfConfiguerTrait + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn checkreg() -> io::Result<()> {
|
||||
pub fn checkreg(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(
|
||||
@@ -262,7 +263,9 @@ pub fn checkreg() -> io::Result<()> {
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("ProfileName") {
|
||||
Ok(profile_name) => {
|
||||
if profile_name.contains("et_") {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete.push(subkey_name);
|
||||
}
|
||||
}
|
||||
@@ -280,7 +283,9 @@ pub fn checkreg() -> io::Result<()> {
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("Description") {
|
||||
Ok(profile_name) => {
|
||||
if profile_name.contains("et_") {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete_unmanaged.push(subkey_name);
|
||||
}
|
||||
}
|
||||
@@ -334,7 +339,7 @@ impl VirtualNic {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos"))]
|
||||
config.platform_config(|config| {
|
||||
// disable packet information so we can process the header by ourselves, see tun2 impl for more details
|
||||
config.packet_information(false);
|
||||
@@ -342,25 +347,32 @@ impl VirtualNic {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match checkreg() {
|
||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||
|
||||
match checkreg(&dev_name) {
|
||||
Ok(_) => tracing::trace!("delete successful!"),
|
||||
Err(e) => tracing::error!("An error occurred: {}", e),
|
||||
}
|
||||
use rand::distributions::Distribution as _;
|
||||
let c = crate::arch::windows::interface_count()?;
|
||||
let mut rng = rand::thread_rng();
|
||||
let s: String = rand::distributions::Alphanumeric
|
||||
.sample_iter(&mut rng)
|
||||
.take(4)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
.to_lowercase();
|
||||
|
||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||
if !dev_name.is_empty() {
|
||||
config.tun_name(format!("{}", dev_name));
|
||||
} else {
|
||||
config.tun_name(format!("et{}_{}", c, s));
|
||||
use rand::distributions::Distribution as _;
|
||||
let c = crate::arch::windows::interface_count()?;
|
||||
let mut rng = rand::thread_rng();
|
||||
let s: String = rand::distributions::Alphanumeric
|
||||
.sample_iter(&mut rng)
|
||||
.take(4)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
.to_lowercase();
|
||||
|
||||
let random_dev_name = format!("et_{}_{}", c, s);
|
||||
config.tun_name(random_dev_name.clone());
|
||||
|
||||
let mut flags = self.global_ctx.get_flags();
|
||||
flags.dev_name = random_dev_name.clone();
|
||||
self.global_ctx.set_flags(flags);
|
||||
}
|
||||
|
||||
config.platform_config(|config| {
|
||||
@@ -479,6 +491,38 @@ impl VirtualNic {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
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(())
|
||||
}
|
||||
|
||||
pub struct NicCtx {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_mgr: Weak<PeerManager>,
|
||||
@@ -503,13 +547,16 @@ impl NicCtx {
|
||||
}
|
||||
}
|
||||
|
||||
async fn assign_ipv4_to_tun_device(&self, ipv4_addr: Ipv4Addr) -> Result<(), Error> {
|
||||
async fn assign_ipv4_to_tun_device(&self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
|
||||
let nic = self.nic.lock().await;
|
||||
nic.link_up().await?;
|
||||
nic.remove_ip(None).await?;
|
||||
nic.add_ip(ipv4_addr, 24).await?;
|
||||
if cfg!(target_os = "macos") {
|
||||
nic.add_route(ipv4_addr, 24).await?;
|
||||
nic.add_ip(ipv4_addr.address(), ipv4_addr.network_length() as i32)
|
||||
.await?;
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
{
|
||||
nic.add_route(ipv4_addr.first_address(), ipv4_addr.network_length())
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -552,6 +599,7 @@ impl NicCtx {
|
||||
}
|
||||
Self::do_forward_nic_to_peers_ipv4(ret.unwrap(), mgr.as_ref()).await;
|
||||
}
|
||||
panic!("nic stream closed");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -572,6 +620,7 @@ impl NicCtx {
|
||||
tracing::error!(?ret, "do_forward_tunnel_to_nic sink error");
|
||||
}
|
||||
}
|
||||
panic!("peer packet receiver closed");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -662,11 +711,17 @@ impl NicCtx {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, ipv4_addr: Ipv4Addr) -> Result<(), Error> {
|
||||
pub async fn run(&mut self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> {
|
||||
let tunnel = {
|
||||
let mut nic = self.nic.lock().await;
|
||||
match nic.create_dev().await {
|
||||
Ok(ret) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||
let _ = reg_change_catrgory_in_profile(&dev_name);
|
||||
}
|
||||
|
||||
self.global_ctx
|
||||
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
|
||||
ret
|
||||
|
||||
@@ -6,14 +6,16 @@ use std::{
|
||||
use crate::{
|
||||
common::{
|
||||
config::{ConfigLoader, TomlConfigLoader},
|
||||
constants::EASYTIER_VERSION,
|
||||
global_ctx::GlobalCtxEvent,
|
||||
stun::StunInfoCollectorTrait,
|
||||
},
|
||||
instance::instance::Instance,
|
||||
peers::rpc_service::PeerManagerRpcService,
|
||||
rpc::{
|
||||
cli::{PeerInfo, Route, StunInfo},
|
||||
peer::GetIpListResponse,
|
||||
proto::{
|
||||
cli::{PeerInfo, Route},
|
||||
common::StunInfo,
|
||||
peer_rpc::GetIpListResponse,
|
||||
},
|
||||
utils::{list_peer_route_pair, PeerRoutePair},
|
||||
};
|
||||
@@ -24,6 +26,8 @@ use tokio::task::JoinSet;
|
||||
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MyNodeInfo {
|
||||
pub virtual_ipv4: String,
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub ips: GetIpListResponse,
|
||||
pub stun_info: StunInfo,
|
||||
pub listeners: Vec<String>,
|
||||
@@ -37,6 +41,7 @@ struct EasyTierData {
|
||||
routes: Arc<RwLock<Vec<Route>>>,
|
||||
peers: Arc<RwLock<Vec<PeerInfo>>>,
|
||||
tun_fd: Arc<RwLock<Option<i32>>>,
|
||||
tun_dev_name: Arc<RwLock<String>>,
|
||||
}
|
||||
|
||||
pub struct EasyTierLauncher {
|
||||
@@ -132,11 +137,17 @@ impl EasyTierLauncher {
|
||||
let vpn_portal = instance.get_vpn_portal_inst();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
|
||||
// Update TUN Device Name
|
||||
*data_c.tun_dev_name.write().unwrap() = global_ctx_c.get_flags().dev_name.clone();
|
||||
|
||||
let node_info = MyNodeInfo {
|
||||
virtual_ipv4: global_ctx_c
|
||||
.get_ipv4()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
hostname: global_ctx_c.get_hostname(),
|
||||
version: EASYTIER_VERSION.to_string(),
|
||||
ips: global_ctx_c.get_ip_collector().collect_ip_addrs().await,
|
||||
stun_info: global_ctx_c.get_stun_info_collector().get_stun_info(),
|
||||
listeners: global_ctx_c
|
||||
@@ -229,6 +240,10 @@ impl EasyTierLauncher {
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn get_dev_name(&self) -> String {
|
||||
self.data.tun_dev_name.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn get_events(&self) -> Vec<(DateTime<Local>, GlobalCtxEvent)> {
|
||||
let events = self.data.events.read().unwrap();
|
||||
events.iter().cloned().collect()
|
||||
@@ -261,6 +276,7 @@ impl Drop for EasyTierLauncher {
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct NetworkInstanceRunningInfo {
|
||||
pub dev_name: String,
|
||||
pub my_node_info: MyNodeInfo,
|
||||
pub events: Vec<(DateTime<Local>, GlobalCtxEvent)>,
|
||||
pub node_info: MyNodeInfo,
|
||||
@@ -300,6 +316,7 @@ impl NetworkInstance {
|
||||
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone());
|
||||
|
||||
Some(NetworkInstanceRunningInfo {
|
||||
dev_name: launcher.get_dev_name(),
|
||||
my_node_info: launcher.get_node_info(),
|
||||
events: launcher.get_events(),
|
||||
node_info: launcher.get_node_info(),
|
||||
|
||||
@@ -6,10 +6,12 @@ mod gateway;
|
||||
mod instance;
|
||||
mod peer_center;
|
||||
mod peers;
|
||||
mod proto;
|
||||
mod vpn_portal;
|
||||
|
||||
pub mod common;
|
||||
pub mod launcher;
|
||||
pub mod rpc;
|
||||
pub mod tunnel;
|
||||
pub mod utils;
|
||||
|
||||
pub const VERSION: &str = common::constants::EASYTIER_VERSION;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
@@ -18,14 +18,17 @@ use crate::{
|
||||
route_trait::{RouteCostCalculator, RouteCostCalculatorInterface},
|
||||
rpc_service::PeerManagerRpcService,
|
||||
},
|
||||
rpc::{GetGlobalPeerMapRequest, GetGlobalPeerMapResponse},
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
GetGlobalPeerMapRequest, GetGlobalPeerMapResponse, GlobalPeerMap, PeerCenterRpc,
|
||||
PeerCenterRpcClientFactory, PeerCenterRpcServer, PeerInfoForGlobalMap,
|
||||
ReportPeersRequest, ReportPeersResponse,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
server::PeerCenterServer,
|
||||
service::{GlobalPeerMap, PeerCenterService, PeerCenterServiceClient, PeerInfoForGlobalMap},
|
||||
Digest, Error,
|
||||
};
|
||||
use super::{server::PeerCenterServer, Digest, Error};
|
||||
|
||||
struct PeerCenterBase {
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
@@ -44,11 +47,14 @@ struct PeridicJobCtx<T> {
|
||||
|
||||
impl PeerCenterBase {
|
||||
pub async fn init(&self) -> Result<(), Error> {
|
||||
self.peer_mgr.get_peer_rpc_mgr().run_service(
|
||||
SERVICE_ID,
|
||||
PeerCenterServer::new(self.peer_mgr.my_peer_id()).serve(),
|
||||
);
|
||||
|
||||
self.peer_mgr
|
||||
.get_peer_rpc_mgr()
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.register(
|
||||
PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())),
|
||||
&self.peer_mgr.get_global_ctx().get_network_name(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -59,7 +65,10 @@ impl PeerCenterBase {
|
||||
}
|
||||
// find peer with alphabetical smallest id.
|
||||
let mut min_peer = peer_mgr.my_peer_id();
|
||||
for peer in peers.iter() {
|
||||
for peer in peers
|
||||
.iter()
|
||||
.filter(|r| r.feature_flag.map(|r| !r.is_public_server).unwrap_or(true))
|
||||
{
|
||||
let peer_id = peer.peer_id;
|
||||
if peer_id < min_peer {
|
||||
min_peer = peer_id;
|
||||
@@ -70,11 +79,17 @@ impl PeerCenterBase {
|
||||
|
||||
async fn init_periodic_job<
|
||||
T: Send + Sync + 'static + Clone,
|
||||
Fut: Future<Output = Result<u32, tarpc::client::RpcError>> + Send + 'static,
|
||||
Fut: Future<Output = Result<u32, rpc_types::error::Error>> + Send + 'static,
|
||||
>(
|
||||
&self,
|
||||
job_ctx: T,
|
||||
job_fn: (impl Fn(PeerCenterServiceClient, Arc<PeridicJobCtx<T>>) -> Fut + Send + Sync + 'static),
|
||||
job_fn: (impl Fn(
|
||||
Box<dyn PeerCenterRpc<Controller = BaseController> + Send>,
|
||||
Arc<PeridicJobCtx<T>>,
|
||||
) -> Fut
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static),
|
||||
) -> () {
|
||||
let my_peer_id = self.peer_mgr.my_peer_id();
|
||||
let peer_mgr = self.peer_mgr.clone();
|
||||
@@ -96,14 +111,14 @@ impl PeerCenterBase {
|
||||
tracing::trace!(?center_peer, "run periodic job");
|
||||
let rpc_mgr = peer_mgr.get_peer_rpc_mgr();
|
||||
let _g = lock.lock().await;
|
||||
let ret = rpc_mgr
|
||||
.do_client_rpc_scoped(SERVICE_ID, center_peer, |c| async {
|
||||
let client =
|
||||
PeerCenterServiceClient::new(tarpc::client::Config::default(), c)
|
||||
.spawn();
|
||||
job_fn(client, ctx.clone()).await
|
||||
})
|
||||
.await;
|
||||
let stub = rpc_mgr
|
||||
.rpc_client()
|
||||
.scoped_client::<PeerCenterRpcClientFactory<BaseController>>(
|
||||
my_peer_id,
|
||||
center_peer,
|
||||
peer_mgr.get_global_ctx().get_network_name(),
|
||||
);
|
||||
let ret = job_fn(stub, ctx.clone()).await;
|
||||
drop(_g);
|
||||
|
||||
let Ok(sleep_time_ms) = ret else {
|
||||
@@ -130,25 +145,34 @@ impl PeerCenterBase {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PeerCenterInstanceService {
|
||||
global_peer_map: Arc<RwLock<GlobalPeerMap>>,
|
||||
global_peer_map_digest: Arc<AtomicCell<Digest>>,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl crate::rpc::cli::peer_center_rpc_server::PeerCenterRpc for PeerCenterInstanceService {
|
||||
#[async_trait::async_trait]
|
||||
impl PeerCenterRpc for PeerCenterInstanceService {
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn get_global_peer_map(
|
||||
&self,
|
||||
_request: tonic::Request<GetGlobalPeerMapRequest>,
|
||||
) -> Result<tonic::Response<GetGlobalPeerMapResponse>, tonic::Status> {
|
||||
let global_peer_map = self.global_peer_map.read().unwrap().clone();
|
||||
Ok(tonic::Response::new(GetGlobalPeerMapResponse {
|
||||
global_peer_map: global_peer_map
|
||||
.map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v))
|
||||
.collect(),
|
||||
}))
|
||||
_: BaseController,
|
||||
_: GetGlobalPeerMapRequest,
|
||||
) -> Result<GetGlobalPeerMapResponse, rpc_types::error::Error> {
|
||||
let global_peer_map = self.global_peer_map.read().unwrap();
|
||||
Ok(GetGlobalPeerMapResponse {
|
||||
global_peer_map: global_peer_map.map.clone(),
|
||||
digest: Some(self.global_peer_map_digest.load()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn report_peers(
|
||||
&self,
|
||||
_: BaseController,
|
||||
_req: ReportPeersRequest,
|
||||
) -> Result<ReportPeersResponse, rpc_types::error::Error> {
|
||||
Err(anyhow::anyhow!("not implemented").into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +190,7 @@ impl PeerCenterInstance {
|
||||
PeerCenterInstance {
|
||||
peer_mgr: peer_mgr.clone(),
|
||||
client: Arc::new(PeerCenterBase::new(peer_mgr.clone())),
|
||||
global_peer_map: Arc::new(RwLock::new(GlobalPeerMap::new())),
|
||||
global_peer_map: Arc::new(RwLock::new(GlobalPeerMap::default())),
|
||||
global_peer_map_digest: Arc::new(AtomicCell::new(Digest::default())),
|
||||
global_peer_map_update_time: Arc::new(AtomicCell::new(Instant::now())),
|
||||
}
|
||||
@@ -193,35 +217,38 @@ impl PeerCenterInstance {
|
||||
|
||||
self.client
|
||||
.init_periodic_job(ctx, |client, ctx| async move {
|
||||
let mut rpc_ctx = tarpc::context::current();
|
||||
rpc_ctx.deadline = SystemTime::now() + Duration::from_secs(3);
|
||||
|
||||
if ctx
|
||||
.job_ctx
|
||||
.global_peer_map_update_time
|
||||
.load()
|
||||
.elapsed()
|
||||
.as_secs()
|
||||
> 60
|
||||
> 120
|
||||
{
|
||||
ctx.job_ctx.global_peer_map_digest.store(Digest::default());
|
||||
}
|
||||
|
||||
let ret = client
|
||||
.get_global_peer_map(rpc_ctx, ctx.job_ctx.global_peer_map_digest.load())
|
||||
.await?;
|
||||
.get_global_peer_map(
|
||||
BaseController::default(),
|
||||
GetGlobalPeerMapRequest {
|
||||
digest: ctx.job_ctx.global_peer_map_digest.load(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let Ok(resp) = ret else {
|
||||
tracing::error!(
|
||||
"get global info from center server got error result: {:?}",
|
||||
ret
|
||||
);
|
||||
return Ok(1000);
|
||||
return Ok(10000);
|
||||
};
|
||||
|
||||
let Some(resp) = resp else {
|
||||
return Ok(5000);
|
||||
};
|
||||
if resp == GetGlobalPeerMapResponse::default() {
|
||||
// digest match, no need to update
|
||||
return Ok(15000);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"get global info from center server: {:?}, digest: {:?}",
|
||||
@@ -229,13 +256,17 @@ impl PeerCenterInstance {
|
||||
resp.digest
|
||||
);
|
||||
|
||||
*ctx.job_ctx.global_peer_map.write().unwrap() = resp.global_peer_map;
|
||||
ctx.job_ctx.global_peer_map_digest.store(resp.digest);
|
||||
*ctx.job_ctx.global_peer_map.write().unwrap() = GlobalPeerMap {
|
||||
map: resp.global_peer_map,
|
||||
};
|
||||
ctx.job_ctx
|
||||
.global_peer_map_digest
|
||||
.store(resp.digest.unwrap_or_default());
|
||||
ctx.job_ctx
|
||||
.global_peer_map_update_time
|
||||
.store(Instant::now());
|
||||
|
||||
Ok(5000)
|
||||
Ok(15000)
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -274,12 +305,15 @@ impl PeerCenterInstance {
|
||||
return Ok(5000);
|
||||
}
|
||||
|
||||
let mut rpc_ctx = tarpc::context::current();
|
||||
rpc_ctx.deadline = SystemTime::now() + Duration::from_secs(3);
|
||||
|
||||
let ret = client
|
||||
.report_peers(rpc_ctx, my_node_id.clone(), peers)
|
||||
.await?;
|
||||
.report_peers(
|
||||
BaseController::default(),
|
||||
ReportPeersRequest {
|
||||
my_peer_id: my_node_id,
|
||||
peer_infos: Some(peers),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if ret.is_ok() {
|
||||
ctx.job_ctx.last_center_peer.store(ctx.center_peer.load());
|
||||
@@ -311,15 +345,22 @@ impl PeerCenterInstance {
|
||||
global_peer_map_update_time: Arc<AtomicCell<Instant>>,
|
||||
}
|
||||
|
||||
impl RouteCostCalculatorInterface for RouteCostCalculatorImpl {
|
||||
fn calculate_cost(&self, src: PeerId, dst: PeerId) -> i32 {
|
||||
let ret = self
|
||||
.global_peer_map_clone
|
||||
impl RouteCostCalculatorImpl {
|
||||
fn directed_cost(&self, src: PeerId, dst: PeerId) -> Option<i32> {
|
||||
self.global_peer_map_clone
|
||||
.map
|
||||
.get(&src)
|
||||
.and_then(|src_peer_info| src_peer_info.direct_peers.get(&dst))
|
||||
.and_then(|info| Some(info.latency_ms));
|
||||
ret.unwrap_or(80)
|
||||
.and_then(|info| Some(info.latency_ms))
|
||||
}
|
||||
}
|
||||
|
||||
impl RouteCostCalculatorInterface for RouteCostCalculatorImpl {
|
||||
fn calculate_cost(&self, src: PeerId, dst: PeerId) -> i32 {
|
||||
if let Some(cost) = self.directed_cost(src, dst) {
|
||||
return cost;
|
||||
}
|
||||
self.directed_cost(dst, src).unwrap_or(100)
|
||||
}
|
||||
|
||||
fn begin_update(&mut self) {
|
||||
@@ -339,7 +380,7 @@ impl PeerCenterInstance {
|
||||
|
||||
Box::new(RouteCostCalculatorImpl {
|
||||
global_peer_map: self.global_peer_map.clone(),
|
||||
global_peer_map_clone: GlobalPeerMap::new(),
|
||||
global_peer_map_clone: GlobalPeerMap::default(),
|
||||
last_update_time: AtomicCell::new(
|
||||
self.global_peer_map_update_time.load() - Duration::from_secs(1),
|
||||
),
|
||||
@@ -395,7 +436,7 @@ mod tests {
|
||||
false
|
||||
}
|
||||
},
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -404,7 +445,7 @@ mod tests {
|
||||
let rpc_service = pc.get_rpc_service();
|
||||
wait_for_condition(
|
||||
|| async { rpc_service.global_peer_map.read().unwrap().map.len() == 3 },
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(20),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
// peer center is not guaranteed to be stable and can be changed when peer enter or leave.
|
||||
// it's used to reduce the cost to exchange infos between peers.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::proto::cli::PeerInfo;
|
||||
use crate::proto::peer_rpc::{DirectConnectedPeerInfo, PeerInfoForGlobalMap};
|
||||
|
||||
pub mod instance;
|
||||
mod server;
|
||||
mod service;
|
||||
|
||||
#[derive(thiserror::Error, Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub enum Error {
|
||||
@@ -18,3 +22,29 @@ pub enum Error {
|
||||
}
|
||||
|
||||
pub type Digest = u64;
|
||||
|
||||
impl From<Vec<PeerInfo>> for PeerInfoForGlobalMap {
|
||||
fn from(peers: Vec<PeerInfo>) -> Self {
|
||||
let mut peer_map = BTreeMap::new();
|
||||
for peer in peers {
|
||||
let Some(min_lat) = peer
|
||||
.conns
|
||||
.iter()
|
||||
.map(|conn| conn.stats.as_ref().unwrap().latency_us)
|
||||
.min()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let dp_info = DirectConnectedPeerInfo {
|
||||
latency_ms: std::cmp::max(1, (min_lat as u32 / 1000) as i32),
|
||||
};
|
||||
|
||||
// sort conn info so hash result is stable
|
||||
peer_map.insert(peer.peer_id, dp_info);
|
||||
}
|
||||
PeerInfoForGlobalMap {
|
||||
direct_peers: peer_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,22 @@ use std::{
|
||||
use crossbeam::atomic::AtomicCell;
|
||||
use dashmap::DashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::{task::JoinSet};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::{common::PeerId, rpc::DirectConnectedPeerInfo};
|
||||
|
||||
use super::{
|
||||
service::{GetGlobalPeerMapResponse, GlobalPeerMap, PeerCenterService, PeerInfoForGlobalMap},
|
||||
Digest, Error,
|
||||
use crate::{
|
||||
common::PeerId,
|
||||
proto::{
|
||||
peer_rpc::{
|
||||
DirectConnectedPeerInfo, GetGlobalPeerMapRequest, GetGlobalPeerMapResponse,
|
||||
GlobalPeerMap, PeerCenterRpc, PeerInfoForGlobalMap, ReportPeersRequest,
|
||||
ReportPeersResponse,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
};
|
||||
|
||||
use super::Digest;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
|
||||
pub(crate) struct SrcDstPeerPair {
|
||||
src: PeerId,
|
||||
@@ -95,15 +102,19 @@ impl PeerCenterServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl PeerCenterService for PeerCenterServer {
|
||||
#[async_trait::async_trait]
|
||||
impl PeerCenterRpc for PeerCenterServer {
|
||||
type Controller = BaseController;
|
||||
|
||||
#[tracing::instrument()]
|
||||
async fn report_peers(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
my_peer_id: PeerId,
|
||||
peers: PeerInfoForGlobalMap,
|
||||
) -> Result<(), Error> {
|
||||
&self,
|
||||
_: BaseController,
|
||||
req: ReportPeersRequest,
|
||||
) -> Result<ReportPeersResponse, rpc_types::error::Error> {
|
||||
let my_peer_id = req.my_peer_id;
|
||||
let peers = req.peer_infos.unwrap_or_default();
|
||||
|
||||
tracing::debug!("receive report_peers");
|
||||
|
||||
let data = get_global_data(self.my_node_id);
|
||||
@@ -125,20 +136,23 @@ impl PeerCenterService for PeerCenterServer {
|
||||
data.digest
|
||||
.store(PeerCenterServer::calc_global_digest(self.my_node_id));
|
||||
|
||||
Ok(())
|
||||
Ok(ReportPeersResponse::default())
|
||||
}
|
||||
|
||||
#[tracing::instrument()]
|
||||
async fn get_global_peer_map(
|
||||
self,
|
||||
_: tarpc::context::Context,
|
||||
digest: Digest,
|
||||
) -> Result<Option<GetGlobalPeerMapResponse>, Error> {
|
||||
&self,
|
||||
_: BaseController,
|
||||
req: GetGlobalPeerMapRequest,
|
||||
) -> Result<GetGlobalPeerMapResponse, rpc_types::error::Error> {
|
||||
let digest = req.digest;
|
||||
|
||||
let data = get_global_data(self.my_node_id);
|
||||
if digest == data.digest.load() && digest != 0 {
|
||||
return Ok(None);
|
||||
return Ok(GetGlobalPeerMapResponse::default());
|
||||
}
|
||||
|
||||
let mut global_peer_map = GlobalPeerMap::new();
|
||||
let mut global_peer_map = GlobalPeerMap::default();
|
||||
for item in data.global_peer_map.iter() {
|
||||
let (pair, entry) = item.pair();
|
||||
global_peer_map
|
||||
@@ -151,9 +165,9 @@ impl PeerCenterService for PeerCenterServer {
|
||||
.insert(pair.dst, entry.info.clone());
|
||||
}
|
||||
|
||||
Ok(Some(GetGlobalPeerMapResponse {
|
||||
global_peer_map,
|
||||
digest: data.digest.load(),
|
||||
}))
|
||||
Ok(GetGlobalPeerMapResponse {
|
||||
global_peer_map: global_peer_map.map,
|
||||
digest: Some(data.digest.load()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{common::PeerId, rpc::DirectConnectedPeerInfo};
|
||||
|
||||
use super::{Digest, Error};
|
||||
use crate::rpc::PeerInfo;
|
||||
|
||||
pub type PeerInfoForGlobalMap = crate::rpc::cli::PeerInfoForGlobalMap;
|
||||
|
||||
impl From<Vec<PeerInfo>> for PeerInfoForGlobalMap {
|
||||
fn from(peers: Vec<PeerInfo>) -> Self {
|
||||
let mut peer_map = BTreeMap::new();
|
||||
for peer in peers {
|
||||
let Some(min_lat) = peer
|
||||
.conns
|
||||
.iter()
|
||||
.map(|conn| conn.stats.as_ref().unwrap().latency_us)
|
||||
.min()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let dp_info = DirectConnectedPeerInfo {
|
||||
latency_ms: std::cmp::max(1, (min_lat as u32 / 1000) as i32),
|
||||
};
|
||||
|
||||
// sort conn info so hash result is stable
|
||||
peer_map.insert(peer.peer_id, dp_info);
|
||||
}
|
||||
PeerInfoForGlobalMap {
|
||||
direct_peers: peer_map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a global peer topology map, peers can use it to find optimal path to other peers
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GlobalPeerMap {
|
||||
pub map: BTreeMap<PeerId, PeerInfoForGlobalMap>,
|
||||
}
|
||||
|
||||
impl GlobalPeerMap {
|
||||
pub fn new() -> Self {
|
||||
GlobalPeerMap {
|
||||
map: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct GetGlobalPeerMapResponse {
|
||||
pub global_peer_map: GlobalPeerMap,
|
||||
pub digest: Digest,
|
||||
}
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait PeerCenterService {
|
||||
// report center server which peer is directly connected to me
|
||||
// digest is a hash of current peer map, if digest not match, we need to transfer the whole map
|
||||
async fn report_peers(my_peer_id: PeerId, peers: PeerInfoForGlobalMap) -> Result<(), Error>;
|
||||
|
||||
async fn get_global_peer_map(digest: Digest)
|
||||
-> Result<Option<GetGlobalPeerMapResponse>, Error>;
|
||||
}
|
||||
@@ -1,27 +1,11 @@
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use tokio::{sync::Mutex, task::JoinSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, NetworkIdentity},
|
||||
PeerId,
|
||||
},
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, scoped_task::ScopedTask, PeerId},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
|
||||
use super::{
|
||||
foreign_network_manager::{ForeignNetworkServiceClient, FOREIGN_NETWORK_SERVICE_ID},
|
||||
peer_conn::PeerConn,
|
||||
peer_map::PeerMap,
|
||||
peer_rpc::PeerRpcManager,
|
||||
PacketRecvChan,
|
||||
};
|
||||
use super::{peer_conn::PeerConn, peer_map::PeerMap, peer_rpc::PeerRpcManager, PacketRecvChan};
|
||||
|
||||
pub struct ForeignNetworkClient {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
@@ -29,9 +13,7 @@ pub struct ForeignNetworkClient {
|
||||
my_peer_id: PeerId,
|
||||
|
||||
peer_map: Arc<PeerMap>,
|
||||
|
||||
next_hop: Arc<DashMap<PeerId, PeerId>>,
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
task: Mutex<Option<ScopedTask<()>>>,
|
||||
}
|
||||
|
||||
impl ForeignNetworkClient {
|
||||
@@ -46,17 +28,13 @@ impl ForeignNetworkClient {
|
||||
global_ctx.clone(),
|
||||
my_peer_id,
|
||||
));
|
||||
let next_hop = Arc::new(DashMap::new());
|
||||
|
||||
Self {
|
||||
global_ctx,
|
||||
peer_rpc,
|
||||
my_peer_id,
|
||||
|
||||
peer_map,
|
||||
|
||||
next_hop,
|
||||
tasks: Mutex::new(JoinSet::new()),
|
||||
task: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,91 +43,19 @@ impl ForeignNetworkClient {
|
||||
self.peer_map.add_new_peer_conn(peer_conn).await
|
||||
}
|
||||
|
||||
async fn collect_next_hop_in_foreign_network_task(
|
||||
network_identity: NetworkIdentity,
|
||||
peer_map: Arc<PeerMap>,
|
||||
peer_rpc: Arc<PeerRpcManager>,
|
||||
next_hop: Arc<DashMap<PeerId, PeerId>>,
|
||||
) {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
peer_map.clean_peer_without_conn().await;
|
||||
|
||||
let new_next_hop = Self::collect_next_hop_in_foreign_network(
|
||||
network_identity.clone(),
|
||||
peer_map.clone(),
|
||||
peer_rpc.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
next_hop.clear();
|
||||
for (k, v) in new_next_hop.into_iter() {
|
||||
next_hop.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_next_hop_in_foreign_network(
|
||||
network_identity: NetworkIdentity,
|
||||
peer_map: Arc<PeerMap>,
|
||||
peer_rpc: Arc<PeerRpcManager>,
|
||||
) -> DashMap<PeerId, PeerId> {
|
||||
let peers = peer_map.list_peers().await;
|
||||
let mut tasks = JoinSet::new();
|
||||
if !peers.is_empty() {
|
||||
tracing::warn!(?peers, my_peer_id = ?peer_rpc.my_peer_id(), "collect next hop in foreign network");
|
||||
}
|
||||
for peer in peers {
|
||||
let peer_rpc = peer_rpc.clone();
|
||||
let network_identity = network_identity.clone();
|
||||
tasks.spawn(async move {
|
||||
let Ok(Some(peers_in_foreign)) = peer_rpc
|
||||
.do_client_rpc_scoped(FOREIGN_NETWORK_SERVICE_ID, peer, |c| async {
|
||||
let c =
|
||||
ForeignNetworkServiceClient::new(tarpc::client::Config::default(), c)
|
||||
.spawn();
|
||||
let mut rpc_ctx = tarpc::context::current();
|
||||
rpc_ctx.deadline = SystemTime::now() + Duration::from_secs(2);
|
||||
let ret = c.list_network_peers(rpc_ctx, network_identity).await;
|
||||
ret
|
||||
})
|
||||
.await
|
||||
else {
|
||||
return (peer, vec![]);
|
||||
};
|
||||
|
||||
(peer, peers_in_foreign)
|
||||
});
|
||||
}
|
||||
|
||||
let new_next_hop = DashMap::new();
|
||||
while let Some(join_ret) = tasks.join_next().await {
|
||||
let Ok((gateway, peer_ids)) = join_ret else {
|
||||
tracing::error!(?join_ret, "collect next hop in foreign network failed");
|
||||
continue;
|
||||
};
|
||||
for ret in peer_ids {
|
||||
new_next_hop.insert(ret, gateway);
|
||||
}
|
||||
}
|
||||
|
||||
new_next_hop
|
||||
}
|
||||
|
||||
pub fn has_next_hop(&self, peer_id: PeerId) -> bool {
|
||||
self.get_next_hop(peer_id).is_some()
|
||||
}
|
||||
|
||||
pub fn is_peer_public_node(&self, peer_id: &PeerId) -> bool {
|
||||
self.peer_map.has_peer(*peer_id)
|
||||
pub async fn list_public_peers(&self) -> Vec<PeerId> {
|
||||
self.peer_map.list_peers().await
|
||||
}
|
||||
|
||||
pub fn get_next_hop(&self, peer_id: PeerId) -> Option<PeerId> {
|
||||
if self.peer_map.has_peer(peer_id) {
|
||||
return Some(peer_id.clone());
|
||||
}
|
||||
self.next_hop.get(&peer_id).map(|v| v.clone())
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn send_msg(&self, msg: ZCPacket, peer_id: PeerId) -> Result<(), Error> {
|
||||
@@ -162,40 +68,32 @@ impl ForeignNetworkClient {
|
||||
?next_hop,
|
||||
"foreign network client send msg failed"
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
?peer_id,
|
||||
?next_hop,
|
||||
"foreign network client send msg success"
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
Err(Error::RouteError(Some("no next hop".to_string())))
|
||||
}
|
||||
|
||||
pub fn list_foreign_peers(&self) -> Vec<PeerId> {
|
||||
let mut peers = vec![];
|
||||
for item in self.next_hop.iter() {
|
||||
if item.key() != &self.my_peer_id {
|
||||
peers.push(item.key().clone());
|
||||
}
|
||||
}
|
||||
peers
|
||||
}
|
||||
|
||||
pub async fn run(&self) {
|
||||
self.tasks
|
||||
.lock()
|
||||
.await
|
||||
.spawn(Self::collect_next_hop_in_foreign_network_task(
|
||||
self.global_ctx.get_network_identity(),
|
||||
self.peer_map.clone(),
|
||||
self.peer_rpc.clone(),
|
||||
self.next_hop.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn get_next_hop_table(&self) -> DashMap<PeerId, PeerId> {
|
||||
let next_hop = DashMap::new();
|
||||
for item in self.next_hop.iter() {
|
||||
next_hop.insert(item.key().clone(), item.value().clone());
|
||||
}
|
||||
next_hop
|
||||
let peer_map = Arc::downgrade(&self.peer_map);
|
||||
*self.task.lock().unwrap() = Some(
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
let Some(peer_map) = peer_map.upgrade() else {
|
||||
break;
|
||||
};
|
||||
peer_map.clean_peer_without_conn().await;
|
||||
}
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_peer_map(&self) -> Arc<PeerMap> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@ pub mod peer_conn_ping;
|
||||
pub mod peer_manager;
|
||||
pub mod peer_map;
|
||||
pub mod peer_ospf_route;
|
||||
pub mod peer_rip_route;
|
||||
pub mod peer_rpc;
|
||||
pub mod peer_rpc_service;
|
||||
pub mod route_trait;
|
||||
pub mod rpc_service;
|
||||
|
||||
@@ -15,6 +15,8 @@ pub mod foreign_network_manager;
|
||||
|
||||
pub mod encrypt;
|
||||
|
||||
pub mod peer_task;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use super::{
|
||||
peer_conn::{PeerConn, PeerConnId},
|
||||
PacketRecvChan,
|
||||
};
|
||||
use crate::rpc::PeerConnInfo;
|
||||
use crate::proto::cli::PeerConnInfo;
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use futures::{SinkExt, StreamExt, TryFutureExt};
|
||||
use futures::{StreamExt, TryFutureExt};
|
||||
|
||||
use prost::Message;
|
||||
|
||||
@@ -18,23 +18,26 @@ use tokio::{
|
||||
time::{timeout, Duration},
|
||||
};
|
||||
|
||||
use tokio_util::sync::PollSender;
|
||||
use tracing::Instrument;
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
config::{NetworkIdentity, NetworkSecretDigest},
|
||||
defer,
|
||||
error::Error,
|
||||
global_ctx::ArcGlobalCtx,
|
||||
PeerId,
|
||||
},
|
||||
rpc::{HandshakeRequest, PeerConnInfo, PeerConnStats, TunnelInfo},
|
||||
tunnel::packet_def::PacketType,
|
||||
proto::{
|
||||
cli::{PeerConnInfo, PeerConnStats},
|
||||
common::TunnelInfo,
|
||||
peer_rpc::HandshakeRequest,
|
||||
},
|
||||
tunnel::{
|
||||
filter::{StatsRecorderTunnelFilter, TunnelFilter, TunnelWithFilter},
|
||||
mpsc::{MpscTunnel, MpscTunnelSender},
|
||||
packet_def::ZCPacket,
|
||||
packet_def::{PacketType, ZCPacket},
|
||||
stats::{Throughput, WindowLatency},
|
||||
Tunnel, TunnelError, ZCPacketStream,
|
||||
},
|
||||
@@ -61,6 +64,7 @@ pub struct PeerConn {
|
||||
tasks: JoinSet<Result<(), TunnelError>>,
|
||||
|
||||
info: Option<HandshakeRequest>,
|
||||
is_client: Option<bool>,
|
||||
|
||||
close_event_sender: Option<mpsc::Sender<PeerConnId>>,
|
||||
|
||||
@@ -89,7 +93,7 @@ impl PeerConn {
|
||||
let peer_conn_tunnel_filter = StatsRecorderTunnelFilter::new();
|
||||
let throughput = peer_conn_tunnel_filter.filter_output();
|
||||
let peer_conn_tunnel = TunnelWithFilter::new(tunnel, peer_conn_tunnel_filter);
|
||||
let mut mpsc_tunnel = MpscTunnel::new(peer_conn_tunnel);
|
||||
let mut mpsc_tunnel = MpscTunnel::new(peer_conn_tunnel, Some(Duration::from_secs(7)));
|
||||
|
||||
let (recv, sink) = (mpsc_tunnel.get_stream(), mpsc_tunnel.get_sink());
|
||||
|
||||
@@ -99,7 +103,9 @@ impl PeerConn {
|
||||
my_peer_id,
|
||||
global_ctx,
|
||||
|
||||
tunnel: Arc::new(Mutex::new(Box::new(mpsc_tunnel))),
|
||||
tunnel: Arc::new(Mutex::new(Box::new(defer::Defer::new(move || {
|
||||
mpsc_tunnel.close()
|
||||
})))),
|
||||
sink,
|
||||
recv: Arc::new(Mutex::new(Some(recv))),
|
||||
tunnel_info,
|
||||
@@ -107,6 +113,7 @@ impl PeerConn {
|
||||
tasks: JoinSet::new(),
|
||||
|
||||
info: None,
|
||||
is_client: None,
|
||||
close_event_sender: None,
|
||||
|
||||
ctrl_resp_sender: ctrl_sender,
|
||||
@@ -215,8 +222,14 @@ impl PeerConn {
|
||||
let rsp = self.wait_handshake_loop().await?;
|
||||
tracing::info!("handshake request: {:?}", rsp);
|
||||
self.info = Some(rsp);
|
||||
self.is_client = Some(false);
|
||||
self.send_handshake().await?;
|
||||
Ok(())
|
||||
|
||||
if self.get_peer_id() == self.my_peer_id {
|
||||
Err(Error::WaitRespError("peer id conflict".to_owned()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -226,7 +239,13 @@ impl PeerConn {
|
||||
let rsp = self.wait_handshake_loop().await?;
|
||||
tracing::info!("handshake response: {:?}", rsp);
|
||||
self.info = Some(rsp);
|
||||
Ok(())
|
||||
self.is_client = Some(true);
|
||||
|
||||
if self.get_peer_id() == self.my_peer_id {
|
||||
Err(Error::WaitRespError("peer id conflict".to_owned()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handshake_done(&self) -> bool {
|
||||
@@ -236,7 +255,7 @@ impl PeerConn {
|
||||
pub async fn start_recv_loop(&mut self, packet_recv_chan: PacketRecvChan) {
|
||||
let mut stream = self.recv.lock().await.take().unwrap();
|
||||
let sink = self.sink.clone();
|
||||
let mut sender = PollSender::new(packet_recv_chan.clone());
|
||||
let sender = packet_recv_chan.clone();
|
||||
let close_event_sender = self.close_event_sender.clone().unwrap();
|
||||
let conn_id = self.conn_id;
|
||||
let ctrl_sender = self.ctrl_resp_sender.clone();
|
||||
@@ -302,6 +321,7 @@ impl PeerConn {
|
||||
self.ctrl_resp_sender.clone(),
|
||||
self.latency_stats.clone(),
|
||||
self.loss_rate_stats.clone(),
|
||||
self.throughput.clone(),
|
||||
);
|
||||
|
||||
let close_event_sender = self.close_event_sender.clone().unwrap();
|
||||
@@ -359,14 +379,17 @@ impl PeerConn {
|
||||
}
|
||||
|
||||
pub fn get_conn_info(&self) -> PeerConnInfo {
|
||||
let info = self.info.as_ref().unwrap();
|
||||
PeerConnInfo {
|
||||
conn_id: self.conn_id.to_string(),
|
||||
my_peer_id: self.my_peer_id,
|
||||
peer_id: self.get_peer_id(),
|
||||
features: self.info.as_ref().unwrap().features.clone(),
|
||||
features: info.features.clone(),
|
||||
tunnel: self.tunnel_info.clone(),
|
||||
stats: Some(self.get_stats()),
|
||||
loss_rate: (f64::from(self.loss_rate_stats.load(Ordering::Relaxed)) / 100.0) as f32,
|
||||
is_client: self.is_client.unwrap_or_default(),
|
||||
network_name: info.network_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,10 +401,29 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::common::global_ctx::tests::get_mock_global_ctx;
|
||||
use crate::common::new_peer_id;
|
||||
use crate::common::scoped_task::ScopedTask;
|
||||
use crate::tunnel::filter::tests::DropSendTunnelFilter;
|
||||
use crate::tunnel::filter::PacketRecorderTunnelFilter;
|
||||
use crate::tunnel::ring::create_ring_tunnel_pair;
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_conn_handshake_same_id() {
|
||||
let (c, s) = create_ring_tunnel_pair();
|
||||
let c_peer_id = new_peer_id();
|
||||
let s_peer_id = c_peer_id;
|
||||
|
||||
let mut c_peer = PeerConn::new(c_peer_id, get_mock_global_ctx(), Box::new(c));
|
||||
let mut s_peer = PeerConn::new(s_peer_id, get_mock_global_ctx(), Box::new(s));
|
||||
|
||||
let (c_ret, s_ret) = tokio::join!(
|
||||
c_peer.do_handshake_as_client(),
|
||||
s_peer.do_handshake_as_server()
|
||||
);
|
||||
|
||||
assert!(c_ret.is_err());
|
||||
assert!(s_ret.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_conn_handshake() {
|
||||
let (c, s) = create_ring_tunnel_pair();
|
||||
@@ -419,13 +461,25 @@ mod tests {
|
||||
assert_eq!(c_peer.get_network_identity(), NetworkIdentity::default());
|
||||
}
|
||||
|
||||
async fn peer_conn_pingpong_test_common(drop_start: u32, drop_end: u32, conn_closed: bool) {
|
||||
async fn peer_conn_pingpong_test_common(
|
||||
drop_start: u32,
|
||||
drop_end: u32,
|
||||
conn_closed: bool,
|
||||
drop_both: bool,
|
||||
) {
|
||||
let (c, s) = create_ring_tunnel_pair();
|
||||
|
||||
// drop 1-3 packets should not affect pingpong
|
||||
let c_recorder = Arc::new(DropSendTunnelFilter::new(drop_start, drop_end));
|
||||
let c = TunnelWithFilter::new(c, c_recorder.clone());
|
||||
|
||||
let s = if drop_both {
|
||||
let s_recorder = Arc::new(DropSendTunnelFilter::new(drop_start, drop_end));
|
||||
Box::new(TunnelWithFilter::new(s, s_recorder.clone()))
|
||||
} else {
|
||||
s
|
||||
};
|
||||
|
||||
let c_peer_id = new_peer_id();
|
||||
let s_peer_id = new_peer_id();
|
||||
|
||||
@@ -452,7 +506,15 @@ mod tests {
|
||||
.start_recv_loop(tokio::sync::mpsc::channel(200).0)
|
||||
.await;
|
||||
|
||||
// wait 5s, conn should not be disconnected
|
||||
let throughput = c_peer.throughput.clone();
|
||||
let _t = ScopedTask::from(tokio::spawn(async move {
|
||||
// if not drop both, we mock some rx traffic for client peer to test pinger
|
||||
while !drop_both {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
throughput.record_rx_bytes(3);
|
||||
}
|
||||
}));
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(15)).await;
|
||||
|
||||
if conn_closed {
|
||||
@@ -463,9 +525,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_conn_pingpong_timeout() {
|
||||
peer_conn_pingpong_test_common(3, 5, false).await;
|
||||
peer_conn_pingpong_test_common(5, 12, true).await;
|
||||
async fn peer_conn_pingpong_timeout_not_close() {
|
||||
peer_conn_pingpong_test_common(3, 5, false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_conn_pingpong_oneside_timeout() {
|
||||
peer_conn_pingpong_test_common(4, 12, false, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_conn_pingpong_bothside_timeout() {
|
||||
peer_conn_pingpong_test_common(4, 12, true, true).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -6,18 +6,98 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::{sync::broadcast, task::JoinSet, time::timeout};
|
||||
use rand::{thread_rng, Rng};
|
||||
use tokio::{
|
||||
sync::broadcast,
|
||||
task::JoinSet,
|
||||
time::{timeout, Interval},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, PeerId},
|
||||
tunnel::{
|
||||
mpsc::MpscTunnelSender,
|
||||
packet_def::{PacketType, ZCPacket},
|
||||
stats::WindowLatency,
|
||||
stats::{Throughput, WindowLatency},
|
||||
TunnelError,
|
||||
},
|
||||
};
|
||||
|
||||
struct PingIntervalController {
|
||||
throughput: Arc<Throughput>,
|
||||
loss_rate_20: Arc<WindowLatency>,
|
||||
|
||||
interval: Interval,
|
||||
|
||||
logic_time: u64,
|
||||
last_send_logic_time: u64,
|
||||
|
||||
backoff_idx: i32,
|
||||
max_backoff_idx: i32,
|
||||
|
||||
last_throughput: Throughput,
|
||||
}
|
||||
|
||||
impl PingIntervalController {
|
||||
fn new(throughput: Arc<Throughput>, loss_rate_20: Arc<WindowLatency>) -> Self {
|
||||
let last_throughput = *throughput;
|
||||
|
||||
Self {
|
||||
throughput,
|
||||
loss_rate_20,
|
||||
interval: tokio::time::interval(Duration::from_secs(1)),
|
||||
logic_time: 0,
|
||||
last_send_logic_time: 0,
|
||||
|
||||
backoff_idx: 0,
|
||||
max_backoff_idx: 5,
|
||||
|
||||
last_throughput,
|
||||
}
|
||||
}
|
||||
|
||||
async fn tick(&mut self) {
|
||||
self.interval.tick().await;
|
||||
self.logic_time += 1;
|
||||
}
|
||||
|
||||
fn tx_increase(&self) -> bool {
|
||||
self.throughput.tx_packets() > self.last_throughput.tx_packets()
|
||||
}
|
||||
|
||||
fn rx_increase(&self) -> bool {
|
||||
self.throughput.rx_packets() > self.last_throughput.rx_packets()
|
||||
}
|
||||
|
||||
fn should_send_ping(&mut self) -> bool {
|
||||
if self.loss_rate_20.get_latency_us::<f64>() > 0.0 {
|
||||
self.backoff_idx = 0;
|
||||
} else if self.tx_increase()
|
||||
&& !self.rx_increase()
|
||||
&& self.logic_time - self.last_send_logic_time > 2
|
||||
{
|
||||
// if tx increase but rx not increase, we should do pingpong more frequently
|
||||
self.backoff_idx = 0;
|
||||
}
|
||||
|
||||
self.last_throughput = *self.throughput;
|
||||
|
||||
if (self.logic_time - self.last_send_logic_time) < (1 << self.backoff_idx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.backoff_idx = std::cmp::min(self.backoff_idx + 1, self.max_backoff_idx);
|
||||
|
||||
// use this makes two peers not pingpong at the same time
|
||||
if self.backoff_idx > self.max_backoff_idx - 2 && thread_rng().gen_bool(0.2) {
|
||||
self.backoff_idx -= 1;
|
||||
}
|
||||
|
||||
self.last_send_logic_time = self.logic_time;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PeerConnPinger {
|
||||
my_peer_id: PeerId,
|
||||
peer_id: PeerId,
|
||||
@@ -25,6 +105,7 @@ pub struct PeerConnPinger {
|
||||
ctrl_sender: broadcast::Sender<ZCPacket>,
|
||||
latency_stats: Arc<WindowLatency>,
|
||||
loss_rate_stats: Arc<AtomicU32>,
|
||||
throughput_stats: Arc<Throughput>,
|
||||
tasks: JoinSet<Result<(), TunnelError>>,
|
||||
}
|
||||
|
||||
@@ -45,6 +126,7 @@ impl PeerConnPinger {
|
||||
ctrl_sender: broadcast::Sender<ZCPacket>,
|
||||
latency_stats: Arc<WindowLatency>,
|
||||
loss_rate_stats: Arc<AtomicU32>,
|
||||
throughput_stats: Arc<Throughput>,
|
||||
) -> Self {
|
||||
Self {
|
||||
my_peer_id,
|
||||
@@ -54,6 +136,7 @@ impl PeerConnPinger {
|
||||
latency_stats,
|
||||
ctrl_sender,
|
||||
loss_rate_stats,
|
||||
throughput_stats,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,17 +208,23 @@ impl PeerConnPinger {
|
||||
|
||||
let (ping_res_sender, mut ping_res_receiver) = tokio::sync::mpsc::channel(100);
|
||||
|
||||
// one with 1% precision
|
||||
let loss_rate_stats_1 = WindowLatency::new(100);
|
||||
// one with 20% precision, so we can fast fail this conn.
|
||||
let loss_rate_stats_20 = Arc::new(WindowLatency::new(5));
|
||||
|
||||
let stopped = Arc::new(AtomicU32::new(0));
|
||||
|
||||
// generate a pingpong task every 200ms
|
||||
let mut pingpong_tasks = JoinSet::new();
|
||||
let ctrl_resp_sender = self.ctrl_sender.clone();
|
||||
let stopped_clone = stopped.clone();
|
||||
let mut controller =
|
||||
PingIntervalController::new(self.throughput_stats.clone(), loss_rate_stats_20.clone());
|
||||
self.tasks.spawn(async move {
|
||||
let mut req_seq = 0;
|
||||
loop {
|
||||
let receiver = ctrl_resp_sender.subscribe();
|
||||
let ping_res_sender = ping_res_sender.clone();
|
||||
controller.tick().await;
|
||||
|
||||
if stopped_clone.load(Ordering::Relaxed) != 0 {
|
||||
return Ok(());
|
||||
@@ -145,7 +234,13 @@ impl PeerConnPinger {
|
||||
pingpong_tasks.join_next().await;
|
||||
}
|
||||
|
||||
if !controller.should_send_ping() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut sink = sink.clone();
|
||||
let receiver = ctrl_resp_sender.subscribe();
|
||||
let ping_res_sender = ping_res_sender.clone();
|
||||
pingpong_tasks.spawn(async move {
|
||||
let mut receiver = receiver.resubscribe();
|
||||
let pingpong_once_ret = Self::do_pingpong_once(
|
||||
@@ -163,16 +258,12 @@ impl PeerConnPinger {
|
||||
});
|
||||
|
||||
req_seq = req_seq.wrapping_add(1);
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// one with 1% precision
|
||||
let loss_rate_stats_1 = WindowLatency::new(100);
|
||||
// one with 20% precision, so we can fast fail this conn.
|
||||
let loss_rate_stats_20 = WindowLatency::new(5);
|
||||
|
||||
let mut counter: u64 = 0;
|
||||
let throughput = self.throughput_stats.clone();
|
||||
let mut last_rx_packets = throughput.rx_packets();
|
||||
|
||||
while let Some(ret) = ping_res_receiver.recv().await {
|
||||
counter += 1;
|
||||
@@ -199,16 +290,31 @@ impl PeerConnPinger {
|
||||
);
|
||||
|
||||
if (counter > 5 && loss_rate_20 > 0.74) || (counter > 150 && loss_rate_1 > 0.20) {
|
||||
tracing::warn!(
|
||||
?ret,
|
||||
?self,
|
||||
?loss_rate_1,
|
||||
?loss_rate_20,
|
||||
"pingpong loss rate too high, closing"
|
||||
);
|
||||
break;
|
||||
let current_rx_packets = throughput.rx_packets();
|
||||
let need_close = if last_rx_packets != current_rx_packets {
|
||||
// if we receive some packet from peers, we should relax the condition
|
||||
counter > 50 && loss_rate_1 > 0.5
|
||||
|
||||
// TODO: wait more time to see if the loss rate is still high after no rx
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if need_close {
|
||||
tracing::warn!(
|
||||
?ret,
|
||||
?self,
|
||||
?loss_rate_1,
|
||||
?loss_rate_20,
|
||||
?last_rx_packets,
|
||||
?current_rx_packets,
|
||||
"pingpong loss rate too high, closing"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
last_rx_packets = throughput.rx_packets();
|
||||
self.loss_rate_stats
|
||||
.store((loss_rate_1 * 100.0) as u32, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ use std::{
|
||||
fmt::Debug,
|
||||
net::Ipv4Addr,
|
||||
sync::{Arc, Weak},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use futures::StreamExt;
|
||||
use dashmap::DashMap;
|
||||
|
||||
use tokio::{
|
||||
sync::{
|
||||
@@ -16,17 +17,28 @@ use tokio::{
|
||||
},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::bytes::Bytes;
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
|
||||
common::{
|
||||
constants::EASYTIER_VERSION,
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, NetworkIdentity},
|
||||
stun::StunInfoCollectorTrait,
|
||||
PeerId,
|
||||
},
|
||||
peers::{
|
||||
peer_conn::PeerConn,
|
||||
peer_rpc::PeerRpcManagerTransport,
|
||||
route_trait::{NextHopPolicy, RouteInterface},
|
||||
route_trait::{ForeignNetworkRouteInfoMap, NextHopPolicy, RouteInterface},
|
||||
PeerPacketFilter,
|
||||
},
|
||||
proto::{
|
||||
cli::{
|
||||
self, list_global_foreign_network_response::OneForeignNetwork,
|
||||
ListGlobalForeignNetworkResponse,
|
||||
},
|
||||
peer_rpc::{ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey},
|
||||
},
|
||||
tunnel::{
|
||||
self,
|
||||
packet_def::{PacketType, ZCPacket},
|
||||
@@ -37,11 +49,10 @@ use crate::{
|
||||
use super::{
|
||||
encrypt::{Encryptor, NullCipher},
|
||||
foreign_network_client::ForeignNetworkClient,
|
||||
foreign_network_manager::ForeignNetworkManager,
|
||||
foreign_network_manager::{ForeignNetworkManager, GlobalForeignNetworkAccessor},
|
||||
peer_conn::PeerConnId,
|
||||
peer_map::PeerMap,
|
||||
peer_ospf_route::PeerRoute,
|
||||
peer_rip_route::BasicRoute,
|
||||
peer_rpc::PeerRpcManager,
|
||||
route_trait::{ArcRoute, Route},
|
||||
BoxNicPacketFilter, BoxPeerPacketFilter, PacketRecvChanReceiver,
|
||||
@@ -75,7 +86,15 @@ impl PeerRpcManagerTransport for RpcTransport {
|
||||
.ok_or(Error::Unknown)?;
|
||||
let peers = self.peers.upgrade().ok_or(Error::Unknown)?;
|
||||
|
||||
if let Some(gateway_id) = peers
|
||||
if foreign_peers.has_next_hop(dst_peer_id) {
|
||||
// do not encrypt for data sending to public server
|
||||
tracing::debug!(
|
||||
?dst_peer_id,
|
||||
?self.my_peer_id,
|
||||
"failed to send msg to peer, try foreign network",
|
||||
);
|
||||
foreign_peers.send_msg(msg, dst_peer_id).await
|
||||
} else if let Some(gateway_id) = peers
|
||||
.get_gateway_peer_id(dst_peer_id, NextHopPolicy::LeastHop)
|
||||
.await
|
||||
{
|
||||
@@ -88,20 +107,11 @@ impl PeerRpcManagerTransport for RpcTransport {
|
||||
self.encryptor
|
||||
.encrypt(&mut msg)
|
||||
.with_context(|| "encrypt failed")?;
|
||||
peers.send_msg_directly(msg, gateway_id).await
|
||||
} else if foreign_peers.has_next_hop(dst_peer_id) {
|
||||
if !foreign_peers.is_peer_public_node(&dst_peer_id) {
|
||||
// do not encrypt for msg sending to public node
|
||||
self.encryptor
|
||||
.encrypt(&mut msg)
|
||||
.with_context(|| "encrypt failed")?;
|
||||
if peers.has_peer(gateway_id) {
|
||||
peers.send_msg_directly(msg, gateway_id).await
|
||||
} else {
|
||||
foreign_peers.send_msg(msg, gateway_id).await
|
||||
}
|
||||
tracing::debug!(
|
||||
?dst_peer_id,
|
||||
?self.my_peer_id,
|
||||
"failed to send msg to peer, try foreign network",
|
||||
);
|
||||
foreign_peers.send_msg(msg, dst_peer_id).await
|
||||
} else {
|
||||
Err(Error::RouteError(Some(format!(
|
||||
"peermgr RpcTransport no route for dst_peer_id: {}",
|
||||
@@ -120,13 +130,11 @@ impl PeerRpcManagerTransport for RpcTransport {
|
||||
}
|
||||
|
||||
pub enum RouteAlgoType {
|
||||
Rip,
|
||||
Ospf,
|
||||
None,
|
||||
}
|
||||
|
||||
enum RouteAlgoInst {
|
||||
Rip(Arc<BasicRoute>),
|
||||
Ospf(Arc<PeerRoute>),
|
||||
None,
|
||||
}
|
||||
@@ -177,7 +185,7 @@ impl PeerManager {
|
||||
) -> Self {
|
||||
let my_peer_id = rand::random();
|
||||
|
||||
let (packet_send, packet_recv) = mpsc::channel(100);
|
||||
let (packet_send, packet_recv) = mpsc::channel(128);
|
||||
let peers = Arc::new(PeerMap::new(
|
||||
packet_send.clone(),
|
||||
global_ctx.clone(),
|
||||
@@ -217,9 +225,6 @@ impl PeerManager {
|
||||
let peer_rpc_mgr = Arc::new(PeerRpcManager::new(rpc_tspt.clone()));
|
||||
|
||||
let route_algo_inst = match route_algo {
|
||||
RouteAlgoType::Rip => {
|
||||
RouteAlgoInst::Rip(Arc::new(BasicRoute::new(my_peer_id, global_ctx.clone())))
|
||||
}
|
||||
RouteAlgoType::Ospf => RouteAlgoInst::Ospf(PeerRoute::new(
|
||||
my_peer_id,
|
||||
global_ctx.clone(),
|
||||
@@ -232,6 +237,7 @@ impl PeerManager {
|
||||
my_peer_id,
|
||||
global_ctx.clone(),
|
||||
packet_send.clone(),
|
||||
Self::build_foreign_network_manager_accessor(&peers),
|
||||
));
|
||||
let foreign_network_client = Arc::new(ForeignNetworkClient::new(
|
||||
global_ctx.clone(),
|
||||
@@ -270,6 +276,34 @@ impl PeerManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_foreign_network_manager_accessor(
|
||||
peer_map: &Arc<PeerMap>,
|
||||
) -> Box<dyn GlobalForeignNetworkAccessor> {
|
||||
struct T {
|
||||
peer_map: Weak<PeerMap>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GlobalForeignNetworkAccessor for T {
|
||||
async fn list_global_foreign_peer(
|
||||
&self,
|
||||
network_identity: &NetworkIdentity,
|
||||
) -> Vec<PeerId> {
|
||||
let Some(peer_map) = self.peer_map.upgrade() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
peer_map
|
||||
.list_peers_own_foreign_network(network_identity)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
Box::new(T {
|
||||
peer_map: Arc::downgrade(peer_map),
|
||||
})
|
||||
}
|
||||
|
||||
async fn add_new_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
|
||||
if self.global_ctx.get_network_identity() != peer_conn.get_network_identity() {
|
||||
return Err(Error::SecretKeyError(
|
||||
@@ -309,21 +343,6 @@ impl PeerManager {
|
||||
self.add_client_tunnel(t).await
|
||||
}
|
||||
|
||||
fn check_network_in_whitelist(&self, network_name: &str) -> Result<(), Error> {
|
||||
if self
|
||||
.global_ctx
|
||||
.get_flags()
|
||||
.foreign_network_whitelist
|
||||
.split(" ")
|
||||
.map(wildmatch::WildMatch::new)
|
||||
.any(|wl| wl.matches(network_name))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("network {} not in whitelist", network_name).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn add_tunnel_as_server(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
|
||||
tracing::info!("add tunnel as server start");
|
||||
@@ -334,27 +353,91 @@ impl PeerManager {
|
||||
{
|
||||
self.add_new_peer_conn(peer).await?;
|
||||
} else {
|
||||
self.check_network_in_whitelist(&peer.get_network_identity().network_name)?;
|
||||
self.foreign_network_manager.add_peer_conn(peer).await?;
|
||||
}
|
||||
tracing::info!("add tunnel as server done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_handle_foreign_network_packet(
|
||||
packet: ZCPacket,
|
||||
my_peer_id: PeerId,
|
||||
peer_map: &PeerMap,
|
||||
foreign_network_mgr: &ForeignNetworkManager,
|
||||
) -> Result<(), ZCPacket> {
|
||||
let pm_header = packet.peer_manager_header().unwrap();
|
||||
if pm_header.packet_type != PacketType::ForeignNetworkPacket as u8 {
|
||||
return Err(packet);
|
||||
}
|
||||
|
||||
let from_peer_id = pm_header.from_peer_id.get();
|
||||
let to_peer_id = pm_header.to_peer_id.get();
|
||||
|
||||
let foreign_hdr = packet.foreign_network_hdr().unwrap();
|
||||
let foreign_network_name = foreign_hdr.get_network_name(packet.payload());
|
||||
let foreign_peer_id = foreign_hdr.get_dst_peer_id();
|
||||
|
||||
if to_peer_id == my_peer_id {
|
||||
// packet sent from other peer to me, extract the inner packet and forward it
|
||||
if let Err(e) = foreign_network_mgr
|
||||
.send_msg_to_peer(
|
||||
&foreign_network_name,
|
||||
foreign_peer_id,
|
||||
packet.foreign_network_packet(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
?e,
|
||||
?foreign_network_name,
|
||||
?foreign_peer_id,
|
||||
"foreign network mgr send_msg_to_peer failed"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
} else if from_peer_id == my_peer_id {
|
||||
// packet is generated from foreign network mgr and should be forward to other peer
|
||||
if let Err(e) = peer_map
|
||||
.send_msg(packet, to_peer_id, NextHopPolicy::LeastHop)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
?e,
|
||||
?to_peer_id,
|
||||
"send_msg_directly failed when forward local generated foreign network packet"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
// target is not me, forward it
|
||||
Err(packet)
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_peer_recv(&self) {
|
||||
let mut recv = ReceiverStream::new(self.packet_recv.lock().await.take().unwrap());
|
||||
let mut recv = self.packet_recv.lock().await.take().unwrap();
|
||||
let my_peer_id = self.my_peer_id;
|
||||
let peers = self.peers.clone();
|
||||
let pipe_line = self.peer_packet_process_pipeline.clone();
|
||||
let foreign_client = self.foreign_network_client.clone();
|
||||
let foreign_mgr = self.foreign_network_manager.clone();
|
||||
let encryptor = self.encryptor.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
tracing::trace!("start_peer_recv");
|
||||
while let Some(mut ret) = recv.next().await {
|
||||
while let Some(ret) = recv.recv().await {
|
||||
let Err(mut ret) =
|
||||
Self::try_handle_foreign_network_packet(ret, my_peer_id, &peers, &foreign_mgr)
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(hdr) = ret.mut_peer_manager_header() else {
|
||||
tracing::warn!(?ret, "invalid packet, skip");
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::trace!(?hdr, "peer recv a packet...");
|
||||
let from_peer_id = hdr.from_peer_id.get();
|
||||
let to_peer_id = hdr.to_peer_id.get();
|
||||
@@ -454,7 +537,10 @@ impl PeerManager {
|
||||
impl PeerPacketFilter for PeerRpcPacketProcessor {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
let hdr = packet.peer_manager_header().unwrap();
|
||||
if hdr.packet_type == PacketType::TaRpc as u8 {
|
||||
if hdr.packet_type == PacketType::TaRpc as u8
|
||||
|| hdr.packet_type == PacketType::RpcReq as u8
|
||||
|| hdr.packet_type == PacketType::RpcResp as u8
|
||||
{
|
||||
self.peer_rpc_tspt_sender.send(packet).unwrap();
|
||||
None
|
||||
} else {
|
||||
@@ -480,6 +566,7 @@ impl PeerManager {
|
||||
my_peer_id: PeerId,
|
||||
peers: Weak<PeerMap>,
|
||||
foreign_network_client: Weak<ForeignNetworkClient>,
|
||||
foreign_network_manager: Weak<ForeignNetworkManager>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -493,36 +580,45 @@ impl PeerManager {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut peers = foreign_client.list_foreign_peers();
|
||||
let mut peers = foreign_client.list_public_peers().await;
|
||||
peers.extend(peer_map.list_peers_with_conn().await);
|
||||
peers
|
||||
}
|
||||
async fn send_route_packet(
|
||||
&self,
|
||||
msg: Bytes,
|
||||
_route_id: u8,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Result<(), Error> {
|
||||
let foreign_client = self
|
||||
.foreign_network_client
|
||||
.upgrade()
|
||||
.ok_or(Error::Unknown)?;
|
||||
let peer_map = self.peers.upgrade().ok_or(Error::Unknown)?;
|
||||
let mut zc_packet = ZCPacket::new_with_payload(&msg);
|
||||
zc_packet.fill_peer_manager_hdr(
|
||||
self.my_peer_id,
|
||||
dst_peer_id,
|
||||
PacketType::Route as u8,
|
||||
);
|
||||
if foreign_client.has_next_hop(dst_peer_id) {
|
||||
foreign_client.send_msg(zc_packet, dst_peer_id).await
|
||||
} else {
|
||||
peer_map.send_msg_directly(zc_packet, dst_peer_id).await
|
||||
}
|
||||
}
|
||||
|
||||
fn my_peer_id(&self) -> PeerId {
|
||||
self.my_peer_id
|
||||
}
|
||||
|
||||
async fn list_foreign_networks(&self) -> ForeignNetworkRouteInfoMap {
|
||||
let ret = DashMap::new();
|
||||
let Some(foreign_mgr) = self.foreign_network_manager.upgrade() else {
|
||||
return ret;
|
||||
};
|
||||
|
||||
let networks = foreign_mgr.list_foreign_networks().await;
|
||||
for (network_name, info) in networks.foreign_networks.iter() {
|
||||
if info.peers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let last_update = foreign_mgr
|
||||
.get_foreign_network_last_update(network_name)
|
||||
.unwrap_or(SystemTime::now());
|
||||
ret.insert(
|
||||
ForeignNetworkRouteInfoKey {
|
||||
peer_id: self.my_peer_id,
|
||||
network_name: network_name.clone(),
|
||||
},
|
||||
ForeignNetworkRouteInfoEntry {
|
||||
foreign_peer_ids: info.peers.iter().map(|x| x.peer_id).collect(),
|
||||
last_update: Some(last_update.into()),
|
||||
version: 0,
|
||||
network_secret_digest: info.network_secret_digest.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
let my_peer_id = self.my_peer_id;
|
||||
@@ -531,6 +627,7 @@ impl PeerManager {
|
||||
my_peer_id,
|
||||
peers: Arc::downgrade(&self.peers),
|
||||
foreign_network_client: Arc::downgrade(&self.foreign_network_client),
|
||||
foreign_network_manager: Arc::downgrade(&self.foreign_network_manager),
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -541,13 +638,12 @@ impl PeerManager {
|
||||
|
||||
pub fn get_route(&self) -> Box<dyn Route + Send + Sync + 'static> {
|
||||
match &self.route_algo_inst {
|
||||
RouteAlgoInst::Rip(route) => Box::new(route.clone()),
|
||||
RouteAlgoInst::Ospf(route) => Box::new(route.clone()),
|
||||
RouteAlgoInst::None => panic!("no route"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_routes(&self) -> Vec<crate::rpc::Route> {
|
||||
pub async fn list_routes(&self) -> Vec<cli::Route> {
|
||||
self.get_route().list_routes().await
|
||||
}
|
||||
|
||||
@@ -555,6 +651,28 @@ impl PeerManager {
|
||||
self.get_route().dump().await
|
||||
}
|
||||
|
||||
pub async fn list_global_foreign_network(&self) -> ListGlobalForeignNetworkResponse {
|
||||
let mut resp = ListGlobalForeignNetworkResponse::default();
|
||||
let ret = self.get_route().list_foreign_network_info().await;
|
||||
for info in ret.infos.iter() {
|
||||
let entry = resp
|
||||
.foreign_networks
|
||||
.entry(info.key.as_ref().unwrap().peer_id)
|
||||
.or_insert_with(|| Default::default());
|
||||
|
||||
let mut f = OneForeignNetwork::default();
|
||||
f.network_name = info.key.as_ref().unwrap().network_name.clone();
|
||||
f.peer_ids
|
||||
.extend(info.value.as_ref().unwrap().foreign_peer_ids.iter());
|
||||
f.last_updated = format!("{}", info.value.as_ref().unwrap().last_update.unwrap());
|
||||
f.version = info.value.as_ref().unwrap().version;
|
||||
|
||||
entry.foreign_networks.push(f);
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) {
|
||||
for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() {
|
||||
pipeline.try_process_packet_from_nic(data).await;
|
||||
@@ -600,8 +718,16 @@ impl PeerManager {
|
||||
|
||||
let mut is_exit_node = false;
|
||||
let mut dst_peers = vec![];
|
||||
// NOTE: currently we only support ipv4 and cidr is 24
|
||||
if ipv4_addr.is_broadcast() || ipv4_addr.is_multicast() || ipv4_addr.octets()[3] == 255 {
|
||||
let network_length = self
|
||||
.global_ctx
|
||||
.get_ipv4()
|
||||
.map(|x| x.network_length())
|
||||
.unwrap_or(24);
|
||||
let ipv4_inet = cidr::Ipv4Inet::new(ipv4_addr, network_length).unwrap();
|
||||
if ipv4_addr.is_broadcast()
|
||||
|| ipv4_addr.is_multicast()
|
||||
|| ipv4_addr == ipv4_inet.last_address()
|
||||
{
|
||||
dst_peers.extend(
|
||||
self.peers
|
||||
.list_routes()
|
||||
@@ -665,13 +791,23 @@ impl PeerManager {
|
||||
.get_gateway_peer_id(*peer_id, next_hop_policy.clone())
|
||||
.await
|
||||
{
|
||||
if let Err(e) = self.peers.send_msg_directly(msg, gateway).await {
|
||||
errs.push(e);
|
||||
}
|
||||
} else if self.foreign_network_client.has_next_hop(*peer_id) {
|
||||
if let Err(e) = self.foreign_network_client.send_msg(msg, *peer_id).await {
|
||||
errs.push(e);
|
||||
if self.peers.has_peer(gateway) {
|
||||
if let Err(e) = self.peers.send_msg_directly(msg, gateway).await {
|
||||
errs.push(e);
|
||||
}
|
||||
} else if self.foreign_network_client.has_next_hop(gateway) {
|
||||
if let Err(e) = self.foreign_network_client.send_msg(msg, gateway).await {
|
||||
errs.push(e);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
?gateway,
|
||||
?peer_id,
|
||||
"cannot send msg to peer through gateway"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(?peer_id, "no gateway for peer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,14 +838,12 @@ impl PeerManager {
|
||||
.await
|
||||
.replace(Arc::downgrade(&self.foreign_network_client));
|
||||
|
||||
self.foreign_network_manager.run().await;
|
||||
self.foreign_network_client.run().await;
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> Result<(), Error> {
|
||||
match &self.route_algo_inst {
|
||||
RouteAlgoInst::Ospf(route) => self.add_route(route.clone()).await,
|
||||
RouteAlgoInst::Rip(route) => self.add_route(route.clone()).await,
|
||||
RouteAlgoInst::None => {}
|
||||
};
|
||||
|
||||
@@ -748,13 +882,6 @@ impl PeerManager {
|
||||
self.nic_channel.clone()
|
||||
}
|
||||
|
||||
pub fn get_basic_route(&self) -> Arc<BasicRoute> {
|
||||
match &self.route_algo_inst {
|
||||
RouteAlgoInst::Rip(route) => route.clone(),
|
||||
_ => panic!("not rip route"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_foreign_network_manager(&self) -> Arc<ForeignNetworkManager> {
|
||||
self.foreign_network_manager.clone()
|
||||
}
|
||||
@@ -762,6 +889,41 @@ impl PeerManager {
|
||||
pub fn get_foreign_network_client(&self) -> Arc<ForeignNetworkClient> {
|
||||
self.foreign_network_client.clone()
|
||||
}
|
||||
|
||||
pub fn get_my_info(&self) -> cli::NodeInfo {
|
||||
cli::NodeInfo {
|
||||
peer_id: self.my_peer_id,
|
||||
ipv4_addr: self
|
||||
.global_ctx
|
||||
.get_ipv4()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
proxy_cidrs: self
|
||||
.global_ctx
|
||||
.get_proxy_cidrs()
|
||||
.into_iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
hostname: self.global_ctx.get_hostname(),
|
||||
stun_info: Some(self.global_ctx.get_stun_info_collector().get_stun_info()),
|
||||
inst_id: self.global_ctx.get_id().to_string(),
|
||||
listeners: self
|
||||
.global_ctx
|
||||
.get_running_listeners()
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
config: self.global_ctx.config.dump(),
|
||||
version: EASYTIER_VERSION.to_string(),
|
||||
feature_flag: Some(self.global_ctx.get_feature_flags()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait(&self) {
|
||||
while !self.tasks.lock().await.is_empty() {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -777,12 +939,11 @@ mod tests {
|
||||
instance::listeners::get_listener_by_url,
|
||||
peers::{
|
||||
peer_manager::RouteAlgoType,
|
||||
peer_rpc::tests::{MockService, TestRpcService, TestRpcServiceClient},
|
||||
peer_rpc::tests::register_service,
|
||||
tests::{connect_peer_manager, wait_route_appear},
|
||||
},
|
||||
rpc::NatType,
|
||||
tunnel::common::tests::wait_for_condition,
|
||||
tunnel::{TunnelConnector, TunnelListener},
|
||||
proto::common::NatType,
|
||||
tunnel::{common::tests::wait_for_condition, TunnelConnector, TunnelListener},
|
||||
};
|
||||
|
||||
use super::PeerManager;
|
||||
@@ -845,25 +1006,18 @@ mod tests {
|
||||
#[values("tcp", "udp", "wg", "quic")] proto1: &str,
|
||||
#[values("tcp", "udp", "wg", "quic")] proto2: &str,
|
||||
) {
|
||||
use crate::proto::{
|
||||
rpc_impl::RpcController,
|
||||
tests::{GreetingClientFactory, SayHelloRequest},
|
||||
};
|
||||
|
||||
let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
peer_mgr_a.get_peer_rpc_mgr().run_service(
|
||||
100,
|
||||
MockService {
|
||||
prefix: "hello a".to_owned(),
|
||||
}
|
||||
.serve(),
|
||||
);
|
||||
register_service(&peer_mgr_a.peer_rpc_mgr, "", 0, "hello a");
|
||||
|
||||
let peer_mgr_b = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
|
||||
let peer_mgr_c = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
|
||||
peer_mgr_c.get_peer_rpc_mgr().run_service(
|
||||
100,
|
||||
MockService {
|
||||
prefix: "hello c".to_owned(),
|
||||
}
|
||||
.serve(),
|
||||
);
|
||||
register_service(&peer_mgr_c.peer_rpc_mgr, "", 0, "hello c");
|
||||
|
||||
let mut listener1 = get_listener_by_url(
|
||||
&format!("{}://0.0.0.0:31013", proto1).parse().unwrap(),
|
||||
@@ -901,16 +1055,26 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let ret = peer_mgr_a
|
||||
.get_peer_rpc_mgr()
|
||||
.do_client_rpc_scoped(100, peer_mgr_c.my_peer_id(), |c| async {
|
||||
let c = TestRpcServiceClient::new(tarpc::client::Config::default(), c).spawn();
|
||||
let ret = c.hello(tarpc::context::current(), "abc".to_owned()).await;
|
||||
ret
|
||||
})
|
||||
let stub = peer_mgr_a
|
||||
.peer_rpc_mgr
|
||||
.rpc_client()
|
||||
.scoped_client::<GreetingClientFactory<RpcController>>(
|
||||
peer_mgr_a.my_peer_id,
|
||||
peer_mgr_c.my_peer_id,
|
||||
"".to_string(),
|
||||
);
|
||||
|
||||
let ret = stub
|
||||
.say_hello(
|
||||
RpcController::default(),
|
||||
SayHelloRequest {
|
||||
name: "abc".to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(ret, "hello c abc");
|
||||
|
||||
assert_eq!(ret.greeting, "hello c abc!");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,12 +7,11 @@ use tokio::sync::RwLock;
|
||||
use crate::{
|
||||
common::{
|
||||
error::Error,
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
|
||||
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
|
||||
PeerId,
|
||||
},
|
||||
rpc::PeerConnInfo,
|
||||
tunnel::packet_def::ZCPacket,
|
||||
tunnel::TunnelError,
|
||||
proto::cli::PeerConnInfo,
|
||||
tunnel::{packet_def::ZCPacket, TunnelError},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -66,7 +65,7 @@ impl PeerMap {
|
||||
}
|
||||
|
||||
pub fn has_peer(&self, peer_id: PeerId) -> bool {
|
||||
self.peer_map.contains_key(&peer_id)
|
||||
peer_id == self.my_peer_id || self.peer_map.contains_key(&peer_id)
|
||||
}
|
||||
|
||||
pub async fn send_msg_directly(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
|
||||
@@ -113,16 +112,28 @@ impl PeerMap {
|
||||
.get_next_hop_with_policy(dst_peer_id, policy.clone())
|
||||
.await
|
||||
{
|
||||
// for foreign network, gateway_peer_id may not connect to me
|
||||
if self.has_peer(gateway_peer_id) {
|
||||
return Some(gateway_peer_id);
|
||||
}
|
||||
// NOTIC: for foreign network, gateway_peer_id may not connect to me
|
||||
return Some(gateway_peer_id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn list_peers_own_foreign_network(
|
||||
&self,
|
||||
network_identity: &NetworkIdentity,
|
||||
) -> Vec<PeerId> {
|
||||
let mut ret = Vec::new();
|
||||
for route in self.routes.read().await.iter() {
|
||||
let peers = route
|
||||
.list_peers_own_foreign_network(&network_identity)
|
||||
.await;
|
||||
ret.extend(peers);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn send_msg(
|
||||
&self,
|
||||
msg: ZCPacket,
|
||||
@@ -240,3 +251,13 @@ impl PeerMap {
|
||||
route_map
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PeerMap {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!(
|
||||
self.my_peer_id,
|
||||
network = ?self.global_ctx.get_network_identity(),
|
||||
"PeerMap is dropped"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user