mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-16 19:05:38 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b7ff0efc5 | |||
| 5833541a6e | |||
| 54c6418f97 | |||
| fc9aac42b4 | |||
| 89b43684d8 | |||
| 31b26222d3 | |||
| e4df03053e | |||
| 833e7eca22 | |||
| b7d85ad2ff | |||
| 8793560e12 | |||
| 58e0e48d59 | |||
| ad4cbbea6d | |||
| db660ee3b1 | |||
| ae54a872ce | |||
| 2aa686f7ad | |||
| ce10bf5e60 | |||
| 28ae9c447a | |||
| ff6da9bbec | |||
| 198c239399 | |||
| 0fbbea963f | |||
| 51165c54f5 | |||
| f14875aa3f | |||
| 6391dceb62 | |||
| 29806b899a | |||
| d63a3c01e4 | |||
| b6fb7ac962 | |||
| 1d22fdc972 | |||
| d135dd5a6f | |||
| 7cae63cb17 | |||
| 232165eff3 | |||
| 2bc4dd8c53 | |||
| cca105e91d | |||
| 3e52490d1b | |||
| d1293276ce | |||
| 4a5e426730 | |||
| fdc2755291 | |||
| b4fbcd8d80 | |||
| 2415cb211e | |||
| 5e51784803 | |||
| 5f0d71b0fe | |||
| 71d41f0a70 |
@@ -0,0 +1,39 @@
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY . /tmp/artifacts
|
||||
RUN mkdir -p /tmp/output; \
|
||||
cd /tmp/artifacts; \
|
||||
ARTIFACT_ARCH=""; \
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
ARTIFACT_ARCH="x86_64"; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
ARTIFACT_ARCH="aarch64"; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
cp /tmp/artifacts/easytier-linux-${ARTIFACT_ARCH}/* /tmp/output;
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
|
||||
|
||||
# users can use "-e TZ=xxx" to adjust it
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
# tcp
|
||||
EXPOSE 11010/tcp
|
||||
# udp
|
||||
EXPOSE 11010/udp
|
||||
# wg
|
||||
EXPOSE 11011/udp
|
||||
# ws
|
||||
EXPOSE 11011/tcp
|
||||
# wss
|
||||
EXPOSE 11012/tcp
|
||||
|
||||
ENTRYPOINT ["easytier-core"]
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -70,6 +70,11 @@ jobs:
|
||||
OS: windows-latest
|
||||
ARTIFACT_NAME: windows-x86_64
|
||||
|
||||
- TARGET: x86_64-unknown-freebsd
|
||||
OS: ubuntu-latest
|
||||
ARTIFACT_NAME: freebsd-13.2-x86_64
|
||||
BSD_VERSION: 13.2
|
||||
|
||||
runs-on: ${{ matrix.OS }}
|
||||
env:
|
||||
NAME: easytier
|
||||
@@ -81,10 +86,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -93,9 +94,6 @@ jobs:
|
||||
./target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Install rust target
|
||||
run: bash ./.github/workflows/install_rust.sh
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
with:
|
||||
@@ -103,13 +101,52 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Core & Cli
|
||||
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
|
||||
else
|
||||
cargo build --release --verbose --target $TARGET
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
- name: Build Core & Cli (X86_64 FreeBSD)
|
||||
uses: cross-platform-actions/action@v0.23.0
|
||||
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
|
||||
env:
|
||||
TARGET: ${{ matrix.TARGET }}
|
||||
with:
|
||||
operating_system: freebsd
|
||||
environment_variables: TARGET
|
||||
architecture: x86-64
|
||||
version: ${{ matrix.BSD_VERSION }}
|
||||
shell: bash
|
||||
memory: 5G
|
||||
cpu_count: 4
|
||||
run: |
|
||||
uname -a
|
||||
echo $SHELL
|
||||
pwd
|
||||
ls -lah
|
||||
whoami
|
||||
env | sort
|
||||
|
||||
sudo pkg install -y git protobuf
|
||||
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source $HOME/.cargo/env
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.77
|
||||
rustup default 1.77
|
||||
|
||||
export CC=clang
|
||||
export CXX=clang++
|
||||
export CARGO_TERM_COLOR=always
|
||||
|
||||
cargo build --release --verbose --target $TARGET
|
||||
|
||||
- name: Install UPX
|
||||
if: ${{ matrix.OS != 'macos-latest' }}
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
@@ -132,7 +169,7 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: EasyTier Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_id:
|
||||
description: 'The run id of EasyTier-Core Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10228239965
|
||||
required: true
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v1.2.0'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
type: boolean
|
||||
default: false
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
if: contains('["KKRainbow"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Download artifact
|
||||
id: download-artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: docker_context
|
||||
- name: List files
|
||||
run: |
|
||||
ls -l -R .
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker_context
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: .github/workflows/Dockerfile
|
||||
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -57,8 +57,8 @@ fi
|
||||
|
||||
# see https://github.com/rust-lang/rustup/issues/3709
|
||||
rustup set auto-self-update disable
|
||||
rustup install 1.79
|
||||
rustup default 1.79
|
||||
rustup install 1.77
|
||||
rustup default 1.77
|
||||
|
||||
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
@@ -72,7 +72,13 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
|
||||
rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
||||
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
||||
cd -
|
||||
|
||||
# https://github.com/rust-lang/rust/issues/128808
|
||||
# remove it after Cargo or rustc fix this.
|
||||
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
|
||||
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
|
||||
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
|
||||
fi
|
||||
else
|
||||
rustup target add $TARGET
|
||||
if [[ $GUI_TARGET != '' ]]; then
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: EasyTier Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
core_run_id:
|
||||
description: 'The run id of EasyTier-Core Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498549
|
||||
required: true
|
||||
gui_run_id:
|
||||
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498557
|
||||
required: true
|
||||
mobile_run_id:
|
||||
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
|
||||
type: number
|
||||
default: 10322498555
|
||||
required: true
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v1.2.3'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
type: boolean
|
||||
default: true
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: contains('["KKRainbow"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.core_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
repo: EasyTier/EasyTier
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Zip release assets
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
mkdir zipped_assets
|
||||
|
||||
find release_assets_nozip -type f -exec mv {} zipped_assets \;
|
||||
ls -l -R ./zipped_assets
|
||||
|
||||
cd release_assets
|
||||
ls -l -R ./
|
||||
chmod -R 755 .
|
||||
for x in `ls`; do
|
||||
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
|
||||
done
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ inputs.version }}
|
||||
draft: true
|
||||
files: |
|
||||
./zipped_assets/*
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ inputs.version }}
|
||||
Generated
+1291
-593
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,28 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
||||
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
|
||||
|
||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-5.png" width="300">
|
||||
<img src="assets/image-4.png" width="300">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
|
||||
- **Safe**: Use WireGuard protocol to encrypt data.
|
||||
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows, will support IOS and Android in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Cross-platform**: Supports MacOS/Linux/Windows/Android, will support IOS in the future. The executable file is statically linked, making deployment simple.
|
||||
- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
|
||||
- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
|
||||
- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
|
||||
@@ -32,159 +32,195 @@
|
||||
- **IPv6 Support**: Supports networking using IPv6.
|
||||
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
## Installation
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
|
||||
Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
|
||||
|
||||
2. **Install via crates.io**
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **Install from source code**
|
||||
|
||||
3. **Install from source code**
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
4. **Install by Docker Compose**
|
||||
|
||||
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
|
||||
|
||||
6. **Install by Homebrew (For MacOS Only)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
|
||||
### Two-node Networking
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
Successful execution of the command will print the following.
|
||||
|
||||
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
|
||||
2. 测试节点 A 是否可访问被代理子网下的节点
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -212,14 +247,14 @@ ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
|
||||
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
|
||||
|
||||
```
|
||||
```sh
|
||||
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
|
||||
|
||||
```
|
||||
```sh
|
||||
$> easytier-cli vpn-portal
|
||||
portal_name: wireguard
|
||||
|
||||
@@ -253,37 +288,36 @@ connected_clients:
|
||||
|
||||
可使用 ``easytier-core --help`` 查看全部配置项
|
||||
|
||||
|
||||
# 路线图
|
||||
## 路线图
|
||||
|
||||
- [ ] 完善文档和用户指南。
|
||||
- [ ] 支持 TCP 打洞等特性。
|
||||
- [ ] 支持 Android、IOS 等移动平台。
|
||||
- [ ] 支持 iOS。
|
||||
- [ ] 支持 Web 配置管理。
|
||||
|
||||
# 社区和贡献
|
||||
## 社区和贡献
|
||||
|
||||
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
|
||||
|
||||
# 相关项目和资源
|
||||
## 相关项目和资源
|
||||
|
||||
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
|
||||
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
|
||||
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
|
||||
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
|
||||
|
||||
# 许可证
|
||||
## 许可证
|
||||
|
||||
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
|
||||
|
||||
# 联系方式
|
||||
## 联系方式
|
||||
|
||||
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- QQ 群: 949700262
|
||||
- Telegram:https://t.me/easytier
|
||||
|
||||
# 赞助
|
||||
## 赞助
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
+32
-29
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,47 +12,50 @@
|
||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primevue/themes": "^4.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.4",
|
||||
"@tauri-apps/plugin-os": "2.0.0-beta.6",
|
||||
"@tauri-apps/plugin-process": "2.0.0-beta.6",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-beta.7",
|
||||
"@primevue/themes": "^4.0.4",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
|
||||
"aura": "link:@primevue/themes/aura",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia": "^2.2.1",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.0",
|
||||
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice/",
|
||||
"vue": "^3.4.31",
|
||||
"primevue": "^4.0.4",
|
||||
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
|
||||
"vue": "^3.4.38",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.0"
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.21.3",
|
||||
"@antfu/eslint-config": "^2.25.1",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@primevue/auto-import-resolver": "^4.0.0",
|
||||
"@tauri-apps/api": "2.0.0-beta.14",
|
||||
"@tauri-apps/cli": "2.0.0-beta.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"@primevue/auto-import-resolver": "^4.0.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tauri-apps/api": "2.0.0-rc.0",
|
||||
"@tauri-apps/cli": "2.0.0-rc.3",
|
||||
"@types/node": "^20.14.15",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vue-macros/volar": "^0.19.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.6.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.5.3",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"unplugin-vue-macros": "^2.9.5",
|
||||
"internal-ip": "^8.0.0",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.17.8",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"unplugin-vue-macros": "^2.11.5",
|
||||
"unplugin-vue-markdown": "^0.26.2",
|
||||
"unplugin-vue-router": "^0.8.8",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-vue-devtools": "^7.3.5",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-vue-devtools": "^7.3.8",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-tsc": "^2.0.26"
|
||||
"vue-tsc": "^2.0.29"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2119
-1372
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "1.2.0"
|
||||
version = "1.2.3"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -12,10 +12,14 @@ name = "app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0-beta", features = [ "tray-icon", "image-png", "image-ico"] }
|
||||
tauri = { version = "2.0.0-rc", features = [
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
"image-ico",
|
||||
] }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -26,22 +30,25 @@ anyhow = "1.0"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
dashmap = "5.5.3"
|
||||
dashmap = "6.0"
|
||||
|
||||
privilege = "0.3"
|
||||
gethostname = "0.4.3"
|
||||
gethostname = "0.5"
|
||||
|
||||
auto-launch = "0.5.0"
|
||||
dunce = "1.0.4"
|
||||
|
||||
tauri-plugin-shell = "2.0.0-beta.8"
|
||||
tauri-plugin-process = "2.0.0-beta.7"
|
||||
tauri-plugin-clipboard-manager = "2.1.0-beta.5"
|
||||
tauri-plugin-positioner = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-process = "2.0.0-rc"
|
||||
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
||||
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
|
||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||
tauri-plugin-os = "2.0.0-beta.7"
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-autostart = "2.0.0-rc"
|
||||
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.0.0-rc.0"
|
||||
|
||||
@@ -1,34 +1,3 @@
|
||||
fn main() {
|
||||
if !cfg!(debug_assertions) && cfg!(target_os = "windows") {
|
||||
let mut windows = tauri_build::WindowsAttributes::new();
|
||||
windows = windows.app_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
|
||||
.expect("failed to run build script");
|
||||
} else {
|
||||
tauri_build::build();
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"path:default",
|
||||
"event:default",
|
||||
"window:default",
|
||||
"window:allow-is-visible",
|
||||
"window:allow-show",
|
||||
"window:allow-hide",
|
||||
"window:allow-set-focus",
|
||||
"app:default",
|
||||
"resources:default",
|
||||
"menu:default",
|
||||
"tray:default",
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:window:allow-is-visible",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"shell:allow-open",
|
||||
"process:allow-exit",
|
||||
"clipboard-manager:allow-read-text",
|
||||
@@ -24,16 +24,16 @@
|
||||
"shell:default",
|
||||
"process:default",
|
||||
"clipboard-manager:default",
|
||||
"tray:default",
|
||||
"tray:allow-new",
|
||||
"tray:allow-set-menu",
|
||||
"tray:allow-set-title",
|
||||
"tray:allow-remove-by-id",
|
||||
"tray:allow-get-by-id",
|
||||
"tray:allow-set-icon",
|
||||
"tray:allow-set-icon-as-template",
|
||||
"tray:allow-set-show-menu-on-left-click",
|
||||
"tray:allow-set-tooltip",
|
||||
"core:tray:default",
|
||||
"core:tray:allow-new",
|
||||
"core:tray:allow-set-menu",
|
||||
"core:tray:allow-set-title",
|
||||
"core:tray:allow-remove-by-id",
|
||||
"core:tray:allow-get-by-id",
|
||||
"core:tray:allow-set-icon",
|
||||
"core:tray:allow-set-icon-as-template",
|
||||
"core:tray:allow-set-show-menu-on-left-click",
|
||||
"core:tray:allow-set-tooltip",
|
||||
"vpnservice:allow-ping",
|
||||
"vpnservice:allow-prepare-vpn",
|
||||
"vpnservice:allow-start-vpn",
|
||||
@@ -44,6 +44,10 @@
|
||||
"os:allow-arch",
|
||||
"os:allow-hostname",
|
||||
"os:allow-platform",
|
||||
"os:allow-locale"
|
||||
"os:allow-locale",
|
||||
"autostart:default",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-is-enabled"
|
||||
]
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Context;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use auto_launch::AutoLaunchBuilder;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{
|
||||
@@ -19,6 +17,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use tauri::Manager as _;
|
||||
|
||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||
enum NetworkingMethod {
|
||||
@@ -172,6 +171,13 @@ static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||
once_cell::sync::Lazy::new(Default::default);
|
||||
|
||||
#[tauri::command]
|
||||
fn is_autostart() -> Result<bool, String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
println!("{:?}", args);
|
||||
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
|
||||
}
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
@@ -224,11 +230,6 @@ fn get_os_hostname() -> Result<String, String> {
|
||||
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
|
||||
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_logging_level(level: String) -> Result<(), String> {
|
||||
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
||||
@@ -262,82 +263,19 @@ fn check_sudo() -> bool {
|
||||
use std::env::current_exe;
|
||||
let is_elevated = privilege::user::privileged();
|
||||
if !is_elevated {
|
||||
let Ok(my_exe) = current_exe() else {
|
||||
let Ok(exe) = current_exe() else {
|
||||
return true;
|
||||
};
|
||||
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
|
||||
let _ = elevated_cmd.force_prompt(true).gui(true).run();
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut elevated_cmd = privilege::runas::Command::new(exe);
|
||||
if args.contains(&AUTOSTART_ARG.to_owned()) {
|
||||
elevated_cmd.arg(AUTOSTART_ARG);
|
||||
}
|
||||
let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
|
||||
}
|
||||
is_elevated
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, _enable: bool) -> Result<bool, anyhow::Error> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
|
||||
use std::env::current_exe;
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_name = app_exe
|
||||
.file_stem()
|
||||
.and_then(|f| f.to_str())
|
||||
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
|
||||
|
||||
let app_path = app_exe
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!("failed to get app_path"))?
|
||||
.to_string();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
|
||||
// use the /Applications/easytier-gui.app
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
let path = path.parent()?.parent()?.parent()?;
|
||||
let extension = path.extension()?.to_str()?;
|
||||
match extension == "app" {
|
||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||
false => None,
|
||||
}
|
||||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
let appimage = _app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
.build()
|
||||
.with_context(|| "failed to build auto launch")?;
|
||||
|
||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||
// 避免重复设置登录项
|
||||
let _ = auto.disable();
|
||||
auto.enable()
|
||||
.with_context(|| "failed to enable auto launch")?
|
||||
} else if !enable {
|
||||
let _ = auto.disable();
|
||||
}
|
||||
|
||||
let enabled = auto.is_enabled()?;
|
||||
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -345,12 +283,41 @@ pub fn run() {
|
||||
use std::process;
|
||||
process::exit(0);
|
||||
}
|
||||
tauri::Builder::default()
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
utils::setup_panic_handler();
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
builder = builder.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![AUTOSTART_ARG]),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
app.webview_windows()
|
||||
.values()
|
||||
.next()
|
||||
.expect("Sorry, no window found")
|
||||
.set_focus()
|
||||
.expect("Can't Bring Window to Focus");
|
||||
}));
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_vpnservice::init())
|
||||
.plugin(tauri_plugin_vpnservice::init());
|
||||
|
||||
builder
|
||||
.setup(|app| {
|
||||
// for logging config
|
||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||
@@ -382,7 +349,7 @@ pub fn run() {
|
||||
toggle_window_visibility(app);
|
||||
}
|
||||
})
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../icons/icon.png"
|
||||
))?)
|
||||
.icon_as_template(false)
|
||||
@@ -396,9 +363,9 @@ pub fn run() {
|
||||
retain_network_instance,
|
||||
collect_network_infos,
|
||||
get_os_hostname,
|
||||
set_auto_launch_status,
|
||||
set_logging_level,
|
||||
set_tun_fd
|
||||
set_tun_fd,
|
||||
is_autostart
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.3",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
||||
Vendored
+2
-97
@@ -29,6 +29,7 @@ declare global {
|
||||
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isAutostart: typeof import('./composables/network')['isAutostart']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
@@ -131,11 +132,11 @@ declare module 'vue' {
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -168,102 +169,6 @@ declare module 'vue' {
|
||||
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||
readonly setTunFd: UnwrapRef<typeof import('./composables/network')['setTunFd']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly MenuItemExit: UnwrapRef<typeof import('./composables/tray')['MenuItemExit']>
|
||||
readonly MenuItemShow: UnwrapRef<typeof import('./composables/tray')['MenuItemShow']>
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly collectNetworkInfos: UnwrapRef<typeof import('./composables/network')['collectNetworkInfos']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly definePage: UnwrapRef<typeof import('unplugin-vue-router/runtime')['definePage']>
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
|
||||
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
|
||||
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
|
||||
@@ -159,14 +159,11 @@ async function watchNetworkInstance() {
|
||||
if (subscribe_running) {
|
||||
return
|
||||
}
|
||||
console.log('network instance change')
|
||||
|
||||
subscribe_running = true
|
||||
try {
|
||||
await onNetworkInstanceChange()
|
||||
} catch (_) {
|
||||
}
|
||||
console.log('network instance change done')
|
||||
subscribe_running = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ export async function getOsHostname() {
|
||||
return await invoke<string>('get_os_hostname')
|
||||
}
|
||||
|
||||
export async function setAutoLaunchStatus(enable: boolean) {
|
||||
return await invoke<boolean>('set_auto_launch_status', { enable })
|
||||
export async function isAutostart() {
|
||||
return await invoke<boolean>('is_autostart')
|
||||
}
|
||||
|
||||
export async function setLoggingLevel(level: string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getCurrent } from '@tauri-apps/api/window'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import pkg from '~/../package.json'
|
||||
@@ -6,11 +6,11 @@ import pkg from '~/../package.json'
|
||||
const DEFAULT_TRAY_NAME = 'main'
|
||||
|
||||
async function toggleVisibility() {
|
||||
if (await getCurrent().isVisible()) {
|
||||
await getCurrent().hide()
|
||||
if (await getCurrentWindow().isVisible()) {
|
||||
await getCurrentWindow().hide()
|
||||
} else {
|
||||
await getCurrent().show()
|
||||
await getCurrent().setFocus()
|
||||
await getCurrentWindow().show()
|
||||
await getCurrentWindow().setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function generateMenuItem() {
|
||||
await MenuItemExit('Exit'),
|
||||
await PredefinedMenuItem.new({ item: 'Separator' }),
|
||||
await MenuItemShow('Show / Hide'),
|
||||
] || []
|
||||
]
|
||||
}
|
||||
|
||||
export async function MenuItemExit(text: string) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { setAutoLaunchStatus } from "~/composables/network"
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(enable: boolean): Promise<boolean> {
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
const ret = await setAutoLaunchStatus(enable)
|
||||
localStorage.setItem('auto_launch', JSON.stringify(ret))
|
||||
return ret
|
||||
} catch (e) {
|
||||
target_enable ? await enable() : await disable()
|
||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||
return isEnabled()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { exit } from '@tauri-apps/plugin-process';
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import { appLogDir } from '@tauri-apps/api/path'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import Config from '~/components/Config.vue'
|
||||
import Status from '~/components/Status.vue'
|
||||
|
||||
import type { NetworkConfig } from '~/types/network'
|
||||
import { loadLanguageAsync } from '~/modules/i18n'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { loadRunningInstanceIdsFromLocalStorage } from '~/stores/network'
|
||||
import { setLoggingLevel } from '~/composables/network'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { appLogDir } from '@tauri-apps/api/path'
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { useTray } from '~/composables/tray';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||
import { useTray } from '~/composables/tray'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const visible = ref(false)
|
||||
@@ -71,7 +71,6 @@ function addNewNetwork() {
|
||||
|
||||
networkStore.$subscribe(async () => {
|
||||
networkStore.saveToLocalStorage()
|
||||
networkStore.saveRunningInstanceIdsToLocalStorage()
|
||||
try {
|
||||
await parseNetworkConfig(networkStore.curNetwork)
|
||||
messageBarSeverity.value = Severity.None
|
||||
@@ -95,6 +94,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
|
||||
try {
|
||||
await runNetworkInstance(cfg)
|
||||
networkStore.addAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
// console.error(e)
|
||||
@@ -109,6 +109,7 @@ async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
|
||||
cb()
|
||||
networkStore.removeNetworkInstance(cfg.instance_id)
|
||||
await retainNetworkInstance(networkStore.networkInstanceIds)
|
||||
networkStore.removeAutoStartInstId(cfg.instance_id)
|
||||
}
|
||||
|
||||
async function updateNetworkInfos() {
|
||||
@@ -120,10 +121,13 @@ onMounted(async () => {
|
||||
intervalId = window.setInterval(async () => {
|
||||
await updateNetworkInfos()
|
||||
}, 500)
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show'))
|
||||
])
|
||||
|
||||
window.setTimeout(async () => {
|
||||
await setTrayMenu([
|
||||
await MenuItemExit(t('tray.exit')),
|
||||
await MenuItemShow(t('tray.show')),
|
||||
])
|
||||
}, 1000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
|
||||
@@ -158,10 +162,10 @@ const setting_menu_items = ref([
|
||||
icon: 'pi pi-file',
|
||||
items: (function () {
|
||||
const levels = ['off', 'warn', 'info', 'debug', 'trace']
|
||||
let items = []
|
||||
for (let level of levels) {
|
||||
const items = []
|
||||
for (const level of levels) {
|
||||
items.push({
|
||||
label: () => t("logging_level_" + level) + (current_log_level === level ? ' ✓' : ''),
|
||||
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
|
||||
command: async () => {
|
||||
current_log_level = level
|
||||
await setLoggingLevel(level)
|
||||
@@ -175,7 +179,7 @@ const setting_menu_items = ref([
|
||||
label: () => t('logging_open_dir'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: async () => {
|
||||
console.log("open log dir", await appLogDir())
|
||||
console.log('open log dir', await appLogDir())
|
||||
await open(await appLogDir())
|
||||
},
|
||||
})
|
||||
@@ -187,7 +191,7 @@ const setting_menu_items = ref([
|
||||
},
|
||||
})
|
||||
return items
|
||||
})()
|
||||
})(),
|
||||
},
|
||||
{
|
||||
label: () => t('exit'),
|
||||
@@ -202,18 +206,22 @@ function toggle_setting_menu(event: any) {
|
||||
setting_menu.value.toggle(event)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
networkStore.loadFromLocalStorage()
|
||||
if (getAutoLaunchStatus()) {
|
||||
let prev_running_ids = loadRunningInstanceIdsFromLocalStorage()
|
||||
for (let id of prev_running_ids) {
|
||||
let cfg = networkStore.networkList.find((item) => item.instance_id === id)
|
||||
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
|
||||
getCurrentWindow().hide()
|
||||
const autoStartIds = networkStore.autoStartInstIds
|
||||
for (const id of autoStartIds) {
|
||||
const cfg = networkStore.networkList.find(item => item.instance_id === id)
|
||||
if (cfg) {
|
||||
networkStore.addNetworkInstance(cfg.instance_id)
|
||||
await runNetworkInstance(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
await initMobileVpnService()
|
||||
}
|
||||
@@ -222,7 +230,6 @@ onMounted(async () => {
|
||||
function isRunning(id: string) {
|
||||
return networkStore.networkInstanceIds.includes(id)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -275,7 +282,7 @@ function isRunning(id: string) {
|
||||
<div>{{ slotProps.option.public_server_url }}</div>
|
||||
<div
|
||||
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')">
|
||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||
{{ networkStore.instances[slotProps.option.instance_id].detail
|
||||
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,8 +302,12 @@ function isRunning(id: string) {
|
||||
<Panel class="h-full overflow-y-auto">
|
||||
<Stepper :value="activeStep">
|
||||
<StepList value="1">
|
||||
<Step value="1">{{ t('config_network') }}</Step>
|
||||
<Step value="2">{{ t('running') }}</Step>
|
||||
<Step value="1">
|
||||
{{ t('config_network') }}
|
||||
</Step>
|
||||
<Step value="2">
|
||||
{{ t('running') }}
|
||||
</Step>
|
||||
</StepList>
|
||||
<StepPanels value="1">
|
||||
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
|
||||
|
||||
@@ -14,6 +14,8 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
instances: {} as Record<string, NetworkInstance>,
|
||||
|
||||
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>,
|
||||
|
||||
autoStartInstIds: [] as string[],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,7 +76,6 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
this.instances[instanceId].error_msg = info.error_msg || ''
|
||||
this.instances[instanceId].detail = info
|
||||
}
|
||||
this.saveRunningInstanceIdsToLocalStorage()
|
||||
},
|
||||
|
||||
loadFromLocalStorage() {
|
||||
@@ -92,27 +93,43 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
|
||||
this.networkList = networkList
|
||||
this.curNetwork = this.networkList[0]
|
||||
|
||||
this.loadAutoStartInstIdsFromLocalStorage()
|
||||
},
|
||||
|
||||
saveToLocalStorage() {
|
||||
localStorage.setItem('networkList', JSON.stringify(this.networkList))
|
||||
},
|
||||
|
||||
saveRunningInstanceIdsToLocalStorage() {
|
||||
let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running)
|
||||
localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids))
|
||||
}
|
||||
saveAutoStartInstIdsToLocalStorage() {
|
||||
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
|
||||
},
|
||||
|
||||
loadAutoStartInstIdsFromLocalStorage() {
|
||||
try {
|
||||
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.autoStartInstIds = []
|
||||
}
|
||||
},
|
||||
|
||||
addAutoStartInstId(instanceId: string) {
|
||||
if (!this.autoStartInstIds.includes(instanceId)) {
|
||||
this.autoStartInstIds.push(instanceId)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
|
||||
removeAutoStartInstId(instanceId: string) {
|
||||
const idx = this.autoStartInstIds.indexOf(instanceId)
|
||||
if (idx !== -1) {
|
||||
this.autoStartInstIds.splice(idx, 1)
|
||||
}
|
||||
this.saveAutoStartInstIdsToLocalStorage()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))
|
||||
|
||||
export function loadRunningInstanceIdsFromLocalStorage(): string[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('runningInstanceIds') || '[]')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
import { internalIpV4Sync } from 'internal-ip';
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
@@ -19,6 +23,7 @@ export default defineConfig(async () => ({
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
svelte(),
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
@@ -87,15 +92,18 @@ export default defineConfig(async () => ({
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
host: '10.147.223.128',
|
||||
host: host || false,
|
||||
strictPort: true,
|
||||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ['**/src-tauri/**'],
|
||||
},
|
||||
hmr: {
|
||||
host: "10.147.223.128",
|
||||
protocol: "ws",
|
||||
},
|
||||
hmr: host
|
||||
? {
|
||||
protocol: 'ws',
|
||||
host: internalIpV4Sync(),
|
||||
port: 1430,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
+35
-19
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "1.2.0"
|
||||
version = "1.2.3"
|
||||
edition = "2021"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
@@ -43,7 +43,7 @@ time = "0.3"
|
||||
toml = "0.8.12"
|
||||
chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
gethostname = "0.4.3"
|
||||
gethostname = "0.5.0"
|
||||
|
||||
futures = { version = "0.3", features = ["bilock", "unstable"] }
|
||||
|
||||
@@ -54,7 +54,7 @@ tokio-util = { version = "0.7.9", features = ["codec", "net"] }
|
||||
async-stream = "0.3.5"
|
||||
async-trait = "0.1.74"
|
||||
|
||||
dashmap = "5.5.3"
|
||||
dashmap = "6.0"
|
||||
timedmap = "=1.0.1"
|
||||
|
||||
# for full-path zero-copy
|
||||
@@ -62,7 +62,7 @@ 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"
|
||||
tachyonix = "0.3.0"
|
||||
|
||||
quinn = { version = "0.11.0", optional = true, features = ["ring"] }
|
||||
rustls = { version = "0.23.0", features = [
|
||||
@@ -71,7 +71,7 @@ rustls = { version = "0.23.0", features = [
|
||||
rcgen = { version = "0.11.1", optional = true }
|
||||
|
||||
# for websocket
|
||||
tokio-websockets = { version = "0.8.2", optional = true, features = [
|
||||
tokio-websockets = { version = "0.8", optional = true, features = [
|
||||
"rustls-webpki-roots",
|
||||
"client",
|
||||
"server",
|
||||
@@ -84,7 +84,7 @@ http = { version = "1", default-features = false, features = [
|
||||
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
||||
|
||||
# for tap device
|
||||
tun = { package = "tun-easytier", version = "0.7.1", features = [
|
||||
tun = { package = "tun-easytier", version = "1.1.1", features = [
|
||||
"async",
|
||||
], optional = true }
|
||||
# for net ns
|
||||
@@ -105,8 +105,8 @@ once_cell = "1.18.0"
|
||||
postcard = { "version" = "1.0.8", features = ["alloc"] }
|
||||
|
||||
# for rpc
|
||||
tonic = "0.10"
|
||||
prost = "0.12"
|
||||
tonic = "0.12"
|
||||
prost = "0.13"
|
||||
anyhow = "1.0"
|
||||
tarpc = { version = "0.32", features = ["tokio1", "serde1"] }
|
||||
|
||||
@@ -126,32 +126,41 @@ bytecodec = "0.4.15"
|
||||
rand = "0.8.5"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
pnet = { version = "0.34.0", features = ["serde"] }
|
||||
pnet = { version = "0.35.0", features = ["serde"] }
|
||||
|
||||
clap = { version = "4.4.8", features = ["unicode", "derive", "wrap_help"] }
|
||||
clap = { version = "4.4.8", features = [
|
||||
"string",
|
||||
"unicode",
|
||||
"derive",
|
||||
"wrap_help",
|
||||
] }
|
||||
|
||||
async-recursion = "1.0.5"
|
||||
|
||||
network-interface = "1.1.1"
|
||||
network-interface = "2.0"
|
||||
|
||||
# for ospf route
|
||||
petgraph = "0.6.5"
|
||||
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.0", optional = true } # for encryption
|
||||
# for wireguard
|
||||
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
|
||||
|
||||
# for encryption
|
||||
ring = { version = "0.17", optional = true }
|
||||
bitflags = "2.5"
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
|
||||
# for cli
|
||||
tabled = "0.15.*"
|
||||
tabled = "0.16"
|
||||
humansize = "2.1.3"
|
||||
|
||||
base64 = "0.21.7"
|
||||
base64 = "0.22"
|
||||
|
||||
derivative = "2.2.0"
|
||||
|
||||
mimalloc-rust = { version = "0.2.1", optional = true }
|
||||
|
||||
# for mips
|
||||
indexmap = { version = "~1.9.3", optional = false, features = ["std"] }
|
||||
|
||||
atomic-shim = "0.2.0"
|
||||
@@ -168,6 +177,9 @@ parking_lot = { version = "0.12.0", optional = true }
|
||||
|
||||
wildmatch = "2.3.4"
|
||||
|
||||
rust-i18n = "3"
|
||||
sys-locale = "0.3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Networking_WinSock",
|
||||
@@ -176,10 +188,12 @@ windows-sys = { version = "0.52", features = [
|
||||
"Win32_System_IO",
|
||||
] }
|
||||
encoding = "0.2"
|
||||
winreg = "0.11"
|
||||
winreg = "0.52"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.10"
|
||||
tonic-build = "0.12"
|
||||
globwalk = "0.8.1"
|
||||
regex = "1"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
reqwest = { version = "0.11", features = ["blocking"] }
|
||||
@@ -192,10 +206,11 @@ rstest = "0.18.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
tokio-socks = "0.5.2"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun"]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"]
|
||||
full = [
|
||||
"quic",
|
||||
"websocket",
|
||||
@@ -204,9 +219,9 @@ full = [
|
||||
"aes-gcm",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"socks5",
|
||||
]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard"]
|
||||
bsd = ["aes-gcm", "mimalloc", "smoltcp"]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
|
||||
mimalloc = ["dep:mimalloc-rust"]
|
||||
@@ -220,3 +235,4 @@ websocket = [
|
||||
"dep:rcgen",
|
||||
]
|
||||
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
|
||||
socks5 = ["dep:smoltcp"]
|
||||
|
||||
@@ -86,6 +86,45 @@ impl WindowsBuild {
|
||||
}
|
||||
}
|
||||
|
||||
fn workdir() -> Option<String> {
|
||||
if let Ok(cargo_manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
||||
return Some(cargo_manifest_dir);
|
||||
}
|
||||
|
||||
let dest = std::env::var("OUT_DIR");
|
||||
if dest.is_err() {
|
||||
return None;
|
||||
}
|
||||
let dest = dest.unwrap();
|
||||
|
||||
let seperator = regex::Regex::new(r"(/target/(.+?)/build/)|(\\target\\(.+?)\\build\\)")
|
||||
.expect("Invalid regex");
|
||||
let parts = seperator.split(dest.as_str()).collect::<Vec<_>>();
|
||||
|
||||
if parts.len() >= 2 {
|
||||
return Some(parts[0].to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn check_locale() {
|
||||
let workdir = workdir().unwrap_or("./".to_string());
|
||||
|
||||
let locale_path = format!("{workdir}/**/locales/**/*");
|
||||
if let Ok(globs) = globwalk::glob(locale_path) {
|
||||
for entry in globs {
|
||||
if let Err(e) = entry {
|
||||
println!("cargo:i18n-error={}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry = entry.unwrap().into_path();
|
||||
println!("cargo:rerun-if-changed={}", entry.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(target_os = "windows")]
|
||||
WindowsBuild::check_for_win();
|
||||
@@ -98,5 +137,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.compile(&["proto/cli.proto"], &["proto/"])
|
||||
.unwrap();
|
||||
// tonic_build::compile_protos("proto/cli.proto")?;
|
||||
check_locale();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
_version: 2
|
||||
|
||||
core_clap:
|
||||
config_file:
|
||||
en: "path to the config file, NOTE: if this is set, all other options will be ignored"
|
||||
zh-CN: "配置文件路径,注意:如果设置了这个选项,其他所有选项都将被忽略"
|
||||
network_name:
|
||||
en: "network name to identify this vpn network"
|
||||
zh-CN: "用于标识此VPN网络的网络名称"
|
||||
network_secret:
|
||||
en: "network secret to verify this node belongs to the vpn network"
|
||||
zh-CN: "网络密钥,用于验证此节点属于VPN网络"
|
||||
ipv4:
|
||||
en: "ipv4 address of this vpn node, if empty, this node will only forward packets and no TUN device will be created"
|
||||
zh-CN: "此VPN节点的IPv4地址,如果为空,则此节点将仅转发数据包,不会创建TUN设备"
|
||||
dhcp:
|
||||
en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed."
|
||||
zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。"
|
||||
peers:
|
||||
en: "peers to connect initially"
|
||||
zh-CN: "最初要连接的对等节点"
|
||||
external_node:
|
||||
en: "use a public shared node to discover peers"
|
||||
zh-CN: "使用公共共享节点来发现对等节点"
|
||||
proxy_networks:
|
||||
en: "export local networks to other peers in the vpn"
|
||||
zh-CN: "将本地网络导出到VPN中的其他对等节点"
|
||||
rpc_portal:
|
||||
en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first"
|
||||
zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888"
|
||||
listeners:
|
||||
en: |+
|
||||
listeners to accept connections, allow format:
|
||||
port number: <11010>. means tcp/udp will listen on 11010, ws/wss will listen on 11010 and 11011, wg will listen on 11011
|
||||
url: <tcp://0.0.0.0:11010>. tcp can be tcp, udp, ring, wg, ws, wss\n
|
||||
proto & port pair: <proto:port>. wg:11011, means listen on 11011 with wireguard protocol url and proto:port can occur multiple times.
|
||||
zh-CN: |+
|
||||
监听器用于接受连接,允许以下格式:
|
||||
端口号:<11010>,意味着tcp/udp将在11010端口监听,ws/wss将在11010和11011端口监听,wg将在11011端口监听。
|
||||
url:<tcp://0.0.0.0:11010>,其中tcp可以是tcp、udp、ring、wg、ws、wss协议。
|
||||
协议和端口对:<proto:port>,例如wg:11011,表示使用WireGuard协议在11011端口监听。URL 和 协议端口对 可以多次出现。
|
||||
no_listener:
|
||||
en: "do not listen on any port, only connect to peers"
|
||||
zh-CN: "不监听任何端口,只连接到对等节点"
|
||||
console_log_level:
|
||||
en: "console log level"
|
||||
zh-CN: "控制台日志级别"
|
||||
file_log_level:
|
||||
en: "file log level"
|
||||
zh-CN: "文件日志级别"
|
||||
file_log_dir:
|
||||
en: "directory to store log files"
|
||||
zh-CN: "存储日志文件的目录"
|
||||
hostname:
|
||||
en: "host name to identify this device"
|
||||
zh-CN: "用于标识此设备的主机名"
|
||||
instance_name:
|
||||
en: "instance name to identify this vpn node in same machine"
|
||||
zh-CN: "实例名称,用于在同一台机器上标识此VPN节点"
|
||||
vpn_portal:
|
||||
en: "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"
|
||||
zh-CN: "定义VPN门户的URL,允许其他VPN客户端连接。示例:wg://0.0.0.0:11010/10.14.14.0/24,表示VPN门户是监听在vpn.example.com:11010的wireguard服务器,VPN客户端在10.14.14.0/24网络中"
|
||||
default_protocol:
|
||||
en: "default protocol to use when connecting to peers"
|
||||
zh-CN: "连接到对等节点时使用的默认协议"
|
||||
disable_encryption:
|
||||
en: "disable encryption for peers communication, default is false, must be same with peers"
|
||||
zh-CN: "禁用对等节点通信的加密,默认为false,必须与对等节点相同"
|
||||
multi_thread:
|
||||
en: "use multi-thread runtime, default is single-thread"
|
||||
zh-CN: "使用多线程运行时,默认为单线程"
|
||||
disable_ipv6:
|
||||
en: "do not use ipv6"
|
||||
zh-CN: "不使用IPv6"
|
||||
dev_name:
|
||||
en: "optional tun interface name"
|
||||
zh-CN: "可选的TUN接口名称"
|
||||
mtu:
|
||||
en: "mtu of the TUN device, default is 1420 for non-encryption, 1400 for encryption"
|
||||
zh-CN: "TUN设备的MTU,默认为非加密时为1420,加密时为1400"
|
||||
latency_first:
|
||||
en: "latency first mode, will try to relay traffic with lowest latency path, default is using shortest path"
|
||||
zh-CN: "延迟优先模式,将尝试使用最低延迟路径转发流量,默认使用最短路径"
|
||||
exit_nodes:
|
||||
en: "exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list"
|
||||
zh-CN: "转发所有流量的出口节点,虚拟IPv4地址,优先级由列表顺序决定"
|
||||
enable_exit_node:
|
||||
en: "allow this node to be an exit node"
|
||||
zh-CN: "允许此节点成为出口节点"
|
||||
no_tun:
|
||||
en: "do not create TUN device, can use subnet proxy to access node"
|
||||
zh-CN: "不创建TUN设备,可以使用子网代理访问节点"
|
||||
use_smoltcp:
|
||||
en: "enable smoltcp stack for subnet proxy"
|
||||
zh-CN: "为子网代理启用smoltcp堆栈"
|
||||
manual_routes:
|
||||
en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16"
|
||||
zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16"
|
||||
relay_network_whitelist:
|
||||
en: |+
|
||||
only forward traffic from the whitelist networks, supporting wildcard strings, multiple network names can be separated by spaces.
|
||||
if this parameter is empty, forwarding is disabled. by default, all networks are allowed.
|
||||
e.g.: '*' (all networks), 'def*' (networks with the prefix 'def'), 'net1 net2' (only allow net1 and net2)"
|
||||
zh-CN: |+
|
||||
仅转发白名单网络的流量,支持通配符字符串。多个网络名称间可以使用英文空格间隔。
|
||||
如果该参数为空,则禁用转发。默认允许所有网络。
|
||||
例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2)"
|
||||
disable_p2p:
|
||||
en: "disable p2p communication, will only relay packets with peers specified by --peers"
|
||||
zh-CN: "禁用P2P通信,只通过--peers指定的节点转发数据包"
|
||||
relay_all_peer_rpc:
|
||||
en: "relay all peer rpc packets, even if the peer is not in the relay network whitelist. this can help peers not in relay network whitelist to establish p2p connection."
|
||||
zh-CN: "转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。这可以帮助白名单外网络中的对等节点建立P2P连接。"
|
||||
socks5:
|
||||
en: "enable socks5 server, allow socks5 client to access virtual network. format: <port>, e.g.: 1080"
|
||||
zh-CN: "启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080"
|
||||
@@ -30,6 +30,8 @@ message PeerConnInfo {
|
||||
TunnelInfo tunnel = 5;
|
||||
PeerConnStats stats = 6;
|
||||
float loss_rate = 7;
|
||||
bool is_client = 8;
|
||||
string network_name = 9;
|
||||
}
|
||||
|
||||
message PeerInfo {
|
||||
@@ -39,7 +41,10 @@ message PeerInfo {
|
||||
|
||||
message ListPeerRequest {}
|
||||
|
||||
message ListPeerResponse { repeated PeerInfo peer_infos = 1; }
|
||||
message ListPeerResponse {
|
||||
repeated PeerInfo peer_infos = 1;
|
||||
NodeInfo my_info = 2;
|
||||
}
|
||||
|
||||
enum NatType {
|
||||
// has NAT; but own a single public IP, port is not changed
|
||||
@@ -73,6 +78,21 @@ message Route {
|
||||
string inst_id = 8;
|
||||
}
|
||||
|
||||
message NodeInfo {
|
||||
uint32 peer_id = 1;
|
||||
string ipv4_addr = 2;
|
||||
repeated string proxy_cidrs = 3;
|
||||
string hostname = 4;
|
||||
StunInfo stun_info = 5;
|
||||
string inst_id = 6;
|
||||
repeated string listeners = 7;
|
||||
string config = 8;
|
||||
}
|
||||
|
||||
message ShowNodeInfoRequest {}
|
||||
|
||||
message ShowNodeInfoResponse { NodeInfo node_info = 1; }
|
||||
|
||||
message ListRouteRequest {}
|
||||
|
||||
message ListRouteResponse { repeated Route routes = 1; }
|
||||
@@ -95,6 +115,7 @@ service PeerManageRpc {
|
||||
rpc DumpRoute(DumpRouteRequest) returns (DumpRouteResponse);
|
||||
rpc ListForeignNetwork(ListForeignNetworkRequest)
|
||||
returns (ListForeignNetworkResponse);
|
||||
rpc ShowNodeInfo(ShowNodeInfoRequest) returns (ShowNodeInfoResponse);
|
||||
}
|
||||
|
||||
enum ConnectorStatus {
|
||||
|
||||
@@ -64,6 +64,9 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>>;
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>);
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url>;
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>);
|
||||
|
||||
fn dump(&self) -> String;
|
||||
}
|
||||
|
||||
@@ -171,6 +174,10 @@ pub struct Flags {
|
||||
pub use_smoltcp: bool,
|
||||
#[derivative(Default(value = "\"*\".to_string()"))]
|
||||
pub foreign_network_whitelist: String,
|
||||
#[derivative(Default(value = "false"))]
|
||||
pub disable_p2p: bool,
|
||||
#[derivative(Default(value = "false"))]
|
||||
pub relay_all_peer_rpc: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
@@ -197,6 +204,8 @@ struct Config {
|
||||
|
||||
routes: Option<Vec<cidr::Ipv4Cidr>>,
|
||||
|
||||
socks5_proxy: Option<url::Url>,
|
||||
|
||||
flags: Option<Flags>,
|
||||
}
|
||||
|
||||
@@ -258,21 +267,15 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
|
||||
match hostname {
|
||||
Some(hostname) => {
|
||||
let hostname = hostname
|
||||
.chars()
|
||||
.filter(|c| !c.is_control())
|
||||
.take(32)
|
||||
.collect::<String>();
|
||||
|
||||
if !hostname.is_empty() {
|
||||
let mut name = hostname
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
|
||||
.take(32)
|
||||
.collect::<String>();
|
||||
|
||||
if name.len() > 32 {
|
||||
name = name.chars().take(32).collect::<String>();
|
||||
}
|
||||
|
||||
if hostname != name {
|
||||
self.set_hostname(Some(name.clone()));
|
||||
}
|
||||
name
|
||||
self.set_hostname(Some(hostname.clone()));
|
||||
hostname
|
||||
} else {
|
||||
self.set_hostname(None);
|
||||
gethostname::gethostname().to_string_lossy().to_string()
|
||||
@@ -502,6 +505,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
|
||||
self.config.lock().unwrap().routes = routes;
|
||||
}
|
||||
|
||||
fn get_socks5_portal(&self) -> Option<url::Url> {
|
||||
self.config.lock().unwrap().socks5_proxy.clone()
|
||||
}
|
||||
|
||||
fn set_socks5_portal(&self, addr: Option<url::Url>) {
|
||||
self.config.lock().unwrap().socks5_proxy = addr;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -384,6 +384,10 @@ impl IfConfiguerTrait for WindowsIfConfiger {
|
||||
}
|
||||
|
||||
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
|
||||
let _ = run_shell_cmd(
|
||||
format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(),
|
||||
)
|
||||
.await;
|
||||
run_shell_cmd(
|
||||
format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(),
|
||||
)
|
||||
@@ -395,7 +399,7 @@ pub struct DummyIfConfiger {}
|
||||
#[async_trait]
|
||||
impl IfConfiguerTrait for DummyIfConfiger {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
pub type IfConfiger = MacIfConfiger;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -404,5 +408,10 @@ pub type IfConfiger = LinuxIfConfiger;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub type IfConfiger = WindowsIfConfiger;
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
#[cfg(not(any(
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "windows",
|
||||
target_os = "freebsd"
|
||||
)))]
|
||||
pub type IfConfiger = DummyIfConfiger;
|
||||
|
||||
@@ -60,7 +60,9 @@ impl InterfaceFilter {
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
impl InterfaceFilter {
|
||||
async fn is_interface_physical(interface_name: &str) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn is_interface_physical(&self) -> bool {
|
||||
let interface_name = &self.iface.name;
|
||||
let output = tokio::process::Command::new("networksetup")
|
||||
.args(&["-listallhardwareports"])
|
||||
.output()
|
||||
@@ -87,11 +89,17 @@ impl InterfaceFilter {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
async fn is_interface_physical(&self) -> bool {
|
||||
// if mac addr is not zero, then it's physical interface
|
||||
self.iface.mac.map(|mac| !mac.is_zero()).unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn filter_iface(&self) -> bool {
|
||||
!self.iface.is_point_to_point()
|
||||
&& !self.iface.is_loopback()
|
||||
&& self.iface.is_up()
|
||||
&& Self::is_interface_physical(&self.iface.name).await
|
||||
&& self.is_interface_physical().await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,10 @@ impl DirectConnectorManager {
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
if self.global_ctx.get_flags().disable_p2p {
|
||||
return;
|
||||
}
|
||||
|
||||
self.run_as_server();
|
||||
self.run_as_client();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ struct ConnectorManagerData {
|
||||
connectors: ConnectorMap,
|
||||
reconnecting: DashSet<String>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
alive_conn_urls: Arc<Mutex<BTreeSet<String>>>,
|
||||
alive_conn_urls: Arc<DashSet<String>>,
|
||||
// user removed connector urls
|
||||
removed_conn_urls: Arc<DashSet<String>>,
|
||||
net_ns: NetNS,
|
||||
@@ -71,7 +71,7 @@ impl ManualConnectorManager {
|
||||
connectors,
|
||||
reconnecting: DashSet::new(),
|
||||
peer_manager,
|
||||
alive_conn_urls: Arc::new(Mutex::new(BTreeSet::new())),
|
||||
alive_conn_urls: Arc::new(DashSet::new()),
|
||||
removed_conn_urls: Arc::new(DashSet::new()),
|
||||
net_ns: global_ctx.net_ns.clone(),
|
||||
global_ctx,
|
||||
@@ -80,7 +80,11 @@ impl ManualConnectorManager {
|
||||
};
|
||||
|
||||
ret.tasks
|
||||
.spawn(Self::conn_mgr_routine(ret.data.clone(), event_subscriber));
|
||||
.spawn(Self::conn_mgr_reconn_routine(ret.data.clone()));
|
||||
ret.tasks.spawn(Self::conn_mgr_handle_event_routine(
|
||||
ret.data.clone(),
|
||||
event_subscriber,
|
||||
));
|
||||
|
||||
ret
|
||||
}
|
||||
@@ -159,10 +163,17 @@ impl ManualConnectorManager {
|
||||
ret
|
||||
}
|
||||
|
||||
async fn conn_mgr_routine(
|
||||
async fn conn_mgr_handle_event_routine(
|
||||
data: Arc<ConnectorManagerData>,
|
||||
mut event_recv: Receiver<GlobalCtxEvent>,
|
||||
) {
|
||||
loop {
|
||||
let event = event_recv.recv().await.expect("event_recv got error");
|
||||
Self::handle_event(&event, &data).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn conn_mgr_reconn_routine(data: Arc<ConnectorManagerData>) {
|
||||
tracing::warn!("conn_mgr_routine started");
|
||||
let mut reconn_interval = tokio::time::interval(std::time::Duration::from_millis(
|
||||
use_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS),
|
||||
@@ -171,15 +182,6 @@ impl ManualConnectorManager {
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = event_recv.recv() => {
|
||||
if let Ok(event) = event {
|
||||
Self::handle_event(&event, data.clone()).await;
|
||||
} else {
|
||||
tracing::warn!(?event, "event_recv got error");
|
||||
panic!("event_recv got error, err: {:?}", event);
|
||||
}
|
||||
}
|
||||
|
||||
_ = reconn_interval.tick() => {
|
||||
let dead_urls = Self::collect_dead_conns(data.clone()).await;
|
||||
if dead_urls.is_empty() {
|
||||
@@ -210,17 +212,24 @@ impl ManualConnectorManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(event: &GlobalCtxEvent, data: Arc<ConnectorManagerData>) {
|
||||
async fn handle_event(event: &GlobalCtxEvent, data: &ConnectorManagerData) {
|
||||
let need_add_alive = |conn_info: &easytier_rpc::PeerConnInfo| conn_info.is_client;
|
||||
match event {
|
||||
GlobalCtxEvent::PeerConnAdded(conn_info) => {
|
||||
if !need_add_alive(conn_info) {
|
||||
return;
|
||||
}
|
||||
let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone();
|
||||
data.alive_conn_urls.lock().await.insert(addr);
|
||||
data.alive_conn_urls.insert(addr);
|
||||
tracing::warn!("peer conn added: {:?}", conn_info);
|
||||
}
|
||||
|
||||
GlobalCtxEvent::PeerConnRemoved(conn_info) => {
|
||||
if !need_add_alive(conn_info) {
|
||||
return;
|
||||
}
|
||||
let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone();
|
||||
data.alive_conn_urls.lock().await.remove(&addr);
|
||||
data.alive_conn_urls.remove(&addr);
|
||||
tracing::warn!("peer conn removed: {:?}", conn_info);
|
||||
}
|
||||
|
||||
@@ -252,13 +261,18 @@ impl ManualConnectorManager {
|
||||
async fn collect_dead_conns(data: Arc<ConnectorManagerData>) -> BTreeSet<String> {
|
||||
Self::handle_remove_connector(data.clone());
|
||||
|
||||
let curr_alive = data.alive_conn_urls.lock().await.clone();
|
||||
let all_urls: BTreeSet<String> = data
|
||||
.connectors
|
||||
.iter()
|
||||
.map(|x| x.key().clone().into())
|
||||
.collect();
|
||||
&all_urls - &curr_alive
|
||||
let mut ret = BTreeSet::new();
|
||||
for url in all_urls.iter() {
|
||||
if !data.alive_conn_urls.contains(url) {
|
||||
ret.insert(url.clone());
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
async fn conn_reconnect_with_ip_version(
|
||||
@@ -316,6 +330,7 @@ impl ManualConnectorManager {
|
||||
ip_versions.push(IpVersion::Both);
|
||||
} else {
|
||||
let addrs = u.socket_addrs(|| Some(1000))?;
|
||||
tracing::info!(?addrs, ?dead_url, "get ip from url done");
|
||||
let mut has_ipv4 = false;
|
||||
let mut has_ipv6 = false;
|
||||
for addr in addrs {
|
||||
|
||||
@@ -602,6 +602,10 @@ impl UdpHolePunchConnector {
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), Error> {
|
||||
if self.data.global_ctx.get_flags().disable_p2p {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.run_as_client().await?;
|
||||
self.run_as_server().await?;
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ enum SubCommand {
|
||||
Route(RouteArgs),
|
||||
PeerCenter,
|
||||
VpnPortal,
|
||||
Node(NodeArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
@@ -101,12 +102,26 @@ enum ConnectorSubCommand {
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum NodeSubCommand {
|
||||
Info,
|
||||
Config,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct NodeArgs {
|
||||
#[command(subcommand)]
|
||||
sub_command: Option<NodeSubCommand>,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum Error {
|
||||
#[error("tonic transport error")]
|
||||
TonicTransportError(#[from] tonic::transport::Error),
|
||||
#[error("tonic rpc error")]
|
||||
TonicRpcError(#[from] tonic::Status),
|
||||
#[error("anyhow error")]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
struct CommandHandler {
|
||||
@@ -447,6 +462,45 @@ async fn main() -> Result<(), Error> {
|
||||
);
|
||||
println!("connected_clients:\n{:#?}", resp.connected_clients);
|
||||
}
|
||||
SubCommand::Node(sub_cmd) => {
|
||||
let mut client = handler.get_peer_manager_client().await?;
|
||||
let node_info = client
|
||||
.show_node_info(ShowNodeInfoRequest::default())
|
||||
.await?
|
||||
.into_inner()
|
||||
.node_info
|
||||
.ok_or(anyhow::anyhow!("node info not found"))?;
|
||||
match sub_cmd.sub_command {
|
||||
Some(NodeSubCommand::Info) | None => {
|
||||
let stun_info = node_info.stun_info.clone().unwrap_or_default();
|
||||
|
||||
let mut builder = tabled::builder::Builder::default();
|
||||
builder.push_record(vec!["Virtual IP", node_info.ipv4_addr.as_str()]);
|
||||
builder.push_record(vec!["Hostname", node_info.hostname.as_str()]);
|
||||
builder.push_record(vec![
|
||||
"Proxy CIDRs",
|
||||
node_info.proxy_cidrs.join(", ").as_str(),
|
||||
]);
|
||||
builder.push_record(vec!["Peer ID", node_info.peer_id.to_string().as_str()]);
|
||||
builder.push_record(vec!["Public IP", stun_info.public_ip.join(", ").as_str()]);
|
||||
builder.push_record(vec![
|
||||
"UDP Stun Type",
|
||||
format!("{:?}", stun_info.udp_nat_type()).as_str(),
|
||||
]);
|
||||
for (idx, l) in node_info.listeners.iter().enumerate() {
|
||||
if l.starts_with("ring") {
|
||||
continue;
|
||||
}
|
||||
builder.push_record(vec![format!("Listener {}", idx).as_str(), l]);
|
||||
}
|
||||
|
||||
println!("{}", builder.build().with(Style::modern()).to_string());
|
||||
}
|
||||
Some(NodeSubCommand::Config) => {
|
||||
println!("{}", node_info.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+119
-71
@@ -4,12 +4,13 @@
|
||||
mod tests;
|
||||
|
||||
use std::{
|
||||
backtrace,
|
||||
io::Write as _,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
extern crate rust_i18n;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
|
||||
@@ -30,6 +31,7 @@ use common::config::{
|
||||
};
|
||||
use instance::instance::Instance;
|
||||
use tokio::net::TcpSocket;
|
||||
use utils::setup_panic_handler;
|
||||
|
||||
use crate::{
|
||||
common::{
|
||||
@@ -52,19 +54,20 @@ struct Cli {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "path to the config file, NOTE: if this is set, all other options will be ignored"
|
||||
help = t!("core_clap.config_file").to_string()
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "network name to identify this vpn network",
|
||||
help = t!("core_clap.network_name").to_string(),
|
||||
default_value = "default"
|
||||
)]
|
||||
network_name: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "network secret to verify this node belongs to the vpn network",
|
||||
help = t!("core_clap.network_secret").to_string(),
|
||||
default_value = ""
|
||||
)]
|
||||
network_secret: String,
|
||||
@@ -72,171 +75,214 @@ struct Cli {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "ipv4 address of this vpn node, if empty, this node will only forward packets and no TUN device will be created"
|
||||
help = t!("core_clap.ipv4").to_string()
|
||||
)]
|
||||
ipv4: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = "automatically determine and set IP address by Easytier, and the
|
||||
IP address starts from 10.0.0.1 by default. Warning, if there is an IP
|
||||
conflict in the network when using DHCP, the IP will be automatically
|
||||
changed."
|
||||
help = t!("core_clap.dhcp").to_string()
|
||||
)]
|
||||
dhcp: bool,
|
||||
|
||||
#[arg(short, long, help = "peers to connect initially", num_args = 0..)]
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = t!("core_clap.peers").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
peers: Vec<String>,
|
||||
|
||||
#[arg(short, long, help = "use a public shared node to discover peers")]
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = t!("core_clap.external_node").to_string()
|
||||
)]
|
||||
external_node: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short = 'n',
|
||||
long,
|
||||
help = "export local networks to other peers in the vpn"
|
||||
help = t!("core_clap.proxy_networks").to_string()
|
||||
)]
|
||||
proxy_networks: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "0",
|
||||
help = "rpc portal address to listen for management. 0 means random
|
||||
port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means
|
||||
listen on 12345 of all interfaces. default is 0 and will try 15888 first"
|
||||
help = t!("core_clap.rpc_portal").to_string(),
|
||||
default_value = "0"
|
||||
)]
|
||||
rpc_portal: String,
|
||||
|
||||
#[arg(short, long, help = "listeners to accept connections, allow format:
|
||||
a port number: 11010, means tcp/udp will listen on 11010, ws/wss will listen on 11010 and 11011, wg will listen on 11011
|
||||
url: tcp://0.0.0.0:11010, tcp can be tcp, udp, ring, wg, ws, wss,
|
||||
proto:port: wg:11011, means listen on 11011 with wireguard protocol
|
||||
url and proto:port can occur multiple times.
|
||||
", default_values_t = ["11010".to_string()],
|
||||
num_args = 0..)]
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
help = t!("core_clap.listeners").to_string(),
|
||||
default_values_t = ["11010".to_string()],
|
||||
num_args = 0..
|
||||
)]
|
||||
listeners: Vec<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "do not listen on any port, only connect to peers",
|
||||
help = t!("core_clap.no_listener").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
no_listener: bool,
|
||||
|
||||
#[arg(long, help = "console log level",
|
||||
value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error", "off"]))]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.console_log_level").to_string()
|
||||
)]
|
||||
console_log_level: Option<String>,
|
||||
|
||||
#[arg(long, help = "file log level",
|
||||
value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error", "off"]))]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.file_log_level").to_string()
|
||||
)]
|
||||
file_log_level: Option<String>,
|
||||
#[arg(long, help = "directory to store log files")]
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.file_log_dir").to_string()
|
||||
)]
|
||||
file_log_dir: Option<String>,
|
||||
|
||||
#[arg(long, help = "host name to identify this device")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.hostname").to_string()
|
||||
)]
|
||||
hostname: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short = 'm',
|
||||
long,
|
||||
default_value = "default",
|
||||
help = "instance name to identify this vpn node in same machine"
|
||||
help = t!("core_clap.instance_name").to_string(),
|
||||
default_value = "default"
|
||||
)]
|
||||
instance_name: 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"
|
||||
help = t!("core_clap.vpn_portal").to_string()
|
||||
)]
|
||||
vpn_portal: Option<String>,
|
||||
|
||||
#[arg(long, help = "default protocol to use when connecting to peers")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.default_protocol").to_string()
|
||||
)]
|
||||
default_protocol: Option<String>,
|
||||
|
||||
#[arg(
|
||||
short = 'u',
|
||||
long,
|
||||
help = "disable encryption for peers communication, default is false, must be same with peers",
|
||||
help = t!("core_clap.disable_encryption").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
disable_encryption: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "use multi-thread runtime, default is single-thread",
|
||||
help = t!("core_clap.multi_thread").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
multi_thread: bool,
|
||||
|
||||
#[arg(long, help = "do not use ipv6", default_value = "false")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.disable_ipv6").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
disable_ipv6: bool,
|
||||
|
||||
#[arg(long, help = "optional tun interface name")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.dev_name").to_string()
|
||||
)]
|
||||
dev_name: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "mtu of the TUN device, default is 1420 for non-encryption, 1400 for encryption"
|
||||
help = t!("core_clap.mtu").to_string()
|
||||
)]
|
||||
mtu: Option<u16>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "latency first mode, will try to relay traffic with lowest latency path, default is using shortest path",
|
||||
help = t!("core_clap.latency_first").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
latency_first: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list",
|
||||
help = t!("core_clap.exit_nodes").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
exit_nodes: Vec<Ipv4Addr>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "allow this node to be an exit node, default is false",
|
||||
help = t!("core_clap.enable_exit_node").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
enable_exit_node: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "do not create TUN device, can use subnet proxy to access node",
|
||||
help = t!("core_clap.no_tun").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
no_tun: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "enable smoltcp stack for subnet proxy",
|
||||
help = t!("core_clap.use_smoltcp").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
use_smoltcp: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "assign routes cidr manually, will disable subnet proxy and
|
||||
wireguard routes propogated from peers. e.g.: 192.168.0.0/16",
|
||||
help = t!("core_clap.manual_routes").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
manual_routes: Option<Vec<String>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "only relay traffic of whitelisted networks, input is a wildcard
|
||||
string, e.g.: '*' (all networks), 'def*' (network prefixed with def), can specify multiple networks
|
||||
disable relay if arg is empty. default is allowing all networks",
|
||||
num_args = 0..,
|
||||
help = t!("core_clap.relay_network_whitelist").to_string(),
|
||||
num_args = 0..
|
||||
)]
|
||||
relay_network_whitelist: Option<Vec<String>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.disable_p2p").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
disable_p2p: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.relay_all_peer_rpc").to_string(),
|
||||
default_value = "false"
|
||||
)]
|
||||
relay_all_peer_rpc: bool,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("core_clap.socks5").to_string()
|
||||
)]
|
||||
socks5: Option<u16>,
|
||||
}
|
||||
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
|
||||
impl Cli {
|
||||
fn parse_listeners(&self) -> Vec<String> {
|
||||
println!("parsing listeners: {:?}", self.listeners);
|
||||
@@ -334,14 +380,12 @@ impl From<Cli> for TomlConfigLoader {
|
||||
|
||||
cfg.set_dhcp(cli.dhcp);
|
||||
|
||||
if !cli.dhcp {
|
||||
if let Some(ipv4) = &cli.ipv4 {
|
||||
cfg.set_ipv4(Some(
|
||||
ipv4.parse()
|
||||
.with_context(|| format!("failed to parse ipv4 address: {}", ipv4))
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
if let Some(ipv4) = &cli.ipv4 {
|
||||
cfg.set_ipv4(Some(
|
||||
ipv4.parse()
|
||||
.with_context(|| format!("failed to parse ipv4 address: {}", ipv4))
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
cfg.set_peers(
|
||||
@@ -453,6 +497,15 @@ impl From<Cli> for TomlConfigLoader {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
if let Some(socks5_proxy) = cli.socks5 {
|
||||
cfg.set_socks5_portal(Some(
|
||||
format!("socks5://0.0.0.0:{}", socks5_proxy)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut f = cfg.get_flags();
|
||||
if cli.default_protocol.is_some() {
|
||||
f.default_protocol = cli.default_protocol.as_ref().unwrap().clone();
|
||||
@@ -470,6 +523,8 @@ impl From<Cli> for TomlConfigLoader {
|
||||
if let Some(wl) = cli.relay_network_whitelist {
|
||||
f.foreign_network_whitelist = wl.join(" ");
|
||||
}
|
||||
f.disable_p2p = cli.disable_p2p;
|
||||
f.relay_all_peer_rpc = cli.relay_all_peer_rpc;
|
||||
cfg.set_flags(f);
|
||||
|
||||
cfg.set_exit_nodes(cli.exit_nodes.clone());
|
||||
@@ -493,16 +548,6 @@ fn peer_conn_info_to_string(p: crate::rpc::PeerConnInfo) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -628,6 +673,9 @@ pub async fn async_main(cli: Cli) {
|
||||
fn main() {
|
||||
setup_panic_handler();
|
||||
|
||||
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
|
||||
rust_i18n::set_locale(&locale);
|
||||
|
||||
let cli = Cli::parse();
|
||||
tracing::info!(cli = ?cli, "cli args parsed");
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jonathan Dizdarevic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
Code is modified from https://github.com/dizda/fast-socks5
|
||||
@@ -0,0 +1,318 @@
|
||||
//! Fast SOCKS5 client/server implementation written in Rust async/.await (with tokio).
|
||||
//!
|
||||
//! This library is maintained by [anyip.io](https://anyip.io/) a residential and mobile socks5 proxy provider.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - An `async`/`.await` [SOCKS5](https://tools.ietf.org/html/rfc1928) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4 Client](https://www.openssh.com/txt/socks4.protocol) implementation.
|
||||
//! - An `async`/`.await` [SOCKS4a Client](https://www.openssh.com/txt/socks4a.protocol) implementation.
|
||||
//! - No **unsafe** code
|
||||
//! - Built on-top of `tokio` library
|
||||
//! - Ultra lightweight and scalable
|
||||
//! - No system dependencies
|
||||
//! - Cross-platform
|
||||
//! - Authentication methods:
|
||||
//! - No-Auth method
|
||||
//! - Username/Password auth method
|
||||
//! - Custom auth methods can be implemented via the Authentication Trait
|
||||
//! - Credentials returned on authentication success
|
||||
//! - All SOCKS5 RFC errors (replies) should be mapped
|
||||
//! - `AsyncRead + AsyncWrite` traits are implemented on Socks5Stream & Socks5Socket
|
||||
//! - `IPv4`, `IPv6`, and `Domains` types are supported
|
||||
//! - Config helper for Socks5Server
|
||||
//! - Helpers to run a Socks5Server à la *"std's TcpStream"* via `incoming.next().await`
|
||||
//! - Examples come with real cases commands scenarios
|
||||
//! - Can disable `DNS resolving`
|
||||
//! - Can skip the authentication/handshake process, which will directly handle command's request (useful to save useless round-trips in a current authenticated environment)
|
||||
//! - Can disable command execution (useful if you just want to forward the request to a different server)
|
||||
//!
|
||||
//!
|
||||
//! ## Install
|
||||
//!
|
||||
//! Open in [crates.io](https://crates.io/crates/fast-socks5).
|
||||
//!
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Please check [`examples`](https://github.com/dizda/fast-socks5/tree/master/examples) directory.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod server;
|
||||
pub mod util;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
use util::target_addr::read_address;
|
||||
use util::target_addr::TargetAddr;
|
||||
use util::target_addr::ToTargetAddr;
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use tracing::error;
|
||||
|
||||
use crate::read_exact;
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub mod consts {
|
||||
pub const SOCKS5_VERSION: u8 = 0x05;
|
||||
|
||||
pub const SOCKS5_AUTH_METHOD_NONE: u8 = 0x00;
|
||||
pub const SOCKS5_AUTH_METHOD_GSSAPI: u8 = 0x01;
|
||||
pub const SOCKS5_AUTH_METHOD_PASSWORD: u8 = 0x02;
|
||||
pub const SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE: u8 = 0xff;
|
||||
|
||||
pub const SOCKS5_CMD_TCP_CONNECT: u8 = 0x01;
|
||||
pub const SOCKS5_CMD_TCP_BIND: u8 = 0x02;
|
||||
pub const SOCKS5_CMD_UDP_ASSOCIATE: u8 = 0x03;
|
||||
|
||||
pub const SOCKS5_ADDR_TYPE_IPV4: u8 = 0x01;
|
||||
pub const SOCKS5_ADDR_TYPE_DOMAIN_NAME: u8 = 0x03;
|
||||
pub const SOCKS5_ADDR_TYPE_IPV6: u8 = 0x04;
|
||||
|
||||
pub const SOCKS5_REPLY_SUCCEEDED: u8 = 0x00;
|
||||
pub const SOCKS5_REPLY_GENERAL_FAILURE: u8 = 0x01;
|
||||
pub const SOCKS5_REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02;
|
||||
pub const SOCKS5_REPLY_NETWORK_UNREACHABLE: u8 = 0x03;
|
||||
pub const SOCKS5_REPLY_HOST_UNREACHABLE: u8 = 0x04;
|
||||
pub const SOCKS5_REPLY_CONNECTION_REFUSED: u8 = 0x05;
|
||||
pub const SOCKS5_REPLY_TTL_EXPIRED: u8 = 0x06;
|
||||
pub const SOCKS5_REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
|
||||
pub const SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08;
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Socks5Command {
|
||||
TCPConnect,
|
||||
TCPBind,
|
||||
UDPAssociate,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Socks5Command {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
Socks5Command::TCPConnect => consts::SOCKS5_CMD_TCP_CONNECT,
|
||||
Socks5Command::TCPBind => consts::SOCKS5_CMD_TCP_BIND,
|
||||
Socks5Command::UDPAssociate => consts::SOCKS5_CMD_UDP_ASSOCIATE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn from_u8(code: u8) -> Option<Socks5Command> {
|
||||
match code {
|
||||
consts::SOCKS5_CMD_TCP_CONNECT => Some(Socks5Command::TCPConnect),
|
||||
consts::SOCKS5_CMD_TCP_BIND => Some(Socks5Command::TCPBind),
|
||||
consts::SOCKS5_CMD_UDP_ASSOCIATE => Some(Socks5Command::UDPAssociate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AuthenticationMethod {
|
||||
None,
|
||||
Password { username: String, password: String },
|
||||
}
|
||||
|
||||
impl AuthenticationMethod {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
AuthenticationMethod::None => consts::SOCKS5_AUTH_METHOD_NONE,
|
||||
AuthenticationMethod::Password {..} =>
|
||||
consts::SOCKS5_AUTH_METHOD_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
fn from_u8(code: u8) -> Option<AuthenticationMethod> {
|
||||
match code {
|
||||
consts::SOCKS5_AUTH_METHOD_NONE => Some(AuthenticationMethod::None),
|
||||
consts::SOCKS5_AUTH_METHOD_PASSWORD => Some(AuthenticationMethod::Password { username: "test".to_string(), password: "test".to_string()}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthenticationMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AuthenticationMethod::None => f.write_str("AuthenticationMethod::None"),
|
||||
AuthenticationMethod::Password { .. } => f.write_str("AuthenticationMethod::Password"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//impl Vec<AuthenticationMethod> {
|
||||
// pub fn as_bytes(&self) -> &[u8] {
|
||||
// self.iter().map(|l| l.as_u8()).collect()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<&[AuthenticationMethod]> for &[u8] {
|
||||
// fn from(_: Vec<AuthenticationMethod>) -> Self {
|
||||
// &[0x00]
|
||||
// }
|
||||
//}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SocksError {
|
||||
#[error("i/o error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("the data for key `{0}` is not available")]
|
||||
Redaction(String),
|
||||
#[error("invalid header (expected {expected:?}, found {found:?})")]
|
||||
InvalidHeader { expected: String, found: String },
|
||||
|
||||
#[error("Auth method unacceptable `{0:?}`.")]
|
||||
AuthMethodUnacceptable(Vec<u8>),
|
||||
#[error("Unsupported SOCKS version `{0}`.")]
|
||||
UnsupportedSocksVersion(u8),
|
||||
#[error("Domain exceeded max sequence length")]
|
||||
ExceededMaxDomainLen(usize),
|
||||
#[error("Authentication failed `{0}`")]
|
||||
AuthenticationFailed(String),
|
||||
#[error("Authentication rejected `{0}`")]
|
||||
AuthenticationRejected(String),
|
||||
|
||||
#[error("Error with reply: {0}.")]
|
||||
ReplyError(#[from] ReplyError),
|
||||
|
||||
#[cfg(feature = "socks4")]
|
||||
#[error("Error with reply: {0}.")]
|
||||
ReplySocks4Error(#[from] socks4::ReplyError),
|
||||
|
||||
#[error("Argument input error: `{0}`.")]
|
||||
ArgumentInputError(&'static str),
|
||||
|
||||
// #[error("Other: `{0}`.")]
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T, E = SocksError> = core::result::Result<T, E>;
|
||||
|
||||
/// SOCKS5 reply code
|
||||
#[derive(Error, Debug, Copy, Clone)]
|
||||
pub enum ReplyError {
|
||||
#[error("Succeeded")]
|
||||
Succeeded,
|
||||
#[error("General failure")]
|
||||
GeneralFailure,
|
||||
#[error("Connection not allowed by ruleset")]
|
||||
ConnectionNotAllowed,
|
||||
#[error("Network unreachable")]
|
||||
NetworkUnreachable,
|
||||
#[error("Host unreachable")]
|
||||
HostUnreachable,
|
||||
#[error("Connection refused")]
|
||||
ConnectionRefused,
|
||||
#[error("Connection timeout")]
|
||||
ConnectionTimeout,
|
||||
#[error("TTL expired")]
|
||||
TtlExpired,
|
||||
#[error("Command not supported")]
|
||||
CommandNotSupported,
|
||||
#[error("Address type not supported")]
|
||||
AddressTypeNotSupported,
|
||||
// OtherReply(u8),
|
||||
}
|
||||
|
||||
impl ReplyError {
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
pub fn as_u8(self) -> u8 {
|
||||
match self {
|
||||
ReplyError::Succeeded => consts::SOCKS5_REPLY_SUCCEEDED,
|
||||
ReplyError::GeneralFailure => consts::SOCKS5_REPLY_GENERAL_FAILURE,
|
||||
ReplyError::ConnectionNotAllowed => consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED,
|
||||
ReplyError::NetworkUnreachable => consts::SOCKS5_REPLY_NETWORK_UNREACHABLE,
|
||||
ReplyError::HostUnreachable => consts::SOCKS5_REPLY_HOST_UNREACHABLE,
|
||||
ReplyError::ConnectionRefused => consts::SOCKS5_REPLY_CONNECTION_REFUSED,
|
||||
ReplyError::ConnectionTimeout => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||
ReplyError::TtlExpired => consts::SOCKS5_REPLY_TTL_EXPIRED,
|
||||
ReplyError::CommandNotSupported => consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED,
|
||||
ReplyError::AddressTypeNotSupported => consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED,
|
||||
// ReplyError::OtherReply(c) => c,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[rustfmt::skip]
|
||||
pub fn from_u8(code: u8) -> ReplyError {
|
||||
match code {
|
||||
consts::SOCKS5_REPLY_SUCCEEDED => ReplyError::Succeeded,
|
||||
consts::SOCKS5_REPLY_GENERAL_FAILURE => ReplyError::GeneralFailure,
|
||||
consts::SOCKS5_REPLY_CONNECTION_NOT_ALLOWED => ReplyError::ConnectionNotAllowed,
|
||||
consts::SOCKS5_REPLY_NETWORK_UNREACHABLE => ReplyError::NetworkUnreachable,
|
||||
consts::SOCKS5_REPLY_HOST_UNREACHABLE => ReplyError::HostUnreachable,
|
||||
consts::SOCKS5_REPLY_CONNECTION_REFUSED => ReplyError::ConnectionRefused,
|
||||
consts::SOCKS5_REPLY_TTL_EXPIRED => ReplyError::TtlExpired,
|
||||
consts::SOCKS5_REPLY_COMMAND_NOT_SUPPORTED => ReplyError::CommandNotSupported,
|
||||
consts::SOCKS5_REPLY_ADDRESS_TYPE_NOT_SUPPORTED => ReplyError::AddressTypeNotSupported,
|
||||
// _ => ReplyError::OtherReply(code),
|
||||
_ => unreachable!("ReplyError code unsupported."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate UDP header
|
||||
///
|
||||
/// # UDP Request header structure.
|
||||
/// ```text
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
/// |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA |
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
/// | 2 | 1 | 1 | Variable | 2 | Variable |
|
||||
/// +----+------+------+----------+----------+----------+
|
||||
///
|
||||
/// The fields in the UDP request header are:
|
||||
///
|
||||
/// o RSV Reserved X'0000'
|
||||
/// o FRAG Current fragment number
|
||||
/// o ATYP address type of following addresses:
|
||||
/// o IP V4 address: X'01'
|
||||
/// o DOMAINNAME: X'03'
|
||||
/// o IP V6 address: X'04'
|
||||
/// o DST.ADDR desired destination address
|
||||
/// o DST.PORT desired destination port
|
||||
/// o DATA user data
|
||||
/// ```
|
||||
pub fn new_udp_header<T: ToTargetAddr>(target_addr: T) -> Result<Vec<u8>> {
|
||||
let mut header = vec![
|
||||
0, 0, // RSV
|
||||
0, // FRAG
|
||||
];
|
||||
header.append(&mut target_addr.to_target_addr()?.to_be_bytes()?);
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Parse data from UDP client on raw buffer, return (frag, target_addr, payload).
|
||||
pub async fn parse_udp_request<'a>(mut req: &'a [u8]) -> Result<(u8, TargetAddr, &'a [u8])> {
|
||||
let rsv = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
if !rsv.eq(&[0u8; 2]) {
|
||||
return Err(ReplyError::GeneralFailure.into());
|
||||
}
|
||||
|
||||
let [frag, atyp] = read_exact!(req, [0u8; 2]).context("Malformed request")?;
|
||||
|
||||
let target_addr = read_address(&mut req, atyp).await.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
Ok((frag, target_addr, req))
|
||||
}
|
||||
@@ -0,0 +1,842 @@
|
||||
use super::new_udp_header;
|
||||
use super::parse_udp_request;
|
||||
use super::read_exact;
|
||||
use super::util::stream::tcp_connect_with_timeout;
|
||||
use super::util::target_addr::{read_address, TargetAddr};
|
||||
use super::Socks5Command;
|
||||
use super::{consts, AuthenticationMethod, ReplyError, Result, SocksError};
|
||||
use anyhow::Context;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{SocketAddr, ToSocketAddrs as StdToSocketAddrs};
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::try_join;
|
||||
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config<A: Authentication = DenyAuthentication> {
|
||||
/// Timeout of the command request
|
||||
request_timeout: u64,
|
||||
/// Avoid useless roundtrips if we don't need the Authentication layer
|
||||
skip_auth: bool,
|
||||
/// Enable dns-resolving
|
||||
dns_resolve: bool,
|
||||
/// Enable command execution
|
||||
execute_command: bool,
|
||||
/// Enable UDP support
|
||||
allow_udp: bool,
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 1-2 auth methods (no auth) rather than 3 (with auth)
|
||||
allow_no_auth: bool,
|
||||
/// Contains the authentication trait to use the user against with
|
||||
auth: Option<Arc<A>>,
|
||||
}
|
||||
|
||||
impl<A: Authentication> Default for Config<A> {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
request_timeout: 10,
|
||||
skip_auth: false,
|
||||
dns_resolve: true,
|
||||
execute_command: true,
|
||||
allow_udp: false,
|
||||
allow_no_auth: false,
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use this trait to handle a custom authentication on your end.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Authentication: Send + Sync {
|
||||
type Item;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item>;
|
||||
}
|
||||
|
||||
/// Basic user/pass auth method provided.
|
||||
pub struct SimpleUserPassword {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// The struct returned when the user has successfully authenticated
|
||||
pub struct AuthSucceeded {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// This is an example to auth via simple credentials.
|
||||
/// If the auth succeed, we return the username authenticated with, for further uses.
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for SimpleUserPassword {
|
||||
type Item = AuthSucceeded;
|
||||
|
||||
async fn authenticate(&self, credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
if let Some((username, password)) = credentials {
|
||||
// Client has supplied credentials
|
||||
if username == self.username && password == self.password {
|
||||
// Some() will allow the authentication and the credentials
|
||||
// will be forwarded to the socket
|
||||
Some(AuthSucceeded { username })
|
||||
} else {
|
||||
// Credentials incorrect, we deny the auth
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// The client hasn't supplied any credentials, which only happens
|
||||
// when `Config::allow_no_auth()` is set as `true`
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This will simply return Option::None, which denies the authentication
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct DenyAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for DenyAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// While this one will always allow the user in.
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub struct AcceptAuthentication {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Authentication for AcceptAuthentication {
|
||||
type Item = ();
|
||||
|
||||
async fn authenticate(&self, _credentials: Option<(String, String)>) -> Option<Self::Item> {
|
||||
Some(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Authentication> Config<A> {
|
||||
/// How much time it should wait until the request timeout.
|
||||
pub fn set_request_timeout(&mut self, n: u64) -> &mut Self {
|
||||
self.request_timeout = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Skip the entire auth/handshake part, which means the server will directly wait for
|
||||
/// the command request.
|
||||
pub fn set_skip_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.skip_auth = value;
|
||||
self.auth = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable authentication
|
||||
/// 'static lifetime for Authentication avoid us to use `dyn Authentication`
|
||||
/// and set the Arc before calling the function.
|
||||
pub fn with_authentication<T: Authentication + 'static>(self, authentication: T) -> Config<T> {
|
||||
Config {
|
||||
request_timeout: self.request_timeout,
|
||||
skip_auth: self.skip_auth,
|
||||
dns_resolve: self.dns_resolve,
|
||||
execute_command: self.execute_command,
|
||||
allow_udp: self.allow_udp,
|
||||
allow_no_auth: self.allow_no_auth,
|
||||
auth: Some(Arc::new(authentication)),
|
||||
}
|
||||
}
|
||||
|
||||
/// For some complex scenarios, we may want to either accept Username/Password configuration
|
||||
/// or IP Whitelisting, in case the client send only 2 auth methods rather than 3 (with auth)
|
||||
pub fn set_allow_no_auth(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_no_auth = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to execute commands
|
||||
pub fn set_execute_command(&mut self, value: bool) -> &mut Self {
|
||||
self.execute_command = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Will the server perform dns resolve
|
||||
pub fn set_dns_resolve(&mut self, value: bool) -> &mut Self {
|
||||
self.dns_resolve = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not to allow udp traffic
|
||||
pub fn set_udp_support(&mut self, value: bool) -> &mut Self {
|
||||
self.allow_udp = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait AsyncTcpConnector {
|
||||
type S: AsyncRead + AsyncWrite + Unpin + Send + Sync;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<Self::S>;
|
||||
}
|
||||
|
||||
pub struct DefaultTcpConnector {}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for DefaultTcpConnector {
|
||||
type S = TcpStream;
|
||||
|
||||
async fn tcp_connect(&self, addr: SocketAddr, timeout_s: u64) -> Result<TcpStream> {
|
||||
tcp_connect_with_timeout(addr, timeout_s).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap TcpStream and contains Socks5 protocol implementation.
|
||||
pub struct Socks5Socket<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
{
|
||||
inner: T,
|
||||
config: Arc<Config<A>>,
|
||||
auth: AuthenticationMethod,
|
||||
target_addr: Option<TargetAddr>,
|
||||
cmd: Option<Socks5Command>,
|
||||
/// Socket address which will be used in the reply message.
|
||||
reply_ip: Option<IpAddr>,
|
||||
/// If the client has been authenticated, that's where we store his credentials
|
||||
/// to be accessed from the socket
|
||||
credentials: Option<A::Item>,
|
||||
tcp_connector: C,
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin, A: Authentication, C: AsyncTcpConnector>
|
||||
Socks5Socket<T, A, C>
|
||||
{
|
||||
pub fn new(socket: T, config: Arc<Config<A>>, tcp_connector: C) -> Self {
|
||||
Socks5Socket {
|
||||
inner: socket,
|
||||
config,
|
||||
auth: AuthenticationMethod::None,
|
||||
target_addr: None,
|
||||
cmd: None,
|
||||
reply_ip: None,
|
||||
credentials: None,
|
||||
tcp_connector,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the bind IP address in Socks5Reply.
|
||||
///
|
||||
/// Only the inner socket owner knows the correct reply bind addr, so leave this field to be
|
||||
/// populated. For those strict clients, users can use this function to set the correct IP
|
||||
/// address.
|
||||
///
|
||||
/// Most popular SOCKS5 clients [1] [2] ignore BND.ADDR and BND.PORT the reply of command
|
||||
/// CONNECT, but this field could be useful in some other command, such as UDP ASSOCIATE.
|
||||
///
|
||||
/// [1]: https://github.com/chromium/chromium/blob/bd2c7a8b65ec42d806277dd30f138a673dec233a/net/socket/socks5_client_socket.cc#L481
|
||||
/// [2]: https://github.com/curl/curl/blob/d15692ebbad5e9cfb871b0f7f51a73e43762cee2/lib/socks.c#L978
|
||||
pub fn set_reply_ip(&mut self, addr: IpAddr) {
|
||||
self.reply_ip = Some(addr);
|
||||
}
|
||||
|
||||
/// Process clients SOCKS requests
|
||||
/// This is the entry point where a whole request is processed.
|
||||
pub async fn upgrade_to_socks5(mut self) -> Result<Socks5Socket<T, A, C>> {
|
||||
trace!("upgrading to socks5...");
|
||||
|
||||
// Handshake
|
||||
if !self.config.skip_auth {
|
||||
let methods = self.get_methods().await?;
|
||||
|
||||
let auth_method = self.can_accept_method(methods).await?;
|
||||
|
||||
if self.config.auth.is_some() {
|
||||
let credentials = self.authenticate(auth_method).await?;
|
||||
self.credentials = Some(credentials);
|
||||
}
|
||||
} else {
|
||||
debug!("skipping auth");
|
||||
}
|
||||
|
||||
match self.request().await {
|
||||
Ok(_) => {}
|
||||
Err(SocksError::ReplyError(e)) => {
|
||||
// If a reply error has been returned, we send it to the client
|
||||
self.reply_error(&e).await?;
|
||||
return Err(e.into()); // propagate the error to end this connection's task
|
||||
}
|
||||
// if any other errors has been detected, we simply end connection's task
|
||||
Err(d) => return Err(d),
|
||||
};
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Consumes the `Socks5Socket`, returning the wrapped stream.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
/// Read the authentication method provided by the client.
|
||||
/// A client send a list of methods that he supports, he could send
|
||||
///
|
||||
/// - 0: Non auth
|
||||
/// - 2: Auth with username/password
|
||||
///
|
||||
/// Altogether, then the server choose to use of of these,
|
||||
/// or deny the handshake (thus the connection).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```text
|
||||
/// {SOCKS Version, methods-length}
|
||||
/// eg. (non-auth) {5, 2}
|
||||
/// eg. (auth) {5, 3}
|
||||
/// ```
|
||||
///
|
||||
async fn get_methods(&mut self) -> Result<Vec<u8>> {
|
||||
trace!("Socks5Socket: get_methods()");
|
||||
// read the first 2 bytes which contains the SOCKS version and the methods len()
|
||||
let [version, methods_len] =
|
||||
read_exact!(self.inner, [0u8; 2]).context("Can't read methods")?;
|
||||
debug!(
|
||||
"Handshake headers: [version: {version}, methods len: {len}]",
|
||||
version = version,
|
||||
len = methods_len,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
// {METHODS available from the client}
|
||||
// eg. (non-auth) {0, 1}
|
||||
// eg. (auth) {0, 1, 2}
|
||||
let methods = read_exact!(self.inner, vec![0u8; methods_len as usize])
|
||||
.context("Can't get methods.")?;
|
||||
debug!("methods supported sent by the client: {:?}", &methods);
|
||||
|
||||
// Return methods available
|
||||
Ok(methods)
|
||||
}
|
||||
|
||||
/// Decide to whether or not, accept the authentication method.
|
||||
/// Don't forget that the methods list sent by the client, contains one or more methods.
|
||||
///
|
||||
/// # Request
|
||||
///
|
||||
/// Client send an array of 3 entries: [0, 1, 2]
|
||||
/// ```text
|
||||
/// {SOCKS Version, Authentication chosen}
|
||||
/// eg. (non-auth) {5, 0}
|
||||
/// eg. (GSSAPI) {5, 1}
|
||||
/// eg. (auth) {5, 2}
|
||||
/// ```
|
||||
///
|
||||
/// # Response
|
||||
/// ```text
|
||||
/// eg. (accept non-auth) {5, 0x00}
|
||||
/// eg. (non-acceptable) {5, 0xff}
|
||||
/// ```
|
||||
///
|
||||
async fn can_accept_method(&mut self, client_methods: Vec<u8>) -> Result<u8> {
|
||||
let method_supported;
|
||||
|
||||
if let Some(_auth) = self.config.auth.as_ref() {
|
||||
if client_methods.contains(&consts::SOCKS5_AUTH_METHOD_PASSWORD) {
|
||||
// can auth with password
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_PASSWORD;
|
||||
} else {
|
||||
// client hasn't provided a password
|
||||
if self.config.allow_no_auth {
|
||||
// but we allow no auth, for ip whitelisting
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
} else {
|
||||
// we don't allow no auth, so we deny the entry
|
||||
debug!("Don't support this auth method, reply with (0xff)");
|
||||
self.inner
|
||||
.write_all(&[
|
||||
consts::SOCKS5_VERSION,
|
||||
consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE,
|
||||
])
|
||||
.await
|
||||
.context("Can't reply with method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthMethodUnacceptable(client_methods));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
method_supported = consts::SOCKS5_AUTH_METHOD_NONE;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Reply with method {} ({})",
|
||||
AuthenticationMethod::from_u8(method_supported).context("Method not supported")?,
|
||||
method_supported
|
||||
);
|
||||
self.inner
|
||||
.write(&[consts::SOCKS5_VERSION, method_supported])
|
||||
.await
|
||||
.context("Can't reply with method auth-none")?;
|
||||
Ok(method_supported)
|
||||
}
|
||||
|
||||
async fn read_username_password(socket: &mut T) -> Result<(String, String)> {
|
||||
trace!("Socks5Socket: authenticate()");
|
||||
let [version, user_len] = read_exact!(socket, [0u8; 2]).context("Can't read user len")?;
|
||||
debug!(
|
||||
"Auth: [version: {version}, user len: {len}]",
|
||||
version = version,
|
||||
len = user_len,
|
||||
);
|
||||
|
||||
if user_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Username malformed ({} chars)",
|
||||
user_len
|
||||
)));
|
||||
}
|
||||
|
||||
let username =
|
||||
read_exact!(socket, vec![0u8; user_len as usize]).context("Can't get username.")?;
|
||||
debug!("username bytes: {:?}", &username);
|
||||
|
||||
let [pass_len] = read_exact!(socket, [0u8; 1]).context("Can't read pass len")?;
|
||||
debug!("Auth: [pass len: {len}]", len = pass_len,);
|
||||
|
||||
if pass_len < 1 {
|
||||
return Err(SocksError::AuthenticationFailed(format!(
|
||||
"Password malformed ({} chars)",
|
||||
pass_len
|
||||
)));
|
||||
}
|
||||
|
||||
let password =
|
||||
read_exact!(socket, vec![0u8; pass_len as usize]).context("Can't get password.")?;
|
||||
debug!("password bytes: {:?}", &password);
|
||||
|
||||
let username = String::from_utf8(username).context("Failed to convert username")?;
|
||||
let password = String::from_utf8(password).context("Failed to convert password")?;
|
||||
|
||||
Ok((username, password))
|
||||
}
|
||||
|
||||
/// Only called if
|
||||
/// - this server has `Authentication` trait implemented.
|
||||
/// - and the client supports authentication via username/password
|
||||
/// - or the client doesn't send authentication, but we let the trait decides if the `allow_no_auth()` set as `true`
|
||||
async fn authenticate(&mut self, auth_method: u8) -> Result<A::Item> {
|
||||
let credentials = if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
let credentials = Self::read_username_password(&mut self.inner).await?;
|
||||
Some(credentials)
|
||||
} else {
|
||||
// the client hasn't provided any credentials, the function auth.authenticate()
|
||||
// will then check None, according to other parameters provided by the trait
|
||||
// such as IP, etc.
|
||||
None
|
||||
};
|
||||
|
||||
let auth = self.config.auth.as_ref().context("No auth module")?;
|
||||
|
||||
if let Some(credentials) = auth.authenticate(credentials).await {
|
||||
if auth_method == consts::SOCKS5_AUTH_METHOD_PASSWORD {
|
||||
// only the password way expect to write a response at this moment
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_REPLY_SUCCEEDED])
|
||||
.await
|
||||
.context("Can't reply auth success")?;
|
||||
}
|
||||
|
||||
info!("User logged successfully.");
|
||||
|
||||
return Ok(credentials);
|
||||
} else {
|
||||
self.inner
|
||||
.write_all(&[1, consts::SOCKS5_AUTH_METHOD_NOT_ACCEPTABLE])
|
||||
.await
|
||||
.context("Can't reply with auth method not acceptable.")?;
|
||||
|
||||
return Err(SocksError::AuthenticationRejected(format!(
|
||||
"Authentication, rejected."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to principally cover ReplyError types for both functions read & execute request.
|
||||
async fn request(&mut self) -> Result<()> {
|
||||
self.read_command().await?;
|
||||
|
||||
if self.config.dns_resolve {
|
||||
self.resolve_dns().await?;
|
||||
} else {
|
||||
debug!("Domain won't be resolved because `dns_resolve`'s config has been turned off.")
|
||||
}
|
||||
|
||||
if self.config.execute_command {
|
||||
self.execute_command().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reply error to the client with the reply code according to the RFC.
|
||||
async fn reply_error(&mut self, error: &ReplyError) -> Result<()> {
|
||||
let reply = new_reply(error, "0.0.0.0:0".parse().unwrap());
|
||||
debug!("reply error to be written: {:?}", &reply);
|
||||
|
||||
self.inner
|
||||
.write(&reply)
|
||||
.await
|
||||
.context("Can't write the reply!")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decide to whether or not, accept the authentication method.
|
||||
/// Don't forget that the methods list sent by the client, contains one or more methods.
|
||||
///
|
||||
/// # Request
|
||||
/// ```text
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// | 1 | 1 | 1 | 1 | Variable | 2 |
|
||||
/// +----+-----+-------+------+----------+----------+
|
||||
/// ```
|
||||
///
|
||||
/// It the request is correct, it should returns a ['SocketAddr'].
|
||||
///
|
||||
async fn read_command(&mut self) -> Result<()> {
|
||||
let [version, cmd, rsv, address_type] =
|
||||
read_exact!(self.inner, [0u8; 4]).context("Malformed request")?;
|
||||
debug!(
|
||||
"Request: [version: {version}, command: {cmd}, rev: {rsv}, address_type: {address_type}]",
|
||||
version = version,
|
||||
cmd = cmd,
|
||||
rsv = rsv,
|
||||
address_type = address_type,
|
||||
);
|
||||
|
||||
if version != consts::SOCKS5_VERSION {
|
||||
return Err(SocksError::UnsupportedSocksVersion(version));
|
||||
}
|
||||
|
||||
match Socks5Command::from_u8(cmd) {
|
||||
None => return Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPConnect => {
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::UDPAssociate => {
|
||||
if !self.config.allow_udp {
|
||||
return Err(ReplyError::CommandNotSupported.into());
|
||||
}
|
||||
self.cmd = Some(cmd);
|
||||
}
|
||||
Socks5Command::TCPBind => return Err(ReplyError::CommandNotSupported.into()),
|
||||
},
|
||||
}
|
||||
|
||||
// Guess address type
|
||||
let target_addr = read_address(&mut self.inner, address_type)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// print explicit error
|
||||
error!("{:#}", e);
|
||||
// then convert it to a reply
|
||||
ReplyError::AddressTypeNotSupported
|
||||
})?;
|
||||
|
||||
self.target_addr = Some(target_addr);
|
||||
|
||||
debug!("Request target is {}", self.target_addr.as_ref().unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function is public, it can be call manually on your own-willing
|
||||
/// if config flag has been turned off: `Config::dns_resolve == false`.
|
||||
pub async fn resolve_dns(&mut self) -> Result<()> {
|
||||
trace!("resolving dns");
|
||||
if let Some(target_addr) = self.target_addr.take() {
|
||||
// decide whether we have to resolve DNS or not
|
||||
self.target_addr = match target_addr {
|
||||
TargetAddr::Domain(_, _) => Some(target_addr.resolve_dns().await?),
|
||||
TargetAddr::Ip(_) => Some(target_addr),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the socks5 command that the client wants.
|
||||
async fn execute_command(&mut self) -> Result<()> {
|
||||
match &self.cmd {
|
||||
None => Err(ReplyError::CommandNotSupported.into()),
|
||||
Some(cmd) => match cmd {
|
||||
Socks5Command::TCPBind => Err(ReplyError::CommandNotSupported.into()),
|
||||
Socks5Command::TCPConnect => return self.execute_command_connect().await,
|
||||
Socks5Command::UDPAssociate => {
|
||||
if self.config.allow_udp {
|
||||
return self.execute_command_udp_assoc().await;
|
||||
} else {
|
||||
Err(ReplyError::CommandNotSupported.into())
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the target address that the client wants,
|
||||
/// then forward the data between them (client <=> target address).
|
||||
async fn execute_command_connect(&mut self) -> Result<()> {
|
||||
// async-std's ToSocketAddrs doesn't supports external trait implementation
|
||||
// @see https://github.com/async-rs/async-std/issues/539
|
||||
let addr = self
|
||||
.target_addr
|
||||
.as_ref()
|
||||
.context("target_addr empty")?
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
// TCP connect with timeout, to avoid memory leak for connection that takes forever
|
||||
let outbound = self
|
||||
.tcp_connector
|
||||
.tcp_connect(addr, self.config.request_timeout)
|
||||
.await?;
|
||||
|
||||
debug!("Connected to remote destination");
|
||||
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
self.inner.flush().await.context("Can't flush the reply!")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer(&mut self.inner, outbound).await
|
||||
}
|
||||
|
||||
/// Bind to a random UDP port, wait for the traffic from
|
||||
/// the client, and then forward the data to the remote addr.
|
||||
async fn execute_command_udp_assoc(&mut self) -> Result<()> {
|
||||
// The DST.ADDR and DST.PORT fields contain the address and port that
|
||||
// the client expects to use to send UDP datagrams on for the
|
||||
// association. The server MAY use this information to limit access
|
||||
// to the association.
|
||||
// @see Page 6, https://datatracker.ietf.org/doc/html/rfc1928.
|
||||
//
|
||||
// We do NOT limit the access from the client currently in this implementation.
|
||||
let _not_used = self.target_addr.as_ref();
|
||||
|
||||
// Listen with UDP6 socket, so the client can connect to it with either
|
||||
// IPv4 or IPv6.
|
||||
let peer_sock = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
// Respect the pre-populated reply IP address.
|
||||
self.inner
|
||||
.write(&new_reply(
|
||||
&ReplyError::Succeeded,
|
||||
SocketAddr::new(
|
||||
self.reply_ip.context("invalid reply ip")?,
|
||||
peer_sock.local_addr()?.port(),
|
||||
),
|
||||
))
|
||||
.await
|
||||
.context("Can't write successful reply")?;
|
||||
|
||||
debug!("Wrote success");
|
||||
|
||||
transfer_udp(peer_sock).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn target_addr(&self) -> Option<&TargetAddr> {
|
||||
self.target_addr.as_ref()
|
||||
}
|
||||
|
||||
pub fn auth(&self) -> &AuthenticationMethod {
|
||||
&self.auth
|
||||
}
|
||||
|
||||
pub fn cmd(&self) -> &Option<Socks5Command> {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
/// Borrow the credentials of the user has authenticated with
|
||||
pub fn get_credentials(&self) -> Option<&<<A as Authentication>::Item as Deref>::Target>
|
||||
where
|
||||
<A as Authentication>::Item: Deref,
|
||||
{
|
||||
self.credentials.as_deref()
|
||||
}
|
||||
|
||||
/// Get the credentials of the user has authenticated with
|
||||
pub fn take_credentials(&mut self) -> Option<A::Item> {
|
||||
self.credentials.take()
|
||||
}
|
||||
|
||||
pub fn tcp_connector(&self) -> &C {
|
||||
&self.tcp_connector
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy data between two peers
|
||||
/// Using 2 different generators, because they could be different structs with same traits.
|
||||
async fn transfer<I, O>(mut inbound: I, mut outbound: O) -> Result<()>
|
||||
where
|
||||
I: AsyncRead + AsyncWrite + Unpin,
|
||||
O: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match tokio::io::copy_bidirectional(&mut inbound, &mut outbound).await {
|
||||
Ok(res) => info!("transfer closed ({}, {})", res.0, res.1),
|
||||
Err(err) => error!("transfer error: {:?}", err),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_udp_request(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, client_addr) = inbound.recv_from(&mut buf).await?;
|
||||
debug!("Server recieve udp from {}", client_addr);
|
||||
inbound.connect(client_addr).await?;
|
||||
|
||||
let (frag, target_addr, data) = parse_udp_request(&buf[..size]).await?;
|
||||
|
||||
if frag != 0 {
|
||||
debug!("Discard UDP frag packets sliently.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Server forward to packet to {}", target_addr);
|
||||
let mut target_addr = target_addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("unreachable")?;
|
||||
|
||||
target_addr.set_ip(match target_addr.ip() {
|
||||
std::net::IpAddr::V4(v4) => std::net::IpAddr::V6(v4.to_ipv6_mapped()),
|
||||
v6 @ std::net::IpAddr::V6(_) => v6,
|
||||
});
|
||||
outbound.send_to(data, target_addr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_udp_response(inbound: &UdpSocket, outbound: &UdpSocket) -> Result<()> {
|
||||
let mut buf = vec![0u8; 0x10000];
|
||||
loop {
|
||||
let (size, remote_addr) = outbound.recv_from(&mut buf).await?;
|
||||
debug!("Recieve packet from {}", remote_addr);
|
||||
|
||||
let mut data = new_udp_header(remote_addr)?;
|
||||
data.extend_from_slice(&buf[..size]);
|
||||
inbound.send(&data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_udp(inbound: UdpSocket) -> Result<()> {
|
||||
let outbound = UdpSocket::bind("[::]:0").await?;
|
||||
|
||||
let req_fut = handle_udp_request(&inbound, &outbound);
|
||||
let res_fut = handle_udp_response(&inbound, &outbound);
|
||||
match try_join!(req_fut, res_fut) {
|
||||
Ok(_) => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Fixes the issue "cannot borrow data in dereference of `Pin<&mut >` as mutable"
|
||||
//
|
||||
// cf. https://users.rust-lang.org/t/take-in-impl-future-cannot-borrow-data-in-a-dereference-of-pin/52042
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> Unpin for Socks5Socket<T, A, S> where
|
||||
T: AsyncRead + AsyncWrite + Unpin
|
||||
{
|
||||
}
|
||||
|
||||
/// Allow us to read directly from the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncRead for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_read(context, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow us to write directly into the struct
|
||||
impl<T, A: Authentication, S: AsyncTcpConnector> AsyncWrite for Socks5Socket<T, A, S>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.inner).poll_write(context, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_flush(context)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
context: &mut std::task::Context,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.inner).poll_shutdown(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate reply code according to the RFC.
|
||||
fn new_reply(error: &ReplyError, sock_addr: SocketAddr) -> Vec<u8> {
|
||||
let (addr_type, mut ip_oct, mut port) = match sock_addr {
|
||||
SocketAddr::V4(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
SocketAddr::V6(sock) => (
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6,
|
||||
sock.ip().octets().to_vec(),
|
||||
sock.port().to_be_bytes().to_vec(),
|
||||
),
|
||||
};
|
||||
|
||||
let mut reply = vec![
|
||||
consts::SOCKS5_VERSION,
|
||||
error.as_u8(), // transform the error into byte code
|
||||
0x00, // reserved
|
||||
addr_type, // address type (ipv4, v6, domain)
|
||||
];
|
||||
reply.append(&mut ip_oct);
|
||||
reply.append(&mut port);
|
||||
|
||||
reply
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod stream;
|
||||
pub mod target_addr;
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::time::Duration;
|
||||
use tokio::io::ErrorKind as IOErrorKind;
|
||||
use tokio::net::{TcpStream, ToSocketAddrs};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::gateway::fast_socks5::{ReplyError, Result};
|
||||
|
||||
/// Easy to destructure bytes buffers by naming each fields:
|
||||
///
|
||||
/// # Examples (before)
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut buf = [0u8; 2];
|
||||
/// stream.read_exact(&mut buf).await?;
|
||||
/// let [version, method_len] = buf;
|
||||
///
|
||||
/// assert_eq!(version, 0x05);
|
||||
/// ```
|
||||
///
|
||||
/// # Examples (after)
|
||||
///
|
||||
/// ```ignore
|
||||
/// let [version, method_len] = read_exact!(stream, [0u8; 2]);
|
||||
///
|
||||
/// assert_eq!(version, 0x05);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! read_exact {
|
||||
($stream: expr, $array: expr) => {{
|
||||
let mut x = $array;
|
||||
// $stream
|
||||
// .read_exact(&mut x)
|
||||
// .await
|
||||
// .map_err(|_| io_err("lol"))?;
|
||||
$stream.read_exact(&mut x).await.map(|_| x)
|
||||
}};
|
||||
}
|
||||
|
||||
pub async fn tcp_connect_with_timeout<T>(addr: T, request_timeout_s: u64) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
let fut = tcp_connect(addr);
|
||||
match timeout(Duration::from_secs(request_timeout_s), fut).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(ReplyError::ConnectionTimeout.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn tcp_connect<T>(addr: T) -> Result<TcpStream>
|
||||
where
|
||||
T: ToSocketAddrs,
|
||||
{
|
||||
match TcpStream::connect(addr).await {
|
||||
Ok(o) => Ok(o),
|
||||
Err(e) => match e.kind() {
|
||||
// Match other TCP errors with ReplyError
|
||||
IOErrorKind::ConnectionRefused => Err(ReplyError::ConnectionRefused.into()),
|
||||
IOErrorKind::ConnectionAborted => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::ConnectionReset => Err(ReplyError::ConnectionNotAllowed.into()),
|
||||
IOErrorKind::NotConnected => Err(ReplyError::NetworkUnreachable.into()),
|
||||
_ => Err(e.into()), // #[error("General failure")] ?
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
use crate::gateway::fast_socks5::consts;
|
||||
use crate::gateway::fast_socks5::consts::SOCKS5_ADDR_TYPE_IPV4;
|
||||
use crate::gateway::fast_socks5::SocksError;
|
||||
use crate::read_exact;
|
||||
|
||||
use anyhow::Context;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
|
||||
use std::vec::IntoIter;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use tokio::net::lookup_host;
|
||||
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// SOCKS5 reply code
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AddrError {
|
||||
#[error("DNS Resolution failed")]
|
||||
DNSResolutionFailed,
|
||||
#[error("Can't read IPv4")]
|
||||
IPv4Unreadable,
|
||||
#[error("Can't read IPv6")]
|
||||
IPv6Unreadable,
|
||||
#[error("Can't read port number")]
|
||||
PortNumberUnreadable,
|
||||
#[error("Can't read domain len")]
|
||||
DomainLenUnreadable,
|
||||
#[error("Can't read Domain content")]
|
||||
DomainContentUnreadable,
|
||||
#[error("Malformed UTF-8")]
|
||||
Utf8,
|
||||
#[error("Unknown address type")]
|
||||
IncorrectAddressType,
|
||||
#[error("{0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A description of a connection target.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum TargetAddr {
|
||||
/// Connect to an IP address.
|
||||
Ip(SocketAddr),
|
||||
/// Connect to a fully qualified domain name.
|
||||
///
|
||||
/// The domain name will be passed along to the proxy server and DNS lookup
|
||||
/// will happen there.
|
||||
Domain(String, u16),
|
||||
}
|
||||
|
||||
impl TargetAddr {
|
||||
pub async fn resolve_dns(self) -> anyhow::Result<TargetAddr> {
|
||||
match self {
|
||||
TargetAddr::Ip(ip) => Ok(TargetAddr::Ip(ip)),
|
||||
TargetAddr::Domain(domain, port) => {
|
||||
debug!("Attempt to DNS resolve the domain {}...", &domain);
|
||||
|
||||
let socket_addr = lookup_host((&domain[..], port))
|
||||
.await
|
||||
.context(AddrError::DNSResolutionFailed)?
|
||||
.next()
|
||||
.ok_or(AddrError::Custom(
|
||||
"Can't fetch DNS to the domain.".to_string(),
|
||||
))?;
|
||||
debug!("domain name resolved to {}", socket_addr);
|
||||
|
||||
// has been converted to an ip
|
||||
Ok(TargetAddr::Ip(socket_addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ip(&self) -> bool {
|
||||
match self {
|
||||
TargetAddr::Ip(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_domain(&self) -> bool {
|
||||
!self.is_ip()
|
||||
}
|
||||
|
||||
pub fn to_be_bytes(&self) -> anyhow::Result<Vec<u8>> {
|
||||
let mut buf = vec![];
|
||||
match self {
|
||||
TargetAddr::Ip(SocketAddr::V4(addr)) => {
|
||||
debug!("TargetAddr::IpV4");
|
||||
|
||||
buf.extend_from_slice(&[SOCKS5_ADDR_TYPE_IPV4]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Ip(SocketAddr::V6(addr)) => {
|
||||
debug!("TargetAddr::IpV6");
|
||||
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_IPV6]);
|
||||
|
||||
debug!("addr ip {:?}", (*addr.ip()).octets());
|
||||
buf.extend_from_slice(&(addr.ip()).octets()); // ip
|
||||
buf.extend_from_slice(&addr.port().to_be_bytes()); // port
|
||||
}
|
||||
TargetAddr::Domain(ref domain, port) => {
|
||||
debug!("TargetAddr::Domain");
|
||||
if domain.len() > u8::max_value() as usize {
|
||||
return Err(SocksError::ExceededMaxDomainLen(domain.len()).into());
|
||||
}
|
||||
buf.extend_from_slice(&[consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME, domain.len() as u8]);
|
||||
buf.extend_from_slice(domain.as_bytes()); // domain content
|
||||
buf.extend_from_slice(&port.to_be_bytes());
|
||||
// port content (.to_be_bytes() convert from u16 to u8 type)
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// async-std ToSocketAddrs doesn't supports external trait implementation
|
||||
// @see https://github.com/async-rs/async-std/issues/539
|
||||
impl std::net::ToSocketAddrs for TargetAddr {
|
||||
type Iter = IntoIter<SocketAddr>;
|
||||
|
||||
fn to_socket_addrs(&self) -> io::Result<IntoIter<SocketAddr>> {
|
||||
match *self {
|
||||
TargetAddr::Ip(addr) => Ok(vec![addr].into_iter()),
|
||||
TargetAddr::Domain(_, _) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Domain name has to be explicitly resolved, please use TargetAddr::resolve_dns().",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetAddr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
TargetAddr::Ip(ref addr) => write!(f, "{}", addr),
|
||||
TargetAddr::Domain(ref addr, ref port) => write!(f, "{}:{}", addr, port),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for objects that can be converted to `TargetAddr`.
|
||||
pub trait ToTargetAddr {
|
||||
/// Converts the value of `self` to a `TargetAddr`.
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr>;
|
||||
}
|
||||
|
||||
impl<'a> ToTargetAddr for (&'a str, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
// try to parse as an IP first
|
||||
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
|
||||
return (addr, self.1).to_target_addr();
|
||||
}
|
||||
|
||||
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
|
||||
return (addr, self.1).to_target_addr();
|
||||
}
|
||||
|
||||
Ok(TargetAddr::Domain(self.0.to_owned(), self.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddr {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
Ok(TargetAddr::Ip(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV4 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V4(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for SocketAddrV6 {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddr::V6(*self).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv4Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddrV4::new(self.0, self.1).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Addr {
|
||||
V4([u8; 4]),
|
||||
V6([u8; 16]),
|
||||
Domain(String), // Vec<[u8]> or Box<[u8]> or String ?
|
||||
}
|
||||
|
||||
/// This function is used by the client & the server
|
||||
pub async fn read_address<T: AsyncRead + Unpin>(
|
||||
stream: &mut T,
|
||||
atyp: u8,
|
||||
) -> anyhow::Result<TargetAddr> {
|
||||
let addr = match atyp {
|
||||
consts::SOCKS5_ADDR_TYPE_IPV4 => {
|
||||
debug!("Address type `IPv4`");
|
||||
Addr::V4(read_exact!(stream, [0u8; 4]).context(AddrError::IPv4Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_IPV6 => {
|
||||
debug!("Address type `IPv6`");
|
||||
Addr::V6(read_exact!(stream, [0u8; 16]).context(AddrError::IPv6Unreadable)?)
|
||||
}
|
||||
consts::SOCKS5_ADDR_TYPE_DOMAIN_NAME => {
|
||||
debug!("Address type `domain`");
|
||||
let len = read_exact!(stream, [0]).context(AddrError::DomainLenUnreadable)?[0];
|
||||
let domain = read_exact!(stream, vec![0u8; len as usize])
|
||||
.context(AddrError::DomainContentUnreadable)?;
|
||||
// make sure the bytes are correct utf8 string
|
||||
let domain = String::from_utf8(domain).context(AddrError::Utf8)?;
|
||||
|
||||
Addr::Domain(domain)
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!(AddrError::IncorrectAddressType)),
|
||||
};
|
||||
|
||||
// Find port number
|
||||
let port = read_exact!(stream, [0u8; 2]).context(AddrError::PortNumberUnreadable)?;
|
||||
// Convert (u8 * 2) into u16
|
||||
let port = (port[0] as u16) << 8 | port[1] as u16;
|
||||
|
||||
// Merge ADDRESS + PORT into a TargetAddr
|
||||
let addr: TargetAddr = match addr {
|
||||
Addr::V4([a, b, c, d]) => (Ipv4Addr::new(a, b, c, d), port).to_target_addr()?,
|
||||
Addr::V6(x) => (Ipv6Addr::from(x), port).to_target_addr()?,
|
||||
Addr::Domain(domain) => TargetAddr::Domain(domain, port),
|
||||
};
|
||||
|
||||
Ok(addr)
|
||||
}
|
||||
@@ -9,6 +9,12 @@ pub mod tcp_proxy;
|
||||
#[cfg(feature = "smoltcp")]
|
||||
pub mod tokio_smoltcp;
|
||||
pub mod udp_proxy;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod fast_socks5;
|
||||
#[cfg(feature = "socks5")]
|
||||
pub mod socks5;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CidrSet {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
gateway::{
|
||||
fast_socks5::{
|
||||
server::{
|
||||
AcceptAuthentication, AsyncTcpConnector, Config, SimpleUserPassword, Socks5Socket,
|
||||
},
|
||||
util::stream::tcp_connect_with_timeout,
|
||||
},
|
||||
tokio_smoltcp::TcpStream,
|
||||
},
|
||||
tunnel::packet_def::PacketType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use dashmap::DashSet;
|
||||
use pnet::packet::{ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, Packet};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
select,
|
||||
};
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{mpsc, Mutex},
|
||||
task::JoinSet,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::GlobalCtx},
|
||||
gateway::tokio_smoltcp::{channel_device, Net, NetConfig},
|
||||
peers::{peer_manager::PeerManager, PeerPacketFilter},
|
||||
tunnel::packet_def::ZCPacket,
|
||||
};
|
||||
|
||||
enum SocksTcpStream {
|
||||
TcpStream(tokio::net::TcpStream),
|
||||
SmolTcpStream(TcpStream),
|
||||
}
|
||||
|
||||
impl AsyncRead for SocksTcpStream {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<std::io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_read(cx, buf)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for SocksTcpStream {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_write(cx, buf)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_write(cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => std::pin::Pin::new(stream).poll_flush(cx),
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_flush(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), std::io::Error>> {
|
||||
match self.get_mut() {
|
||||
SocksTcpStream::TcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_shutdown(cx)
|
||||
}
|
||||
SocksTcpStream::SmolTcpStream(ref mut stream) => {
|
||||
std::pin::Pin::new(stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||
struct Socks5Entry {
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
}
|
||||
|
||||
type Socks5EntrySet = Arc<DashSet<Socks5Entry>>;
|
||||
|
||||
struct Socks5ServerNet {
|
||||
ipv4_addr: Ipv4Addr,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
smoltcp_net: Arc<Net>,
|
||||
forward_tasks: Arc<std::sync::Mutex<JoinSet<()>>>,
|
||||
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
impl Socks5ServerNet {
|
||||
pub fn new(
|
||||
ipv4_addr: Ipv4Addr,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
entries: Socks5EntrySet,
|
||||
) -> Self {
|
||||
let mut forward_tasks = JoinSet::new();
|
||||
let mut cap = smoltcp::phy::DeviceCapabilities::default();
|
||||
cap.max_transmission_unit = 1280;
|
||||
cap.medium = smoltcp::phy::Medium::Ip;
|
||||
let (dev, stack_sink, mut stack_stream) = channel_device::ChannelDevice::new(cap);
|
||||
|
||||
let packet_recv = packet_recv.clone();
|
||||
forward_tasks.spawn(async move {
|
||||
let mut smoltcp_stack_receiver = packet_recv.lock().await;
|
||||
while let Some(packet) = smoltcp_stack_receiver.recv().await {
|
||||
tracing::trace!(?packet, "receive from peer send to smoltcp packet");
|
||||
if let Err(e) = stack_sink.send(Ok(packet.payload().to_vec())).await {
|
||||
tracing::error!("send to smoltcp stack failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack sink exited");
|
||||
panic!("smoltcp stack sink exited");
|
||||
});
|
||||
|
||||
forward_tasks.spawn(async move {
|
||||
while let Some(data) = stack_stream.recv().await {
|
||||
tracing::trace!(
|
||||
?data,
|
||||
"receive from smoltcp stack and send to peer mgr packet"
|
||||
);
|
||||
let Some(ipv4) = Ipv4Packet::new(&data) else {
|
||||
tracing::error!(?data, "smoltcp stack stream get non ipv4 packet");
|
||||
continue;
|
||||
};
|
||||
|
||||
let dst = ipv4.get_destination();
|
||||
let packet = ZCPacket::new_with_payload(&data);
|
||||
if let Err(e) = peer_manager.send_msg_ipv4(packet, dst).await {
|
||||
tracing::error!("send to peer failed in smoltcp sender: {:?}", e);
|
||||
}
|
||||
}
|
||||
tracing::error!("smoltcp stack stream exited");
|
||||
panic!("smoltcp stack stream exited");
|
||||
});
|
||||
|
||||
let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip);
|
||||
let net = Net::new(
|
||||
dev,
|
||||
NetConfig::new(
|
||||
interface_config,
|
||||
format!("{}/24", ipv4_addr).parse().unwrap(),
|
||||
vec![format!("{}", ipv4_addr).parse().unwrap()],
|
||||
),
|
||||
);
|
||||
|
||||
Self {
|
||||
ipv4_addr,
|
||||
auth,
|
||||
|
||||
smoltcp_net: Arc::new(net),
|
||||
forward_tasks: Arc::new(std::sync::Mutex::new(forward_tasks)),
|
||||
|
||||
entries,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tcp_stream(&self, stream: tokio::net::TcpStream) {
|
||||
let mut config = Config::<AcceptAuthentication>::default();
|
||||
config.set_request_timeout(10);
|
||||
config.set_skip_auth(false);
|
||||
config.set_allow_no_auth(true);
|
||||
|
||||
struct SmolTcpConnector(
|
||||
Arc<Net>,
|
||||
Socks5EntrySet,
|
||||
std::sync::Mutex<Option<Socks5Entry>>,
|
||||
);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTcpConnector for SmolTcpConnector {
|
||||
type S = SocksTcpStream;
|
||||
|
||||
async fn tcp_connect(
|
||||
&self,
|
||||
addr: SocketAddr,
|
||||
timeout_s: u64,
|
||||
) -> crate::gateway::fast_socks5::Result<SocksTcpStream> {
|
||||
let local_addr = self.0.get_address();
|
||||
let port = self.0.get_port();
|
||||
|
||||
let entry = Socks5Entry {
|
||||
src: SocketAddr::new(local_addr, port),
|
||||
dst: addr,
|
||||
};
|
||||
*self.2.lock().unwrap() = Some(entry.clone());
|
||||
self.1.insert(entry);
|
||||
|
||||
if addr.ip() == local_addr {
|
||||
let modified_addr =
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), addr.port());
|
||||
|
||||
Ok(SocksTcpStream::TcpStream(
|
||||
tcp_connect_with_timeout(modified_addr, timeout_s).await?,
|
||||
))
|
||||
} else {
|
||||
let remote_socket = timeout(
|
||||
Duration::from_secs(timeout_s),
|
||||
self.0.tcp_connect(addr, port),
|
||||
)
|
||||
.await
|
||||
.with_context(|| "connect to remote timeout")?;
|
||||
|
||||
Ok(SocksTcpStream::SmolTcpStream(remote_socket.map_err(
|
||||
|e| super::fast_socks5::SocksError::Other(e.into()),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmolTcpConnector {
|
||||
fn drop(&mut self) {
|
||||
if let Some(entry) = self.2.lock().unwrap().take() {
|
||||
self.1.remove(&entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let socket = Socks5Socket::new(
|
||||
stream,
|
||||
Arc::new(config),
|
||||
SmolTcpConnector(
|
||||
self.smoltcp_net.clone(),
|
||||
self.entries.clone(),
|
||||
std::sync::Mutex::new(None),
|
||||
),
|
||||
);
|
||||
|
||||
self.forward_tasks.lock().unwrap().spawn(async move {
|
||||
match socket.upgrade_to_socks5().await {
|
||||
Ok(_) => {
|
||||
tracing::info!("socks5 handle success");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("socks5 handshake failed: {:?}", e);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Socks5Server {
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
packet_sender: mpsc::Sender<ZCPacket>,
|
||||
packet_recv: Arc<Mutex<mpsc::Receiver<ZCPacket>>>,
|
||||
|
||||
net: Arc<Mutex<Option<Socks5ServerNet>>>,
|
||||
entries: Socks5EntrySet,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl PeerPacketFilter for Socks5Server {
|
||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||
let hdr = packet.peer_manager_header().unwrap();
|
||||
if hdr.packet_type != PacketType::Data as u8 {
|
||||
return Some(packet);
|
||||
};
|
||||
|
||||
let payload_bytes = packet.payload();
|
||||
|
||||
let ipv4 = Ipv4Packet::new(payload_bytes).unwrap();
|
||||
if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap();
|
||||
let entry = Socks5Entry {
|
||||
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
|
||||
src: SocketAddr::new(ipv4.get_destination().into(), tcp_packet.get_destination()),
|
||||
};
|
||||
|
||||
if !self.entries.contains(&entry) {
|
||||
return Some(packet);
|
||||
}
|
||||
|
||||
let _ = self.packet_sender.try_send(packet).ok();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Socks5Server {
|
||||
pub fn new(
|
||||
global_ctx: Arc<GlobalCtx>,
|
||||
peer_manager: Arc<PeerManager>,
|
||||
auth: Option<SimpleUserPassword>,
|
||||
) -> Arc<Self> {
|
||||
let (packet_sender, packet_recv) = mpsc::channel(1024);
|
||||
Arc::new(Self {
|
||||
global_ctx,
|
||||
peer_manager,
|
||||
auth,
|
||||
|
||||
tasks: Arc::new(Mutex::new(JoinSet::new())),
|
||||
packet_recv: Arc::new(Mutex::new(packet_recv)),
|
||||
packet_sender,
|
||||
|
||||
net: Arc::new(Mutex::new(None)),
|
||||
entries: Arc::new(DashSet::new()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_net_update_task(self: &Arc<Self>) {
|
||||
let net = self.net.clone();
|
||||
let global_ctx = self.global_ctx.clone();
|
||||
let peer_manager = self.peer_manager.clone();
|
||||
let packet_recv = self.packet_recv.clone();
|
||||
let entries = self.entries.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
let mut prev_ipv4 = None;
|
||||
loop {
|
||||
let mut event_recv = global_ctx.subscribe();
|
||||
|
||||
let cur_ipv4 = global_ctx.get_ipv4();
|
||||
if prev_ipv4 != cur_ipv4 {
|
||||
prev_ipv4 = cur_ipv4;
|
||||
entries.clear();
|
||||
|
||||
if cur_ipv4.is_none() {
|
||||
let _ = net.lock().await.take();
|
||||
} else {
|
||||
net.lock().await.replace(Socks5ServerNet::new(
|
||||
cur_ipv4.unwrap(),
|
||||
None,
|
||||
peer_manager.clone(),
|
||||
packet_recv.clone(),
|
||||
entries.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
select! {
|
||||
_ = event_recv.recv() => {}
|
||||
_ = tokio::time::sleep(Duration::from_secs(120)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn run(self: &Arc<Self>) -> Result<(), Error> {
|
||||
let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bind_addr = format!(
|
||||
"{}:{}",
|
||||
proxy_url.host_str().unwrap(),
|
||||
proxy_url.port().unwrap()
|
||||
);
|
||||
|
||||
let listener = {
|
||||
let _g = self.global_ctx.net_ns.guard();
|
||||
TcpListener::bind(bind_addr.parse::<SocketAddr>().unwrap()).await?
|
||||
};
|
||||
|
||||
self.peer_manager
|
||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||
.await;
|
||||
|
||||
self.run_net_update_task().await;
|
||||
|
||||
let net = self.net.clone();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((socket, _addr)) => {
|
||||
tracing::info!("accept a new connection, {:?}", socket);
|
||||
if let Some(net) = net.lock().await.as_ref() {
|
||||
net.handle_tcp_stream(socket);
|
||||
}
|
||||
}
|
||||
Err(err) => tracing::error!("accept error = {:?}", err),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,55 @@ impl ProxyTcpStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "smoltcp")]
|
||||
struct SmolTcpListener {
|
||||
listener_task: JoinSet<()>,
|
||||
listen_count: usize,
|
||||
|
||||
stream_rx: mpsc::UnboundedReceiver<Result<(tokio_smoltcp::TcpStream, SocketAddr)>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "smoltcp")]
|
||||
impl SmolTcpListener {
|
||||
pub async fn new(net: Arc<Mutex<Option<Net>>>, listen_count: usize) -> Self {
|
||||
let mut tasks = JoinSet::new();
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let locked_net = net.lock().await;
|
||||
for _ in 0..listen_count {
|
||||
let mut tcp = locked_net
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.tcp_bind("0.0.0.0:8899".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let tx = tx.clone();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
tx.send(tcp.accept().await.map_err(|e| {
|
||||
anyhow::anyhow!("smol tcp listener accept failed: {:?}", e).into()
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
listener_task: tasks,
|
||||
listen_count,
|
||||
stream_rx: rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept(&mut self) -> Result<(tokio_smoltcp::TcpStream, SocketAddr)> {
|
||||
self.stream_rx.recv().await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyTcpListener {
|
||||
KernelTcpListener(TcpListener),
|
||||
#[cfg(feature = "smoltcp")]
|
||||
SmolTcpListener(tokio_smoltcp::TcpListener),
|
||||
SmolTcpListener(SmolTcpListener),
|
||||
}
|
||||
|
||||
impl ProxyTcpListener {
|
||||
@@ -375,8 +420,8 @@ impl TcpProxy {
|
||||
),
|
||||
);
|
||||
net.set_any_ip(true);
|
||||
let tcp = net.tcp_bind("0.0.0.0:8899".parse().unwrap()).await?;
|
||||
self.smoltcp_net.lock().await.replace(net);
|
||||
let tcp = SmolTcpListener::new(self.smoltcp_net.clone(), 64).await;
|
||||
|
||||
self.enable_smoltcp
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicU16, Ordering},
|
||||
Arc,
|
||||
@@ -134,7 +134,10 @@ impl Net {
|
||||
fut,
|
||||
)
|
||||
}
|
||||
fn get_port(&self) -> u16 {
|
||||
pub fn get_address(&self) -> IpAddr {
|
||||
self.ip_addr.address().into()
|
||||
}
|
||||
pub fn get_port(&self) -> u16 {
|
||||
self.from_port
|
||||
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| {
|
||||
Some(if x > 60000 { 10000 } else { x + 1 })
|
||||
@@ -147,10 +150,10 @@ impl Net {
|
||||
TcpListener::new(self.reactor.clone(), addr.into()).await
|
||||
}
|
||||
/// Opens a TCP connection to a remote host.
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr) -> io::Result<TcpStream> {
|
||||
pub async fn tcp_connect(&self, addr: SocketAddr, local_port: u16) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(
|
||||
self.reactor.clone(),
|
||||
(self.ip_addr.address(), self.get_port()).into(),
|
||||
(self.ip_addr.address(), local_port).into(),
|
||||
addr.into(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -31,6 +32,9 @@ use crate::vpn_portal::{self, VpnPortal};
|
||||
|
||||
use super::listeners::ListenerManager;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
use crate::gateway::socks5::Socks5Server;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IpProxy {
|
||||
tcp_proxy: Arc<TcpProxy>,
|
||||
@@ -91,7 +95,7 @@ impl NicCtx {
|
||||
}
|
||||
}
|
||||
|
||||
type ArcNicCtx = Arc<Mutex<Option<NicCtx>>>;
|
||||
type ArcNicCtx = Arc<Mutex<Option<Box<dyn Any + 'static + Send>>>>;
|
||||
|
||||
pub struct Instance {
|
||||
inst_name: String,
|
||||
@@ -115,6 +119,9 @@ pub struct Instance {
|
||||
|
||||
vpn_portal: Arc<Mutex<Box<dyn VpnPortal>>>,
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server: Arc<Socks5Server>,
|
||||
|
||||
global_ctx: ArcGlobalCtx,
|
||||
}
|
||||
|
||||
@@ -160,6 +167,9 @@ impl Instance {
|
||||
#[cfg(not(feature = "wireguard"))]
|
||||
let vpn_portal_inst = vpn_portal::NullVpnPortal;
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
let socks5_server = Socks5Server::new(global_ctx.clone(), peer_manager.clone(), None);
|
||||
|
||||
Instance {
|
||||
inst_name: global_ctx.inst_name.clone(),
|
||||
id,
|
||||
@@ -180,6 +190,9 @@ impl Instance {
|
||||
|
||||
vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))),
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
socks5_server,
|
||||
|
||||
global_ctx,
|
||||
}
|
||||
}
|
||||
@@ -197,14 +210,28 @@ impl Instance {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_nic_ctx(arc_nic_ctx: ArcNicCtx) {
|
||||
// use a mock nic ctx to consume packets.
|
||||
async fn clear_nic_ctx(
|
||||
arc_nic_ctx: ArcNicCtx,
|
||||
packet_recv: Arc<Mutex<PacketRecvChanReceiver>>,
|
||||
) {
|
||||
let _ = arc_nic_ctx.lock().await.take();
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
let mut packet_recv = packet_recv.lock().await;
|
||||
while let Some(packet) = packet_recv.recv().await {
|
||||
tracing::trace!("packet consumed by mock nic ctx: {:?}", packet);
|
||||
}
|
||||
});
|
||||
arc_nic_ctx.lock().await.replace(Box::new(tasks));
|
||||
|
||||
tracing::debug!("nic ctx cleared.");
|
||||
}
|
||||
|
||||
async fn use_new_nic_ctx(arc_nic_ctx: ArcNicCtx, nic_ctx: NicCtx) {
|
||||
let mut g = arc_nic_ctx.lock().await;
|
||||
*g = Some(nic_ctx);
|
||||
*g = Some(Box::new(nic_ctx));
|
||||
tracing::debug!("nic ctx updated.");
|
||||
}
|
||||
|
||||
@@ -274,7 +301,7 @@ impl Instance {
|
||||
"dhcp start changing ip"
|
||||
);
|
||||
|
||||
Self::clear_nic_ctx(nic_ctx.clone()).await;
|
||||
Self::clear_nic_ctx(nic_ctx.clone(), _peer_packet_receiver.clone()).await;
|
||||
|
||||
if let Some(ip) = candidate_ipv4_addr {
|
||||
if global_ctx_c.no_tun() {
|
||||
@@ -329,9 +356,9 @@ impl Instance {
|
||||
self.listener_manager.lock().await.run().await?;
|
||||
self.peer_manager.run().await?;
|
||||
|
||||
if self.global_ctx.config.get_flags().no_tun {
|
||||
self.peer_packet_receiver.lock().await.close();
|
||||
} else {
|
||||
Self::clear_nic_ctx(self.nic_ctx.clone(), self.peer_packet_receiver.clone()).await;
|
||||
|
||||
if !self.global_ctx.config.get_flags().no_tun {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if let Some(ipv4_addr) = self.global_ctx.get_ipv4() {
|
||||
let mut new_nic_ctx = NicCtx::new(
|
||||
@@ -372,6 +399,9 @@ impl Instance {
|
||||
self.run_vpn_portal().await?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "socks5")]
|
||||
self.socks5_server.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -532,7 +562,7 @@ impl Instance {
|
||||
fd: i32,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
println!("setup_nic_ctx_for_android, fd: {}", fd);
|
||||
Self::clear_nic_ctx(nic_ctx.clone()).await;
|
||||
Self::clear_nic_ctx(nic_ctx.clone(), peer_packet_receiver.clone()).await;
|
||||
if fd <= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
pub mod instance;
|
||||
pub mod listeners;
|
||||
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod tun_codec;
|
||||
#[cfg(feature = "tun")]
|
||||
pub mod virtual_nic;
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
use byteorder::{NativeEndian, NetworkEndian, WriteBytesExt};
|
||||
use tokio_util::bytes::{BufMut, Bytes, BytesMut};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
/// A packet protocol IP version
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
enum PacketProtocol {
|
||||
#[default]
|
||||
IPv4,
|
||||
IPv6,
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
// Note: the protocol in the packet information header is platform dependent.
|
||||
impl PacketProtocol {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16),
|
||||
PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16),
|
||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"neither an IPv4 nor IPv6 packet",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
|
||||
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
|
||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"neither an IPv4 nor IPv6 packet",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TunPacketBuffer {
|
||||
Bytes(Bytes),
|
||||
BytesMut(BytesMut),
|
||||
}
|
||||
|
||||
impl From<TunPacketBuffer> for Bytes {
|
||||
fn from(buf: TunPacketBuffer) -> Self {
|
||||
match buf {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes,
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.freeze(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for TunPacketBuffer {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes.as_ref(),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tun Packet to be sent or received on the TUN interface.
|
||||
#[derive(Debug)]
|
||||
pub struct TunPacket(PacketProtocol, TunPacketBuffer);
|
||||
|
||||
/// Infer the protocol based on the first nibble in the packet buffer.
|
||||
fn infer_proto(buf: &[u8]) -> PacketProtocol {
|
||||
match buf[0] >> 4 {
|
||||
4 => PacketProtocol::IPv4,
|
||||
6 => PacketProtocol::IPv6,
|
||||
p => PacketProtocol::Other(p),
|
||||
}
|
||||
}
|
||||
|
||||
impl TunPacket {
|
||||
/// Create a new `TunPacket` based on a byte slice.
|
||||
pub fn new(buffer: TunPacketBuffer) -> TunPacket {
|
||||
let proto = infer_proto(buffer.as_ref());
|
||||
TunPacket(proto, buffer)
|
||||
}
|
||||
|
||||
/// Return this packet's bytes.
|
||||
pub fn get_bytes(&self) -> &[u8] {
|
||||
match &self.1 {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes.as_ref(),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Bytes {
|
||||
match self.1 {
|
||||
TunPacketBuffer::Bytes(bytes) => bytes,
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes.freeze(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes_mut(self) -> BytesMut {
|
||||
match self.1 {
|
||||
TunPacketBuffer::Bytes(_) => panic!("cannot into_bytes_mut from bytes"),
|
||||
TunPacketBuffer::BytesMut(bytes) => bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A TunPacket Encoder/Decoder.
|
||||
pub struct TunPacketCodec(bool, i32);
|
||||
|
||||
impl TunPacketCodec {
|
||||
/// Create a new `TunPacketCodec` specifying whether the underlying
|
||||
/// tunnel Device has enabled the packet information header.
|
||||
pub fn new(pi: bool, mtu: i32) -> TunPacketCodec {
|
||||
TunPacketCodec(pi, mtu)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for TunPacketCodec {
|
||||
type Item = TunPacket;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if buf.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut pkt = buf.split_to(buf.len());
|
||||
|
||||
// reserve enough space for the next packet
|
||||
if self.0 {
|
||||
buf.reserve(self.1 as usize + 4);
|
||||
} else {
|
||||
buf.reserve(self.1 as usize);
|
||||
}
|
||||
|
||||
// if the packet information is enabled we have to ignore the first 4 bytes
|
||||
if self.0 {
|
||||
let _ = pkt.split_to(4);
|
||||
}
|
||||
|
||||
let proto = infer_proto(pkt.as_ref());
|
||||
Ok(Some(TunPacket(proto, TunPacketBuffer::BytesMut(pkt))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<TunPacket> for TunPacketCodec {
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, item: TunPacket, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
dst.reserve(item.get_bytes().len() + 4);
|
||||
match item {
|
||||
TunPacket(proto, bytes) if self.0 => {
|
||||
// build the packet information header comprising of 2 u16
|
||||
// fields: flags and protocol.
|
||||
let mut buf = Vec::<u8>::with_capacity(4);
|
||||
|
||||
// flags is always 0
|
||||
buf.write_u16::<NativeEndian>(0)?;
|
||||
// write the protocol as network byte order
|
||||
buf.write_u16::<NetworkEndian>(proto.into_pi_field()?)?;
|
||||
|
||||
dst.put_slice(&buf);
|
||||
dst.put(Bytes::from(bytes));
|
||||
}
|
||||
TunPacket(_, bytes) => dst.put(Bytes::from(bytes)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ use tokio::{
|
||||
task::JoinSet,
|
||||
};
|
||||
use tokio_util::bytes::Bytes;
|
||||
use tun::{create_as_async, AsyncDevice, Configuration, Device as _, Layer};
|
||||
use tun::{AbstractDevice, AsyncDevice, Configuration, Layer};
|
||||
use zerocopy::{NativeEndian, NetworkEndian};
|
||||
|
||||
pin_project! {
|
||||
@@ -119,7 +119,7 @@ impl PacketProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
|
||||
fn into_pi_field(self) -> Result<u16, io::Error> {
|
||||
use nix::libc;
|
||||
match self {
|
||||
@@ -237,20 +237,15 @@ impl AsyncWrite for TunAsyncWrite {
|
||||
}
|
||||
|
||||
pub struct VirtualNic {
|
||||
dev_name: String,
|
||||
queue_num: usize,
|
||||
|
||||
global_ctx: ArcGlobalCtx,
|
||||
|
||||
ifname: Option<String>,
|
||||
ifcfg: Box<dyn IfConfiguerTrait + Send + Sync + 'static>,
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn checkreg() -> io::Result<()> {
|
||||
use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey,enums::KEY_ALL_ACCESS};
|
||||
// 打开根键
|
||||
pub fn checkreg(dev_name: &str) -> io::Result<()> {
|
||||
use winreg::{enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS, RegKey};
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
// 打开指定的子键
|
||||
let profiles_key = hklm.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||
KEY_ALL_ACCESS,
|
||||
@@ -259,21 +254,21 @@ pub fn checkreg() -> io::Result<()> {
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\Unmanaged",
|
||||
KEY_ALL_ACCESS,
|
||||
)?;
|
||||
// 收集要删除的子键名称
|
||||
// collect subkeys to delete
|
||||
let mut keys_to_delete = Vec::new();
|
||||
let mut keys_to_delete_unmanaged = Vec::new();
|
||||
for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) {
|
||||
let subkey = profiles_key.open_subkey(&subkey_name)?;
|
||||
// 尝试读取 ProfileName 值
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("ProfileName") {
|
||||
Ok(profile_name) => {
|
||||
// 检查 ProfileName 是否包含 "et"
|
||||
if profile_name.contains("et_") {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete.push(subkey_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 打印错误信息
|
||||
tracing::error!(
|
||||
"Failed to read ProfileName for subkey {}: {}",
|
||||
subkey_name,
|
||||
@@ -284,16 +279,16 @@ pub fn checkreg() -> io::Result<()> {
|
||||
}
|
||||
for subkey_name in unmanaged_key.enum_keys().filter_map(Result::ok) {
|
||||
let subkey = unmanaged_key.open_subkey(&subkey_name)?;
|
||||
// 尝试读取 ProfileName 值
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("Description") {
|
||||
Ok(profile_name) => {
|
||||
// 检查 ProfileName 是否包含 "et"
|
||||
if profile_name.contains("et_") {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete_unmanaged.push(subkey_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 打印错误信息
|
||||
tracing::error!(
|
||||
"Failed to read ProfileName for subkey {}: {}",
|
||||
subkey_name,
|
||||
@@ -302,7 +297,7 @@ pub fn checkreg() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
//删除收集到的子键
|
||||
// delete collected subkeys
|
||||
if !keys_to_delete.is_empty() {
|
||||
for subkey_name in keys_to_delete {
|
||||
match profiles_key.delete_subkey_all(&subkey_name) {
|
||||
@@ -325,25 +320,13 @@ pub fn checkreg() -> io::Result<()> {
|
||||
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_tun(&mut self) -> Result<AsyncDevice, Error> {
|
||||
async fn create_tun(&mut self) -> Result<tun::platform::Device, Error> {
|
||||
let mut config = Configuration::default();
|
||||
config.layer(Layer::L3);
|
||||
|
||||
@@ -351,22 +334,25 @@ impl VirtualNic {
|
||||
{
|
||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||
if !dev_name.is_empty() {
|
||||
config.name(format!("{}", dev_name));
|
||||
config.tun_name(format!("{}", dev_name));
|
||||
}
|
||||
config.platform(|config| {
|
||||
// detect protocol by ourselves for cross platform
|
||||
config.packet_information(false);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos"))]
|
||||
config.platform_config(|config| {
|
||||
// disable packet information so we can process the header by ourselves, see tun2 impl for more details
|
||||
config.packet_information(false);
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match checkreg(){
|
||||
let dev_name = self.global_ctx.get_flags().dev_name;
|
||||
|
||||
match checkreg(&dev_name) {
|
||||
Ok(_) => tracing::trace!("delete successful!"),
|
||||
Err(e) => tracing::error!("An error occurred: {}", e),
|
||||
}
|
||||
use rand::distributions::Distribution as _;
|
||||
use std::net::IpAddr;
|
||||
let c = crate::arch::windows::interface_count()?;
|
||||
let mut rng = rand::thread_rng();
|
||||
let s: String = rand::distributions::Alphanumeric
|
||||
@@ -376,13 +362,14 @@ impl VirtualNic {
|
||||
.collect::<String>()
|
||||
.to_lowercase();
|
||||
|
||||
config.name(format!("et{}_{}_{}", self.dev_name, c, s));
|
||||
// set a temporary address
|
||||
config.address(format!("172.0.{}.3", c).parse::<IpAddr>().unwrap());
|
||||
if !dev_name.is_empty() {
|
||||
config.tun_name(format!("{}", dev_name));
|
||||
} else {
|
||||
config.tun_name(format!("et_{}_{}", c, s));
|
||||
}
|
||||
|
||||
config.platform(|config| {
|
||||
config.platform_config(|config| {
|
||||
config.skip_config(true);
|
||||
config.guid(None);
|
||||
config.ring_cap(Some(std::cmp::min(
|
||||
config.min_ring_cap() * 32,
|
||||
config.max_ring_cap(),
|
||||
@@ -390,14 +377,10 @@ impl VirtualNic {
|
||||
});
|
||||
}
|
||||
|
||||
if self.queue_num != 1 {
|
||||
todo!("queue_num != 1")
|
||||
}
|
||||
config.queues(self.queue_num);
|
||||
config.up();
|
||||
|
||||
let _g = self.global_ctx.net_ns.guard();
|
||||
Ok(create_as_async(&config)?)
|
||||
Ok(tun::create(&config)?)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -409,12 +392,11 @@ impl VirtualNic {
|
||||
let mut config = Configuration::default();
|
||||
config.layer(Layer::L3);
|
||||
config.raw_fd(tun_fd);
|
||||
config.platform(|config| {
|
||||
config.no_close_fd_on_drop(true);
|
||||
});
|
||||
config.close_fd_on_drop(false);
|
||||
config.up();
|
||||
|
||||
let dev = create_as_async(&config)?;
|
||||
let dev = tun::create(&config)?;
|
||||
let dev = AsyncDevice::new(dev)?;
|
||||
let (a, b) = BiLock::new(dev);
|
||||
let ft = TunnelWrapper::new(
|
||||
TunStream::new(a, false),
|
||||
@@ -432,9 +414,11 @@ impl VirtualNic {
|
||||
|
||||
pub async fn create_dev(&mut self) -> Result<Box<dyn Tunnel>, Error> {
|
||||
let dev = self.create_tun().await?;
|
||||
let ifname = dev.get_ref().name()?;
|
||||
let ifname = dev.tun_name()?;
|
||||
self.ifcfg.wait_interface_show(ifname.as_str()).await?;
|
||||
|
||||
let dev = AsyncDevice::new(dev)?;
|
||||
|
||||
let flags = self.global_ctx.config.get_flags();
|
||||
let mut mtu_in_config = flags.mtu;
|
||||
if flags.enable_encryption {
|
||||
@@ -529,7 +513,8 @@ impl NicCtx {
|
||||
nic.link_up().await?;
|
||||
nic.remove_ip(None).await?;
|
||||
nic.add_ip(ipv4_addr, 24).await?;
|
||||
if cfg!(target_os = "macos") {
|
||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||
{
|
||||
nic.add_route(ipv4_addr, 24).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
||||
use dashmap::DashMap;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Mutex,
|
||||
},
|
||||
task::JoinSet,
|
||||
@@ -37,6 +37,7 @@ use super::{
|
||||
struct ForeignNetworkEntry {
|
||||
network: NetworkIdentity,
|
||||
peer_map: Arc<PeerMap>,
|
||||
relay_data: bool,
|
||||
}
|
||||
|
||||
impl ForeignNetworkEntry {
|
||||
@@ -45,9 +46,14 @@ impl ForeignNetworkEntry {
|
||||
packet_sender: PacketRecvChan,
|
||||
global_ctx: ArcGlobalCtx,
|
||||
my_peer_id: PeerId,
|
||||
relay_data: bool,
|
||||
) -> Self {
|
||||
let peer_map = Arc::new(PeerMap::new(packet_sender, global_ctx, my_peer_id));
|
||||
Self { network, peer_map }
|
||||
Self {
|
||||
network,
|
||||
peer_map,
|
||||
relay_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,9 +201,30 @@ impl ForeignNetworkManager {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_network_in_whitelist(&self, network_name: &str) -> Result<(), Error> {
|
||||
if self
|
||||
.global_ctx
|
||||
.get_flags()
|
||||
.foreign_network_whitelist
|
||||
.split(" ")
|
||||
.map(wildmatch::WildMatch::new)
|
||||
.any(|wl| wl.matches(network_name))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("network {} not in whitelist", network_name).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> {
|
||||
tracing::info!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager");
|
||||
|
||||
let relay_peer_rpc = self.global_ctx.get_flags().relay_all_peer_rpc;
|
||||
let ret = self.check_network_in_whitelist(&peer_conn.get_network_identity().network_name);
|
||||
if ret.is_err() && !relay_peer_rpc {
|
||||
return ret;
|
||||
}
|
||||
|
||||
let entry = self
|
||||
.data
|
||||
.network_peer_maps
|
||||
@@ -208,6 +235,7 @@ impl ForeignNetworkManager {
|
||||
self.packet_sender.clone(),
|
||||
self.global_ctx.clone(),
|
||||
self.my_peer_id,
|
||||
!ret.is_err(),
|
||||
))
|
||||
})
|
||||
.clone();
|
||||
@@ -232,8 +260,16 @@ impl ForeignNetworkManager {
|
||||
async fn start_global_event_handler(&self) {
|
||||
let data = self.data.clone();
|
||||
let mut s = self.global_ctx.subscribe();
|
||||
let (ev_tx, mut ev_rx) = unbounded_channel();
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
while let Ok(e) = s.recv().await {
|
||||
ev_tx.send(e).unwrap();
|
||||
}
|
||||
panic!("global event handler at foreign network manager exit");
|
||||
});
|
||||
|
||||
self.tasks.lock().await.spawn(async move {
|
||||
while let Some(e) = ev_rx.recv().await {
|
||||
if let GlobalCtxEvent::PeerRemoved(peer_id) = &e {
|
||||
tracing::info!(?e, "remove peer from foreign network manager");
|
||||
data.remove_peer(*peer_id);
|
||||
@@ -280,6 +316,10 @@ impl ForeignNetworkManager {
|
||||
}
|
||||
|
||||
if let Some(entry) = data.get_network_entry(&from_network) {
|
||||
if !entry.relay_data && hdr.packet_type == PacketType::Data as u8 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ret = entry
|
||||
.peer_map
|
||||
.send_msg(packet_bytes, to_peer_id, NextHopPolicy::LeastHop)
|
||||
@@ -424,6 +464,31 @@ mod tests {
|
||||
foreign_network_whitelist_helper("net2abc".to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn only_relay_peer_rpc() {
|
||||
let pm_center = create_mock_peer_manager_with_mock_stun(crate::rpc::NatType::Unknown).await;
|
||||
let mut flag = pm_center.get_global_ctx().get_flags();
|
||||
flag.foreign_network_whitelist = "".to_string();
|
||||
flag.relay_all_peer_rpc = true;
|
||||
pm_center.get_global_ctx().config.set_flags(flag);
|
||||
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]
|
||||
#[should_panic]
|
||||
async fn foreign_network_whitelist_fail() {
|
||||
|
||||
@@ -61,6 +61,7 @@ pub struct PeerConn {
|
||||
tasks: JoinSet<Result<(), TunnelError>>,
|
||||
|
||||
info: Option<HandshakeRequest>,
|
||||
is_client: Option<bool>,
|
||||
|
||||
close_event_sender: Option<mpsc::Sender<PeerConnId>>,
|
||||
|
||||
@@ -107,6 +108,7 @@ impl PeerConn {
|
||||
tasks: JoinSet::new(),
|
||||
|
||||
info: None,
|
||||
is_client: None,
|
||||
close_event_sender: None,
|
||||
|
||||
ctrl_resp_sender: ctrl_sender,
|
||||
@@ -215,6 +217,7 @@ impl PeerConn {
|
||||
let rsp = self.wait_handshake_loop().await?;
|
||||
tracing::info!("handshake request: {:?}", rsp);
|
||||
self.info = Some(rsp);
|
||||
self.is_client = Some(false);
|
||||
self.send_handshake().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -226,6 +229,7 @@ impl PeerConn {
|
||||
let rsp = self.wait_handshake_loop().await?;
|
||||
tracing::info!("handshake response: {:?}", rsp);
|
||||
self.info = Some(rsp);
|
||||
self.is_client = Some(true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -359,14 +363,17 @@ impl PeerConn {
|
||||
}
|
||||
|
||||
pub fn get_conn_info(&self) -> PeerConnInfo {
|
||||
let info = self.info.as_ref().unwrap();
|
||||
PeerConnInfo {
|
||||
conn_id: self.conn_id.to_string(),
|
||||
my_peer_id: self.my_peer_id,
|
||||
peer_id: self.get_peer_id(),
|
||||
features: self.info.as_ref().unwrap().features.clone(),
|
||||
features: info.features.clone(),
|
||||
tunnel: self.tunnel_info.clone(),
|
||||
stats: Some(self.get_stats()),
|
||||
loss_rate: (f64::from(self.loss_rate_stats.load(Ordering::Relaxed)) / 100.0) as f32,
|
||||
is_client: self.is_client.unwrap_or_default(),
|
||||
network_name: info.network_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::bytes::Bytes;
|
||||
|
||||
use crate::{
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, PeerId},
|
||||
common::{error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait, PeerId},
|
||||
peers::{
|
||||
peer_conn::PeerConn,
|
||||
peer_rpc::PeerRpcManagerTransport,
|
||||
@@ -309,21 +309,6 @@ impl PeerManager {
|
||||
self.add_client_tunnel(t).await
|
||||
}
|
||||
|
||||
fn check_network_in_whitelist(&self, network_name: &str) -> Result<(), Error> {
|
||||
if self
|
||||
.global_ctx
|
||||
.get_flags()
|
||||
.foreign_network_whitelist
|
||||
.split(" ")
|
||||
.map(wildmatch::WildMatch::new)
|
||||
.any(|wl| wl.matches(network_name))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("network {} not in whitelist", network_name).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn add_tunnel_as_server(&self, tunnel: Box<dyn Tunnel>) -> Result<(), Error> {
|
||||
tracing::info!("add tunnel as server start");
|
||||
@@ -334,7 +319,6 @@ impl PeerManager {
|
||||
{
|
||||
self.add_new_peer_conn(peer).await?;
|
||||
} else {
|
||||
self.check_network_in_whitelist(&peer.get_network_identity().network_name)?;
|
||||
self.foreign_network_manager.add_peer_conn(peer).await?;
|
||||
}
|
||||
tracing::info!("add tunnel as server done");
|
||||
@@ -762,6 +746,33 @@ impl PeerManager {
|
||||
pub fn get_foreign_network_client(&self) -> Arc<ForeignNetworkClient> {
|
||||
self.foreign_network_client.clone()
|
||||
}
|
||||
|
||||
pub fn get_my_info(&self) -> crate::rpc::NodeInfo {
|
||||
crate::rpc::NodeInfo {
|
||||
peer_id: self.my_peer_id,
|
||||
ipv4_addr: self
|
||||
.global_ctx
|
||||
.get_ipv4()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_default(),
|
||||
proxy_cidrs: self
|
||||
.global_ctx
|
||||
.get_proxy_cidrs()
|
||||
.into_iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
hostname: self.global_ctx.get_hostname(),
|
||||
stun_info: Some(self.global_ctx.get_stun_info_collector().get_stun_info()),
|
||||
inst_id: self.global_ctx.get_id().to_string(),
|
||||
listeners: self
|
||||
.global_ctx
|
||||
.get_running_listeners()
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
config: self.global_ctx.config.dump(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use crate::rpc::{
|
||||
cli::PeerInfo, peer_manage_rpc_server::PeerManageRpc, DumpRouteRequest, DumpRouteResponse,
|
||||
ListForeignNetworkRequest, ListForeignNetworkResponse, ListPeerRequest, ListPeerResponse,
|
||||
ListRouteRequest, ListRouteResponse,
|
||||
ListRouteRequest, ListRouteResponse, ShowNodeInfoRequest, ShowNodeInfoResponse,
|
||||
};
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
@@ -81,4 +81,13 @@ impl PeerManageRpc for PeerManagerRpcService {
|
||||
.await;
|
||||
Ok(Response::new(reply))
|
||||
}
|
||||
|
||||
async fn show_node_info(
|
||||
&self,
|
||||
_request: Request<ShowNodeInfoRequest>, // Accept request of type HelloRequest
|
||||
) -> Result<Response<ShowNodeInfoResponse>, Status> {
|
||||
Ok(Response::new(ShowNodeInfoResponse {
|
||||
node_info: Some(self.peer_manager.get_my_info()),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn get_inst_config(inst_name: &str, ns: Option<&str>, ipv4: &str) -> TomlCon
|
||||
"ws://0.0.0.0:11011".parse().unwrap(),
|
||||
"wss://0.0.0.0:11012".parse().unwrap(),
|
||||
]);
|
||||
config.set_socks5_portal(Some("socks5://0.0.0.0:12345".parse().unwrap()));
|
||||
config
|
||||
}
|
||||
|
||||
@@ -630,3 +631,122 @@ pub async fn wireguard_vpn_portal() {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "wireguard")]
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn socks5_vpn_portal(#[values("10.144.144.1", "10.144.144.3")] dst_addr: &str) {
|
||||
use rand::Rng as _;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{TcpListener, TcpStream},
|
||||
};
|
||||
use tokio_socks::tcp::socks5::Socks5Stream;
|
||||
|
||||
let _insts = init_three_node("tcp").await;
|
||||
|
||||
let mut buf = vec![0u8; 1024];
|
||||
rand::thread_rng().fill(&mut buf[..]);
|
||||
|
||||
let buf_clone = buf.clone();
|
||||
let dst_addr_clone = dst_addr.to_owned();
|
||||
let task = tokio::spawn(async move {
|
||||
let net_ns = if dst_addr_clone == "10.144.144.1" {
|
||||
NetNS::new(Some("net_a".into()))
|
||||
} else {
|
||||
NetNS::new(Some("net_c".into()))
|
||||
};
|
||||
let _g = net_ns.guard();
|
||||
|
||||
let socket = TcpListener::bind("0.0.0.0:22222").await.unwrap();
|
||||
let (mut st, addr) = socket.accept().await.unwrap();
|
||||
|
||||
if dst_addr_clone == "10.144.144.3" {
|
||||
assert_eq!(addr.ip().to_string(), "10.144.144.1".to_string());
|
||||
} else {
|
||||
assert_eq!(addr.ip().to_string(), "127.0.0.1".to_string());
|
||||
}
|
||||
|
||||
let rbuf = &mut [0u8; 1024];
|
||||
st.read_exact(rbuf).await.unwrap();
|
||||
assert_eq!(rbuf, buf_clone.as_slice());
|
||||
});
|
||||
|
||||
let net_ns = NetNS::new(Some("net_a".into()));
|
||||
let _g = net_ns.guard();
|
||||
|
||||
println!("connect to socks5 portal");
|
||||
let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap();
|
||||
println!("connect to socks5 portal done");
|
||||
|
||||
stream.set_nodelay(true).unwrap();
|
||||
let mut conn = Socks5Stream::connect_with_socket(stream, format!("{}:22222", dst_addr))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
conn.write_all(&buf).await.unwrap();
|
||||
drop(conn);
|
||||
|
||||
tokio::join!(task).0.unwrap();
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) {
|
||||
prepare_linux_namespaces();
|
||||
|
||||
let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1");
|
||||
if is_foreign {
|
||||
center_node_config
|
||||
.set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string()));
|
||||
}
|
||||
let mut center_inst = Instance::new(center_node_config);
|
||||
|
||||
let mut inst1 = Instance::new(get_inst_config("inst1", Some("net_b"), "10.144.145.1"));
|
||||
let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_c"), "10.144.145.2"));
|
||||
|
||||
center_inst.run().await.unwrap();
|
||||
inst1.run().await.unwrap();
|
||||
inst2.run().await.unwrap();
|
||||
|
||||
assert_ne!(inst1.id(), center_inst.id());
|
||||
assert_ne!(inst2.id(), center_inst.id());
|
||||
|
||||
inst1
|
||||
.get_conn_manager()
|
||||
.add_connector(RingTunnelConnector::new(
|
||||
format!("ring://{}", center_inst.id()).parse().unwrap(),
|
||||
));
|
||||
|
||||
inst2
|
||||
.get_conn_manager()
|
||||
.add_connector(RingTunnelConnector::new(
|
||||
format!("ring://{}", center_inst.id()).parse().unwrap(),
|
||||
));
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
|
||||
let peer_map = if !is_foreign {
|
||||
inst1.get_peer_manager().get_peer_map()
|
||||
} else {
|
||||
inst1
|
||||
.get_peer_manager()
|
||||
.get_foreign_network_client()
|
||||
.get_peer_map()
|
||||
};
|
||||
|
||||
let conns = peer_map
|
||||
.list_peer_conns(center_inst.peer_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, conns.len());
|
||||
|
||||
wait_for_condition(
|
||||
|| async { ping_test("net_b", "10.144.145.2", None).await },
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ impl FromUrl for SocketAddr {
|
||||
fn from_url(url: url::Url, ip_version: IpVersion) -> Result<Self, TunnelError> {
|
||||
let addrs = url.socket_addrs(|| None)?;
|
||||
tracing::debug!(?addrs, ?ip_version, ?url, "convert url to socket addrs");
|
||||
let mut addrs = addrs
|
||||
let addrs = addrs
|
||||
.into_iter()
|
||||
.filter(|addr| match ip_version {
|
||||
IpVersion::V4 => addr.is_ipv4(),
|
||||
@@ -213,7 +213,13 @@ impl FromUrl for SocketAddr {
|
||||
IpVersion::Both => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
addrs.pop().ok_or(TunnelError::NoDnsRecordFound(ip_version))
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
// randomly select one address
|
||||
addrs
|
||||
.choose(&mut rand::thread_rng())
|
||||
.copied()
|
||||
.ok_or(TunnelError::NoDnsRecordFound(ip_version))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -114,10 +114,18 @@ pub fn list_peer_route_pair(peers: Vec<PeerInfo>, routes: Vec<Route>) -> Vec<Pee
|
||||
|
||||
for route in routes.iter() {
|
||||
let peer = peers.iter().find(|peer| peer.peer_id == route.peer_id);
|
||||
pairs.push(PeerRoutePair {
|
||||
let has_tunnel = peer.map(|p| !p.conns.is_empty()).unwrap_or(false);
|
||||
let mut pair = PeerRoutePair {
|
||||
route: route.clone(),
|
||||
peer: peer.cloned(),
|
||||
});
|
||||
};
|
||||
|
||||
// it is relayed by public server, adjust the cost
|
||||
if !has_tunnel && pair.route.cost == 1 {
|
||||
pair.route.cost = 2;
|
||||
}
|
||||
|
||||
pairs.push(pair);
|
||||
}
|
||||
|
||||
pairs
|
||||
@@ -239,6 +247,18 @@ pub fn utf8_or_gbk_to_string(s: &[u8]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_panic_handler() {
|
||||
use std::backtrace;
|
||||
use std::io::Write;
|
||||
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);
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::common::config::{self};
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script copy from alist , Thank for it!
|
||||
|
||||
SKIP_FOLDER_VERIFY=false
|
||||
SKIP_FOLDER_FIX=false
|
||||
|
||||
COMMEND=$1
|
||||
shift
|
||||
|
||||
# Check path
|
||||
if [[ "$#" -ge 1 && ! "$1" == --* ]]; then
|
||||
INSTALL_PATH=$1
|
||||
shift
|
||||
fi
|
||||
|
||||
# Check other option
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-folder-verify) SKIP_FOLDER_VERIFY=true ;;
|
||||
--skip-folder-fix) SKIP_FOLDER_FIX=true ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ -z "$INSTALL_PATH" ]; then
|
||||
INSTALL_PATH='/opt/easytier'
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_PATH" == */ ]]; then
|
||||
INSTALL_PATH=${INSTALL_PATH%?}
|
||||
fi
|
||||
|
||||
if ! $SKIP_FOLDER_FIX && ! [[ "$INSTALL_PATH" == */easytier ]]; then
|
||||
INSTALL_PATH="$INSTALL_PATH/easytier"
|
||||
fi
|
||||
|
||||
echo INSTALL PATH : $INSTALL_PATH
|
||||
echo SKIP FOLDER FIX : $SKIP_FOLDER_FIX
|
||||
echo SKIP FOLDER VERIFY : $SKIP_FOLDER_VERIFY
|
||||
|
||||
RED_COLOR='\e[1;31m'
|
||||
GREEN_COLOR='\e[1;32m'
|
||||
YELLOW_COLOR='\e[1;33m'
|
||||
BLUE_COLOR='\e[1;34m'
|
||||
PINK_COLOR='\e[1;35m'
|
||||
SHAN='\e[1;33;5m'
|
||||
RES='\e[0m'
|
||||
# clear
|
||||
|
||||
# check if unzip is installed
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo -e "\r\n${RED_COLOR}Error: unzip is not installed${RES}\r\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\r\n${RED_COLOR}----------------------NOTICE----------------------${RES}\r\n"
|
||||
echo " This is a temporary script to install EasyTier "
|
||||
echo " EasyTier requires a dedicated empty folder to install"
|
||||
echo " EasyTier is a developing product and may have some issues "
|
||||
echo " Using EasyTier requires some basic skills "
|
||||
echo " You need to face the risks brought by using EasyTier at your own risk "
|
||||
echo -e "\r\n${RED_COLOR}-------------------------------------------------${RES}\r\n"
|
||||
|
||||
# Get platform
|
||||
if command -v arch >/dev/null 2>&1; then
|
||||
platform=$(arch)
|
||||
else
|
||||
platform=$(uname -m)
|
||||
fi
|
||||
|
||||
case "$platform" in
|
||||
amd64 | x86_64)
|
||||
ARCH="x86_64"
|
||||
;;
|
||||
arm64 | aarch64 | *armv8*)
|
||||
ARCH="aarch64"
|
||||
;;
|
||||
*armv7*)
|
||||
ARCH="armv7"
|
||||
;;
|
||||
*arm*)
|
||||
ARCH="arm"
|
||||
;;
|
||||
mips)
|
||||
ARCH="mips"
|
||||
;;
|
||||
mipsel)
|
||||
ARCH="mipsel"
|
||||
;;
|
||||
*)
|
||||
ARCH="UNKNOWN"
|
||||
;;
|
||||
esac
|
||||
|
||||
# support hf
|
||||
if [[ "$ARCH" == "armv7" || "$ARCH" == "arm" ]]; then
|
||||
if cat /proc/cpuinfo | grep Features | grep -i 'half' >/dev/null 2>&1; then
|
||||
ARCH=${ARCH}hf
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "\r\n${GREEN_COLOR}Your platform: ${ARCH} (${platform}) ${RES}\r\n" 1>&2
|
||||
|
||||
GH_PROXY='https://mirror.ghproxy.com/'
|
||||
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
echo -e "\r\n${RED_COLOR}This script requires run as Root !${RES}\r\n" 1>&2
|
||||
exit 1
|
||||
elif [ "$ARCH" == "UNKNOWN" ]; then
|
||||
echo -e "\r\n${RED_COLOR}Opus${RES}, this script do not support your platfrom\r\nTry ${GREEN_COLOR}install by band${RES}\r\n"
|
||||
exit 1
|
||||
elif ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo -e "\r\n${RED_COLOR}Opus${RES}, your Linux do not support systemctl\r\nnTry ${GREEN_COLOR}install by band${RES}\r\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHECK() {
|
||||
if ! $SKIP_FOLDER_VERIFY; then
|
||||
if [ -f "$INSTALL_PATH/easytier-core" ]; then
|
||||
echo "There is EasyTier in $INSTALL_PATH. Please choose other path or use \"update\""
|
||||
echo -e "Or use Try ${GREEN_COLOR}--skip-folder-verify${RES} to skip"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ ! -d "$INSTALL_PATH/" ]; then
|
||||
mkdir -p $INSTALL_PATH
|
||||
else
|
||||
# Check weather path is empty
|
||||
if ! $SKIP_FOLDER_VERIFY; then
|
||||
if [ -n "$(ls -A $INSTALL_PATH)" ]; then
|
||||
echo "EasyTier requires to be installed in an empty directory. Please choose a empty path"
|
||||
echo -e "Or use Try ${GREEN_COLOR}--skip-folder-verify${RES} to skip"
|
||||
echo -e "Current path: $INSTALL_PATH ( use ${GREEN_COLOR}--skip-folder-fix${RES} to disable folder fix )"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
INSTALL() {
|
||||
# Get version number
|
||||
RESPONSE=$(curl -s "https://api.github.com/repos/EasyTier/EasyTier/releases/latest")
|
||||
LATEST_VERSION=$(echo "$RESPONSE" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
LATEST_VERSION=$(echo -e "$LATEST_VERSION" | tr -d '[:space:]')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo -e "\r\n${RED_COLOR}Opus${RES}, failure to get latest version. Check your internel\r\nOr try ${GREEN_COLOR}install by band${RES}\r\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download
|
||||
echo -e "\r\n${GREEN_COLOR}Downloading EasyTier $LATEST_VERSION ...${RES}"
|
||||
rm -rf /tmp/easytier_tmp_install.zip
|
||||
curl -L ${GH_PROXY}https://github.com/EasyTier/EasyTier/releases/latest/download/easytier-linux-${ARCH}-${LATEST_VERSION}.zip -o /tmp/easytier_tmp_install.zip $CURL_BAR
|
||||
# Unzip resource
|
||||
echo -e "\r\n${GREEN_COLOR}Unzip resource ...${RES}"
|
||||
unzip -o /tmp/easytier_tmp_install.zip -d $INSTALL_PATH/
|
||||
mkdir $INSTALL_PATH/config
|
||||
mv $INSTALL_PATH/easytier-linux-${ARCH}/* $INSTALL_PATH/
|
||||
rm -rf $INSTALL_PATH/easytier-linux-${ARCH}/
|
||||
chmod +x $INSTALL_PATH/easytier-core $INSTALL_PATH/easytier-cli
|
||||
if [ -f $INSTALL_PATH/easytier-core ] || [ -f $INSTALL_PATH/easytier-cli ]; then
|
||||
echo -e "${GREEN_COLOR} Download successfully! ${RES}"
|
||||
else
|
||||
echo -e "${RED_COLOR} Download failed! ${RES}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
INIT() {
|
||||
if [ ! -f "$INSTALL_PATH/easytier-core" ]; then
|
||||
echo -e "\r\n${RED_COLOR}Opus${RES}, unable to find EasyTier\r\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create default blank file config
|
||||
cat >$INSTALL_PATH/config/default.conf <<EOF
|
||||
instance_name = "default"
|
||||
dhcp = true
|
||||
listeners = [
|
||||
"tcp://0.0.0.0:11010",
|
||||
"udp://0.0.0.0:11010",
|
||||
"wg://0.0.0.0:11011",
|
||||
"ws://0.0.0.0:11011/",
|
||||
"wss://0.0.0.0:11012/",
|
||||
]
|
||||
exit_nodes = []
|
||||
peer = []
|
||||
rpc_portal = "127.0.0.1:15888"
|
||||
|
||||
[network_identity]
|
||||
network_name = "default"
|
||||
network_secret = ""
|
||||
|
||||
[flags]
|
||||
default_protocol = "udp"
|
||||
dev_name = ""
|
||||
enable_encryption = true
|
||||
enable_ipv6 = true
|
||||
mtu = 1380
|
||||
latency_first = false
|
||||
enable_exit_node = false
|
||||
no_tun = false
|
||||
use_smoltcp = false
|
||||
foreign_network_whitelist = "*"
|
||||
disable_p2p = false
|
||||
relay_all_peer_rpc = false
|
||||
EOF
|
||||
|
||||
# Create systemd
|
||||
cat >/etc/systemd/system/easytier@.service <<EOF
|
||||
[Unit]
|
||||
Description=EasyTier Service
|
||||
Wants=network.target
|
||||
After=network.target network.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$INSTALL_PATH
|
||||
ExecStart=$INSTALL_PATH/easytier-core -c $INSTALL_PATH/config/%i.conf
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# # Create run script
|
||||
# cat >$INSTALL_PATH/run.sh <<EOF
|
||||
# $INSTALL_PATH/easytier-core
|
||||
# EOF
|
||||
|
||||
# Startup
|
||||
systemctl daemon-reload
|
||||
systemctl enable easytier@default >/dev/null 2>&1
|
||||
systemctl start easytier@default
|
||||
|
||||
# For issues from the previous version
|
||||
rm -rf /etc/systemd/system/easytier.service
|
||||
rm -rf /usr/bin/easytier-core
|
||||
rm -rf /usr/bin/easytier-cli
|
||||
|
||||
# Add link
|
||||
ln -s $INSTALL_PATH/easytier-core /usr/sbin/easytier-core
|
||||
ln -s $INSTALL_PATH/easytier-cli /usr/sbin/easytier-cli
|
||||
}
|
||||
|
||||
SUCCESS() {
|
||||
clear
|
||||
echo " Install EasyTier successfully!"
|
||||
echo -e "\r\nDefault Port: ${GREEN_COLOR}11010(UDP+TCP)${RES}, Notice allowing in firewall!\r\n"
|
||||
echo -e "Default Network Name: ${GREEN_COLOR}default${RES}, Please change it to your own network name!\r\n"
|
||||
|
||||
echo -e "Now EasyTier supports multiple config files. You can create config files in the ${GREEN_COLOR}${INSTALL_PATH}/config/${RES} folder"
|
||||
echo -e "For more information, please check the documents in offical site"
|
||||
echo -e "The management example of a single configuration file is as follows"
|
||||
|
||||
echo
|
||||
echo -e "Status: ${GREEN_COLOR}systemctl status easytier@default${RES}"
|
||||
echo -e "Start: ${GREEN_COLOR}systemctl start easytier@default${RES}"
|
||||
echo -e "Restart: ${GREEN_COLOR}systemctl restart easytier@default${RES}"
|
||||
echo -e "Stop: ${GREEN_COLOR}systemctl stop easytier@default${RES}"
|
||||
echo
|
||||
}
|
||||
|
||||
UNINSTALL() {
|
||||
echo -e "\r\n${GREEN_COLOR}Uninstall EasyTier ...${RES}\r\n"
|
||||
echo -e "${GREEN_COLOR}Stop process ...${RES}"
|
||||
systemctl disable "easytier@*" >/dev/null 2>&1
|
||||
systemctl stop "easytier@*" >/dev/null 2>&1
|
||||
echo -e "${GREEN_COLOR}Delete files ...${RES}"
|
||||
rm -rf $INSTALL_PATH /etc/systemd/system/easytier.service /usr/bin/easytier-core /usr/bin/easytier-cli /etc/systemd/system/easytier@.service /usr/sbin/easytier-cli /usr/sbin/easytier-cli
|
||||
systemctl daemon-reload
|
||||
echo -e "\r\n${GREEN_COLOR}EasyTier was removed successfully! ${RES}\r\n"
|
||||
}
|
||||
|
||||
UPDATE() {
|
||||
if [ ! -f "$INSTALL_PATH/easytier-core" ]; then
|
||||
echo -e "\r\n${RED_COLOR}Opus${RES}, unable to find EasyTier\r\n"
|
||||
exit 1
|
||||
else
|
||||
echo
|
||||
echo -e "${GREEN_COLOR}Stopping EasyTier process${RES}\r\n"
|
||||
systemctl stop "easytier@*"
|
||||
# Backup
|
||||
rm -rf /tmp/easytier_tmp_update
|
||||
mkdir -p /tmp/easytier_tmp_update
|
||||
cp -a $INSTALL_PATH/* /tmp/easytier_tmp_update/
|
||||
INSTALL
|
||||
if [ -f $INSTALL_PATH/easytier-core ]; then
|
||||
echo -e "${GREEN_COLOR} Vrify successfully ${RES}"
|
||||
else
|
||||
echo -e "${RED_COLOR} Download failed, unable to update${RES}"
|
||||
echo "Rollback all ..."
|
||||
rm -rf $INSTALL_PATH/*
|
||||
mv /tmp/easytier_tmp_update/* $INSTALL_PATH/
|
||||
systemctl start "easytier@*"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "\r\n${GREEN_COLOR} Starting EasyTier process${RES}"
|
||||
systemctl start "easytier@*"
|
||||
echo -e "\r\n${GREEN_COLOR} EasyTier was the latest stable version! ${RES}\r\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# CURL progress
|
||||
if curl --help | grep progress-bar >/dev/null 2>&1; then # $CURL_BAR
|
||||
CURL_BAR="--progress-bar"
|
||||
fi
|
||||
|
||||
# The temp directory must exist
|
||||
if [ ! -d "/tmp" ]; then
|
||||
mkdir -p /tmp
|
||||
fi
|
||||
|
||||
echo $COMMEND
|
||||
|
||||
if [ "$COMMEND" = "uninstall" ]; then
|
||||
UNINSTALL
|
||||
elif [ "$COMMEND" = "update" ]; then
|
||||
UPDATE
|
||||
elif [ "$COMMEND" = "install" ]; then
|
||||
CHECK
|
||||
INSTALL
|
||||
INIT
|
||||
if [ -f "$INSTALL_PATH/easytier-core" ]; then
|
||||
SUCCESS
|
||||
else
|
||||
echo -e "${RED_COLOR} Install fail, try install by hand${RES}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED_COLOR} Error Commend ${RES}\n\r"
|
||||
echo " ALLOW:"
|
||||
echo -e "\n\r${GREEN_COLOR} install, uninstall, update ${RES}"
|
||||
fi
|
||||
|
||||
rm -rf /tmp/easytier_tmp_*
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "tauri-plugin-vpnservice"
|
||||
version = "0.0.0"
|
||||
authors = [ "You" ]
|
||||
authors = ["You"]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
@@ -9,9 +9,9 @@ exclude = ["/examples", "/webview-dist", "/webview-src", "/node_modules"]
|
||||
links = "tauri-plugin-vpnservice"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0-beta.23" }
|
||||
tauri = { version = "2.0.0-rc" }
|
||||
serde = "1.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.0-beta.18", features = ["build"] }
|
||||
tauri-plugin = { version = "2.0.0-rc", features = ["build"] }
|
||||
|
||||
@@ -88,9 +88,9 @@ class TauriVpnService : VpnService() {
|
||||
builder.addDnsServer(dns)
|
||||
|
||||
for (route in routes) {
|
||||
val ipParts = ipv4Addr.split("/")
|
||||
val ipParts = route.split("/")
|
||||
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
|
||||
builder.addAddress(ipParts[0], ipParts[1].toInt())
|
||||
builder.addRoute(ipParts[0], ipParts[1].toInt())
|
||||
}
|
||||
|
||||
for (app in disallowedApplications) {
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
"pretest": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": ">=2.0.0-beta.6"
|
||||
"@tauri-apps/api": "2.0.0-rc.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"rollup": "^4.9.6",
|
||||
"typescript": "^5.3.3",
|
||||
"tslib": "^2.6.2"
|
||||
"rollup": "^4.20.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+91
-91
@@ -9,21 +9,21 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: '>=2.0.0-beta.6'
|
||||
version: 2.0.0-beta.14
|
||||
specifier: 2.0.0-rc.0
|
||||
version: 2.0.0-rc.0
|
||||
devDependencies:
|
||||
'@rollup/plugin-typescript':
|
||||
specifier: ^11.1.6
|
||||
version: 11.1.6(rollup@4.18.1)(tslib@2.6.3)(typescript@5.5.3)
|
||||
version: 11.1.6(rollup@4.20.0)(tslib@2.6.3)(typescript@5.5.4)
|
||||
rollup:
|
||||
specifier: ^4.9.6
|
||||
version: 4.18.1
|
||||
specifier: ^4.20.0
|
||||
version: 4.20.0
|
||||
tslib:
|
||||
specifier: ^2.6.2
|
||||
specifier: ^2.6.3
|
||||
version: 2.6.3
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.5.3
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
|
||||
packages:
|
||||
|
||||
@@ -49,88 +49,88 @@ packages:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.18.1':
|
||||
resolution: {integrity: sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==}
|
||||
'@rollup/rollup-android-arm-eabi@4.20.0':
|
||||
resolution: {integrity: sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.18.1':
|
||||
resolution: {integrity: sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==}
|
||||
'@rollup/rollup-android-arm64@4.20.0':
|
||||
resolution: {integrity: sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.18.1':
|
||||
resolution: {integrity: sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==}
|
||||
'@rollup/rollup-darwin-arm64@4.20.0':
|
||||
resolution: {integrity: sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.18.1':
|
||||
resolution: {integrity: sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==}
|
||||
'@rollup/rollup-darwin-x64@4.20.0':
|
||||
resolution: {integrity: sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.18.1':
|
||||
resolution: {integrity: sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==}
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.20.0':
|
||||
resolution: {integrity: sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.18.1':
|
||||
resolution: {integrity: sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==}
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.20.0':
|
||||
resolution: {integrity: sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.18.1':
|
||||
resolution: {integrity: sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==}
|
||||
'@rollup/rollup-linux-arm64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.18.1':
|
||||
resolution: {integrity: sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==}
|
||||
'@rollup/rollup-linux-arm64-musl@4.20.0':
|
||||
resolution: {integrity: sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.18.1':
|
||||
resolution: {integrity: sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==}
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.18.1':
|
||||
resolution: {integrity: sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==}
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.18.1':
|
||||
resolution: {integrity: sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==}
|
||||
'@rollup/rollup-linux-s390x-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.18.1':
|
||||
resolution: {integrity: sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==}
|
||||
'@rollup/rollup-linux-x64-gnu@4.20.0':
|
||||
resolution: {integrity: sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.18.1':
|
||||
resolution: {integrity: sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==}
|
||||
'@rollup/rollup-linux-x64-musl@4.20.0':
|
||||
resolution: {integrity: sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.18.1':
|
||||
resolution: {integrity: sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==}
|
||||
'@rollup/rollup-win32-arm64-msvc@4.20.0':
|
||||
resolution: {integrity: sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.18.1':
|
||||
resolution: {integrity: sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==}
|
||||
'@rollup/rollup-win32-ia32-msvc@4.20.0':
|
||||
resolution: {integrity: sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.18.1':
|
||||
resolution: {integrity: sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==}
|
||||
'@rollup/rollup-win32-x64-msvc@4.20.0':
|
||||
resolution: {integrity: sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tauri-apps/api@2.0.0-beta.14':
|
||||
resolution: {integrity: sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg==}
|
||||
'@tauri-apps/api@2.0.0-rc.0':
|
||||
resolution: {integrity: sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==}
|
||||
engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
|
||||
|
||||
'@types/estree@1.0.5':
|
||||
@@ -151,8 +151,8 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-core-module@2.14.0:
|
||||
resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==}
|
||||
is-core-module@2.15.0:
|
||||
resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
path-parse@1.0.7:
|
||||
@@ -166,8 +166,8 @@ packages:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
|
||||
rollup@4.18.1:
|
||||
resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==}
|
||||
rollup@4.20.0:
|
||||
resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -178,79 +178,79 @@ packages:
|
||||
tslib@2.6.3:
|
||||
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||
|
||||
typescript@5.5.3:
|
||||
resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==}
|
||||
typescript@5.5.4:
|
||||
resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@rollup/plugin-typescript@11.1.6(rollup@4.18.1)(tslib@2.6.3)(typescript@5.5.3)':
|
||||
'@rollup/plugin-typescript@11.1.6(rollup@4.20.0)(tslib@2.6.3)(typescript@5.5.4)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.18.1)
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.20.0)
|
||||
resolve: 1.22.8
|
||||
typescript: 5.5.3
|
||||
typescript: 5.5.4
|
||||
optionalDependencies:
|
||||
rollup: 4.18.1
|
||||
rollup: 4.20.0
|
||||
tslib: 2.6.3
|
||||
|
||||
'@rollup/pluginutils@5.1.0(rollup@4.18.1)':
|
||||
'@rollup/pluginutils@5.1.0(rollup@4.20.0)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
optionalDependencies:
|
||||
rollup: 4.18.1
|
||||
rollup: 4.20.0
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.18.1':
|
||||
'@rollup/rollup-android-arm-eabi@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.18.1':
|
||||
'@rollup/rollup-android-arm64@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.18.1':
|
||||
'@rollup/rollup-darwin-arm64@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.18.1':
|
||||
'@rollup/rollup-darwin-x64@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.18.1':
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.18.1':
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.18.1':
|
||||
'@rollup/rollup-linux-arm64-gnu@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.18.1':
|
||||
'@rollup/rollup-linux-arm64-musl@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.18.1':
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.18.1':
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.18.1':
|
||||
'@rollup/rollup-linux-s390x-gnu@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.18.1':
|
||||
'@rollup/rollup-linux-x64-gnu@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.18.1':
|
||||
'@rollup/rollup-linux-x64-musl@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.18.1':
|
||||
'@rollup/rollup-win32-arm64-msvc@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.18.1':
|
||||
'@rollup/rollup-win32-ia32-msvc@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.18.1':
|
||||
'@rollup/rollup-win32-x64-msvc@4.20.0':
|
||||
optional: true
|
||||
|
||||
'@tauri-apps/api@2.0.0-beta.14': {}
|
||||
'@tauri-apps/api@2.0.0-rc.0': {}
|
||||
|
||||
'@types/estree@1.0.5': {}
|
||||
|
||||
@@ -265,7 +265,7 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
is-core-module@2.14.0:
|
||||
is-core-module@2.15.0:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
||||
@@ -275,34 +275,34 @@ snapshots:
|
||||
|
||||
resolve@1.22.8:
|
||||
dependencies:
|
||||
is-core-module: 2.14.0
|
||||
is-core-module: 2.15.0
|
||||
path-parse: 1.0.7
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
|
||||
rollup@4.18.1:
|
||||
rollup@4.20.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.18.1
|
||||
'@rollup/rollup-android-arm64': 4.18.1
|
||||
'@rollup/rollup-darwin-arm64': 4.18.1
|
||||
'@rollup/rollup-darwin-x64': 4.18.1
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.18.1
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.18.1
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.18.1
|
||||
'@rollup/rollup-linux-arm64-musl': 4.18.1
|
||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.18.1
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.18.1
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.18.1
|
||||
'@rollup/rollup-linux-x64-gnu': 4.18.1
|
||||
'@rollup/rollup-linux-x64-musl': 4.18.1
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.18.1
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.18.1
|
||||
'@rollup/rollup-win32-x64-msvc': 4.18.1
|
||||
'@rollup/rollup-android-arm-eabi': 4.20.0
|
||||
'@rollup/rollup-android-arm64': 4.20.0
|
||||
'@rollup/rollup-darwin-arm64': 4.20.0
|
||||
'@rollup/rollup-darwin-x64': 4.20.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.20.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.20.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.20.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.20.0
|
||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.20.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.20.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.20.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.20.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.20.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.20.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.20.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.20.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
tslib@2.6.3: {}
|
||||
|
||||
typescript@5.5.3: {}
|
||||
typescript@5.5.4: {}
|
||||
|
||||
Reference in New Issue
Block a user