mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-17 11:25:35 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0d2045e52 | |||
| 835cd407bf | |||
| f5ba5bb146 | |||
| 7a694257d9 | |||
| 67abf4446d | |||
| 7035a3fef4 | |||
| 4445916ba7 | |||
| a102a8bfc7 | |||
| c9e8c35e77 | |||
| 1a1be8138a | |||
| e06e8a9e8a | |||
| 56fd6e4ab6 | |||
| 215db09925 | |||
| 9fff5e4fec | |||
| 802d3f78d7 | |||
| 3593035eb9 | |||
| 757d76c9da | |||
| 445e68ddd1 | |||
| b540ec3f46 | |||
| 5c90431876 | |||
| 793889c3b7 | |||
| eb42086f9c | |||
| d0efc40efb | |||
| ae704d1d5f | |||
| 525dfd9fc1 | |||
| 18bd178bbd | |||
| 088155f6f3 | |||
| b750faa66f | |||
| ef3309814d | |||
| b87a05b457 | |||
| 754439f03c |
@@ -186,7 +186,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||||
cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
cargo +nightly-2025-09-01 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||||
else
|
else
|
||||||
if [[ $OS =~ ^windows.*$ ]]; then
|
if [[ $OS =~ ^windows.*$ ]]; then
|
||||||
SUFFIX=.exe
|
SUFFIX=.exe
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
image_tag:
|
image_tag:
|
||||||
description: 'Tag for this image build'
|
description: 'Tag for this image build'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.4.3'
|
default: 'v2.4.5'
|
||||||
required: true
|
required: true
|
||||||
mark_latest:
|
mark_latest:
|
||||||
description: 'Mark this image as latest'
|
description: 'Mark this image as latest'
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
|||||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||||
|
|
||||||
rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
rustup toolchain install nightly-2025-09-01-x86_64-unknown-linux-gnu
|
||||||
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
|
rustup component add rust-src --toolchain nightly-2025-09-01-x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
# https://github.com/rust-lang/rust/issues/128808
|
# https://github.com/rust-lang/rust/issues/128808
|
||||||
# remove it after Cargo or rustc fix this.
|
# remove it after Cargo or rustc fix this.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ on:
|
|||||||
version:
|
version:
|
||||||
description: 'Version for this release'
|
description: 'Version for this release'
|
||||||
type: string
|
type: string
|
||||||
default: 'v2.4.3'
|
default: 'v2.4.5'
|
||||||
required: true
|
required: true
|
||||||
make_latest:
|
make_latest:
|
||||||
description: 'Mark this release as latest'
|
description: 'Mark this release as latest'
|
||||||
|
|||||||
Generated
+55
-27
@@ -129,6 +129,24 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_log-sys"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_logger"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f"
|
||||||
|
dependencies = [
|
||||||
|
"android_log-sys",
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -2090,7 +2108,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier"
|
name = "easytier"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2179,6 +2197,7 @@ dependencies = [
|
|||||||
"sys-locale",
|
"sys-locale",
|
||||||
"tabled",
|
"tabled",
|
||||||
"tachyonix",
|
"tachyonix",
|
||||||
|
"tempfile",
|
||||||
"thiserror 1.0.63",
|
"thiserror 1.0.63",
|
||||||
"thunk-rs",
|
"thunk-rs",
|
||||||
"tikv-jemalloc-ctl",
|
"tikv-jemalloc-ctl",
|
||||||
@@ -2195,7 +2214,6 @@ dependencies = [
|
|||||||
"toml 0.8.19",
|
"toml 0.8.19",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tun-easytier",
|
"tun-easytier",
|
||||||
"url",
|
"url",
|
||||||
@@ -2213,6 +2231,19 @@ dependencies = [
|
|||||||
"zstd",
|
"zstd",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "easytier-android-jni"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"android_logger",
|
||||||
|
"easytier",
|
||||||
|
"jni",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-ffi"
|
name = "easytier-ffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2227,7 +2258,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2314,7 +2345,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -2517,6 +2548,16 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -2595,9 +2636,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.1.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
@@ -3364,9 +3405,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
version = "0.8.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hash32",
|
"hash32",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
@@ -7856,8 +7897,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "smoltcp"
|
name = "smoltcp"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/smoltcp-rs/smoltcp.git?rev=0a926767a68bc88d5512afefa7529c5ecdade4ea#0a926767a68bc88d5512afefa7529c5ecdade4ea"
|
||||||
checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -8803,15 +8843,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.12.0"
|
version = "3.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"getrandom 0.3.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 0.38.34",
|
"rustix 1.0.7",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9440,18 +9480,6 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-appender"
|
|
||||||
version = "0.2.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"thiserror 1.0.63",
|
|
||||||
"time",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.28"
|
version = "0.1.28"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"easytier-web",
|
"easytier-web",
|
||||||
"easytier-contrib/easytier-ffi",
|
"easytier-contrib/easytier-ffi",
|
||||||
"easytier-contrib/easytier-uptime",
|
"easytier-contrib/easytier-uptime",
|
||||||
|
"easytier-contrib/easytier-android-jni",
|
||||||
]
|
]
|
||||||
default-members = ["easytier", "easytier-web"]
|
default-members = ["easytier", "easytier-web"]
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
|||||||
# See https://easytier.cn/en/guide/installation.html#installation-methods
|
# See https://easytier.cn/en/guide/installation.html#installation-methods
|
||||||
|
|
||||||
# 4. Linux Quick Install
|
# 4. Linux Quick Install
|
||||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
|
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash -s install
|
||||||
|
|
||||||
# 5. MacOS via Homebrew
|
# 5. MacOS via Homebrew
|
||||||
brew tap brewforge/chinese
|
brew tap brewforge/chinese
|
||||||
@@ -105,9 +105,9 @@ After successful execution, you can check the network status using `easytier-cli
|
|||||||
```text
|
```text
|
||||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.3-70e69a38~ |
|
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.5-70e69a38~ |
|
||||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.3-70e69a38~ |
|
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.5-70e69a38~ |
|
||||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.3-70e69a38~ |
|
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.5-70e69a38~ |
|
||||||
```
|
```
|
||||||
|
|
||||||
You can test connectivity between nodes:
|
You can test connectivity between nodes:
|
||||||
|
|||||||
+4
-4
@@ -59,7 +59,7 @@ cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
|||||||
# 参见 https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F
|
# 参见 https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F
|
||||||
|
|
||||||
# 4. Linux 快速安装
|
# 4. Linux 快速安装
|
||||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
|
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash -s install
|
||||||
|
|
||||||
# 5. MacOS 通过 Homebrew 安装
|
# 5. MacOS 通过 Homebrew 安装
|
||||||
brew tap brewforge/chinese
|
brew tap brewforge/chinese
|
||||||
@@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea
|
|||||||
```text
|
```text
|
||||||
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
|
||||||
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
|
||||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.3-70e69a38~ |
|
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.5-70e69a38~ |
|
||||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.3-70e69a38~ |
|
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.5-70e69a38~ |
|
||||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.3-70e69a38~ |
|
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.5-70e69a38~ |
|
||||||
```
|
```
|
||||||
|
|
||||||
您可以测试节点之间的连通性:
|
您可以测试节点之间的连通性:
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "easytier-android-jni"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jni = "0.21"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
log = "0.4"
|
||||||
|
android_logger = "0.13"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
easytier = { path = "../../easytier" }
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# EasyTier Android JNI
|
||||||
|
|
||||||
|
这是 EasyTier 的 Android JNI 绑定库,允许 Android 应用程序调用 EasyTier 的网络功能。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🚀 完整的 EasyTier FFI 接口封装
|
||||||
|
- 📱 原生 Android JNI 支持
|
||||||
|
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
|
||||||
|
- 🛡️ 类型安全的 Java 接口
|
||||||
|
- 📝 详细的错误处理和日志记录
|
||||||
|
|
||||||
|
## 支持的架构
|
||||||
|
|
||||||
|
- `arm64-v8a` (aarch64-linux-android)
|
||||||
|
- `armeabi-v7a` (armv7-linux-androideabi)
|
||||||
|
- `x86` (i686-linux-android)
|
||||||
|
- `x86_64` (x86_64-linux-android)
|
||||||
|
|
||||||
|
## 构建要求
|
||||||
|
|
||||||
|
### 系统要求
|
||||||
|
|
||||||
|
- Rust 1.70+
|
||||||
|
- Android NDK r21+
|
||||||
|
- Linux/macOS 开发环境
|
||||||
|
|
||||||
|
### 环境设置
|
||||||
|
|
||||||
|
1. **安装 Rust**
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source ~/.cargo/env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安装 Android NDK**
|
||||||
|
- 下载 Android NDK: https://developer.android.com/ndk/downloads
|
||||||
|
- 解压到合适的目录
|
||||||
|
- 设置环境变量:
|
||||||
|
```bash
|
||||||
|
export ANDROID_NDK_ROOT=/path/to/android-ndk
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **添加 Android 目标**
|
||||||
|
```bash
|
||||||
|
rustup target add aarch64-linux-android
|
||||||
|
rustup target add armv7-linux-androideabi
|
||||||
|
rustup target add i686-linux-android
|
||||||
|
rustup target add x86_64-linux-android
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建步骤
|
||||||
|
|
||||||
|
1. **克隆项目并进入目录**
|
||||||
|
```bash
|
||||||
|
cd /path/to/EasyTier/easytier-contrib/easytier-android-jni
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **运行构建脚本**
|
||||||
|
```bash
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **构建完成后,库文件将生成在 `target/android/` 目录下**
|
||||||
|
```
|
||||||
|
target/android/
|
||||||
|
├── arm64-v8a/
|
||||||
|
│ └── libeasytier_android_jni.so
|
||||||
|
├── armeabi-v7a/
|
||||||
|
│ └── libeasytier_android_jni.so
|
||||||
|
├── x86/
|
||||||
|
│ └── libeasytier_android_jni.so
|
||||||
|
└── x86_64/
|
||||||
|
└── libeasytier_android_jni.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## Android 项目集成
|
||||||
|
|
||||||
|
### 1. 复制库文件
|
||||||
|
|
||||||
|
将生成的 `.so` 文件复制到您的 Android 项目中:
|
||||||
|
|
||||||
|
```
|
||||||
|
your-android-project/
|
||||||
|
└── src/main/
|
||||||
|
├── jniLibs/
|
||||||
|
│ ├── arm64-v8a/
|
||||||
|
│ │ └── libeasytier_android_jni.so
|
||||||
|
│ ├── armeabi-v7a/
|
||||||
|
│ │ └── libeasytier_android_jni.so
|
||||||
|
│ ├── x86/
|
||||||
|
│ │ └── libeasytier_android_jni.so
|
||||||
|
│ └── x86_64/
|
||||||
|
│ └── libeasytier_android_jni.so
|
||||||
|
└── java/
|
||||||
|
└── com/easytier/jni/
|
||||||
|
└── EasyTierJNI.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 复制 Java 接口
|
||||||
|
|
||||||
|
将 `java/com/easytier/jni/EasyTierJNI.java` 复制到您的 Android 项目的相应包路径下。
|
||||||
|
|
||||||
|
### 3. 添加权限
|
||||||
|
|
||||||
|
在 `AndroidManifest.xml` 中添加必要的权限:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```java
|
||||||
|
import com.easytier.jni.EasyTierJNI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class EasyTierManager {
|
||||||
|
|
||||||
|
// 初始化网络实例
|
||||||
|
public void startNetwork() {
|
||||||
|
String config = """
|
||||||
|
inst_name = "my_instance"
|
||||||
|
network = "my_network"
|
||||||
|
""";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析配置
|
||||||
|
int result = EasyTierJNI.parseConfig(config);
|
||||||
|
if (result != 0) {
|
||||||
|
String error = EasyTierJNI.getLastError();
|
||||||
|
throw new RuntimeException("配置解析失败: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动网络实例
|
||||||
|
result = EasyTierJNI.runNetworkInstance(config);
|
||||||
|
if (result != 0) {
|
||||||
|
String error = EasyTierJNI.getLastError();
|
||||||
|
throw new RuntimeException("网络实例启动失败: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("EasyTier 网络实例启动成功");
|
||||||
|
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
System.err.println("启动失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取网络信息
|
||||||
|
public void getNetworkInfo() {
|
||||||
|
try {
|
||||||
|
Map<String, String> infos = EasyTierJNI.collectNetworkInfosAsMap(10);
|
||||||
|
for (Map.Entry<String, String> entry : infos.entrySet()) {
|
||||||
|
System.out.println(entry.getKey() + ": " + entry.getValue());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
System.err.println("获取网络信息失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止所有实例
|
||||||
|
public void stopNetwork() {
|
||||||
|
try {
|
||||||
|
int result = EasyTierJNI.stopAllInstances();
|
||||||
|
if (result == 0) {
|
||||||
|
System.out.println("所有网络实例已停止");
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
System.err.println("停止网络失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VPN 服务集成
|
||||||
|
|
||||||
|
如果您要在 Android VPN 服务中使用:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class EasyTierVpnService extends VpnService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
// 建立 VPN 连接
|
||||||
|
ParcelFileDescriptor vpnInterface = establishVpnInterface();
|
||||||
|
|
||||||
|
if (vpnInterface != null) {
|
||||||
|
int fd = vpnInterface.getFd();
|
||||||
|
|
||||||
|
// 设置 TUN 文件描述符
|
||||||
|
try {
|
||||||
|
EasyTierJNI.setTunFd("my_instance", fd);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
Log.e("EasyTier", "设置 TUN FD 失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParcelFileDescriptor establishVpnInterface() {
|
||||||
|
Builder builder = new Builder();
|
||||||
|
builder.setMtu(1500);
|
||||||
|
builder.addAddress("10.0.0.2", 24);
|
||||||
|
builder.addRoute("0.0.0.0", 0);
|
||||||
|
builder.setSession("EasyTier VPN");
|
||||||
|
|
||||||
|
return builder.establish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 参考
|
||||||
|
|
||||||
|
### EasyTierJNI 类方法
|
||||||
|
|
||||||
|
| 方法 | 描述 | 参数 | 返回值 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| `parseConfig(String config)` | 解析 TOML 配置 | config: 配置字符串 | 0=成功, -1=失败 |
|
||||||
|
| `runNetworkInstance(String config)` | 启动网络实例 | config: 配置字符串 | 0=成功, -1=失败 |
|
||||||
|
| `setTunFd(String instanceName, int fd)` | 设置 TUN 文件描述符 | instanceName: 实例名, fd: 文件描述符 | 0=成功, -1=失败 |
|
||||||
|
| `retainNetworkInstance(String[] names)` | 保留指定实例 | names: 实例名数组 | 0=成功, -1=失败 |
|
||||||
|
| `collectNetworkInfos(int maxLength)` | 收集网络信息 | maxLength: 最大条目数 | 信息字符串数组 |
|
||||||
|
| `collectNetworkInfosAsMap(int maxLength)` | 收集网络信息为 Map | maxLength: 最大条目数 | Map<String, String> |
|
||||||
|
| `getLastError()` | 获取最后错误 | 无 | 错误消息字符串 |
|
||||||
|
| `stopAllInstances()` | 停止所有实例 | 无 | 0=成功, -1=失败 |
|
||||||
|
| `retainSingleInstance(String name)` | 保留单个实例 | name: 实例名 | 0=成功, -1=失败 |
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **构建失败: "Android NDK not found"**
|
||||||
|
- 确保设置了 `ANDROID_NDK_ROOT` 环境变量
|
||||||
|
- 检查 NDK 路径是否正确
|
||||||
|
|
||||||
|
2. **运行时错误: "java.lang.UnsatisfiedLinkError"**
|
||||||
|
- 确保 `.so` 文件放在正确的 `jniLibs` 目录下
|
||||||
|
- 检查目标架构是否匹配
|
||||||
|
|
||||||
|
3. **配置解析失败**
|
||||||
|
- 检查 TOML 配置格式是否正确
|
||||||
|
- 使用 `getLastError()` 获取详细错误信息
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
- 启用 Android 日志查看 JNI 层的日志输出
|
||||||
|
- 使用 `adb logcat -s EasyTier-JNI` 查看相关日志
|
||||||
|
- 检查 `getLastError()` 返回的错误信息
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目遵循与 EasyTier 主项目相同的许可证。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request 来改进这个项目。
|
||||||
|
|
||||||
|
## 相关链接
|
||||||
|
|
||||||
|
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
|
||||||
|
- [Android NDK 文档](https://developer.android.com/ndk)
|
||||||
|
- [Rust JNI 文档](https://docs.rs/jni/)
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# EasyTier Android JNI 构建脚本
|
||||||
|
# 用于编译适用于 Android 平台的 JNI 库
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
echo -e "${GREEN}EasyTier Android JNI 构建脚本${NC}"
|
||||||
|
echo "=============================="
|
||||||
|
|
||||||
|
# 检查 Rust 是否安装
|
||||||
|
if ! command -v rustc &> /dev/null; then
|
||||||
|
echo -e "${RED}错误: 未找到 Rust 编译器,请先安装 Rust${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 cargo 是否安装
|
||||||
|
if ! command -v cargo &> /dev/null; then
|
||||||
|
echo -e "${RED}错误: 未找到 Cargo,请先安装 Rust 工具链${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Android 目标架构
|
||||||
|
# TARGETS=("aarch64-linux-android" "armv7-linux-androideabi" "i686-linux-android" "x86_64-linux-android")
|
||||||
|
TARGETS=("aarch64-linux-android")
|
||||||
|
|
||||||
|
# 检查是否安装了 Android 目标
|
||||||
|
echo -e "${YELLOW}检查 Android 目标架构...${NC}"
|
||||||
|
for target in "${TARGETS[@]}"; do
|
||||||
|
if ! rustup target list --installed | grep -q "$target"; then
|
||||||
|
echo -e "${YELLOW}安装目标架构: $target${NC}"
|
||||||
|
rustup target add "$target"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}目标架构已安装: $target${NC}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 创建输出目录
|
||||||
|
OUTPUT_DIR="./target/android"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# 构建函数
|
||||||
|
build_for_target() {
|
||||||
|
local target=$1
|
||||||
|
echo -e "${YELLOW}构建目标: $target${NC}"
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
export CC_aarch64_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
|
||||||
|
export CC_armv7_linux_androideabi="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
|
||||||
|
export CC_i686_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"
|
||||||
|
export CC_x86_64_linux_android="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
|
||||||
|
|
||||||
|
# 首先构建 easytier-ffi
|
||||||
|
echo -e "${YELLOW}构建 easytier-ffi for $target${NC}"
|
||||||
|
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo build --target="$target" --release)
|
||||||
|
|
||||||
|
# 设置链接器环境变量
|
||||||
|
export RUSTFLAGS="-L $(readlink -f $REPO_ROOT/target/$target/release) -l easytier_ffi"
|
||||||
|
echo $RUSTFLAGS
|
||||||
|
|
||||||
|
# 构建 JNI 库
|
||||||
|
cargo build --target="$target" --release
|
||||||
|
|
||||||
|
# 复制库文件到输出目录
|
||||||
|
local arch_dir
|
||||||
|
case $target in
|
||||||
|
"aarch64-linux-android")
|
||||||
|
arch_dir="arm64-v8a"
|
||||||
|
;;
|
||||||
|
"armv7-linux-androideabi")
|
||||||
|
arch_dir="armeabi-v7a"
|
||||||
|
;;
|
||||||
|
"i686-linux-android")
|
||||||
|
arch_dir="x86"
|
||||||
|
;;
|
||||||
|
"x86_64-linux-android")
|
||||||
|
arch_dir="x86_64"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$OUTPUT_DIR/$arch_dir"
|
||||||
|
cp "$REPO_ROOT/target/$target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$arch_dir/"
|
||||||
|
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$arch_dir/${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查 Android NDK
|
||||||
|
if [ -z "$ANDROID_NDK_ROOT" ]; then
|
||||||
|
echo -e "${RED}错误: 未设置 ANDROID_NDK_ROOT 环境变量${NC}"
|
||||||
|
echo "请设置 ANDROID_NDK_ROOT 指向您的 Android NDK 安装目录"
|
||||||
|
echo "例如: export ANDROID_NDK_ROOT=/path/to/android-ndk"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$ANDROID_NDK_ROOT" ]; then
|
||||||
|
echo -e "${RED}错误: Android NDK 目录不存在: $ANDROID_NDK_ROOT${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
|
||||||
|
|
||||||
|
# 构建所有目标
|
||||||
|
echo -e "${YELLOW}开始构建所有目标架构...${NC}"
|
||||||
|
for target in "${TARGETS[@]}"; do
|
||||||
|
build_for_target "$target"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}构建完成!${NC}"
|
||||||
|
echo -e "${GREEN}所有库文件已生成到: $OUTPUT_DIR${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "目录结构:"
|
||||||
|
ls -la "$OUTPUT_DIR"/*/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}使用说明:${NC}"
|
||||||
|
echo "1. 将生成的 .so 文件复制到您的 Android 项目的 src/main/jniLibs/ 目录下"
|
||||||
|
echo "2. 将 java/com/easytier/jni/EasyTierJNI.java 复制到您的 Android 项目中"
|
||||||
|
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# EasyTier Android JNI 示例配置文件
|
||||||
|
# 这是一个基本的配置示例,展示如何配置 EasyTier 网络实例
|
||||||
|
|
||||||
|
# 实例名称 (必需)
|
||||||
|
inst_name = "android_instance"
|
||||||
|
|
||||||
|
# 网络名称 (必需)
|
||||||
|
network = "my_easytier_network"
|
||||||
|
|
||||||
|
# 网络密钥 (可选,用于网络加密)
|
||||||
|
# network_secret = "your_secret_key_here"
|
||||||
|
|
||||||
|
# 监听地址 (可选)
|
||||||
|
# listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
|
||||||
|
|
||||||
|
# 对等节点地址 (可选)
|
||||||
|
# peers = ["tcp://peer1.example.com:11010", "udp://peer2.example.com:11010"]
|
||||||
|
|
||||||
|
# 虚拟 IP 地址 (可选)
|
||||||
|
# ipv4 = "10.144.144.1"
|
||||||
|
|
||||||
|
# 主机名 (可选)
|
||||||
|
# hostname = "android-device"
|
||||||
|
|
||||||
|
# 启用 IPv6 (可选)
|
||||||
|
# ipv6 = "fd00::1"
|
||||||
|
|
||||||
|
# 代理网络 (可选)
|
||||||
|
# proxy_networks = ["192.168.1.0/24"]
|
||||||
|
|
||||||
|
# 退出节点 (可选)
|
||||||
|
# exit_nodes = ["peer1"]
|
||||||
|
|
||||||
|
# 启用加密 (可选)
|
||||||
|
# enable_encryption = true
|
||||||
|
|
||||||
|
# 启用 IPv4 转发 (可选)
|
||||||
|
# enable_ipv4 = true
|
||||||
|
|
||||||
|
# 启用 IPv6 转发 (可选)
|
||||||
|
# enable_ipv6 = false
|
||||||
|
|
||||||
|
# MTU 设置 (可选)
|
||||||
|
# mtu = 1420
|
||||||
|
|
||||||
|
# 日志级别 (可选: error, warn, info, debug, trace)
|
||||||
|
# log_level = "info"
|
||||||
|
|
||||||
|
# 禁用 P2P (可选)
|
||||||
|
# disable_p2p = false
|
||||||
|
|
||||||
|
# 使用多路径 (可选)
|
||||||
|
# use_multi_path = true
|
||||||
|
|
||||||
|
# 延迟优先 (可选)
|
||||||
|
# latency_first = false
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.easytier.jni
|
||||||
|
|
||||||
|
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
|
||||||
|
object EasyTierJNI {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// 加载本地库
|
||||||
|
System.loadLibrary("easytier_android_jni")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 TUN 文件描述符
|
||||||
|
* @param instanceName 实例名称
|
||||||
|
* @param fd TUN 文件描述符
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun setTunFd(instanceName: String, fd: Int): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析配置字符串
|
||||||
|
* @param config TOML 格式的配置字符串
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当配置解析失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun parseConfig(config: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行网络实例
|
||||||
|
* @param config TOML 格式的配置字符串
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当实例启动失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun runNetworkInstance(config: String): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保留指定的网络实例,停止其他实例
|
||||||
|
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun retainNetworkInstance(instanceNames: Array<String>?): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集网络信息
|
||||||
|
* @param maxLength 最大返回条目数
|
||||||
|
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后的错误消息
|
||||||
|
* @return 错误消息字符串,如果没有错误则返回 null
|
||||||
|
*/
|
||||||
|
@JvmStatic external fun getLastError(): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便利方法:停止所有网络实例
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun stopAllInstances(): Int {
|
||||||
|
return retainNetworkInstance(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便利方法:停止指定实例外的所有实例
|
||||||
|
* @param instanceName 要保留的实例名称
|
||||||
|
* @return 0 表示成功,-1 表示失败
|
||||||
|
* @throws RuntimeException 当操作失败时抛出异常
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun retainSingleInstance(instanceName: String): Int {
|
||||||
|
return retainNetworkInstance(arrayOf(instanceName))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
package com.easytier.jni
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.wire.WireJsonAdapterFactory
|
||||||
|
import common.Ipv4Inet
|
||||||
|
import web.NetworkInstanceRunningInfoMap
|
||||||
|
|
||||||
|
fun parseIpv4InetToString(inet: Ipv4Inet?): String? {
|
||||||
|
val addr = inet?.address?.addr ?: return null
|
||||||
|
val networkLength = inet.network_length
|
||||||
|
|
||||||
|
// 将 int32 转换为 IPv4 字符串
|
||||||
|
val ip =
|
||||||
|
String.format(
|
||||||
|
"%d.%d.%d.%d",
|
||||||
|
(addr shr 24) and 0xFF,
|
||||||
|
(addr shr 16) and 0xFF,
|
||||||
|
(addr shr 8) and 0xFF,
|
||||||
|
addr and 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
return "$ip/$networkLength"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** EasyTier 管理类 负责管理 EasyTier 实例的生命周期、监控网络状态变化、控制 VpnService */
|
||||||
|
class EasyTierManager(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val instanceName: String,
|
||||||
|
private val networkConfig: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "EasyTierManager"
|
||||||
|
private const val MONITOR_INTERVAL = 3000L // 3秒监控间隔
|
||||||
|
}
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var isRunning = false
|
||||||
|
private var currentIpv4: String? = null
|
||||||
|
private var currentProxyCidrs: List<String> = emptyList()
|
||||||
|
private var vpnServiceIntent: Intent? = null
|
||||||
|
|
||||||
|
// JSON 解析器
|
||||||
|
private val moshi = Moshi.Builder().add(WireJsonAdapterFactory()).build()
|
||||||
|
private val adapter = moshi.adapter(NetworkInstanceRunningInfoMap::class.java)
|
||||||
|
|
||||||
|
// 监控任务
|
||||||
|
private val monitorRunnable =
|
||||||
|
object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (isRunning) {
|
||||||
|
monitorNetworkStatus()
|
||||||
|
handler.postDelayed(this, MONITOR_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动 EasyTier 实例和监控 */
|
||||||
|
fun start() {
|
||||||
|
if (isRunning) {
|
||||||
|
Log.w(TAG, "EasyTier 实例已经在运行中")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 启动 EasyTier 实例
|
||||||
|
val result = EasyTierJNI.runNetworkInstance(networkConfig)
|
||||||
|
if (result == 0) {
|
||||||
|
isRunning = true
|
||||||
|
Log.i(TAG, "EasyTier 实例启动成功: $instanceName")
|
||||||
|
|
||||||
|
// 开始监控网络状态
|
||||||
|
handler.post(monitorRunnable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "EasyTier 实例启动失败: $result")
|
||||||
|
val error = EasyTierJNI.getLastError()
|
||||||
|
Log.e(TAG, "错误信息: $error")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "启动 EasyTier 实例时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止 EasyTier 实例和监控 */
|
||||||
|
fun stop() {
|
||||||
|
if (!isRunning) {
|
||||||
|
Log.w(TAG, "EasyTier 实例未在运行")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = false
|
||||||
|
|
||||||
|
// 停止监控任务
|
||||||
|
handler.removeCallbacks(monitorRunnable)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 停止 VpnService
|
||||||
|
stopVpnService()
|
||||||
|
|
||||||
|
// 停止 EasyTier 实例
|
||||||
|
EasyTierJNI.stopAllInstances()
|
||||||
|
Log.i(TAG, "EasyTier 实例已停止: $instanceName")
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
currentIpv4 = null
|
||||||
|
currentProxyCidrs = emptyList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "停止 EasyTier 实例时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监控网络状态 */
|
||||||
|
private fun monitorNetworkStatus() {
|
||||||
|
try {
|
||||||
|
val infosJson = EasyTierJNI.collectNetworkInfos(10)
|
||||||
|
if (infosJson.isNullOrEmpty()) {
|
||||||
|
Log.d(TAG, "未获取到网络信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val networkInfoMap = parseNetworkInfo(infosJson)
|
||||||
|
val networkInfo = networkInfoMap?.map?.get(instanceName)
|
||||||
|
|
||||||
|
if (networkInfo == null) {
|
||||||
|
Log.d(TAG, "未找到实例 $instanceName 的网络信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "网络信息: $networkInfo")
|
||||||
|
|
||||||
|
// 检查实例是否正在运行
|
||||||
|
if (!networkInfo.running) {
|
||||||
|
Log.w(TAG, "EasyTier 实例未运行: ${networkInfo.error_msg}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newIpv4Inet = networkInfo.my_node_info?.virtual_ipv4
|
||||||
|
|
||||||
|
if (newIpv4Inet == null) {
|
||||||
|
Log.w(TAG, "EasyTier No Ipv4: $networkInfo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前节点的 IPv4 地址
|
||||||
|
val newIpv4 = parseIpv4InetToString(newIpv4Inet)
|
||||||
|
|
||||||
|
// 获取所有节点的 proxy_cidrs
|
||||||
|
val newProxyCidrs = mutableListOf<String>()
|
||||||
|
networkInfo.routes?.forEach { route ->
|
||||||
|
route.proxy_cidrs?.let { cidrs -> newProxyCidrs.addAll(cidrs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有变化
|
||||||
|
val ipv4Changed = newIpv4 != currentIpv4
|
||||||
|
val proxyCidrsChanged = newProxyCidrs != currentProxyCidrs
|
||||||
|
|
||||||
|
if (ipv4Changed || proxyCidrsChanged) {
|
||||||
|
Log.i(TAG, "网络状态发生变化:")
|
||||||
|
Log.i(TAG, " IPv4: $currentIpv4 -> $newIpv4")
|
||||||
|
Log.i(TAG, " Proxy CIDRs: $currentProxyCidrs -> $newProxyCidrs")
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
currentIpv4 = newIpv4
|
||||||
|
currentProxyCidrs = newProxyCidrs.toList()
|
||||||
|
|
||||||
|
// 重启 VpnService
|
||||||
|
if (newIpv4 != null) {
|
||||||
|
restartVpnService(newIpv4, newProxyCidrs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "网络状态无变化 - IPv4: $currentIpv4, Proxy CIDRs: ${currentProxyCidrs.size} 个")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "监控网络状态时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析网络信息 JSON */
|
||||||
|
private fun parseNetworkInfo(jsonString: String): NetworkInstanceRunningInfoMap? {
|
||||||
|
return try {
|
||||||
|
adapter.fromJson(jsonString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "解析网络信息失败", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重启 VpnService */
|
||||||
|
private fun restartVpnService(ipv4: String, proxyCidrs: List<String>) {
|
||||||
|
try {
|
||||||
|
// 先停止现有的 VpnService
|
||||||
|
stopVpnService()
|
||||||
|
|
||||||
|
// 启动新的 VpnService
|
||||||
|
startVpnService(ipv4, proxyCidrs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "重启 VpnService 时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动 VpnService */
|
||||||
|
private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(activity, EasyTierVpnService::class.java)
|
||||||
|
intent.putExtra("ipv4_address", ipv4)
|
||||||
|
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
|
||||||
|
intent.putExtra("instance_name", instanceName)
|
||||||
|
|
||||||
|
activity.startService(intent)
|
||||||
|
vpnServiceIntent = intent
|
||||||
|
|
||||||
|
Log.i(TAG, "VpnService 已启动 - IPv4: $ipv4, Proxy CIDRs: $proxyCidrs")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "启动 VpnService 时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止 VpnService */
|
||||||
|
private fun stopVpnService() {
|
||||||
|
try {
|
||||||
|
vpnServiceIntent?.let { intent ->
|
||||||
|
activity.stopService(intent)
|
||||||
|
Log.i(TAG, "VpnService 已停止")
|
||||||
|
}
|
||||||
|
vpnServiceIntent = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "停止 VpnService 时发生异常", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前状态信息 */
|
||||||
|
fun getStatus(): EasyTierStatus {
|
||||||
|
return EasyTierStatus(
|
||||||
|
isRunning = isRunning,
|
||||||
|
instanceName = instanceName,
|
||||||
|
currentIpv4 = currentIpv4,
|
||||||
|
currentProxyCidrs = currentProxyCidrs.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态数据类 */
|
||||||
|
data class EasyTierStatus(
|
||||||
|
val isRunning: Boolean,
|
||||||
|
val instanceName: String,
|
||||||
|
val currentIpv4: String?,
|
||||||
|
val currentProxyCidrs: List<String>
|
||||||
|
)
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package com.easytier.jni
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class EasyTierVpnService : VpnService() {
|
||||||
|
|
||||||
|
private var vpnInterface: ParcelFileDescriptor? = null
|
||||||
|
private var isRunning = false
|
||||||
|
private var instanceName: String? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "EasyTierVpnService"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
Log.d(TAG, "VPN Service created")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
// 获取传入的参数
|
||||||
|
val ipv4Address = intent?.getStringExtra("ipv4_address")
|
||||||
|
val proxyCidrs = intent?.getStringArrayListExtra("proxy_cidrs") ?: arrayListOf()
|
||||||
|
instanceName = intent?.getStringExtra("instance_name")
|
||||||
|
|
||||||
|
if (ipv4Address == null || instanceName == null) {
|
||||||
|
Log.e(TAG, "缺少必要参数: ipv4Address=$ipv4Address, instanceName=$instanceName")
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"启动 VPN Service - IPv4: $ipv4Address, Proxy CIDRs: $proxyCidrs, Instance: $instanceName"
|
||||||
|
)
|
||||||
|
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
setupVpnInterface(ipv4Address, proxyCidrs)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(TAG, "VPN 设置失败", t)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupVpnInterface(ipv4Address: String, proxyCidrs: List<String>) {
|
||||||
|
try {
|
||||||
|
// 解析 IPv4 地址和网络长度
|
||||||
|
val (ip, networkLength) = parseIpv4Address(ipv4Address)
|
||||||
|
|
||||||
|
// 1. 准备 VpnService.Builder
|
||||||
|
val builder = Builder()
|
||||||
|
builder.setSession("EasyTier VPN")
|
||||||
|
.addAddress(ip, networkLength)
|
||||||
|
.addDnsServer("223.5.5.5")
|
||||||
|
.addDnsServer("114.114.114.114")
|
||||||
|
.addDisallowedApplication("com.easytier.easytiervpn")
|
||||||
|
|
||||||
|
// 2. 添加路由表 - 为每个 proxy CIDR 添加路由
|
||||||
|
proxyCidrs.forEach { cidr ->
|
||||||
|
try {
|
||||||
|
val (routeIp, routeLength) = parseCidr(cidr)
|
||||||
|
builder.addRoute(routeIp, routeLength)
|
||||||
|
Log.d(TAG, "添加路由: $routeIp/$routeLength")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "解析 CIDR 失败: $cidr", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构建虚拟网络接口
|
||||||
|
vpnInterface = builder.establish()
|
||||||
|
|
||||||
|
if (vpnInterface == null) {
|
||||||
|
Log.e(TAG, "创建 VPN 接口失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "VPN 接口创建成功")
|
||||||
|
|
||||||
|
// 4. 将 TUN 文件描述符传递给 EasyTier
|
||||||
|
instanceName?.let { name ->
|
||||||
|
val fd = vpnInterface!!.fd
|
||||||
|
val result = EasyTierJNI.setTunFd(name, fd)
|
||||||
|
if (result == 0) {
|
||||||
|
Log.i(TAG, "TUN 文件描述符设置成功: $fd")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "TUN 文件描述符设置失败: $result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = true
|
||||||
|
|
||||||
|
// 5. 保持服务运行
|
||||||
|
while (isRunning && vpnInterface != null) {
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(TAG, "VPN 接口设置过程中发生错误", t)
|
||||||
|
} finally {
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 IPv4 地址,返回 IP 和网络长度 */
|
||||||
|
private fun parseIpv4Address(ipv4Address: String): Pair<String, Int> {
|
||||||
|
return if (ipv4Address.contains("/")) {
|
||||||
|
val parts = ipv4Address.split("/")
|
||||||
|
Pair(parts[0], parts[1].toInt())
|
||||||
|
} else {
|
||||||
|
// 默认使用 /24 网络
|
||||||
|
Pair(ipv4Address, 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 CIDR,返回 IP 和网络长度 */
|
||||||
|
private fun parseCidr(cidr: String): Pair<String, Int> {
|
||||||
|
val parts = cidr.split("/")
|
||||||
|
if (parts.size != 2) {
|
||||||
|
throw IllegalArgumentException("无效的 CIDR 格式: $cidr")
|
||||||
|
}
|
||||||
|
return Pair(parts[0], parts[1].toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanup() {
|
||||||
|
isRunning = false
|
||||||
|
vpnInterface?.close()
|
||||||
|
vpnInterface = null
|
||||||
|
Log.i(TAG, "VPN 接口已清理")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
Log.d(TAG, "VPN Service destroyed")
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 使用说明
|
||||||
|
|
||||||
|
1. 需要将 proto 文件放入 app/src/main/proto
|
||||||
|
2. android/gradle/libs.versions.toml 中加入依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
# Wire 核心运行时
|
||||||
|
android-wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version = "5.3.11" }
|
||||||
|
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||||
|
android-wire-moshi-adapter = { group = "com.squareup.wire", name = "wire-moshi-adapter", version = "5.3.11" }
|
||||||
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.9.0" }
|
||||||
|
```
|
||||||
|
|
||||||
|
3. build.gradle.kts 中加入
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins {
|
||||||
|
...
|
||||||
|
alias(libs.plugins.wire)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
...
|
||||||
|
implementation(libs.android.wire.runtime)
|
||||||
|
implementation(libs.android.wire.moshi.adapter)
|
||||||
|
implementation(libs.moshi)
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
wire {
|
||||||
|
kotlin {
|
||||||
|
rpcRole = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 调用 easytier-contrib/easytier-android-jni/build.sh 生成 jni 和 ffi 的 so 文件。
|
||||||
|
并将生成的 so 文件放到 android/app/src/main/jniLibs/arm64-v8a 目录下。
|
||||||
|
|
||||||
|
5. 使用 EasyTierManager 可以拉起 EasyTier 实例并启动 Android VpnService 组件。
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
use easytier::proto::web::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
|
||||||
|
use jni::objects::{JClass, JObjectArray, JString};
|
||||||
|
use jni::sys::{jint, jstring};
|
||||||
|
use jni::JNIEnv;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
// 定义 KeyValuePair 结构体
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct KeyValuePair {
|
||||||
|
pub key: *const std::ffi::c_char,
|
||||||
|
pub value: *const std::ffi::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 声明外部 C 函数
|
||||||
|
extern "C" {
|
||||||
|
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
|
||||||
|
fn get_error_msg(out: *mut *const std::ffi::c_char);
|
||||||
|
fn free_string(s: *const std::ffi::c_char);
|
||||||
|
fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
||||||
|
fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
|
||||||
|
fn retain_network_instance(
|
||||||
|
inst_names: *const *const std::ffi::c_char,
|
||||||
|
length: usize,
|
||||||
|
) -> std::ffi::c_int;
|
||||||
|
fn collect_network_infos(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 Android 日志
|
||||||
|
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default()
|
||||||
|
.with_max_level(log::LevelFilter::Debug)
|
||||||
|
.with_tag("EasyTier-JNI"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 辅助函数:从 Java String 转换为 CString
|
||||||
|
fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
|
||||||
|
let java_str = env
|
||||||
|
.get_string(jstr)
|
||||||
|
.map_err(|e| format!("Failed to get string: {:?}", e))?;
|
||||||
|
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
|
||||||
|
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:获取错误消息
|
||||||
|
fn get_last_error() -> Option<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut error_ptr: *const std::ffi::c_char = ptr::null();
|
||||||
|
get_error_msg(&mut error_ptr);
|
||||||
|
if error_ptr.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let error_cstr = CStr::from_ptr(error_ptr);
|
||||||
|
let error_str = error_cstr.to_string_lossy().into_owned();
|
||||||
|
free_string(error_ptr);
|
||||||
|
Some(error_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:抛出 Java 异常
|
||||||
|
fn throw_exception(env: &mut JNIEnv, message: &str) {
|
||||||
|
let _ = env.throw_new("java/lang/RuntimeException", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 TUN 文件描述符
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
inst_name: JString,
|
||||||
|
fd: jint,
|
||||||
|
) -> jint {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
|
||||||
|
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析配置
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
config: JString,
|
||||||
|
) -> jint {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = parse_config(config_cstr.as_ptr());
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 运行网络实例
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
config: JString,
|
||||||
|
) -> jint {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
|
||||||
|
let config_cstr = match jstring_to_cstring(&mut env, &config) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Invalid config string: {}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = run_network_instance(config_cstr.as_ptr());
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保持网络实例
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
instance_names: JObjectArray,
|
||||||
|
) -> jint {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
|
||||||
|
// 处理 null 数组的情况
|
||||||
|
if instance_names.is_null() {
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数组长度
|
||||||
|
let array_length = match env.get_array_length(&instance_names) {
|
||||||
|
Ok(len) => len as usize,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果数组为空,停止所有实例
|
||||||
|
if array_length == 0 {
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(ptr::null(), 0);
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 Java 字符串数组为 C 字符串数组
|
||||||
|
let mut c_strings = Vec::with_capacity(array_length);
|
||||||
|
let mut c_string_ptrs = Vec::with_capacity(array_length);
|
||||||
|
|
||||||
|
for i in 0..array_length {
|
||||||
|
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
|
||||||
|
Ok(obj) => obj,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Failed to get array element {}: {:?}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if java_string.is_null() {
|
||||||
|
continue; // 跳过 null 元素
|
||||||
|
}
|
||||||
|
|
||||||
|
let jstring = JString::from(java_string);
|
||||||
|
let c_string = match jstring_to_cstring(&mut env, &jstring) {
|
||||||
|
Ok(cstr) => cstr,
|
||||||
|
Err(e) => {
|
||||||
|
throw_exception(
|
||||||
|
&mut env,
|
||||||
|
&format!("Invalid instance name at index {}: {}", i, e),
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
c_string_ptrs.push(c_string.as_ptr());
|
||||||
|
c_strings.push(c_string); // 保持 CString 的所有权
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
|
||||||
|
if result != 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 收集网络信息
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
) -> jstring {
|
||||||
|
Lazy::force(&LOGGER_INIT);
|
||||||
|
|
||||||
|
const MAX_INFOS: usize = 100;
|
||||||
|
let mut infos = vec![
|
||||||
|
KeyValuePair {
|
||||||
|
key: ptr::null(),
|
||||||
|
value: ptr::null(),
|
||||||
|
};
|
||||||
|
MAX_INFOS
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let count = collect_network_infos(infos.as_mut_ptr(), MAX_INFOS);
|
||||||
|
if count < 0 {
|
||||||
|
if let Some(error) = get_last_error() {
|
||||||
|
throw_exception(&mut env, &error);
|
||||||
|
}
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ret = NetworkInstanceRunningInfoMap::default();
|
||||||
|
|
||||||
|
// 使用 serde_json 构建 JSON
|
||||||
|
for info in infos.iter().take(count as usize) {
|
||||||
|
let key_ptr = info.key;
|
||||||
|
let val_ptr = info.value;
|
||||||
|
if key_ptr.is_null() || val_ptr.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = CStr::from_ptr(key_ptr).to_string_lossy();
|
||||||
|
let val = CStr::from_ptr(val_ptr).to_string_lossy();
|
||||||
|
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to parse JSON");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ret.map.insert(key.to_string(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
|
||||||
|
match env.new_string(&json_str) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => {
|
||||||
|
throw_exception(&mut env, "Failed to create JSON string");
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取最后的错误信息
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
|
||||||
|
env: JNIEnv,
|
||||||
|
_class: JClass,
|
||||||
|
) -> jstring {
|
||||||
|
match get_last_error() {
|
||||||
|
Some(error) => match env.new_string(&error) {
|
||||||
|
Ok(jstr) => jstr.into_raw(),
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
},
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,10 @@ get_tun_iface() {
|
|||||||
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
|
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
|
||||||
}
|
}
|
||||||
get_hot_iface() {
|
get_hot_iface() {
|
||||||
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||||
|
}
|
||||||
|
get_usb_iface() {
|
||||||
|
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
|
||||||
}
|
}
|
||||||
get_hot_cidr() {
|
get_hot_cidr() {
|
||||||
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
|
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
|
||||||
@@ -33,10 +36,12 @@ set_nat_rules() {
|
|||||||
ET_IFACE=$(get_et_iface)
|
ET_IFACE=$(get_et_iface)
|
||||||
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
|
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
|
||||||
HOT_IFACE=$(get_hot_iface)
|
HOT_IFACE=$(get_hot_iface)
|
||||||
|
USB_IFACE=$(get_usb_iface)
|
||||||
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
|
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
|
||||||
|
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
|
||||||
|
|
||||||
# 如果热点关闭就删除自定义链
|
# 如果热点关闭就删除自定义链
|
||||||
[ -n "$ET_IFACE" ] && [ -n "$HOT_CIDR" ] || return 1
|
[ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
|
||||||
|
|
||||||
# 创建自定义链(如不存在)
|
# 创建自定义链(如不存在)
|
||||||
iptables -t nat -N ET_NAT 2>/dev/null
|
iptables -t nat -N ET_NAT 2>/dev/null
|
||||||
@@ -49,13 +54,22 @@ set_nat_rules() {
|
|||||||
iptables -I FORWARD 1 -j ET_FWD
|
iptables -I FORWARD 1 -j ET_FWD
|
||||||
|
|
||||||
# 添加规则
|
# 添加规则
|
||||||
|
if [ -n "$HOT_CIDR" ]; then
|
||||||
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||||
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
|
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
|
||||||
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||||
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
|
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
|
||||||
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
if [ -n "$USB_CIDR" ]; then
|
||||||
|
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
|
||||||
|
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
|
||||||
|
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
|
||||||
|
-m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR ↔ $ET_IFACE" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
flush_rules() {
|
flush_rules() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
id=easytier_magisk
|
id=easytier_magisk
|
||||||
name=EasyTier_Magisk
|
name=EasyTier_Magisk
|
||||||
version=v2.4.3
|
version=v2.4.5
|
||||||
versionCode=1
|
versionCode=1
|
||||||
author=EasyTier
|
author=EasyTier
|
||||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ import {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const nodes = ref([])
|
const nodes = ref([])
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
const statusFilter = ref('true')
|
const statusFilter = ref('')
|
||||||
const protocolFilter = ref('')
|
const protocolFilter = ref('')
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const selectedNode = ref(null)
|
const selectedNode = ref(null)
|
||||||
@@ -292,7 +292,7 @@ const apiUrl = ref(window.location.href)
|
|||||||
// 分页数据
|
// 分页数据
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 20,
|
per_page: 50,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ pub async fn admin_get_nodes(
|
|||||||
verify_admin_token(&headers)?;
|
verify_admin_token(&headers)?;
|
||||||
|
|
||||||
let page = pagination.page.unwrap_or(1);
|
let page = pagination.page.unwrap_or(1);
|
||||||
let per_page = pagination.per_page.unwrap_or(20);
|
let per_page = pagination.per_page.unwrap_or(200);
|
||||||
let offset = (page - 1) * per_page;
|
let offset = (page - 1) * per_page;
|
||||||
|
|
||||||
let mut query = entity::shared_nodes::Entity::find();
|
let mut query = entity::shared_nodes::Entity::find();
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ impl HealthChecker {
|
|||||||
);
|
);
|
||||||
|
|
||||||
self.instance_mgr
|
self.instance_mgr
|
||||||
.run_network_instance(cfg.clone(), ConfigSource::FFI)
|
.run_network_instance(cfg.clone(), ConfigSource::Web)
|
||||||
.with_context(|| "failed to run network instance")?;
|
.with_context(|| "failed to run network instance")?;
|
||||||
self.inst_id_map.insert(node_id, cfg.get_id());
|
self.inst_id_map.insert(node_id, cfg.get_id());
|
||||||
|
|
||||||
@@ -650,7 +650,7 @@ impl HealthChecker {
|
|||||||
node_id,
|
node_id,
|
||||||
HealthStatus::Unhealthy,
|
HealthStatus::Unhealthy,
|
||||||
None,
|
None,
|
||||||
Some(e.to_string()),
|
Some(format!("inst id: {}, err: {}", inst_id, e)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "easytier-gui",
|
"name": "easytier-gui",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.4.3",
|
"version": "2.4.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -13,18 +13,17 @@
|
|||||||
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "4.3.3",
|
"@primeuix/themes": "^1.2.3",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
|
||||||
"@tauri-apps/plugin-os": "2.3.0",
|
"@tauri-apps/plugin-os": "2.3.0",
|
||||||
"@tauri-apps/plugin-process": "2.3.0",
|
"@tauri-apps/plugin-process": "2.3.0",
|
||||||
"@tauri-apps/plugin-shell": "2.3.0",
|
"@tauri-apps/plugin-shell": "2.3.0",
|
||||||
"@vueuse/core": "^11.2.0",
|
"@vueuse/core": "^11.2.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
|
||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"pinia": "^2.2.4",
|
"pinia": "^2.2.4",
|
||||||
"primevue": "4.3.3",
|
"primevue": "^4.3.9",
|
||||||
"tauri-plugin-vpnservice-api": "workspace:*",
|
"tauri-plugin-vpnservice-api": "workspace:*",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^3.7.3",
|
"@antfu/eslint-config": "^3.7.3",
|
||||||
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
"@intlify/unplugin-vue-i18n": "^5.2.0",
|
||||||
"@primevue/auto-import-resolver": "4.3.3",
|
"@primevue/auto-import-resolver": "4.3.9",
|
||||||
"@tauri-apps/api": "2.7.0",
|
"@tauri-apps/api": "2.7.0",
|
||||||
"@tauri-apps/cli": "2.7.1",
|
"@tauri-apps/cli": "2.7.1",
|
||||||
"@types/default-gateway": "^7.2.2",
|
"@types/default-gateway": "^7.2.2",
|
||||||
|
|||||||
Generated
-7220
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-gui"
|
name = "easytier-gui"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
description = "EasyTier GUI"
|
description = "EasyTier GUI"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ fn easytier_version() -> Result<String, String> {
|
|||||||
Ok(easytier::VERSION.to_string())
|
Ok(easytier::VERSION.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
use tauri::ActivationPolicy;
|
||||||
|
app.set_activation_policy(if visible {
|
||||||
|
ActivationPolicy::Regular
|
||||||
|
} else {
|
||||||
|
ActivationPolicy::Accessory
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let _ = (app, visible);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn is_autostart() -> Result<bool, String> {
|
fn is_autostart() -> Result<bool, String> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
@@ -199,6 +216,8 @@ pub fn run() {
|
|||||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||||
level: None,
|
level: None,
|
||||||
file: None,
|
file: None,
|
||||||
|
size_mb: None,
|
||||||
|
count: None,
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -243,7 +262,8 @@ pub fn run() {
|
|||||||
set_logging_level,
|
set_logging_level,
|
||||||
set_tun_fd,
|
set_tun_fd,
|
||||||
is_autostart,
|
is_autostart,
|
||||||
easytier_version
|
easytier_version,
|
||||||
|
set_dock_visibility
|
||||||
])
|
])
|
||||||
.on_window_event(|_win, event| match event {
|
.on_window_event(|_win, event| match event {
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
|
|||||||
@@ -17,9 +17,13 @@
|
|||||||
"createUpdaterArtifacts": false
|
"createUpdaterArtifacts": false
|
||||||
},
|
},
|
||||||
"productName": "easytier-gui",
|
"productName": "easytier-gui",
|
||||||
"version": "2.4.3",
|
"version": "2.4.5",
|
||||||
"identifier": "com.kkrainbow.easytier",
|
"identifier": "com.kkrainbow.easytier",
|
||||||
"plugins": {},
|
"plugins": {
|
||||||
|
"shell": {
|
||||||
|
"open": "^.+"
|
||||||
|
}
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
|
|||||||
Vendored
+1
-9
@@ -9,7 +9,6 @@ declare global {
|
|||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
const MenuItemExit: typeof import('./composables/tray')['MenuItemExit']
|
const MenuItemExit: typeof import('./composables/tray')['MenuItemExit']
|
||||||
const MenuItemShow: typeof import('./composables/tray')['MenuItemShow']
|
const MenuItemShow: typeof import('./composables/tray')['MenuItemShow']
|
||||||
const ReinitTray: typeof import('./composables/tray')['ReinitTray']
|
|
||||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||||
const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos']
|
const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos']
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: typeof import('vue')['computed']
|
||||||
@@ -18,10 +17,8 @@ declare global {
|
|||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: typeof import('vue')['customRef']
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
|
||||||
const defineStore: typeof import('pinia')['defineStore']
|
const defineStore: typeof import('pinia')['defineStore']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const event2human: typeof import('./composables/utils')['event2human']
|
|
||||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||||
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
|
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
@@ -30,7 +27,6 @@ declare global {
|
|||||||
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
|
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
|
||||||
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
const getOsHostname: typeof import('./composables/network')['getOsHostname']
|
||||||
const h: typeof import('vue')['h']
|
const h: typeof import('vue')['h']
|
||||||
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
|
|
||||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
const isAutostart: typeof import('./composables/network')['isAutostart']
|
const isAutostart: typeof import('./composables/network')['isAutostart']
|
||||||
@@ -38,7 +34,6 @@ declare global {
|
|||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
const loadRunningInstanceIdsFromLocalStorage: typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']
|
|
||||||
const mapActions: typeof import('pinia')['mapActions']
|
const mapActions: typeof import('pinia')['mapActions']
|
||||||
const mapGetters: typeof import('pinia')['mapGetters']
|
const mapGetters: typeof import('pinia')['mapGetters']
|
||||||
const mapState: typeof import('pinia')['mapState']
|
const mapState: typeof import('pinia')['mapState']
|
||||||
@@ -46,8 +41,6 @@ declare global {
|
|||||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
|
|
||||||
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
|
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
@@ -74,7 +67,6 @@ declare global {
|
|||||||
const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance']
|
const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance']
|
||||||
const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance']
|
const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance']
|
||||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||||
const setAutoLaunchStatus: typeof import('./composables/network')['setAutoLaunchStatus']
|
|
||||||
const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel']
|
const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel']
|
||||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||||
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
||||||
@@ -85,7 +77,6 @@ declare global {
|
|||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
|
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
@@ -116,6 +107,7 @@ declare global {
|
|||||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|
||||||
// for vue template auto import
|
// for vue template auto import
|
||||||
import { UnwrapRef } from 'vue'
|
import { UnwrapRef } from 'vue'
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ async function registerVpnServiceListener() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRoutesForVpn(routes: Route[]): string[] {
|
function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfig): string[] {
|
||||||
if (!routes) {
|
if (!routes) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -108,6 +108,10 @@ function getRoutesForVpn(routes: Route[]): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node_config.routes.forEach(r => {
|
||||||
|
ret.push(r)
|
||||||
|
})
|
||||||
|
|
||||||
// sort and dedup
|
// sort and dedup
|
||||||
return Array.from(new Set(ret)).sort()
|
return Array.from(new Set(ret)).sort()
|
||||||
}
|
}
|
||||||
@@ -142,7 +146,7 @@ async function onNetworkInstanceChange() {
|
|||||||
network_length = 24
|
network_length = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = getRoutesForVpn(curNetworkInfo?.routes)
|
const routes = getRoutesForVpn(curNetworkInfo?.routes, networkStore.curNetwork)
|
||||||
|
|
||||||
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
|
||||||
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primeuix/themes/aura';
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import ToastService from 'primevue/toastservice'
|
import ToastService from 'primevue/toastservice'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
export async function loadDockVisibilityAsync(visible: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await invoke('set_dock_visibility', { visible })
|
||||||
|
localStorage.setItem('dock_visibility', JSON.stringify(visible))
|
||||||
|
return visible
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Failed to set dock visibility:', e)
|
||||||
|
return getDockVisibilityStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDockVisibilityStatus(): boolean {
|
||||||
|
const stored = localStorage.getItem('dock_visibility')
|
||||||
|
return stored !== null ? JSON.parse(stored) : true
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from
|
|||||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||||
import { useTray } from '~/composables/tray'
|
import { useTray } from '~/composables/tray'
|
||||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||||
|
import { getDockVisibilityStatus, loadDockVisibilityAsync } from '~/modules/dock_visibility'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@@ -177,6 +178,14 @@ const setting_menu_items = ref([
|
|||||||
await loadAutoLaunchStatusAsync(!getAutoLaunchStatus())
|
await loadAutoLaunchStatusAsync(!getAutoLaunchStatus())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () => getDockVisibilityStatus() ? t('hide_dock_icon') : t('show_dock_icon'),
|
||||||
|
icon: 'pi pi-eye-slash',
|
||||||
|
command: async () => {
|
||||||
|
await loadDockVisibilityAsync(!getDockVisibilityStatus())
|
||||||
|
},
|
||||||
|
visible: () => type() === 'macos',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () => t('logging'),
|
label: () => t('logging'),
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-file',
|
||||||
@@ -283,34 +292,35 @@ async function saveTomlConfig(tomlConfig: string) {
|
|||||||
<About />
|
<About />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<div>
|
<div class="w-full">
|
||||||
<Toolbar>
|
<div class="flex items-center gap-4 p-4 h-20">
|
||||||
<template #start>
|
<!-- 网络按钮 -->
|
||||||
<div class="flex items-center">
|
<div class="flex shrink-0 items-center">
|
||||||
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
|
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" class="hidden md:inline-flex"
|
||||||
|
@click="addNewNetwork" />
|
||||||
|
<Button icon="pi pi-plus" severity="primary" class="md:hidden px-6" @click="addNewNetwork" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #center>
|
<!-- 网络选择 - 占据中间剩余空间 -->
|
||||||
<div class="min-w-40">
|
|
||||||
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
|
||||||
:placeholder="t('select_network')" class="w-full">
|
:placeholder="t('select_network')" class="flex-1 h-full min-w-0">
|
||||||
<template #value="slotProps">
|
<template #value="slotProps">
|
||||||
<div class="flex items-start content-center">
|
<div class="flex items-center content-center min-w-0">
|
||||||
<div class="mr-4 flex-col">
|
<div class="mr-4 flex-col min-w-0 flex-1">
|
||||||
<span>{{ slotProps.value.network_name }}</span>
|
<span class="truncate block"> {{ slotProps.value.network_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
<Tag class="my-auto leading-3 shrink-0"
|
||||||
|
:severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
|
||||||
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #option="slotProps">
|
<template #option="slotProps">
|
||||||
<div class="flex flex-col items-start content-center max-w-full">
|
<div class="flex flex-col items-start content-center max-w-full">
|
||||||
<div class="flex">
|
<div class="flex items-center min-w-0 w-full">
|
||||||
<div class="mr-4">
|
<div class="mr-4 min-w-0 flex-1">
|
||||||
{{ t('network_name') }}: {{ slotProps.option.network_name }}
|
<span class="truncate block">{{ t('network_name') }}: {{ slotProps.option.network_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tag class="my-auto leading-3"
|
<Tag class="my-auto leading-3 shrink-0"
|
||||||
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
|
||||||
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
|
||||||
</div>
|
</div>
|
||||||
@@ -329,15 +339,16 @@ async function saveTomlConfig(tomlConfig: string) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #end>
|
<!-- 设置按钮 -->
|
||||||
|
<div class="flex items-center shrink-0">
|
||||||
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
|
||||||
|
class="hidden md:inline-flex" aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||||
|
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" class="md:hidden px-6"
|
||||||
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
|
||||||
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
|
||||||
</template>
|
</div>
|
||||||
</Toolbar>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Panel class="h-full overflow-y-auto">
|
<Panel class="h-full overflow-y-auto">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "easytier-web"
|
name = "easytier-web"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||||
|
|
||||||
|
|||||||
@@ -18,18 +18,19 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "4.3.3",
|
"@primeuix/themes": "^1.2.3",
|
||||||
"@vueuse/core": "^11.1.0",
|
"@vueuse/core": "^11.1.0",
|
||||||
"aura": "link:@primevue\\themes\\aura",
|
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"chart.js": "^4.5.0",
|
||||||
"floating-vue": "^5.2",
|
"floating-vue": "^5.2",
|
||||||
"ip-num": "1.5.1",
|
"ip-num": "1.5.1",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "4.3.3",
|
"primevue": "^4.3.9",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"ts-md5": "^1.3.1",
|
"ts-md5": "^1.3.1",
|
||||||
"uuid": "^11.0.2",
|
"uuid": "^11.0.2",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-i18n": "^10.0.4"
|
"vue-i18n": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const bool_flags: BoolFlag[] = [
|
|||||||
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const portForwardProtocolOptions = ref(["tcp","udp"]);
|
const portForwardProtocolOptions = ref(["tcp", "udp"]);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
|||||||
<div class="frontend-lib">
|
<div class="frontend-lib">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="w-11/12 self-center ">
|
<div class="w-full self-center ">
|
||||||
<Panel :header="t('basic_settings')">
|
<Panel :header="t('basic_settings')">
|
||||||
<div class="flex flex-col gap-y-2">
|
<div class="flex flex-col gap-y-2">
|
||||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||||
@@ -227,9 +227,8 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
|||||||
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||||
|
|
||||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions" class="grow"
|
||||||
class="grow" dropdown :complete-on-focus="false"
|
dropdown :complete-on-focus="false" @complete="searchPresetPublicServers" />
|
||||||
@complete="searchPresetPublicServers" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -436,56 +435,36 @@ const portForwardProtocolOptions = ref(["tcp","udp"]);
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="(row, index) in curNetwork.port_forwards" class="form-row">
|
<div v-for="(row, index) in curNetwork.port_forwards" class="form-row">
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||||
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false"/>
|
<SelectButton v-model="row.proto" :options="portForwardProtocolOptions" :allow-empty="false" />
|
||||||
<div style="flex-grow: 4;">
|
<div style="flex-grow: 4;">
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputText
|
<InputText v-model="row.bind_ip" :placeholder="t('port_forwards_bind_addr')" />
|
||||||
v-model="row.bind_ip"
|
|
||||||
:placeholder="t('port_forwards_bind_addr')"
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span style="font-weight: bold">:</span>
|
<span style="font-weight: bold">:</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
<InputNumber v-model="row.bind_port" :format="false"
|
<InputNumber v-model="row.bind_port" :format="false" inputId="horizontal-buttons" :step="1"
|
||||||
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1"
|
mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
|
||||||
:max="65535" fluid
|
|
||||||
class="max-w-20"/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow: 4;">
|
<div style="flex-grow: 4;">
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputText
|
<InputText v-model="row.dst_ip" :placeholder="t('port_forwards_dst_addr')" />
|
||||||
v-model="row.dst_ip"
|
|
||||||
:placeholder="t('port_forwards_dst_addr')"
|
|
||||||
/>
|
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<span style="font-weight: bold">:</span>
|
<span style="font-weight: bold">:</span>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
<InputNumber v-model="row.dst_port" :format="false"
|
<InputNumber v-model="row.dst_port" :format="false" inputId="horizontal-buttons" :step="1"
|
||||||
inputId="horizontal-buttons" :step="1" mode="decimal" :min="1"
|
mode="decimal" :min="1" :max="65535" fluid class="max-w-20" />
|
||||||
:max="65535" fluid
|
|
||||||
class="max-w-20"/>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<Button
|
<Button v-if="curNetwork.port_forwards.length > 0" icon="pi pi-trash" severity="danger" text
|
||||||
v-if="curNetwork.port_forwards.length > 0"
|
rounded @click="removeRow(index, curNetwork.port_forwards)" />
|
||||||
icon="pi pi-trash"
|
|
||||||
severity="danger"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
@click="removeRow(index,curNetwork.port_forwards)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-content-end mt-4">
|
<div class="flex justify-content-end mt-4">
|
||||||
<Button
|
<Button icon="pi pi-plus" :label="t('port_forwards_add_btn')" severity="success"
|
||||||
icon="pi pi-plus"
|
@click="addRow(curNetwork.port_forwards)" />
|
||||||
:label="t('port_forwards_add_btn')"
|
|
||||||
severity="success"
|
|
||||||
@click="addRow(curNetwork.port_forwards)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-blue-900/20 dark:to-indigo-800/20 rounded-xl p-4 border border-blue-200 dark:border-blue-700 shadow-md hover:shadow-lg transition-all duration-300">
|
||||||
|
<div class="flex items-center justify-center mb-3">
|
||||||
|
<div class="flex gap-2 text-sm">
|
||||||
|
<span class="flex items-center gap-1 w-32">
|
||||||
|
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
|
<span class="text-green-600 dark:text-green-400 truncate">{{ t('upload') }}: {{ currentUpload }}/s</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1 w-32">
|
||||||
|
<div class="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 truncate">{{ t('download') }}: {{ currentDownload }}/s</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-32">
|
||||||
|
<canvas ref="chartCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
LineController,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler
|
||||||
|
} from 'chart.js'
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 注册Chart.js组件
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
LineController,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
Filler
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
uploadRate: string
|
||||||
|
downloadRate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const chartCanvas = ref<HTMLCanvasElement>()
|
||||||
|
let chart: ChartJS | null = null
|
||||||
|
let updateTimer: number | null = null
|
||||||
|
|
||||||
|
// 存储历史数据,最多保存30个数据点(1分钟历史)
|
||||||
|
const maxDataPoints = 120
|
||||||
|
const uploadHistory: number[] = []
|
||||||
|
const downloadHistory: number[] = []
|
||||||
|
const timeLabels: string[] = []
|
||||||
|
|
||||||
|
const currentUpload = ref('0')
|
||||||
|
const currentDownload = ref('0')
|
||||||
|
|
||||||
|
// 将带单位的速率字符串转换为字节数
|
||||||
|
function parseRateToBytes(rateStr: string): number {
|
||||||
|
if (!rateStr || rateStr === '0') return 0
|
||||||
|
|
||||||
|
const match = rateStr.match(/([0-9.]+)\s*([KMGT]?i?B)/i)
|
||||||
|
if (!match) return 0
|
||||||
|
|
||||||
|
const value = parseFloat(match[1])
|
||||||
|
const unit = match[2].toUpperCase()
|
||||||
|
|
||||||
|
const multipliers: { [key: string]: number } = {
|
||||||
|
'B': 1,
|
||||||
|
'KB': 1000,
|
||||||
|
'KIB': 1024,
|
||||||
|
'MB': 1000000,
|
||||||
|
'MIB': 1024 * 1024,
|
||||||
|
'GB': 1000000000,
|
||||||
|
'GIB': 1024 * 1024 * 1024,
|
||||||
|
'TB': 1000000000000,
|
||||||
|
'TIB': 1024 * 1024 * 1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
return value * (multipliers[unit] || 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化字节为可读格式
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes < 1) return bytes.toFixed(1) + ' B'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
function updateData() {
|
||||||
|
const uploadBytes = parseRateToBytes(props.uploadRate)
|
||||||
|
const downloadBytes = parseRateToBytes(props.downloadRate)
|
||||||
|
|
||||||
|
currentUpload.value = formatBytes(uploadBytes)
|
||||||
|
currentDownload.value = formatBytes(downloadBytes)
|
||||||
|
|
||||||
|
// 添加新数据点
|
||||||
|
uploadHistory.push(uploadBytes)
|
||||||
|
downloadHistory.push(downloadBytes)
|
||||||
|
|
||||||
|
// 生成时间标签
|
||||||
|
const now = new Date()
|
||||||
|
const timeStr = now.toLocaleTimeString('zh-CN', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
timeLabels.push(timeStr)
|
||||||
|
|
||||||
|
// 保持数据点数量不超过最大值
|
||||||
|
if (uploadHistory.length > maxDataPoints) {
|
||||||
|
uploadHistory.shift()
|
||||||
|
downloadHistory.shift()
|
||||||
|
timeLabels.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
|
if (chart) {
|
||||||
|
chart.data.labels = timeLabels
|
||||||
|
chart.data.datasets[0].data = uploadHistory
|
||||||
|
chart.data.datasets[1].data = downloadHistory
|
||||||
|
chart.update('none')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
function initChart() {
|
||||||
|
if (!chartCanvas.value) return
|
||||||
|
|
||||||
|
const ctx = chartCanvas.value.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
chart = new ChartJS(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: timeLabels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: t('upload'),
|
||||||
|
data: uploadHistory,
|
||||||
|
borderColor: 'rgb(34, 197, 94)',
|
||||||
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('download'),
|
||||||
|
data: downloadHistory,
|
||||||
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (context: any) {
|
||||||
|
const value = context.parsed.y
|
||||||
|
return `${context.dataset.label}: ${formatBytes(value)}/s`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
display: true,
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 3,
|
||||||
|
font: {
|
||||||
|
size: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
min: 0,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function (value: any) {
|
||||||
|
return formatBytes(value as number)
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: 8
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
duration: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听props变化
|
||||||
|
watch([() => props.uploadRate, () => props.downloadRate], () => {
|
||||||
|
updateData()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// add initial point
|
||||||
|
const now = new Date();
|
||||||
|
for (let i = 0; i < maxDataPoints; i++) {
|
||||||
|
let date = new Date(now.getTime() - (maxDataPoints - i) * 2000)
|
||||||
|
const timeStr = date.toLocaleTimeString(navigator.language, {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
uploadHistory.push(0)
|
||||||
|
downloadHistory.push(0)
|
||||||
|
timeLabels.push(timeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
initChart()
|
||||||
|
updateData()
|
||||||
|
|
||||||
|
// 启动定时器,每2秒更新一次图表
|
||||||
|
updateTimer = window.setInterval(() => {
|
||||||
|
updateData()
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy()
|
||||||
|
}
|
||||||
|
if (updateTimer) {
|
||||||
|
clearInterval(updateTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import { IPv4 } from 'ip-num/IPNumber'
|
import { IPv4 } from 'ip-num/IPNumber'
|
||||||
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
|
import { NetworkInstance, type TunnelInfo, type NodeInfo, type PeerRoutePair } from '../types/network'
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||||
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
import { Badge, DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
|
||||||
|
import NetworkChart from './NetworkChart.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
curNetworkInst: NetworkInstance | null,
|
curNetworkInst: NetworkInstance | null,
|
||||||
@@ -106,8 +107,30 @@ function ipFormat(info: PeerRoutePair) {
|
|||||||
return ip ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : ''
|
return ip ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function oneTunnelProto(tunnel?: TunnelInfo): string {
|
||||||
|
if (!tunnel)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
const local_addr = tunnel.local_addr
|
||||||
|
let isIPv6 = false;
|
||||||
|
if (local_addr?.url) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(local_addr.url, 'http://dummy');
|
||||||
|
// IPv6 addresses in URLs are enclosed in brackets and contain ':'
|
||||||
|
isIPv6 = /^\[.*:.*\]$/.test(urlObj.hostname);
|
||||||
|
} catch (e) {
|
||||||
|
// fallback to original check if URL parsing fails
|
||||||
|
isIPv6 = local_addr.url.indexOf('[') >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isIPv6)
|
||||||
|
return `${tunnel.tunnel_type}6`
|
||||||
|
else
|
||||||
|
return tunnel.tunnel_type
|
||||||
|
}
|
||||||
|
|
||||||
function tunnelProto(info: PeerRoutePair) {
|
function tunnelProto(info: PeerRoutePair) {
|
||||||
return [...new Set(info.peer?.conns.map(c => c.tunnel?.tunnel_type))].join(',')
|
return [...new Set(info.peer?.conns.map(c => oneTunnelProto(c.tunnel)))].join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
const myNodeInfo = computed(() => {
|
const myNodeInfo = computed(() => {
|
||||||
@@ -263,6 +286,10 @@ let prevTxSum = 0
|
|||||||
let prevRxSum = 0
|
let prevRxSum = 0
|
||||||
const txRate = ref('0')
|
const txRate = ref('0')
|
||||||
const rxRate = ref('0')
|
const rxRate = ref('0')
|
||||||
|
|
||||||
|
// 控制节点详细信息chips的显示/隐藏
|
||||||
|
const showNodeDetails = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
rateIntervalId = window.setInterval(() => {
|
rateIntervalId = window.setInterval(() => {
|
||||||
const curTxSum = txGlobalSum()
|
const curTxSum = txGlobalSum()
|
||||||
@@ -343,36 +370,23 @@ function showEventLogs() {
|
|||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="flex w-full flex-col gap-y-5">
|
<div class="flex w-full flex-col gap-y-5">
|
||||||
<div class="m-0 flex flex-row justify-center gap-x-5">
|
<div class="gap-4">
|
||||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
|
<!-- 网络流量图表 -->
|
||||||
<div class="font-bold">
|
<div class="w-full">
|
||||||
{{ t('peer_count') }}
|
<NetworkChart :upload-rate="txRate" :download-rate="rxRate" />
|
||||||
</div>
|
|
||||||
<div class="text-5xl mt-1">
|
|
||||||
{{ peerCount }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
|
<!-- 展开/收起节点详细信息的divider按钮 -->
|
||||||
<div class="font-bold">
|
<div class="w-full">
|
||||||
{{ t('upload') }}
|
<Button @click="showNodeDetails = !showNodeDetails"
|
||||||
</div>
|
:icon="showNodeDetails ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||||
<div class="text-xl mt-2">
|
:label="showNodeDetails ? t('hide_node_details') : t('show_node_details')" severity="secondary" outlined
|
||||||
{{ txRate }}/s
|
class="w-full justify-center" size="small" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
|
<!-- 节点详细信息chips,根据showNodeDetails状态显示/隐藏 -->
|
||||||
<div class="font-bold">
|
<div v-show="showNodeDetails" class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
||||||
{{ t('download') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xl mt-2">
|
|
||||||
{{ rxRate }}/s
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
|
|
||||||
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
|
||||||
class="mr-2 mt-2 text-sm" />
|
class="mr-2 mt-2 text-sm" />
|
||||||
</div>
|
</div>
|
||||||
@@ -389,7 +403,15 @@ function showEventLogs() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('peer_info') }}
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>{{ t('peer_info') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Badge :value="peerCount" severity="info"
|
||||||
|
class="text-lg font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import './style.css'
|
|||||||
|
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import { Config, Status, ConfigEditDialog } from "./components";
|
import { Config, Status, ConfigEditDialog } from "./components";
|
||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primeuix/themes/aura';
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
import I18nUtils from './modules/i18n'
|
import I18nUtils from './modules/i18n'
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ logging_open_dir: 打开日志目录
|
|||||||
logging_copy_dir: 复制日志路径
|
logging_copy_dir: 复制日志路径
|
||||||
disable_auto_launch: 关闭开机自启
|
disable_auto_launch: 关闭开机自启
|
||||||
enable_auto_launch: 开启开机自启
|
enable_auto_launch: 开启开机自启
|
||||||
|
hide_dock_icon: 隐藏 Dock 图标
|
||||||
|
show_dock_icon: 显示 Dock 图标
|
||||||
exit: 退出
|
exit: 退出
|
||||||
chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
|
chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
|
||||||
|
show_node_details: 显示节点详细信息
|
||||||
|
hide_node_details: 隐藏节点详细信息
|
||||||
hostname_placeholder: '留空默认为主机名: {0}'
|
hostname_placeholder: '留空默认为主机名: {0}'
|
||||||
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
|
||||||
off_text: 点击关闭
|
off_text: 点击关闭
|
||||||
@@ -210,6 +214,7 @@ event:
|
|||||||
ConnectionError: 连接错误
|
ConnectionError: 连接错误
|
||||||
Connecting: 正在连接
|
Connecting: 正在连接
|
||||||
ConnectError: 连接错误
|
ConnectError: 连接错误
|
||||||
|
VpnPortalStarted: VPN门户已启动
|
||||||
VpnPortalClientConnected: VPN门户客户端已连接
|
VpnPortalClientConnected: VPN门户客户端已连接
|
||||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||||
|
|||||||
@@ -44,8 +44,12 @@ logging_open_dir: Open Log Directory
|
|||||||
logging_copy_dir: Copy Log Path
|
logging_copy_dir: Copy Log Path
|
||||||
disable_auto_launch: Disable Launch on Reboot
|
disable_auto_launch: Disable Launch on Reboot
|
||||||
enable_auto_launch: Enable Launch on Reboot
|
enable_auto_launch: Enable Launch on Reboot
|
||||||
|
hide_dock_icon: Hide Dock Icon
|
||||||
|
show_dock_icon: Show Dock Icon
|
||||||
exit: Exit
|
exit: Exit
|
||||||
use_latency_first: Latency First Mode
|
use_latency_first: Latency First Mode
|
||||||
|
show_node_details: Show Node Details
|
||||||
|
hide_node_details: Hide Node Details
|
||||||
chips_placeholder: 'e.g: {0}, select from the dropdown after input'
|
chips_placeholder: 'e.g: {0}, select from the dropdown after input'
|
||||||
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
hostname_placeholder: 'Leave blank and default to host name: {0}'
|
||||||
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
|
||||||
@@ -210,6 +214,7 @@ event:
|
|||||||
ConnectionError: ConnectionError
|
ConnectionError: ConnectionError
|
||||||
Connecting: Connecting
|
Connecting: Connecting
|
||||||
ConnectError: ConnectError
|
ConnectError: ConnectError
|
||||||
|
VpnPortalStarted: VpnPortalStarted
|
||||||
VpnPortalClientConnected: VpnPortalClientConnected
|
VpnPortalClientConnected: VpnPortalClientConnected
|
||||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||||
DhcpIpv4Changed: DhcpIpv4Changed
|
DhcpIpv4Changed: DhcpIpv4Changed
|
||||||
|
|||||||
@@ -246,10 +246,14 @@ export interface PeerRoutePair {
|
|||||||
peer?: PeerInfo
|
peer?: PeerInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UrlPb {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface TunnelInfo {
|
export interface TunnelInfo {
|
||||||
tunnel_type: string
|
tunnel_type: string
|
||||||
local_addr: string
|
local_addr: UrlPb
|
||||||
remote_addr: string
|
remote_addr: UrlPb
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerConnStats {
|
export interface PeerConnStats {
|
||||||
@@ -302,6 +306,7 @@ export enum EventType {
|
|||||||
Connecting = 'Connecting', // any
|
Connecting = 'Connecting', // any
|
||||||
ConnectError = 'ConnectError', // string, string, string
|
ConnectError = 'ConnectError', // string, string, string
|
||||||
|
|
||||||
|
VpnPortalStarted = 'VpnPortalStarted', // string
|
||||||
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
||||||
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primevue/themes": "4.3.3",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"aura": "link:@primevue/themes/aura",
|
"@primeuix/themes": "^1.2.3",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"easytier-frontend-lib": "workspace:*",
|
"easytier-frontend-lib": "workspace:*",
|
||||||
"primevue": "4.3.3",
|
"primevue": "^4.3.9",
|
||||||
"tailwindcss-primeui": "^0.3.4",
|
"tailwindcss-primeui": "^0.3.4",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "4",
|
|
||||||
"vue-i18n": "^9.9.1",
|
"vue-i18n": "^9.9.1",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0"
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@primevue/auto-import-resolver": "4.3.9",
|
||||||
"@types/node": "^22.8.6",
|
"@types/node": "^22.8.6",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||||
import {computed, ref} from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Api } from 'easytier-frontend-lib'
|
import { Api } from 'easytier-frontend-lib'
|
||||||
import {AutoComplete, Divider, Button, Textarea} from "primevue";
|
import { AutoComplete, Divider, Button, Textarea } from "primevue";
|
||||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||||
|
|
||||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ const parseConfig = async () => {
|
|||||||
<div class="sm:block md:flex w-full">
|
<div class="sm:block md:flex w-full">
|
||||||
<div class="sm:w-full md:w-1/2 p-4">
|
<div class="sm:w-full md:w-1/2 p-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="w-11/12 self-center ">
|
<div class="w-full self-center ">
|
||||||
<label>ApiHost</label>
|
<label>ApiHost</label>
|
||||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||||
@complete="apiHostSearch" class="w-full" />
|
@complete="apiHostSearch" class="w-full" />
|
||||||
@@ -78,13 +78,11 @@ const parseConfig = async () => {
|
|||||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||||
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
<pre v-if="errorMessage"
|
||||||
<Textarea
|
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||||
v-model="toml_config"
|
<Textarea v-model="toml_config" spellcheck="false"
|
||||||
spellcheck="false"
|
|
||||||
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
|
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
|
||||||
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
|
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"></Textarea>
|
||||||
></Textarea>
|
|
||||||
<div class="mt-3 flex justify-center">
|
<div class="mt-3 flex justify-center">
|
||||||
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import './style.css'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import EasytierFrontendLib from 'easytier-frontend-lib'
|
import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primeuix/themes/aura';
|
||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
import { I18nUtils } from 'easytier-frontend-lib'
|
import { I18nUtils } from 'easytier-frontend-lib'
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ impl LoggingConfigLoader for &Cli {
|
|||||||
dir: self.file_log_dir.clone(),
|
dir: self.file_log_dir.clone(),
|
||||||
level: self.file_log_level.clone(),
|
level: self.file_log_level.clone(),
|
||||||
file: None,
|
file: None,
|
||||||
|
size_mb: None,
|
||||||
|
count: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -3,7 +3,7 @@ name = "easytier"
|
|||||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||||
homepage = "https://github.com/EasyTier/EasyTier"
|
homepage = "https://github.com/EasyTier/EasyTier"
|
||||||
repository = "https://github.com/EasyTier/EasyTier"
|
repository = "https://github.com/EasyTier/EasyTier"
|
||||||
version = "2.4.3"
|
version = "2.4.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["kkrainbow"]
|
authors = ["kkrainbow"]
|
||||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||||
@@ -36,7 +36,6 @@ tracing-subscriber = { version = "0.3", features = [
|
|||||||
"local-time",
|
"local-time",
|
||||||
"time",
|
"time",
|
||||||
] }
|
] }
|
||||||
tracing-appender = "0.2.3"
|
|
||||||
console-subscriber = { version = "0.4.1", optional = true }
|
console-subscriber = { version = "0.4.1", optional = true }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
auto_impl = "1.1.0"
|
auto_impl = "1.1.0"
|
||||||
@@ -164,7 +163,7 @@ mimalloc = { version = "*", optional = true }
|
|||||||
# mips
|
# mips
|
||||||
atomic-shim = "0.2.0"
|
atomic-shim = "0.2.0"
|
||||||
|
|
||||||
smoltcp = { version = "0.12.0", optional = true, default-features = false, features = [
|
smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a68bc88d5512afefa7529c5ecdade4ea", optional = true, default-features = false, features = [
|
||||||
"std",
|
"std",
|
||||||
"medium-ip",
|
"medium-ip",
|
||||||
"proto-ipv4",
|
"proto-ipv4",
|
||||||
@@ -238,6 +237,7 @@ windows = { version = "0.52.0", features = [
|
|||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_Networking",
|
"Win32_Networking",
|
||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
|
"Win32_System_Variant",
|
||||||
"Win32_Networking_WinSock",
|
"Win32_Networking_WinSock",
|
||||||
"Win32_System_IO",
|
"Win32_System_IO",
|
||||||
] }
|
] }
|
||||||
@@ -258,11 +258,17 @@ jemallocator = { package = "tikv-jemallocator", version = "0.6.0", optional = tr
|
|||||||
] }
|
] }
|
||||||
jemalloc-ctl = { package = "tikv-jemalloc-ctl", version = "0.6.0", optional = true, features = [
|
jemalloc-ctl = { package = "tikv-jemalloc-ctl", version = "0.6.0", optional = true, features = [
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||||
jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||||
"background_threads_runtime_support",
|
"background_threads_runtime_support",
|
||||||
"background_threads",
|
"background_threads",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = "0.12"
|
tonic-build = "0.12"
|
||||||
globwalk = "0.8.1"
|
globwalk = "0.8.1"
|
||||||
@@ -290,6 +296,7 @@ serial_test = "3.0.0"
|
|||||||
rstest = "0.25.0"
|
rstest = "0.25.0"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
|
tempfile = "3.22.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||||
defguard_wireguard_rs = "0.4.2"
|
defguard_wireguard_rs = "0.4.2"
|
||||||
|
|||||||
@@ -208,6 +208,21 @@ core_clap:
|
|||||||
enable_relay_foreign_network_kcp:
|
enable_relay_foreign_network_kcp:
|
||||||
en: "if true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets)"
|
en: "if true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets)"
|
||||||
zh-CN: "如果为true,则作为共享节点时也可以转发其他网络的 KCP 数据包。默认值为false(不转发)"
|
zh-CN: "如果为true,则作为共享节点时也可以转发其他网络的 KCP 数据包。默认值为false(不转发)"
|
||||||
|
stun_servers:
|
||||||
|
en: "Override default STUN servers; If configured but empty, STUN servers are not used"
|
||||||
|
zh-CN: "覆盖内置的默认 STUN server 列表;如果设置了但是为空,则不使用 STUN servers;如果没设置,则使用默认 STUN server 列表"
|
||||||
|
stun_servers_v6:
|
||||||
|
en: "Override default STUN servers, IPv6; If configured but empty, IPv6 STUN servers are not used"
|
||||||
|
zh-CN: "覆盖内置的默认 IPv6 STUN server 列表;如果设置了但是为空,则不使用 IPv6 STUN servers;如果没设置,则使用默认 IPv6 STUN server 列表"
|
||||||
|
check_config:
|
||||||
|
en: Check config validity without starting the network
|
||||||
|
zh-CN: 检查配置文件的有效性并退出
|
||||||
|
file_log_size_mb:
|
||||||
|
en: "per file log size in MB, default is 100MB"
|
||||||
|
zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB"
|
||||||
|
file_log_count:
|
||||||
|
en: "max file log count, default is 10"
|
||||||
|
zh-CN: "最大文件日志数量,默认值为 10"
|
||||||
|
|
||||||
core_app:
|
core_app:
|
||||||
panic_backtrace_save:
|
panic_backtrace_save:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{io, net::SocketAddr, os::windows::io::AsRawSocket};
|
use std::{io, mem::ManuallyDrop, net::SocketAddr, os::windows::io::AsRawSocket};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use network_interface::NetworkInterfaceConfig;
|
use network_interface::NetworkInterfaceConfig;
|
||||||
@@ -18,6 +18,8 @@ use windows::{
|
|||||||
System::Com::{
|
System::Com::{
|
||||||
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
|
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
|
||||||
},
|
},
|
||||||
|
System::Ole::{SafeArrayCreateVector, SafeArrayPutElement},
|
||||||
|
System::Variant::{VARENUM, VARIANT, VT_ARRAY, VT_BSTR, VT_VARIANT},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -247,20 +249,20 @@ pub fn add_interface_to_firewall_allowlist(interface_name: &str) -> anyhow::Resu
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create rules for each protocol type
|
// Create rules for each protocol type
|
||||||
add_protocol_firewall_rules(&policy, interface_name, "TCP", 6)?; // TCP protocol number 6
|
add_protocol_firewall_rules(&policy, interface_name, "TCP", Some(6))?; // TCP protocol number 6
|
||||||
tracing::debug!("Added TCP firewall rules for interface: {}", interface_name);
|
tracing::debug!("Added TCP firewall rules for interface: {}", interface_name);
|
||||||
|
|
||||||
add_protocol_firewall_rules(&policy, interface_name, "UDP", 17)?; // UDP protocol number 17
|
add_protocol_firewall_rules(&policy, interface_name, "UDP", Some(17))?; // UDP protocol number 17
|
||||||
tracing::debug!("Added UDP firewall rules for interface: {}", interface_name);
|
tracing::debug!("Added UDP firewall rules for interface: {}", interface_name);
|
||||||
|
|
||||||
add_protocol_firewall_rules(&policy, interface_name, "ICMP", 1)?; // ICMP protocol number 1
|
add_protocol_firewall_rules(&policy, interface_name, "ICMP", Some(1))?; // ICMP protocol number 1
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Added ICMP firewall rules for interface: {}",
|
"Added ICMP firewall rules for interface: {}",
|
||||||
interface_name
|
interface_name
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add fallback rules for all protocols
|
// Add fallback rules for all protocols
|
||||||
add_all_protocols_firewall_rules(&policy, interface_name)?;
|
add_protocol_firewall_rules(&policy, interface_name, "ALL", None)?;
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Added fallback all-protocols rules for interface: {}",
|
"Added fallback all-protocols rules for interface: {}",
|
||||||
interface_name
|
interface_name
|
||||||
@@ -279,7 +281,7 @@ fn add_protocol_firewall_rules(
|
|||||||
policy: &INetFwPolicy2,
|
policy: &INetFwPolicy2,
|
||||||
interface_name: &str,
|
interface_name: &str,
|
||||||
protocol_name: &str,
|
protocol_name: &str,
|
||||||
protocol_number: i32,
|
protocol_number: Option<i32>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Create rules for both inbound and outbound traffic
|
// Create rules for both inbound and outbound traffic
|
||||||
for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
|
for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
|
||||||
@@ -307,7 +309,9 @@ fn add_protocol_firewall_rules(
|
|||||||
unsafe {
|
unsafe {
|
||||||
rule.SetName(&name_bstr)?;
|
rule.SetName(&name_bstr)?;
|
||||||
rule.SetDescription(&desc_bstr)?;
|
rule.SetDescription(&desc_bstr)?;
|
||||||
|
if let Some(protocol_number) = protocol_number {
|
||||||
rule.SetProtocol(protocol_number)?;
|
rule.SetProtocol(protocol_number)?;
|
||||||
|
}
|
||||||
rule.SetAction(NET_FW_ACTION_ALLOW)?;
|
rule.SetAction(NET_FW_ACTION_ALLOW)?;
|
||||||
|
|
||||||
if is_inbound {
|
if is_inbound {
|
||||||
@@ -322,61 +326,35 @@ fn add_protocol_firewall_rules(
|
|||||||
)?;
|
)?;
|
||||||
rule.SetGrouping(&BSTR::from("EasyTier"))?;
|
rule.SetGrouping(&BSTR::from("EasyTier"))?;
|
||||||
|
|
||||||
// Get rule collection and add new rule
|
// Set the interface for this rule to apply to the specific network interface
|
||||||
let rules = policy.Rules()?;
|
// According to Microsoft docs, interfaces should be represented by their friendly name
|
||||||
rules.Remove(&name_bstr)?; // Remove existing rule with same name first
|
// We need to create a SAFEARRAY of VARIANT strings containing the interface name
|
||||||
rules.Add(&rule)?;
|
let interface_bstr = BSTR::from(interface_name);
|
||||||
}
|
|
||||||
|
// Create a SAFEARRAY containing one interface name
|
||||||
|
let interface_array = SafeArrayCreateVector(VT_VARIANT, 0, 1);
|
||||||
|
if interface_array.is_null() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to create SAFEARRAY"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let index = 0i32;
|
||||||
}
|
let mut variant_interface = VARIANT::default();
|
||||||
|
(*variant_interface.Anonymous.Anonymous).vt = VT_BSTR;
|
||||||
|
(*variant_interface.Anonymous.Anonymous).Anonymous.bstrVal =
|
||||||
|
ManuallyDrop::new(interface_bstr);
|
||||||
|
|
||||||
/// Add fallback rules for all protocols
|
SafeArrayPutElement(
|
||||||
fn add_all_protocols_firewall_rules(
|
interface_array,
|
||||||
policy: &INetFwPolicy2,
|
&index as *const _ as *const i32,
|
||||||
interface_name: &str,
|
&variant_interface as *const _ as *const std::ffi::c_void,
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// Create rules for both inbound and outbound traffic
|
|
||||||
for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
|
|
||||||
// Create firewall rule instance
|
|
||||||
let rule: INetFwRule = unsafe {
|
|
||||||
CoCreateInstance(
|
|
||||||
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
|
|
||||||
None,
|
|
||||||
CLSCTX_ALL,
|
|
||||||
)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let rule_name = format!(
|
|
||||||
"EasyTier {} - All Protocols ({})",
|
|
||||||
interface_name, direction_name
|
|
||||||
);
|
|
||||||
let description = format!(
|
|
||||||
"Allow all protocol traffic on EasyTier interface {}",
|
|
||||||
interface_name
|
|
||||||
);
|
|
||||||
|
|
||||||
let name_bstr = BSTR::from(&rule_name);
|
|
||||||
let desc_bstr = BSTR::from(&description);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
rule.SetName(&name_bstr)?;
|
|
||||||
rule.SetDescription(&desc_bstr)?;
|
|
||||||
// Don't set protocol - allows all protocols by default
|
|
||||||
rule.SetAction(NET_FW_ACTION_ALLOW)?;
|
|
||||||
|
|
||||||
if is_inbound {
|
|
||||||
rule.SetDirection(NET_FW_RULE_DIR_IN)?;
|
|
||||||
} else {
|
|
||||||
rule.SetDirection(NET_FW_RULE_DIR_OUT)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?;
|
|
||||||
rule.SetProfiles(
|
|
||||||
NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0 | NET_FW_PROFILE2_DOMAIN.0,
|
|
||||||
)?;
|
)?;
|
||||||
rule.SetGrouping(&BSTR::from("EasyTier"))?;
|
|
||||||
|
// Create the VARIANT that contains the SAFEARRAY
|
||||||
|
let mut interface_variant = VARIANT::default();
|
||||||
|
(*interface_variant.Anonymous.Anonymous).vt = VARENUM(VT_ARRAY.0 | VT_VARIANT.0);
|
||||||
|
(*interface_variant.Anonymous.Anonymous).Anonymous.parray = interface_array;
|
||||||
|
|
||||||
|
rule.SetInterfaces(interface_variant)?;
|
||||||
|
|
||||||
// Get rule collection and add new rule
|
// Get rule collection and add new rule
|
||||||
let rules = policy.Rules()?;
|
let rules = policy.Rules()?;
|
||||||
@@ -402,8 +380,7 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
|
|||||||
|
|
||||||
let rules = unsafe { policy.Rules()? };
|
let rules = unsafe { policy.Rules()? };
|
||||||
|
|
||||||
// Remove protocol-specific rules
|
for protocol_name in ["TCP", "UDP", "ICMP", "ALL"] {
|
||||||
for protocol_name in ["TCP", "UDP", "ICMP"] {
|
|
||||||
for direction in ["Inbound", "Outbound"] {
|
for direction in ["Inbound", "Outbound"] {
|
||||||
let rule_name = format!(
|
let rule_name = format!(
|
||||||
"EasyTier {} - {} Protocol ({})",
|
"EasyTier {} - {} Protocol ({})",
|
||||||
@@ -416,18 +393,6 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove fallback protocol rules
|
|
||||||
for direction in ["Inbound", "Outbound"] {
|
|
||||||
let rule_name = format!(
|
|
||||||
"EasyTier {} - All Protocols ({})",
|
|
||||||
interface_name, direction
|
|
||||||
);
|
|
||||||
let name_bstr = BSTR::from(&rule_name);
|
|
||||||
unsafe {
|
|
||||||
let _ = rules.Remove(&name_bstr); // Ignore errors, rule might not exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use cidr::IpCidr;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
common::stun::StunInfoCollector,
|
||||||
proto::{
|
proto::{
|
||||||
acl::Acl,
|
acl::Acl,
|
||||||
common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
||||||
@@ -200,8 +201,11 @@ pub trait ConfigLoader: Send + Sync {
|
|||||||
fn get_udp_whitelist(&self) -> Vec<String>;
|
fn get_udp_whitelist(&self) -> Vec<String>;
|
||||||
fn set_udp_whitelist(&self, whitelist: Vec<String>);
|
fn set_udp_whitelist(&self, whitelist: Vec<String>);
|
||||||
|
|
||||||
fn get_stun_servers(&self) -> Vec<String>;
|
fn get_stun_servers(&self) -> Option<Vec<String>>;
|
||||||
fn set_stun_servers(&self, servers: Vec<String>);
|
fn set_stun_servers(&self, servers: Option<Vec<String>>);
|
||||||
|
|
||||||
|
fn get_stun_servers_v6(&self) -> Option<Vec<String>>;
|
||||||
|
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>);
|
||||||
|
|
||||||
fn dump(&self) -> String;
|
fn dump(&self) -> String;
|
||||||
}
|
}
|
||||||
@@ -308,6 +312,8 @@ pub struct FileLoggerConfig {
|
|||||||
pub level: Option<String>,
|
pub level: Option<String>,
|
||||||
pub file: Option<String>,
|
pub file: Option<String>,
|
||||||
pub dir: Option<String>,
|
pub dir: Option<String>,
|
||||||
|
pub size_mb: Option<u64>,
|
||||||
|
pub count: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
|
||||||
@@ -374,7 +380,7 @@ impl From<PortForwardConfig> for PortForwardConfigPb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
netns: Option<String>,
|
netns: Option<String>,
|
||||||
hostname: Option<String>,
|
hostname: Option<String>,
|
||||||
@@ -412,6 +418,7 @@ struct Config {
|
|||||||
tcp_whitelist: Option<Vec<String>>,
|
tcp_whitelist: Option<Vec<String>>,
|
||||||
udp_whitelist: Option<Vec<String>>,
|
udp_whitelist: Option<Vec<String>>,
|
||||||
stun_servers: Option<Vec<String>>,
|
stun_servers: Option<Vec<String>>,
|
||||||
|
stun_servers_v6: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -791,17 +798,20 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
self.config.lock().unwrap().udp_whitelist = Some(whitelist);
|
self.config.lock().unwrap().udp_whitelist = Some(whitelist);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_stun_servers(&self) -> Vec<String> {
|
fn get_stun_servers(&self) -> Option<Vec<String>> {
|
||||||
self.config
|
self.config.lock().unwrap().stun_servers.clone()
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.stun_servers
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_stun_servers(&self, servers: Vec<String>) {
|
fn set_stun_servers(&self, servers: Option<Vec<String>>) {
|
||||||
self.config.lock().unwrap().stun_servers = Some(servers);
|
self.config.lock().unwrap().stun_servers = servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_stun_servers_v6(&self) -> Option<Vec<String>> {
|
||||||
|
self.config.lock().unwrap().stun_servers_v6.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_stun_servers_v6(&self, servers: Option<Vec<String>>) {
|
||||||
|
self.config.lock().unwrap().stun_servers_v6 = servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump(&self) -> String {
|
fn dump(&self) -> String {
|
||||||
@@ -826,6 +836,12 @@ impl ConfigLoader for TomlConfigLoader {
|
|||||||
|
|
||||||
let mut config = self.config.lock().unwrap().clone();
|
let mut config = self.config.lock().unwrap().clone();
|
||||||
config.flags = Some(flag_map);
|
config.flags = Some(flag_map);
|
||||||
|
if config.stun_servers == Some(StunInfoCollector::get_default_servers()) {
|
||||||
|
config.stun_servers = None;
|
||||||
|
}
|
||||||
|
if config.stun_servers_v6 == Some(StunInfoCollector::get_default_servers_v6()) {
|
||||||
|
config.stun_servers_v6 = None;
|
||||||
|
}
|
||||||
toml::to_string_pretty(&config).unwrap()
|
toml::to_string_pretty(&config).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,14 +854,14 @@ pub mod tests {
|
|||||||
fn test_stun_servers_config() {
|
fn test_stun_servers_config() {
|
||||||
let config = TomlConfigLoader::default();
|
let config = TomlConfigLoader::default();
|
||||||
let stun_servers = config.get_stun_servers();
|
let stun_servers = config.get_stun_servers();
|
||||||
assert!(stun_servers.is_empty());
|
assert!(stun_servers.is_none());
|
||||||
|
|
||||||
// Test setting custom stun servers
|
// Test setting custom stun servers
|
||||||
let custom_servers = vec!["txt:stun.easytier.cn".to_string()];
|
let custom_servers = vec!["txt:stun.easytier.cn".to_string()];
|
||||||
config.set_stun_servers(custom_servers.clone());
|
config.set_stun_servers(Some(custom_servers.clone()));
|
||||||
|
|
||||||
let retrieved_servers = config.get_stun_servers();
|
let retrieved_servers = config.get_stun_servers();
|
||||||
assert_eq!(retrieved_servers, custom_servers);
|
assert_eq!(retrieved_servers.unwrap(), custom_servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -859,7 +875,7 @@ stun_servers = [
|
|||||||
]"#;
|
]"#;
|
||||||
|
|
||||||
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
let config = TomlConfigLoader::new_from_str(config_str).unwrap();
|
||||||
let stun_servers = config.get_stun_servers();
|
let stun_servers = config.get_stun_servers().unwrap();
|
||||||
|
|
||||||
assert_eq!(stun_servers.len(), 3);
|
assert_eq!(stun_servers.len(), 3);
|
||||||
assert_eq!(stun_servers[0], "stun.l.google.com:19302");
|
assert_eq!(stun_servers[0], "stun.l.google.com:19302");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::{
|
use std::{
|
||||||
hash::Hasher,
|
hash::Hasher,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -43,6 +44,7 @@ pub enum GlobalCtxEvent {
|
|||||||
Connecting(url::Url),
|
Connecting(url::Url),
|
||||||
ConnectError(String, String, String), // (dst, ip version, error message)
|
ConnectError(String, String, String), // (dst, ip version, error message)
|
||||||
|
|
||||||
|
VpnPortalStarted(String), // (portal)
|
||||||
VpnPortalClientConnected(String, String), // (portal, client ip)
|
VpnPortalClientConnected(String, String), // (portal, client ip)
|
||||||
VpnPortalClientDisconnected(String, String), // (portal, client ip)
|
VpnPortalClientDisconnected(String, String), // (portal, client ip)
|
||||||
|
|
||||||
@@ -114,12 +116,21 @@ impl GlobalCtx {
|
|||||||
|
|
||||||
let (event_bus, _) = tokio::sync::broadcast::channel(8);
|
let (event_bus, _) = tokio::sync::broadcast::channel(8);
|
||||||
|
|
||||||
let stun_servers = config_fs.get_stun_servers();
|
let stun_info_collector = StunInfoCollector::new_with_default_servers();
|
||||||
let stun_info_collection = Arc::new(if stun_servers.is_empty() {
|
|
||||||
StunInfoCollector::new_with_default_servers()
|
if let Some(stun_servers) = config_fs.get_stun_servers() {
|
||||||
|
stun_info_collector.set_stun_servers(stun_servers);
|
||||||
} else {
|
} else {
|
||||||
StunInfoCollector::new(stun_servers)
|
stun_info_collector.set_stun_servers(StunInfoCollector::get_default_servers());
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if let Some(stun_servers) = config_fs.get_stun_servers_v6() {
|
||||||
|
stun_info_collector.set_stun_servers_v6(stun_servers);
|
||||||
|
} else {
|
||||||
|
stun_info_collector.set_stun_servers_v6(StunInfoCollector::get_default_servers_v6());
|
||||||
|
}
|
||||||
|
|
||||||
|
let stun_info_collector = Arc::new(stun_info_collector);
|
||||||
|
|
||||||
let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env = "ohos");
|
let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env = "ohos");
|
||||||
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
|
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
|
||||||
@@ -145,12 +156,12 @@ impl GlobalCtx {
|
|||||||
|
|
||||||
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
|
||||||
net_ns,
|
net_ns,
|
||||||
stun_info_collection.clone(),
|
stun_info_collector.clone(),
|
||||||
)))),
|
)))),
|
||||||
|
|
||||||
hostname: Mutex::new(hostname),
|
hostname: Mutex::new(hostname),
|
||||||
|
|
||||||
stun_info_collection: Mutex::new(stun_info_collection),
|
stun_info_collection: Mutex::new(stun_info_collector),
|
||||||
|
|
||||||
running_listeners: Mutex::new(Vec::new()),
|
running_listeners: Mutex::new(Vec::new()),
|
||||||
|
|
||||||
@@ -230,6 +241,13 @@ impl GlobalCtx {
|
|||||||
self.config.get_id()
|
self.config.get_id()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_ip_in_same_network(&self, ip: &IpAddr) -> bool {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.contains(v4)).unwrap_or(false),
|
||||||
|
IpAddr::V6(v6) => self.get_ipv6().map(|x| x.contains(v6)).unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_network_identity(&self) -> NetworkIdentity {
|
pub fn get_network_identity(&self) -> NetworkIdentity {
|
||||||
self.config.get_network_identity()
|
self.config.get_network_identity()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,7 @@ impl IfConfiguerTrait for MacIfConfiger {
|
|||||||
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
|
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
|
||||||
} else {
|
} else {
|
||||||
run_shell_cmd(
|
run_shell_cmd(
|
||||||
format!(
|
format!("ifconfig {} inet {} delete", name, ip.unwrap().address()).as_str(),
|
||||||
"ifconfig {} inet {} delete",
|
|
||||||
name,
|
|
||||||
ip.unwrap().address().to_string()
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub mod stats_manager;
|
|||||||
pub mod stun;
|
pub mod stun;
|
||||||
pub mod stun_codec_ext;
|
pub mod stun_codec_ext;
|
||||||
pub mod token_bucket;
|
pub mod token_bucket;
|
||||||
|
pub mod tracing_rolling_appender;
|
||||||
|
|
||||||
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||||
format: F,
|
format: F,
|
||||||
|
|||||||
@@ -59,18 +59,52 @@ impl InterfaceFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache for networksetup command output
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
static NETWORKSETUP_CACHE: std::sync::OnceLock<Mutex<(String, std::time::Instant)>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||||
impl InterfaceFilter {
|
impl InterfaceFilter {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
async fn is_interface_physical(&self) -> bool {
|
async fn get_networksetup_output() -> String {
|
||||||
let interface_name = &self.iface.name;
|
use anyhow::Context;
|
||||||
let output = tokio::process::Command::new("networksetup")
|
use std::time::{Duration, Instant};
|
||||||
.args(&["-listallhardwareports"])
|
let cache = NETWORKSETUP_CACHE.get_or_init(|| Mutex::new((String::new(), Instant::now())));
|
||||||
|
let mut cache_guard = cache.lock().await;
|
||||||
|
|
||||||
|
// Check if cache is still valid (less than 1 minute old)
|
||||||
|
if cache_guard.1.elapsed() < Duration::from_secs(60) && !cache_guard.0.is_empty() {
|
||||||
|
return cache_guard.0.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache is expired or empty, fetch new data
|
||||||
|
let stdout = tokio::process::Command::new("networksetup")
|
||||||
|
.args(["-listallhardwareports"])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to execute command");
|
.with_context(|| "Failed to execute networksetup command")
|
||||||
|
.and_then(|output| {
|
||||||
|
std::str::from_utf8(&output.stdout)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.with_context(|| "Failed to convert networksetup output to string")
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
tracing::error!("Failed to execute networksetup command: {:?}", e);
|
||||||
|
String::new()
|
||||||
|
});
|
||||||
|
|
||||||
let stdout = std::str::from_utf8(&output.stdout).expect("Invalid UTF-8");
|
// Update cache
|
||||||
|
cache_guard.0 = stdout.clone();
|
||||||
|
cache_guard.1 = Instant::now();
|
||||||
|
|
||||||
|
stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
async fn is_interface_physical(&self) -> bool {
|
||||||
|
let interface_name = &self.iface.name;
|
||||||
|
let stdout = Self::get_networksetup_output().await;
|
||||||
|
|
||||||
let lines: Vec<&str> = stdout.lines().collect();
|
let lines: Vec<&str> = stdout.lines().collect();
|
||||||
|
|
||||||
@@ -79,11 +113,7 @@ impl InterfaceFilter {
|
|||||||
|
|
||||||
if line.contains("Device:") && line.contains(interface_name) {
|
if line.contains("Device:") && line.contains(interface_name) {
|
||||||
let next_line = lines[i + 1];
|
let next_line = lines[i + 1];
|
||||||
if next_line.contains("Virtual Interface") {
|
return !next_line.contains("Virtual Interface");
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -718,10 +718,10 @@ impl StunInfoCollectorTrait for StunInfoCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StunInfoCollector {
|
impl StunInfoCollector {
|
||||||
pub fn new(stun_servers: Vec<String>) -> Self {
|
pub fn new(stun_servers: Vec<String>, stun_servers_v6: Vec<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
stun_servers: Arc::new(RwLock::new(stun_servers)),
|
||||||
stun_servers_v6: Arc::new(RwLock::new(Self::get_default_servers_v6())),
|
stun_servers_v6: Arc::new(RwLock::new(stun_servers_v6)),
|
||||||
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||||
public_ipv6: Arc::new(AtomicCell::new(None)),
|
public_ipv6: Arc::new(AtomicCell::new(None)),
|
||||||
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
||||||
@@ -732,7 +732,17 @@ impl StunInfoCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_default_servers() -> Self {
|
pub fn new_with_default_servers() -> Self {
|
||||||
Self::new(Self::get_default_servers())
|
Self::new(Self::get_default_servers(), Self::get_default_servers_v6())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_stun_servers(&self, stun_servers: Vec<String>) {
|
||||||
|
let mut g = self.stun_servers.write().unwrap();
|
||||||
|
*g = stun_servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_stun_servers_v6(&self, stun_servers_v6: Vec<String>) {
|
||||||
|
let mut g = self.stun_servers_v6.write().unwrap();
|
||||||
|
*g = stun_servers_v6;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_default_servers() -> Vec<String> {
|
pub fn get_default_servers() -> Vec<String> {
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
This module is copied and modified from https://github.com/cavivie/tracing-rolling-file
|
||||||
|
|
||||||
|
tracing-rolling-file is dual-licensed under The MIT License [1] and
|
||||||
|
Apache 2.0 License [2].
|
||||||
|
|
||||||
|
Copyright (c) 2021 Cavivie and contributors.
|
||||||
|
|
||||||
|
This is same as the Rust Project's own license.
|
||||||
|
|
||||||
|
|
||||||
|
[1]: <http://opensource.org/licenses/MIT>, which is reproduced below:
|
||||||
|
|
||||||
|
~~~~
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021, eFolder Inc dba Axcient.
|
||||||
|
|
||||||
|
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.
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
|
||||||
|
[2]: <http://www.apache.org/licenses/LICENSE-2.0>, which is reproduced below:
|
||||||
|
|
||||||
|
~~~~
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
~~~~
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RollingConditionBase {
|
||||||
|
last_write_opt: Option<DateTime<Local>>,
|
||||||
|
frequency_opt: Option<RollingFrequency>,
|
||||||
|
max_size_opt: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingConditionBase {
|
||||||
|
/// Constructs a new struct that does not yet have any condition set.
|
||||||
|
pub fn new() -> RollingConditionBase {
|
||||||
|
RollingConditionBase {
|
||||||
|
last_write_opt: None,
|
||||||
|
frequency_opt: None,
|
||||||
|
max_size_opt: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover on the given frequency
|
||||||
|
pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBase {
|
||||||
|
self.frequency_opt = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when the date changes
|
||||||
|
pub fn daily(mut self) -> RollingConditionBase {
|
||||||
|
self.frequency_opt = Some(RollingFrequency::EveryDay);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when the date or hour changes
|
||||||
|
pub fn hourly(mut self) -> RollingConditionBase {
|
||||||
|
self.frequency_opt = Some(RollingFrequency::EveryHour);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when the date or minute changes
|
||||||
|
pub fn minutely(mut self) -> RollingConditionBase {
|
||||||
|
self.frequency_opt = Some(RollingFrequency::EveryMinute);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when a certain size is reached
|
||||||
|
pub fn max_size(mut self, x: u64) -> RollingConditionBase {
|
||||||
|
self.max_size_opt = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RollingConditionBase {
|
||||||
|
fn default() -> Self {
|
||||||
|
RollingConditionBase::new().frequency(RollingFrequency::EveryDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingCondition for RollingConditionBase {
|
||||||
|
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
|
||||||
|
let mut rollover = false;
|
||||||
|
if let Some(frequency) = self.frequency_opt.as_ref() {
|
||||||
|
if let Some(last_write) = self.last_write_opt.as_ref() {
|
||||||
|
if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
|
||||||
|
rollover = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(max_size) = self.max_size_opt.as_ref() {
|
||||||
|
if current_filesize >= *max_size {
|
||||||
|
rollover = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.last_write_opt = Some(*now);
|
||||||
|
rollover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RollingFileAppenderBaseBuilder {
|
||||||
|
condition: RollingConditionBase,
|
||||||
|
filename: String,
|
||||||
|
max_filecount: usize,
|
||||||
|
current_filesize: u64,
|
||||||
|
writer_opt: Option<BufWriter<File>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RollingFileAppenderBaseBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
RollingFileAppenderBaseBuilder {
|
||||||
|
condition: RollingConditionBase::default(),
|
||||||
|
filename: String::new(),
|
||||||
|
max_filecount: 10,
|
||||||
|
current_filesize: 0,
|
||||||
|
writer_opt: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingFileAppenderBaseBuilder {
|
||||||
|
/// Sets the log filename. Uses absolute path if provided, otherwise
|
||||||
|
/// creates files in the current working directory.
|
||||||
|
pub fn filename(mut self, filename: String) -> Self {
|
||||||
|
self.filename = filename;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition for the maximum number of files to create before rolling
|
||||||
|
/// over and deleting the oldest one.
|
||||||
|
pub fn max_filecount(mut self, max_filecount: usize) -> Self {
|
||||||
|
self.max_filecount = max_filecount;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover on a daily basis
|
||||||
|
pub fn condition_daily(mut self) -> Self {
|
||||||
|
self.condition.frequency_opt = Some(RollingFrequency::EveryDay);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when the date or hour changes
|
||||||
|
pub fn condition_hourly(mut self) -> Self {
|
||||||
|
self.condition.frequency_opt = Some(RollingFrequency::EveryHour);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when the date or minute changes
|
||||||
|
pub fn condition_minutely(mut self) -> Self {
|
||||||
|
self.condition.frequency_opt = Some(RollingFrequency::EveryMinute);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a condition to rollover when a certain size is reached
|
||||||
|
pub fn condition_max_file_size(mut self, x: u64) -> Self {
|
||||||
|
self.condition.max_size_opt = Some(x);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a RollingFileAppenderBase instance from the current settings.
|
||||||
|
///
|
||||||
|
/// Returns an error if the filename is empty.
|
||||||
|
pub fn build(self) -> Result<RollingFileAppenderBase, &'static str> {
|
||||||
|
if self.filename.is_empty() {
|
||||||
|
return Err("A filename is required to be set and can not be blank");
|
||||||
|
}
|
||||||
|
Ok(RollingFileAppenderBase {
|
||||||
|
condition: self.condition,
|
||||||
|
filename: self.filename,
|
||||||
|
max_filecount: self.max_filecount,
|
||||||
|
current_filesize: self.current_filesize,
|
||||||
|
writer_opt: self.writer_opt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingFileAppenderBase {
|
||||||
|
/// Creates a new rolling file appender builder instance with the default
|
||||||
|
/// settings without a filename set.
|
||||||
|
pub fn builder() -> RollingFileAppenderBaseBuilder {
|
||||||
|
RollingFileAppenderBaseBuilder::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rolling file appender with a rolling condition based on date/time or size.
|
||||||
|
pub type RollingFileAppenderBase = RollingFileAppender<RollingConditionBase>;
|
||||||
|
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct Context {
|
||||||
|
_tempdir: tempfile::TempDir,
|
||||||
|
rolling: RollingFileAppenderBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
fn verify_contains(&mut self, needle: &str, n: usize) {
|
||||||
|
self.rolling.flush().unwrap();
|
||||||
|
let p = self.rolling.filename_for(n);
|
||||||
|
let haystack = fs::read_to_string(&p).unwrap();
|
||||||
|
if !haystack.contains(needle) {
|
||||||
|
panic!("file {:?} did not contain expected contents {}", p, needle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_context(condition: RollingConditionBase, max_files: usize) -> Context {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let filename = tempdir.path().join("test.log");
|
||||||
|
let rolling = RollingFileAppenderBase::new(filename, condition, max_files).unwrap();
|
||||||
|
Context {
|
||||||
|
_tempdir: tempdir,
|
||||||
|
rolling,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_builder_context(mut builder: RollingFileAppenderBaseBuilder) -> Context {
|
||||||
|
if builder.filename.is_empty() {
|
||||||
|
builder = builder.filename(String::from("test.log"));
|
||||||
|
}
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let filename = tempdir.path().join(&builder.filename);
|
||||||
|
builder = builder.filename(String::from(filename.as_os_str().to_str().unwrap()));
|
||||||
|
Context {
|
||||||
|
_tempdir: tempdir,
|
||||||
|
rolling: builder.build().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frequency_every_day() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().daily(), 9);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 1\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 2\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 3\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 4\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 5\n",
|
||||||
|
&Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
|
||||||
|
c.verify_contains("Line 1", 3);
|
||||||
|
c.verify_contains("Line 2", 3);
|
||||||
|
c.verify_contains("Line 3", 2);
|
||||||
|
c.verify_contains("Line 4", 1);
|
||||||
|
c.verify_contains("Line 5", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frequency_every_day_limited_files() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().daily(), 2);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 1\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 2\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 3\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 4\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 5\n",
|
||||||
|
&Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
|
||||||
|
c.verify_contains("Line 3", 2);
|
||||||
|
c.verify_contains("Line 4", 1);
|
||||||
|
c.verify_contains("Line 5", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frequency_every_hour() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().hourly(), 9);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 1\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 2\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 2).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 3\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 2, 1, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 4\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 2, 1, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
|
||||||
|
c.verify_contains("Line 1", 2);
|
||||||
|
c.verify_contains("Line 2", 2);
|
||||||
|
c.verify_contains("Line 3", 1);
|
||||||
|
c.verify_contains("Line 4", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frequency_every_minute() {
|
||||||
|
let mut c = build_context(
|
||||||
|
RollingConditionBase::new().frequency(RollingFrequency::EveryMinute),
|
||||||
|
9,
|
||||||
|
);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 1\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 2\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 3\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 4).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 4\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 5\n",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"Line 6\n",
|
||||||
|
&Local.with_ymd_and_hms(2022, 3, 30, 2, 3, 0).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
|
||||||
|
c.verify_contains("Line 1", 3);
|
||||||
|
c.verify_contains("Line 2", 3);
|
||||||
|
c.verify_contains("Line 3", 3);
|
||||||
|
c.verify_contains("Line 4", 2);
|
||||||
|
c.verify_contains("Line 5", 1);
|
||||||
|
c.verify_contains("Line 6", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn max_size() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().max_size(10), 9);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"12345",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"6789",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"abcdefghijkl",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"ZZZ",
|
||||||
|
&Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
|
||||||
|
c.verify_contains("1234567890", 2);
|
||||||
|
c.verify_contains("abcdefghijkl", 1);
|
||||||
|
c.verify_contains("ZZZ", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn max_size_existing() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().max_size(10), 9);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"12345",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// close the file and make sure that it can re-open it, and that it
|
||||||
|
// resets the file size properly.
|
||||||
|
c.rolling.writer_opt.take();
|
||||||
|
c.rolling.current_filesize = 0;
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"6789",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"abcdefghijkl",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"ZZZ",
|
||||||
|
&Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
|
||||||
|
c.verify_contains("1234567890", 2);
|
||||||
|
c.verify_contains("abcdefghijkl", 1);
|
||||||
|
c.verify_contains("ZZZ", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daily_and_max_size() {
|
||||||
|
let mut c = build_context(RollingConditionBase::new().daily().max_size(10), 9);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"12345",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"6789",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"abcdefghijkl",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 3, 3, 3).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"ZZZ",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
|
||||||
|
c.verify_contains("123456789", 2);
|
||||||
|
c.verify_contains("0abcdefghijkl", 1);
|
||||||
|
c.verify_contains("ZZZ", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rolling_file_appender_builder() {
|
||||||
|
let builder = RollingFileAppender::builder();
|
||||||
|
|
||||||
|
let builder = builder.condition_daily().condition_max_file_size(10);
|
||||||
|
let mut c = build_builder_context(builder);
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"abcdefghijklmnop",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
c.rolling
|
||||||
|
.write_with_datetime(
|
||||||
|
b"12345678",
|
||||||
|
&Local.with_ymd_and_hms(2021, 3, 31, 5, 4, 4).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(AsRef::<Path>::as_ref(&c.rolling.filename_for(1)).exists());
|
||||||
|
assert!(Path::new(&c.rolling.filename_for(0)).exists());
|
||||||
|
c.verify_contains("abcdefghijklmnop", 1);
|
||||||
|
c.verify_contains("12345678", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rolling_file_appender_builder_no_filename() {
|
||||||
|
let builder = RollingFileAppender::builder();
|
||||||
|
let appender = builder.condition_daily().build();
|
||||||
|
assert!(appender.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#![deny(warnings)]
|
||||||
|
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
|
fs::{self, File, OpenOptions},
|
||||||
|
io::{self, BufWriter, Write},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Determines when a file should be "rolled over".
|
||||||
|
pub trait RollingCondition {
|
||||||
|
/// Determine and return whether or not the file should be rolled over.
|
||||||
|
fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines how often a file should be rolled over
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RollingFrequency {
|
||||||
|
EveryDay,
|
||||||
|
EveryHour,
|
||||||
|
EveryMinute,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingFrequency {
|
||||||
|
/// Calculates a datetime that will be different if data should be in
|
||||||
|
/// different files.
|
||||||
|
pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
|
||||||
|
let (year, month, day) = (dt.year(), dt.month(), dt.day());
|
||||||
|
let (hour, min, sec) = match self {
|
||||||
|
RollingFrequency::EveryDay => (0, 0, 0),
|
||||||
|
RollingFrequency::EveryHour => (dt.hour(), 0, 0),
|
||||||
|
RollingFrequency::EveryMinute => (dt.hour(), dt.minute(), 0),
|
||||||
|
};
|
||||||
|
Local
|
||||||
|
.with_ymd_and_hms(year, month, day, hour, min, sec)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes data to a file, and "rolls over" to preserve older data in
|
||||||
|
/// a separate set of files. Old files have a Debian-style naming scheme
|
||||||
|
/// where we have base_filename, base_filename.1, ..., base_filename.N
|
||||||
|
/// where N is the maximum number of rollover files to keep.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RollingFileAppender<RC>
|
||||||
|
where
|
||||||
|
RC: RollingCondition,
|
||||||
|
{
|
||||||
|
condition: RC,
|
||||||
|
filename: String,
|
||||||
|
max_filecount: usize,
|
||||||
|
current_filesize: u64,
|
||||||
|
writer_opt: Option<BufWriter<File>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<RC> RollingFileAppender<RC>
|
||||||
|
where
|
||||||
|
RC: RollingCondition,
|
||||||
|
{
|
||||||
|
/// Creates a new rolling file appender with the given condition.
|
||||||
|
/// The filename parent path must already exist.
|
||||||
|
pub fn new(
|
||||||
|
filename: impl AsRef<Path>,
|
||||||
|
condition: RC,
|
||||||
|
max_filecount: usize,
|
||||||
|
) -> io::Result<RollingFileAppender<RC>> {
|
||||||
|
let filename = filename.as_ref().to_str().unwrap().to_string();
|
||||||
|
let mut appender = RollingFileAppender {
|
||||||
|
condition,
|
||||||
|
filename,
|
||||||
|
max_filecount,
|
||||||
|
current_filesize: 0,
|
||||||
|
writer_opt: None,
|
||||||
|
};
|
||||||
|
// Fail if we can't open the file initially...
|
||||||
|
appender.open_writer_if_needed()?;
|
||||||
|
Ok(appender)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the final filename, where n==0 indicates the current file
|
||||||
|
fn filename_for(&self, n: usize) -> String {
|
||||||
|
let f = self.filename.clone();
|
||||||
|
if n > 0 {
|
||||||
|
format!("{}.{}", f, n)
|
||||||
|
} else {
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotates old files to make room for a new one.
|
||||||
|
/// This may result in the deletion of the oldest file
|
||||||
|
fn rotate_files(&mut self) -> io::Result<()> {
|
||||||
|
// ignore any failure removing the oldest file (may not exist)
|
||||||
|
let _ = fs::remove_file(self.filename_for(self.max_filecount.max(1)));
|
||||||
|
let mut r = Ok(());
|
||||||
|
for i in (0..self.max_filecount.max(1)).rev() {
|
||||||
|
let rotate_from = self.filename_for(i);
|
||||||
|
let rotate_to = self.filename_for(i + 1);
|
||||||
|
if let Err(e) = fs::rename(&rotate_from, &rotate_to).or_else(|e| match e.kind() {
|
||||||
|
io::ErrorKind::NotFound => Ok(()),
|
||||||
|
_ => Err(e),
|
||||||
|
}) {
|
||||||
|
// capture the error, but continue the loop,
|
||||||
|
// to maximize ability to rename everything
|
||||||
|
r = Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forces a rollover to happen immediately.
|
||||||
|
pub fn rollover(&mut self) -> io::Result<()> {
|
||||||
|
// Before closing, make sure all data is flushed successfully.
|
||||||
|
self.flush()?;
|
||||||
|
// We must close the current file before rotating files
|
||||||
|
self.writer_opt.take();
|
||||||
|
self.current_filesize = 0;
|
||||||
|
self.rotate_files()?;
|
||||||
|
self.open_writer_if_needed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a writer for the current file.
|
||||||
|
fn open_writer_if_needed(&mut self) -> io::Result<()> {
|
||||||
|
if self.writer_opt.is_none() {
|
||||||
|
let path = self.filename_for(0);
|
||||||
|
let path = Path::new(&path);
|
||||||
|
let mut open_options = OpenOptions::new();
|
||||||
|
open_options.append(true).create(true);
|
||||||
|
let new_file = match open_options.open(path) {
|
||||||
|
Ok(new_file) => new_file,
|
||||||
|
Err(err) => {
|
||||||
|
let Some(parent) = path.parent() else {
|
||||||
|
return Err(err);
|
||||||
|
};
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
open_options.open(path)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.writer_opt = Some(BufWriter::new(new_file));
|
||||||
|
self.current_filesize = path.metadata().map_or(0, |m| m.len());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes data using the given datetime to calculate the rolling condition
|
||||||
|
pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
|
||||||
|
if self.condition.should_rollover(now, self.current_filesize) {
|
||||||
|
if let Err(e) = self.rollover() {
|
||||||
|
// If we can't rollover, just try to continue writing anyway
|
||||||
|
// (better than missing data).
|
||||||
|
// This will likely used to implement logging, so
|
||||||
|
// avoid using log::warn and log to stderr directly
|
||||||
|
eprintln!("WARNING: Failed to rotate logfile {}: {}", self.filename, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.open_writer_if_needed()?;
|
||||||
|
if let Some(writer) = self.writer_opt.as_mut() {
|
||||||
|
let buf_len = buf.len();
|
||||||
|
writer.write_all(buf).map(|_| {
|
||||||
|
self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
|
||||||
|
buf_len
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(io::Error::other("unexpected condition: writer is missing"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<RC> io::Write for RollingFileAppender<RC>
|
||||||
|
where
|
||||||
|
RC: RollingCondition,
|
||||||
|
{
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
let now = Local::now();
|
||||||
|
self.write_with_datetime(buf, &now)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(writer) = self.writer_opt.as_mut() {
|
||||||
|
writer.flush()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileAppenderWrapper {
|
||||||
|
appender: std::sync::Arc<parking_lot::Mutex<RollingFileAppenderBase>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing_subscriber::fmt::MakeWriter<'_> for FileAppenderWrapper {
|
||||||
|
type Writer = FileAppenderWriter;
|
||||||
|
|
||||||
|
fn make_writer(&self) -> Self::Writer {
|
||||||
|
FileAppenderWriter {
|
||||||
|
appender: self.appender.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileAppenderWrapper {
|
||||||
|
pub fn new(appender: RollingFileAppenderBase) -> Self {
|
||||||
|
Self {
|
||||||
|
appender: std::sync::Arc::new(parking_lot::Mutex::new(appender)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileAppenderWriter {
|
||||||
|
appender: std::sync::Arc<parking_lot::Mutex<RollingFileAppenderBase>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::io::Write for FileAppenderWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.appender.lock().write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
self.appender.lock().flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod base;
|
||||||
|
pub use base::*;
|
||||||
+124
-15
@@ -30,16 +30,18 @@ use easytier::{
|
|||||||
cli::{
|
cli::{
|
||||||
list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, AddPortForwardRequest,
|
list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, AddPortForwardRequest,
|
||||||
ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest,
|
ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest,
|
||||||
GetAclStatsRequest, GetPrometheusStatsRequest, GetStatsRequest,
|
GetAclStatsRequest, GetLoggerConfigRequest, GetPrometheusStatsRequest, GetStatsRequest,
|
||||||
GetVpnPortalInfoRequest, GetWhitelistRequest, ListConnectorRequest,
|
GetVpnPortalInfoRequest, GetWhitelistRequest, ListConnectorRequest,
|
||||||
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest,
|
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest,
|
||||||
ListPeerRequest, ListPeerResponse, ListPortForwardRequest, ListRouteRequest,
|
ListPeerRequest, ListPeerResponse, ListPortForwardRequest, ListRouteRequest,
|
||||||
ListRouteResponse, ManageMappedListenerRequest, MappedListenerManageAction,
|
ListRouteResponse, LogLevel, LoggerRpc, LoggerRpcClientFactory,
|
||||||
MappedListenerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
|
ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc,
|
||||||
|
MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
|
||||||
PeerManageRpcClientFactory, PortForwardManageRpc, PortForwardManageRpcClientFactory,
|
PeerManageRpcClientFactory, PortForwardManageRpc, PortForwardManageRpcClientFactory,
|
||||||
RemovePortForwardRequest, SetWhitelistRequest, ShowNodeInfoRequest, StatsRpc,
|
RemovePortForwardRequest, SetLoggerConfigRequest, SetWhitelistRequest,
|
||||||
StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc,
|
ShowNodeInfoRequest, StatsRpc, StatsRpcClientFactory, TcpProxyEntryState,
|
||||||
TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory,
|
TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc,
|
||||||
|
VpnPortalRpcClientFactory,
|
||||||
},
|
},
|
||||||
common::{NatType, SocketType},
|
common::{NatType, SocketType},
|
||||||
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
||||||
@@ -47,7 +49,7 @@ use easytier::{
|
|||||||
rpc_types::controller::BaseController,
|
rpc_types::controller::BaseController,
|
||||||
},
|
},
|
||||||
tunnel::tcp::TcpTunnelConnector,
|
tunnel::tcp::TcpTunnelConnector,
|
||||||
utils::{cost_to_str, float_to_str, PeerRoutePair},
|
utils::{cost_to_str, PeerRoutePair},
|
||||||
};
|
};
|
||||||
|
|
||||||
rust_i18n::i18n!("locales", fallback = "en");
|
rust_i18n::i18n!("locales", fallback = "en");
|
||||||
@@ -105,6 +107,8 @@ enum SubCommand {
|
|||||||
Whitelist(WhitelistArgs),
|
Whitelist(WhitelistArgs),
|
||||||
#[command(about = "show statistics information")]
|
#[command(about = "show statistics information")]
|
||||||
Stats(StatsArgs),
|
Stats(StatsArgs),
|
||||||
|
#[command(about = "manage logger configuration")]
|
||||||
|
Logger(LoggerArgs),
|
||||||
#[command(about = t!("core_clap.generate_completions").to_string())]
|
#[command(about = t!("core_clap.generate_completions").to_string())]
|
||||||
GenAutocomplete { shell: Shell },
|
GenAutocomplete { shell: Shell },
|
||||||
}
|
}
|
||||||
@@ -272,6 +276,23 @@ enum StatsSubCommand {
|
|||||||
Prometheus,
|
Prometheus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
struct LoggerArgs {
|
||||||
|
#[command(subcommand)]
|
||||||
|
sub_command: Option<LoggerSubCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum LoggerSubCommand {
|
||||||
|
/// Get current logger configuration
|
||||||
|
Get,
|
||||||
|
/// Set logger level
|
||||||
|
Set {
|
||||||
|
#[arg(help = "Log level (disabled, error, warning, info, debug, trace)")]
|
||||||
|
level: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
struct ServiceArgs {
|
struct ServiceArgs {
|
||||||
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
|
#[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")]
|
||||||
@@ -443,6 +464,18 @@ impl CommandHandler<'_> {
|
|||||||
.with_context(|| "failed to get stats client")?)
|
.with_context(|| "failed to get stats client")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_logger_client(
|
||||||
|
&self,
|
||||||
|
) -> Result<Box<dyn LoggerRpc<Controller = BaseController>>, Error> {
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.scoped_client::<LoggerRpcClientFactory<BaseController>>("".to_string())
|
||||||
|
.await
|
||||||
|
.with_context(|| "failed to get logger client")?)
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_peers(&self) -> Result<ListPeerResponse, Error> {
|
async fn list_peers(&self) -> Result<ListPeerResponse, Error> {
|
||||||
let client = self.get_peer_manager_client().await?;
|
let client = self.get_peer_manager_client().await?;
|
||||||
let request = ListPeerRequest::default();
|
let request = ListPeerRequest::default();
|
||||||
@@ -484,12 +517,19 @@ impl CommandHandler<'_> {
|
|||||||
ipv4: String,
|
ipv4: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
cost: String,
|
cost: String,
|
||||||
|
#[tabled(rename = "lat(ms)")]
|
||||||
lat_ms: String,
|
lat_ms: String,
|
||||||
|
#[tabled(rename = "loss")]
|
||||||
loss_rate: String,
|
loss_rate: String,
|
||||||
|
#[tabled(rename = "rx")]
|
||||||
rx_bytes: String,
|
rx_bytes: String,
|
||||||
|
#[tabled(rename = "tx")]
|
||||||
tx_bytes: String,
|
tx_bytes: String,
|
||||||
|
#[tabled(rename = "tunnel")]
|
||||||
tunnel_proto: String,
|
tunnel_proto: String,
|
||||||
|
#[tabled(rename = "NAT")]
|
||||||
nat_type: String,
|
nat_type: String,
|
||||||
|
#[tabled(skip)]
|
||||||
id: String,
|
id: String,
|
||||||
version: String,
|
version: String,
|
||||||
}
|
}
|
||||||
@@ -497,6 +537,11 @@ impl CommandHandler<'_> {
|
|||||||
impl From<PeerRoutePair> for PeerTableItem {
|
impl From<PeerRoutePair> for PeerTableItem {
|
||||||
fn from(p: PeerRoutePair) -> Self {
|
fn from(p: PeerRoutePair) -> Self {
|
||||||
let route = p.route.clone().unwrap_or_default();
|
let route = p.route.clone().unwrap_or_default();
|
||||||
|
let lat_ms = if route.cost == 1 {
|
||||||
|
p.get_latency_ms().unwrap_or(0.0)
|
||||||
|
} else {
|
||||||
|
route.path_latency_latency_first() as f64
|
||||||
|
};
|
||||||
PeerTableItem {
|
PeerTableItem {
|
||||||
cidr: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
|
cidr: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
|
||||||
ipv4: route
|
ipv4: route
|
||||||
@@ -506,12 +551,8 @@ impl CommandHandler<'_> {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
hostname: route.hostname.clone(),
|
hostname: route.hostname.clone(),
|
||||||
cost: cost_to_str(route.cost),
|
cost: cost_to_str(route.cost),
|
||||||
lat_ms: if route.cost == 1 {
|
lat_ms: format!("{:.2}", lat_ms),
|
||||||
float_to_str(p.get_latency_ms().unwrap_or(0.0), 3)
|
loss_rate: format!("{:.1}%", p.get_loss_rate().unwrap_or(0.0) * 100.0),
|
||||||
} else {
|
|
||||||
route.path_latency_latency_first().to_string()
|
|
||||||
},
|
|
||||||
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
|
|
||||||
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
|
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||||
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
|
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||||
tunnel_proto: p
|
tunnel_proto: p
|
||||||
@@ -1155,6 +1196,66 @@ impl CommandHandler<'_> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_logger_get(&self) -> Result<(), Error> {
|
||||||
|
let client = self.get_logger_client().await?;
|
||||||
|
let request = GetLoggerConfigRequest {};
|
||||||
|
let response = client
|
||||||
|
.get_logger_config(BaseController::default(), request)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match self.output_format {
|
||||||
|
OutputFormat::Table => {
|
||||||
|
let level_str = match response.level() {
|
||||||
|
LogLevel::Disabled => "disabled",
|
||||||
|
LogLevel::Error => "error",
|
||||||
|
LogLevel::Warning => "warning",
|
||||||
|
LogLevel::Info => "info",
|
||||||
|
LogLevel::Debug => "debug",
|
||||||
|
LogLevel::Trace => "trace",
|
||||||
|
};
|
||||||
|
println!("Current Log Level: {}", level_str);
|
||||||
|
}
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let json = serde_json::to_string_pretty(&response)?;
|
||||||
|
println!("{}", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_logger_set(&self, level: &str) -> Result<(), Error> {
|
||||||
|
let log_level = match level.to_lowercase().as_str() {
|
||||||
|
"disabled" => LogLevel::Disabled,
|
||||||
|
"error" => LogLevel::Error,
|
||||||
|
"warning" => LogLevel::Warning,
|
||||||
|
"info" => LogLevel::Info,
|
||||||
|
"debug" => LogLevel::Debug,
|
||||||
|
"trace" => LogLevel::Trace,
|
||||||
|
_ => return Err(anyhow::anyhow!("Invalid log level: {}. Valid levels are: disabled, error, warning, info, debug, trace", level)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = self.get_logger_client().await?;
|
||||||
|
let request = SetLoggerConfigRequest {
|
||||||
|
level: log_level.into(),
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.set_logger_config(BaseController::default(), request)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match self.output_format {
|
||||||
|
OutputFormat::Table => {
|
||||||
|
println!("Log level successfully set to: {}", level);
|
||||||
|
}
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let json = serde_json::to_string_pretty(&response)?;
|
||||||
|
println!("{}", json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_port_list(ports_str: &str) -> Result<Vec<String>, Error> {
|
fn parse_port_list(ports_str: &str) -> Result<Vec<String>, Error> {
|
||||||
let mut ports = Vec::new();
|
let mut ports = Vec::new();
|
||||||
for port_spec in ports_str.split(',') {
|
for port_spec in ports_str.split(',') {
|
||||||
@@ -1460,7 +1561,7 @@ where
|
|||||||
{
|
{
|
||||||
match format {
|
match format {
|
||||||
OutputFormat::Table => {
|
OutputFormat::Table => {
|
||||||
println!("{}", tabled::Table::new(items).with(Style::modern()));
|
println!("{}", tabled::Table::new(items).with(Style::markdown()));
|
||||||
}
|
}
|
||||||
OutputFormat::Json => {
|
OutputFormat::Json => {
|
||||||
println!("{}", serde_json::to_string_pretty(items)?);
|
println!("{}", serde_json::to_string_pretty(items)?);
|
||||||
@@ -1752,7 +1853,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
builder.push_record(vec![format!("Listener {}", idx).as_str(), l]);
|
builder.push_record(vec![format!("Listener {}", idx).as_str(), l]);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{}", builder.build().with(Style::modern()));
|
println!("{}", builder.build().with(Style::markdown()));
|
||||||
}
|
}
|
||||||
Some(NodeSubCommand::Config) => {
|
Some(NodeSubCommand::Config) => {
|
||||||
println!("{}", node_info.config);
|
println!("{}", node_info.config);
|
||||||
@@ -1988,6 +2089,14 @@ async fn main() -> Result<(), Error> {
|
|||||||
println!("{}", response.prometheus_text);
|
println!("{}", response.prometheus_text);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SubCommand::Logger(logger_args) => match &logger_args.sub_command {
|
||||||
|
Some(LoggerSubCommand::Get) | None => {
|
||||||
|
handler.handle_logger_get().await?;
|
||||||
|
}
|
||||||
|
Some(LoggerSubCommand::Set { level }) => {
|
||||||
|
handler.handle_logger_set(level).await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
SubCommand::GenAutocomplete { shell } => {
|
SubCommand::GenAutocomplete { shell } => {
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
easytier::print_completions(shell, &mut cmd, "easytier-cli");
|
easytier::print_completions(shell, &mut cmd, "easytier-cli");
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use std::{
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cidr::IpCidr;
|
use cidr::IpCidr;
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
|
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::{
|
common::{
|
||||||
@@ -35,6 +34,7 @@ use easytier::{
|
|||||||
utils::{init_logger, setup_panic_handler},
|
utils::{init_logger, setup_panic_handler},
|
||||||
web_client,
|
web_client,
|
||||||
};
|
};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
windows_service::define_windows_service!(ffi_service_main, win_service_main);
|
windows_service::define_windows_service!(ffi_service_main, win_service_main);
|
||||||
@@ -132,6 +132,9 @@ struct Cli {
|
|||||||
|
|
||||||
#[clap(long, help = t!("core_clap.generate_completions").to_string())]
|
#[clap(long, help = t!("core_clap.generate_completions").to_string())]
|
||||||
gen_autocomplete: Option<Shell>,
|
gen_autocomplete: Option<Shell>,
|
||||||
|
|
||||||
|
#[clap(long, help = t!("core_clap.check_config").to_string())]
|
||||||
|
check_config: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -572,6 +575,15 @@ struct NetworkOptions {
|
|||||||
num_args = 0..
|
num_args = 0..
|
||||||
)]
|
)]
|
||||||
stun_servers: Option<Vec<String>>,
|
stun_servers: Option<Vec<String>>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "ET_STUN_SERVERS_V6",
|
||||||
|
value_delimiter = ',',
|
||||||
|
help = t!("core_clap.stun_servers_v6").to_string(),
|
||||||
|
num_args = 0..
|
||||||
|
)]
|
||||||
|
stun_servers_v6: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -596,6 +608,20 @@ struct LoggingOptions {
|
|||||||
help = t!("core_clap.file_log_dir").to_string()
|
help = t!("core_clap.file_log_dir").to_string()
|
||||||
)]
|
)]
|
||||||
file_log_dir: Option<String>,
|
file_log_dir: Option<String>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "ET_FILE_LOG_SIZE",
|
||||||
|
help = t!("core_clap.file_log_size_mb").to_string()
|
||||||
|
)]
|
||||||
|
file_log_size: Option<u64>,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
env = "ET_FILE_LOG_COUNT",
|
||||||
|
help = t!("core_clap.file_log_count").to_string()
|
||||||
|
)]
|
||||||
|
file_log_count: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
rust_i18n::i18n!("locales", fallback = "en");
|
rust_i18n::i18n!("locales", fallback = "en");
|
||||||
@@ -943,9 +969,16 @@ impl NetworkOptions {
|
|||||||
cfg.set_udp_whitelist(old_udp_whitelist);
|
cfg.set_udp_whitelist(old_udp_whitelist);
|
||||||
|
|
||||||
if let Some(stun_servers) = &self.stun_servers {
|
if let Some(stun_servers) = &self.stun_servers {
|
||||||
cfg.set_stun_servers(stun_servers.clone());
|
let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
|
||||||
|
old_stun_servers.extend(stun_servers.iter().cloned());
|
||||||
|
cfg.set_stun_servers(Some(old_stun_servers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
|
||||||
|
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
|
||||||
|
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
|
||||||
|
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -962,6 +995,8 @@ impl LoggingConfigLoader for &LoggingOptions {
|
|||||||
level: self.file_log_level.clone(),
|
level: self.file_log_level.clone(),
|
||||||
dir: self.file_log_dir.clone(),
|
dir: self.file_log_dir.clone(),
|
||||||
file: None,
|
file: None,
|
||||||
|
size_mb: self.file_log_size,
|
||||||
|
count: self.file_log_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1036,6 +1071,18 @@ fn win_service_event_loop(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_cli() -> Cli {
|
||||||
|
let mut cli = Cli::parse();
|
||||||
|
// for --stun-servers="", we want vec![], but clap will give vec![""], hack for that
|
||||||
|
if let Some(stun_servers) = &mut cli.network_options.stun_servers {
|
||||||
|
stun_servers.retain(|s| !s.trim().is_empty());
|
||||||
|
}
|
||||||
|
if let Some(stun_servers_v6) = &mut cli.network_options.stun_servers_v6 {
|
||||||
|
stun_servers_v6.retain(|s| !s.trim().is_empty());
|
||||||
|
}
|
||||||
|
cli
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -1046,7 +1093,7 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
|||||||
|
|
||||||
_ = win_service_set_work_dir(&arg[0]);
|
_ = win_service_set_work_dir(&arg[0]);
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = parse_cli();
|
||||||
|
|
||||||
let stop_notify_send = Arc::new(Notify::new());
|
let stop_notify_send = Arc::new(Notify::new());
|
||||||
let stop_notify_recv = Arc::clone(&stop_notify_send);
|
let stop_notify_recv = Arc::clone(&stop_notify_send);
|
||||||
@@ -1078,7 +1125,7 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||||
init_logger(&cli.logging_options, false)?;
|
init_logger(&cli.logging_options, true)?;
|
||||||
|
|
||||||
if cli.config_server.is_some() {
|
if cli.config_server.is_some() {
|
||||||
set_default_machine_id(cli.machine_id);
|
set_default_machine_id(cli.machine_id);
|
||||||
@@ -1138,8 +1185,15 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
|||||||
if let Some(config_files) = cli.config_file {
|
if let Some(config_files) = cli.config_file {
|
||||||
let config_file_count = config_files.len();
|
let config_file_count = config_files.len();
|
||||||
for config_file in config_files {
|
for config_file in config_files {
|
||||||
let mut cfg = TomlConfigLoader::new(&config_file)
|
let mut cfg = if config_file == PathBuf::from("-") {
|
||||||
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
|
let mut stdin = String::new();
|
||||||
|
_ = tokio::io::stdin().read_to_string(&mut stdin).await?;
|
||||||
|
TomlConfigLoader::new_from_str(stdin.as_str())
|
||||||
|
.with_context(|| "failed to load config from stdin")?
|
||||||
|
} else {
|
||||||
|
TomlConfigLoader::new(&config_file)
|
||||||
|
.with_context(|| format!("failed to load config file: {:?}", config_file))?
|
||||||
|
};
|
||||||
|
|
||||||
if cli.network_options.can_merge(&cfg, config_file_count) {
|
if cli.network_options.can_merge(&cfg, config_file_count) {
|
||||||
cli.network_options.merge_into(&mut cfg).with_context(|| {
|
cli.network_options.merge_into(&mut cfg).with_context(|| {
|
||||||
@@ -1254,12 +1308,24 @@ async fn main() -> ExitCode {
|
|||||||
set_prof_active(true);
|
set_prof_active(true);
|
||||||
let _monitor = std::thread::spawn(memory_monitor);
|
let _monitor = std::thread::spawn(memory_monitor);
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = parse_cli();
|
||||||
|
|
||||||
if let Some(shell) = cli.gen_autocomplete {
|
if let Some(shell) = cli.gen_autocomplete {
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
easytier::print_completions(shell, &mut cmd, "easytier-core");
|
easytier::print_completions(shell, &mut cmd, "easytier-core");
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify configurations
|
||||||
|
if cli.check_config {
|
||||||
|
if let Err(e) = validate_config(&cli).await {
|
||||||
|
eprintln!("Config validation failed: {:?}", e);
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
} else {
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut ret_code = 0;
|
let mut ret_code = 0;
|
||||||
|
|
||||||
if let Err(e) = run_main(cli).await {
|
if let Err(e) = run_main(cli).await {
|
||||||
@@ -1274,3 +1340,25 @@ async fn main() -> ExitCode {
|
|||||||
|
|
||||||
ExitCode::from(ret_code)
|
ExitCode::from(ret_code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
|
||||||
|
// Check if config file is provided
|
||||||
|
let config_files = cli
|
||||||
|
.config_file
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("--config-file is required when using --check-config"))?;
|
||||||
|
|
||||||
|
for config_file in config_files {
|
||||||
|
if config_file == &PathBuf::from("-") {
|
||||||
|
let mut stdin = String::new();
|
||||||
|
_ = tokio::io::stdin().read_to_string(&mut stdin).await?;
|
||||||
|
TomlConfigLoader::new_from_str(stdin.as_str())
|
||||||
|
.with_context(|| "config source: stdin")?;
|
||||||
|
} else {
|
||||||
|
TomlConfigLoader::new(config_file)
|
||||||
|
.with_context(|| format!("config source: {:?}", config_file))?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -274,10 +274,12 @@ impl IcmpProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let peer_manager = self.peer_manager.clone();
|
let peer_manager = self.peer_manager.clone();
|
||||||
|
let is_latency_first = self.global_ctx.get_flags().latency_first;
|
||||||
self.tasks.lock().await.spawn(
|
self.tasks.lock().await.spawn(
|
||||||
async move {
|
async move {
|
||||||
while let Some(msg) = receiver.recv().await {
|
while let Some(mut msg) = receiver.recv().await {
|
||||||
let hdr = msg.peer_manager_header().unwrap();
|
let hdr = msg.mut_peer_manager_header().unwrap();
|
||||||
|
hdr.set_latency_first(is_latency_first);
|
||||||
let to_peer_id = hdr.to_peer_id.into();
|
let to_peer_id = hdr.to_peer_id.into();
|
||||||
let Some(pm) = peer_manager.upgrade() else {
|
let Some(pm) = peer_manager.upgrade() else {
|
||||||
tracing::warn!("peer manager is gone, icmp proxy send loop exit");
|
tracing::warn!("peer manager is gone, icmp proxy send loop exit");
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ impl KcpProxyDst {
|
|||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
route: Arc<dyn crate::peers::route_trait::Route + Send + Sync + 'static>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut conn_data = kcp_stream.conn_data().clone();
|
let mut conn_data = kcp_stream.conn_data().clone();
|
||||||
let parsed_conn_data = KcpConnData::decode(&mut conn_data)
|
let parsed_conn_data = KcpConnData::decode(&mut conn_data)
|
||||||
|
|||||||
@@ -252,13 +252,13 @@ pub struct QUICProxyDst {
|
|||||||
endpoint: Arc<quinn::Endpoint>,
|
endpoint: Arc<quinn::Endpoint>,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||||
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
route: Arc<dyn crate::peers::route_trait::Route + Send + Sync + 'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QUICProxyDst {
|
impl QUICProxyDst {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
route: Arc<dyn crate::peers::route_trait::Route + Send + Sync + 'static>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let _g = global_ctx.net_ns.guard();
|
let _g = global_ctx.net_ns.guard();
|
||||||
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
|
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
|
||||||
@@ -324,7 +324,7 @@ impl QUICProxyDst {
|
|||||||
ctx: Arc<GlobalCtx>,
|
ctx: Arc<GlobalCtx>,
|
||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
route: Arc<dyn crate::peers::route_trait::Route + Send + Sync + 'static>,
|
||||||
) {
|
) {
|
||||||
let remote_addr = conn.remote_address();
|
let remote_addr = conn.remote_address();
|
||||||
defer!(
|
defer!(
|
||||||
@@ -368,7 +368,7 @@ impl QUICProxyDst {
|
|||||||
cidr_set: Arc<CidrSet>,
|
cidr_set: Arc<CidrSet>,
|
||||||
proxy_entry_key: SocketAddr,
|
proxy_entry_key: SocketAddr,
|
||||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||||
route: Arc<(dyn crate::peers::route_trait::Route + Send + Sync + 'static)>,
|
route: Arc<dyn crate::peers::route_trait::Route + Send + Sync + 'static>,
|
||||||
) -> Result<(QUICStream, TcpStream, ProxyAclHandler)> {
|
) -> Result<(QUICStream, TcpStream, ProxyAclHandler)> {
|
||||||
let conn = incoming.await.with_context(|| "accept failed")?;
|
let conn = incoming.await.with_context(|| "accept failed")?;
|
||||||
let addr = conn.remote_address();
|
let addr = conn.remote_address();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
sync::{Arc, Weak},
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, Weak,
|
||||||
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,7 +41,7 @@ use tokio::{
|
|||||||
io::{AsyncRead, AsyncWrite},
|
io::{AsyncRead, AsyncWrite},
|
||||||
net::{TcpListener, TcpSocket, UdpSocket},
|
net::{TcpListener, TcpSocket, UdpSocket},
|
||||||
select,
|
select,
|
||||||
sync::{mpsc, Mutex},
|
sync::{mpsc, Mutex, Notify},
|
||||||
task::JoinSet,
|
task::JoinSet,
|
||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
@@ -418,12 +421,21 @@ pub struct Socks5Server {
|
|||||||
|
|
||||||
kcp_endpoint: Mutex<Option<Weak<KcpEndpoint>>>,
|
kcp_endpoint: Mutex<Option<Weak<KcpEndpoint>>>,
|
||||||
|
|
||||||
cancel_tokens: DashMap<PortForwardConfig, DropGuard>,
|
socks5_enabled: Arc<AtomicBool>,
|
||||||
|
cancel_tokens: Arc<DashMap<PortForwardConfig, DropGuard>>,
|
||||||
|
port_forward_list_change_notifier: Arc<Notify>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl PeerPacketFilter for Socks5Server {
|
impl PeerPacketFilter for Socks5Server {
|
||||||
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
async fn try_process_packet_from_peer(&self, packet: ZCPacket) -> Option<ZCPacket> {
|
||||||
|
if self.cancel_tokens.is_empty()
|
||||||
|
&& self.entries.is_empty()
|
||||||
|
&& !self.socks5_enabled.load(Ordering::Relaxed)
|
||||||
|
{
|
||||||
|
return Some(packet);
|
||||||
|
}
|
||||||
|
|
||||||
let hdr = packet.peer_manager_header().unwrap();
|
let hdr = packet.peer_manager_header().unwrap();
|
||||||
if hdr.packet_type != PacketType::Data as u8 {
|
if hdr.packet_type != PacketType::Data as u8 {
|
||||||
return Some(packet);
|
return Some(packet);
|
||||||
@@ -438,7 +450,9 @@ impl PeerPacketFilter for Socks5Server {
|
|||||||
|
|
||||||
let entry_key = match ipv4.get_next_level_protocol() {
|
let entry_key = match ipv4.get_next_level_protocol() {
|
||||||
IpNextHeaderProtocols::Tcp => {
|
IpNextHeaderProtocols::Tcp => {
|
||||||
let tcp_packet = TcpPacket::new(ipv4.payload()).unwrap();
|
let Some(tcp_packet) = TcpPacket::new(ipv4.payload()) else {
|
||||||
|
return Some(packet);
|
||||||
|
};
|
||||||
Socks5Entry {
|
Socks5Entry {
|
||||||
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
|
dst: SocketAddr::new(ipv4.get_source().into(), tcp_packet.get_source()),
|
||||||
src: SocketAddr::new(
|
src: SocketAddr::new(
|
||||||
@@ -467,7 +481,9 @@ impl PeerPacketFilter for Socks5Server {
|
|||||||
return Some(packet);
|
return Some(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
let udp_packet = UdpPacket::new(ipv4.payload()).unwrap();
|
let Some(udp_packet) = UdpPacket::new(ipv4.payload()) else {
|
||||||
|
return Some(packet);
|
||||||
|
};
|
||||||
Socks5Entry {
|
Socks5Entry {
|
||||||
dst: SocketAddr::new(ipv4.get_source().into(), udp_packet.get_source()),
|
dst: SocketAddr::new(ipv4.get_source().into(), udp_packet.get_source()),
|
||||||
src: SocketAddr::new(
|
src: SocketAddr::new(
|
||||||
@@ -519,7 +535,9 @@ impl Socks5Server {
|
|||||||
|
|
||||||
kcp_endpoint: Mutex::new(None),
|
kcp_endpoint: Mutex::new(None),
|
||||||
|
|
||||||
cancel_tokens: DashMap::new(),
|
socks5_enabled: Arc::new(AtomicBool::new(false)),
|
||||||
|
cancel_tokens: Arc::new(DashMap::new()),
|
||||||
|
port_forward_list_change_notifier: Arc::new(Notify::new()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,9 +549,18 @@ impl Socks5Server {
|
|||||||
let entries = self.entries.clone();
|
let entries = self.entries.clone();
|
||||||
let tcp_forward_task = self.tcp_forward_task.clone();
|
let tcp_forward_task = self.tcp_forward_task.clone();
|
||||||
let udp_client_map = self.udp_client_map.clone();
|
let udp_client_map = self.udp_client_map.clone();
|
||||||
|
let cancel_tokens = self.cancel_tokens.clone();
|
||||||
|
let port_forward_list_change_notifier = self.port_forward_list_change_notifier.clone();
|
||||||
|
let socks5_enabled = self.socks5_enabled.clone();
|
||||||
self.tasks.lock().unwrap().spawn(async move {
|
self.tasks.lock().unwrap().spawn(async move {
|
||||||
let mut prev_ipv4 = None;
|
let mut prev_ipv4 = None;
|
||||||
loop {
|
loop {
|
||||||
|
if cancel_tokens.is_empty() && !socks5_enabled.load(Ordering::Relaxed) {
|
||||||
|
let _ = net.lock().await.take();
|
||||||
|
port_forward_list_change_notifier.notified().await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let mut event_recv = global_ctx.subscribe();
|
let mut event_recv = global_ctx.subscribe();
|
||||||
|
|
||||||
let cur_ipv4 = global_ctx.get_ipv4();
|
let cur_ipv4 = global_ctx.get_ipv4();
|
||||||
@@ -570,7 +597,6 @@ impl Socks5Server {
|
|||||||
kcp_endpoint: Option<Weak<KcpEndpoint>>,
|
kcp_endpoint: Option<Weak<KcpEndpoint>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
*self.kcp_endpoint.lock().await = kcp_endpoint;
|
*self.kcp_endpoint.lock().await = kcp_endpoint;
|
||||||
let mut need_start = false;
|
|
||||||
if let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() {
|
if let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() {
|
||||||
let bind_addr = format!(
|
let bind_addr = format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -598,22 +624,18 @@ impl Socks5Server {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.socks5_enabled.store(true, Ordering::Relaxed);
|
||||||
join_joinset_background(self.tasks.clone(), "socks5 server".to_string());
|
join_joinset_background(self.tasks.clone(), "socks5 server".to_string());
|
||||||
|
|
||||||
need_start = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let cfgs = self.global_ctx.config.get_port_forwards();
|
let cfgs = self.global_ctx.config.get_port_forwards();
|
||||||
self.reload_port_forwards(&cfgs).await?;
|
self.reload_port_forwards(&cfgs).await?;
|
||||||
need_start = need_start || !cfgs.is_empty();
|
|
||||||
|
|
||||||
if need_start {
|
|
||||||
self.peer_manager
|
self.peer_manager
|
||||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
self.run_net_update_task().await;
|
self.run_net_update_task().await;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -635,6 +657,7 @@ impl Socks5Server {
|
|||||||
self.add_port_forward(cfg.clone()).await?;
|
self.add_port_forward(cfg.clone()).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.port_forward_list_change_notifier.notify_one();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -436,9 +436,12 @@ impl UdpProxy {
|
|||||||
// forward packets to peer manager
|
// forward packets to peer manager
|
||||||
let mut receiver = self.receiver.lock().await.take().unwrap();
|
let mut receiver = self.receiver.lock().await.take().unwrap();
|
||||||
let peer_manager = self.peer_manager.clone();
|
let peer_manager = self.peer_manager.clone();
|
||||||
|
let is_latency_first = self.global_ctx.get_flags().latency_first;
|
||||||
self.tasks.lock().await.spawn(async move {
|
self.tasks.lock().await.spawn(async move {
|
||||||
while let Ok(msg) = receiver.recv().await {
|
while let Ok(mut msg) = receiver.recv().await {
|
||||||
let to_peer_id: PeerId = msg.peer_manager_header().unwrap().to_peer_id.get();
|
let hdr = msg.mut_peer_manager_header().unwrap();
|
||||||
|
hdr.set_latency_first(is_latency_first);
|
||||||
|
let to_peer_id = hdr.to_peer_id.into();
|
||||||
tracing::trace!(?msg, ?to_peer_id, "udp nat packet response send");
|
tracing::trace!(?msg, ?to_peer_id, "udp nat packet response send");
|
||||||
let ret = peer_manager.send_msg(msg, to_peer_id).await;
|
let ret = peer_manager.send_msg(msg, to_peer_id).await;
|
||||||
if ret.is_err() {
|
if ret.is_err() {
|
||||||
|
|||||||
@@ -990,6 +990,7 @@ impl Instance {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::instance::logger_rpc_service::LoggerRpcService;
|
||||||
use crate::proto::cli::*;
|
use crate::proto::cli::*;
|
||||||
|
|
||||||
let peer_mgr = self.peer_manager.clone();
|
let peer_mgr = self.peer_manager.clone();
|
||||||
@@ -999,6 +1000,7 @@ impl Instance {
|
|||||||
let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service();
|
let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service();
|
||||||
let port_forward_manager_rpc = self.get_port_forward_manager_rpc_service();
|
let port_forward_manager_rpc = self.get_port_forward_manager_rpc_service();
|
||||||
let stats_rpc_service = self.get_stats_rpc_service();
|
let stats_rpc_service = self.get_stats_rpc_service();
|
||||||
|
let logger_rpc_service = LoggerRpcService::new();
|
||||||
|
|
||||||
let s = self.rpc_server.as_mut().unwrap();
|
let s = self.rpc_server.as_mut().unwrap();
|
||||||
let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone());
|
let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone());
|
||||||
@@ -1027,6 +1029,8 @@ impl Instance {
|
|||||||
crate::proto::cli::StatsRpcServer::new(stats_rpc_service),
|
crate::proto::cli::StatsRpcServer::new(stats_rpc_service),
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
s.registry()
|
||||||
|
.register(LoggerRpcServer::new(logger_rpc_service), "");
|
||||||
|
|
||||||
if let Some(ip_proxy) = self.ip_proxy.as_ref() {
|
if let Some(ip_proxy) = self.ip_proxy.as_ref() {
|
||||||
s.registry().register(
|
s.registry().register(
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use std::sync::{mpsc::Sender, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use crate::proto::{
|
||||||
|
cli::{
|
||||||
|
GetLoggerConfigRequest, GetLoggerConfigResponse, LogLevel, LoggerRpc,
|
||||||
|
SetLoggerConfigRequest, SetLoggerConfigResponse,
|
||||||
|
},
|
||||||
|
rpc_types::{self, controller::BaseController},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static LOGGER_LEVEL_SENDER: std::sync::OnceLock<Mutex<Sender<String>>> = OnceLock::new();
|
||||||
|
pub static CURRENT_LOG_LEVEL: std::sync::OnceLock<Mutex<String>> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LoggerRpcService;
|
||||||
|
|
||||||
|
impl LoggerRpcService {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_level_to_string(level: LogLevel) -> String {
|
||||||
|
match level {
|
||||||
|
LogLevel::Disabled => "off".to_string(),
|
||||||
|
LogLevel::Error => "error".to_string(),
|
||||||
|
LogLevel::Warning => "warn".to_string(),
|
||||||
|
LogLevel::Info => "info".to_string(),
|
||||||
|
LogLevel::Debug => "debug".to_string(),
|
||||||
|
LogLevel::Trace => "trace".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_to_log_level(level_str: &str) -> LogLevel {
|
||||||
|
match level_str.to_lowercase().as_str() {
|
||||||
|
"off" | "disabled" => LogLevel::Disabled,
|
||||||
|
"error" => LogLevel::Error,
|
||||||
|
"warn" | "warning" => LogLevel::Warning,
|
||||||
|
"info" => LogLevel::Info,
|
||||||
|
"debug" => LogLevel::Debug,
|
||||||
|
"trace" => LogLevel::Trace,
|
||||||
|
_ => LogLevel::Info, // 默认为 Info 级别
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl LoggerRpc for LoggerRpcService {
|
||||||
|
type Controller = BaseController;
|
||||||
|
|
||||||
|
async fn set_logger_config(
|
||||||
|
&self,
|
||||||
|
_: BaseController,
|
||||||
|
request: SetLoggerConfigRequest,
|
||||||
|
) -> Result<SetLoggerConfigResponse, rpc_types::error::Error> {
|
||||||
|
let level_str = Self::log_level_to_string(request.level());
|
||||||
|
|
||||||
|
// 更新当前日志级别
|
||||||
|
if let Some(current_level) = CURRENT_LOG_LEVEL.get() {
|
||||||
|
if let Ok(mut level) = current_level.lock() {
|
||||||
|
*level = level_str.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送新的日志级别到 logger 重载器
|
||||||
|
if let Some(sender) = LOGGER_LEVEL_SENDER.get() {
|
||||||
|
if let Ok(sender) = sender.lock() {
|
||||||
|
if let Err(e) = sender.send(level_str) {
|
||||||
|
tracing::warn!("Failed to send new log level to reloader: {}", e);
|
||||||
|
return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!(
|
||||||
|
"Failed to update log level: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!(
|
||||||
|
"Logger sender is not available"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!(
|
||||||
|
"Logger reloader is not initialized"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SetLoggerConfigResponse {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_logger_config(
|
||||||
|
&self,
|
||||||
|
_: BaseController,
|
||||||
|
_request: GetLoggerConfigRequest,
|
||||||
|
) -> Result<GetLoggerConfigResponse, rpc_types::error::Error> {
|
||||||
|
let current_level_str = if let Some(current_level) = CURRENT_LOG_LEVEL.get() {
|
||||||
|
if let Ok(level) = current_level.lock() {
|
||||||
|
level.clone()
|
||||||
|
} else {
|
||||||
|
"info".to_string() // 默认级别
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"info".to_string() // 默认级别
|
||||||
|
};
|
||||||
|
|
||||||
|
let level = Self::string_to_log_level(¤t_level_str);
|
||||||
|
|
||||||
|
Ok(GetLoggerConfigResponse {
|
||||||
|
level: level.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,3 +6,5 @@ pub mod listeners;
|
|||||||
|
|
||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
pub mod virtual_nic;
|
pub mod virtual_nic;
|
||||||
|
|
||||||
|
pub mod logger_rpc_service;
|
||||||
|
|||||||
@@ -127,10 +127,7 @@ impl PacketProtocol {
|
|||||||
match self {
|
match self {
|
||||||
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
|
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
|
||||||
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
|
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
|
||||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
PacketProtocol::Other(_) => Err(io::Error::other("neither an IPv4 nor IPv6 packet")),
|
||||||
io::ErrorKind::Other,
|
|
||||||
"neither an IPv4 nor IPv6 packet",
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,7 +901,7 @@ impl NicCtx {
|
|||||||
// remove the 10.0.0.0/24 route (which is added by rust-tun by default)
|
// remove the 10.0.0.0/24 route (which is added by rust-tun by default)
|
||||||
let _ = nic
|
let _ = nic
|
||||||
.ifcfg
|
.ifcfg
|
||||||
.remove_ipv4_route(&nic.ifname(), "10.0.0.0".parse().unwrap(), 24)
|
.remove_ipv4_route(nic.ifname(), "10.0.0.0".parse().unwrap(), 24)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,6 +270,13 @@ fn handle_event(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalCtxEvent::VpnPortalStarted(portal) => {
|
||||||
|
print_event(
|
||||||
|
instance_id,
|
||||||
|
format!("vpn portal started. portal: {}", portal),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => {
|
GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => {
|
||||||
print_event(
|
print_event(
|
||||||
instance_id,
|
instance_id,
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ impl ForeignNetworkEntry {
|
|||||||
flags.disable_relay_kcp = !global_ctx.get_flags().enable_relay_foreign_network_kcp;
|
flags.disable_relay_kcp = !global_ctx.get_flags().enable_relay_foreign_network_kcp;
|
||||||
config.set_flags(flags);
|
config.set_flags(flags);
|
||||||
|
|
||||||
|
config.set_mapped_listeners(Some(global_ctx.config.get_mapped_listeners()));
|
||||||
|
|
||||||
let foreign_global_ctx = Arc::new(GlobalCtx::new(config));
|
let foreign_global_ctx = Arc::new(GlobalCtx::new(config));
|
||||||
foreign_global_ctx
|
foreign_global_ctx
|
||||||
.replace_stun_info_collector(Box::new(global_ctx.get_stun_info_collector().clone()));
|
.replace_stun_info_collector(Box::new(global_ctx.get_stun_info_collector().clone()));
|
||||||
|
|||||||
@@ -1055,10 +1055,19 @@ impl PeerManager {
|
|||||||
|| ipv4_addr.is_multicast()
|
|| ipv4_addr.is_multicast()
|
||||||
|| *ipv4_addr == ipv4_inet.last_address()
|
|| *ipv4_addr == ipv4_inet.last_address()
|
||||||
{
|
{
|
||||||
dst_peers.extend(self.peers.list_routes().await.iter().map(|x| *x.key()));
|
dst_peers.extend(self.peers.list_routes().await.iter().filter_map(|x| {
|
||||||
|
if *x.key() != self.my_peer_id {
|
||||||
|
Some(*x.key())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}));
|
||||||
} else if let Some(peer_id) = self.peers.get_peer_id_by_ipv4(ipv4_addr).await {
|
} else if let Some(peer_id) = self.peers.get_peer_id_by_ipv4(ipv4_addr).await {
|
||||||
dst_peers.push(peer_id);
|
dst_peers.push(peer_id);
|
||||||
} else {
|
} else if !self
|
||||||
|
.global_ctx
|
||||||
|
.is_ip_in_same_network(&std::net::IpAddr::V4(*ipv4_addr))
|
||||||
|
{
|
||||||
for exit_node in &self.exit_nodes {
|
for exit_node in &self.exit_nodes {
|
||||||
let IpAddr::V4(exit_node) = exit_node else {
|
let IpAddr::V4(exit_node) = exit_node else {
|
||||||
continue;
|
continue;
|
||||||
@@ -1072,8 +1081,12 @@ impl PeerManager {
|
|||||||
}
|
}
|
||||||
#[cfg(target_env = "ohos")]
|
#[cfg(target_env = "ohos")]
|
||||||
{
|
{
|
||||||
if dst_peers.is_empty() {
|
if dst_peers.is_empty()
|
||||||
tracing::info!("no peer id for ipv4: {}, set exit_node for ohos", ipv4_addr);
|
&& !self
|
||||||
|
.global_ctx
|
||||||
|
.is_ip_in_same_network(&std::net::IpAddr::V4(*ipv4_addr))
|
||||||
|
{
|
||||||
|
tracing::trace!("no peer id for ipv4: {}, set exit_node for ohos", ipv4_addr);
|
||||||
dst_peers.push(self.my_peer_id.clone());
|
dst_peers.push(self.my_peer_id.clone());
|
||||||
is_exit_node = true;
|
is_exit_node = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -720,15 +720,20 @@ struct NextHopInfo {
|
|||||||
}
|
}
|
||||||
// dst_peer_id -> (next_hop_peer_id, cost, path_len)
|
// dst_peer_id -> (next_hop_peer_id, cost, path_len)
|
||||||
type NextHopMap = DashMap<PeerId, NextHopInfo>;
|
type NextHopMap = DashMap<PeerId, NextHopInfo>;
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct PeerIdAndVersion {
|
||||||
|
peer_id: PeerId,
|
||||||
|
version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
// computed with SyncedRouteInfo. used to get next hop.
|
// computed with SyncedRouteInfo. used to get next hop.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct RouteTable {
|
struct RouteTable {
|
||||||
peer_infos: DashMap<PeerId, RoutePeerInfo>,
|
peer_infos: DashMap<PeerId, RoutePeerInfo>,
|
||||||
next_hop_map: NextHopMap,
|
next_hop_map: NextHopMap,
|
||||||
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerId>,
|
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerIdAndVersion>,
|
||||||
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerId>,
|
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerIdAndVersion>,
|
||||||
cidr_peer_id_map: DashMap<cidr::IpCidr, PeerId>,
|
cidr_peer_id_map: DashMap<cidr::IpCidr, PeerIdAndVersion>,
|
||||||
next_hop_map_version: AtomicVersion,
|
next_hop_map_version: AtomicVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,15 +839,15 @@ impl RouteTable {
|
|||||||
});
|
});
|
||||||
self.ipv4_peer_id_map.retain(|_, v| {
|
self.ipv4_peer_id_map.retain(|_, v| {
|
||||||
// remove ipv4 map for peers we cannot reach.
|
// remove ipv4 map for peers we cannot reach.
|
||||||
self.next_hop_map.contains_key(v)
|
self.next_hop_map.contains_key(&v.peer_id)
|
||||||
});
|
});
|
||||||
self.ipv6_peer_id_map.retain(|_, v| {
|
self.ipv6_peer_id_map.retain(|_, v| {
|
||||||
// remove ipv6 map for peers we cannot reach.
|
// remove ipv6 map for peers we cannot reach.
|
||||||
self.next_hop_map.contains_key(v)
|
self.next_hop_map.contains_key(&v.peer_id)
|
||||||
});
|
});
|
||||||
self.cidr_peer_id_map.retain(|_, v| {
|
self.cidr_peer_id_map.retain(|_, v| {
|
||||||
// remove cidr map for peers we cannot reach.
|
// remove cidr map for peers we cannot reach.
|
||||||
self.next_hop_map.contains_key(v)
|
self.next_hop_map.contains_key(&v.peer_id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,8 +962,19 @@ impl RouteTable {
|
|||||||
|
|
||||||
self.peer_infos.insert(*peer_id, info.clone());
|
self.peer_infos.insert(*peer_id, info.clone());
|
||||||
|
|
||||||
let is_new_peer_better = |old_peer_id: PeerId| -> bool {
|
let peer_id_and_version = PeerIdAndVersion {
|
||||||
let old_next_hop = self.get_next_hop(old_peer_id);
|
peer_id: *peer_id,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_new_peer_better = |old_peer: &PeerIdAndVersion| -> bool {
|
||||||
|
if peer_id_and_version.version > old_peer.version {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if peer_id_and_version.peer_id == old_peer.peer_id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let old_next_hop = self.get_next_hop(old_peer.peer_id);
|
||||||
let new_next_hop = item.value();
|
let new_next_hop = item.value();
|
||||||
old_next_hop.is_none() || new_next_hop.path_len < old_next_hop.unwrap().path_len
|
old_next_hop.is_none() || new_next_hop.path_len < old_next_hop.unwrap().path_len
|
||||||
};
|
};
|
||||||
@@ -967,34 +983,34 @@ impl RouteTable {
|
|||||||
self.ipv4_peer_id_map
|
self.ipv4_peer_id_map
|
||||||
.entry(ipv4_addr.into())
|
.entry(ipv4_addr.into())
|
||||||
.and_modify(|v| {
|
.and_modify(|v| {
|
||||||
if *v != *peer_id && is_new_peer_better(*v) {
|
if is_new_peer_better(v) {
|
||||||
*v = *peer_id;
|
*v = peer_id_and_version;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.or_insert(*peer_id);
|
.or_insert(peer_id_and_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ipv6_addr) = info.ipv6_addr.and_then(|x| x.address) {
|
if let Some(ipv6_addr) = info.ipv6_addr.and_then(|x| x.address) {
|
||||||
self.ipv6_peer_id_map
|
self.ipv6_peer_id_map
|
||||||
.entry(ipv6_addr.into())
|
.entry(ipv6_addr.into())
|
||||||
.and_modify(|v| {
|
.and_modify(|v| {
|
||||||
if *v != *peer_id && is_new_peer_better(*v) {
|
if is_new_peer_better(v) {
|
||||||
*v = *peer_id;
|
*v = peer_id_and_version;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.or_insert(*peer_id);
|
.or_insert(peer_id_and_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
for cidr in info.proxy_cidrs.iter() {
|
for cidr in info.proxy_cidrs.iter() {
|
||||||
self.cidr_peer_id_map
|
self.cidr_peer_id_map
|
||||||
.entry(cidr.parse().unwrap())
|
.entry(cidr.parse().unwrap())
|
||||||
.and_modify(|v| {
|
.and_modify(|v| {
|
||||||
if *v != *peer_id && is_new_peer_better(*v) {
|
if is_new_peer_better(v) {
|
||||||
// if the next hop is not set or the new next hop is better, update it.
|
// if the next hop is not set or the new next hop is better, update it.
|
||||||
*v = *peer_id;
|
*v = peer_id_and_version;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.or_insert(*peer_id);
|
.or_insert(peer_id_and_version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,7 +1020,7 @@ impl RouteTable {
|
|||||||
for item in self.cidr_peer_id_map.iter() {
|
for item in self.cidr_peer_id_map.iter() {
|
||||||
let (k, v) = item.pair();
|
let (k, v) = item.pair();
|
||||||
if k.contains(&ipv4) {
|
if k.contains(&ipv4) {
|
||||||
return Some(*v);
|
return Some(v.peer_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -2376,8 +2392,17 @@ impl Route for PeerRoute {
|
|||||||
|
|
||||||
async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option<PeerId> {
|
async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option<PeerId> {
|
||||||
let route_table = &self.service_impl.route_table;
|
let route_table = &self.service_impl.route_table;
|
||||||
if let Some(peer_id) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
|
if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
|
||||||
return Some(*peer_id);
|
return Some(p.peer_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only get peer id for proxy when the dst ipv4 is not in same network with us
|
||||||
|
if self
|
||||||
|
.global_ctx
|
||||||
|
.is_ip_in_same_network(&std::net::IpAddr::V4(*ipv4_addr))
|
||||||
|
{
|
||||||
|
tracing::trace!(?ipv4_addr, "ipv4 addr is in same network with us");
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(peer_id) = route_table.get_peer_id_for_proxy(ipv4_addr) {
|
if let Some(peer_id) = route_table.get_peer_id_for_proxy(ipv4_addr) {
|
||||||
@@ -2390,8 +2415,8 @@ impl Route for PeerRoute {
|
|||||||
|
|
||||||
async fn get_peer_id_by_ipv6(&self, ipv6_addr: &Ipv6Addr) -> Option<PeerId> {
|
async fn get_peer_id_by_ipv6(&self, ipv6_addr: &Ipv6Addr) -> Option<PeerId> {
|
||||||
let route_table = &self.service_impl.route_table;
|
let route_table = &self.service_impl.route_table;
|
||||||
if let Some(peer_id) = route_table.ipv6_peer_id_map.get(ipv6_addr) {
|
if let Some(p) = route_table.ipv6_peer_id_map.get(ipv6_addr) {
|
||||||
return Some(*peer_id);
|
return Some(p.peer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add proxy support for IPv6 similar to IPv4
|
// TODO: Add proxy support for IPv6 similar to IPv4
|
||||||
@@ -2493,6 +2518,7 @@ mod tests {
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Inet};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use prost_reflect::{DynamicMessage, ReflectMessage};
|
use prost_reflect::{DynamicMessage, ReflectMessage};
|
||||||
|
|
||||||
@@ -2504,7 +2530,7 @@ mod tests {
|
|||||||
peer_manager::{PeerManager, RouteAlgoType},
|
peer_manager::{PeerManager, RouteAlgoType},
|
||||||
peer_ospf_route::PeerRouteServiceImpl,
|
peer_ospf_route::PeerRouteServiceImpl,
|
||||||
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface},
|
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface},
|
||||||
tests::connect_peer_manager,
|
tests::{connect_peer_manager, create_mock_peer_manager},
|
||||||
},
|
},
|
||||||
proto::{
|
proto::{
|
||||||
common::NatType,
|
common::NatType,
|
||||||
@@ -2964,4 +2990,58 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(req, req2);
|
assert_eq!(req, req2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_peer_id_map_override() {
|
||||||
|
let p_a = create_mock_peer_manager().await;
|
||||||
|
let p_b = create_mock_peer_manager().await;
|
||||||
|
let p_c = create_mock_peer_manager().await;
|
||||||
|
|
||||||
|
connect_peer_manager(p_a.clone(), p_b.clone()).await;
|
||||||
|
connect_peer_manager(p_b.clone(), p_c.clone()).await;
|
||||||
|
|
||||||
|
let ip: Ipv4Inet = "10.0.0.1/24".parse().unwrap();
|
||||||
|
let ipv6: Ipv6Inet = "2001:db8::1/64".parse().unwrap();
|
||||||
|
let proxy: Ipv4Cidr = "10.3.0.0/24".parse().unwrap();
|
||||||
|
let check_route_peer_id = async |p: Arc<PeerManager>| {
|
||||||
|
let p = p.clone();
|
||||||
|
wait_for_condition(
|
||||||
|
|| async {
|
||||||
|
p_a.get_route().get_peer_id_by_ipv4(&ip.address()).await == Some(p.my_peer_id())
|
||||||
|
&& p_a.get_route().get_peer_id_by_ipv6(&ipv6.address()).await
|
||||||
|
== Some(p.my_peer_id())
|
||||||
|
&& p_a
|
||||||
|
.get_route()
|
||||||
|
.get_peer_id_by_ipv4(&proxy.first_address())
|
||||||
|
.await
|
||||||
|
== Some(p.my_peer_id())
|
||||||
|
},
|
||||||
|
Duration::from_secs(5),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
|
||||||
|
p_c.get_global_ctx().set_ipv4(Some(ip));
|
||||||
|
p_c.get_global_ctx().set_ipv6(Some(ipv6));
|
||||||
|
p_c.get_global_ctx()
|
||||||
|
.config
|
||||||
|
.add_proxy_cidr(proxy, None)
|
||||||
|
.unwrap();
|
||||||
|
check_route_peer_id(p_c.clone()).await;
|
||||||
|
|
||||||
|
p_b.get_global_ctx().set_ipv4(Some(ip));
|
||||||
|
p_b.get_global_ctx().set_ipv6(Some(ipv6));
|
||||||
|
p_b.get_global_ctx()
|
||||||
|
.config
|
||||||
|
.add_proxy_cidr(proxy, None)
|
||||||
|
.unwrap();
|
||||||
|
check_route_peer_id(p_b.clone()).await;
|
||||||
|
|
||||||
|
p_b.get_global_ctx()
|
||||||
|
.set_ipv4(Some("10.0.0.2/24".parse().unwrap()));
|
||||||
|
p_b.get_global_ctx()
|
||||||
|
.set_ipv6(Some("2001:db8::2/64".parse().unwrap()));
|
||||||
|
p_b.get_global_ctx().config.remove_proxy_cidr(proxy);
|
||||||
|
check_route_peer_id(p_c.clone()).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,3 +325,31 @@ service StatsRpc {
|
|||||||
rpc GetStats(GetStatsRequest) returns (GetStatsResponse);
|
rpc GetStats(GetStatsRequest) returns (GetStatsResponse);
|
||||||
rpc GetPrometheusStats(GetPrometheusStatsRequest) returns (GetPrometheusStatsResponse);
|
rpc GetPrometheusStats(GetPrometheusStatsRequest) returns (GetPrometheusStatsResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LogLevel {
|
||||||
|
DISABLED = 0;
|
||||||
|
ERROR = 1;
|
||||||
|
WARNING = 2;
|
||||||
|
INFO = 3;
|
||||||
|
DEBUG = 4;
|
||||||
|
TRACE = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetLoggerConfigRequest {
|
||||||
|
LogLevel level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetLoggerConfigResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLoggerConfigRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLoggerConfigResponse {
|
||||||
|
LogLevel level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service LoggerRpc {
|
||||||
|
rpc SetLoggerConfig(SetLoggerConfigRequest) returns (SetLoggerConfigResponse);
|
||||||
|
rpc GetLoggerConfig(GetLoggerConfigRequest) returns (GetLoggerConfigResponse);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use url::Host;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/cli.rs"));
|
include!(concat!(env!("OUT_DIR"), "/cli.rs"));
|
||||||
|
|
||||||
impl PeerRoutePair {
|
impl PeerRoutePair {
|
||||||
@@ -70,6 +72,25 @@ impl PeerRoutePair {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_tunnel_ipv6(tunnel_info: &super::common::TunnelInfo) -> bool {
|
||||||
|
let Some(local_addr) = &tunnel_info.local_addr else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let u: url::Url = local_addr.clone().into();
|
||||||
|
u.host()
|
||||||
|
.map(|h| matches!(h, Host::Ipv6(_)))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tunnel_proto_str(tunnel_info: &super::common::TunnelInfo) -> String {
|
||||||
|
if Self::is_tunnel_ipv6(tunnel_info) {
|
||||||
|
format!("{}6", tunnel_info.tunnel_type)
|
||||||
|
} else {
|
||||||
|
tunnel_info.tunnel_type.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
|
pub fn get_conn_protos(&self) -> Option<Vec<String>> {
|
||||||
let mut ret = vec![];
|
let mut ret = vec![];
|
||||||
let p = self.peer.as_ref()?;
|
let p = self.peer.as_ref()?;
|
||||||
@@ -78,8 +99,9 @@ impl PeerRoutePair {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// insert if not exists
|
// insert if not exists
|
||||||
if !ret.contains(&tunnel_info.tunnel_type) {
|
let tunnel_type = Self::get_tunnel_proto_str(tunnel_info);
|
||||||
ret.push(tunnel_info.tunnel_type.clone());
|
if !ret.contains(&tunnel_type) {
|
||||||
|
ret.push(tunnel_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn del_netns(name: &str) {
|
|||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_netns(name: &str, ipv4: &str) {
|
pub fn create_netns(name: &str, ipv4: &str, ipv6: &str) {
|
||||||
// create netns
|
// create netns
|
||||||
let _ = std::process::Command::new("ip")
|
let _ = std::process::Command::new("ip")
|
||||||
.args(["netns", "add", name])
|
.args(["netns", "add", name])
|
||||||
@@ -76,6 +76,7 @@ pub fn create_netns(name: &str, ipv4: &str) {
|
|||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
for ip in [ipv4, ipv6] {
|
||||||
let _ = std::process::Command::new("ip")
|
let _ = std::process::Command::new("ip")
|
||||||
.args([
|
.args([
|
||||||
"netns",
|
"netns",
|
||||||
@@ -84,12 +85,13 @@ pub fn create_netns(name: &str, ipv4: &str) {
|
|||||||
"ip",
|
"ip",
|
||||||
"addr",
|
"addr",
|
||||||
"add",
|
"add",
|
||||||
ipv4,
|
ip,
|
||||||
"dev",
|
"dev",
|
||||||
get_guest_veth_name(name),
|
get_guest_veth_name(name),
|
||||||
])
|
])
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_bridge(name: &str) {
|
pub fn prepare_bridge(name: &str) {
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ pub fn prepare_linux_namespaces() {
|
|||||||
del_netns("net_c");
|
del_netns("net_c");
|
||||||
del_netns("net_d");
|
del_netns("net_d");
|
||||||
|
|
||||||
create_netns("net_a", "10.1.1.1/24");
|
create_netns("net_a", "10.1.1.1/24", "fd11::1/64");
|
||||||
create_netns("net_b", "10.1.1.2/24");
|
create_netns("net_b", "10.1.1.2/24", "fd11::2/64");
|
||||||
create_netns("net_c", "10.1.2.3/24");
|
create_netns("net_c", "10.1.2.3/24", "fd12::3/64");
|
||||||
create_netns("net_d", "10.1.2.4/24");
|
create_netns("net_d", "10.1.2.4/24", "fd12::4/64");
|
||||||
|
|
||||||
prepare_bridge("br_a");
|
prepare_bridge("br_a");
|
||||||
prepare_bridge("br_b");
|
prepare_bridge("br_b");
|
||||||
@@ -931,10 +931,18 @@ fn run_wireguard_client(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "wireguard")]
|
#[cfg(feature = "wireguard")]
|
||||||
|
#[rstest::rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
pub async fn wireguard_vpn_portal() {
|
pub async fn wireguard_vpn_portal(#[values(true, false)] test_v6: bool) {
|
||||||
let mut insts = init_three_node("tcp").await;
|
let mut insts = init_three_node("tcp").await;
|
||||||
|
|
||||||
|
if test_v6 {
|
||||||
|
ping6_test("net_d", "fd12::3", None).await;
|
||||||
|
} else {
|
||||||
|
ping_test("net_d", "10.1.2.3", None).await;
|
||||||
|
}
|
||||||
|
|
||||||
let net_ns = NetNS::new(Some("net_d".into()));
|
let net_ns = NetNS::new(Some("net_d".into()));
|
||||||
let _g = net_ns.guard();
|
let _g = net_ns.guard();
|
||||||
insts[2]
|
insts[2]
|
||||||
@@ -946,11 +954,17 @@ pub async fn wireguard_vpn_portal() {
|
|||||||
});
|
});
|
||||||
insts[2].run_vpn_portal().await.unwrap();
|
insts[2].run_vpn_portal().await.unwrap();
|
||||||
|
|
||||||
|
let dst_socket_addr = if test_v6 {
|
||||||
|
"[fd12::3]:22121".parse().unwrap()
|
||||||
|
} else {
|
||||||
|
"10.1.2.3:22121".parse().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let net_ns = NetNS::new(Some("net_d".into()));
|
let net_ns = NetNS::new(Some("net_d".into()));
|
||||||
let _g = net_ns.guard();
|
let _g = net_ns.guard();
|
||||||
let wg_cfg = get_wg_config_for_portal(&insts[2].get_global_ctx().get_network_identity());
|
let wg_cfg = get_wg_config_for_portal(&insts[2].get_global_ctx().get_network_identity());
|
||||||
run_wireguard_client(
|
run_wireguard_client(
|
||||||
"10.1.2.3:22121".parse().unwrap(),
|
dst_socket_addr,
|
||||||
Key::try_from(wg_cfg.my_public_key()).unwrap(),
|
Key::try_from(wg_cfg.my_public_key()).unwrap(),
|
||||||
Key::try_from(wg_cfg.peer_secret_key()).unwrap(),
|
Key::try_from(wg_cfg.peer_secret_key()).unwrap(),
|
||||||
vec!["10.14.14.0/24".to_string(), "10.144.144.0/24".to_string()],
|
vec!["10.14.14.0/24".to_string(), "10.144.144.0/24".to_string()],
|
||||||
|
|||||||
@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
|
|||||||
unsafe {
|
unsafe {
|
||||||
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
|
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
|
||||||
tracing::warn!(?dev_idx, ?dev_name, "bind device");
|
tracing::warn!(?dev_idx, ?dev_name, "bind device");
|
||||||
|
if bind_addr.is_ipv4() {
|
||||||
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
|
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
|
||||||
|
} else {
|
||||||
|
socket2_socket.bind_device_by_index_v6(std::num::NonZeroU32::new(dev_idx))?;
|
||||||
|
}
|
||||||
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
|
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -629,7 +629,10 @@ impl WgTunnelConnector {
|
|||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
) -> Result<Box<dyn super::Tunnel>, super::TunnelError> {
|
) -> Result<Box<dyn super::Tunnel>, super::TunnelError> {
|
||||||
tracing::warn!("wg connect: {:?}", addr);
|
tracing::warn!("wg connect: {:?}", addr);
|
||||||
let local_addr = udp.local_addr().unwrap().to_string();
|
let local_addr = udp
|
||||||
|
.local_addr()
|
||||||
|
.with_context(|| "Failed to get local addr")?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let mut wg_peer = WgPeer::new(Arc::new(udp), config.clone(), addr);
|
let mut wg_peer = WgPeer::new(Arc::new(udp), config.clone(), addr);
|
||||||
let udp = wg_peer.udp_socket();
|
let udp = wg_peer.udp_socket();
|
||||||
|
|||||||
+29
-10
@@ -6,7 +6,9 @@ use tracing_subscriber::{
|
|||||||
layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
|
layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::{config::LoggingConfigLoader, get_logger_timer_rfc3339};
|
use crate::common::{
|
||||||
|
config::LoggingConfigLoader, get_logger_timer_rfc3339, tracing_rolling_appender::*,
|
||||||
|
};
|
||||||
|
|
||||||
pub type PeerRoutePair = crate::proto::cli::PeerRoutePair;
|
pub type PeerRoutePair = crate::proto::cli::PeerRoutePair;
|
||||||
|
|
||||||
@@ -28,6 +30,8 @@ pub fn init_logger(
|
|||||||
config: impl LoggingConfigLoader,
|
config: impl LoggingConfigLoader,
|
||||||
need_reload: bool,
|
need_reload: bool,
|
||||||
) -> Result<Option<NewFilterSender>, anyhow::Error> {
|
) -> Result<Option<NewFilterSender>, anyhow::Error> {
|
||||||
|
use crate::instance::logger_rpc_service::{CURRENT_LOG_LEVEL, LOGGER_LEVEL_SENDER};
|
||||||
|
|
||||||
let file_config = config.get_file_logger_config();
|
let file_config = config.get_file_logger_config();
|
||||||
let file_level = file_config
|
let file_level = file_config
|
||||||
.level
|
.level
|
||||||
@@ -50,7 +54,12 @@ pub fn init_logger(
|
|||||||
|
|
||||||
if need_reload {
|
if need_reload {
|
||||||
let (sender, recver) = std::sync::mpsc::channel();
|
let (sender, recver) = std::sync::mpsc::channel();
|
||||||
ret_sender = Some(sender);
|
ret_sender = Some(sender.clone());
|
||||||
|
|
||||||
|
// 初始化全局状态
|
||||||
|
let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(sender));
|
||||||
|
let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(file_level.to_string()));
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
println!("Start log filter reloader");
|
println!("Start log filter reloader");
|
||||||
while let Ok(lf) = recver.recv() {
|
while let Ok(lf) = recver.recv() {
|
||||||
@@ -72,15 +81,25 @@ pub fn init_logger(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::Builder::new()
|
let dir = file_config.dir.as_deref().unwrap_or(".");
|
||||||
.rotation(tracing_appender::rolling::Rotation::DAILY)
|
let file = file_config.file.as_deref().unwrap_or("easytier.log");
|
||||||
.max_log_files(5)
|
let path = std::path::Path::new(dir).join(file);
|
||||||
.filename_prefix(file_config.file.unwrap_or("easytier".to_string()))
|
let path_str = path.to_string_lossy().into_owned();
|
||||||
.filename_suffix("log")
|
|
||||||
.build(file_config.dir.unwrap_or("./".to_string()))
|
let builder = RollingFileAppenderBase::builder();
|
||||||
.with_context(|| "failed to initialize rolling file appender")?;
|
let file_appender = builder
|
||||||
|
.filename(path_str)
|
||||||
|
.condition_daily()
|
||||||
|
.max_filecount(file_config.count.unwrap_or(10))
|
||||||
|
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let wrapper = FileAppenderWrapper::new(file_appender);
|
||||||
|
|
||||||
|
// Create a simple wrapper that implements MakeWriter
|
||||||
file_layer = Some(
|
file_layer = Some(
|
||||||
l.with_writer(file_appender)
|
l.with_writer(wrapper)
|
||||||
.with_timer(get_logger_timer_rfc3339())
|
.with_timer(get_logger_timer_rfc3339())
|
||||||
.with_filter(file_filter),
|
.with_filter(file_filter),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ struct WireGuardImpl {
|
|||||||
global_ctx: ArcGlobalCtx,
|
global_ctx: ArcGlobalCtx,
|
||||||
peer_mgr: Arc<PeerManager>,
|
peer_mgr: Arc<PeerManager>,
|
||||||
wg_config: WgConfig,
|
wg_config: WgConfig,
|
||||||
listenr_addr: SocketAddr,
|
listener_addr: SocketAddr,
|
||||||
|
|
||||||
wg_peer_ip_table: WgPeerIpTable,
|
wg_peer_ip_table: WgPeerIpTable,
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ impl WireGuardImpl {
|
|||||||
let wg_config = get_wg_config_for_portal(&nid);
|
let wg_config = get_wg_config_for_portal(&nid);
|
||||||
|
|
||||||
let vpn_cfg = global_ctx.config.get_vpn_portal_config().unwrap();
|
let vpn_cfg = global_ctx.config.get_vpn_portal_config().unwrap();
|
||||||
let listenr_addr = vpn_cfg.wireguard_listen;
|
let listener_addr = vpn_cfg.wireguard_listen;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
global_ctx,
|
global_ctx,
|
||||||
peer_mgr,
|
peer_mgr,
|
||||||
wg_config,
|
wg_config,
|
||||||
listenr_addr,
|
listener_addr,
|
||||||
wg_peer_ip_table: Arc::new(DashMap::new()),
|
wg_peer_ip_table: Arc::new(DashMap::new()),
|
||||||
tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())),
|
tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())),
|
||||||
}
|
}
|
||||||
@@ -209,12 +209,11 @@ impl WireGuardImpl {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), err(level = Level::WARN))]
|
async fn start_listener(&self, listener_addr: &SocketAddr) -> anyhow::Result<()> {
|
||||||
async fn start(&self) -> anyhow::Result<()> {
|
let mut listener_url = url::Url::parse("wg://0.0.0.0:0").unwrap();
|
||||||
let mut l = WgTunnelListener::new(
|
listener_url.set_port(Some(listener_addr.port())).unwrap();
|
||||||
format!("wg://{}", self.listenr_addr).parse().unwrap(),
|
listener_url.set_ip_host(listener_addr.ip()).unwrap();
|
||||||
self.wg_config.clone(),
|
let mut l = WgTunnelListener::new(listener_url.clone(), self.wg_config.clone());
|
||||||
);
|
|
||||||
|
|
||||||
tracing::info!("Wireguard VPN Portal Starting");
|
tracing::info!("Wireguard VPN Portal Starting");
|
||||||
|
|
||||||
@@ -224,9 +223,6 @@ impl WireGuardImpl {
|
|||||||
.await
|
.await
|
||||||
.with_context(|| "Failed to start wireguard listener for vpn portal")?;
|
.with_context(|| "Failed to start wireguard listener for vpn portal")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
join_joinset_background(self.tasks.clone(), "wireguard".to_string());
|
|
||||||
|
|
||||||
let tasks = Arc::downgrade(&self.tasks.clone());
|
let tasks = Arc::downgrade(&self.tasks.clone());
|
||||||
let peer_mgr = self.peer_mgr.clone();
|
let peer_mgr = self.peer_mgr.clone();
|
||||||
let wg_peer_ip_table = self.wg_peer_ip_table.clone();
|
let wg_peer_ip_table = self.wg_peer_ip_table.clone();
|
||||||
@@ -243,6 +239,32 @@ impl WireGuardImpl {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.global_ctx
|
||||||
|
.issue_event(GlobalCtxEvent::VpnPortalStarted(listener_url.to_string()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), err(level = Level::WARN))]
|
||||||
|
async fn start(&self) -> anyhow::Result<()> {
|
||||||
|
tracing::info!("Wireguard VPN Portal Starting");
|
||||||
|
|
||||||
|
self.start_listener(&self.listener_addr).await?;
|
||||||
|
// if binding to v4 unspecified, also start a listener on v6 unspecified
|
||||||
|
if let SocketAddr::V4(v4) = &self.listener_addr {
|
||||||
|
if v4.ip().is_unspecified() {
|
||||||
|
let _ = self
|
||||||
|
.start_listener(&SocketAddr::V6(SocketAddrV6::new(
|
||||||
|
Ipv6Addr::UNSPECIFIED,
|
||||||
|
v4.port(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
join_joinset_background(self.tasks.clone(), "wireguard".to_string());
|
||||||
self.start_pipeline_processor().await;
|
self.start_pipeline_processor().await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -324,7 +346,7 @@ PersistentKeepalive = 25
|
|||||||
"#,
|
"#,
|
||||||
peer_secret_key = BASE64_STANDARD.encode(cfg.peer_secret_key()),
|
peer_secret_key = BASE64_STANDARD.encode(cfg.peer_secret_key()),
|
||||||
my_public_key = BASE64_STANDARD.encode(cfg.my_public_key()),
|
my_public_key = BASE64_STANDARD.encode(cfg.my_public_key()),
|
||||||
listenr_addr = self.inner.as_ref().unwrap().listenr_addr,
|
listenr_addr = self.inner.as_ref().unwrap().listener_addr,
|
||||||
allow_ips = allow_ips,
|
allow_ips = allow_ips,
|
||||||
address = client_cidr.first_address().to_string() + "/32",
|
address = client_cidr.first_address().to_string() + "/32",
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+2723
-3374
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user