Compare commits

..

26 Commits

Author SHA1 Message Date
Sijie.Sun
d8446778cc update readme (#65)
add picture of gui
2024-04-29 21:50:58 +08:00
Sijie.Sun
70dee329d1 fix crash bugs (#64) 2024-04-29 21:02:05 +08:00
Sijie.Sun
6595c2837e fix win multi network (#63) 2024-04-28 23:21:58 +08:00
sijie.sun
577cef131b fix wireguard deadlock 2024-04-28 22:24:24 +08:00
sijie.sun
b3717d974b ipv6 set v6 only when bind 2024-04-28 22:24:24 +08:00
sijie.sun
d8033a77b9 support use ipv6 2024-04-28 22:24:24 +08:00
sijie.sun
3a965efab2 allow tunnel listener alloc port after listen 2024-04-28 22:24:24 +08:00
sijie.sun
a3e85a1270 tunnel support ipv6 2024-04-28 22:24:24 +08:00
Sijie.Sun
66b3241be7 fix handshake dead lock, clean old code (#61)
* fix handshake dead lock
* remove old code
2024-04-27 16:27:42 +08:00
Sijie.Sun
fcc73159b3 support encryption (#60) 2024-04-27 13:44:59 +08:00
Sijie.Sun
69651ae3fd Perf improve (#59)
* improve perf

* fix forward
2024-04-26 23:02:07 +08:00
Sijie.Sun
096af6aa45 fix tun device on mac (#58) 2024-04-26 21:19:47 +08:00
Sijie.Sun
57c9f11371 adapt tun device to zerocopy (#57) 2024-04-25 23:25:37 +08:00
Sijie.Sun
3467890270 zero copy tunnel (#55)
make tunnel zero copy, for better performance. remove most of the locks in io path.
introduce quic tunnel
prepare for encryption
2024-04-24 23:12:46 +08:00
Sijie.Sun
39021d7b1b fix gui minor-bugs (#54)
1. cannot persist locale setting.
2. set forcus after show from tray icon
2024-04-21 10:00:01 +08:00
Sijie.Sun
0ddcda1b31 introduce gui based on tauri (#52) 2024-04-14 23:29:34 +08:00
Sijie.Sun
50e14798d6 fix ring tunnel cannot close (#51) 2024-04-07 11:35:22 +08:00
Sijie.Sun
727ef37ae4 add client gui for easytier (#50) 2024-04-06 22:44:30 +08:00
Sijie.Sun
4eb7efe5fc use workspace, prepare for config server and gui (#48) 2024-04-04 10:33:53 +08:00
Sijie.Sun
bb4ae71869 bump easytier version to 0.1.2 (#45) 2024-04-03 23:14:23 +08:00
Sijie.Sun
892b06dfd3 some wg & cli & README improve (#47)
1. fix vpn client cannot access local node
2. fix wg client config no allowedip field
3. some cli & README improve
2024-04-03 22:22:44 +08:00
Sijie.Sun
e4be86cf92 allow specify bind dev for tunnels. also fix bugs #46)
1. fix wireguard / udp tunnel stack overflow on win.
2. custom panic handler to save panic stack.
3. fix iface filter on windows and linux.
4. add scheme black list to direct connector
2024-04-03 21:46:52 +08:00
Sijie.Sun
25a7603990 Add WireGuard Client to Readme (#44)
* Add README for Wireguard Client

* add default protocol flag

* wireguard connector support bind device
2024-03-31 21:10:59 +08:00
Sijie.Sun
05cabb2651 Support wireguard vpn portal (#43)
* support wireguard vpn portal
  user can use wireguard client to access easytier network

* add vpn portal cli

* clean logs

* avoid ospf msg too large
2024-03-30 22:15:14 +08:00
Sijie.Sun
90110aa587 add wireguard tunnel (#42)
peers can connect with each other using wireguard protocol.
2024-03-28 10:01:25 +08:00
Sijie.Sun
ce889e990e some minor bug fixs (#41)
* fix joinset leak; 

* fix udp packet format

* fix trace log panic

* avoid waiting after listener accept
2024-03-24 22:21:47 +08:00
132 changed files with 19535 additions and 4566 deletions

View File

@@ -22,19 +22,25 @@ jobs:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest
GUI_TARGET: aarch64-unknown-linux-gnu
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest
GUI_TARGET: x86_64-unknown-linux-gnu
- TARGET: x86_64-apple-darwin
OS: macos-latest
GUI_TARGET: x86_64-apple-darwin
- TARGET: aarch64-apple-darwin
OS: macos-latest
GUI_TARGET: aarch64-apple-darwin
- TARGET: x86_64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: x86_64-pc-windows-msvc
runs-on: ${{ matrix.OS }}
env:
NAME: easytier
TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }}
GUI_TARGET: ${{ matrix.GUI_TARGET }}
steps:
- uses: actions/checkout@v3
- name: Setup protoc
@@ -42,19 +48,36 @@ jobs:
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'yarn'
cache-dependency-path: easytier-gui/yarn.lock
- name: Install Yarn
run: npm install -g yarn
- name: Cargo cache
uses: actions/cache@v4.0.0
with:
path: |
~/.cargo
./target
key: build-cargo-registry-${{matrix.TARGET}}
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: |
# dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools
# for easytier-gui
sudo apt install libwebkit2gtk-4.0-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
# curl -s musl.cc | grep mipsel
case $TARGET in
mipsel-unknown-linux-musl)
@@ -87,16 +110,60 @@ jobs:
rustup install 1.75
rustup default 1.75
rustup target add $TARGET
rustup target add $GUI_TARGET
- name: Run build
run: cargo build --release --verbose --target $TARGET
- name: Install for aarch64 gui cross compile
run: |
# see https://tauri.app/v1/guides/building/linux/
if [[ $TARGET == "aarch64-unknown-linux-musl" ]]; then
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt-get update && sudo apt-get upgrade -y
sudo apt install libwebkit2gtk-4.0-dev:arm64
sudo apt install libssl-dev:arm64
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
fi
- name: Run build GUI
run: |
cd easytier-gui
yarn install
if [[ $OS =~ ^ubuntu.*$ && ! $GUI_TARGET =~ ^x86_64.*$ ]]; then
# only build deb for non-x86_64 linux
yarn tauri build -- --target $GUI_TARGET --verbose --bundles deb
else
yarn tauri build -- --target $GUI_TARGET --verbose
fi
- name: Compress
run: |
mkdir -p ./artifacts/objects/
# windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
cp third_party/Packet.dll ./artifacts/objects/
cp third_party/wintun.dll ./artifacts/objects/
cp easytier/third_party/Packet.dll ./artifacts/objects/
cp easytier/third_party/wintun.dll ./artifacts/objects/
fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME
@@ -105,6 +172,20 @@ jobs:
fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
# copy gui bundle, gui is built without specific target
if [[ $OS =~ ^windows.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/nsis/*.exe ./artifacts/objects/
elif [[ $OS =~ ^macos.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
elif [[ $OS =~ ^ubuntu.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
# currently only x86 appimage is supported
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
fi
fi
tar -cvf ./artifacts/$NAME-$TARGET-$TAG.tar -C ./artifacts/objects/ .
rm -rf ./artifacts/objects/
- name: Archive artifact
@@ -124,7 +205,6 @@ jobs:
remote-path: /easytier-releases/${{ github.sha }}/
no-delete-remote-files: true
retry: 5
increment: true
test:
runs-on: ubuntu-latest
steps:
@@ -140,12 +220,17 @@ jobs:
run: |
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
sudo ip addr add 2001:db8::2/64 dev lo
- name: Cargo cache
uses: actions/cache@v4.0.0
with:
path: |
~/.cargo
./target
key: build-cargo-registry-test
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
- name: Run tests
run: sudo -E env "PATH=$PATH" cargo test --verbose
run: |
sudo -E env "PATH=$PATH" cargo test --verbose
sudo chown -R $USER:$USER ./target
sudo chown -R $USER:$USER ~/.cargo

8
.gitignore vendored
View File

@@ -4,10 +4,6 @@ debug/
target/
target-*/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
@@ -24,3 +20,7 @@ flamegraph.svg
root-target
nohup.out
.DS_Store
components.d.ts

6609
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +1,10 @@
[package]
name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/KKRainbow/EasyTier"
repository = "https://github.com/KKRainbow/EasyTier"
version = "0.1.1"
edition = "2021"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"]
rust-version = "1.75"
license-file = "LICENSE"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "easytier-core"
path = "src/easytier-core.rs"
[[bin]]
name = "easytier-cli"
path = "src/easytier-cli.rs"
test = false
[dependencies]
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"local-time",
"time",
] }
tracing-appender = "0.2.3"
log = "0.4"
thiserror = "1.0"
auto_impl = "1.1.0"
crossbeam = "0.8.4"
time = "0.3"
toml = "0.8.12"
chrono = "0.4.35"
gethostname = "0.4.3"
futures = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
tokio-util = { version = "0.7.9", features = ["codec", "net"] }
async-stream = "0.3.5"
async-trait = "0.1.74"
dashmap = "5.5.3"
timedmap = "1.0.1"
# for tap device
tun = { version = "0.6.1", features = ["async"] }
# for net ns
nix = { version = "0.27", features = ["sched", "socket", "ioctl"] }
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
# for ring tunnel
crossbeam-queue = "0.3"
once_cell = "1.18.0"
# for packet
rkyv = { "version" = "0.7.42", features = [
"validation",
"archive_le",
"strict",
"copy_unsafe",
"arbitrary_enum_discriminant",
] }
postcard = { "version" = "1.0.8", features = ["alloc"] }
# for rpc
tonic = "0.10"
prost = "0.12"
anyhow = "1.0"
tarpc = { version = "0.32", features = ["tokio1", "serde1"] }
url = { version = "2.5", features = ["serde"] }
# for tun packet
byteorder = "1.5.0"
# for proxy
cidr = "0.2.2"
socket2 = "0.5.5"
# for hole punching
stun_codec = "0.3.4"
bytecodec = "0.4.15"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
pnet = { version = "0.34.0", features = ["serde"] }
public-ip = { version = "0.2", features = ["default"] }
clap = { version = "4.4.8", features = ["unicode", "derive", "wrap_help"] }
async-recursion = "1.0.5"
network-interface = "1.1.1"
# for ospf route
pathfinding = "4.9.1"
# for cli
tabled = "0.15.*"
humansize = "2.1.3"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_Networking_WinSock",
"Win32_NetworkManagement_IpHelper",
"Win32_Foundation",
"Win32_System_IO",
] }
[build-dependencies]
tonic-build = "0.10"
[target.'cfg(windows)'.build-dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
zip = "0.6.6"
[dev-dependencies]
serial_test = "3.0.0"
[workspace]
resolver = "2"
members = ["easytier", "easytier-gui/src-tauri"]
default-members = [ "easytier" ]
[profile.dev]
panic = "abort"
panic = "unwind"
[profile.release]
panic = "abort"
lto = true
panic = "unwind"

View File

@@ -7,11 +7,18 @@
[简体中文](/README_CN.md) | [English](/README.md)
EasyTier is a simple, plug-and-play, 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
- **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.
- **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.
@@ -19,13 +26,15 @@
- **Smart Routing**: Selects links based on traffic to reduce latency and increase throughput.
- **TCP Support**: Provides reliable data transmission through concurrent TCP links when UDP is limited, optimizing performance.
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
- **IPv6 Support**: Supports networking using IPv6.
## Installation
1. **Download the precompiled binary file**
Visit the [GitHub Release page](https://github.com/KKRainbow/EasyTier/releases) to download the binary file suitable for your operating system.
Visit the [GitHub Release page](https://github.com/KKRainbow/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
@@ -38,6 +47,8 @@
```
## 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.
@@ -66,7 +77,7 @@
```
Successful execution of the command will print the following.
![alt text](assets/image-2.png)
![alt text](/assets/image-2.png)
2. Execute on Node B
```sh
@@ -84,11 +95,11 @@
```sh
easytier-cli peer
```
![alt text](assets/image.png)
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](assets/image-1.png)
![alt text](/assets/image-1.png)
---
@@ -138,7 +149,7 @@
```sh
easytier-cli route
```
![alt text](assets/image-3.png)
![alt text](/assets/image-3.png)
2. Test whether Node A can access nodes under the proxied subnet
@@ -157,17 +168,72 @@
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'
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'
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.
Assuming the network topology is as follows:
```mermaid
flowchart LR
ios[[iPhone \n WireGuard Installed]]
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]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
To enable an iPhone to access the EasyTier network through Node A, the following configuration can be applied:
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.
```
# 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.
```
$> easytier-cli vpn-portal
portal_name: wireguard
client_config:
[Interface]
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
Address = 10.14.14.0/24 # should assign an ip from this cidr manually
[Peer]
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
AllowedIPs = 192.168.80.0/20,10.147.223.0/24,10.144.144.0/24
Endpoint = 0.0.0.0:11013 # should be the public ip of the vpn server
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.
### Configurations
@@ -199,4 +265,4 @@
- Ask questions or report problems: [GitHub Issues](https://github.com/KKRainbow/EasyTier/issues)
- Discussion and exchange: [GitHub Discussions](https://github.com/KKRainbow/EasyTier/discussions)
- QQ Group: 949700262
- Telegramhttps://t.me/easytier

View File

@@ -7,11 +7,18 @@
[简体中文](/README_CN.md) | [English](/README.md)
一个简单、即插即用、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
<p align="center">
<img src="assets/image-6.png" width="300">
<img src="assets/image-7.png" width="300">
</p>
## 特点
- **去中心化**:无需依赖中心化服务,节点平等且独立。
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
- **跨平台**:支持 MacOS/Linux/Windows未来将支持 IOS 和 Android。可执行文件静态链接部署简单。
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
@@ -19,12 +26,13 @@
- **智能路由**:根据流量智能选择链路,减少延迟,提高吞吐量。
- **TCP 支持**:在 UDP 受限的情况下,通过并发 TCP 链接提供可靠的数据传输,优化性能。
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。
## 安装
1. **下载预编译的二进制文件**
访问 [GitHub Release 页面](https://github.com/KKRainbow/EasyTier/releases) 下载适用于您操作系统的二进制文件。
访问 [GitHub Release 页面](https://github.com/KKRainbow/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
2. **通过 crates.io 安装**
```sh
@@ -39,6 +47,8 @@
## 快速开始
> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
确保已按照 [安装指南](#安装) 安装 EasyTier并且 easytier-core 和 easytier-cli 两个命令都已经可用。
### 双节点组网
@@ -66,7 +76,7 @@ nodea <-----> nodeb
```
命令执行成功会有如下打印。
![alt text](assets/image-2.png)
![alt text](/assets/image-2.png)
2. 在节点 B 执行
```sh
@@ -84,11 +94,11 @@ nodea <-----> nodeb
```sh
easytier-cli peer
```
![alt text](assets/image.png)
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](assets/image-1.png)
![alt text](/assets/image-1.png)
---
@@ -138,7 +148,7 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
```sh
easytier-cli route
```
![alt text](assets/image-3.png)
![alt text](/assets/image-3.png)
2. 测试节点 A 是否可访问被代理子网下的节点
@@ -157,19 +167,77 @@ EasyTier 支持共享公网节点进行组网。目前已部署共享的公网
以双节点为例,节点 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://easytier.public.kkrainbow.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://easytier.public.kkrainbow.top:11010
```
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
---
### 使用 WireGuard 客户端接入
EasyTier 可以用作 WireGuard 服务端,让任意安装了 WireGuard 客户端的设备访问 EasyTier 网络。对于目前 EasyTier 不支持的平台 (如 iOS、Android 等),可以使用这种方式接入 EasyTier 网络。
假设网络拓扑如下:
```mermaid
flowchart LR
ios[[iPhone \n 安装 WireGuard]]
subgraph 节点 A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
end
subgraph 节点 B
nodeb[EasyTier\n10.144.144.2]
end
id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
我们需要 iPhone 通过节点 A 访问 EasyTier 网络,则可进行如下配置:
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
```
# 以下参数的含义为: 监听 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 的配置。
```
$> easytier-cli vpn-portal
portal_name: wireguard
client_config:
[Interface]
PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
Address = 10.14.14.0/24 # should assign an ip from this cidr manually
[Peer]
PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
AllowedIPs = 192.168.80.0/20,10.147.223.0/24,10.144.144.0/24
Endpoint = 0.0.0.0:11013 # should be the public ip of the vpn server
connected_clients:
[]
```
使用 Client Config 前,需要将 Interface Address 和 Peer Endpoint 分别修改为客户端的 IP 和 EasyTier 节点的 IP。将配置文件导入 WireGuard 客户端,即可访问 EasyTier 网络。
---
### 其他配置
可使用 ``easytier-core --help`` 查看全部配置项
@@ -178,7 +246,7 @@ sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -
# 路线图
- [ ] 完善文档和用户指南。
- [ ] 支持加密、TCP 打洞等特性。
- [ ] 支持 TCP 打洞等特性。
- [ ] 支持 Android、IOS 等移动平台。
- [ ] 支持 Web 配置管理。
@@ -200,4 +268,5 @@ EasyTier 根据 [Apache License 2.0](https://github.com/KKRainbow/EasyTier/blob/
- 提问或报告问题:[GitHub Issues](https://github.com/KKRainbow/EasyTier/issues)
- 讨论和交流:[GitHub Discussions](https://github.com/KKRainbow/EasyTier/discussions)
- QQ 群: 949700262
- QQ 群: 949700262
- Telegramhttps://t.me/easytier

BIN
assets/image-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
assets/image-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
assets/image-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
assets/image-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

24
easytier-gui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
easytier-gui/README.md Normal file
View File

@@ -0,0 +1,16 @@
# Tauri + Vue 3 + TypeScript
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

14
easytier-gui/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

37
easytier-gui/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "easytier-gui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^1",
"pinia": "^2.1.7",
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^3.51.0",
"vue": "^3.3.4",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"naive-ui": "^2.38.1",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.0.2",
"unplugin-vue-components": "^0.26.0",
"uuid": "^9.0.1",
"vfonts": "^0.0.3",
"vite": "^5.0.0",
"vue-i18n": "^9.12.0",
"vue-tsc": "^1.8.5"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

7
easytier-gui/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

View File

@@ -0,0 +1,30 @@
[package]
name = "easytier-gui"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "process-exit", "system-tray", "shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
easytier = { path = "../../easytier" }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
chrono = { version = "0.4.37", features = ["serde"] }
once_cell = "1.18.0"
dashmap = "5.5.3"
privilege = "0.3"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

View File

@@ -0,0 +1,214 @@
use std::{
collections::VecDeque,
sync::{atomic::AtomicBool, Arc, RwLock},
};
use chrono::{DateTime, Local};
use easytier::{
common::{
config::{ConfigLoader, TomlConfigLoader},
global_ctx::GlobalCtxEvent,
stun::StunInfoCollectorTrait,
},
instance::instance::Instance,
peers::rpc_service::PeerManagerRpcService,
rpc::{
cli::{PeerInfo, Route, StunInfo},
peer::GetIpListResponse,
},
};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct MyNodeInfo {
pub virtual_ipv4: String,
pub ips: GetIpListResponse,
pub stun_info: StunInfo,
pub listeners: Vec<String>,
pub vpn_portal_cfg: Option<String>,
}
#[derive(Default, Clone)]
struct EasyTierData {
events: Arc<RwLock<VecDeque<(DateTime<Local>, GlobalCtxEvent)>>>,
node_info: Arc<RwLock<MyNodeInfo>>,
routes: Arc<RwLock<Vec<Route>>>,
peers: Arc<RwLock<Vec<PeerInfo>>>,
}
pub struct EasyTierLauncher {
instance_alive: Arc<AtomicBool>,
stop_flag: Arc<AtomicBool>,
thread_handle: Option<std::thread::JoinHandle<()>>,
running_cfg: String,
error_msg: Arc<RwLock<Option<String>>>,
data: EasyTierData,
}
impl EasyTierLauncher {
pub fn new() -> Self {
let instance_alive = Arc::new(AtomicBool::new(false));
Self {
instance_alive,
thread_handle: None,
error_msg: Arc::new(RwLock::new(None)),
running_cfg: String::new(),
stop_flag: Arc::new(AtomicBool::new(false)),
data: EasyTierData::default(),
}
}
async fn handle_easytier_event(event: GlobalCtxEvent, data: EasyTierData) {
let mut events = data.events.write().unwrap();
events.push_back((chrono::Local::now(), event));
if events.len() > 100 {
events.pop_front();
}
}
async fn easytier_routine(
cfg: TomlConfigLoader,
stop_signal: Arc<tokio::sync::Notify>,
data: EasyTierData,
) -> Result<(), anyhow::Error> {
let mut instance = Instance::new(cfg);
let peer_mgr = instance.get_peer_manager();
// Subscribe to global context events
let global_ctx = instance.get_global_ctx();
let data_c = data.clone();
tokio::spawn(async move {
let mut receiver = global_ctx.subscribe();
while let Ok(event) = receiver.recv().await {
Self::handle_easytier_event(event, data_c.clone()).await;
}
});
// update my node info
let data_c = data.clone();
let global_ctx_c = instance.get_global_ctx();
let peer_mgr_c = peer_mgr.clone();
let vpn_portal = instance.get_vpn_portal_inst();
tokio::spawn(async move {
loop {
let node_info = MyNodeInfo {
virtual_ipv4: global_ctx_c
.get_ipv4()
.map(|x| x.to_string())
.unwrap_or_default(),
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
.get_running_listeners()
.iter()
.map(|x| x.to_string())
.collect(),
vpn_portal_cfg: Some(
vpn_portal
.lock()
.await
.dump_client_config(peer_mgr_c.clone())
.await,
),
};
*data_c.node_info.write().unwrap() = node_info.clone();
*data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await;
*data_c.peers.write().unwrap() = PeerManagerRpcService::new(peer_mgr_c.clone())
.list_peers()
.await;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
instance.run().await?;
stop_signal.notified().await;
Ok(())
}
pub fn start<F>(&mut self, cfg_generator: F)
where
F: FnOnce() -> Result<TomlConfigLoader, anyhow::Error> + Send + Sync,
{
let error_msg = self.error_msg.clone();
let cfg = cfg_generator();
if let Err(e) = cfg {
error_msg.write().unwrap().replace(e.to_string());
return;
}
self.running_cfg = cfg.as_ref().unwrap().dump();
let stop_flag = self.stop_flag.clone();
let instance_alive = self.instance_alive.clone();
instance_alive.store(true, std::sync::atomic::Ordering::Relaxed);
let data = self.data.clone();
self.thread_handle = Some(std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let stop_notifier = Arc::new(tokio::sync::Notify::new());
let stop_notifier_clone = stop_notifier.clone();
rt.spawn(async move {
while !stop_flag.load(std::sync::atomic::Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
stop_notifier_clone.notify_one();
});
let ret = rt.block_on(Self::easytier_routine(
cfg.unwrap(),
stop_notifier.clone(),
data,
));
if let Err(e) = ret {
error_msg.write().unwrap().replace(e.to_string());
}
instance_alive.store(false, std::sync::atomic::Ordering::Relaxed);
}));
}
pub fn error_msg(&self) -> Option<String> {
self.error_msg.read().unwrap().clone()
}
pub fn running(&self) -> bool {
self.instance_alive
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn get_events(&self) -> Vec<(DateTime<Local>, GlobalCtxEvent)> {
let events = self.data.events.read().unwrap();
events.iter().cloned().collect()
}
pub fn get_node_info(&self) -> MyNodeInfo {
self.data.node_info.read().unwrap().clone()
}
pub fn get_routes(&self) -> Vec<Route> {
self.data.routes.read().unwrap().clone()
}
pub fn get_peers(&self) -> Vec<PeerInfo> {
self.data.peers.read().unwrap().clone()
}
}
impl Drop for EasyTierLauncher {
fn drop(&mut self) {
self.stop_flag
.store(true, std::sync::atomic::Ordering::Relaxed);
if let Some(handle) = self.thread_handle.take() {
if let Err(e) = handle.join() {
println!("Error when joining thread: {:?}", e);
}
}
}
}

View File

@@ -0,0 +1,354 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod launcher;
use std::{collections::BTreeMap, env::current_exe, process};
use anyhow::Context;
use chrono::{DateTime, Local};
use dashmap::DashMap;
use easytier::{
common::{
config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, VpnPortalConfig},
global_ctx::GlobalCtxEvent,
},
rpc::{PeerInfo, Route},
utils::{list_peer_route_pair, PeerRoutePair},
};
use launcher::{EasyTierLauncher, MyNodeInfo};
use serde::{Deserialize, Serialize};
use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
Window,
};
#[derive(Deserialize, Serialize, PartialEq, Debug)]
enum NetworkingMethod {
PublicServer,
Manual,
Standalone,
}
impl Default for NetworkingMethod {
fn default() -> Self {
NetworkingMethod::PublicServer
}
}
#[derive(Deserialize, Serialize, Debug, Default)]
struct NetworkConfig {
instance_id: String,
virtual_ipv4: String,
network_name: String,
network_secret: String,
networking_method: NetworkingMethod,
public_server_url: String,
peer_urls: Vec<String>,
proxy_cidrs: Vec<String>,
enable_vpn_portal: bool,
vpn_portal_listne_port: i32,
vpn_portal_client_network_addr: String,
vpn_portal_client_network_len: i32,
advanced_settings: bool,
listener_urls: Vec<String>,
rpc_port: i32,
}
impl NetworkConfig {
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
let cfg = TomlConfigLoader::default();
cfg.set_id(
self.instance_id
.parse()
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
);
cfg.set_inst_name(self.network_name.clone());
cfg.set_network_identity(NetworkIdentity::new(
self.network_name.clone(),
self.network_secret.clone(),
));
if self.virtual_ipv4.len() > 0 {
cfg.set_ipv4(
self.virtual_ipv4.parse().with_context(|| {
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
})?,
)
}
match self.networking_method {
NetworkingMethod::PublicServer => {
cfg.set_peers(vec![PeerConfig {
uri: self.public_server_url.parse().with_context(|| {
format!(
"failed to parse public server uri: {}",
self.public_server_url
)
})?,
}]);
}
NetworkingMethod::Manual => {
let mut peers = vec![];
for peer_url in self.peer_urls.iter() {
if peer_url.is_empty() {
continue;
}
peers.push(PeerConfig {
uri: peer_url
.parse()
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
});
}
cfg.set_peers(peers);
}
NetworkingMethod::Standalone => {}
}
let mut listener_urls = vec![];
for listener_url in self.listener_urls.iter() {
if listener_url.is_empty() {
continue;
}
listener_urls.push(
listener_url
.parse()
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
);
}
cfg.set_listeners(listener_urls);
for n in self.proxy_cidrs.iter() {
cfg.add_proxy_cidr(
n.parse()
.with_context(|| format!("failed to parse proxy network: {}", n))?,
);
}
cfg.set_rpc_portal(
format!("127.0.0.1:{}", self.rpc_port)
.parse()
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
);
if self.enable_vpn_portal {
let cidr = format!(
"{}/{}",
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
);
cfg.set_vpn_portal_config(VpnPortalConfig {
client_cidr: cidr
.parse()
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listne_port)
.parse()
.with_context(|| {
format!(
"failed to parse vpn portal wireguard listen port. {}",
self.vpn_portal_listne_port
)
})?,
});
}
Ok(cfg)
}
}
#[derive(Deserialize, Serialize, Debug)]
struct NetworkInstanceRunningInfo {
my_node_info: MyNodeInfo,
events: Vec<(DateTime<Local>, GlobalCtxEvent)>,
node_info: MyNodeInfo,
routes: Vec<Route>,
peers: Vec<PeerInfo>,
peer_route_pairs: Vec<PeerRoutePair>,
running: bool,
error_msg: Option<String>,
}
struct NetworkInstance {
config: TomlConfigLoader,
launcher: Option<EasyTierLauncher>,
}
impl NetworkInstance {
fn new(cfg: NetworkConfig) -> Result<Self, anyhow::Error> {
Ok(Self {
config: cfg.gen_config()?,
launcher: None,
})
}
fn is_easytier_running(&self) -> bool {
self.launcher.is_some() && self.launcher.as_ref().unwrap().running()
}
fn get_running_info(&self) -> Option<NetworkInstanceRunningInfo> {
if self.launcher.is_none() {
return None;
}
let launcher = self.launcher.as_ref().unwrap();
let peers = launcher.get_peers();
let routes = launcher.get_routes();
let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone());
Some(NetworkInstanceRunningInfo {
my_node_info: launcher.get_node_info(),
events: launcher.get_events(),
node_info: launcher.get_node_info(),
routes,
peers,
peer_route_pairs,
running: launcher.running(),
error_msg: launcher.error_msg(),
})
}
fn start(&mut self) -> Result<(), anyhow::Error> {
if self.is_easytier_running() {
return Ok(());
}
let mut launcher = EasyTierLauncher::new();
launcher.start(|| Ok(self.config.clone()));
self.launcher = Some(launcher);
Ok(())
}
}
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
once_cell::sync::Lazy::new(DashMap::new);
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn parse_network_config(cfg: &str) -> Result<String, String> {
let cfg: NetworkConfig = serde_json::from_str(cfg).map_err(|e| e.to_string())?;
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
Ok(toml.dump())
}
#[tauri::command]
fn run_network_instance(cfg: &str) -> Result<String, String> {
let cfg: NetworkConfig = serde_json::from_str(cfg).map_err(|e| e.to_string())?;
if INSTANCE_MAP.contains_key(&cfg.instance_id) {
return Err("instance already exists".to_string());
}
let instance_id = cfg.instance_id.clone();
let mut instance = NetworkInstance::new(cfg).map_err(|e| e.to_string())?;
instance.start().map_err(|e| e.to_string())?;
println!("instance {} started", instance_id);
INSTANCE_MAP.insert(instance_id, instance);
Ok("".to_string())
}
#[tauri::command]
fn retain_network_instance(instance_ids: &str) -> Result<(), String> {
let instance_ids: Vec<String> =
serde_json::from_str(instance_ids).map_err(|e| e.to_string())?;
let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k));
println!(
"instance {:?} retained",
INSTANCE_MAP
.iter()
.map(|item| item.key().clone())
.collect::<Vec<_>>()
);
Ok(())
}
#[tauri::command]
fn collect_network_infos() -> Result<String, String> {
let mut ret = BTreeMap::new();
for instance in INSTANCE_MAP.iter() {
if let Some(info) = instance.get_running_info() {
ret.insert(instance.key().clone(), info);
}
}
Ok(serde_json::to_string(&ret).map_err(|e| e.to_string())?)
}
fn toggle_window_visibility(window: &Window) {
if window.is_visible().unwrap() {
window.hide().unwrap();
} else {
window.show().unwrap();
window.set_focus().unwrap();
}
}
fn check_sudo() -> bool {
let is_elevated = privilege::user::privileged();
if !is_elevated {
let Ok(my_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();
}
is_elevated
}
fn main() {
if !check_sudo() {
process::exit(0);
}
let quit = CustomMenuItem::new("quit".to_string(), "退出 Quit");
let hide = CustomMenuItem::new("hide".to_string(), "显示 Show / 隐藏 Hide");
let tray_menu = SystemTrayMenu::new()
.add_item(quit)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(hide);
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
parse_network_config,
run_network_instance,
retain_network_instance,
collect_network_infos
])
.system_tray(SystemTray::new().with_menu(tray_menu))
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::DoubleClick {
position: _,
size: _,
..
} => {
let window = app.get_window("main").unwrap();
toggle_window_visibility(&window);
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"quit" => {
std::process::exit(0);
}
"hide" => {
let window = app.get_window("main").unwrap();
toggle_window_visibility(&window);
}
_ => {}
},
_ => {}
})
.on_window_event(|event| match event.event() {
tauri::WindowEvent::CloseRequested { api, .. } => {
event.window().hide().unwrap();
api.prevent_close();
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,49 @@
{
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "easytier-gui",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"process": {
"exit": true
}
},
"windows": [
{
"title": "easytier-gui",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
},
"systemTray": {
"iconPath": "icons/icon.ico",
"iconAsTemplate": true
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.kkrainbow.easyiter-client",
"icon": [
"icons/icon.png",
"icons/icon.rgba",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
}

View File

@@ -0,0 +1,16 @@
{
"tauri": {
"bundle": {
"externalBin": [],
"resources": [
"./wintun.dll",
"./Packet.dll"
],
"windows": {
"webviewInstallMode": {
"type": "embedBootstrapper"
}
}
}
}
}

258
easytier-gui/src/App.vue Normal file
View File

@@ -0,0 +1,258 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import Stepper from 'primevue/stepper';
import StepperPanel from 'primevue/stepperpanel';
import { useToast } from "primevue/usetoast";
import {
i18n, loadLocaleFromLocalStorage, NetworkConfig, parseNetworkConfig,
useNetworkStore, runNetworkInstance, retainNetworkInstance, collectNetworkInfos,
changeLocale
} from './main';
import Config from './components/Config.vue';
import Status from './components/Status.vue';
import { exit } from '@tauri-apps/api/process';
const visible = ref(false);
const tomlConfig = ref("");
const items = ref([
{
label: () => i18n.global.t('show_config'),
icon: 'pi pi-file-edit',
command: async () => {
try {
const ret = await parseNetworkConfig(networkStore.curNetwork);
tomlConfig.value = ret;
} catch (e: any) {
tomlConfig.value = e;
}
visible.value = true;
}
},
{
label: () => i18n.global.t('del_cur_network'),
icon: 'pi pi-times',
command: async () => {
networkStore.removeNetworkInstance(networkStore.curNetwork.instance_id);
await retainNetworkInstance(networkStore.networkInstanceIds);
networkStore.delCurNetwork();
},
disabled: () => networkStore.networkList.length <= 1,
},
])
enum Severity {
None = "none",
Success = "success",
Info = "info",
Warn = "warn",
Error = "error",
}
const messageBarSeverity = ref(Severity.None);
const messageBarContent = ref("");
const toast = useToast();
const networkStore = useNetworkStore();
const addNewNetwork = () => {
networkStore.addNewNetwork();
networkStore.curNetwork = networkStore.lastNetwork;
}
const networkMenuName = (network: NetworkConfig) => {
return network.network_name + " (" + network.instance_id + ")";
}
networkStore.$subscribe(async () => {
networkStore.saveToLocalStroage();
try {
await parseNetworkConfig(networkStore.curNetwork);
messageBarSeverity.value = Severity.None;
} catch (e: any) {
messageBarContent.value = e;
messageBarSeverity.value = Severity.Error;
}
});
async function runNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
cb({} as MouseEvent);
networkStore.removeNetworkInstance(cfg.instance_id);
await retainNetworkInstance(networkStore.networkInstanceIds);
networkStore.addNetworkInstance(cfg.instance_id);
try {
await runNetworkInstance(cfg);
} catch (e: any) {
console.error(e);
toast.add({ severity: 'info', detail: e });
}
}
async function stopNetworkCb(cfg: NetworkConfig, cb: (e: MouseEvent) => void) {
console.log("stopNetworkCb", cfg, cb);
cb({} as MouseEvent);
networkStore.removeNetworkInstance(cfg.instance_id);
await retainNetworkInstance(networkStore.networkInstanceIds);
}
async function updateNetworkInfos() {
networkStore.updateWithNetworkInfos(await collectNetworkInfos());
}
let intervalId = 0;
onMounted(() => {
intervalId = setInterval(async () => {
await updateNetworkInfos();
}, 500);
});
onUnmounted(() => clearInterval(intervalId))
const curNetworkHasInstance = computed(() => {
return networkStore.networkInstanceIds.includes(networkStore.curNetworkId);
});
const activeStep = computed(() => {
return curNetworkHasInstance.value ? 1 : 0;
});
const setting_menu = ref();
const setting_menu_items = ref([
{
label: () => i18n.global.t('settings'),
items: [
{
label: () => i18n.global.t('exchange_language'),
icon: 'pi pi-refresh',
command: () => {
changeLocale((i18n.global.locale.value === 'en' ? 'cn' : 'en'));
}
},
{
label: () => i18n.global.t('exit'),
icon: 'pi pi-times',
command: async () => {
await exit(1);
}
}
]
}
]);
const toggle_setting_menu = (event: any) => {
setting_menu.value.toggle(event);
};
onMounted(async () => {
networkStore.loadFromLocalStorage();
changeLocale(loadLocaleFromLocalStorage());
});
</script>
<template>
<!-- <n-config-provider :theme="lightTheme"> -->
<div id="root" class="flex flex-column">
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
<Panel>
<ScrollPanel style="width: 100%; height: 300px">
<pre>{{ tomlConfig }}</pre>
</ScrollPanel>
</Panel>
<Divider />
<div class="flex justify-content-end gap-2">
<Button type="button" :label="$t('close')" @click="visible = false"></Button>
</div>
</Dialog>
<div>
<Toolbar>
<template #start>
<div class="flex align-items-center gap-2">
<Button icon="pi pi-plus" class="mr-2" severity="primary" :label="$t('add_new_network')"
@click="addNewNetwork" />
</div>
</template>
<template #center>
<div class="min-w-80 mr-20">
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList"
:optionLabel="networkMenuName" :placeholder="$t('select_network')" :highlightOnSelect="true"
:checkmark="true" class="w-full md:w-32rem" />
</div>
</template>
<template #end>
<Button icon="pi pi-cog" class="mr-2" severity="secondary" aria-haspopup="true" @click="toggle_setting_menu"
:label="$t('settings')" aria-controls="overlay_setting_menu" />
<Menu ref="setting_menu" id="overlay_setting_menu" :model="setting_menu_items" :popup="true" />
</template>
</Toolbar>
</div>
<Stepper class="h-full overflow-y-auto" :active-step="activeStep">
<StepperPanel :header="$t('config_network')" class="w">
<template #content="{ nextCallback }">
<Config @run-network="runNetworkCb($event, nextCallback)" :instance-id="networkStore.curNetworkId"
:config-invalid="messageBarSeverity != Severity.None" />
</template>
</StepperPanel>
<StepperPanel :header="$t('running')">
<template #content="{ prevCallback }">
<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, prevCallback)" />
</div>
</template>
</StepperPanel>
</Stepper>
<div>
<Menubar :model="items" breakpoint="300px">
</Menubar>
<InlineMessage v-if="messageBarSeverity !== Severity.None" class="absolute bottom-0 right-0" severity="error">
{{ messageBarContent }}</InlineMessage>
</div>
</div>
</template>
<style scoped>
#root {
height: 100vh;
width: 100vw;
}
</style>
<style>
body {
height: 100vh;
width: 100vw;
padding: 0;
margin: 0;
overflow: hidden;
}
.p-menubar .p-menuitem {
margin: 0;
}
/*
.p-tabview-panel {
height: 100%;
} */
</style>
<script lang="ts">
</script>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import InputGroup from "primevue/inputgroup";
import InputGroupAddon from "primevue/inputgroupaddon";
import { ref, defineProps, computed } from "vue";
import { i18n, useNetworkStore, NetworkingMethod } from "../main";
const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: i18n.global.t('public_server') },
{ value: NetworkingMethod.Manual, label: i18n.global.t('manual') },
{ value: NetworkingMethod.Standalone, label: i18n.global.t('standalone') },
]);
const props = defineProps<{
configInvalid?: boolean,
instanceId?: string,
}>()
defineEmits(["runNetwork"]);
const networkStore = useNetworkStore();
const curNetwork = computed(() => {
if (props.instanceId) {
console.log("instanceId", props.instanceId);
const c = networkStore.networkList.find(n => n.instance_id == props.instanceId);
if (c != undefined) {
return c;
}
}
return networkStore.curNetwork;
});
const presetPublicServers = [
"tcp://easytier.public.kkrainbow.top:11010",
];
</script>
<template>
<div class="flex flex-column h-full">
<div class="flex flex-column">
<div class="w-10/12 max-w-fit self-center ">
<Panel header="Basic Settings">
<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">
<label for="virtual_ip">{{ $t('virtual_ipv4') }}</label>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" aria-describedby="virtual_ipv4-help" />
<InputGroupAddon>
<span>/24</span>
</InputGroupAddon>
</InputGroup>
</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="network_name">{{ $t('network_name') }}</label>
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
</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" />
</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>
<div class="items-center flex flex-row p-fluid gap-x-1">
<Dropdown v-model="curNetwork.networking_method" :options="networking_methods" optionLabel="label"
optionValue="value" placeholder="Select Method" class="" />
<Chips id="chips" v-model="curNetwork.peer_urls"
:placeholder="$t('chips_placeholder', ['tcp://8.8.8.8:11010'])" separator=" " class="grow"
v-if="curNetwork.networking_method == NetworkingMethod.Manual" />
<Dropdown :editable="true" v-model="curNetwork.public_server_url" class="grow"
:options="presetPublicServers"
v-if="curNetwork.networking_method == NetworkingMethod.PublicServer" />
</div>
</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" />
</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>
<div class="items-center flex flex-row gap-x-4">
<ToggleButton onIcon="pi pi-check" offIcon="pi pi-times" v-model="curNetwork.enable_vpn_portal"
:onLabel="$t('off_text')" :offLabel="$t('on_text')" />
<div class="grow" v-if="curNetwork.enable_vpn_portal">
<InputGroup>
<InputText :placeholder="$t('vpn_portal_client_network')"
v-model="curNetwork.vpn_portal_client_network_addr" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
</div>
<InputNumber :placeholder="$t('vpn_portal_listen_port')" class="" v-if="curNetwork.enable_vpn_portal"
:format="false" v-model="curNetwork.vpn_portal_listne_port" :min="0" :max="65535" />
</div>
</div>
</div>
</div>
</Panel>
<Divider />
<Panel :header="$t('advanced_settings')" toggleable>
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="listener_urls">{{ $t('listener_urls') }}</label>
<Chips id="listener_urls" v-model="curNetwork.listener_urls"
:placeholder="$t('chips_placeholder', ['tcp://1.1.1.1:11010'])" separator=" " class="w-full" />
</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" />
</div>
</div>
</div>
</Panel>
<Divider />
<div class="flex pt-4 justify-content-center">
<Button :label="$t('run_network')" icon="pi pi-arrow-right" iconPos="right" @click="$emit('runNetwork', curNetwork)"
:disabled="configInvalid" />
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,358 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useNetworkStore } from '../main';
const networkStore = useNetworkStore();
const props = defineProps<{
instanceId?: string,
}>()
const curNetwork = computed(() => {
if (props.instanceId) {
console.log("instanceId", props.instanceId);
const c = networkStore.networkList.find(n => n.instance_id == props.instanceId);
if (c != undefined) {
return c;
}
}
return networkStore.curNetwork;
});
let curNetworkInst = computed(() => {
return networkStore.networkInstances.find(n => n.instance_id == curNetwork.value.instance_id);
});
let peerRouteInfos = computed(() => {
if (curNetworkInst.value) {
return curNetworkInst.value.detail.peer_route_pairs;
}
return [];
});
let routeCost = (info: any) => {
if (info.route) {
const cost = info.route.cost;
return cost == 1 ? "p2p" : `relay(${cost})`
}
return '?';
};
function resolveObjPath(path: string, obj = self, separator = '.') {
var properties = Array.isArray(path) ? path : path.split(separator)
return properties.reduce((prev, curr) => prev?.[curr], obj)
}
let statsCommon = (info: any, field: string) => {
if (!info.peer) {
return undefined;
}
let conns = info.peer.conns;
return conns.reduce((acc: number, conn: any) => {
return acc + resolveObjPath(field, conn);
}, 0);
};
function humanFileSize(bytes: number, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = si
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return bytes.toFixed(dp) + ' ' + units[u];
}
let latencyMs = (info: any) => {
let lat_us_sum = statsCommon(info, 'stats.latency_us');
return lat_us_sum ? `${lat_us_sum / 1000 / info.peer.conns.length}ms` : '';
};
let txBytes = (info: any) => {
let tx = statsCommon(info, 'stats.tx_bytes');
return tx ? humanFileSize(tx) : '';
}
let rxBytes = (info: any) => {
let rx = statsCommon(info, 'stats.rx_bytes');
return rx ? humanFileSize(rx) : '';
}
let lossRate = (info: any) => {
let lossRate = statsCommon(info, 'loss_rate');
return lossRate != undefined ? `${Math.round(lossRate * 100)}%` : '';
}
const myNodeInfo = computed(() => {
if (!curNetworkInst.value) {
return {};
}
return curNetworkInst.value.detail?.my_node_info;
});
interface Chip {
label: string;
icon: string;
}
let myNodeInfoChips = computed(() => {
if (!curNetworkInst.value) {
return [];
}
let chips: Array<Chip> = [];
let my_node_info = curNetworkInst.value.detail?.my_node_info;
if (!my_node_info) {
return chips;
}
// local ipv4s
let local_ipv4s = my_node_info.ips?.interface_ipv4s;
for (let [idx, ip] of local_ipv4s?.entries()) {
chips.push({
label: `Local IPv4 ${idx}: ${ip}`,
icon: '',
} as Chip);
}
// local ipv6s
let local_ipv6s = my_node_info.ips?.interface_ipv6s;
for (let [idx, ip] of local_ipv6s?.entries()) {
chips.push({
label: `Local IPv6 ${idx}: ${ip}`,
icon: '',
} as Chip);
}
// public ip
let public_ip = my_node_info.ips?.public_ipv4;
if (public_ip) {
chips.push({
label: `Public IP: ${public_ip}`,
icon: '',
} as Chip);
}
// listeners:
let listeners = my_node_info.listeners;
for (let [idx, listener] of listeners?.entries()) {
chips.push({
label: `Listener ${idx}: ${listener}`,
icon: '',
} as Chip);
}
// udp nat type
enum NatType {
// has NAT; but own a single public IP, port is not changed
Unknown = 0,
OpenInternet = 1,
NoPAT = 2,
FullCone = 3,
Restricted = 4,
PortRestricted = 5,
Symmetric = 6,
SymUdpFirewall = 7,
};
let udpNatType: NatType = my_node_info.stun_info?.udp_nat_type;
if (udpNatType != undefined) {
let udpNatTypeStrMap = {
[NatType.Unknown]: 'Unknown',
[NatType.OpenInternet]: 'Open Internet',
[NatType.NoPAT]: 'No PAT',
[NatType.FullCone]: 'Full Cone',
[NatType.Restricted]: 'Restricted',
[NatType.PortRestricted]: 'Port Restricted',
[NatType.Symmetric]: 'Symmetric',
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
};
chips.push({
label: `UDP NAT Type: ${udpNatTypeStrMap[udpNatType]}`,
icon: '',
} as Chip);
}
return chips;
});
const globalSumCommon = (field: string) => {
let sum = 0;
if (!peerRouteInfos.value) {
return sum;
}
for (let info of peerRouteInfos.value) {
let tx = statsCommon(info, field);
if (tx) {
sum += tx;
}
}
return sum;
};
const txGlobalSum = () => {
return globalSumCommon('stats.tx_bytes');
};
const rxGlobalSum = () => {
return globalSumCommon('stats.rx_bytes');
}
const peerCount = computed(() => {
if (!peerRouteInfos.value) {
return 0;
}
return peerRouteInfos.value.length;
});
// calculate tx/rx rate every 2 seconds
let rateIntervalId = 0;
let rateInterval = 2000;
let prevTxSum = 0;
let prevRxSum = 0;
let txRate = ref('0');
let rxRate = ref('0');
onMounted(() => {
rateIntervalId = setInterval(() => {
let curTxSum = txGlobalSum();
txRate.value = humanFileSize((curTxSum - prevTxSum) / (rateInterval / 1000));
prevTxSum = curTxSum;
let curRxSum = rxGlobalSum();
rxRate.value = humanFileSize((curRxSum - prevRxSum) / (rateInterval / 1000));
prevRxSum = curRxSum;
}, rateInterval);
});
onUnmounted(() => {
clearInterval(rateIntervalId);
});
const dialogVisible = ref(false);
const dialogContent = ref('');
const showVpnPortalConfig = () => {
let my_node_info = myNodeInfo.value;
if (!my_node_info) {
return;
}
const url = "https://www.wireguardconfig.com/qrcode";
dialogContent.value = `${my_node_info.vpn_portal_cfg}\n\n # can generate QR code: ${url}`;
dialogVisible.value = true;
}
const showEventLogs = () => {
let detail = curNetworkInst.value?.detail;
if (!detail) {
return;
}
dialogContent.value = detail.events;
dialogVisible.value = true;
}
</script>
<template>
<div>
<Dialog v-model:visible="dialogVisible" modal header="Dialog" :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="Close" @click="dialogVisible = false"></Button>
</div>
</Dialog>
<Card v-if="curNetworkInst?.error_msg">
<template #title>Run Network Error</template>
<template #content>
<div class="flex flex-column gap-y-5">
<div class="text-red-500">
{{ curNetworkInst.error_msg }}
</div>
</div>
</template>
</Card>
<Card v-if="!curNetworkInst?.error_msg">
<template #title>{{ $t('my_node_info') }}</template>
<template #content>
<div class="flex w-full flex-column gap-y-5">
<div class="m-0 flex flex-row justify-center gap-x-5">
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
style="border: 1px solid green">
<div class="font-bold">
{{ $t('peer_count') }}
</div>
<div class="text-5xl mt-1">{{ peerCount }}</div>
</div>
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
style="border: 1px solid purple">
<div class="font-bold">
{{ $t('upload') }}
</div>
<div class="text-xl mt-2">{{ txRate }}/s</div>
</div>
<div class="rounded-full w-36 h-36 flex flex-column align-items-center pt-4"
style="border: 1px solid fuchsia">
<div class="font-bold">
{{ $t('download') }}
</div>
<div class="text-xl mt-2">{{ rxRate }}/s</div>
</div>
</div>
<div class="flex flex-row align-items-center flex-wrap w-full">
<Chip v-for="chip in myNodeInfoChips" :label="chip.label" :icon="chip.icon" class="mr-2 mt-2">
</Chip>
</div>
<div class="m-0 flex flex-row justify-center gap-x-5 text-sm" v-if="myNodeInfo">
<Button severity="info" :label="$t('show_vpn_portal_config')" @click="showVpnPortalConfig" />
<Button severity="info" :label="$t('show_event_log')" @click="showEventLogs" />
</div>
</div>
</template>
</Card>
<Divider />
<Card v-if="!curNetworkInst?.error_msg">
<template #title>{{ $t('peer_info') }}</template>
<template #content>
<DataTable :value="peerRouteInfos" tableStyle="min-width: 50rem">
<Column field="route.ipv4_addr" :header="$t('virtual_ipv4')"></Column>
<Column field="route.hostname" :header="$t('hostname')"></Column>
<Column :field="routeCost" :header="$t('route_cost')"></Column>
<Column :field="latencyMs" :header="$t('latency')"></Column>
<Column :field="txBytes" :header="$t('upload_bytes')"></Column>
<Column :field="rxBytes" :header="$t('download_bytes')"></Column>
<Column :field="lossRate" :header="$t('loss_rate')"></Column>
</DataTable>
</template>
</Card>
</div>
</template>

362
easytier-gui/src/main.ts Normal file
View File

@@ -0,0 +1,362 @@
import "./styles.css";
import "primevue/resources/themes/aura-light-green/theme.css";
import "primeicons/primeicons.css";
import "primeflex/primeflex.css";
import { createPinia, defineStore } from 'pinia'
import { createMemoryHistory, createRouter } from 'vue-router'
import { createApp } from "vue";
import PrimeVue from 'primevue/config';
import App from "./App.vue";
import { invoke } from "@tauri-apps/api/tauri";
import { v4 as uuidv4 } from 'uuid';
import ToastService from 'primevue/toastservice';
const pinia = createPinia()
export enum NetworkingMethod {
PublicServer = "PublicServer",
Manual = "Manual",
Standalone = "Standalone",
}
export interface NetworkConfig {
instance_id: string,
virtual_ipv4: string
network_name: string
network_secret: string
networking_method: NetworkingMethod,
public_server_url: string,
peer_urls: Array<string>,
proxy_cidrs: Array<string>,
enable_vpn_portal: boolean,
vpn_portal_listne_port: number,
vpn_portal_client_network_addr: string,
vpn_portal_client_network_len: number,
advanced_settings: boolean,
listener_urls: Array<string>,
rpc_port: number,
}
function default_network(): NetworkConfig {
return {
instance_id: uuidv4(),
virtual_ipv4: "",
network_name: "default",
network_secret: "",
networking_method: NetworkingMethod.PublicServer,
public_server_url: "tcp://easytier.public.kkrainbow.top:11010",
peer_urls: [],
proxy_cidrs: [],
enable_vpn_portal: false,
vpn_portal_listne_port: 22022,
vpn_portal_client_network_addr: "",
vpn_portal_client_network_len: 24,
advanced_settings: false,
listener_urls: [
"tcp://0.0.0.0:11010",
"udp://0.0.0.0:11010",
"wg://0.0.0.0:11011",
],
rpc_port: 15888,
}
}
export interface NetworkInstance {
instance_id: string,
running: boolean,
error_msg: string,
detail: any,
}
export const useNetworkStore = defineStore('network', {
state: () => {
const networkList = [default_network()];
return {
// for initially empty lists
networkList: networkList as NetworkConfig[],
// for data that is not yet loaded
curNetwork: networkList[0],
// uuid -> instance
instances: {} as Record<string, NetworkInstance>,
networkInfos: {} as Record<string, any>,
}
},
getters: {
lastNetwork(): NetworkConfig {
return this.networkList[this.networkList.length - 1];
},
curNetworkId(): string {
return this.curNetwork.instance_id;
},
networkInstances(): Array<NetworkInstance> {
return Object.values(this.instances);
},
networkInstanceIds(): Array<string> {
return Object.keys(this.instances);
}
},
actions: {
addNewNetwork() {
this.networkList.push(default_network());
},
delCurNetwork() {
const curNetworkIdx = this.networkList.indexOf(this.curNetwork);
this.networkList.splice(curNetworkIdx, 1);
const nextCurNetworkIdx = Math.min(curNetworkIdx, this.networkList.length - 1);
this.curNetwork = this.networkList[nextCurNetworkIdx];
},
removeNetworkInstance(instanceId: string) {
delete this.instances[instanceId];
},
addNetworkInstance(instanceId: string) {
this.instances[instanceId] = {
instance_id: instanceId,
running: false,
error_msg: "",
detail: {},
};
},
updateWithNetworkInfos(networkInfos: Record<string, any>) {
this.networkInfos = networkInfos;
for (const [instanceId, info] of Object.entries(networkInfos)) {
if (this.instances[instanceId] === undefined) {
this.addNetworkInstance(instanceId);
}
this.instances[instanceId].running = info["running"];
this.instances[instanceId].error_msg = info["error_msg"];
this.instances[instanceId].detail = info;
}
},
loadFromLocalStorage() {
const networkList = JSON.parse(localStorage.getItem("networkList") || '[]');
let result = [];
for (const cfg of networkList) {
result.push({
...default_network(),
...cfg,
});
}
if (result.length === 0) {
result.push(default_network());
}
this.networkList = result;
this.curNetwork = this.networkList[0];
},
saveToLocalStroage() {
localStorage.setItem("networkList", JSON.stringify(this.networkList));
}
}
})
export async function parseNetworkConfig(cfg: NetworkConfig): Promise<string> {
const ret: string = await invoke("parse_network_config", { cfg: JSON.stringify(cfg) });
return ret;
}
export async function runNetworkInstance(cfg: NetworkConfig) {
const ret: string = await invoke("run_network_instance", { cfg: JSON.stringify(cfg) });
return ret;
}
export async function retainNetworkInstance(instanceIds: Array<string>) {
const ret: string = await invoke("retain_network_instance", { instanceIds: JSON.stringify(instanceIds) });
return ret;
}
export async function collectNetworkInfos() {
const ret: string = await invoke("collect_network_infos", {});
return JSON.parse(ret);
}
import { createI18n } from 'vue-i18n'
const messages = {
en: {
"network": "Network",
"networking_method": "Networking Method",
"public_server": "Public Server",
"manual": "Manual",
"standalone": "Standalone",
"virtual_ipv4": "Virtual IPv4",
"network_name": "Network Name",
"network_secret": "Network Secret",
"public_server_url": "Public Server URL",
"peer_urls": "Peer URLs",
"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",
"advanced_settings": "Advanced Settings",
"listener_urls": "Listener URLs",
"rpc_port": "RPC Port",
"config_network": "Config Network",
"running": "Running",
"error_msg": "Error Message",
"detail": "Detail",
"add_new_network": "Add New Network",
"del_cur_network": "Delete Current Network",
"select_network": "Select Network",
"network_instances": "Network Instances",
"instance_id": "Instance ID",
"network_infos": "Network Infos",
"parse_network_config": "Parse Network Config",
"retain_network_instance": "Retain Network Instance",
"collect_network_infos": "Collect Network Infos",
"settings": "Settings",
"exchange_language": "切换中文",
"exit": "Exit",
"chips_placeholder": "e.g: {0}, press Enter to add",
"off_text": "Press to disable",
"on_text": "Press to enable",
"show_config": "Show Config",
"close": "Close",
"my_node_info": "My Node Info",
"peer_count": "Connected",
"upload": "Upload",
"download": "Download",
"show_vpn_portal_config": "Show VPN Portal Config",
"show_event_log": "Show Event Log",
"peer_info": "Peer Info",
"route_cost": "Route Cost",
"hostname": "Hostname",
"latency": "Latency",
"upload_bytes": "Upload",
"download_bytes": "Download",
"loss_rate": "Loss Rate",
"run_network": "Run Network",
"stop_network": "Stop Network",
},
cn: {
"network": "网络",
"networking_method": "网络方式",
"public_server": "公共服务器",
"manual": "手动",
"standalone": "独立",
"virtual_ipv4": "虚拟IPv4地址",
"network_name": "网络名称",
"network_secret": "网络密码",
"public_server_url": "公共服务器地址",
"peer_urls": "对等节点地址",
"proxy_cidrs": "子网代理CIDR",
"enable_vpn_portal": "启用VPN门户",
"vpn_portal_listen_port": "监听端口",
"vpn_portal_client_network": "客户端子网",
"advanced_settings": "高级设置",
"listener_urls": "监听地址",
"rpc_port": "RPC端口",
"config_network": "配置网络",
"running": "运行中",
"error_msg": "错误信息",
"detail": "详情",
"add_new_network": "添加新网络",
"del_cur_network": "删除当前网络",
"select_network": "选择网络",
"network_instances": "网络实例",
"instance_id": "实例ID",
"network_infos": "网络信息",
"parse_network_config": "解析网络配置",
"retain_network_instance": "保留网络实例",
"collect_network_infos": "收集网络信息",
"settings": "设置",
"exchange_language": "Switch to English",
"exit": "退出",
"chips_placeholder": "例如: {0}, 按回车添加",
"off_text": "点击关闭",
"on_text": "点击开启",
"show_config": "显示配置",
"close": "关闭",
"my_node_info": "当前节点信息",
"peer_count": "已连接",
"upload": "上传",
"download": "下载",
"show_vpn_portal_config": "显示VPN门户配置",
"show_event_log": "显示事件日志",
"peer_info": "节点信息",
"hostname": "主机名",
"route_cost": "路由",
"latency": "延迟",
"upload_bytes": "上传",
"download_bytes": "下载",
"loss_rate": "丢包率",
"run_network": "运行网络",
"stop_network": "停止网络",
}
}
function saveLocaleToLocalStorage(locale: string) {
localStorage.setItem("locale", locale);
}
export function loadLocaleFromLocalStorage(): 'en' | 'cn' {
const v = localStorage.getItem("locale")
if (v === 'en' || v === 'cn') {
return v;
} else {
return 'en';
}
}
export const i18n = createI18n({
legacy: false,
locale: 'en', // set locale
fallbackLocale: 'cn', // set fallback locale
messages,
})
export function changeLocale(locale: 'en' | 'cn') {
i18n.global.locale.value = locale;
saveLocaleToLocalStorage(locale);
}
const app = createApp(App);
app.use(i18n, { useScope: 'global' })
app.use(pinia)
app.use(PrimeVue);
app.use(ToastService);
app.mount("#app");
export const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", component: App }]
});

View File

@@ -0,0 +1,33 @@
@layer tailwind-base, primevue, tailwind-utilities;
@layer tailwind-base {
@tailwind base;
}
@layer tailwind-utilities {
@tailwind components;
@tailwind utilities;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 12px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: white;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.card {
background: var(--surface-card);
padding: 2rem;
border-radius: 10px;
margin-bottom: 1rem;
}

7
easytier-gui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,31 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from 'unplugin-vue-components/vite';
import { PrimeVueResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [
vue(),
Components({
dts: true,
resolvers: [
PrimeVueResolver()
]
})],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

1663
easytier-gui/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

166
easytier/Cargo.toml Normal file
View File

@@ -0,0 +1,166 @@
[package]
name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/KKRainbow/EasyTier"
repository = "https://github.com/KKRainbow/EasyTier"
version = "0.1.2"
edition = "2021"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"]
rust-version = "1.75"
license-file = "LICENSE"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "easytier-core"
path = "src/easytier-core.rs"
[[bin]]
name = "easytier-cli"
path = "src/easytier-cli.rs"
test = false
[lib]
name = "easytier"
path = "src/lib.rs"
test = false
[dependencies]
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"local-time",
"time",
] }
tracing-appender = "0.2.3"
log = "0.4"
thiserror = "1.0"
auto_impl = "1.1.0"
crossbeam = "0.8.4"
time = "0.3"
toml = "0.8.12"
chrono = "0.4.35"
gethostname = "0.4.3"
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"] }
async-stream = "0.3.5"
async-trait = "0.1.74"
dashmap = "5.5.3"
timedmap = "=1.0.1"
# for full-path zero-copy
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
bytes = "1.5.0"
pin-project-lite = "0.2.13"
atomicbox = "0.4.0"
tachyonix = "0.2.1"
quinn = { version = "0.10.2" }
rustls = { version = "0.21.0", features = ["dangerous_configuration"] }
rcgen = "0.11.1"
# for tap device
tun = { version = "0.6.1", features = ["async"] }
# for net ns
nix = { version = "0.27", features = ["sched", "socket", "ioctl"] }
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
# for ring tunnel
crossbeam-queue = "0.3"
once_cell = "1.18.0"
# for packet
rkyv = { "version" = "0.7.42", features = [
"validation",
"archive_le",
"strict",
"copy_unsafe",
"arbitrary_enum_discriminant",
] }
postcard = { "version" = "1.0.8", features = ["alloc"] }
# for rpc
tonic = "0.10"
prost = "0.12"
anyhow = "1.0"
tarpc = { version = "0.32", features = ["tokio1", "serde1"] }
url = { version = "2.5", features = ["serde"] }
percent-encoding = "2.3.1"
# for tun packet
byteorder = "1.5.0"
# for proxy
cidr = { version = "0.2.2", features = ["serde"] }
socket2 = "0.5.5"
# for hole punching
stun_codec = "0.3.4"
bytecodec = "0.4.15"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
pnet = { version = "0.34.0", features = ["serde"] }
public-ip = { version = "0.2", features = ["default"] }
clap = { version = "4.4.8", features = ["unicode", "derive", "wrap_help"] }
async-recursion = "1.0.5"
network-interface = "1.1.1"
# for ospf route
pathfinding = "4.9.1"
# for encryption
boringtun = { version = "0.6.0" }
ring = { version = "0.16" }
bitflags = "2.5"
# for cli
tabled = "0.15.*"
humansize = "2.1.3"
base64 = "0.21.7"
derivative = "2.2.0"
mimalloc-rust = "0.2.1"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_Networking_WinSock",
"Win32_NetworkManagement_IpHelper",
"Win32_Foundation",
"Win32_System_IO",
] }
[build-dependencies]
tonic-build = "0.10"
[target.'cfg(windows)'.build-dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
zip = "0.6.6"
[dev-dependencies]
serial_test = "3.0.0"
rstest = "0.18.2"
defguard_wireguard_rs = "0.4.2"

1
easytier/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

1
easytier/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

1
easytier/README_CN.md Symbolic link
View File

@@ -0,0 +1 @@
../README_CN.md

1
easytier/assets Symbolic link
View File

@@ -0,0 +1 @@
../assets

View File

@@ -75,7 +75,7 @@ impl WindowsBuild {
pub fn check_for_win() {
// add third_party dir to link search path
println!("cargo:rustc-link-search=native=third_party/");
println!("cargo:rustc-link-search=native=easytier/third_party/");
let protoc_path = if let Some(o) = Self::check_protoc_exist() {
println!("cargo:info=use os exist protoc: {:?}", o);
o

View File

@@ -142,3 +142,36 @@ message GetGlobalPeerMapResponse {
service PeerCenterRpc {
rpc GetGlobalPeerMap (GetGlobalPeerMapRequest) returns (GetGlobalPeerMapResponse);
}
message VpnPortalInfo {
string vpn_type = 1;
string client_config = 2;
repeated string connected_clients = 3;
}
message GetVpnPortalInfoRequest {}
message GetVpnPortalInfoResponse {
VpnPortalInfo vpn_portal_info = 1;
}
service VpnPortalRpc {
rpc GetVpnPortalInfo (GetVpnPortalInfoRequest) returns (GetVpnPortalInfoResponse);
}
message HandshakeRequest {
uint32 magic = 1;
uint32 my_peer_id = 2;
uint32 version = 3;
repeated string features = 4;
string network_name = 5;
bytes network_secret_digrest = 6;
}
message TaRpcPacket {
uint32 from_peer = 1;
uint32 to_peer = 2;
uint32 service_id = 3;
uint32 transact_id = 4;
bool is_req = 5;
bytes content = 6;
}

View File

@@ -19,8 +19,6 @@ use windows_sys::{
},
};
use crate::tunnels::common::get_interface_name_by_ip;
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
let handle = socket.as_raw_socket() as SOCKET;
@@ -62,6 +60,16 @@ pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
Ok(())
}
pub fn interface_count() -> io::Result<usize> {
let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
io::Error::new(
ErrorKind::NotFound,
format!("Failed to get interfaces. error: {}", e),
)
})?;
Ok(ifaces.len())
}
pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
io::Error::new(
@@ -132,13 +140,14 @@ pub fn set_ip_unicast_if<S: AsRawSocket>(
pub fn setup_socket_for_win<S: AsRawSocket>(
socket: &S,
bind_addr: &SocketAddr,
bind_dev: Option<String>,
is_udp: bool,
) -> io::Result<()> {
if is_udp {
disable_connection_reset(socket)?;
}
if let Some(iface) = get_interface_name_by_ip(&bind_addr.ip()) {
if let Some(iface) = bind_dev {
set_ip_unicast_if(socket, bind_addr, iface.as_str())?;
}

View File

@@ -6,9 +6,12 @@ use std::{
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::tunnel::generate_digest_from_str;
#[auto_impl::auto_impl(Box, &)]
pub trait ConfigLoader: Send + Sync {
fn get_id(&self) -> uuid::Uuid;
fn set_id(&self, id: uuid::Uuid);
fn get_inst_name(&self) -> String;
fn set_inst_name(&self, name: String);
@@ -42,20 +45,58 @@ pub trait ConfigLoader: Send + Sync {
fn get_rpc_portal(&self) -> Option<SocketAddr>;
fn set_rpc_portal(&self, addr: SocketAddr);
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
fn get_flags(&self) -> Flags;
fn set_flags(&self, flags: Flags);
fn dump(&self) -> String;
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub type NetworkSecretDigest = [u8; 32];
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct NetworkIdentity {
pub network_name: String,
pub network_secret: String,
pub network_secret: Option<String>,
#[serde(skip)]
pub network_secret_digest: Option<NetworkSecretDigest>,
}
impl PartialEq for NetworkIdentity {
fn eq(&self, other: &Self) -> bool {
if self.network_name != other.network_name {
return false;
}
if self.network_secret.is_some()
&& other.network_secret.is_some()
&& self.network_secret != other.network_secret
{
return false;
}
if self.network_secret_digest.is_some()
&& other.network_secret_digest.is_some()
&& self.network_secret_digest != other.network_secret_digest
{
return false;
}
return true;
}
}
impl NetworkIdentity {
pub fn new(network_name: String, network_secret: String) -> Self {
let mut network_secret_digest = [0u8; 32];
generate_digest_from_str(&network_name, &network_secret, &mut network_secret_digest);
NetworkIdentity {
network_name,
network_secret,
network_secret: Some(network_secret),
network_secret_digest: Some(network_secret_digest),
}
}
@@ -87,11 +128,29 @@ pub struct ConsoleLoggerConfig {
pub level: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct VpnPortalConfig {
pub client_cidr: cidr::Ipv4Cidr,
pub wireguard_listen: SocketAddr,
}
// Flags is used to control the behavior of the program
#[derive(derivative::Derivative, Deserialize, Serialize)]
#[derivative(Debug, Clone, PartialEq, Default)]
pub struct Flags {
#[derivative(Default(value = "\"tcp\".to_string()"))]
pub default_protocol: String,
#[derivative(Default(value = "true"))]
pub enable_encryption: bool,
#[derivative(Default(value = "true"))]
pub enable_ipv6: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct Config {
netns: Option<String>,
instance_name: Option<String>,
instance_id: Option<String>,
instance_id: Option<uuid::Uuid>,
ipv4: Option<String>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
@@ -103,6 +162,10 @@ struct Config {
console_logger: Option<ConsoleLoggerConfig>,
rpc_portal: Option<SocketAddr>,
vpn_portal_config: Option<VpnPortalConfig>,
flags: Option<Flags>,
}
#[derive(Debug, Clone)]
@@ -222,21 +285,17 @@ impl ConfigLoader for TomlConfigLoader {
let mut locked_config = self.config.lock().unwrap();
if locked_config.instance_id.is_none() {
let id = uuid::Uuid::new_v4();
locked_config.instance_id = Some(id.to_string());
locked_config.instance_id = Some(id);
id
} else {
uuid::Uuid::parse_str(locked_config.instance_id.as_ref().unwrap())
.with_context(|| {
format!(
"failed to parse instance id as uuid: {}, you can use this id: {}",
locked_config.instance_id.as_ref().unwrap(),
uuid::Uuid::new_v4()
)
})
.unwrap()
locked_config.instance_id.as_ref().unwrap().clone()
}
}
fn set_id(&self, id: uuid::Uuid) {
self.config.lock().unwrap().instance_id = Some(id);
}
fn get_network_identity(&self) -> NetworkIdentity {
self.config
.lock()
@@ -314,6 +373,26 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().rpc_portal = Some(addr);
}
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig> {
self.config.lock().unwrap().vpn_portal_config.clone()
}
fn set_vpn_portal_config(&self, config: VpnPortalConfig) {
self.config.lock().unwrap().vpn_portal_config = Some(config);
}
fn get_flags(&self) -> Flags {
self.config
.lock()
.unwrap()
.flags
.clone()
.unwrap_or_default()
}
fn set_flags(&self, flags: Flags) {
self.config.lock().unwrap().flags = Some(flags);
}
fn dump(&self) -> String {
toml::to_string_pretty(&*self.config.lock().unwrap()).unwrap()
}

View File

@@ -1,7 +1,3 @@
pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1;
pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 60;
pub const DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC: u64 = 60;
macro_rules! define_global_var {
($name:ident, $type:ty, $init:expr) => {
pub static $name: once_cell::sync::Lazy<tokio::sync::Mutex<$type>> =

View File

@@ -2,7 +2,7 @@ use std::{io, result};
use thiserror::Error;
use crate::tunnels;
use crate::tunnel;
use super::PeerId;
@@ -13,7 +13,7 @@ pub enum Error {
#[error("rust tun error {0}")]
TunError(#[from] tun::Error),
#[error("tunnel error {0}")]
TunnelError(#[from] tunnels::TunnelError),
TunnelError(#[from] tunnel::TunnelError),
#[error("Peer has no conn, PeerId: {0}")]
PeerNoConnectionError(PeerId),
#[error("RouteError: {0:?}")]
@@ -38,6 +38,15 @@ pub enum Error {
Unknown,
#[error("anyhow error: {0}")]
AnyhowError(#[from] anyhow::Error),
#[error("wait resp error: {0}")]
WaitRespError(String),
#[error("message decode error: {0}")]
MessageDecodeError(String),
#[error("secret key error: {0}")]
SecretKeyError(String),
}
pub type Result<T> = result::Result<T, Error>;

View File

@@ -1,10 +1,14 @@
use std::sync::{Arc, Mutex};
use std::collections::hash_map::DefaultHasher;
use std::{
hash::Hasher,
sync::{Arc, Mutex},
};
use crate::rpc::PeerConnInfo;
use crossbeam::atomic::AtomicCell;
use super::{
config::ConfigLoader,
config::{ConfigLoader, Flags},
netns::NetNS,
network::IPCollector,
stun::{StunInfoCollector, StunInfoCollectorTrait},
@@ -13,7 +17,7 @@ use super::{
pub type NetworkIdentity = crate::common::config::NetworkIdentity;
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum GlobalCtxEvent {
TunDeviceReady(String),
@@ -28,6 +32,9 @@ pub enum GlobalCtxEvent {
Connecting(url::Url),
ConnectError(String, String), // (dst, error message)
VpnPortalClientConnected(String, String), // (portal, client ip)
VpnPortalClientDisconnected(String, String), // (portal, client ip)
}
type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
@@ -192,6 +199,31 @@ impl GlobalCtx {
pub fn add_running_listener(&self, url: url::Url) {
self.running_listeners.lock().unwrap().push(url);
}
pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> {
self.config.get_vpn_portal_config().map(|x| x.client_cidr)
}
pub fn get_flags(&self) -> Flags {
self.config.get_flags()
}
pub fn get_128_key(&self) -> [u8; 16] {
let mut key = [0u8; 16];
let secret = self
.config
.get_network_identity()
.network_secret
.unwrap_or_default();
// fill key according to network secret
let mut hasher = DefaultHasher::new();
hasher.write(secret.as_bytes());
key[0..8].copy_from_slice(&hasher.finish().to_be_bytes());
hasher.write(&key[0..8]);
key[8..16].copy_from_slice(&hasher.finish().to_be_bytes());
hasher.write(&key[0..16]);
key
}
}
#[cfg(test)]

View File

@@ -6,7 +6,7 @@ use tokio::process::Command;
use super::error::Error;
#[async_trait]
pub trait IfConfiguerTrait {
pub trait IfConfiguerTrait: Send + Sync {
async fn add_ipv4_route(
&self,
name: &str,

120
easytier/src/common/mod.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::{
fmt::Debug,
future,
sync::{Arc, Mutex},
};
use tokio::task::JoinSet;
use tracing::Instrument;
pub mod config;
pub mod constants;
pub mod error;
pub mod global_ctx;
pub mod ifcfg;
pub mod netns;
pub mod network;
pub mod rkyv_util;
pub mod stun;
pub mod stun_codec_ext;
pub fn get_logger_timer<F: time::formatting::Formattable>(
format: F,
) -> tracing_subscriber::fmt::time::OffsetTime<F> {
unsafe {
time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound)
};
let local_offset = time::UtcOffset::current_local_offset()
.unwrap_or(time::UtcOffset::from_whole_seconds(0).unwrap());
tracing_subscriber::fmt::time::OffsetTime::new(local_offset, format)
}
pub fn get_logger_timer_rfc3339(
) -> tracing_subscriber::fmt::time::OffsetTime<time::format_description::well_known::Rfc3339> {
get_logger_timer(time::format_description::well_known::Rfc3339)
}
pub type PeerId = u32;
pub fn new_peer_id() -> PeerId {
rand::random()
}
pub fn join_joinset_background<T: Debug + Send + Sync + 'static>(
js: Arc<Mutex<JoinSet<T>>>,
origin: String,
) {
let js = Arc::downgrade(&js);
tokio::spawn(
async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
if js.weak_count() == 0 {
tracing::info!("joinset task exit");
break;
}
future::poll_fn(|cx| {
tracing::debug!("try join joinset tasks");
let Some(js) = js.upgrade() else {
return std::task::Poll::Ready(());
};
let mut js = js.lock().unwrap();
while !js.is_empty() {
let ret = js.poll_join_next(cx);
if ret.is_pending() {
return std::task::Poll::Pending;
}
}
std::task::Poll::Ready(())
})
.await;
}
}
.instrument(tracing::info_span!(
"join_joinset_background",
origin = origin
)),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_join_joinset_backgroud() {
let js = Arc::new(Mutex::new(JoinSet::<()>::new()));
join_joinset_background(js.clone(), "TEST".to_owned());
js.try_lock().unwrap().spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
});
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(js.try_lock().unwrap().is_empty());
for _ in 0..5 {
js.try_lock().unwrap().spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
});
tokio::task::yield_now().await;
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
for _ in 0..5 {
js.try_lock().unwrap().spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
});
tokio::task::yield_now().await;
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(js.try_lock().unwrap().is_empty());
let weak_js = Arc::downgrade(&js);
drop(js);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert_eq!(weak_js.weak_count(), 0);
}
}

View File

@@ -7,7 +7,9 @@ use tokio::{
task::JoinSet,
};
use super::{constants::DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC, netns::NetNS};
use super::netns::NetNS;
pub const CACHED_IP_LIST_TIMEOUT_SEC: u64 = 60;
struct InterfaceFilter {
iface: NetworkInterface,
@@ -15,33 +17,37 @@ struct InterfaceFilter {
#[cfg(target_os = "linux")]
impl InterfaceFilter {
async fn is_iface_bridge(&self) -> bool {
let path = format!("/sys/class/net/{}/bridge", self.iface.name);
async fn is_tun_tap_device(&self) -> bool {
let path = format!("/sys/class/net/{}/tun_flags", self.iface.name);
tokio::fs::metadata(&path).await.is_ok()
}
async fn is_iface_phsical(&self) -> bool {
let path = format!("/sys/class/net/{}/device", self.iface.name);
tokio::fs::metadata(&path).await.is_ok()
async fn has_valid_ip(&self) -> bool {
self.iface
.ips
.iter()
.map(|ip| ip.ip())
.any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast())
}
async fn filter_iface(&self) -> bool {
tracing::trace!(
"filter linux iface: {:?}, is_point_to_point: {}, is_loopback: {}, is_up: {}, is_lower_up: {}, is_bridge: {}, is_physical: {}",
"filter linux iface: {:?}, is_point_to_point: {}, is_loopback: {}, is_up: {}, is_lower_up: {}, is_tun: {}, has_valid_ip: {}",
self.iface,
self.iface.is_point_to_point(),
self.iface.is_loopback(),
self.iface.is_up(),
self.iface.is_lower_up(),
self.is_iface_bridge().await,
self.is_iface_phsical().await,
self.is_tun_tap_device().await,
self.has_valid_ip().await
);
!self.iface.is_point_to_point()
&& !self.iface.is_loopback()
&& self.iface.is_up()
&& self.iface.is_lower_up()
&& (self.is_iface_bridge().await || self.is_iface_phsical().await)
&& !self.is_tun_tap_device().await
&& self.has_valid_ip().await
}
}
@@ -85,7 +91,22 @@ impl InterfaceFilter {
#[cfg(target_os = "windows")]
impl InterfaceFilter {
async fn filter_iface(&self) -> bool {
!self.iface.is_point_to_point() && !self.iface.is_loopback() && self.iface.is_up()
tracing::debug!(
"iface_name: {:?}, p2p: {:?}, is_up: {:?}, iface: {:?}",
self.iface.name,
self.iface.is_point_to_point(),
self.iface.is_up(),
self.iface
);
!self.iface.is_point_to_point()
&& !self.iface.is_loopback()
&& self
.iface
.ips
.iter()
.map(|ip| ip.ip())
.any(|ip| !ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast())
&& self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false)
}
}
@@ -143,10 +164,8 @@ impl IPCollector {
loop {
let ip_addrs = Self::do_collect_ip_addrs(true, net_ns.clone()).await;
*cached_ip_list.write().await = ip_addrs;
tokio::time::sleep(std::time::Duration::from_secs(
DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC,
))
.await;
tokio::time::sleep(std::time::Duration::from_secs(CACHED_IP_LIST_TIMEOUT_SEC))
.await;
}
});
}
@@ -154,6 +173,25 @@ impl IPCollector {
return self.cached_ip_list.read().await.deref().clone();
}
pub async fn collect_interfaces(net_ns: NetNS) -> Vec<NetworkInterface> {
let _g = net_ns.guard();
let ifaces = pnet::datalink::interfaces();
let mut ret = vec![];
for iface in ifaces {
let f = InterfaceFilter {
iface: iface.clone(),
};
if !f.filter_iface().await {
continue;
}
ret.push(iface);
}
ret
}
#[tracing::instrument(skip(net_ns))]
async fn do_collect_ip_addrs(with_public: bool, net_ns: NetNS) -> GetIpListResponse {
let mut ret = crate::rpc::peer::GetIpListResponse::new();
@@ -170,17 +208,9 @@ impl IPCollector {
}
}
let ifaces = Self::collect_interfaces(net_ns.clone()).await;
let _g = net_ns.guard();
let ifaces = pnet::datalink::interfaces();
for iface in ifaces {
let f = InterfaceFilter {
iface: iface.clone(),
};
if !f.filter_iface().await {
continue;
}
for ip in iface.ips {
let ip: std::net::IpAddr = ip.ip();
if ip.is_loopback() || ip.is_multicast() {

View File

@@ -87,7 +87,7 @@ impl Stun {
pub fn new(stun_server: SocketAddr) -> Self {
Self {
stun_server,
req_repeat: 5,
req_repeat: 2,
resp_timeout: Duration::from_millis(3000),
}
}
@@ -127,7 +127,7 @@ impl Stun {
continue;
};
tracing::info!(b = ?&udp_buf[..len], ?tids, ?remote_addr, ?stun_host, "recv stun response, msg: {:#?}", msg);
tracing::debug!(b = ?&udp_buf[..len], ?tids, ?remote_addr, ?stun_host, "recv stun response, msg: {:#?}", msg);
if msg.class() != MessageClass::SuccessResponse
|| msg.method() != BINDING
@@ -194,7 +194,7 @@ impl Stun {
changed_addr
}
#[tracing::instrument(ret, err, level = Level::INFO)]
#[tracing::instrument(ret, err, level = Level::DEBUG)]
pub async fn bind_request(
&self,
source_port: u16,
@@ -208,6 +208,7 @@ impl Stun {
let mut tids = vec![];
for _ in 0..self.req_repeat {
let tid = rand::random::<u32>();
// let tid = 1;
let mut buf = [0u8; 28];
// memset buf
unsafe { std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len()) };
@@ -250,7 +251,7 @@ impl Stun {
real_port_changed,
};
tracing::info!(
tracing::debug!(
?stun_host,
?recv_addr,
?changed_socket_addr,
@@ -300,7 +301,7 @@ impl UdpNatTypeDetector {
let ret = stun.bind_request(source_port, true, true).await;
if let Ok(resp) = ret {
if !resp.real_ip_changed || !resp.real_port_changed {
tracing::info!(
tracing::debug!(
?server_ip,
?ret,
"stun bind request return with unchanged ip and port"
@@ -311,7 +312,7 @@ impl UdpNatTypeDetector {
}
ret_test2 = ret.ok();
ret_test3 = stun.bind_request(source_port, false, true).await.ok();
tracing::info!(?ret_test3, "stun bind request with changed port");
tracing::debug!(?ret_test3, "stun bind request with changed port");
succ = true;
break;
}
@@ -320,7 +321,7 @@ impl UdpNatTypeDetector {
return NatType::Unknown;
}
tracing::info!(
tracing::debug!(
?ret_test1_1,
?ret_test1_2,
?ret_test2,
@@ -431,9 +432,9 @@ impl StunInfoCollector {
// stun server cross nation may return a external ip address with high latency and loss rate
vec![
"stun.miwifi.com:3478".to_string(),
"stun.qq.com:3478".to_string(),
// "stun.chat.bilibili.com:3478".to_string(), // bilibili's stun server doesn't repond to change_ip and change_port
"fwa.lifesizecloud.com:3478".to_string(),
"stun.chat.bilibili.com:3478".to_string(), // bilibili's stun server doesn't repond to change_ip and change_port
"stun.cloudflare.com:3478".to_string(),
"stun.syncthing.net:3478".to_string(),
"stun.isp.net.au:3478".to_string(),
"stun.nextcloud.com:3478".to_string(),
"stun.freeswitch.org:3478".to_string(),
@@ -443,9 +444,6 @@ impl StunInfoCollector {
"stun.radiojar.com:3478".to_string(),
"stun.sonetel.com:3478".to_string(),
"stun.voipgate.com:3478".to_string(),
"stun.counterpath.com:3478".to_string(),
"180.235.108.91:3478".to_string(),
"193.22.2.248:3478".to_string(),
]
}
@@ -511,30 +509,17 @@ mod tests {
#[tokio::test]
async fn test_stun_bind_request() {
// miwifi / qq seems not correctly responde to change_ip and change_port, they always try to change the src ip and port.
let mut ips = HostResolverIter::new(vec!["stun1.l.google.com:19302".to_string()]);
let stun = Stun::new(ips.next().await.unwrap());
// let stun = Stun::new("180.235.108.91:3478".to_string());
// let stun = Stun::new("193.22.2.248:3478".to_string());
// let stun = Stun::new("stun.chat.bilibili.com:3478".to_string());
// let stun = Stun::new("stun.miwifi.com:3478".to_string());
// github actions are port restricted nat, so we only test last one.
// let rs = stun.bind_request(12345, true, true).await.unwrap();
// assert!(rs.ip_changed);
// assert!(rs.port_changed);
// let rs = stun.bind_request(12345, true, false).await.unwrap();
// assert!(rs.ip_changed);
// assert!(!rs.port_changed);
// let rs = stun.bind_request(12345, false, true).await.unwrap();
// assert!(!rs.ip_changed);
// assert!(rs.port_changed);
let rs = stun.bind_request(12345, false, false).await.unwrap();
assert!(!rs.ip_changed);
assert!(!rs.port_changed);
// let mut ips = HostResolverIter::new(vec!["stun1.l.google.com:19302".to_string()]);
let mut ips_ = HostResolverIter::new(vec!["stun.canets.org:3478".to_string()]);
let mut ips = vec![];
while let Some(ip) = ips_.next().await {
ips.push(ip);
}
println!("ip: {:?}", ips);
for ip in ips.iter() {
let stun = Stun::new(ip.clone());
let _rs = stun.bind_request(12345, true, true).await;
}
}
#[tokio::test]

View File

@@ -1,23 +1,22 @@
// try connect peers directly, with either its public ip or lan ip
use std::sync::Arc;
use std::{net::SocketAddr, sync::Arc};
use crate::{
common::{
constants::{self, DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC},
error::Error,
global_ctx::ArcGlobalCtx,
PeerId,
},
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
peers::{peer_manager::PeerManager, peer_rpc::PeerRpcManager},
};
use crate::rpc::{peer::GetIpListResponse, PeerConnInfo};
use tokio::{task::JoinSet, time::timeout};
use tracing::Instrument;
use url::Host;
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;
@@ -76,10 +75,25 @@ impl DirectConnectorManagerRpcServer {
#[derive(Hash, Eq, PartialEq, Clone)]
struct DstBlackListItem(PeerId, String);
#[derive(Hash, Eq, PartialEq, Clone)]
struct DstSchemeBlackListItem(PeerId, String);
struct DirectConnectorManagerData {
global_ctx: ArcGlobalCtx,
peer_manager: Arc<PeerManager>,
dst_blacklist: timedmap::TimedMap<DstBlackListItem, ()>,
dst_sceme_blacklist: timedmap::TimedMap<DstSchemeBlackListItem, ()>,
}
impl DirectConnectorManagerData {
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Self {
Self {
global_ctx,
peer_manager,
dst_blacklist: timedmap::TimedMap::new(),
dst_sceme_blacklist: timedmap::TimedMap::new(),
}
}
}
impl std::fmt::Debug for DirectConnectorManagerData {
@@ -101,11 +115,7 @@ impl DirectConnectorManager {
pub fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Self {
Self {
global_ctx: global_ctx.clone(),
data: Arc::new(DirectConnectorManagerData {
global_ctx,
peer_manager,
dst_blacklist: timedmap::TimedMap::new(),
}),
data: Arc::new(DirectConnectorManagerData::new(global_ctx, peer_manager)),
tasks: JoinSet::new(),
}
}
@@ -117,7 +127,7 @@ impl DirectConnectorManager {
pub fn run_as_server(&mut self) {
self.data.peer_manager.get_peer_rpc_mgr().run_service(
constants::DIRECT_CONNECTOR_SERVICE_ID,
DIRECT_CONNECTOR_SERVICE_ID,
DirectConnectorManagerRpcServer::new(self.global_ctx.clone()).serve(),
);
}
@@ -163,7 +173,7 @@ impl DirectConnectorManager {
return Err(Error::UrlInBlacklist);
}
let connector = create_connector_by_url(&addr, data.global_ctx.get_ip_collector()).await?;
let connector = create_connector_by_url(&addr, &data.global_ctx).await?;
let (peer_id, conn_id) = timeout(
std::time::Duration::from_secs(5),
data.peer_manager.try_connect(connector),
@@ -193,7 +203,7 @@ impl DirectConnectorManager {
data: Arc<DirectConnectorManagerData>,
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) {
@@ -208,11 +218,119 @@ impl DirectConnectorManager {
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
);
}
return Err(e);
} else {
log::info!("try_connect_to_ip success, peer_id: {}", dst_peer_id);
return Ok(());
}
}
#[tracing::instrument]
async fn do_try_direct_connect_internal(
data: Arc<DirectConnectorManagerData>,
dst_peer_id: PeerId,
ip_list: GetIpListResponse,
) -> Result<(), Error> {
let enable_ipv6 = data.global_ctx.get_flags().enable_ipv6;
let available_listeners = ip_list
.listeners
.iter()
.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(),
))
})
.filter(|l| enable_ipv6 || !matches!(l.host().unwrap().to_owned(), Host::Ipv6(_)))
.collect::<Vec<_>>();
let mut listener = available_listeners.get(0).ok_or(anyhow::anyhow!(
"peer {} have no valid listener",
dst_peer_id
))?;
// if have default listener, use it first
listener = available_listeners
.iter()
.find(|l| l.scheme() == data.global_ctx.get_flags().default_protocol)
.unwrap_or(listener);
let mut tasks = JoinSet::new();
let listener_host = listener.socket_addrs(|| None).unwrap().pop();
match listener_host {
Some(SocketAddr::V4(_)) => {
ip_list.interface_ipv4s.iter().for_each(|ip| {
let mut addr = (*listener).clone();
if addr.set_host(Some(ip.as_str())).is_ok() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
}
});
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(),
));
}
}
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() {
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr.to_string(),
));
}
});
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(),
));
}
}
p => {
tracing::error!(?p, ?listener, "failed to parse ip version from listener");
}
}
let mut has_succ = false;
while let Some(ret) = tasks.join_next().await {
if let Err(e) = ret {
log::error!("join direct connect task failed: {:?}", e);
} else if let Ok(Ok(_)) = ret {
has_succ = true;
}
}
if !has_succ {
data.dst_sceme_blacklist.insert(
DstSchemeBlackListItem(dst_peer_id.clone(), listener.scheme().to_string()),
(),
std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC),
);
}
Ok(())
}
#[tracing::instrument]
async fn do_try_direct_connect(
data: Arc<DirectConnectorManagerData>,
@@ -240,67 +358,37 @@ impl DirectConnectorManager {
})
.await?;
let available_listeners = ip_list
.listeners
.iter()
.filter_map(|l| if l.scheme() != "ring" { Some(l) } else { None })
.collect::<Vec<_>>();
let listener = available_listeners
.get(0)
.ok_or(anyhow::anyhow!("peer {} have no listener", dst_peer_id))?;
let mut tasks = JoinSet::new();
ip_list.interface_ipv4s.iter().for_each(|ip| {
let addr = format!(
"{}://{}:{}",
listener.scheme(),
ip,
listener.port().unwrap_or(11010)
);
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr,
));
});
let addr = format!(
"{}://{}:{}",
listener.scheme(),
ip_list.public_ipv4.clone(),
listener.port().unwrap_or(11010)
);
tasks.spawn(Self::try_connect_to_ip(
data.clone(),
dst_peer_id.clone(),
addr,
));
while let Some(ret) = tasks.join_next().await {
if let Err(e) = ret {
log::error!("join direct connect task failed: {:?}", e);
}
}
Ok(())
Self::do_try_direct_connect_internal(data, dst_peer_id, ip_list).await
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::{
connector::direct::DirectConnectorManager,
connector::direct::{
DirectConnectorManager, DirectConnectorManagerData, DstBlackListItem,
DstSchemeBlackListItem,
},
instance::listeners::ListenerManager,
peers::tests::{
connect_peer_manager, create_mock_peer_manager, wait_route_appear,
wait_route_appear_with_cost,
},
tunnels::tcp_tunnel::TcpTunnelListener,
rpc::peer::GetIpListResponse,
};
#[rstest::rstest]
#[tokio::test]
async fn direct_connector_basic_test() {
async fn direct_connector_basic_test(
#[values("tcp", "udp", "wg")] proto: &str,
#[values("true", "false")] ipv6: bool,
) {
if ipv6 && proto != "udp" {
return;
}
let p_a = create_mock_peer_manager().await;
let p_b = create_mock_peer_manager().await;
let p_c = create_mock_peer_manager().await;
@@ -315,14 +403,20 @@ mod tests {
dm_a.run_as_client();
dm_c.run_as_server();
if !ipv6 {
let port = if proto == "wg" { 11040 } else { 11041 };
p_c.get_global_ctx().config.set_listeners(vec![format!(
"{}://0.0.0.0:{}",
proto, port
)
.parse()
.unwrap()]);
}
let mut f = p_c.get_global_ctx().config.get_flags();
f.enable_ipv6 = ipv6;
p_c.get_global_ctx().config.set_flags(f);
let mut lis_c = ListenerManager::new(p_c.get_global_ctx(), p_c.clone());
lis_c
.add_listener(TcpTunnelListener::new(
"tcp://0.0.0.0:11040".parse().unwrap(),
))
.await
.unwrap();
lis_c.prepare_listeners().await.unwrap();
lis_c.run().await.unwrap();
@@ -330,4 +424,31 @@ mod tests {
.await
.unwrap();
}
#[tokio::test]
async fn direct_connector_scheme_blacklist() {
let p_a = create_mock_peer_manager().await;
let data = Arc::new(DirectConnectorManagerData::new(
p_a.get_global_ctx(),
p_a.clone(),
));
let mut ip_list = GetIpListResponse::new();
ip_list
.listeners
.push("tcp://127.0.0.1:10222".parse().unwrap());
ip_list.interface_ipv4s.push("127.0.0.1".to_string());
DirectConnectorManager::do_try_direct_connect_internal(data.clone(), 1, ip_list.clone())
.await
.unwrap();
assert!(data
.dst_sceme_blacklist
.contains(&DstSchemeBlackListItem(1, "tcp".into())));
assert!(data
.dst_blacklist
.contains(&DstBlackListItem(1, ip_list.listeners[0].to_string())));
}
}

View File

@@ -1,5 +1,6 @@
use std::{collections::BTreeSet, sync::Arc};
use anyhow::Context;
use dashmap::{DashMap, DashSet};
use tokio::{
sync::{broadcast::Receiver, mpsc, Mutex},
@@ -7,7 +8,12 @@ use tokio::{
time::timeout,
};
use crate::{common::PeerId, peers::peer_conn::PeerConnId, rpc as easytier_rpc};
use crate::{
common::PeerId,
peers::peer_conn::PeerConnId,
rpc as easytier_rpc,
tunnel::{IpVersion, TunnelConnector},
};
use crate::{
common::{
@@ -21,13 +27,13 @@ use crate::{
connector_manage_rpc_server::ConnectorManageRpc, Connector, ConnectorStatus,
ListConnectorRequest, ManageConnectorRequest,
},
tunnels::{Tunnel, TunnelConnector},
use_global_var,
};
use super::create_connector_by_url;
type ConnectorMap = Arc<DashMap<String, Box<dyn TunnelConnector + Send + Sync>>>;
type MutexConnector = Arc<Mutex<Box<dyn TunnelConnector>>>;
type ConnectorMap = Arc<DashMap<String, MutexConnector>>;
#[derive(Debug, Clone)]
struct ReconnResult {
@@ -81,16 +87,17 @@ impl ManualConnectorManager {
pub fn add_connector<T>(&self, connector: T)
where
T: TunnelConnector + Send + Sync + 'static,
T: TunnelConnector + 'static,
{
log::info!("add_connector: {}", connector.remote_url());
self.data
.connectors
.insert(connector.remote_url().into(), Box::new(connector));
self.data.connectors.insert(
connector.remote_url().into(),
Arc::new(Mutex::new(Box::new(connector))),
);
}
pub async fn add_connector_by_url(&self, url: &str) -> Result<(), Error> {
self.add_connector(create_connector_by_url(url, self.global_ctx.get_ip_collector()).await?);
self.add_connector(create_connector_by_url(url, &self.global_ctx).await?);
Ok(())
}
@@ -251,64 +258,109 @@ impl ManualConnectorManager {
&all_urls - &curr_alive
}
async fn conn_reconnect_with_ip_version(
data: Arc<ConnectorManagerData>,
dead_url: String,
connector: MutexConnector,
ip_version: IpVersion,
) -> Result<ReconnResult, Error> {
let ip_collector = data.global_ctx.get_ip_collector();
let net_ns = data.net_ns.clone();
connector.lock().await.set_ip_version(ip_version);
set_bind_addr_for_peer_connector(
connector.lock().await.as_mut(),
ip_version == IpVersion::V4,
&ip_collector,
)
.await;
data.global_ctx.issue_event(GlobalCtxEvent::Connecting(
connector.lock().await.remote_url().clone(),
));
let _g = net_ns.guard();
log::info!("reconnect try connect... conn: {:?}", connector);
let tunnel = connector.lock().await.connect().await?;
log::info!("reconnect get tunnel succ: {:?}", tunnel);
assert_eq!(
dead_url,
tunnel.info().unwrap().remote_addr,
"info: {:?}",
tunnel.info()
);
let (peer_id, conn_id) = data.peer_manager.add_client_tunnel(tunnel).await?;
log::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url);
Ok(ReconnResult {
dead_url,
peer_id,
conn_id,
})
}
async fn conn_reconnect(
data: Arc<ConnectorManagerData>,
dead_url: String,
connector: Box<dyn TunnelConnector + Send + Sync>,
connector: MutexConnector,
) -> Result<ReconnResult, Error> {
let connector = Arc::new(Mutex::new(Some(connector)));
let net_ns = data.net_ns.clone();
log::info!("reconnect: {}", dead_url);
let connector_clone = connector.clone();
let data_clone = data.clone();
let url_clone = dead_url.clone();
let ip_collector = data.global_ctx.get_ip_collector();
let reconn_task = async move {
let mut locked = connector_clone.lock().await;
let conn = locked.as_mut().unwrap();
// TODO: should support set v6 here, use url in connector array
set_bind_addr_for_peer_connector(conn, true, &ip_collector).await;
data_clone
.global_ctx
.issue_event(GlobalCtxEvent::Connecting(conn.remote_url().clone()));
let _g = net_ns.guard();
log::info!("reconnect try connect... conn: {:?}", conn);
let tunnel = conn.connect().await?;
log::info!("reconnect get tunnel succ: {:?}", tunnel);
assert_eq!(
url_clone,
tunnel.info().unwrap().remote_addr,
"info: {:?}",
tunnel.info()
);
let (peer_id, conn_id) = data_clone.peer_manager.add_client_tunnel(tunnel).await?;
log::info!("reconnect succ: {} {} {}", peer_id, conn_id, url_clone);
Ok(ReconnResult {
dead_url: url_clone,
peer_id,
conn_id,
})
};
let ret = timeout(std::time::Duration::from_secs(1), reconn_task).await;
log::info!("reconnect: {} done, ret: {:?}", dead_url, ret);
if ret.is_err() || ret.as_ref().unwrap().is_err() {
data.global_ctx.issue_event(GlobalCtxEvent::ConnectError(
dead_url.clone(),
format!("{:?}", ret),
));
let mut ip_versions = vec![];
let u = url::Url::parse(&dead_url)
.with_context(|| format!("failed to parse connector url {:?}", dead_url))?;
if u.scheme() == "ring" {
ip_versions.push(IpVersion::Both);
} else {
let addrs = u.socket_addrs(|| Some(1000))?;
let mut has_ipv4 = false;
let mut has_ipv6 = false;
for addr in addrs {
if addr.is_ipv4() {
if !has_ipv4 {
ip_versions.insert(0, IpVersion::V4);
}
has_ipv4 = true;
} else if addr.is_ipv6() {
if !has_ipv6 {
ip_versions.push(IpVersion::V6);
}
has_ipv6 = true;
}
}
}
let conn = connector.lock().await.take().unwrap();
data.reconnecting.remove(&dead_url).unwrap();
data.connectors.insert(dead_url.clone(), conn);
let mut reconn_ret = Err(Error::AnyhowError(anyhow::anyhow!(
"cannot get ip from url"
)));
for ip_version in ip_versions {
let ret = timeout(
std::time::Duration::from_secs(1),
Self::conn_reconnect_with_ip_version(
data.clone(),
dead_url.clone(),
connector.clone(),
ip_version,
),
)
.await;
log::info!("reconnect: {} done, ret: {:?}", dead_url, ret);
ret?
if ret.is_ok() && ret.as_ref().unwrap().is_ok() {
reconn_ret = ret.unwrap();
break;
} else {
if ret.is_err() {
reconn_ret = Err(ret.unwrap_err().into());
} else if ret.as_ref().unwrap().is_err() {
reconn_ret = Err(ret.unwrap().unwrap_err());
}
}
}
data.reconnecting.remove(&dead_url).unwrap();
data.connectors.insert(dead_url.clone(), connector);
reconn_ret
}
}
@@ -359,7 +411,7 @@ mod tests {
use crate::{
peers::tests::create_mock_peer_manager,
set_global_var,
tunnels::{Tunnel, TunnelError},
tunnel::{Tunnel, TunnelError},
};
use super::*;
@@ -379,7 +431,7 @@ mod tests {
}
async fn connect(&mut self) -> Result<Box<dyn Tunnel>, TunnelError> {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
Err(TunnelError::CommonError("fake error".into()))
Err(TunnelError::InvalidPacket("fake error".into()))
}
}

View File

@@ -0,0 +1,111 @@
use std::{
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
sync::Arc,
};
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, network::IPCollector},
tunnel::{
check_scheme_and_get_socket_addr,
quic::QUICTunnelConnector,
ring::RingTunnelConnector,
tcp::TcpTunnelConnector,
udp::UdpTunnelConnector,
wireguard::{WgConfig, WgTunnelConnector},
TunnelConnector,
},
};
pub mod direct;
pub mod manual;
pub mod udp_hole_punch;
async fn set_bind_addr_for_peer_connector(
connector: &mut (impl TunnelConnector + ?Sized),
is_ipv4: bool,
ip_collector: &Arc<IPCollector>,
) {
let ips = ip_collector.collect_ip_addrs().await;
if is_ipv4 {
let mut bind_addrs = vec![];
for ipv4 in ips.interface_ipv4s {
let socket_addr = SocketAddrV4::new(ipv4.parse().unwrap(), 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();
bind_addrs.push(socket_addr);
}
connector.set_bind_addrs(bind_addrs);
}
let _ = connector;
}
pub async fn create_connector_by_url(
url: &str,
global_ctx: &ArcGlobalCtx,
) -> Result<Box<dyn TunnelConnector + 'static>, Error> {
let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?;
match url.scheme() {
"tcp" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "tcp")?;
let mut connector = TcpTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
return Ok(Box::new(connector));
}
"udp" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "udp")?;
let mut connector = UdpTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
return Ok(Box::new(connector));
}
"ring" => {
check_scheme_and_get_socket_addr::<uuid::Uuid>(&url, "ring")?;
let connector = RingTunnelConnector::new(url);
return Ok(Box::new(connector));
}
"quic" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "quic")?;
let mut connector = QUICTunnelConnector::new(url);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
return Ok(Box::new(connector));
}
"wg" => {
let dst_addr = check_scheme_and_get_socket_addr::<SocketAddr>(&url, "wg")?;
let nid = global_ctx.get_network_identity();
let wg_config = WgConfig::new_from_network_identity(
&nid.network_name,
&nid.network_secret.unwrap_or_default(),
);
let mut connector = WgTunnelConnector::new(url, wg_config);
set_bind_addr_for_peer_connector(
&mut connector,
dst_addr.is_ipv4(),
&global_ctx.get_ip_collector(),
)
.await;
return Ok(Box::new(connector));
}
_ => {
return Err(Error::InvalidUrl(url.into()));
}
}
}

View File

@@ -2,20 +2,20 @@ use std::{net::SocketAddr, sync::Arc};
use anyhow::Context;
use crossbeam::atomic::AtomicCell;
use rand::{seq::SliceRandom, Rng, SeedableRng};
use rand::{seq::SliceRandom, SeedableRng};
use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet};
use tracing::Instrument;
use crate::{
common::{
constants, error::Error, global_ctx::ArcGlobalCtx, rkyv_util::encode_to_bytes,
constants, error::Error, global_ctx::ArcGlobalCtx, join_joinset_background,
stun::StunInfoCollectorTrait, PeerId,
},
peers::peer_manager::PeerManager,
rpc::NatType,
tunnels::{
tunnel::{
common::setup_sokcet2,
udp_tunnel::{UdpPacket, UdpTunnelConnector, UdpTunnelListener},
udp::{new_hole_punch_packet, UdpTunnelConnector, UdpTunnelListener},
Tunnel, TunnelConnCounter, TunnelListener,
},
};
@@ -75,9 +75,15 @@ impl UdpHolePunchListener {
while let Ok(conn) = listener.accept().await {
last_connected_time_clone.store(std::time::Instant::now());
tracing::warn!(?conn, "udp hole punching listener got peer connection");
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");
}
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);
@@ -115,7 +121,7 @@ struct UdpHolePunchConnectorData {
struct UdpHolePunchRpcServer {
data: Arc<UdpHolePunchConnectorData>,
tasks: Arc<Mutex<JoinSet<()>>>,
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
}
#[tarpc::server]
@@ -140,18 +146,13 @@ impl UdpHolePunchService for UdpHolePunchRpcServer {
|| my_udp_nat_type == NatType::Restricted as i32
{
// send punch msg to local_mapped_addr for 3 seconds, 3.3 packet per second
self.tasks.lock().await.spawn(async move {
self.tasks.lock().unwrap().spawn(async move {
for _ in 0..10 {
tracing::info!(?local_mapped_addr, "sending hole punching packet");
// generate a 128 bytes vec with random data
let mut rng = rand::rngs::StdRng::from_entropy();
let mut buf = vec![0u8; 128];
rng.fill(&mut buf[..]);
let udp_packet = UdpPacket::new_hole_punch_packet(buf);
let udp_packet_bytes = encode_to_bytes::<_, 256>(&udp_packet);
let udp_packet = new_hole_punch_packet();
let _ = socket
.send_to(udp_packet_bytes.as_ref(), local_mapped_addr)
.send_to(&udp_packet.into_bytes(), local_mapped_addr)
.await;
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
}
@@ -164,10 +165,9 @@ impl UdpHolePunchService for UdpHolePunchRpcServer {
impl UdpHolePunchRpcServer {
pub fn new(data: Arc<UdpHolePunchConnectorData>) -> Self {
Self {
data,
tasks: Arc::new(Mutex::new(JoinSet::new())),
}
let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new()));
join_joinset_background(tasks.clone(), "UdpHolePunchRpcServer".to_owned());
Self { data, tasks }
}
async fn select_listener(&self) -> Option<(Arc<UdpSocket>, SocketAddr)> {
@@ -404,7 +404,7 @@ impl UdpHolePunchConnector {
let socket = UdpSocket::from_std(socket2_socket.into())?;
Ok(connector
.try_connect_with_socket(socket)
.try_connect_with_socket(socket, remote_mapped_addr)
.await
.with_context(|| "UdpTunnelConnector failed to connect remote")?)
}

View File

@@ -3,11 +3,14 @@
use std::{net::SocketAddr, vec};
use clap::{command, Args, Parser, Subcommand};
use rpc::vpn_portal_rpc_client::VpnPortalRpcClient;
use utils::{list_peer_route_pair, PeerRoutePair};
mod arch;
mod common;
mod rpc;
mod tunnels;
mod tunnel;
mod utils;
use crate::{
common::stun::{StunInfoCollector, UdpNatTypeDetector},
@@ -16,6 +19,7 @@ use crate::{
peer_center_rpc_client::PeerCenterRpcClient, peer_manage_rpc_client::PeerManageRpcClient,
*,
},
utils::{cost_to_str, float_to_str},
};
use humansize::format_size;
use tabled::settings::Style;
@@ -38,6 +42,7 @@ enum SubCommand {
Stun,
Route,
PeerCenter,
VpnPortal,
}
#[derive(Args, Debug)]
@@ -92,107 +97,6 @@ enum Error {
TonicRpcError(#[from] tonic::Status),
}
#[derive(Debug)]
struct PeerRoutePair {
route: Route,
peer: Option<PeerInfo>,
}
impl PeerRoutePair {
fn get_latency_ms(&self) -> Option<f64> {
let mut ret = u64::MAX;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret = ret.min(stats.latency_us);
}
if ret == u64::MAX {
None
} else {
Some(f64::from(ret as u32) / 1000.0)
}
}
fn get_rx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.rx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
fn get_tx_bytes(&self) -> Option<u64> {
let mut ret = 0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(stats) = &conn.stats else {
continue;
};
ret += stats.tx_bytes;
}
if ret == 0 {
None
} else {
Some(ret)
}
}
fn get_loss_rate(&self) -> Option<f64> {
let mut ret = 0.0;
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
ret += conn.loss_rate;
}
if ret == 0.0 {
None
} else {
Some(ret as f64)
}
}
fn get_conn_protos(&self) -> Option<Vec<String>> {
let mut ret = vec![];
let p = self.peer.as_ref()?;
for conn in p.conns.iter() {
let Some(tunnel_info) = &conn.tunnel else {
continue;
};
// insert if not exists
if !ret.contains(&tunnel_info.tunnel_type) {
ret.push(tunnel_info.tunnel_type.clone());
}
}
if ret.is_empty() {
None
} else {
Some(ret)
}
}
fn get_udp_nat_type(self: &Self) -> String {
let mut ret = NatType::Unknown;
if let Some(r) = &self.route.stun_info {
ret = NatType::try_from(r.udp_nat_type).unwrap();
}
format!("{:?}", ret)
}
}
struct CommandHandler {
addr: String,
}
@@ -216,6 +120,12 @@ impl CommandHandler {
Ok(PeerCenterRpcClient::connect(self.addr.clone()).await?)
}
async fn get_vpn_portal_client(
&self,
) -> Result<VpnPortalRpcClient<tonic::transport::Channel>, Error> {
Ok(VpnPortalRpcClient::connect(self.addr.clone()).await?)
}
async fn list_peers(&self) -> Result<ListPeerResponse, Error> {
let mut client = self.get_peer_manager_client().await?;
let request = tonic::Request::new(ListPeerRequest::default());
@@ -231,19 +141,9 @@ impl CommandHandler {
}
async fn list_peer_route_pair(&self) -> Result<Vec<PeerRoutePair>, Error> {
let mut peers = self.list_peers().await?.peer_infos;
let mut routes = self.list_routes().await?.routes;
let mut pairs: Vec<PeerRoutePair> = vec![];
for route in routes.iter_mut() {
let peer = peers.iter_mut().find(|peer| peer.peer_id == route.peer_id);
pairs.push(PeerRoutePair {
route: route.clone(),
peer: peer.cloned(),
});
}
Ok(pairs)
let peers = self.list_peers().await?.peer_infos;
let routes = self.list_routes().await?.routes;
Ok(list_peer_route_pair(peers, routes))
}
#[allow(dead_code)]
@@ -261,9 +161,9 @@ impl CommandHandler {
struct PeerTableItem {
ipv4: String,
hostname: String,
cost: i32,
lat_ms: f64,
loss_rate: f64,
cost: String,
lat_ms: String,
loss_rate: String,
rx_bytes: String,
tx_bytes: String,
tunnel_proto: String,
@@ -276,9 +176,9 @@ impl CommandHandler {
PeerTableItem {
ipv4: p.route.ipv4_addr.clone(),
hostname: p.route.hostname.clone(),
cost: p.route.cost,
lat_ms: p.get_latency_ms().unwrap_or(0.0),
loss_rate: p.get_loss_rate().unwrap_or(0.0),
cost: cost_to_str(p.route.cost),
lat_ms: float_to_str(p.get_latency_ms().unwrap_or(0.0), 3),
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
tunnel_proto: p.get_conn_protos().unwrap_or(vec![]).join(",").to_string(),
@@ -452,6 +352,18 @@ async fn main() -> Result<(), Error> {
.to_string()
);
}
SubCommand::VpnPortal => {
let mut vpn_portal_client = handler.get_vpn_portal_client().await?;
let resp = vpn_portal_client
.get_vpn_portal_info(GetVpnPortalInfoRequest::default())
.await?
.into_inner()
.vpn_portal_info
.unwrap_or_default();
println!("portal_name: {}\n", resp.vpn_type);
println!("client_config:{}", resp.client_config);
println!("connected_clients:\n{:#?}", resp.connected_clients);
}
}
Ok(())

View File

@@ -3,7 +3,7 @@
#[cfg(test)]
mod tests;
use std::net::SocketAddr;
use std::{backtrace, io::Write as _, net::SocketAddr};
use anyhow::Context;
use clap::Parser;
@@ -16,10 +16,11 @@ mod instance;
mod peer_center;
mod peers;
mod rpc;
mod tunnels;
mod tunnel;
mod vpn_portal;
use common::{
config::{ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig},
config::{ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig, VpnPortalConfig},
get_logger_timer_rfc3339,
};
use instance::instance::Instance;
@@ -31,6 +32,11 @@ use crate::common::{
global_ctx::GlobalCtxEvent,
};
use mimalloc_rust::*;
#[global_allocator]
static GLOBAL_MIMALLOC: GlobalMiMalloc = GlobalMiMalloc;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
@@ -73,7 +79,8 @@ struct Cli {
#[arg(short, long, help = "listeners to accept connections, pass '' to avoid listening.",
default_values_t = ["tcp://0.0.0.0:11010".to_string(),
"udp://0.0.0.0:11010".to_string()])]
"udp://0.0.0.0:11010".to_string(),
"wg://0.0.0.0:11011".to_string()])]
listeners: Vec<String>,
/// specify the linux network namespace, default is the root namespace
@@ -104,16 +111,45 @@ struct Cli {
help = "instance uuid to identify this vpn node in whole vpn network example: 123e4567-e89b-12d3-a456-426614174000"
)]
instance_id: Option<String>,
#[arg(
long,
help = "url that defines the vpn portal, allow other vpn clients to connect.
example: wg://0.0.0.0:11010/10.14.14.0/24, means the vpn portal is a wireguard server listening on vpn.example.com:11010,
and the vpn client is in network of 10.14.14.0/24"
)]
vpn_portal: Option<String>,
#[arg(long, help = "default protocol to use when connecting to peers")]
default_protocol: Option<String>,
#[arg(
short = 'u',
long,
help = "disable encryption for peers communication, default is false, must be same with peers",
default_value = "false"
)]
disable_encryption: bool,
#[arg(
long,
help = "use multi-thread runtime, default is single-thread",
default_value = "false"
)]
multi_thread: bool,
#[arg(long, help = "do not use ipv6", default_value = "false")]
disable_ipv6: bool,
}
impl From<Cli> for TomlConfigLoader {
fn from(cli: Cli) -> Self {
let cfg = TomlConfigLoader::default();
cfg.set_inst_name(cli.instance_name.clone());
cfg.set_network_identity(NetworkIdentity {
network_name: cli.network_name.clone(),
network_secret: cli.network_secret.clone(),
});
cfg.set_network_identity(NetworkIdentity::new(
cli.network_name.clone(),
cli.network_secret.clone(),
));
cfg.set_netns(cli.net_ns.clone());
if let Some(ipv4) = &cli.ipv4 {
@@ -196,6 +232,46 @@ impl From<Cli> for TomlConfigLoader {
});
}
if cli.vpn_portal.is_some() {
let url: url::Url = cli
.vpn_portal
.clone()
.unwrap()
.parse()
.with_context(|| {
format!(
"failed to parse vpn portal url: {}",
cli.vpn_portal.unwrap()
)
})
.unwrap();
cfg.set_vpn_portal_config(VpnPortalConfig {
client_cidr: url.path()[1..]
.parse()
.with_context(|| {
format!("failed to parse vpn portal client cidr: {}", url.path())
})
.unwrap(),
wireguard_listen: format!("{}:{}", url.host_str().unwrap(), url.port().unwrap())
.parse()
.with_context(|| {
format!(
"failed to parse vpn portal wireguard listen address: {}",
url.host_str().unwrap()
)
})
.unwrap(),
});
}
let mut f = cfg.get_flags();
if cli.default_protocol.is_some() {
f.default_protocol = cli.default_protocol.as_ref().unwrap().clone();
}
f.enable_encryption = !cli.disable_encryption;
f.enable_ipv6 = !cli.disable_ipv6;
cfg.set_flags(f);
cfg
}
}
@@ -267,12 +343,18 @@ fn peer_conn_info_to_string(p: crate::rpc::PeerConnInfo) -> String {
)
}
#[tokio::main(flavor = "current_thread")]
#[tracing::instrument]
pub async fn main() {
let cli = Cli::parse();
tracing::info!(cli = ?cli, "cli args parsed");
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();
init_logger(&cfg);
@@ -336,6 +418,20 @@ pub async fn main() {
GlobalCtxEvent::ConnectError(dst, err) => {
print_event(format!("connect to peer error. dst: {}, err: {}", dst, err));
}
GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => {
print_event(format!(
"vpn portal client connected. portal: {}, client_addr: {}",
portal, client_addr
));
}
GlobalCtxEvent::VpnPortalClientDisconnected(portal, client_addr) => {
print_event(format!(
"vpn portal client disconnected. portal: {}, client_addr: {}",
portal, client_addr
));
}
}
}
});
@@ -349,3 +445,25 @@ pub async fn main() {
inst.wait().await;
}
fn main() {
setup_panic_handler();
let cli = Cli::parse();
tracing::info!(cli = ?cli, "cli args parsed");
if cli.multi_thread {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap()
.block_on(async move { async_main(cli).await })
} else {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move { async_main(cli).await })
}
}

View File

@@ -16,12 +16,13 @@ use tokio::{
sync::{mpsc::UnboundedSender, Mutex},
task::JoinSet,
};
use tokio_util::bytes::Bytes;
use tracing::Instrument;
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
peers::{packet, peer_manager::PeerManager, PeerPacketFilter},
peers::{peer_manager::PeerManager, PeerPacketFilter},
tunnel::packet_def::{PacketType, ZCPacket},
};
use super::CidrSet;
@@ -78,11 +79,7 @@ fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit<u8>]) -> Result<(usize, I
Ok((size, addr))
}
fn socket_recv_loop(
socket: Socket,
nat_table: IcmpNatTable,
sender: UnboundedSender<packet::Packet>,
) {
fn socket_recv_loop(socket: Socket, nat_table: IcmpNatTable, sender: UnboundedSender<ZCPacket>) {
let mut buf = [0u8; 4096];
let data: &mut [MaybeUninit<u8>] = unsafe { std::mem::transmute(&mut buf[12..]) };
@@ -126,13 +123,14 @@ fn socket_recv_loop(
ipv4_packet.set_destination(dest_ip);
ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable()));
let peer_packet = packet::Packet::new_data_packet(
v.my_peer_id,
v.src_peer_id,
&ipv4_packet.to_immutable().packet(),
let mut p = ZCPacket::new_with_payload(ipv4_packet.packet());
p.fill_peer_manager_hdr(
v.my_peer_id.into(),
v.src_peer_id.into(),
PacketType::Data as u8,
);
if let Err(e) = sender.send(peer_packet) {
if let Err(e) = sender.send(p) {
tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e);
break;
}
@@ -141,61 +139,12 @@ fn socket_recv_loop(
#[async_trait::async_trait]
impl PeerPacketFilter for IcmpProxy {
async fn try_process_packet_from_peer(
&self,
packet: &packet::ArchivedPacket,
_: &Bytes,
) -> Option<()> {
let _ = self.global_ctx.get_ipv4()?;
if packet.packet_type != packet::PacketType::Data {
return None;
};
let ipv4 = Ipv4Packet::new(&packet.payload.as_bytes())?;
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Icmp
{
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
if let Some(_) = self.try_handle_peer_packet(&packet).await {
return None;
} else {
return Some(packet);
}
if !self.cidr_set.contains_v4(ipv4.get_destination()) {
return None;
}
let icmp_packet = icmp::echo_request::EchoRequestPacket::new(&ipv4.payload())?;
if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest {
// drop it because we do not support other icmp types
tracing::trace!("unsupported icmp type: {:?}", icmp_packet.get_icmp_type());
return Some(());
}
let icmp_id = icmp_packet.get_identifier();
let icmp_seq = icmp_packet.get_sequence_number();
let key = IcmpNatKey {
dst_ip: ipv4.get_destination().into(),
icmp_id,
icmp_seq,
};
let value = IcmpNatEntry::new(
packet.from_peer.into(),
packet.to_peer.into(),
ipv4.get_source().into(),
)
.ok()?;
if let Some(old) = self.nat_table.insert(key, value) {
tracing::info!("icmp nat table entry replaced: {:?}", old);
}
if let Err(e) = self.send_icmp_packet(ipv4.get_destination(), &icmp_packet) {
tracing::error!("send icmp packet failed: {:?}", e);
}
Some(())
}
}
@@ -262,8 +211,9 @@ impl IcmpProxy {
self.tasks.lock().await.spawn(
async move {
while let Some(msg) = receiver.recv().await {
let to_peer_id = msg.to_peer.into();
let ret = peer_manager.send_msg(msg.into(), to_peer_id).await;
let hdr = msg.peer_manager_header().unwrap();
let to_peer_id = hdr.to_peer_id.into();
let ret = peer_manager.send_msg(msg, to_peer_id).await;
if ret.is_err() {
tracing::error!("send icmp packet to peer failed: {:?}", ret);
}
@@ -290,4 +240,58 @@ impl IcmpProxy {
Ok(())
}
async fn try_handle_peer_packet(&self, packet: &ZCPacket) -> Option<()> {
let _ = self.global_ctx.get_ipv4()?;
let hdr = packet.peer_manager_header().unwrap();
if hdr.packet_type != PacketType::Data as u8 {
return None;
};
let ipv4 = Ipv4Packet::new(&packet.payload())?;
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Icmp
{
return None;
}
if !self.cidr_set.contains_v4(ipv4.get_destination()) {
return None;
}
let icmp_packet = icmp::echo_request::EchoRequestPacket::new(&ipv4.payload())?;
if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest {
// drop it because we do not support other icmp types
tracing::trace!("unsupported icmp type: {:?}", icmp_packet.get_icmp_type());
return Some(());
}
let icmp_id = icmp_packet.get_identifier();
let icmp_seq = icmp_packet.get_sequence_number();
let key = IcmpNatKey {
dst_ip: ipv4.get_destination().into(),
icmp_id,
icmp_seq,
};
let value = IcmpNatEntry::new(
hdr.from_peer_id.into(),
hdr.to_peer_id.into(),
ipv4.get_source().into(),
)
.ok()?;
if let Some(old) = self.nat_table.insert(key, value) {
tracing::info!("icmp nat table entry replaced: {:?}", old);
}
if let Err(e) = self.send_icmp_packet(ipv4.get_destination(), &icmp_packet) {
tracing::error!("send icmp packet failed: {:?}", e);
}
Some(())
}
}

View File

@@ -1,5 +1,4 @@
use dashmap::DashSet;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use tokio::task::JoinSet;
use crate::common::global_ctx::ArcGlobalCtx;
@@ -11,7 +10,7 @@ pub mod udp_proxy;
#[derive(Debug)]
struct CidrSet {
global_ctx: ArcGlobalCtx,
cidr_set: Arc<DashSet<cidr::IpCidr>>,
cidr_set: Arc<Mutex<Vec<cidr::IpCidr>>>,
tasks: JoinSet<()>,
}
@@ -19,7 +18,7 @@ impl CidrSet {
pub fn new(global_ctx: ArcGlobalCtx) -> Self {
let mut ret = Self {
global_ctx,
cidr_set: Arc::new(DashSet::new()),
cidr_set: Arc::new(Mutex::new(vec![])),
tasks: JoinSet::new(),
};
ret.run_cidr_updater();
@@ -35,9 +34,9 @@ impl CidrSet {
let cidrs = global_ctx.get_proxy_cidrs();
if cidrs != last_cidrs {
last_cidrs = cidrs.clone();
cidr_set.clear();
cidr_set.lock().unwrap().clear();
for cidr in cidrs.iter() {
cidr_set.insert(cidr.clone());
cidr_set.lock().unwrap().push(cidr.clone());
}
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
@@ -47,10 +46,16 @@ impl CidrSet {
pub fn contains_v4(&self, ip: std::net::Ipv4Addr) -> bool {
let ip = ip.into();
return self.cidr_set.iter().any(|cidr| cidr.contains(&ip));
let s = self.cidr_set.lock().unwrap();
for cidr in s.iter() {
if cidr.contains(&ip) {
return true;
}
}
false
}
pub fn is_empty(&self) -> bool {
return self.cidr_set.is_empty();
self.cidr_set.lock().unwrap().is_empty()
}
}

View File

@@ -2,7 +2,9 @@ use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet};
use pnet::packet::tcp::{ipv4_checksum, MutableTcpPacket};
use pnet::packet::tcp::{ipv4_checksum, MutableTcpPacket, TcpPacket};
use pnet::packet::MutablePacket;
use pnet::packet::Packet;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::sync::atomic::AtomicU16;
use std::sync::Arc;
@@ -11,15 +13,16 @@ use tokio::io::copy_bidirectional;
use tokio::net::{TcpListener, TcpSocket, TcpStream};
use tokio::sync::Mutex;
use tokio::task::JoinSet;
use tokio_util::bytes::{Bytes, BytesMut};
use tracing::Instrument;
use crate::common::error::Result;
use crate::common::global_ctx::GlobalCtx;
use crate::common::join_joinset_background;
use crate::common::netns::NetNS;
use crate::peers::packet::{self, ArchivedPacket};
use crate::peers::peer_manager::PeerManager;
use crate::peers::{NicPacketFilter, PeerPacketFilter};
use crate::tunnel::packet_def::{PacketType, ZCPacket};
use super::CidrSet;
@@ -71,7 +74,7 @@ pub struct TcpProxy {
peer_manager: Arc<PeerManager>,
local_port: AtomicU16,
tasks: Arc<Mutex<JoinSet<()>>>,
tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
syn_map: SynSockMap,
conn_map: ConnSockMap,
@@ -82,98 +85,37 @@ pub struct TcpProxy {
#[async_trait::async_trait]
impl PeerPacketFilter for TcpProxy {
async fn try_process_packet_from_peer(&self, packet: &ArchivedPacket, _: &Bytes) -> Option<()> {
let ipv4_addr = self.global_ctx.get_ipv4()?;
if packet.packet_type != packet::PacketType::Data {
return None;
};
let payload_bytes = packet.payload.as_bytes();
let ipv4 = Ipv4Packet::new(payload_bytes)?;
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
async fn try_process_packet_from_peer(&self, mut packet: ZCPacket) -> Option<ZCPacket> {
if let Some(_) = self.try_handle_peer_packet(&mut packet).await {
if let Err(e) = self.peer_manager.get_nic_channel().send(packet).await {
tracing::error!("send to nic failed: {:?}", e);
}
return None;
} else {
Some(packet)
}
if !self.cidr_set.contains_v4(ipv4.get_destination()) {
return None;
}
tracing::trace!(ipv4 = ?ipv4, cidr_set = ?self.cidr_set, "proxy tcp packet received");
let mut packet_buffer = BytesMut::with_capacity(payload_bytes.len());
packet_buffer.extend_from_slice(&payload_bytes.to_vec());
let (ip_buffer, tcp_buffer) =
packet_buffer.split_at_mut(ipv4.get_header_length() as usize * 4);
let mut ip_packet = MutableIpv4Packet::new(ip_buffer).unwrap();
let mut tcp_packet = MutableTcpPacket::new(tcp_buffer).unwrap();
let is_tcp_syn = tcp_packet.get_flags() & pnet::packet::tcp::TcpFlags::SYN != 0;
if is_tcp_syn {
let source_ip = ip_packet.get_source();
let source_port = tcp_packet.get_source();
let src = SocketAddr::V4(SocketAddrV4::new(source_ip, source_port));
let dest_ip = ip_packet.get_destination();
let dest_port = tcp_packet.get_destination();
let dst = SocketAddr::V4(SocketAddrV4::new(dest_ip, dest_port));
let old_val = self
.syn_map
.insert(src, Arc::new(NatDstEntry::new(src, dst)));
tracing::trace!(src = ?src, dst = ?dst, old_entry = ?old_val, "tcp syn received");
}
ip_packet.set_destination(ipv4_addr);
tcp_packet.set_destination(self.get_local_port());
Self::update_ipv4_packet_checksum(&mut ip_packet, &mut tcp_packet);
tracing::trace!(ip_packet = ?ip_packet, tcp_packet = ?tcp_packet, "tcp packet forwarded");
if let Err(e) = self
.peer_manager
.get_nic_channel()
.send(packet_buffer.freeze())
.await
{
tracing::error!("send to nic failed: {:?}", e);
}
Some(())
}
}
#[async_trait::async_trait]
impl NicPacketFilter for TcpProxy {
async fn try_process_packet_from_nic(&self, mut data: BytesMut) -> BytesMut {
async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) {
let Some(my_ipv4) = self.global_ctx.get_ipv4() else {
return data;
return;
};
let header_len = {
let Some(ipv4) = &Ipv4Packet::new(&data[..]) else {
return data;
};
if ipv4.get_version() != 4
|| ipv4.get_source() != my_ipv4
|| ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp
{
return data;
}
ipv4.get_header_length() as usize * 4
};
let (ip_buffer, tcp_buffer) = data.split_at_mut(header_len);
let mut ip_packet = MutableIpv4Packet::new(ip_buffer).unwrap();
let mut tcp_packet = MutableTcpPacket::new(tcp_buffer).unwrap();
let data = zc_packet.payload();
let ip_packet = Ipv4Packet::new(data).unwrap();
if ip_packet.get_version() != 4
|| ip_packet.get_source() != my_ipv4
|| ip_packet.get_next_level_protocol() != IpNextHeaderProtocols::Tcp
{
return;
}
let tcp_packet = TcpPacket::new(ip_packet.payload()).unwrap();
if tcp_packet.get_source() != self.get_local_port() {
return data;
return;
}
let dst_addr = SocketAddr::V4(SocketAddrV4::new(
@@ -186,7 +128,7 @@ impl NicPacketFilter for TcpProxy {
entry
} else {
let Some(syn_entry) = self.syn_map.get(&dst_addr) else {
return data;
return;
};
syn_entry
};
@@ -198,13 +140,18 @@ impl NicPacketFilter for TcpProxy {
panic!("v4 nat entry src ip is not v4");
};
let mut ip_packet = MutableIpv4Packet::new(zc_packet.mut_payload()).unwrap();
ip_packet.set_source(ip);
let dst = ip_packet.get_destination();
let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap();
tcp_packet.set_source(nat_entry.dst.port());
Self::update_ipv4_packet_checksum(&mut ip_packet, &mut tcp_packet);
Self::update_tcp_packet_checksum(&mut tcp_packet, &ip, &dst);
drop(tcp_packet);
Self::update_ip_packet_checksum(&mut ip_packet);
tracing::trace!(dst_addr = ?dst_addr, nat_entry = ?nat_entry, packet = ?ip_packet, "tcp packet after modified");
data
}
}
@@ -215,7 +162,7 @@ impl TcpProxy {
peer_manager,
local_port: AtomicU16::new(0),
tasks: Arc::new(Mutex::new(JoinSet::new())),
tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())),
syn_map: Arc::new(DashMap::new()),
conn_map: Arc::new(DashMap::new()),
@@ -225,17 +172,20 @@ impl TcpProxy {
})
}
fn update_ipv4_packet_checksum(
ipv4_packet: &mut MutableIpv4Packet,
fn update_tcp_packet_checksum(
tcp_packet: &mut MutableTcpPacket,
ipv4_src: &Ipv4Addr,
ipv4_dst: &Ipv4Addr,
) {
tcp_packet.set_checksum(ipv4_checksum(
&tcp_packet.to_immutable(),
&ipv4_packet.get_source(),
&ipv4_packet.get_destination(),
ipv4_src,
ipv4_dst,
));
}
ipv4_packet.set_checksum(pnet::packet::ipv4::checksum(&ipv4_packet.to_immutable()));
fn update_ip_packet_checksum(ip_packet: &mut MutableIpv4Packet) {
ip_packet.set_checksum(pnet::packet::ipv4::checksum(&ip_packet.to_immutable()));
}
pub async fn start(self: &Arc<Self>) -> Result<()> {
@@ -247,6 +197,7 @@ impl TcpProxy {
self.peer_manager
.add_nic_packet_process_pipeline(Box::new(self.clone()))
.await;
join_joinset_background(self.tasks.clone(), "TcpProxy".to_owned());
Ok(())
}
@@ -268,7 +219,7 @@ impl TcpProxy {
tokio::time::sleep(Duration::from_secs(10)).await;
}
};
tasks.lock().await.spawn(syn_map_cleaner_task);
tasks.lock().unwrap().spawn(syn_map_cleaner_task);
Ok(())
}
@@ -300,6 +251,7 @@ impl TcpProxy {
tracing::error!("tcp connection from unknown source: {:?}", socket_addr);
continue;
};
tracing::info!(?socket_addr, "tcp connection accepted for proxy");
assert_eq!(entry.state.load(), NatDstEntryState::SynReceived);
let entry_clone = entry.clone();
@@ -312,7 +264,7 @@ impl TcpProxy {
let old_nat_val = conn_map.insert(entry_clone.id, entry_clone.clone());
assert!(old_nat_val.is_none());
tasks.lock().await.spawn(Self::connect_to_nat_dst(
tasks.lock().unwrap().spawn(Self::connect_to_nat_dst(
net_ns.clone(),
tcp_stream,
conn_map.clone(),
@@ -325,7 +277,7 @@ impl TcpProxy {
};
self.tasks
.lock()
.await
.unwrap()
.spawn(accept_task.instrument(tracing::info_span!("tcp_proxy_listener")));
Ok(())
@@ -402,4 +354,60 @@ impl TcpProxy {
pub fn get_local_port(&self) -> u16 {
self.local_port.load(std::sync::atomic::Ordering::Relaxed)
}
async fn try_handle_peer_packet(&self, packet: &mut ZCPacket) -> Option<()> {
let ipv4_addr = self.global_ctx.get_ipv4()?;
let hdr = packet.peer_manager_header().unwrap();
if hdr.packet_type != PacketType::Data as u8 {
return None;
};
let payload_bytes = packet.mut_payload();
let ipv4 = Ipv4Packet::new(payload_bytes)?;
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
return None;
}
if !self.cidr_set.contains_v4(ipv4.get_destination()) {
return None;
}
tracing::info!(ipv4 = ?ipv4, cidr_set = ?self.cidr_set, "proxy tcp packet received");
let ip_packet = Ipv4Packet::new(payload_bytes).unwrap();
let tcp_packet = TcpPacket::new(ip_packet.payload()).unwrap();
let is_tcp_syn = tcp_packet.get_flags() & pnet::packet::tcp::TcpFlags::SYN != 0;
if is_tcp_syn {
let source_ip = ip_packet.get_source();
let source_port = tcp_packet.get_source();
let src = SocketAddr::V4(SocketAddrV4::new(source_ip, source_port));
let dest_ip = ip_packet.get_destination();
let dest_port = tcp_packet.get_destination();
let dst = SocketAddr::V4(SocketAddrV4::new(dest_ip, dest_port));
let old_val = self
.syn_map
.insert(src, Arc::new(NatDstEntry::new(src, dst)));
tracing::trace!(src = ?src, dst = ?dst, old_entry = ?old_val, "tcp syn received");
}
let mut ip_packet = MutableIpv4Packet::new(payload_bytes).unwrap();
ip_packet.set_destination(ipv4_addr);
let source = ip_packet.get_source();
let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap();
tcp_packet.set_destination(self.get_local_port());
Self::update_tcp_packet_checksum(&mut tcp_packet, &source, &ipv4_addr);
drop(tcp_packet);
Self::update_ip_packet_checksum(&mut ip_packet);
tracing::info!(?source, ?ipv4_addr, ?packet, "tcp packet after modified");
Some(())
}
}

View File

@@ -21,13 +21,15 @@ use tokio::{
time::timeout,
};
use tokio_util::bytes::Bytes;
use tracing::Level;
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
peers::{packet, peer_manager::PeerManager, PeerPacketFilter},
tunnels::common::setup_sokcet2,
peers::{peer_manager::PeerManager, PeerPacketFilter},
tunnel::{
common::setup_sokcet2,
packet_def::{PacketType, ZCPacket},
},
};
use super::CidrSet;
@@ -79,7 +81,7 @@ impl UdpNatEntry {
async fn compose_ipv4_packet(
self: &Arc<Self>,
packet_sender: &mut UnboundedSender<packet::Packet>,
packet_sender: &mut UnboundedSender<ZCPacket>,
buf: &mut [u8],
src_v4: &SocketAddrV4,
payload_len: usize,
@@ -140,13 +142,10 @@ impl UdpNatEntry {
tracing::trace!(?ipv4_packet, "udp nat packet response send");
let peer_packet = packet::Packet::new_data_packet(
self.my_peer_id,
self.src_peer_id,
&ipv4_packet.to_immutable().packet(),
);
let mut p = ZCPacket::new_with_payload(ipv4_packet.packet());
p.fill_peer_manager_hdr(self.my_peer_id, self.src_peer_id, PacketType::Data as u8);
if let Err(e) = packet_sender.send(peer_packet) {
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()));
}
@@ -158,7 +157,7 @@ impl UdpNatEntry {
Ok(())
}
async fn forward_task(self: Arc<Self>, mut packet_sender: UnboundedSender<packet::Packet>) {
async fn forward_task(self: Arc<Self>, mut packet_sender: UnboundedSender<ZCPacket>) {
let mut buf = [0u8; 8192];
let mut udp_body: &mut [u8] = unsafe { std::mem::transmute(&mut buf[20 + 8..]) };
let mut ip_id = 1;
@@ -204,7 +203,7 @@ impl UdpNatEntry {
else {
break;
};
ip_id += 1;
ip_id = ip_id.wrapping_add(1);
}
self.stop();
@@ -220,31 +219,25 @@ pub struct UdpProxy {
nat_table: Arc<DashMap<UdpNatKey, Arc<UdpNatEntry>>>,
sender: UnboundedSender<packet::Packet>,
receiver: Mutex<Option<UnboundedReceiver<packet::Packet>>>,
sender: UnboundedSender<ZCPacket>,
receiver: Mutex<Option<UnboundedReceiver<ZCPacket>>>,
tasks: Mutex<JoinSet<()>>,
}
#[async_trait::async_trait]
impl PeerPacketFilter for UdpProxy {
async fn try_process_packet_from_peer(
&self,
packet: &packet::ArchivedPacket,
_: &Bytes,
) -> Option<()> {
impl UdpProxy {
async fn try_handle_packet(&self, packet: &ZCPacket) -> Option<()> {
if self.cidr_set.is_empty() {
return None;
}
let _ = self.global_ctx.get_ipv4()?;
if packet.packet_type != packet::PacketType::Data {
let hdr = packet.peer_manager_header().unwrap();
if hdr.packet_type != PacketType::Data as u8 {
return None;
};
let ipv4 = Ipv4Packet::new(packet.payload.as_bytes())?;
let ipv4 = Ipv4Packet::new(packet.payload())?;
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Udp {
return None;
}
@@ -272,8 +265,8 @@ impl PeerPacketFilter for UdpProxy {
tracing::info!(?packet, ?ipv4, ?udp_packet, "udp nat table entry created");
let _g = self.global_ctx.net_ns.guard();
Ok(Arc::new(UdpNatEntry::new(
packet.from_peer.into(),
packet.to_peer.into(),
hdr.from_peer_id.get(),
hdr.to_peer_id.get(),
nat_key.src_socket,
)?))
})
@@ -316,6 +309,17 @@ impl PeerPacketFilter for UdpProxy {
}
}
#[async_trait::async_trait]
impl PeerPacketFilter for UdpProxy {
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
if let Some(_) = self.try_handle_packet(&packet).await {
return None;
} else {
return Some(packet);
}
}
}
impl UdpProxy {
pub fn new(
global_ctx: ArcGlobalCtx,
@@ -362,9 +366,9 @@ impl UdpProxy {
let peer_manager = self.peer_manager.clone();
self.tasks.lock().await.spawn(async move {
while let Some(msg) = receiver.recv().await {
let to_peer_id: PeerId = msg.to_peer.into();
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.into(), to_peer_id).await;
let ret = peer_manager.send_msg(msg, to_peer_id).await;
if ret.is_err() {
tracing::error!("send icmp packet to peer failed: {:?}", ret);
}

View File

@@ -1,14 +1,15 @@
use std::borrow::BorrowMut;
use std::net::Ipv4Addr;
use std::sync::Arc;
use std::pin::Pin;
use std::sync::{Arc, Weak};
use anyhow::Context;
use futures::StreamExt;
use pnet::packet::ethernet::EthernetPacket;
use futures::{SinkExt, StreamExt};
use pnet::packet::ipv4::Ipv4Packet;
use tokio::{sync::Mutex, task::JoinSet};
use tokio_util::bytes::{Bytes, BytesMut};
use tonic::transport::server::TcpIncoming;
use tonic::transport::Server;
use crate::common::config::ConfigLoader;
@@ -25,20 +26,55 @@ use crate::peer_center::instance::PeerCenterInstance;
use crate::peers::peer_conn::PeerConnId;
use crate::peers::peer_manager::{PeerManager, RouteAlgoType};
use crate::peers::rpc_service::PeerManagerRpcService;
use crate::tunnels::SinkItem;
use crate::peers::PacketRecvChanReceiver;
use crate::rpc::vpn_portal_rpc_server::VpnPortalRpc;
use crate::rpc::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo};
use crate::tunnel::packet_def::ZCPacket;
use tokio_stream::wrappers::ReceiverStream;
use crate::tunnel::{ZCPacketSink, ZCPacketStream};
use crate::vpn_portal::{self, VpnPortal};
use super::listeners::ListenerManager;
use super::virtual_nic;
use crate::common::ifcfg::IfConfiguerTrait;
#[derive(Clone)]
struct IpProxy {
tcp_proxy: Arc<TcpProxy>,
icmp_proxy: Arc<IcmpProxy>,
udp_proxy: Arc<UdpProxy>,
}
impl IpProxy {
fn new(global_ctx: ArcGlobalCtx, peer_manager: Arc<PeerManager>) -> Result<Self, Error> {
let tcp_proxy = TcpProxy::new(global_ctx.clone(), peer_manager.clone());
let icmp_proxy = IcmpProxy::new(global_ctx.clone(), peer_manager.clone())
.with_context(|| "create icmp proxy failed")?;
let udp_proxy = UdpProxy::new(global_ctx.clone(), peer_manager.clone())
.with_context(|| "create udp proxy failed")?;
Ok(IpProxy {
tcp_proxy,
icmp_proxy,
udp_proxy,
})
}
async fn start(&self) -> Result<(), Error> {
self.tcp_proxy.start().await?;
self.icmp_proxy.start().await?;
self.udp_proxy.start().await?;
Ok(())
}
}
pub struct Instance {
inst_name: String,
id: uuid::Uuid,
virtual_nic: Option<Arc<virtual_nic::VirtualNic>>,
peer_packet_receiver: Option<ReceiverStream<SinkItem>>,
peer_packet_receiver: Option<PacketRecvChanReceiver>,
tasks: JoinSet<()>,
@@ -48,12 +84,12 @@ pub struct Instance {
direct_conn_manager: Arc<DirectConnectorManager>,
udp_hole_puncher: Arc<Mutex<UdpHolePunchConnector>>,
tcp_proxy: Arc<TcpProxy>,
icmp_proxy: Arc<IcmpProxy>,
udp_proxy: Arc<UdpProxy>,
ip_proxy: Option<IpProxy>,
peer_center: Arc<PeerCenterInstance>,
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
global_ctx: ArcGlobalCtx,
}
@@ -92,22 +128,16 @@ impl Instance {
let udp_hole_puncher = UdpHolePunchConnector::new(global_ctx.clone(), peer_manager.clone());
let arc_tcp_proxy = TcpProxy::new(global_ctx.clone(), peer_manager.clone());
let arc_icmp_proxy = IcmpProxy::new(global_ctx.clone(), peer_manager.clone())
.with_context(|| "create icmp proxy failed")
.unwrap();
let arc_udp_proxy = UdpProxy::new(global_ctx.clone(), peer_manager.clone())
.with_context(|| "create udp proxy failed")
.unwrap();
let peer_center = Arc::new(PeerCenterInstance::new(peer_manager.clone()));
let vpn_portal_inst = vpn_portal::wireguard::WireGuard::default();
Instance {
inst_name: global_ctx.inst_name.clone(),
id,
virtual_nic: None,
peer_packet_receiver: Some(ReceiverStream::new(peer_packet_receiver)),
peer_packet_receiver: Some(peer_packet_receiver),
tasks: JoinSet::new(),
peer_manager,
@@ -116,12 +146,12 @@ impl Instance {
direct_conn_manager: Arc::new(direct_conn_manager),
udp_hole_puncher: Arc::new(Mutex::new(udp_hole_puncher)),
tcp_proxy: arc_tcp_proxy,
icmp_proxy: arc_icmp_proxy,
udp_proxy: arc_udp_proxy,
ip_proxy: None,
peer_center,
vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))),
global_ctx,
}
}
@@ -130,16 +160,19 @@ impl Instance {
self.conn_manager.clone()
}
async fn do_forward_nic_to_peers_ipv4(ret: BytesMut, mgr: &PeerManager) {
if let Some(ipv4) = Ipv4Packet::new(&ret) {
async fn do_forward_nic_to_peers_ipv4(ret: ZCPacket, mgr: &PeerManager) {
if let Some(ipv4) = Ipv4Packet::new(ret.payload()) {
if ipv4.get_version() != 4 {
tracing::info!("[USER_PACKET] not ipv4 packet: {:?}", ipv4);
return;
}
let dst_ipv4 = ipv4.get_destination();
tracing::trace!(
?ret,
"[USER_PACKET] recv new packet from tun device and forward to peers."
);
// TODO: use zero-copy
let send_ret = mgr.send_msg_ipv4(ret, dst_ipv4).await;
if send_ret.is_err() {
tracing::trace!(?send_ret, "[USER_PACKET] send_msg_ipv4 failed")
@@ -149,23 +182,23 @@ impl Instance {
}
}
async fn do_forward_nic_to_peers_ethernet(mut ret: BytesMut, mgr: &PeerManager) {
if let Some(eth) = EthernetPacket::new(&ret) {
log::warn!("begin to forward: {:?}, type: {}", eth, eth.get_ethertype());
Self::do_forward_nic_to_peers_ipv4(ret.split_off(14), mgr).await;
} else {
log::warn!("not ipv4 packet: {:?}", ret);
}
}
// async fn do_forward_nic_to_peers_ethernet(mut ret: BytesMut, mgr: &PeerManager) {
// if let Some(eth) = EthernetPacket::new(&ret) {
// log::warn!("begin to forward: {:?}, type: {}", eth, eth.get_ethertype());
// Self::do_forward_nic_to_peers_ipv4(ret.split_off(14), mgr).await;
// } else {
// log::warn!("not ipv4 packet: {:?}", ret);
// }
// }
fn do_forward_nic_to_peers(&mut self) -> Result<(), Error> {
fn do_forward_nic_to_peers(
&mut self,
mut stream: Pin<Box<dyn ZCPacketStream>>,
) -> Result<(), Error> {
// read from nic and write to corresponding tunnel
let nic = self.virtual_nic.as_ref().unwrap();
let nic = nic.clone();
let mgr = self.peer_manager.clone();
self.tasks.spawn(async move {
let mut stream = nic.pin_recv_stream();
while let Some(ret) = stream.next().await {
if ret.is_err() {
log::error!("read from nic failed: {:?}", ret);
@@ -181,24 +214,20 @@ impl Instance {
fn do_forward_peers_to_nic(
tasks: &mut JoinSet<()>,
nic: Arc<virtual_nic::VirtualNic>,
channel: Option<ReceiverStream<Bytes>>,
mut sink: Pin<Box<dyn ZCPacketSink>>,
channel: Option<PacketRecvChanReceiver>,
) {
tasks.spawn(async move {
let send = nic.pin_send_stream();
let channel = channel.unwrap();
let ret = channel
.map(|packet| {
log::trace!(
"[USER_PACKET] forward packet from peers to nic. packet: {:?}",
packet
);
Ok(packet)
})
.forward(send)
.await;
if ret.is_err() {
panic!("do_forward_tunnel_to_nic");
let mut channel = channel.unwrap();
while let Some(packet) = channel.recv().await {
tracing::trace!(
"[USER_PACKET] forward packet from peers to nic. packet: {:?}",
packet
);
let ret = sink.send(packet).await;
if ret.is_err() {
tracing::error!(?ret, "do_forward_tunnel_to_nic sink error");
}
}
});
}
@@ -213,19 +242,19 @@ impl Instance {
}
async fn prepare_tun_device(&mut self) -> Result<(), Error> {
let nic = virtual_nic::VirtualNic::new(self.get_global_ctx())
.create_dev()
.await?;
let mut nic = virtual_nic::VirtualNic::new(self.get_global_ctx());
let tunnel = nic.create_dev().await?;
self.global_ctx
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
let (stream, sink) = tunnel.split();
self.virtual_nic = Some(Arc::new(nic));
self.do_forward_nic_to_peers().unwrap();
self.do_forward_nic_to_peers(stream).unwrap();
Self::do_forward_peers_to_nic(
self.tasks.borrow_mut(),
self.virtual_nic.as_ref().unwrap().clone(),
sink,
self.peer_packet_receiver.take(),
);
@@ -257,11 +286,14 @@ impl Instance {
self.listener_manager.lock().await.run().await?;
self.peer_manager.run().await?;
self.run_rpc_server().unwrap();
self.run_rpc_server()?;
self.ip_proxy = Some(IpProxy::new(
self.get_global_ctx(),
self.get_peer_manager(),
)?);
self.ip_proxy.as_ref().unwrap().start().await?;
self.tcp_proxy.start().await.unwrap();
self.icmp_proxy.start().await.unwrap();
self.udp_proxy.start().await.unwrap();
self.run_proxy_cidrs_route_updater();
self.udp_hole_puncher.lock().await.run().await?;
@@ -270,6 +302,22 @@ impl Instance {
self.add_initial_peers().await?;
if self.global_ctx.get_vpn_portal_cidr().is_some() {
self.run_vpn_portal().await?;
}
Ok(())
}
pub async fn run_vpn_portal(&mut self) -> Result<(), Error> {
if self.global_ctx.get_vpn_portal_cidr().is_none() {
return Err(anyhow::anyhow!("vpn portal cidr not set.").into());
}
self.vpn_portal
.lock()
.await
.start(self.get_global_ctx(), self.get_peer_manager())
.await?;
Ok(())
}
@@ -304,7 +352,46 @@ impl Instance {
self.peer_manager.my_peer_id()
}
fn run_rpc_server(&mut self) -> Result<(), Box<dyn std::error::Error>> {
fn get_vpn_portal_rpc_service(&self) -> impl VpnPortalRpc {
struct VpnPortalRpcService {
peer_mgr: Weak<PeerManager>,
vpn_portal: Weak<Mutex<Box<dyn VpnPortal>>>,
}
#[tonic::async_trait]
impl VpnPortalRpc for VpnPortalRpcService {
async fn get_vpn_portal_info(
&self,
_request: tonic::Request<GetVpnPortalInfoRequest>,
) -> Result<tonic::Response<GetVpnPortalInfoResponse>, tonic::Status> {
let Some(vpn_portal) = self.vpn_portal.upgrade() else {
return Err(tonic::Status::unavailable("vpn portal not available"));
};
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
return Err(tonic::Status::unavailable("peer manager not available"));
};
let vpn_portal = vpn_portal.lock().await;
let ret = GetVpnPortalInfoResponse {
vpn_portal_info: Some(VpnPortalInfo {
vpn_type: vpn_portal.name(),
client_config: vpn_portal.dump_client_config(peer_mgr).await,
connected_clients: vpn_portal.list_clients().await,
}),
};
Ok(tonic::Response::new(ret))
}
}
VpnPortalRpcService {
peer_mgr: Arc::downgrade(&self.peer_manager),
vpn_portal: Arc::downgrade(&self.vpn_portal),
}
}
fn run_rpc_server(&mut self) -> Result<(), Error> {
let Some(addr) = self.global_ctx.config.get_rpc_portal() else {
tracing::info!("rpc server not enabled, because rpc_portal is not set.");
return Ok(());
@@ -313,7 +400,10 @@ impl Instance {
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()
@@ -332,7 +422,10 @@ impl Instance {
peer_center.get_rpc_service(),
),
)
.serve(addr)
.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();
@@ -342,8 +435,11 @@ impl Instance {
fn run_proxy_cidrs_route_updater(&mut self) {
let peer_mgr = self.peer_manager.clone();
let global_ctx = self.global_ctx.clone();
let net_ns = self.global_ctx.net_ns.clone();
let nic = self.virtual_nic.as_ref().unwrap().clone();
let ifcfg = nic.get_ifcfg();
let ifname = nic.ifname().to_owned();
self.tasks.spawn(async move {
let mut cur_proxy_cidrs = vec![];
@@ -358,6 +454,10 @@ impl Instance {
proxy_cidrs.push(cidr);
}
}
// add vpn portal cidr to proxy_cidrs
if let Some(vpn_cfg) = global_ctx.config.get_vpn_portal_config() {
proxy_cidrs.push(vpn_cfg.client_cidr);
}
// if route is in cur_proxy_cidrs but not in proxy_cidrs, delete it.
for cidr in cur_proxy_cidrs.iter() {
@@ -366,10 +466,9 @@ impl Instance {
}
let _g = net_ns.guard();
let ret = nic
.get_ifcfg()
let ret = ifcfg
.remove_ipv4_route(
nic.ifname(),
ifname.as_str(),
cidr.first_address(),
cidr.network_length(),
)
@@ -389,9 +488,12 @@ impl Instance {
continue;
}
let _g = net_ns.guard();
let ret = nic
.get_ifcfg()
.add_ipv4_route(nic.ifname(), cidr.first_address(), cidr.network_length())
let ret = ifcfg
.add_ipv4_route(
ifname.as_str(),
cidr.first_address(),
cidr.network_length(),
)
.await;
if ret.is_err() {
@@ -412,4 +514,8 @@ impl Instance {
pub fn get_global_ctx(&self) -> ArcGlobalCtx {
self.global_ctx.clone()
}
pub fn get_vpn_portal_inst(&self) -> Arc<Mutex<Box<dyn VpnPortal>>> {
self.vpn_portal.clone()
}
}

View File

@@ -11,12 +11,38 @@ use crate::{
netns::NetNS,
},
peers::peer_manager::PeerManager,
tunnels::{
ring_tunnel::RingTunnelListener, tcp_tunnel::TcpTunnelListener,
udp_tunnel::UdpTunnelListener, Tunnel, TunnelListener,
tunnel::{
quic::QUICTunnelListener,
ring::RingTunnelListener,
tcp::TcpTunnelListener,
udp::UdpTunnelListener,
wireguard::{WgConfig, WgTunnelListener},
Tunnel, TunnelListener,
},
};
pub fn get_listener_by_url(
l: &url::Url,
ctx: ArcGlobalCtx,
) -> Result<Box<dyn TunnelListener>, Error> {
Ok(match l.scheme() {
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
"wg" => {
let nid = ctx.get_network_identity();
let wg_config = WgConfig::new_from_network_identity(
&nid.network_name,
&nid.network_secret.unwrap_or_default(),
);
Box::new(WgTunnelListener::new(l.clone(), wg_config))
}
"quic" => Box::new(QUICTunnelListener::new(l.clone())),
_ => {
unreachable!("unsupported listener uri");
}
})
}
#[async_trait]
pub trait TunnelHandlerForListener {
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error>;
@@ -30,10 +56,16 @@ impl TunnelHandlerForListener for PeerManager {
}
}
#[derive(Debug, Clone)]
struct Listener {
inner: Arc<Mutex<dyn TunnelListener>>,
must_succ: bool,
}
pub struct ListenerManager<H> {
global_ctx: ArcGlobalCtx,
net_ns: NetNS,
listeners: Vec<Arc<Mutex<dyn TunnelListener>>>,
listeners: Vec<Listener>,
peer_manager: Arc<H>,
tasks: JoinSet<()>,
@@ -51,36 +83,42 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
}
pub async fn prepare_listeners(&mut self) -> Result<(), Error> {
self.add_listener(RingTunnelListener::new(
format!("ring://{}", self.global_ctx.get_id())
.parse()
.unwrap(),
))
self.add_listener(
RingTunnelListener::new(
format!("ring://{}", self.global_ctx.get_id())
.parse()
.unwrap(),
),
true,
)
.await?;
for l in self.global_ctx.config.get_listener_uris().iter() {
match l.scheme() {
"tcp" => {
self.add_listener(TcpTunnelListener::new(l.clone())).await?;
}
"udp" => {
self.add_listener(UdpTunnelListener::new(l.clone())).await?;
}
_ => {
log::warn!("unsupported listener uri: {}", l);
}
}
let lis = get_listener_by_url(l, self.global_ctx.clone())?;
self.add_listener(lis, true).await?;
}
if self.global_ctx.config.get_flags().enable_ipv6 {
let _ = self
.add_listener(
UdpTunnelListener::new("udp://[::]:0".parse().unwrap()),
false,
)
.await?;
}
Ok(())
}
pub async fn add_listener<Listener>(&mut self, listener: Listener) -> Result<(), Error>
pub async fn add_listener<L>(&mut self, listener: L, must_succ: bool) -> Result<(), Error>
where
Listener: TunnelListener + 'static,
L: TunnelListener + 'static,
{
let listener = Arc::new(Mutex::new(listener));
self.listeners.push(listener);
self.listeners.push(Listener {
inner: listener,
must_succ,
});
Ok(())
}
@@ -100,31 +138,36 @@ impl<H: TunnelHandlerForListener + Send + Sync + 'static + Debug> ListenerManage
tunnel_info.remote_addr.clone(),
));
tracing::info!(ret = ?ret, "conn accepted");
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,
e.to_string(),
));
tracing::error!(error = ?e, "handle conn error");
}
let peer_manager = peer_manager.clone();
let global_ctx = global_ctx.clone();
tokio::spawn(async move {
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,
e.to_string(),
));
tracing::error!(error = ?e, "handle conn error");
}
});
}
}
pub async fn run(&mut self) -> Result<(), Error> {
for listener in &self.listeners {
let _guard = self.net_ns.guard();
let addr = listener.lock().await.local_url();
let addr = listener.inner.lock().await.local_url();
log::warn!("run listener: {:?}", listener);
listener
.inner
.lock()
.await
.listen()
.await
.with_context(|| format!("failed to add listener {}", addr))?;
self.tasks.spawn(Self::run_listener(
listener.clone(),
listener.inner.clone(),
self.peer_manager.clone(),
self.global_ctx.clone(),
));
@@ -141,7 +184,7 @@ mod tests {
use crate::{
common::global_ctx::tests::get_mock_global_ctx,
tunnels::{ring_tunnel::RingTunnelConnector, TunnelConnector},
tunnel::{packet_def::ZCPacket, ring::RingTunnelConnector, TunnelConnector},
};
use super::*;
@@ -151,9 +194,12 @@ mod tests {
#[async_trait]
impl TunnelHandlerForListener for MockListenerHandler {
async fn handle_tunnel(&self, _tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
async fn handle_tunnel(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
let data = "abc";
_tunnel.pin_sink().send(data.into()).await.unwrap();
let (_recv, mut send) = tunnel.split();
let zc_packet = ZCPacket::new_with_payload(data.as_bytes());
send.send(zc_packet).await.unwrap();
Err(Error::Unknown)
}
}
@@ -166,14 +212,18 @@ mod tests {
let ring_id = format!("ring://{}", uuid::Uuid::new_v4());
listener_mgr
.add_listener(RingTunnelListener::new(ring_id.parse().unwrap()))
.add_listener(RingTunnelListener::new(ring_id.parse().unwrap()), true)
.await
.unwrap();
listener_mgr.run().await.unwrap();
let connect_once = |ring_id| async move {
let tunnel = RingTunnelConnector::new(ring_id).connect().await.unwrap();
assert_eq!(tunnel.pin_stream().next().await.unwrap().unwrap(), "abc");
let (mut recv, _send) = tunnel.split();
assert_eq!(
recv.next().await.unwrap().unwrap().payload(),
"abc".as_bytes()
);
tunnel
};

View File

@@ -0,0 +1,389 @@
use std::{
io,
net::Ipv4Addr,
pin::Pin,
task::{Context, Poll},
};
use crate::{
common::{
error::Error,
global_ctx::ArcGlobalCtx,
ifcfg::{IfConfiger, IfConfiguerTrait},
},
tunnel::{
common::{reserve_buf, FramedWriter, TunnelWrapper, ZCPacketToBytes},
packet_def::{ZCPacket, ZCPacketType, TAIL_RESERVED_SIZE},
StreamItem, Tunnel, TunnelError,
},
};
use byteorder::WriteBytesExt as _;
use bytes::{BufMut, BytesMut};
use futures::{lock::BiLock, ready, Stream};
use pin_project_lite::pin_project;
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_util::bytes::Bytes;
use tun::{create_as_async, AsyncDevice, Configuration, Device as _, Layer};
use zerocopy::{NativeEndian, NetworkEndian};
pin_project! {
pub struct TunStream {
#[pin]
l: BiLock<AsyncDevice>,
cur_buf: BytesMut,
has_packet_info: bool,
payload_offset: usize,
}
}
impl TunStream {
pub fn new(l: BiLock<AsyncDevice>, has_packet_info: bool) -> Self {
let mut payload_offset = ZCPacketType::NIC.get_packet_offsets().payload_offset;
if has_packet_info {
payload_offset -= 4;
}
Self {
l,
cur_buf: BytesMut::new(),
has_packet_info,
payload_offset,
}
}
}
impl Stream for TunStream {
type Item = StreamItem;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<StreamItem>> {
let mut self_mut = self.project();
let mut g = ready!(self_mut.l.poll_lock(cx));
reserve_buf(&mut self_mut.cur_buf, 2500, 128 * 1024);
if self_mut.cur_buf.len() == 0 {
unsafe {
self_mut.cur_buf.set_len(*self_mut.payload_offset);
}
}
let buf = self_mut.cur_buf.chunk_mut().as_mut_ptr();
let buf = unsafe { std::slice::from_raw_parts_mut(buf, 2500) };
let mut buf = ReadBuf::new(buf);
let ret = ready!(g.as_pin_mut().poll_read(cx, &mut buf));
let len = buf.filled().len();
if len == 0 {
return Poll::Ready(None);
}
unsafe { self_mut.cur_buf.advance_mut(len + TAIL_RESERVED_SIZE) };
let mut ret_buf = self_mut.cur_buf.split();
let cur_len = ret_buf.len();
ret_buf.truncate(cur_len - TAIL_RESERVED_SIZE);
match ret {
Ok(_) => Poll::Ready(Some(Ok(ZCPacket::new_from_buf(ret_buf, ZCPacketType::NIC)))),
Err(err) => {
println!("tun stream error: {:?}", err);
Poll::Ready(None)
}
}
}
}
#[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!()
}
}
/// 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),
}
}
struct TunZCPacketToBytes {
has_packet_info: bool,
}
impl TunZCPacketToBytes {
pub fn new(has_packet_info: bool) -> Self {
Self { has_packet_info }
}
pub fn fill_packet_info(
&self,
mut buf: &mut [u8],
proto: PacketProtocol,
) -> Result<(), io::Error> {
// flags is always 0
buf.write_u16::<NativeEndian>(0)?;
// write the protocol as network byte order
buf.write_u16::<NetworkEndian>(proto.into_pi_field()?)?;
Ok(())
}
}
impl ZCPacketToBytes for TunZCPacketToBytes {
fn into_bytes(&self, zc_packet: ZCPacket) -> Result<Bytes, TunnelError> {
let payload_offset = zc_packet.payload_offset();
let mut inner = zc_packet.inner();
// we have peer manager header, so payload offset must larger than 4
assert!(payload_offset >= 4);
let ret = if self.has_packet_info {
let mut inner = inner.split_off(payload_offset - 4);
let proto = infer_proto(&inner[4..]);
self.fill_packet_info(&mut inner[0..4], proto)?;
inner
} else {
inner.split_off(payload_offset)
};
tracing::debug!(?ret, ?payload_offset, "convert zc packet to tun packet");
Ok(ret.into())
}
}
pin_project! {
pub struct TunAsyncWrite {
#[pin]
l: BiLock<AsyncDevice>,
}
}
impl AsyncWrite for TunAsyncWrite {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
let self_mut = self.project();
let mut g = ready!(self_mut.l.poll_lock(cx));
g.as_pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let self_mut = self.project();
let mut g = ready!(self_mut.l.poll_lock(cx));
g.as_pin_mut().poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let self_mut = self.project();
let mut g = ready!(self_mut.l.poll_lock(cx));
g.as_pin_mut().poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<Result<usize, io::Error>> {
let self_mut = self.project();
let mut g = ready!(self_mut.l.poll_lock(cx));
g.as_pin_mut().poll_write_vectored(cx, bufs)
}
fn is_write_vectored(&self) -> bool {
true
}
}
pub struct VirtualNic {
dev_name: String,
queue_num: usize,
global_ctx: ArcGlobalCtx,
ifname: Option<String>,
ifcfg: Box<dyn IfConfiguerTrait + Send + Sync + 'static>,
}
impl VirtualNic {
pub fn new(global_ctx: ArcGlobalCtx) -> Self {
Self {
dev_name: "".to_owned(),
queue_num: 1,
global_ctx,
ifname: None,
ifcfg: Box::new(IfConfiger {}),
}
}
pub fn set_dev_name(mut self, dev_name: &str) -> Result<Self, Error> {
self.dev_name = dev_name.to_owned();
Ok(self)
}
pub fn set_queue_num(mut self, queue_num: usize) -> Result<Self, Error> {
self.queue_num = queue_num;
Ok(self)
}
async fn create_dev_ret_err(&mut self) -> Result<Box<dyn Tunnel>, Error> {
let mut config = Configuration::default();
let has_packet_info = cfg!(target_os = "macos");
config.layer(Layer::L3);
#[cfg(target_os = "linux")]
{
config.platform(|config| {
// detect protocol by ourselves for cross platform
config.packet_information(false);
});
}
#[cfg(target_os = "windows")]
{
use std::net::IpAddr;
let c = crate::arch::windows::interface_count()?;
config.name(format!("et{}_{}", self.dev_name, c));
// set a temporary address
config.address(format!("172.0.{}.3", c).parse::<IpAddr>().unwrap());
}
if self.queue_num != 1 {
todo!("queue_num != 1")
}
config.queues(self.queue_num);
config.up();
let dev = {
let _g = self.global_ctx.net_ns.guard();
create_as_async(&config)?
};
let ifname = dev.get_ref().name()?;
self.ifcfg.wait_interface_show(ifname.as_str()).await?;
let (a, b) = BiLock::new(dev);
let ft = TunnelWrapper::new(
TunStream::new(a, has_packet_info),
FramedWriter::new_with_converter(
TunAsyncWrite { l: b },
TunZCPacketToBytes::new(has_packet_info),
),
None,
);
self.ifname = Some(ifname.to_owned());
Ok(Box::new(ft))
}
pub async fn create_dev(&mut self) -> Result<Box<dyn Tunnel>, Error> {
self.create_dev_ret_err().await
}
pub fn ifname(&self) -> &str {
self.ifname.as_ref().unwrap().as_str()
}
pub async fn link_up(&self) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg.set_link_status(self.ifname(), true).await?;
Ok(())
}
pub async fn add_route(&self, address: Ipv4Addr, cidr: u8) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg
.add_ipv4_route(self.ifname(), address, cidr)
.await?;
Ok(())
}
pub async fn remove_ip(&self, ip: Option<Ipv4Addr>) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg.remove_ip(self.ifname(), ip).await?;
Ok(())
}
pub async fn add_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg
.add_ipv4_ip(self.ifname(), ip, cidr as u8)
.await?;
Ok(())
}
pub fn get_ifcfg(&self) -> impl IfConfiguerTrait {
IfConfiger {}
}
}
#[cfg(test)]
mod tests {
use crate::common::{error::Error, global_ctx::tests::get_mock_global_ctx};
use super::VirtualNic;
async fn run_test_helper() -> Result<VirtualNic, Error> {
let mut dev = VirtualNic::new(get_mock_global_ctx());
let _tunnel = dev.create_dev().await?;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
dev.link_up().await?;
dev.remove_ip(None).await?;
dev.add_ip("10.144.111.1".parse().unwrap(), 24).await?;
Ok(dev)
}
#[tokio::test]
async fn tun_test() {
let _dev = run_test_helper().await.unwrap();
// let mut stream = nic.pin_recv_stream();
// while let Some(item) = stream.next().await {
// println!("item: {:?}", item);
// }
// let framed = dev.into_framed();
// let (mut s, mut b) = framed.split();
// loop {
// let tmp = b.next().await.unwrap().unwrap();
// let tmp = EthernetPacket::new(tmp.get_bytes());
// println!("ret: {:?}", tmp.unwrap());
// }
}
}

13
easytier/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
#![allow(dead_code)]
pub mod arch;
pub mod common;
pub mod connector;
pub mod gateway;
pub mod instance;
pub mod peer_center;
pub mod peers;
pub mod rpc;
pub mod tunnel;
pub mod utils;
pub mod vpn_portal;

View File

@@ -8,6 +8,7 @@ use std::{
time::{Duration, SystemTime},
};
use crossbeam::atomic::AtomicCell;
use futures::Future;
use tokio::{
sync::{Mutex, RwLock},
@@ -37,6 +38,7 @@ static SERVICE_ID: u32 = 5;
struct PeridicJobCtx<T> {
peer_mgr: Arc<PeerManager>,
center_peer: AtomicCell<PeerId>,
job_ctx: T,
}
@@ -81,6 +83,7 @@ impl PeerCenterBase {
async move {
let ctx = Arc::new(PeridicJobCtx {
peer_mgr: peer_mgr.clone(),
center_peer: AtomicCell::new(PeerId::default()),
job_ctx,
});
loop {
@@ -89,6 +92,7 @@ impl PeerCenterBase {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
};
ctx.center_peer.store(center_peer.clone());
tracing::trace!(?center_peer, "run periodic job");
let rpc_mgr = peer_mgr.get_peer_rpc_mgr();
let _g = lock.lock().await;
@@ -226,11 +230,13 @@ impl PeerCenterInstance {
service: PeerManagerRpcService,
need_send_peers: AtomicBool,
last_report_peers: Mutex<PeerInfoForGlobalMap>,
last_center_peer: AtomicCell<PeerId>,
}
let ctx = Arc::new(Ctx {
service: PeerManagerRpcService::new(self.peer_mgr.clone()),
need_send_peers: AtomicBool::new(true),
last_report_peers: Mutex::new(PeerInfoForGlobalMap::default()),
last_center_peer: AtomicCell::new(PeerId::default()),
});
self.client
@@ -241,10 +247,14 @@ impl PeerCenterInstance {
let mut peers = PeerInfoForGlobalMap::default();
for _ in 1..10 {
peers = ctx.job_ctx.service.list_peers().await.into();
if peers == *ctx.job_ctx.last_report_peers.lock().await {
if ctx.center_peer.load() != ctx.job_ctx.last_center_peer.load() {
// if center peer changed, report peers immediately
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
if peers == *ctx.job_ctx.last_report_peers.lock().await {
return Ok(3000);
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
*ctx.job_ctx.last_report_peers.lock().await = peers.clone();
@@ -276,6 +286,7 @@ impl PeerCenterInstance {
return Ok(500);
}
ctx.job_ctx.last_center_peer.store(ctx.center_peer.load());
ctx.job_ctx.need_send_peers.store(false, Ordering::Relaxed);
Ok(3000)
})

View File

@@ -0,0 +1,34 @@
use crate::tunnel::packet_def::ZCPacket;
pub mod ring_aes_gcm;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("packet is not encrypted")]
NotEcrypted,
#[error("packet is too short. len: {0}")]
PacketTooShort(usize),
#[error("decryption failed")]
DecryptionFailed,
#[error("encryption failed")]
EncryptionFailed,
#[error("invalid tag. tag: {0:?}")]
InvalidTag(Vec<u8>),
}
pub trait Encryptor: Send + Sync + 'static {
fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error>;
fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error>;
}
pub struct NullCipher;
impl Encryptor for NullCipher {
fn encrypt(&self, _zc_packet: &mut ZCPacket) -> Result<(), Error> {
Ok(())
}
fn decrypt(&self, _zc_packet: &mut ZCPacket) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,161 @@
use rand::RngCore;
use ring::aead::{self};
use ring::aead::{LessSafeKey, UnboundKey};
use zerocopy::{AsBytes, FromBytes};
use crate::tunnel::packet_def::{AesGcmTail, ZCPacket, AES_GCM_ENCRYPTION_RESERVED};
use super::{Encryptor, Error};
#[derive(Clone)]
pub struct AesGcmCipher {
pub(crate) cipher: AesGcmEnum,
}
pub enum AesGcmEnum {
AesGCM128(LessSafeKey, [u8; 16]),
AesGCM256(LessSafeKey, [u8; 32]),
}
impl Clone for AesGcmEnum {
fn clone(&self) -> Self {
match &self {
AesGcmEnum::AesGCM128(_, key) => {
let c =
LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, key.as_slice()).unwrap());
AesGcmEnum::AesGCM128(c, *key)
}
AesGcmEnum::AesGCM256(_, key) => {
let c =
LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, key.as_slice()).unwrap());
AesGcmEnum::AesGCM256(c, *key)
}
}
}
}
impl AesGcmCipher {
pub fn new_128(key: [u8; 16]) -> Self {
let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_128_GCM, &key).unwrap());
Self {
cipher: AesGcmEnum::AesGCM128(cipher, key),
}
}
pub fn new_256(key: [u8; 32]) -> Self {
let cipher = LessSafeKey::new(UnboundKey::new(&aead::AES_256_GCM, &key).unwrap());
Self {
cipher: AesGcmEnum::AesGCM256(cipher, key),
}
}
}
impl Encryptor for AesGcmCipher {
fn decrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> {
let pm_header = zc_packet.peer_manager_header().unwrap();
if !pm_header.is_encrypted() {
return Err(Error::NotEcrypted);
}
let payload_len = zc_packet.payload().len();
if payload_len < AES_GCM_ENCRYPTION_RESERVED {
return Err(Error::PacketTooShort(zc_packet.payload().len()));
}
let text_and_tag_len = payload_len - AES_GCM_ENCRYPTION_RESERVED + 16;
let aes_tail = AesGcmTail::ref_from_suffix(zc_packet.payload()).unwrap();
let nonce = aead::Nonce::assume_unique_for_key(aes_tail.nonce.clone());
let rs = match &self.cipher {
AesGcmEnum::AesGCM128(cipher, _) => cipher.open_in_place(
nonce,
aead::Aad::empty(),
&mut zc_packet.mut_payload()[..text_and_tag_len],
),
AesGcmEnum::AesGCM256(cipher, _) => cipher.open_in_place(
nonce,
aead::Aad::empty(),
&mut zc_packet.mut_payload()[..text_and_tag_len],
),
};
if let Err(_) = rs {
return Err(Error::DecryptionFailed);
}
let pm_header = zc_packet.mut_peer_manager_header().unwrap();
pm_header.set_encrypted(false);
let old_len = zc_packet.buf_len();
zc_packet
.mut_inner()
.truncate(old_len - AES_GCM_ENCRYPTION_RESERVED);
return Ok(());
}
fn encrypt(&self, zc_packet: &mut ZCPacket) -> Result<(), Error> {
let pm_header = zc_packet.peer_manager_header().unwrap();
if pm_header.is_encrypted() {
tracing::warn!(?zc_packet, "packet is already encrypted");
return Ok(());
}
let mut tail = AesGcmTail::default();
rand::thread_rng().fill_bytes(&mut tail.nonce);
let nonce = aead::Nonce::assume_unique_for_key(tail.nonce.clone());
let rs = match &self.cipher {
AesGcmEnum::AesGCM128(cipher, _) => cipher.seal_in_place_separate_tag(
nonce,
aead::Aad::empty(),
zc_packet.mut_payload(),
),
AesGcmEnum::AesGCM256(cipher, _) => cipher.seal_in_place_separate_tag(
nonce,
aead::Aad::empty(),
zc_packet.mut_payload(),
),
};
return match rs {
Ok(tag) => {
let tag = tag.as_ref();
if tag.len() != 16 {
return Err(Error::InvalidTag(tag.to_vec()));
}
tail.tag.copy_from_slice(tag);
let pm_header = zc_packet.mut_peer_manager_header().unwrap();
pm_header.set_encrypted(true);
zc_packet.mut_inner().extend_from_slice(tail.as_bytes());
Ok(())
}
Err(_) => Err(Error::EncryptionFailed),
};
}
}
#[cfg(test)]
mod tests {
use crate::{
peers::encrypt::{ring_aes_gcm::AesGcmCipher, Encryptor},
tunnel::packet_def::{ZCPacket, AES_GCM_ENCRYPTION_RESERVED},
};
#[test]
fn test_aes_gcm_cipher() {
let key = [0u8; 16];
let cipher = AesGcmCipher::new_128(key);
let text = b"1234567";
let mut packet = ZCPacket::new_with_payload(text);
packet.fill_peer_manager_hdr(0, 0, 0);
cipher.encrypt(&mut packet).unwrap();
assert_eq!(
packet.payload().len(),
text.len() + AES_GCM_ENCRYPTION_RESERVED
);
assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), true);
cipher.decrypt(&mut packet).unwrap();
assert_eq!(packet.payload(), text);
assert_eq!(packet.peer_manager_header().unwrap().is_encrypted(), false);
}
}

View File

@@ -4,16 +4,15 @@ use std::{
};
use dashmap::DashMap;
use tokio::{
sync::{mpsc, Mutex},
task::JoinSet,
};
use tokio_util::bytes::Bytes;
use tokio::{sync::Mutex, task::JoinSet};
use crate::common::{
error::Error,
global_ctx::{ArcGlobalCtx, NetworkIdentity},
PeerId,
use crate::{
common::{
error::Error,
global_ctx::{ArcGlobalCtx, NetworkIdentity},
PeerId,
},
tunnel::packet_def::ZCPacket,
};
use super::{
@@ -21,6 +20,7 @@ use super::{
peer_conn::PeerConn,
peer_map::PeerMap,
peer_rpc::PeerRpcManager,
PacketRecvChan,
};
pub struct ForeignNetworkClient {
@@ -37,7 +37,7 @@ pub struct ForeignNetworkClient {
impl ForeignNetworkClient {
pub fn new(
global_ctx: ArcGlobalCtx,
packet_sender_to_mgr: mpsc::Sender<Bytes>,
packet_sender_to_mgr: PacketRecvChan,
peer_rpc: Arc<PeerRpcManager>,
my_peer_id: PeerId,
) -> Self {
@@ -141,6 +141,10 @@ impl ForeignNetworkClient {
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 fn get_next_hop(&self, peer_id: PeerId) -> Option<PeerId> {
if self.peer_map.has_peer(peer_id) {
return Some(peer_id.clone());
@@ -148,7 +152,7 @@ impl ForeignNetworkClient {
self.next_hop.get(&peer_id).map(|v| v.clone())
}
pub async fn send_msg(&self, msg: Bytes, peer_id: PeerId) -> Result<(), Error> {
pub async fn send_msg(&self, msg: ZCPacket, peer_id: PeerId) -> Result<(), Error> {
if let Some(next_hop) = self.get_next_hop(peer_id) {
let ret = self.peer_map.send_msg_directly(msg, next_hop).await;
if ret.is_err() {

View File

@@ -15,19 +15,21 @@ use tokio::{
},
task::JoinSet,
};
use tokio_util::bytes::Bytes;
use crate::common::{
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
PeerId,
use crate::{
common::{
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity},
PeerId,
},
tunnel::packet_def::{PacketType, ZCPacket},
};
use super::{
packet::{self},
peer_conn::PeerConn,
peer_map::PeerMap,
peer_rpc::{PeerRpcManager, PeerRpcManagerTransport},
PacketRecvChan, PacketRecvChanReceiver,
};
struct ForeignNetworkEntry {
@@ -38,7 +40,7 @@ struct ForeignNetworkEntry {
impl ForeignNetworkEntry {
fn new(
network: NetworkIdentity,
packet_sender: mpsc::Sender<Bytes>,
packet_sender: PacketRecvChan,
global_ctx: ArcGlobalCtx,
my_peer_id: PeerId,
) -> Self {
@@ -53,7 +55,7 @@ struct ForeignNetworkManagerData {
}
impl ForeignNetworkManagerData {
async fn send_msg(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
async fn send_msg(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
let network_name = self
.peer_network_map
.get(&dst_peer_id)
@@ -94,7 +96,7 @@ struct RpcTransport {
my_peer_id: PeerId,
data: Arc<ForeignNetworkManagerData>,
packet_recv: Mutex<UnboundedReceiver<Bytes>>,
packet_recv: Mutex<UnboundedReceiver<ZCPacket>>,
}
#[async_trait::async_trait]
@@ -103,11 +105,11 @@ impl PeerRpcManagerTransport for RpcTransport {
self.my_peer_id
}
async fn send(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
async fn send(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
self.data.send_msg(msg, dst_peer_id).await
}
async fn recv(&self) -> Result<Bytes, Error> {
async fn recv(&self) -> Result<ZCPacket, Error> {
if let Some(o) = self.packet_recv.lock().await.recv().await {
Ok(o)
} else {
@@ -138,14 +140,14 @@ impl ForeignNetworkService for Arc<ForeignNetworkManagerData> {
pub struct ForeignNetworkManager {
my_peer_id: PeerId,
global_ctx: ArcGlobalCtx,
packet_sender_to_mgr: mpsc::Sender<Bytes>,
packet_sender_to_mgr: PacketRecvChan,
packet_sender: mpsc::Sender<Bytes>,
packet_recv: Mutex<Option<mpsc::Receiver<Bytes>>>,
packet_sender: PacketRecvChan,
packet_recv: Mutex<Option<PacketRecvChanReceiver>>,
data: Arc<ForeignNetworkManagerData>,
rpc_mgr: Arc<PeerRpcManager>,
rpc_transport_sender: UnboundedSender<Bytes>,
rpc_transport_sender: UnboundedSender<ZCPacket>,
tasks: Mutex<JoinSet<()>>,
}
@@ -154,7 +156,7 @@ impl ForeignNetworkManager {
pub fn new(
my_peer_id: PeerId,
global_ctx: ArcGlobalCtx,
packet_sender_to_mgr: mpsc::Sender<Bytes>,
packet_sender_to_mgr: PacketRecvChan,
) -> Self {
// recv packet from all foreign networks
let (packet_sender, packet_recv) = mpsc::channel(1000);
@@ -189,7 +191,7 @@ impl ForeignNetworkManager {
}
pub async fn add_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
tracing::warn!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager");
tracing::info!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager");
let entry = self
.data
@@ -210,8 +212,13 @@ impl ForeignNetworkManager {
peer_conn.get_network_identity().network_name.clone(),
);
if entry.network.network_secret != peer_conn.get_network_identity().network_secret {
return Err(anyhow::anyhow!("network secret not match").into());
if entry.network != peer_conn.get_network_identity() {
return Err(anyhow::anyhow!(
"network secret not match. exp: {:?} real: {:?}",
entry.network,
peer_conn.get_network_identity()
)
.into());
}
Ok(entry.peer_map.add_new_peer_conn(peer_conn).await)
@@ -222,10 +229,11 @@ impl ForeignNetworkManager {
let mut s = self.global_ctx.subscribe();
self.tasks.lock().await.spawn(async move {
while let Ok(e) = s.recv().await {
tracing::warn!(?e, "global event");
if let GlobalCtxEvent::PeerRemoved(peer_id) = &e {
tracing::info!(?e, "remove peer from foreign network manager");
data.remove_peer(*peer_id);
} else if let GlobalCtxEvent::PeerConnRemoved(..) = &e {
tracing::info!(?e, "clear no conn peer from foreign network manager");
data.clear_no_conn_peer();
}
}
@@ -241,12 +249,15 @@ impl ForeignNetworkManager {
self.tasks.lock().await.spawn(async move {
while let Some(packet_bytes) = recv.recv().await {
let packet = packet::Packet::decode(&packet_bytes);
let from_peer_id = packet.from_peer.into();
let to_peer_id = packet.to_peer.into();
let Some(hdr) = packet_bytes.peer_manager_header() else {
tracing::warn!("invalid packet, skip");
continue;
};
let from_peer_id = hdr.from_peer_id.get();
let to_peer_id = hdr.to_peer_id.get();
if to_peer_id == my_node_id {
if packet.packet_type == packet::PacketType::TaRpc {
rpc_sender.send(packet_bytes.clone()).unwrap();
if hdr.packet_type == PacketType::TaRpc as u8 {
rpc_sender.send(packet_bytes).unwrap();
continue;
}
if let Err(e) = sender_to_mgr.send(packet_bytes).await {
@@ -331,10 +342,10 @@ mod tests {
let (s, _r) = tokio::sync::mpsc::channel(1000);
let peer_mgr = Arc::new(PeerManager::new(
RouteAlgoType::Ospf,
get_mock_global_ctx_with_network(Some(NetworkIdentity {
network_name: network.to_string(),
network_secret: network.to_string(),
})),
get_mock_global_ctx_with_network(Some(NetworkIdentity::new(
network.to_string(),
network.to_string(),
))),
s,
));
replace_stun_info_collector(peer_mgr.clone(), NatType::Unknown);
@@ -342,6 +353,27 @@ mod tests {
peer_mgr
}
#[tokio::test]
async fn foreign_network_basic() {
let pm_center = create_mock_peer_manager_with_mock_stun(crate::rpc::NatType::Unknown).await;
tracing::debug!("pm_center: {:?}", pm_center.my_peer_id());
let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
tracing::debug!(
"pma_net1: {:?}, pmb_net1: {:?}",
pma_net1.my_peer_id(),
pmb_net1.my_peer_id()
);
connect_peer_manager(pma_net1.clone(), pm_center.clone()).await;
connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await;
wait_route_appear(pma_net1.clone(), pmb_net1.clone())
.await
.unwrap();
assert_eq!(1, pma_net1.list_routes().await.len());
assert_eq!(1, pmb_net1.list_routes().await.len());
}
#[tokio::test]
async fn test_foreign_network_manager() {
let pm_center = create_mock_peer_manager_with_mock_stun(crate::rpc::NatType::Unknown).await;
@@ -349,11 +381,23 @@ mod tests {
create_mock_peer_manager_with_mock_stun(crate::rpc::NatType::Unknown).await;
connect_peer_manager(pm_center.clone(), pm_center2.clone()).await;
tracing::debug!(
"pm_center: {:?}, pm_center2: {:?}",
pm_center.my_peer_id(),
pm_center2.my_peer_id()
);
let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await;
connect_peer_manager(pma_net1.clone(), pm_center.clone()).await;
connect_peer_manager(pmb_net1.clone(), pm_center.clone()).await;
tracing::debug!(
"pma_net1: {:?}, pmb_net1: {:?}",
pma_net1.my_peer_id(),
pmb_net1.my_peer_id()
);
let now = std::time::Instant::now();
let mut succ = false;
while now.elapsed().as_secs() < 10 {
@@ -398,8 +442,15 @@ mod tests {
.unwrap();
assert_eq!(2, pmc_net1.list_routes().await.len());
tracing::debug!("pmc_net1: {:?}", pmc_net1.my_peer_id());
let pma_net2 = create_mock_peer_manager_for_foreign_network("net2").await;
let pmb_net2 = create_mock_peer_manager_for_foreign_network("net2").await;
tracing::debug!(
"pma_net2: {:?}, pmb_net2: {:?}",
pma_net2.my_peer_id(),
pmb_net2.my_peer_id()
);
connect_peer_manager(pma_net2.clone(), pm_center.clone()).await;
connect_peer_manager(pmb_net2.clone(), pm_center.clone()).await;
wait_route_appear(pma_net2.clone(), pmb_net2.clone())

View File

@@ -1,6 +1,8 @@
pub mod packet;
pub mod peer;
// pub mod peer_conn;
pub mod peer_conn;
pub mod peer_conn_ping;
pub mod peer_manager;
pub mod peer_map;
pub mod peer_ospf_route;
@@ -12,28 +14,29 @@ pub mod rpc_service;
pub mod foreign_network_client;
pub mod foreign_network_manager;
pub mod encrypt;
#[cfg(test)]
pub mod tests;
use tokio_util::bytes::{Bytes, BytesMut};
use crate::tunnel::packet_def::ZCPacket;
#[async_trait::async_trait]
#[auto_impl::auto_impl(Arc)]
pub trait PeerPacketFilter {
async fn try_process_packet_from_peer(
&self,
_packet: &packet::ArchivedPacket,
_data: &Bytes,
) -> Option<()> {
None
async fn try_process_packet_from_peer(&self, _zc_packet: ZCPacket) -> Option<ZCPacket> {
Some(_zc_packet)
}
}
#[async_trait::async_trait]
#[auto_impl::auto_impl(Arc)]
pub trait NicPacketFilter {
async fn try_process_packet_from_nic(&self, data: BytesMut) -> BytesMut;
async fn try_process_packet_from_nic(&self, data: &mut ZCPacket);
}
type BoxPeerPacketFilter = Box<dyn PeerPacketFilter + Send + Sync>;
type BoxNicPacketFilter = Box<dyn NicPacketFilter + Send + Sync>;
pub type PacketRecvChan = tokio::sync::mpsc::Sender<ZCPacket>;
pub type PacketRecvChanReceiver = tokio::sync::mpsc::Receiver<ZCPacket>;

View File

@@ -99,7 +99,7 @@ pub enum PacketType {
TaRpc = 6,
}
#[derive(Archive, Deserialize, Serialize, Debug)]
#[derive(Archive, Deserialize, Serialize)]
#[archive(compare(PartialEq), check_bytes)]
// Derives can be passed through to the generated type:
pub struct Packet {
@@ -109,6 +109,19 @@ pub struct Packet {
pub payload: String,
}
impl std::fmt::Debug for Packet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Packet {{ from_peer: {}, to_peer: {}, packet_type: {:?}, payload: {:?} }}",
self.from_peer,
self.to_peer,
self.packet_type,
&self.payload.as_bytes()
)
}
}
impl std::fmt::Debug for ArchivedPacket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(

View File

@@ -1,24 +1,27 @@
use std::sync::Arc;
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use tokio::{
select,
sync::{mpsc, Mutex},
task::JoinHandle,
};
use tokio_util::bytes::Bytes;
use tokio::{select, sync::mpsc, task::JoinHandle};
use tracing::Instrument;
use super::peer_conn::{PeerConn, PeerConnId};
use crate::common::{
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
PeerId,
use super::{
peer_conn::{PeerConn, PeerConnId},
PacketRecvChan,
};
use crate::rpc::PeerConnInfo;
use crate::{
common::{
error::Error,
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
PeerId,
},
tunnel::packet_def::ZCPacket,
};
type ArcPeerConn = Arc<Mutex<PeerConn>>;
type ArcPeerConn = Arc<PeerConn>;
type ConnMap = Arc<DashMap<PeerConnId, ArcPeerConn>>;
pub struct Peer {
@@ -26,18 +29,20 @@ pub struct Peer {
conns: ConnMap,
global_ctx: ArcGlobalCtx,
packet_recv_chan: mpsc::Sender<Bytes>,
packet_recv_chan: PacketRecvChan,
close_event_sender: mpsc::Sender<PeerConnId>,
close_event_listener: JoinHandle<()>,
shutdown_notifier: Arc<tokio::sync::Notify>,
default_conn_id: AtomicCell<PeerConnId>,
}
impl Peer {
pub fn new(
peer_node_id: PeerId,
packet_recv_chan: mpsc::Sender<Bytes>,
packet_recv_chan: PacketRecvChan,
global_ctx: ArcGlobalCtx,
) -> Self {
let conns: ConnMap = Arc::new(DashMap::new());
@@ -64,7 +69,7 @@ impl Peer {
if let Some((_, conn)) = conns_copy.remove(&ret) {
global_ctx_copy.issue_event(GlobalCtxEvent::PeerConnRemoved(
conn.lock().await.get_conn_info(),
conn.get_conn_info(),
));
}
}
@@ -93,27 +98,40 @@ impl Peer {
close_event_listener,
shutdown_notifier,
default_conn_id: AtomicCell::new(PeerConnId::default()),
}
}
pub async fn add_peer_conn(&self, mut conn: PeerConn) {
conn.set_close_event_sender(self.close_event_sender.clone());
conn.start_recv_loop(self.packet_recv_chan.clone());
conn.start_recv_loop(self.packet_recv_chan.clone()).await;
conn.start_pingpong();
self.global_ctx
.issue_event(GlobalCtxEvent::PeerConnAdded(conn.get_conn_info()));
self.conns
.insert(conn.get_conn_id(), Arc::new(Mutex::new(conn)));
self.conns.insert(conn.get_conn_id(), Arc::new(conn));
}
pub async fn send_msg(&self, msg: Bytes) -> Result<(), Error> {
let Some(conn) = self.conns.iter().next() else {
async fn select_conn(&self) -> Option<ArcPeerConn> {
let default_conn_id = self.default_conn_id.load();
if let Some(conn) = self.conns.get(&default_conn_id) {
return Some(conn.clone());
}
let conn = self.conns.iter().next();
if conn.is_none() {
return None;
}
let conn = conn.unwrap().clone();
self.default_conn_id.store(conn.get_conn_id());
Some(conn)
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
let Some(conn) = self.select_conn().await else {
return Err(Error::PeerNoConnectionError(self.peer_node_id));
};
let conn_clone = conn.clone();
drop(conn);
conn_clone.lock().await.send_msg(msg).await?;
conn.send_msg(msg).await?;
Ok(())
}
@@ -136,7 +154,7 @@ impl Peer {
let mut ret = Vec::new();
for conn in conns {
ret.push(conn.lock().await.get_conn_info());
ret.push(conn.get_conn_info());
}
ret
}
@@ -158,7 +176,7 @@ mod tests {
use crate::{
common::{global_ctx::tests::get_mock_global_ctx, new_peer_id},
peers::peer_conn::PeerConn,
tunnels::ring_tunnel::create_ring_tunnel_pair,
tunnel::ring::create_ring_tunnel_pair,
};
use super::Peer;

View File

@@ -0,0 +1,485 @@
use std::{
any::Any,
fmt::Debug,
pin::Pin,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
},
};
use futures::{SinkExt, StreamExt, TryFutureExt};
use prost::Message;
use tokio::{
sync::{broadcast, mpsc, Mutex},
task::JoinSet,
time::{timeout, Duration},
};
use tokio_util::sync::PollSender;
use tracing::Instrument;
use zerocopy::AsBytes;
use crate::{
common::{
config::{NetworkIdentity, NetworkSecretDigest},
error::Error,
global_ctx::ArcGlobalCtx,
PeerId,
},
peers::packet::PacketType,
rpc::{HandshakeRequest, PeerConnInfo, PeerConnStats, TunnelInfo},
tunnel::{
filter::{StatsRecorderTunnelFilter, TunnelFilter, TunnelWithFilter},
mpsc::{MpscTunnel, MpscTunnelSender},
packet_def::ZCPacket,
stats::{Throughput, WindowLatency},
Tunnel, TunnelError, ZCPacketStream,
},
};
use super::{peer_conn_ping::PeerConnPinger, PacketRecvChan};
pub type PeerConnId = uuid::Uuid;
const MAGIC: u32 = 0xd1e1a5e1;
const VERSION: u32 = 1;
pub struct PeerConn {
conn_id: PeerConnId,
my_peer_id: PeerId,
global_ctx: ArcGlobalCtx,
tunnel: Arc<Mutex<Box<dyn Any + Send + 'static>>>,
sink: MpscTunnelSender,
recv: Arc<Mutex<Option<Pin<Box<dyn ZCPacketStream>>>>>,
tunnel_info: Option<TunnelInfo>,
tasks: JoinSet<Result<(), TunnelError>>,
info: Option<HandshakeRequest>,
close_event_sender: Option<mpsc::Sender<PeerConnId>>,
ctrl_resp_sender: broadcast::Sender<ZCPacket>,
latency_stats: Arc<WindowLatency>,
throughput: Arc<Throughput>,
loss_rate_stats: Arc<AtomicU32>,
}
impl Debug for PeerConn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PeerConn")
.field("conn_id", &self.conn_id)
.field("my_peer_id", &self.my_peer_id)
.field("info", &self.info)
.finish()
}
}
impl PeerConn {
pub fn new(my_peer_id: PeerId, global_ctx: ArcGlobalCtx, tunnel: Box<dyn Tunnel>) -> Self {
let tunnel_info = tunnel.info();
let (ctrl_sender, _ctrl_receiver) = broadcast::channel(100);
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 (recv, sink) = (mpsc_tunnel.get_stream(), mpsc_tunnel.get_sink());
PeerConn {
conn_id: PeerConnId::new_v4(),
my_peer_id,
global_ctx,
tunnel: Arc::new(Mutex::new(Box::new(mpsc_tunnel))),
sink,
recv: Arc::new(Mutex::new(Some(recv))),
tunnel_info,
tasks: JoinSet::new(),
info: None,
close_event_sender: None,
ctrl_resp_sender: ctrl_sender,
latency_stats: Arc::new(WindowLatency::new(15)),
throughput,
loss_rate_stats: Arc::new(AtomicU32::new(0)),
}
}
pub fn get_conn_id(&self) -> PeerConnId {
self.conn_id
}
async fn wait_handshake(&mut self, need_retry: &mut bool) -> Result<HandshakeRequest, Error> {
*need_retry = false;
let mut locked = self.recv.lock().await;
let recv = locked.as_mut().unwrap();
let Some(rsp) = recv.next().await else {
return Err(Error::WaitRespError(
"conn closed during wait handshake response".to_owned(),
));
};
*need_retry = true;
let rsp = rsp?;
let Some(peer_mgr_hdr) = rsp.peer_manager_header() else {
return Err(Error::WaitRespError(format!(
"unexpected packet: {:?}, cannot decode peer manager hdr",
rsp
)));
};
if peer_mgr_hdr.packet_type != PacketType::HandShake as u8 {
return Err(Error::WaitRespError(format!(
"unexpected packet type: {:?}",
peer_mgr_hdr.packet_type
)));
}
let rsp = HandshakeRequest::decode(rsp.payload()).map_err(|e| {
Error::WaitRespError(format!("decode handshake response error: {:?}", e))
})?;
if rsp.network_secret_digrest.len() != std::mem::size_of::<NetworkSecretDigest>() {
return Err(Error::WaitRespError(
"invalid network secret digest".to_owned(),
));
}
return Ok(rsp);
}
async fn wait_handshake_loop(&mut self) -> Result<HandshakeRequest, Error> {
timeout(Duration::from_secs(5), async move {
loop {
let mut need_retry = true;
match self.wait_handshake(&mut need_retry).await {
Ok(rsp) => return Ok(rsp),
Err(e) => {
log::warn!("wait handshake error: {:?}", e);
if !need_retry {
return Err(e);
}
}
}
}
})
.map_err(|e| Error::WaitRespError(format!("wait handshake timeout: {:?}", e)))
.await?
}
async fn send_handshake(&mut self) -> Result<(), Error> {
let network = self.global_ctx.get_network_identity();
let mut req = HandshakeRequest {
magic: MAGIC,
my_peer_id: self.my_peer_id,
version: VERSION,
features: Vec::new(),
network_name: network.network_name.clone(),
..Default::default()
};
req.network_secret_digrest
.extend_from_slice(&network.network_secret_digest.unwrap_or_default());
let hs_req = req.encode_to_vec();
let mut zc_packet = ZCPacket::new_with_payload(hs_req.as_bytes());
zc_packet.fill_peer_manager_hdr(
self.my_peer_id,
PeerId::default(),
PacketType::HandShake as u8,
);
self.sink.send(zc_packet).await.map_err(|e| {
tracing::warn!("send handshake request error: {:?}", e);
Error::WaitRespError("send handshake request error".to_owned())
})?;
Ok(())
}
#[tracing::instrument]
pub async fn do_handshake_as_server(&mut self) -> Result<(), Error> {
let rsp = self.wait_handshake_loop().await?;
tracing::info!("handshake request: {:?}", rsp);
self.info = Some(rsp);
self.send_handshake().await?;
Ok(())
}
#[tracing::instrument]
pub async fn do_handshake_as_client(&mut self) -> Result<(), Error> {
self.send_handshake().await?;
tracing::info!("waiting for handshake request from server");
let rsp = self.wait_handshake_loop().await?;
tracing::info!("handshake response: {:?}", rsp);
self.info = Some(rsp);
Ok(())
}
pub fn handshake_done(&self) -> bool {
self.info.is_some()
}
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 close_event_sender = self.close_event_sender.clone().unwrap();
let conn_id = self.conn_id;
let ctrl_sender = self.ctrl_resp_sender.clone();
let _conn_info = self.get_conn_info();
let conn_info_for_instrument = self.get_conn_info();
self.tasks.spawn(
async move {
tracing::info!("start recving peer conn packet");
let mut task_ret = Ok(());
while let Some(ret) = stream.next().await {
if ret.is_err() {
tracing::error!(error = ?ret, "peer conn recv error");
task_ret = Err(ret.err().unwrap());
break;
}
let mut zc_packet = ret.unwrap();
let Some(peer_mgr_hdr) = zc_packet.mut_peer_manager_header() else {
tracing::error!(
"unexpected packet: {:?}, cannot decode peer manager hdr",
zc_packet
);
continue;
};
if peer_mgr_hdr.packet_type == PacketType::Ping as u8 {
peer_mgr_hdr.packet_type = PacketType::Pong as u8;
if let Err(e) = sink.send(zc_packet).await {
tracing::error!(?e, "peer conn send req error");
}
} else if peer_mgr_hdr.packet_type == PacketType::Pong as u8 {
if let Err(e) = ctrl_sender.send(zc_packet) {
tracing::error!(?e, "peer conn send ctrl resp error");
}
} else {
if sender.send(zc_packet).await.is_err() {
break;
}
}
}
tracing::info!("end recving peer conn packet");
drop(sink);
if let Err(e) = close_event_sender.send(conn_id).await {
tracing::error!(error = ?e, "peer conn close event send error");
}
task_ret
}
.instrument(
tracing::info_span!("peer conn recv loop", conn_info = ?conn_info_for_instrument),
),
);
}
pub fn start_pingpong(&mut self) {
let mut pingpong = PeerConnPinger::new(
self.my_peer_id,
self.get_peer_id(),
self.sink.clone(),
self.ctrl_resp_sender.clone(),
self.latency_stats.clone(),
self.loss_rate_stats.clone(),
);
let close_event_sender = self.close_event_sender.clone().unwrap();
let conn_id = self.conn_id;
self.tasks.spawn(async move {
pingpong.pingpong().await;
tracing::warn!(?pingpong, "pingpong task exit");
if let Err(e) = close_event_sender.send(conn_id).await {
log::warn!("close event sender error: {:?}", e);
}
Ok(())
});
}
pub async fn send_msg(&self, msg: ZCPacket) -> Result<(), Error> {
Ok(self.sink.send(msg).await?)
}
pub fn get_peer_id(&self) -> PeerId {
self.info.as_ref().unwrap().my_peer_id
}
pub fn get_network_identity(&self) -> NetworkIdentity {
let info = self.info.as_ref().unwrap();
let mut ret = NetworkIdentity {
network_name: info.network_name.clone(),
..Default::default()
};
ret.network_secret_digest = Some([0u8; 32]);
ret.network_secret_digest
.as_mut()
.unwrap()
.copy_from_slice(&info.network_secret_digrest);
ret
}
pub fn set_close_event_sender(&mut self, sender: mpsc::Sender<PeerConnId>) {
self.close_event_sender = Some(sender);
}
pub fn get_stats(&self) -> PeerConnStats {
PeerConnStats {
latency_us: self.latency_stats.get_latency_us(),
tx_bytes: self.throughput.tx_bytes(),
rx_bytes: self.throughput.rx_bytes(),
tx_packets: self.throughput.tx_packets(),
rx_packets: self.throughput.rx_packets(),
}
}
pub fn get_conn_info(&self) -> PeerConnInfo {
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(),
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,
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::common::global_ctx::tests::get_mock_global_ctx;
use crate::common::new_peer_id;
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() {
let (c, s) = create_ring_tunnel_pair();
let c_recorder = Arc::new(PacketRecorderTunnelFilter::new());
let s_recorder = Arc::new(PacketRecorderTunnelFilter::new());
let c = TunnelWithFilter::new(c, c_recorder.clone());
let s = TunnelWithFilter::new(s, s_recorder.clone());
let c_peer_id = new_peer_id();
let s_peer_id = new_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()
);
c_ret.unwrap();
s_ret.unwrap();
assert_eq!(c_recorder.sent.lock().unwrap().len(), 1);
assert_eq!(c_recorder.received.lock().unwrap().len(), 1);
assert_eq!(s_recorder.sent.lock().unwrap().len(), 1);
assert_eq!(s_recorder.received.lock().unwrap().len(), 1);
assert_eq!(c_peer.get_peer_id(), s_peer_id);
assert_eq!(s_peer.get_peer_id(), c_peer_id);
assert_eq!(c_peer.get_network_identity(), s_peer.get_network_identity());
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) {
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 c_peer_id = new_peer_id();
let s_peer_id = new_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()
);
s_peer.set_close_event_sender(tokio::sync::mpsc::channel(1).0);
s_peer
.start_recv_loop(tokio::sync::mpsc::channel(200).0)
.await;
assert!(c_ret.is_ok());
assert!(s_ret.is_ok());
let (close_send, mut close_recv) = tokio::sync::mpsc::channel(1);
c_peer.set_close_event_sender(close_send);
c_peer.start_pingpong();
c_peer
.start_recv_loop(tokio::sync::mpsc::channel(200).0)
.await;
// wait 5s, conn should not be disconnected
tokio::time::sleep(Duration::from_secs(15)).await;
if conn_closed {
assert!(close_recv.try_recv().is_ok());
} else {
assert!(close_recv.try_recv().is_err());
}
}
#[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;
}
#[tokio::test]
async fn close_tunnel_during_handshake() {
let (c, s) = create_ring_tunnel_pair();
let mut c_peer = PeerConn::new(new_peer_id(), get_mock_global_ctx(), Box::new(c));
let j = tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
drop(s);
});
timeout(Duration::from_millis(1500), c_peer.do_handshake_as_client())
.await
.unwrap()
.unwrap_err();
let _ = tokio::join!(j);
}
}

View File

@@ -0,0 +1,219 @@
use std::{
sync::{
atomic::{AtomicU32, Ordering},
Arc,
},
time::Duration,
};
use tokio::{sync::broadcast, task::JoinSet, time::timeout};
use crate::{
common::{error::Error, PeerId},
tunnel::{
mpsc::MpscTunnelSender,
packet_def::{PacketType, ZCPacket},
stats::WindowLatency,
TunnelError,
},
};
pub struct PeerConnPinger {
my_peer_id: PeerId,
peer_id: PeerId,
sink: MpscTunnelSender,
ctrl_sender: broadcast::Sender<ZCPacket>,
latency_stats: Arc<WindowLatency>,
loss_rate_stats: Arc<AtomicU32>,
tasks: JoinSet<Result<(), TunnelError>>,
}
impl std::fmt::Debug for PeerConnPinger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PeerConnPinger")
.field("my_peer_id", &self.my_peer_id)
.field("peer_id", &self.peer_id)
.finish()
}
}
impl PeerConnPinger {
pub fn new(
my_peer_id: PeerId,
peer_id: PeerId,
sink: MpscTunnelSender,
ctrl_sender: broadcast::Sender<ZCPacket>,
latency_stats: Arc<WindowLatency>,
loss_rate_stats: Arc<AtomicU32>,
) -> Self {
Self {
my_peer_id,
peer_id,
sink,
tasks: JoinSet::new(),
latency_stats,
ctrl_sender,
loss_rate_stats,
}
}
fn new_ping_packet(my_node_id: PeerId, peer_id: PeerId, seq: u32) -> ZCPacket {
let mut packet = ZCPacket::new_with_payload(&seq.to_le_bytes());
packet.fill_peer_manager_hdr(my_node_id, peer_id, PacketType::Ping as u8);
packet
}
async fn do_pingpong_once(
my_node_id: PeerId,
peer_id: PeerId,
sink: &mut MpscTunnelSender,
receiver: &mut broadcast::Receiver<ZCPacket>,
seq: u32,
) -> Result<u128, Error> {
// should add seq here. so latency can be calculated more accurately
let req = Self::new_ping_packet(my_node_id, peer_id, seq);
sink.send(req).await?;
let now = std::time::Instant::now();
// wait until we get a pong packet in ctrl_resp_receiver
let resp = timeout(Duration::from_secs(1), async {
loop {
match receiver.recv().await {
Ok(p) => {
let payload = p.payload();
let Ok(seq_buf) = payload[0..4].try_into() else {
tracing::debug!("pingpong recv invalid packet, continue");
continue;
};
let resp_seq = u32::from_le_bytes(seq_buf);
if resp_seq == seq {
break;
}
}
Err(e) => {
return Err(Error::WaitRespError(format!(
"wait ping response error: {:?}",
e
)));
}
}
}
Ok(())
})
.await;
tracing::trace!(?resp, "wait ping response done");
if resp.is_err() {
return Err(Error::WaitRespError(
"wait ping response timeout".to_owned(),
));
}
if resp.as_ref().unwrap().is_err() {
return Err(resp.unwrap().err().unwrap());
}
Ok(now.elapsed().as_micros())
}
pub async fn pingpong(&mut self) {
let sink = self.sink.clone();
let my_node_id = self.my_peer_id;
let peer_id = self.peer_id;
let latency_stats = self.latency_stats.clone();
let (ping_res_sender, mut ping_res_receiver) = tokio::sync::mpsc::channel(100);
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();
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();
if stopped_clone.load(Ordering::Relaxed) != 0 {
return Ok(());
}
while pingpong_tasks.len() > 5 {
pingpong_tasks.join_next().await;
}
let mut sink = sink.clone();
pingpong_tasks.spawn(async move {
let mut receiver = receiver.resubscribe();
let pingpong_once_ret = Self::do_pingpong_once(
my_node_id,
peer_id,
&mut sink,
&mut receiver,
req_seq,
)
.await;
if let Err(e) = ping_res_sender.send(pingpong_once_ret).await {
tracing::info!(?e, "pingpong task send result error, exit..");
};
});
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;
while let Some(ret) = ping_res_receiver.recv().await {
counter += 1;
if let Ok(lat) = ret {
latency_stats.record_latency(lat as u32);
loss_rate_stats_1.record_latency(0);
loss_rate_stats_20.record_latency(0);
} else {
loss_rate_stats_1.record_latency(1);
loss_rate_stats_20.record_latency(1);
}
let loss_rate_20: f64 = loss_rate_stats_20.get_latency_us();
let loss_rate_1: f64 = loss_rate_stats_1.get_latency_us();
tracing::trace!(
?ret,
?self,
?loss_rate_1,
?loss_rate_20,
"pingpong task recv pingpong_once result"
);
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;
}
self.loss_rate_stats
.store((loss_rate_1 * 100.0) as u32, Ordering::Relaxed);
}
stopped.store(1, Ordering::Relaxed);
ping_res_receiver.close();
}
}

View File

@@ -4,7 +4,9 @@ use std::{
sync::{Arc, Weak},
};
use anyhow::Context;
use async_trait::async_trait;
use futures::StreamExt;
use tokio::{
@@ -15,21 +17,22 @@ use tokio::{
task::JoinSet,
};
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::bytes::{Bytes, BytesMut};
use tokio_util::bytes::Bytes;
use crate::{
common::{
error::Error, global_ctx::ArcGlobalCtx, rkyv_util::extract_bytes_from_archived_string,
PeerId,
},
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
peers::{
packet, peer_conn::PeerConn, peer_rpc::PeerRpcManagerTransport,
route_trait::RouteInterface, PeerPacketFilter,
},
tunnels::{SinkItem, Tunnel, TunnelConnector},
tunnel::{
packet_def::{PacketType, ZCPacket},
SinkItem, Tunnel, TunnelConnector,
},
};
use super::{
encrypt::{ring_aes_gcm::AesGcmCipher, Encryptor, NullCipher},
foreign_network_client::ForeignNetworkClient,
foreign_network_manager::ForeignNetworkManager,
peer_conn::PeerConnId,
@@ -38,7 +41,7 @@ use super::{
peer_rip_route::BasicRoute,
peer_rpc::PeerRpcManager,
route_trait::{ArcRoute, Route},
BoxNicPacketFilter, BoxPeerPacketFilter,
BoxNicPacketFilter, BoxPeerPacketFilter, PacketRecvChanReceiver,
};
struct RpcTransport {
@@ -46,8 +49,10 @@ struct RpcTransport {
peers: Weak<PeerMap>,
foreign_peers: Mutex<Option<Weak<ForeignNetworkClient>>>,
packet_recv: Mutex<UnboundedReceiver<Bytes>>,
peer_rpc_tspt_sender: UnboundedSender<Bytes>,
packet_recv: Mutex<UnboundedReceiver<ZCPacket>>,
peer_rpc_tspt_sender: UnboundedSender<ZCPacket>,
encryptor: Arc<Box<dyn Encryptor>>,
}
#[async_trait::async_trait]
@@ -56,7 +61,7 @@ impl PeerRpcManagerTransport for RpcTransport {
self.my_peer_id
}
async fn send(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
async fn send(&self, mut msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
let foreign_peers = self
.foreign_peers
.lock()
@@ -67,21 +72,39 @@ impl PeerRpcManagerTransport for RpcTransport {
.ok_or(Error::Unknown)?;
let peers = self.peers.upgrade().ok_or(Error::Unknown)?;
let ret = peers.send_msg(msg.clone(), dst_peer_id).await;
if matches!(ret, Err(Error::RouteError(..))) && foreign_peers.has_next_hop(dst_peer_id) {
tracing::info!(
if let Some(gateway_id) = peers.get_gateway_peer_id(dst_peer_id).await {
tracing::trace!(
?dst_peer_id,
?gateway_id,
?self.my_peer_id,
"send msg to peer via gateway",
);
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")?;
}
tracing::debug!(
?dst_peer_id,
?self.my_peer_id,
"failed to send msg to peer, try foreign network",
);
return foreign_peers.send_msg(msg, dst_peer_id).await;
foreign_peers.send_msg(msg, dst_peer_id).await
} else {
Err(Error::RouteError(Some(format!(
"peermgr RpcTransport no route for dst_peer_id: {}",
dst_peer_id
))))
}
ret
}
async fn recv(&self) -> Result<Bytes, Error> {
async fn recv(&self) -> Result<ZCPacket, Error> {
if let Some(o) = self.packet_recv.lock().await.recv().await {
Ok(o)
} else {
@@ -110,7 +133,7 @@ pub struct PeerManager {
tasks: Arc<Mutex<JoinSet<()>>>,
packet_recv: Arc<Mutex<Option<mpsc::Receiver<Bytes>>>>,
packet_recv: Arc<Mutex<Option<PacketRecvChanReceiver>>>,
peers: Arc<PeerMap>,
@@ -124,6 +147,8 @@ pub struct PeerManager {
foreign_network_manager: Arc<ForeignNetworkManager>,
foreign_network_client: Arc<ForeignNetworkClient>,
encryptor: Arc<Box<dyn Encryptor>>,
}
impl Debug for PeerManager {
@@ -151,6 +176,13 @@ impl PeerManager {
my_peer_id,
));
let encryptor: Arc<Box<dyn Encryptor>> =
Arc::new(if global_ctx.get_flags().enable_encryption {
Box::new(AesGcmCipher::new_128(global_ctx.get_128_key()))
} else {
Box::new(NullCipher)
});
// TODO: remove these because we have impl pipeline processor.
let (peer_rpc_tspt_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel();
let rpc_tspt = Arc::new(RpcTransport {
@@ -159,6 +191,7 @@ impl PeerManager {
foreign_peers: Mutex::new(None),
packet_recv: Mutex::new(peer_rpc_tspt_recv),
peer_rpc_tspt_sender,
encryptor: encryptor.clone(),
});
let peer_rpc_mgr = Arc::new(PeerRpcManager::new(rpc_tspt.clone()));
@@ -208,9 +241,20 @@ impl PeerManager {
foreign_network_manager,
foreign_network_client,
encryptor,
}
}
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(
"network identity not match".to_string(),
));
}
Ok(self.peers.add_new_peer_conn(peer_conn).await)
}
pub async fn add_client_tunnel(
&self,
tunnel: Box<dyn Tunnel>,
@@ -219,8 +263,10 @@ impl PeerManager {
peer.do_handshake_as_client().await?;
let conn_id = peer.get_conn_id();
let peer_id = peer.get_peer_id();
if peer.get_network_identity() == self.global_ctx.get_network_identity() {
self.peers.add_new_peer_conn(peer).await;
if peer.get_network_identity().network_name
== self.global_ctx.get_network_identity().network_name
{
self.add_new_peer_conn(peer).await?;
} else {
self.foreign_network_client.add_new_peer_conn(peer).await;
}
@@ -244,8 +290,10 @@ impl PeerManager {
tracing::info!("add tunnel as server start");
let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel);
peer.do_handshake_as_server().await?;
if peer.get_network_identity() == self.global_ctx.get_network_identity() {
self.peers.add_new_peer_conn(peer).await;
if peer.get_network_identity().network_name
== self.global_ctx.get_network_identity().network_name
{
self.add_new_peer_conn(peer).await?;
} else {
self.foreign_network_manager.add_peer_conn(peer).await?;
}
@@ -258,39 +306,47 @@ impl PeerManager {
let my_peer_id = self.my_peer_id;
let peers = self.peers.clone();
let pipe_line = self.peer_packet_process_pipeline.clone();
let encryptor = self.encryptor.clone();
self.tasks.lock().await.spawn(async move {
log::trace!("start_peer_recv");
while let Some(ret) = recv.next().await {
log::trace!("peer recv a packet...: {:?}", ret);
let packet = packet::Packet::decode(&ret);
let from_peer_id: PeerId = packet.from_peer.into();
let to_peer_id: PeerId = packet.to_peer.into();
while let Some(mut ret) = recv.next().await {
let Some(hdr) = ret.peer_manager_header() else {
tracing::warn!(?ret, "invalid packet, skip");
continue;
};
tracing::trace!(?hdr, ?ret, "peer recv a packet...");
let from_peer_id = hdr.from_peer_id.get();
let to_peer_id = hdr.to_peer_id.get();
if to_peer_id != my_peer_id {
log::trace!(
"need forward: to_peer_id: {:?}, my_peer_id: {:?}",
to_peer_id,
my_peer_id
);
let ret = peers.send_msg(ret.clone(), to_peer_id).await;
tracing::trace!(?to_peer_id, ?my_peer_id, "need forward");
let ret = peers.send_msg(ret, to_peer_id).await;
if ret.is_err() {
log::error!(
"forward packet error: {:?}, dst: {:?}, from: {:?}",
ret,
to_peer_id,
from_peer_id
);
tracing::error!(?ret, ?to_peer_id, ?from_peer_id, "forward packet error");
}
} else {
if let Err(e) = encryptor
.decrypt(&mut ret)
.with_context(|| "decrypt failed")
{
tracing::error!(?e, "decrypt failed");
}
let mut processed = false;
let mut zc_packet = Some(ret);
let mut idx = 0;
for pipeline in pipe_line.read().await.iter().rev() {
if let Some(_) = pipeline.try_process_packet_from_peer(&packet, &ret).await
{
tracing::debug!(?zc_packet, ?idx, "try_process_packet_from_peer");
idx += 1;
zc_packet = pipeline
.try_process_packet_from_peer(zc_packet.unwrap())
.await;
if zc_packet.is_none() {
processed = true;
break;
}
}
if !processed {
tracing::error!("unexpected packet: {:?}", ret);
tracing::error!(?zc_packet, "unhandled packet");
}
}
}
@@ -321,20 +377,15 @@ impl PeerManager {
}
#[async_trait::async_trait]
impl PeerPacketFilter for NicPacketProcessor {
async fn try_process_packet_from_peer(
&self,
packet: &packet::ArchivedPacket,
data: &Bytes,
) -> Option<()> {
if packet.packet_type == packet::PacketType::Data {
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 {
tracing::trace!(?packet, "send packet to nic channel");
// TODO: use a function to get the body ref directly for zero copy
self.nic_channel
.send(extract_bytes_from_archived_string(data, &packet.payload))
.await
.unwrap();
Some(())
} else {
self.nic_channel.send(packet).await.unwrap();
None
} else {
Some(packet)
}
}
}
@@ -345,21 +396,18 @@ impl PeerManager {
// for peer rpc packet
struct PeerRpcPacketProcessor {
peer_rpc_tspt_sender: UnboundedSender<Bytes>,
peer_rpc_tspt_sender: UnboundedSender<ZCPacket>,
}
#[async_trait::async_trait]
impl PeerPacketFilter for PeerRpcPacketProcessor {
async fn try_process_packet_from_peer(
&self,
packet: &packet::ArchivedPacket,
data: &Bytes,
) -> Option<()> {
if packet.packet_type == packet::PacketType::TaRpc {
self.peer_rpc_tspt_sender.send(data.clone()).unwrap();
Some(())
} else {
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 {
self.peer_rpc_tspt_sender.send(packet).unwrap();
None
} else {
Some(packet)
}
}
}
@@ -401,7 +449,7 @@ impl PeerManager {
async fn send_route_packet(
&self,
msg: Bytes,
route_id: u8,
_route_id: u8,
dst_peer_id: PeerId,
) -> Result<(), Error> {
let foreign_client = self
@@ -409,15 +457,17 @@ impl PeerManager {
.upgrade()
.ok_or(Error::Unknown)?;
let peer_map = self.peers.upgrade().ok_or(Error::Unknown)?;
let packet_bytes: Bytes =
packet::Packet::new_route_packet(self.my_peer_id, dst_peer_id, route_id, &msg)
.into();
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) {
return foreign_client.send_msg(packet_bytes, dst_peer_id).await;
foreign_client.send_msg(zc_packet, dst_peer_id).await
} else {
peer_map.send_msg_directly(zc_packet, dst_peer_id).await
}
peer_map.send_msg_directly(packet_bytes, dst_peer_id).await
}
fn my_peer_id(&self) -> PeerId {
self.my_peer_id
@@ -450,18 +500,17 @@ impl PeerManager {
self.get_route().list_routes().await
}
async fn run_nic_packet_process_pipeline(&self, mut data: BytesMut) -> BytesMut {
async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) {
for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() {
data = pipeline.try_process_packet_from_nic(data).await;
pipeline.try_process_packet_from_nic(data).await;
}
data
}
pub async fn send_msg(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
pub async fn send_msg(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
self.peers.send_msg(msg, dst_peer_id).await
}
pub async fn send_msg_ipv4(&self, msg: BytesMut, ipv4_addr: Ipv4Addr) -> Result<(), Error> {
pub async fn send_msg_ipv4(&self, mut msg: ZCPacket, ipv4_addr: Ipv4Addr) -> Result<(), Error> {
log::trace!(
"do send_msg in peer manager, msg: {:?}, ipv4_addr: {}",
msg,
@@ -487,25 +536,37 @@ impl PeerManager {
return Ok(());
}
let msg = self.run_nic_packet_process_pipeline(msg).await;
msg.fill_peer_manager_hdr(self.my_peer_id, 0, packet::PacketType::Data as u8);
self.run_nic_packet_process_pipeline(&mut msg).await;
self.encryptor
.encrypt(&mut msg)
.with_context(|| "encrypt failed")?;
let mut errs: Vec<Error> = vec![];
for peer_id in dst_peers.iter() {
let msg: Bytes =
packet::Packet::new_data_packet(self.my_peer_id, peer_id.clone(), &msg).into();
let send_ret = self.peers.send_msg(msg.clone(), *peer_id).await;
let mut msg = Some(msg);
let total_dst_peers = dst_peers.len();
for i in 0..total_dst_peers {
let mut msg = if i == total_dst_peers - 1 {
msg.take().unwrap()
} else {
msg.clone().unwrap()
};
if matches!(send_ret, Err(Error::RouteError(..)))
&& self.foreign_network_client.has_next_hop(*peer_id)
{
let foreign_send_ret = self.foreign_network_client.send_msg(msg, *peer_id).await;
if foreign_send_ret.is_ok() {
continue;
let peer_id = &dst_peers[i];
msg.mut_peer_manager_header()
.unwrap()
.to_peer_id
.set(*peer_id);
if let Some(gateway) = self.peers.get_gateway_peer_id(*peer_id).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 let Err(send_ret) = send_ret {
errs.push(send_ret);
}
}
@@ -601,12 +662,23 @@ impl PeerManager {
#[cfg(test)]
mod tests {
use std::{fmt::Debug, sync::Arc};
use crate::{
connector::udp_hole_punch::tests::create_mock_peer_manager_with_mock_stun,
peers::tests::{connect_peer_manager, wait_for_condition, wait_route_appear},
connector::{
create_connector_by_url, udp_hole_punch::tests::create_mock_peer_manager_with_mock_stun,
},
instance::listeners::get_listener_by_url,
peers::{
peer_rpc::tests::{MockService, TestRpcService, TestRpcServiceClient},
tests::{connect_peer_manager, wait_for_condition, wait_route_appear},
},
rpc::NatType,
tunnel::{TunnelConnector, TunnelListener},
};
use super::PeerManager;
#[tokio::test]
async fn drop_peer_manager() {
let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await;
@@ -638,4 +710,98 @@ mod tests {
)
.await;
}
async fn connect_peer_manager_with<C: TunnelConnector + Debug + 'static, L: TunnelListener>(
client_mgr: Arc<PeerManager>,
server_mgr: &Arc<PeerManager>,
mut client: C,
server: &mut L,
) {
server.listen().await.unwrap();
tokio::spawn(async move {
client.set_bind_addrs(vec![]);
client_mgr.try_connect(client).await.unwrap();
});
server_mgr
.add_client_tunnel(server.accept().await.unwrap())
.await
.unwrap();
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial(forward_packet_test)]
async fn forward_packet(
#[values("tcp", "udp", "wg", "quic")] proto1: &str,
#[values("tcp", "udp", "wg", "quic")] proto2: &str,
) {
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(),
);
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(),
);
let mut listener1 = get_listener_by_url(
&format!("{}://0.0.0.0:31013", proto1).parse().unwrap(),
peer_mgr_b.get_global_ctx(),
)
.unwrap();
let connector1 = create_connector_by_url(
format!("{}://127.0.0.1:31013", proto1).as_str(),
&peer_mgr_a.get_global_ctx(),
)
.await
.unwrap();
connect_peer_manager_with(peer_mgr_a.clone(), &peer_mgr_b, connector1, &mut listener1)
.await;
wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone())
.await
.unwrap();
let mut listener2 = get_listener_by_url(
&format!("{}://0.0.0.0:31014", proto2).parse().unwrap(),
peer_mgr_c.get_global_ctx(),
)
.unwrap();
let connector2 = create_connector_by_url(
format!("{}://127.0.0.1:31014", proto2).as_str(),
&peer_mgr_b.get_global_ctx(),
)
.await
.unwrap();
connect_peer_manager_with(peer_mgr_b.clone(), &peer_mgr_c, connector2, &mut listener2)
.await;
wait_route_appear(peer_mgr_a.clone(), peer_mgr_c.clone())
.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
})
.await
.unwrap();
assert_eq!(ret, "hello c abc");
}
}

View File

@@ -2,8 +2,7 @@ use std::{net::Ipv4Addr, sync::Arc};
use anyhow::Context;
use dashmap::DashMap;
use tokio::sync::{mpsc, RwLock};
use tokio_util::bytes::Bytes;
use tokio::sync::RwLock;
use crate::{
common::{
@@ -12,29 +11,27 @@ use crate::{
PeerId,
},
rpc::PeerConnInfo,
tunnels::TunnelError,
tunnel::packet_def::ZCPacket,
tunnel::TunnelError,
};
use super::{
peer::Peer,
peer_conn::{PeerConn, PeerConnId},
route_trait::ArcRoute,
PacketRecvChan,
};
pub struct PeerMap {
global_ctx: ArcGlobalCtx,
my_peer_id: PeerId,
peer_map: DashMap<PeerId, Arc<Peer>>,
packet_send: mpsc::Sender<Bytes>,
packet_send: PacketRecvChan,
routes: RwLock<Vec<ArcRoute>>,
}
impl PeerMap {
pub fn new(
packet_send: mpsc::Sender<Bytes>,
global_ctx: ArcGlobalCtx,
my_peer_id: PeerId,
) -> Self {
pub fn new(packet_send: PacketRecvChan, global_ctx: ArcGlobalCtx, my_peer_id: PeerId) -> Self {
PeerMap {
global_ctx,
my_peer_id,
@@ -72,7 +69,7 @@ impl PeerMap {
self.peer_map.contains_key(&peer_id)
}
pub async fn send_msg_directly(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
pub async fn send_msg_directly(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
if dst_peer_id == self.my_peer_id {
return Ok(self
.packet_send
@@ -87,48 +84,47 @@ impl PeerMap {
}
None => {
log::error!("no peer for dst_peer_id: {}", dst_peer_id);
return Err(Error::RouteError(None));
return Err(Error::RouteError(Some(format!(
"peer map sengmsg directly no connected dst_peer_id: {}",
dst_peer_id
))));
}
}
Ok(())
}
pub async fn send_msg(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error> {
pub async fn get_gateway_peer_id(&self, dst_peer_id: PeerId) -> Option<PeerId> {
if dst_peer_id == self.my_peer_id {
return Ok(self
.packet_send
.send(msg)
.await
.with_context(|| "send msg to self failed")?);
return Some(dst_peer_id);
}
if self.has_peer(dst_peer_id) {
return Some(dst_peer_id);
}
// get route info
let mut gateway_peer_id = None;
for route in self.routes.read().await.iter() {
gateway_peer_id = route.get_next_hop(dst_peer_id).await;
if gateway_peer_id.is_none() {
continue;
} else {
break;
if let Some(gateway_peer_id) = route.get_next_hop(dst_peer_id).await {
// for foreign network, gateway_peer_id may not connect to me
if self.has_peer(gateway_peer_id) {
return Some(gateway_peer_id);
}
}
}
if gateway_peer_id.is_none() && self.has_peer(dst_peer_id) {
gateway_peer_id = Some(dst_peer_id);
}
None
}
let Some(gateway_peer_id) = gateway_peer_id else {
tracing::trace!(
"no gateway for dst_peer_id: {}, peers: {:?}, my_peer_id: {}",
dst_peer_id,
self.peer_map.iter().map(|v| *v.key()).collect::<Vec<_>>(),
self.my_peer_id
);
return Err(Error::RouteError(None));
pub async fn send_msg(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
let Some(gateway_peer_id) = self.get_gateway_peer_id(dst_peer_id).await else {
return Err(Error::RouteError(Some(format!(
"peer map sengmsg no gateway for dst_peer_id: {}",
dst_peer_id
))));
};
self.send_msg_directly(msg.clone(), gateway_peer_id).await?;
self.send_msg_directly(msg, gateway_peer_id).await?;
return Ok(());
}

View File

@@ -99,6 +99,7 @@ impl RoutePeerInfo {
.get_proxy_cidrs()
.iter()
.map(|x| x.to_string())
.chain(global_ctx.get_vpn_portal_cidr().map(|x| x.to_string()))
.collect(),
hostname: global_ctx.get_hostname(),
udp_stun_info: global_ctx
@@ -202,6 +203,7 @@ struct SyncRouteInfoResponse {
trait RouteService {
async fn sync_route_info(
my_peer_id: PeerId,
my_session_id: SessionId,
is_initiator: bool,
peer_infos: Option<Vec<RoutePeerInfo>>,
conn_bitmap: Option<RouteConnBitmap>,
@@ -385,6 +387,10 @@ impl RouteTable {
self.next_hop_map.get(&dst_peer_id).map(|x| *x)
}
fn peer_reachable(&self, peer_id: PeerId) -> bool {
self.next_hop_map.contains_key(&peer_id)
}
fn get_nat_type(&self, peer_id: PeerId) -> Option<NatType> {
self.peer_infos
.get(&peer_id)
@@ -407,10 +413,10 @@ impl RouteTable {
// build next hop map
self.next_hop_map.clear();
self.next_hop_map.insert(my_peer_id, (my_peer_id, 0));
for item in self.peer_infos.iter() {
let peer_id = *item.key();
if peer_id == my_peer_id {
self.next_hop_map.insert(peer_id, (peer_id, 0));
continue;
}
let Some(path) = pathfinding::prelude::bfs(
@@ -542,6 +548,15 @@ impl SyncRouteSession {
self.we_are_initiator.store(is_initiator, Ordering::Relaxed);
self.need_sync_initiator_info.store(true, Ordering::Relaxed);
}
fn update_dst_session_id(&self, session_id: SessionId) {
if session_id != self.dst_session_id.load(Ordering::Relaxed) {
tracing::warn!(?self, ?session_id, "session id mismatch, clear saved info.");
self.dst_session_id.store(session_id, Ordering::Relaxed);
self.dst_saved_conn_bitmap_version.clear();
self.dst_saved_peer_info_versions.clear();
}
}
}
struct PeerRouteServiceImpl {
@@ -617,8 +632,7 @@ impl PeerRouteServiceImpl {
.synced_route_info
.update_my_peer_info(self.my_peer_id, &self.global_ctx)
{
self.update_cached_local_conn_bitmap();
self.update_route_table();
self.update_route_table_and_cached_local_conn_bitmap();
return true;
}
false
@@ -631,8 +645,7 @@ impl PeerRouteServiceImpl {
.update_my_conn_info(self.my_peer_id, connected_peers);
if updated {
self.update_cached_local_conn_bitmap();
self.update_route_table();
self.update_route_table_and_cached_local_conn_bitmap();
}
updated
@@ -643,12 +656,27 @@ impl PeerRouteServiceImpl {
.build_from_synced_info(self.my_peer_id, &self.synced_route_info);
}
fn update_cached_local_conn_bitmap(&self) {
fn update_route_table_and_cached_local_conn_bitmap(&self) {
// update route table first because we want to filter out unreachable peers.
self.update_route_table();
// the conn_bitmap should contain complete list of directly connected peers.
// use union of dst peers can preserve this property.
let all_dst_peer_ids = self
.synced_route_info
.conn_map
.iter()
.map(|x| x.value().clone().0.into_iter())
.flatten()
.collect::<BTreeSet<_>>();
let all_peer_ids = self
.synced_route_info
.conn_map
.iter()
.map(|x| (*x.key(), x.value().1.get()))
// do not sync conn info of peers that are not reachable from any peer.
.filter(|p| all_dst_peer_ids.contains(&p.0) || self.route_table.peer_reachable(p.0))
.collect::<Vec<_>>();
let mut conn_bitmap = RouteConnBitmap::new();
@@ -680,6 +708,12 @@ impl PeerRouteServiceImpl {
{
continue;
}
// do not send unreachable peer info to dst peer.
if !self.route_table.peer_reachable(*item.key()) {
continue;
}
route_infos.push(item.value().clone());
}
@@ -770,6 +804,7 @@ impl PeerRouteServiceImpl {
.sync_route_info(
rpc_ctx,
my_peer_id,
session.my_session_id.load(Ordering::Relaxed),
session.we_are_initiator.load(Ordering::Relaxed),
peer_infos.clone(),
conn_bitmap.clone(),
@@ -790,19 +825,7 @@ impl PeerRouteServiceImpl {
.need_sync_initiator_info
.store(false, Ordering::Relaxed);
if ret.session_id != session.dst_session_id.load(Ordering::Relaxed) {
tracing::warn!(
?ret,
?my_peer_id,
?dst_peer_id,
"session id mismatch, clear saved info."
);
session
.dst_session_id
.store(ret.session_id, Ordering::Relaxed);
session.dst_saved_conn_bitmap_version.clear();
session.dst_saved_peer_info_versions.clear();
}
session.update_dst_session_id(ret.session_id);
if let Some(peer_infos) = &peer_infos {
session.update_dst_saved_peer_info_version(&peer_infos);
@@ -840,6 +863,7 @@ impl RouteService for RouteSessionManager {
self,
_: tarpc::context::Context,
from_peer_id: PeerId,
from_session_id: SessionId,
is_initiator: bool,
peer_infos: Option<Vec<RoutePeerInfo>>,
conn_bitmap: Option<RouteConnBitmap>,
@@ -853,6 +877,8 @@ impl RouteService for RouteSessionManager {
session.rpc_rx_count.fetch_add(1, Ordering::Relaxed);
session.update_dst_session_id(from_session_id);
if let Some(peer_infos) = &peer_infos {
service_impl.synced_route_info.update_peer_infos(
my_peer_id,
@@ -867,8 +893,7 @@ impl RouteService for RouteSessionManager {
session.update_dst_saved_conn_bitmap_version(conn_bitmap);
}
service_impl.update_cached_local_conn_bitmap();
service_impl.update_route_table();
service_impl.update_route_table_and_cached_local_conn_bitmap();
tracing::debug!(
"sync_route_info: from_peer_id: {:?}, is_initiator: {:?}, peer_infos: {:?}, conn_bitmap: {:?}, synced_route_info: {:?} session: {:?}, new_route_table: {:?}",
@@ -1012,7 +1037,7 @@ impl RouteSessionManager {
.map(|x| *x)
.collect::<Vec<_>>();
tracing::info!(?service_impl.my_peer_id, ?peers, ?session_peers, ?initiator_candidates, "maintain_sessions begin");
tracing::debug!(?service_impl.my_peer_id, ?peers, ?session_peers, ?initiator_candidates, "maintain_sessions begin");
if initiator_candidates.is_empty() {
next_sleep_ms = 1000;
@@ -1240,7 +1265,7 @@ impl Route for PeerRoute {
return Some(peer_id);
}
tracing::info!("no peer id for ipv4: {}", ipv4_addr);
tracing::info!(?ipv4_addr, "no peer id for ipv4");
None
}
}
@@ -1360,9 +1385,8 @@ mod tests {
let i_a = get_is_initiator(&r_a, p_b.my_peer_id());
let i_b = get_is_initiator(&r_b, p_a.my_peer_id());
assert_ne!(i_a.0, i_a.1);
assert_ne!(i_b.0, i_b.1);
assert_ne!(i_a.0, i_b.0);
assert_eq!(i_a.0, i_b.1);
assert_eq!(i_b.0, i_a.1);
drop(r_b);
drop(p_b);

View File

@@ -15,14 +15,12 @@ use tracing::Instrument;
use crate::{
common::{error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait, PeerId},
peers::{
packet,
route_trait::{Route, RouteInterfaceBox},
},
peers::route_trait::{Route, RouteInterfaceBox},
rpc::{NatType, StunInfo},
tunnel::packet_def::{PacketType, ZCPacket},
};
use super::{packet::CtrlPacketPayload, PeerPacketFilter};
use super::PeerPacketFilter;
const SEND_ROUTE_PERIOD_SEC: u64 = 60;
const SEND_ROUTE_FAST_REPLY_SEC: u64 = 5;
@@ -52,6 +50,7 @@ impl SyncPeerInfo {
.get_proxy_cidrs()
.iter()
.map(|x| x.to_string())
.chain(global_ctx.get_vpn_portal_cidr().map(|x| x.to_string()))
.collect(),
hostname: global_ctx.get_hostname(),
udp_stun_info: global_ctx
@@ -624,26 +623,15 @@ impl Route for BasicRoute {
#[async_trait::async_trait]
impl PeerPacketFilter for BasicRoute {
async fn try_process_packet_from_peer(
&self,
packet: &packet::ArchivedPacket,
_data: &Bytes,
) -> Option<()> {
if packet.packet_type == packet::PacketType::RoutePacket {
let CtrlPacketPayload::RoutePacket(route_packet) =
CtrlPacketPayload::from_packet(packet)
else {
return None;
};
self.handle_route_packet(
packet.from_peer.into(),
route_packet.body.into_boxed_slice().into(),
)
.await;
Some(())
} else {
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
let hdr = packet.peer_manager_header().unwrap();
if hdr.packet_type == PacketType::Route as u8 {
let b = packet.payload().to_vec();
self.handle_route_packet(hdr.from_peer_id.get(), b.into())
.await;
None
} else {
Some(packet)
}
}
}

View File

@@ -2,22 +2,22 @@ use std::sync::{atomic::AtomicU32, Arc};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use rkyv::Deserialize;
use prost::Message;
use tarpc::{server::Channel, transport::channel::UnboundedChannel};
use tokio::{
sync::mpsc::{self, UnboundedSender},
task::JoinSet,
};
use tokio_util::bytes::Bytes;
use tracing::Instrument;
use crate::{
common::{error::Error, PeerId},
peers::packet::Packet,
rpc::TaRpcPacket,
tunnel::packet_def::{PacketType, ZCPacket},
};
use super::packet::CtrlPacketPayload;
type PeerRpcServiceId = u32;
type PeerRpcTransactId = u32;
@@ -25,11 +25,11 @@ type PeerRpcTransactId = u32;
#[auto_impl::auto_impl(Arc)]
pub trait PeerRpcManagerTransport: Send + Sync + 'static {
fn my_peer_id(&self) -> PeerId;
async fn send(&self, msg: Bytes, dst_peer_id: PeerId) -> Result<(), Error>;
async fn recv(&self) -> Result<Bytes, Error>;
async fn send(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error>;
async fn recv(&self) -> Result<ZCPacket, Error>;
}
type PacketSender = UnboundedSender<Packet>;
type PacketSender = UnboundedSender<ZCPacket>;
struct PeerRpcEndPoint {
peer_id: PeerId,
@@ -63,16 +63,6 @@ impl std::fmt::Debug for PeerRpcManager {
}
}
#[derive(Debug)]
struct TaRpcPacketInfo {
from_peer: PeerId,
to_peer: PeerId,
service_id: PeerRpcServiceId,
transact_id: PeerRpcTransactId,
is_req: bool,
content: Vec<u8>,
}
impl PeerRpcManager {
pub fn new(tspt: impl PeerRpcManagerTransport) -> Self {
Self {
@@ -100,7 +90,7 @@ impl PeerRpcManager {
let tspt = self.tspt.clone();
let creator = Box::new(move |peer_id: PeerId| {
let mut tasks = JoinSet::new();
let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel::<Packet>();
let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel();
let (mut client_transport, server_transport) = tarpc::transport::channel::unbounded();
let server = tarpc::server::BaseChannel::with_defaults(server_transport);
@@ -122,7 +112,7 @@ impl PeerRpcManager {
continue;
};
tracing::trace!(resp = ?resp, "recv packet from client");
tracing::debug!(resp = ?resp, "server recv packet from service provider");
if resp.is_err() {
tracing::warn!(err = ?resp.err(),
"[PEER RPC MGR] client_transport in server side got channel error, ignore it.");
@@ -136,7 +126,7 @@ impl PeerRpcManager {
continue;
}
let msg = Packet::new_tarpc_packet(
let msg = Self::build_rpc_packet(
tspt.my_peer_id(),
cur_req_peer_id,
service_id,
@@ -145,12 +135,13 @@ impl PeerRpcManager {
serialized_resp.unwrap(),
);
if let Err(e) = tspt.send(msg.into(), peer_id).await {
if let Err(e) = tspt.send(msg, peer_id).await {
tracing::error!(error = ?e, peer_id = ?peer_id, service_id = ?service_id, "send resp to peer failed");
}
}
Some(packet) = packet_receiver.recv() => {
let info = Self::parse_rpc_packet(&packet);
tracing::debug!(?info, "server recv packet from peer");
if let Err(e) = info {
tracing::error!(error = ?e, packet = ?packet, "parse rpc packet failed");
continue;
@@ -168,7 +159,7 @@ impl PeerRpcManager {
}
assert_eq!(info.service_id, service_id);
cur_req_peer_id = Some(packet.from_peer.clone().into());
cur_req_peer_id = Some(info.from_peer);
cur_transact_id = info.transact_id;
tracing::trace!("recv packet from peer, packet: {:?}", packet);
@@ -219,19 +210,33 @@ impl PeerRpcManager {
)
}
fn parse_rpc_packet(packet: &Packet) -> Result<TaRpcPacketInfo, Error> {
let ctrl_packet_payload = CtrlPacketPayload::from_packet2(&packet);
match &ctrl_packet_payload {
CtrlPacketPayload::TaRpc(id, tid, is_req, body) => Ok(TaRpcPacketInfo {
from_peer: packet.from_peer.into(),
to_peer: packet.to_peer.into(),
service_id: *id,
transact_id: *tid,
is_req: *is_req,
content: body.clone(),
}),
_ => Err(Error::ShellCommandError("invalid packet".to_owned())),
}
fn parse_rpc_packet(packet: &ZCPacket) -> Result<TaRpcPacket, Error> {
let payload = packet.payload();
TaRpcPacket::decode(payload).map_err(|e| Error::MessageDecodeError(e.to_string()))
}
fn build_rpc_packet(
from_peer: PeerId,
to_peer: PeerId,
service_id: PeerRpcServiceId,
transact_id: PeerRpcTransactId,
is_req: bool,
content: Vec<u8>,
) -> ZCPacket {
let packet = TaRpcPacket {
from_peer,
to_peer,
service_id,
transact_id,
is_req,
content,
};
let mut buf = Vec::new();
packet.encode(&mut buf).unwrap();
let mut zc_packet = ZCPacket::new_with_payload(&buf);
zc_packet.fill_peer_manager_hdr(from_peer, to_peer, PacketType::TaRpc as u8);
zc_packet
}
pub fn run(&self) {
@@ -245,9 +250,9 @@ impl PeerRpcManager {
tracing::warn!("peer rpc transport read aborted, exiting");
break;
};
let packet = Packet::decode(&o);
let packet: Packet = packet.deserialize(&mut rkyv::Infallible).unwrap();
let info = Self::parse_rpc_packet(&packet).unwrap();
let info = Self::parse_rpc_packet(&o).unwrap();
tracing::debug!(?info, "recv rpc packet from peer");
if info.is_req {
if !service_registry.contains_key(&info.service_id) {
@@ -265,15 +270,15 @@ impl PeerRpcManager {
service_registry.get(&info.service_id).unwrap()(info.from_peer)
});
endpoint.packet_sender.send(packet).unwrap();
endpoint.packet_sender.send(o).unwrap();
} else {
if let Some(a) = client_resp_receivers.get(&PeerRpcClientCtxKey(
info.from_peer,
info.service_id,
info.transact_id,
)) {
log::trace!("recv resp: {:?}", packet);
if let Err(e) = a.send(packet) {
tracing::trace!("recv resp: {:?}", info);
if let Err(e) = a.send(o) {
tracing::error!(error = ?e, "send resp to client failed");
}
} else {
@@ -297,7 +302,8 @@ impl PeerRpcManager {
Fut: std::future::Future<Output = RpcRet>,
{
let mut tasks = JoinSet::new();
let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel::<Packet>();
let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel();
let (client_transport, server_transport) =
tarpc::transport::channel::unbounded::<CM, Req>();
@@ -321,7 +327,7 @@ impl PeerRpcManager {
continue;
}
let a = Packet::new_tarpc_packet(
let packet = Self::build_rpc_packet(
tspt.my_peer_id(),
dst_peer_id,
service_id,
@@ -330,7 +336,9 @@ impl PeerRpcManager {
a.unwrap(),
);
if let Err(e) = tspt.send(a.into(), dst_peer_id).await {
tracing::debug!(?packet, "client send rpc packet to peer");
if let Err(e) = tspt.send(packet, dst_peer_id).await {
tracing::error!(error = ?e, dst_peer_id = ?dst_peer_id, "send to peer failed");
}
}
@@ -342,11 +350,12 @@ impl PeerRpcManager {
while let Some(packet) = packet_receiver.recv().await {
tracing::trace!("tunnel recv: {:?}", packet);
let info = PeerRpcManager::parse_rpc_packet(&packet);
let info = Self::parse_rpc_packet(&packet);
if let Err(e) = info {
tracing::error!(error = ?e, "parse rpc packet failed");
continue;
}
tracing::debug!(?info, "client recv rpc packet from peer");
let decoded = postcard::from_bytes(&info.unwrap().content.as_slice());
if let Err(e) = decoded {
@@ -380,9 +389,11 @@ impl PeerRpcManager {
}
#[cfg(test)]
mod tests {
pub mod tests {
use std::{pin::Pin, sync::Arc};
use futures::{SinkExt, StreamExt};
use tokio_util::bytes::Bytes;
use tokio::sync::Mutex;
use crate::{
common::{error::Error, new_peer_id, PeerId},
@@ -390,7 +401,10 @@ mod tests {
peer_rpc::PeerRpcManager,
tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear},
},
tunnels::{self, ring_tunnel::create_ring_tunnel_pair},
tunnel::{
packet_def::ZCPacket, ring::create_ring_tunnel_pair, Tunnel, ZCPacketSink,
ZCPacketStream,
},
};
use super::PeerRpcManagerTransport;
@@ -401,8 +415,8 @@ mod tests {
}
#[derive(Clone)]
struct MockService {
prefix: String,
pub struct MockService {
pub prefix: String,
}
#[tarpc::server]
@@ -415,7 +429,8 @@ mod tests {
#[tokio::test]
async fn peer_rpc_basic_test() {
struct MockTransport {
tunnel: Box<dyn tunnels::Tunnel>,
sink: Arc<Mutex<Pin<Box<dyn ZCPacketSink>>>>,
stream: Arc<Mutex<Pin<Box<dyn ZCPacketStream>>>>,
my_peer_id: PeerId,
}
@@ -424,22 +439,25 @@ mod tests {
fn my_peer_id(&self) -> PeerId {
self.my_peer_id
}
async fn send(&self, msg: Bytes, _dst_peer_id: PeerId) -> Result<(), Error> {
async fn send(&self, msg: ZCPacket, _dst_peer_id: PeerId) -> Result<(), Error> {
println!("rpc mgr send: {:?}", msg);
self.tunnel.pin_sink().send(msg).await.unwrap();
self.sink.lock().await.send(msg).await.unwrap();
Ok(())
}
async fn recv(&self) -> Result<Bytes, Error> {
let ret = self.tunnel.pin_stream().next().await.unwrap();
async fn recv(&self) -> Result<ZCPacket, Error> {
let ret = self.stream.lock().await.next().await.unwrap();
println!("rpc mgr recv: {:?}", ret);
return ret.map(|v| v.freeze()).map_err(|_| Error::Unknown);
return ret.map_err(|e| e.into());
}
}
let (ct, st) = create_ring_tunnel_pair();
let (cts, ctsr) = ct.split();
let (sts, stsr) = st.split();
let server_rpc_mgr = PeerRpcManager::new(MockTransport {
tunnel: st,
sink: Arc::new(Mutex::new(ctsr)),
stream: Arc::new(Mutex::new(cts)),
my_peer_id: new_peer_id(),
});
server_rpc_mgr.run();
@@ -449,7 +467,8 @@ mod tests {
server_rpc_mgr.run_service(1, s.serve());
let client_rpc_mgr = PeerRpcManager::new(MockTransport {
tunnel: ct,
sink: Arc::new(Mutex::new(stsr)),
stream: Arc::new(Mutex::new(sts)),
my_peer_id: new_peer_id(),
});
client_rpc_mgr.run();

View File

@@ -4,7 +4,7 @@ use futures::Future;
use crate::{
common::{error::Error, global_ctx::tests::get_mock_global_ctx, PeerId},
tunnels::ring_tunnel::create_ring_tunnel_pair,
tunnel::ring::create_ring_tunnel_pair,
};
use super::peer_manager::{PeerManager, RouteAlgoType};

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize, Default)]
pub struct GetIpListResponse {
pub public_ipv4: String,
pub interface_ipv4s: Vec<String>,

View File

@@ -116,7 +116,7 @@ pub fn add_ns_to_bridge(br_name: &str, ns_name: &str) {
pub fn enable_log() {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
.with_default_directive(tracing::level_filters::LevelFilter::TRACE.into())
.from_env()
.unwrap()
.add_directive("tarpc=error".parse().unwrap());

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