Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1be8138a | ||
|
|
e06e8a9e8a | ||
|
|
56fd6e4ab6 | ||
|
|
215db09925 | ||
|
|
9fff5e4fec | ||
|
|
802d3f78d7 | ||
|
|
3593035eb9 | ||
|
|
757d76c9da | ||
|
|
445e68ddd1 | ||
|
|
b540ec3f46 | ||
|
|
5c90431876 | ||
|
|
793889c3b7 | ||
|
|
eb42086f9c | ||
|
|
d0efc40efb | ||
|
|
ae704d1d5f | ||
|
|
525dfd9fc1 | ||
|
|
18bd178bbd | ||
|
|
088155f6f3 | ||
|
|
b750faa66f | ||
|
|
ef3309814d | ||
|
|
b87a05b457 | ||
|
|
754439f03c |
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
@@ -186,7 +186,7 @@ jobs:
|
||||
fi
|
||||
|
||||
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
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.4.3'
|
||||
default: 'v2.4.4'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
|
||||
4
.github/workflows/install_rust.sh
vendored
4
.github/workflows/install_rust.sh
vendored
@@ -44,8 +44,8 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
ar x libgcc.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 component add rust-src --toolchain 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-2025-09-01-x86_64-unknown-linux-gnu
|
||||
|
||||
# https://github.com/rust-lang/rust/issues/128808
|
||||
# remove it after Cargo or rustc fix this.
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.4.3'
|
||||
default: 'v2.4.4'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
|
||||
82
Cargo.lock
generated
82
Cargo.lock
generated
@@ -129,6 +129,24 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -2090,7 +2108,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easytier"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -2179,6 +2197,7 @@ dependencies = [
|
||||
"sys-locale",
|
||||
"tabled",
|
||||
"tachyonix",
|
||||
"tempfile",
|
||||
"thiserror 1.0.63",
|
||||
"thunk-rs",
|
||||
"tikv-jemalloc-ctl",
|
||||
@@ -2195,7 +2214,6 @@ dependencies = [
|
||||
"toml 0.8.19",
|
||||
"tonic-build",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tun-easytier",
|
||||
"url",
|
||||
@@ -2213,6 +2231,19 @@ dependencies = [
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-android-jni"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"easytier",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
@@ -2227,7 +2258,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-gui"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -2314,7 +2345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "easytier-web"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -2517,6 +2548,16 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@@ -2595,9 +2636,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
@@ -3364,9 +3405,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.8.0"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||
checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
@@ -7856,8 +7897,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "smoltcp"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
|
||||
source = "git+https://github.com/smoltcp-rs/smoltcp.git?rev=0a926767a68bc88d5512afefa7529c5ecdade4ea#0a926767a68bc88d5512afefa7529c5ecdade4ea"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"byteorder",
|
||||
@@ -8803,15 +8843,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.12.0"
|
||||
version = "3.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64"
|
||||
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix 0.38.34",
|
||||
"windows-sys 0.59.0",
|
||||
"rustix 1.0.7",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9440,18 +9480,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.28"
|
||||
|
||||
@@ -7,6 +7,7 @@ members = [
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
"easytier-contrib/easytier-android-jni",
|
||||
]
|
||||
default-members = ["easytier", "easytier-web"]
|
||||
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
|
||||
|
||||
# 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
|
||||
brew tap brewforge/chinese
|
||||
@@ -105,9 +105,9 @@ After successful execution, you can check the network status using `easytier-cli
|
||||
```text
|
||||
| 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.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.3-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.3-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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 安装
|
||||
brew tap brewforge/chinese
|
||||
@@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea
|
||||
```text
|
||||
| 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.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.3-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.3-70e69a38~ |
|
||||
| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.4-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.4-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.4-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
|
||||
16
easytier-contrib/easytier-android-jni/Cargo.toml
Normal file
16
easytier-contrib/easytier-android-jni/Cargo.toml
Normal file
@@ -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" }
|
||||
267
easytier-contrib/easytier-android-jni/README.md
Normal file
267
easytier-contrib/easytier-android-jni/README.md
Normal file
@@ -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
easytier-contrib/easytier-android-jni/build.sh
Executable file
125
easytier-contrib/easytier-android-jni/build.sh
Executable file
@@ -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 类的方法"
|
||||
56
easytier-contrib/easytier-android-jni/example_config.toml
Normal file
56
easytier-contrib/easytier-android-jni/example_config.toml
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 组件。
|
||||
319
easytier-contrib/easytier-android-jni/src/lib.rs
Normal file
319
easytier-contrib/easytier-android-jni/src/lib.rs
Normal file
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.4.3
|
||||
version=v2.4.4
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; flex-direction: column; gap: 1px; align-items: flex-start;">
|
||||
<el-tag v-if="row.version" size="small" style="font-size: 11px; padding: 1px 4px;">{{ row.version
|
||||
}}</el-tag>
|
||||
}}</el-tag>
|
||||
<span v-else class="text-muted" style="font-size: 11px;">未知</span>
|
||||
<el-tag :type="row.allow_relay ? 'success' : 'info'" size="small"
|
||||
style="font-size: 9px; padding: 1px 3px;">
|
||||
@@ -281,7 +281,7 @@ import {
|
||||
const loading = ref(false)
|
||||
const nodes = ref([])
|
||||
const searchText = ref('')
|
||||
const statusFilter = ref('true')
|
||||
const statusFilter = ref('')
|
||||
const protocolFilter = ref('')
|
||||
const detailDialogVisible = ref(false)
|
||||
const selectedNode = ref(null)
|
||||
@@ -292,7 +292,7 @@ const apiUrl = ref(window.location.href)
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
per_page: 50,
|
||||
total: 0
|
||||
})
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ pub async fn admin_get_nodes(
|
||||
verify_admin_token(&headers)?;
|
||||
|
||||
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 mut query = entity::shared_nodes::Entity::find();
|
||||
|
||||
@@ -436,7 +436,7 @@ impl HealthChecker {
|
||||
);
|
||||
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), ConfigSource::FFI)
|
||||
.run_network_instance(cfg.clone(), ConfigSource::Web)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
self.inst_id_map.insert(node_id, cfg.get_id());
|
||||
|
||||
@@ -650,7 +650,7 @@ impl HealthChecker {
|
||||
node_id,
|
||||
HealthStatus::Unhealthy,
|
||||
None,
|
||||
Some(e.to_string()),
|
||||
Some(format!("inst id: {}, err: {}", inst_id, e)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -30,6 +30,23 @@ fn easytier_version() -> Result<String, 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]
|
||||
fn is_autostart() -> Result<bool, String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
@@ -199,6 +216,8 @@ pub fn run() {
|
||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||
level: None,
|
||||
file: None,
|
||||
size_mb: None,
|
||||
count: None,
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -243,7 +262,8 @@ pub fn run() {
|
||||
set_logging_level,
|
||||
set_tun_fd,
|
||||
is_autostart,
|
||||
easytier_version
|
||||
easytier_version,
|
||||
set_dock_visibility
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.4.3",
|
||||
"version": "2.4.4",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
||||
18
easytier-gui/src/modules/dock_visibility.ts
Normal file
18
easytier-gui/src/modules/dock_visibility.ts
Normal file
@@ -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 { useTray } from '~/composables/tray'
|
||||
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
import { getDockVisibilityStatus, loadDockVisibilityAsync } from '~/modules/dock_visibility'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const visible = ref(false)
|
||||
@@ -177,6 +178,14 @@ const setting_menu_items = ref([
|
||||
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'),
|
||||
icon: 'pi pi-file',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
edition = "2021"
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
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 { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
|
||||
@@ -106,8 +106,30 @@ function ipFormat(info: PeerRoutePair) {
|
||||
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) {
|
||||
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(() => {
|
||||
|
||||
@@ -44,6 +44,8 @@ logging_open_dir: 打开日志目录
|
||||
logging_copy_dir: 复制日志路径
|
||||
disable_auto_launch: 关闭开机自启
|
||||
enable_auto_launch: 开启开机自启
|
||||
hide_dock_icon: 隐藏 Dock 图标
|
||||
show_dock_icon: 显示 Dock 图标
|
||||
exit: 退出
|
||||
chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
|
||||
hostname_placeholder: '留空默认为主机名: {0}'
|
||||
@@ -210,6 +212,7 @@ event:
|
||||
ConnectionError: 连接错误
|
||||
Connecting: 正在连接
|
||||
ConnectError: 连接错误
|
||||
VpnPortalStarted: VPN门户已启动
|
||||
VpnPortalClientConnected: VPN门户客户端已连接
|
||||
VpnPortalClientDisconnected: VPN门户客户端已断开连接
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
|
||||
@@ -44,6 +44,8 @@ logging_open_dir: Open Log Directory
|
||||
logging_copy_dir: Copy Log Path
|
||||
disable_auto_launch: Disable Launch on Reboot
|
||||
enable_auto_launch: Enable Launch on Reboot
|
||||
hide_dock_icon: Hide Dock Icon
|
||||
show_dock_icon: Show Dock Icon
|
||||
exit: Exit
|
||||
use_latency_first: Latency First Mode
|
||||
chips_placeholder: 'e.g: {0}, select from the dropdown after input'
|
||||
@@ -210,6 +212,7 @@ event:
|
||||
ConnectionError: ConnectionError
|
||||
Connecting: Connecting
|
||||
ConnectError: ConnectError
|
||||
VpnPortalStarted: VpnPortalStarted
|
||||
VpnPortalClientConnected: VpnPortalClientConnected
|
||||
VpnPortalClientDisconnected: VpnPortalClientDisconnected
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface NetworkConfig {
|
||||
enable_private_mode?: boolean
|
||||
|
||||
rpc_portal_whitelists: string[]
|
||||
|
||||
|
||||
port_forwards: PortForwardConfig[]
|
||||
}
|
||||
|
||||
@@ -246,10 +246,14 @@ export interface PeerRoutePair {
|
||||
peer?: PeerInfo
|
||||
}
|
||||
|
||||
export interface UrlPb {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface TunnelInfo {
|
||||
tunnel_type: string
|
||||
local_addr: string
|
||||
remote_addr: string
|
||||
local_addr: UrlPb
|
||||
remote_addr: UrlPb
|
||||
}
|
||||
|
||||
export interface PeerConnStats {
|
||||
@@ -302,6 +306,7 @@ export enum EventType {
|
||||
Connecting = 'Connecting', // any
|
||||
ConnectError = 'ConnectError', // string, string, string
|
||||
|
||||
VpnPortalStarted = 'VpnPortalStarted', // string
|
||||
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
|
||||
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
|
||||
|
||||
|
||||
@@ -119,6 +119,8 @@ impl LoggingConfigLoader for &Cli {
|
||||
dir: self.file_log_dir.clone(),
|
||||
level: self.file_log_level.clone(),
|
||||
file: None,
|
||||
size_mb: None,
|
||||
count: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.4.3"
|
||||
version = "2.4.4"
|
||||
edition = "2021"
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
@@ -36,7 +36,6 @@ tracing-subscriber = { version = "0.3", features = [
|
||||
"local-time",
|
||||
"time",
|
||||
] }
|
||||
tracing-appender = "0.2.3"
|
||||
console-subscriber = { version = "0.4.1", optional = true }
|
||||
thiserror = "1.0"
|
||||
auto_impl = "1.1.0"
|
||||
@@ -164,7 +163,7 @@ mimalloc = { version = "*", optional = true }
|
||||
# mips
|
||||
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",
|
||||
"medium-ip",
|
||||
"proto-ipv4",
|
||||
@@ -258,11 +257,17 @@ jemallocator = { package = "tikv-jemallocator", version = "0.6.0", optional = tr
|
||||
] }
|
||||
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 = [
|
||||
"background_threads_runtime_support",
|
||||
"background_threads",
|
||||
], optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
|
||||
], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.12"
|
||||
globwalk = "0.8.1"
|
||||
@@ -290,6 +295,7 @@ serial_test = "3.0.0"
|
||||
rstest = "0.25.0"
|
||||
futures-util = "0.3.30"
|
||||
maplit = "1.0.2"
|
||||
tempfile = "3.22.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
|
||||
@@ -208,6 +208,21 @@ core_clap:
|
||||
enable_relay_foreign_network_kcp:
|
||||
en: "if true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets)"
|
||||
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:
|
||||
panic_backtrace_save:
|
||||
|
||||
@@ -10,6 +10,7 @@ use cidr::IpCidr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
common::stun::StunInfoCollector,
|
||||
proto::{
|
||||
acl::Acl,
|
||||
common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
|
||||
@@ -200,8 +201,11 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_udp_whitelist(&self) -> Vec<String>;
|
||||
fn set_udp_whitelist(&self, whitelist: Vec<String>);
|
||||
|
||||
fn get_stun_servers(&self) -> Vec<String>;
|
||||
fn set_stun_servers(&self, servers: Vec<String>);
|
||||
fn get_stun_servers(&self) -> Option<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;
|
||||
}
|
||||
@@ -308,6 +312,8 @@ pub struct FileLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
pub file: Option<String>,
|
||||
pub dir: Option<String>,
|
||||
pub size_mb: Option<u64>,
|
||||
pub count: Option<usize>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
netns: Option<String>,
|
||||
hostname: Option<String>,
|
||||
@@ -412,6 +418,7 @@ struct Config {
|
||||
tcp_whitelist: Option<Vec<String>>,
|
||||
udp_whitelist: Option<Vec<String>>,
|
||||
stun_servers: Option<Vec<String>>,
|
||||
stun_servers_v6: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -791,17 +798,20 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().udp_whitelist = Some(whitelist);
|
||||
}
|
||||
|
||||
fn get_stun_servers(&self) -> Vec<String> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.stun_servers
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
fn get_stun_servers(&self) -> Option<Vec<String>> {
|
||||
self.config.lock().unwrap().stun_servers.clone()
|
||||
}
|
||||
|
||||
fn set_stun_servers(&self, servers: Vec<String>) {
|
||||
self.config.lock().unwrap().stun_servers = Some(servers);
|
||||
fn set_stun_servers(&self, servers: Option<Vec<String>>) {
|
||||
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 {
|
||||
@@ -826,6 +836,12 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
|
||||
let mut config = self.config.lock().unwrap().clone();
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -838,14 +854,14 @@ pub mod tests {
|
||||
fn test_stun_servers_config() {
|
||||
let config = TomlConfigLoader::default();
|
||||
let stun_servers = config.get_stun_servers();
|
||||
assert!(stun_servers.is_empty());
|
||||
assert!(stun_servers.is_none());
|
||||
|
||||
// Test setting custom stun servers
|
||||
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();
|
||||
assert_eq!(retrieved_servers, custom_servers);
|
||||
assert_eq!(retrieved_servers.unwrap(), custom_servers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -859,7 +875,7 @@ stun_servers = [
|
||||
]"#;
|
||||
|
||||
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[0], "stun.l.google.com:19302");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::net::IpAddr;
|
||||
use std::{
|
||||
hash::Hasher,
|
||||
sync::{Arc, Mutex},
|
||||
@@ -43,7 +44,8 @@ pub enum GlobalCtxEvent {
|
||||
Connecting(url::Url),
|
||||
ConnectError(String, String, String), // (dst, ip version, error message)
|
||||
|
||||
VpnPortalClientConnected(String, String), // (portal, client ip)
|
||||
VpnPortalStarted(String), // (portal)
|
||||
VpnPortalClientConnected(String, String), // (portal, client ip)
|
||||
VpnPortalClientDisconnected(String, String), // (portal, client ip)
|
||||
|
||||
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
|
||||
@@ -114,12 +116,21 @@ impl GlobalCtx {
|
||||
|
||||
let (event_bus, _) = tokio::sync::broadcast::channel(8);
|
||||
|
||||
let stun_servers = config_fs.get_stun_servers();
|
||||
let stun_info_collection = Arc::new(if stun_servers.is_empty() {
|
||||
StunInfoCollector::new_with_default_servers()
|
||||
let stun_info_collector = StunInfoCollector::new_with_default_servers();
|
||||
|
||||
if let Some(stun_servers) = config_fs.get_stun_servers() {
|
||||
stun_info_collector.set_stun_servers(stun_servers);
|
||||
} 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 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(
|
||||
net_ns,
|
||||
stun_info_collection.clone(),
|
||||
stun_info_collector.clone(),
|
||||
)))),
|
||||
|
||||
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()),
|
||||
|
||||
@@ -230,6 +241,13 @@ impl GlobalCtx {
|
||||
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 {
|
||||
self.config.get_network_identity()
|
||||
}
|
||||
|
||||
@@ -71,12 +71,7 @@ impl IfConfiguerTrait for MacIfConfiger {
|
||||
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
|
||||
} else {
|
||||
run_shell_cmd(
|
||||
format!(
|
||||
"ifconfig {} inet {} delete",
|
||||
name,
|
||||
ip.unwrap().address().to_string()
|
||||
)
|
||||
.as_str(),
|
||||
format!("ifconfig {} inet {} delete", name, ip.unwrap().address()).as_str(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod stats_manager;
|
||||
pub mod stun;
|
||||
pub mod stun_codec_ext;
|
||||
pub mod token_bucket;
|
||||
pub mod tracing_rolling_appender;
|
||||
|
||||
pub fn get_logger_timer<F: time::formatting::Formattable>(
|
||||
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"))]
|
||||
impl InterfaceFilter {
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn is_interface_physical(&self) -> bool {
|
||||
let interface_name = &self.iface.name;
|
||||
let output = tokio::process::Command::new("networksetup")
|
||||
.args(&["-listallhardwareports"])
|
||||
async fn get_networksetup_output() -> String {
|
||||
use anyhow::Context;
|
||||
use std::time::{Duration, Instant};
|
||||
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()
|
||||
.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();
|
||||
|
||||
@@ -79,11 +113,7 @@ impl InterfaceFilter {
|
||||
|
||||
if line.contains("Device:") && line.contains(interface_name) {
|
||||
let next_line = lines[i + 1];
|
||||
if next_line.contains("Virtual Interface") {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return !next_line.contains("Virtual Interface");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -718,10 +718,10 @@ impl StunInfoCollectorTrait for 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 {
|
||||
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)),
|
||||
public_ipv6: Arc::new(AtomicCell::new(None)),
|
||||
nat_test_result_time: Arc::new(AtomicCell::new(Local::now())),
|
||||
@@ -732,7 +732,17 @@ impl StunInfoCollector {
|
||||
}
|
||||
|
||||
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> {
|
||||
|
||||
242
easytier/src/common/tracing_rolling_appender/LICENSE
Normal file
242
easytier/src/common/tracing_rolling_appender/LICENSE
Normal file
@@ -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.
|
||||
~~~~
|
||||
517
easytier/src/common/tracing_rolling_appender/base.rs
Normal file
517
easytier/src/common/tracing_rolling_appender/base.rs
Normal file
@@ -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
|
||||
224
easytier/src/common/tracing_rolling_appender/mod.rs
Normal file
224
easytier/src/common/tracing_rolling_appender/mod.rs
Normal file
@@ -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::*;
|
||||
@@ -30,16 +30,18 @@ use easytier::{
|
||||
cli::{
|
||||
list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, AddPortForwardRequest,
|
||||
ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest,
|
||||
GetAclStatsRequest, GetPrometheusStatsRequest, GetStatsRequest,
|
||||
GetAclStatsRequest, GetLoggerConfigRequest, GetPrometheusStatsRequest, GetStatsRequest,
|
||||
GetVpnPortalInfoRequest, GetWhitelistRequest, ListConnectorRequest,
|
||||
ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest,
|
||||
ListPeerRequest, ListPeerResponse, ListPortForwardRequest, ListRouteRequest,
|
||||
ListRouteResponse, ManageMappedListenerRequest, MappedListenerManageAction,
|
||||
MappedListenerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
|
||||
ListRouteResponse, LogLevel, LoggerRpc, LoggerRpcClientFactory,
|
||||
ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc,
|
||||
MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc,
|
||||
PeerManageRpcClientFactory, PortForwardManageRpc, PortForwardManageRpcClientFactory,
|
||||
RemovePortForwardRequest, SetWhitelistRequest, ShowNodeInfoRequest, StatsRpc,
|
||||
StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc,
|
||||
TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory,
|
||||
RemovePortForwardRequest, SetLoggerConfigRequest, SetWhitelistRequest,
|
||||
ShowNodeInfoRequest, StatsRpc, StatsRpcClientFactory, TcpProxyEntryState,
|
||||
TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc,
|
||||
VpnPortalRpcClientFactory,
|
||||
},
|
||||
common::{NatType, SocketType},
|
||||
peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory},
|
||||
@@ -47,7 +49,7 @@ use easytier::{
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
tunnel::tcp::TcpTunnelConnector,
|
||||
utils::{cost_to_str, float_to_str, PeerRoutePair},
|
||||
utils::{cost_to_str, PeerRoutePair},
|
||||
};
|
||||
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
@@ -105,6 +107,8 @@ enum SubCommand {
|
||||
Whitelist(WhitelistArgs),
|
||||
#[command(about = "show statistics information")]
|
||||
Stats(StatsArgs),
|
||||
#[command(about = "manage logger configuration")]
|
||||
Logger(LoggerArgs),
|
||||
#[command(about = t!("core_clap.generate_completions").to_string())]
|
||||
GenAutocomplete { shell: Shell },
|
||||
}
|
||||
@@ -272,6 +276,23 @@ enum StatsSubCommand {
|
||||
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)]
|
||||
struct ServiceArgs {
|
||||
#[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")?)
|
||||
}
|
||||
|
||||
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> {
|
||||
let client = self.get_peer_manager_client().await?;
|
||||
let request = ListPeerRequest::default();
|
||||
@@ -484,12 +517,19 @@ impl CommandHandler<'_> {
|
||||
ipv4: String,
|
||||
hostname: String,
|
||||
cost: String,
|
||||
#[tabled(rename = "lat(ms)")]
|
||||
lat_ms: String,
|
||||
#[tabled(rename = "loss")]
|
||||
loss_rate: String,
|
||||
#[tabled(rename = "rx")]
|
||||
rx_bytes: String,
|
||||
#[tabled(rename = "tx")]
|
||||
tx_bytes: String,
|
||||
#[tabled(rename = "tunnel")]
|
||||
tunnel_proto: String,
|
||||
#[tabled(rename = "NAT")]
|
||||
nat_type: String,
|
||||
#[tabled(skip)]
|
||||
id: String,
|
||||
version: String,
|
||||
}
|
||||
@@ -497,6 +537,11 @@ impl CommandHandler<'_> {
|
||||
impl From<PeerRoutePair> for PeerTableItem {
|
||||
fn from(p: PeerRoutePair) -> Self {
|
||||
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 {
|
||||
cidr: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(),
|
||||
ipv4: route
|
||||
@@ -506,12 +551,8 @@ impl CommandHandler<'_> {
|
||||
.unwrap_or_default(),
|
||||
hostname: route.hostname.clone(),
|
||||
cost: cost_to_str(route.cost),
|
||||
lat_ms: if route.cost == 1 {
|
||||
float_to_str(p.get_latency_ms().unwrap_or(0.0), 3)
|
||||
} else {
|
||||
route.path_latency_latency_first().to_string()
|
||||
},
|
||||
loss_rate: float_to_str(p.get_loss_rate().unwrap_or(0.0), 3),
|
||||
lat_ms: format!("{:.2}", lat_ms),
|
||||
loss_rate: format!("{:.1}%", p.get_loss_rate().unwrap_or(0.0) * 100.0),
|
||||
rx_bytes: format_size(p.get_rx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||
tx_bytes: format_size(p.get_tx_bytes().unwrap_or(0), humansize::DECIMAL),
|
||||
tunnel_proto: p
|
||||
@@ -1155,6 +1196,66 @@ impl CommandHandler<'_> {
|
||||
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> {
|
||||
let mut ports = Vec::new();
|
||||
for port_spec in ports_str.split(',') {
|
||||
@@ -1460,7 +1561,7 @@ where
|
||||
{
|
||||
match format {
|
||||
OutputFormat::Table => {
|
||||
println!("{}", tabled::Table::new(items).with(Style::modern()));
|
||||
println!("{}", tabled::Table::new(items).with(Style::markdown()));
|
||||
}
|
||||
OutputFormat::Json => {
|
||||
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]);
|
||||
}
|
||||
|
||||
println!("{}", builder.build().with(Style::modern()));
|
||||
println!("{}", builder.build().with(Style::markdown()));
|
||||
}
|
||||
Some(NodeSubCommand::Config) => {
|
||||
println!("{}", node_info.config);
|
||||
@@ -1988,6 +2089,14 @@ async fn main() -> Result<(), Error> {
|
||||
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 } => {
|
||||
let mut cmd = Cli::command();
|
||||
easytier::print_completions(shell, &mut cmd, "easytier-cli");
|
||||
|
||||
@@ -13,7 +13,6 @@ use std::{
|
||||
use anyhow::Context;
|
||||
use cidr::IpCidr;
|
||||
use clap::{CommandFactory, Parser};
|
||||
|
||||
use clap_complete::Shell;
|
||||
use easytier::{
|
||||
common::{
|
||||
@@ -35,6 +34,7 @@ use easytier::{
|
||||
utils::{init_logger, setup_panic_handler},
|
||||
web_client,
|
||||
};
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
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())]
|
||||
gen_autocomplete: Option<Shell>,
|
||||
|
||||
#[clap(long, help = t!("core_clap.check_config").to_string())]
|
||||
check_config: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -572,6 +575,15 @@ struct NetworkOptions {
|
||||
num_args = 0..
|
||||
)]
|
||||
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)]
|
||||
@@ -596,6 +608,20 @@ struct LoggingOptions {
|
||||
help = t!("core_clap.file_log_dir").to_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");
|
||||
@@ -942,10 +968,8 @@ impl NetworkOptions {
|
||||
old_udp_whitelist.extend(self.udp_whitelist.clone());
|
||||
cfg.set_udp_whitelist(old_udp_whitelist);
|
||||
|
||||
if let Some(stun_servers) = &self.stun_servers {
|
||||
cfg.set_stun_servers(stun_servers.clone());
|
||||
}
|
||||
|
||||
cfg.set_stun_servers(self.stun_servers.clone());
|
||||
cfg.set_stun_servers_v6(self.stun_servers_v6.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -962,6 +986,8 @@ impl LoggingConfigLoader for &LoggingOptions {
|
||||
level: self.file_log_level.clone(),
|
||||
dir: self.file_log_dir.clone(),
|
||||
file: None,
|
||||
size_mb: self.file_log_size,
|
||||
count: self.file_log_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1036,6 +1062,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")]
|
||||
fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||
use std::sync::Arc;
|
||||
@@ -1046,7 +1084,7 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||
|
||||
_ = 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_recv = Arc::clone(&stop_notify_send);
|
||||
@@ -1078,7 +1116,7 @@ fn win_service_main(arg: Vec<std::ffi::OsString>) {
|
||||
}
|
||||
|
||||
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() {
|
||||
set_default_machine_id(cli.machine_id);
|
||||
@@ -1138,8 +1176,15 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
if let Some(config_files) = cli.config_file {
|
||||
let config_file_count = config_files.len();
|
||||
for config_file in config_files {
|
||||
let mut cfg = TomlConfigLoader::new(&config_file)
|
||||
.with_context(|| format!("failed to load config file: {:?}", config_file))?;
|
||||
let mut cfg = 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(|| "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) {
|
||||
cli.network_options.merge_into(&mut cfg).with_context(|| {
|
||||
@@ -1254,12 +1299,24 @@ async fn main() -> ExitCode {
|
||||
set_prof_active(true);
|
||||
let _monitor = std::thread::spawn(memory_monitor);
|
||||
|
||||
let cli = Cli::parse();
|
||||
let cli = parse_cli();
|
||||
|
||||
if let Some(shell) = cli.gen_autocomplete {
|
||||
let mut cmd = Cli::command();
|
||||
easytier::print_completions(shell, &mut cmd, "easytier-core");
|
||||
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;
|
||||
|
||||
if let Err(e) = run_main(cli).await {
|
||||
@@ -1274,3 +1331,25 @@ async fn main() -> ExitCode {
|
||||
|
||||
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 is_latency_first = self.global_ctx.get_flags().latency_first;
|
||||
self.tasks.lock().await.spawn(
|
||||
async move {
|
||||
while let Some(msg) = receiver.recv().await {
|
||||
let hdr = msg.peer_manager_header().unwrap();
|
||||
while let Some(mut msg) = receiver.recv().await {
|
||||
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 Some(pm) = peer_manager.upgrade() else {
|
||||
tracing::warn!("peer manager is gone, icmp proxy send loop exit");
|
||||
|
||||
@@ -457,7 +457,7 @@ impl KcpProxyDst {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
proxy_entries: Arc<DashMap<ConnId, TcpProxyEntry>>,
|
||||
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<()> {
|
||||
let mut conn_data = kcp_stream.conn_data().clone();
|
||||
let parsed_conn_data = KcpConnData::decode(&mut conn_data)
|
||||
|
||||
@@ -252,13 +252,13 @@ pub struct QUICProxyDst {
|
||||
endpoint: Arc<quinn::Endpoint>,
|
||||
proxy_entries: Arc<DashMap<SocketAddr, TcpProxyEntry>>,
|
||||
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 {
|
||||
pub fn new(
|
||||
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> {
|
||||
let _g = global_ctx.net_ns.guard();
|
||||
let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap())
|
||||
@@ -324,7 +324,7 @@ impl QUICProxyDst {
|
||||
ctx: Arc<GlobalCtx>,
|
||||
cidr_set: Arc<CidrSet>,
|
||||
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();
|
||||
defer!(
|
||||
@@ -368,7 +368,7 @@ impl QUICProxyDst {
|
||||
cidr_set: Arc<CidrSet>,
|
||||
proxy_entry_key: SocketAddr,
|
||||
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)> {
|
||||
let conn = incoming.await.with_context(|| "accept failed")?;
|
||||
let addr = conn.remote_address();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::{Arc, Weak},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Weak,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -38,7 +41,7 @@ use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::{TcpListener, TcpSocket, UdpSocket},
|
||||
select,
|
||||
sync::{mpsc, Mutex},
|
||||
sync::{mpsc, Mutex, Notify},
|
||||
task::JoinSet,
|
||||
time::timeout,
|
||||
};
|
||||
@@ -418,12 +421,21 @@ pub struct Socks5Server {
|
||||
|
||||
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]
|
||||
impl PeerPacketFilter for Socks5Server {
|
||||
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();
|
||||
if hdr.packet_type != PacketType::Data as u8 {
|
||||
return Some(packet);
|
||||
@@ -519,7 +531,9 @@ impl Socks5Server {
|
||||
|
||||
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 +545,18 @@ impl Socks5Server {
|
||||
let entries = self.entries.clone();
|
||||
let tcp_forward_task = self.tcp_forward_task.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 {
|
||||
let mut prev_ipv4 = None;
|
||||
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 cur_ipv4 = global_ctx.get_ipv4();
|
||||
@@ -570,7 +593,6 @@ impl Socks5Server {
|
||||
kcp_endpoint: Option<Weak<KcpEndpoint>>,
|
||||
) -> Result<(), Error> {
|
||||
*self.kcp_endpoint.lock().await = kcp_endpoint;
|
||||
let mut need_start = false;
|
||||
if let Some(proxy_url) = self.global_ctx.config.get_socks5_portal() {
|
||||
let bind_addr = format!(
|
||||
"{}:{}",
|
||||
@@ -598,22 +620,18 @@ impl Socks5Server {
|
||||
}
|
||||
});
|
||||
|
||||
self.socks5_enabled.store(true, Ordering::Relaxed);
|
||||
join_joinset_background(self.tasks.clone(), "socks5 server".to_string());
|
||||
|
||||
need_start = true;
|
||||
};
|
||||
|
||||
let cfgs = self.global_ctx.config.get_port_forwards();
|
||||
self.reload_port_forwards(&cfgs).await?;
|
||||
need_start = need_start || !cfgs.is_empty();
|
||||
|
||||
if need_start {
|
||||
self.peer_manager
|
||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||
.await;
|
||||
self.peer_manager
|
||||
.add_packet_process_pipeline(Box::new(self.clone()))
|
||||
.await;
|
||||
|
||||
self.run_net_update_task().await;
|
||||
}
|
||||
self.run_net_update_task().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -635,6 +653,7 @@ impl Socks5Server {
|
||||
self.add_port_forward(cfg.clone()).await?;
|
||||
}
|
||||
}
|
||||
self.port_forward_list_change_notifier.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -436,9 +436,12 @@ impl UdpProxy {
|
||||
// forward packets to peer manager
|
||||
let mut receiver = self.receiver.lock().await.take().unwrap();
|
||||
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 {
|
||||
while let Ok(msg) = receiver.recv().await {
|
||||
let to_peer_id: PeerId = msg.peer_manager_header().unwrap().to_peer_id.get();
|
||||
while let Ok(mut msg) = receiver.recv().await {
|
||||
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");
|
||||
let ret = peer_manager.send_msg(msg, to_peer_id).await;
|
||||
if ret.is_err() {
|
||||
|
||||
@@ -990,6 +990,7 @@ impl Instance {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
use crate::instance::logger_rpc_service::LoggerRpcService;
|
||||
use crate::proto::cli::*;
|
||||
|
||||
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 port_forward_manager_rpc = self.get_port_forward_manager_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 peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone());
|
||||
@@ -1027,6 +1029,8 @@ impl Instance {
|
||||
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() {
|
||||
s.registry().register(
|
||||
|
||||
109
easytier/src/instance/logger_rpc_service.rs
Normal file
109
easytier/src/instance/logger_rpc_service.rs
Normal file
@@ -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")]
|
||||
pub mod virtual_nic;
|
||||
|
||||
pub mod logger_rpc_service;
|
||||
|
||||
@@ -127,10 +127,7 @@ impl PacketProtocol {
|
||||
match self {
|
||||
PacketProtocol::IPv4 => Ok(libc::PF_INET as u16),
|
||||
PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16),
|
||||
PacketProtocol::Other(_) => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"neither an IPv4 nor IPv6 packet",
|
||||
)),
|
||||
PacketProtocol::Other(_) => Err(io::Error::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)
|
||||
let _ = nic
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
print_event(
|
||||
instance_id,
|
||||
|
||||
@@ -171,6 +171,8 @@ impl ForeignNetworkEntry {
|
||||
flags.disable_relay_kcp = !global_ctx.get_flags().enable_relay_foreign_network_kcp;
|
||||
config.set_flags(flags);
|
||||
|
||||
config.set_mapped_listeners(Some(global_ctx.config.get_mapped_listeners()));
|
||||
|
||||
let foreign_global_ctx = Arc::new(GlobalCtx::new(config));
|
||||
foreign_global_ctx
|
||||
.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 == 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 {
|
||||
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 {
|
||||
let IpAddr::V4(exit_node) = exit_node else {
|
||||
continue;
|
||||
@@ -1072,8 +1081,12 @@ impl PeerManager {
|
||||
}
|
||||
#[cfg(target_env = "ohos")]
|
||||
{
|
||||
if dst_peers.is_empty() {
|
||||
tracing::info!("no peer id for ipv4: {}, set exit_node for ohos", ipv4_addr);
|
||||
if dst_peers.is_empty()
|
||||
&& !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());
|
||||
is_exit_node = true;
|
||||
}
|
||||
|
||||
@@ -720,15 +720,20 @@ struct NextHopInfo {
|
||||
}
|
||||
// dst_peer_id -> (next_hop_peer_id, cost, path_len)
|
||||
type NextHopMap = DashMap<PeerId, NextHopInfo>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PeerIdAndVersion {
|
||||
peer_id: PeerId,
|
||||
version: Version,
|
||||
}
|
||||
|
||||
// computed with SyncedRouteInfo. used to get next hop.
|
||||
#[derive(Debug)]
|
||||
struct RouteTable {
|
||||
peer_infos: DashMap<PeerId, RoutePeerInfo>,
|
||||
next_hop_map: NextHopMap,
|
||||
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerId>,
|
||||
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerId>,
|
||||
cidr_peer_id_map: DashMap<cidr::IpCidr, PeerId>,
|
||||
ipv4_peer_id_map: DashMap<Ipv4Addr, PeerIdAndVersion>,
|
||||
ipv6_peer_id_map: DashMap<Ipv6Addr, PeerIdAndVersion>,
|
||||
cidr_peer_id_map: DashMap<cidr::IpCidr, PeerIdAndVersion>,
|
||||
next_hop_map_version: AtomicVersion,
|
||||
}
|
||||
|
||||
@@ -834,15 +839,15 @@ impl RouteTable {
|
||||
});
|
||||
self.ipv4_peer_id_map.retain(|_, v| {
|
||||
// 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| {
|
||||
// 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| {
|
||||
// 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());
|
||||
|
||||
let is_new_peer_better = |old_peer_id: PeerId| -> bool {
|
||||
let old_next_hop = self.get_next_hop(old_peer_id);
|
||||
let peer_id_and_version = PeerIdAndVersion {
|
||||
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();
|
||||
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
|
||||
.entry(ipv4_addr.into())
|
||||
.and_modify(|v| {
|
||||
if *v != *peer_id && is_new_peer_better(*v) {
|
||||
*v = *peer_id;
|
||||
if is_new_peer_better(v) {
|
||||
*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) {
|
||||
self.ipv6_peer_id_map
|
||||
.entry(ipv6_addr.into())
|
||||
.and_modify(|v| {
|
||||
if *v != *peer_id && is_new_peer_better(*v) {
|
||||
*v = *peer_id;
|
||||
if is_new_peer_better(v) {
|
||||
*v = peer_id_and_version;
|
||||
}
|
||||
})
|
||||
.or_insert(*peer_id);
|
||||
.or_insert(peer_id_and_version);
|
||||
}
|
||||
|
||||
for cidr in info.proxy_cidrs.iter() {
|
||||
self.cidr_peer_id_map
|
||||
.entry(cidr.parse().unwrap())
|
||||
.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.
|
||||
*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() {
|
||||
let (k, v) = item.pair();
|
||||
if k.contains(&ipv4) {
|
||||
return Some(*v);
|
||||
return Some(v.peer_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -2376,8 +2392,17 @@ impl Route for PeerRoute {
|
||||
|
||||
async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option<PeerId> {
|
||||
let route_table = &self.service_impl.route_table;
|
||||
if let Some(peer_id) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
|
||||
return Some(*peer_id);
|
||||
if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
|
||||
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) {
|
||||
@@ -2390,8 +2415,8 @@ impl Route for PeerRoute {
|
||||
|
||||
async fn get_peer_id_by_ipv6(&self, ipv6_addr: &Ipv6Addr) -> Option<PeerId> {
|
||||
let route_table = &self.service_impl.route_table;
|
||||
if let Some(peer_id) = route_table.ipv6_peer_id_map.get(ipv6_addr) {
|
||||
return Some(*peer_id);
|
||||
if let Some(p) = route_table.ipv6_peer_id_map.get(ipv6_addr) {
|
||||
return Some(p.peer_id);
|
||||
}
|
||||
|
||||
// TODO: Add proxy support for IPv6 similar to IPv4
|
||||
@@ -2493,6 +2518,7 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use cidr::{Ipv4Cidr, Ipv4Inet, Ipv6Inet};
|
||||
use dashmap::DashMap;
|
||||
use prost_reflect::{DynamicMessage, ReflectMessage};
|
||||
|
||||
@@ -2504,7 +2530,7 @@ mod tests {
|
||||
peer_manager::{PeerManager, RouteAlgoType},
|
||||
peer_ospf_route::PeerRouteServiceImpl,
|
||||
route_trait::{NextHopPolicy, Route, RouteCostCalculatorInterface},
|
||||
tests::connect_peer_manager,
|
||||
tests::{connect_peer_manager, create_mock_peer_manager},
|
||||
},
|
||||
proto::{
|
||||
common::NatType,
|
||||
@@ -2964,4 +2990,58 @@ mod tests {
|
||||
|
||||
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 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"));
|
||||
|
||||
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>> {
|
||||
let mut ret = vec![];
|
||||
let p = self.peer.as_ref()?;
|
||||
@@ -78,8 +99,9 @@ impl PeerRoutePair {
|
||||
continue;
|
||||
};
|
||||
// insert if not exists
|
||||
if !ret.contains(&tunnel_info.tunnel_type) {
|
||||
ret.push(tunnel_info.tunnel_type.clone());
|
||||
let tunnel_type = Self::get_tunnel_proto_str(tunnel_info);
|
||||
if !ret.contains(&tunnel_type) {
|
||||
ret.push(tunnel_type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -629,7 +629,10 @@ impl WgTunnelConnector {
|
||||
addr: SocketAddr,
|
||||
) -> Result<Box<dyn super::Tunnel>, super::TunnelError> {
|
||||
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 udp = wg_peer.udp_socket();
|
||||
|
||||
@@ -6,7 +6,9 @@ use tracing_subscriber::{
|
||||
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;
|
||||
|
||||
@@ -28,6 +30,8 @@ pub fn init_logger(
|
||||
config: impl LoggingConfigLoader,
|
||||
need_reload: bool,
|
||||
) -> 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_level = file_config
|
||||
.level
|
||||
@@ -50,7 +54,12 @@ pub fn init_logger(
|
||||
|
||||
if need_reload {
|
||||
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 || {
|
||||
println!("Start log filter reloader");
|
||||
while let Ok(lf) = recver.recv() {
|
||||
@@ -72,15 +81,20 @@ pub fn init_logger(
|
||||
});
|
||||
}
|
||||
|
||||
let file_appender = tracing_appender::rolling::Builder::new()
|
||||
.rotation(tracing_appender::rolling::Rotation::DAILY)
|
||||
.max_log_files(5)
|
||||
.filename_prefix(file_config.file.unwrap_or("easytier".to_string()))
|
||||
.filename_suffix("log")
|
||||
.build(file_config.dir.unwrap_or("./".to_string()))
|
||||
.with_context(|| "failed to initialize rolling file appender")?;
|
||||
let builder = RollingFileAppenderBase::builder();
|
||||
let file_appender = builder
|
||||
.filename(file_config.file.unwrap_or("easytier.log".to_string()))
|
||||
.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(
|
||||
l.with_writer(file_appender)
|
||||
l.with_writer(wrapper)
|
||||
.with_timer(get_logger_timer_rfc3339())
|
||||
.with_filter(file_filter),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ struct WireGuardImpl {
|
||||
global_ctx: ArcGlobalCtx,
|
||||
peer_mgr: Arc<PeerManager>,
|
||||
wg_config: WgConfig,
|
||||
listenr_addr: SocketAddr,
|
||||
listener_addr: SocketAddr,
|
||||
|
||||
wg_peer_ip_table: WgPeerIpTable,
|
||||
|
||||
@@ -62,13 +62,13 @@ impl WireGuardImpl {
|
||||
let wg_config = get_wg_config_for_portal(&nid);
|
||||
|
||||
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 {
|
||||
global_ctx,
|
||||
peer_mgr,
|
||||
wg_config,
|
||||
listenr_addr,
|
||||
listener_addr,
|
||||
wg_peer_ip_table: Arc::new(DashMap::new()),
|
||||
tasks: Arc::new(std::sync::Mutex::new(JoinSet::new())),
|
||||
}
|
||||
@@ -209,12 +209,11 @@ impl WireGuardImpl {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), err(level = Level::WARN))]
|
||||
async fn start(&self) -> anyhow::Result<()> {
|
||||
let mut l = WgTunnelListener::new(
|
||||
format!("wg://{}", self.listenr_addr).parse().unwrap(),
|
||||
self.wg_config.clone(),
|
||||
);
|
||||
async fn start_listener(&self, listener_addr: &SocketAddr) -> anyhow::Result<()> {
|
||||
let mut listener_url = url::Url::parse("wg://0.0.0.0:0").unwrap();
|
||||
listener_url.set_port(Some(listener_addr.port())).unwrap();
|
||||
listener_url.set_ip_host(listener_addr.ip()).unwrap();
|
||||
let mut l = WgTunnelListener::new(listener_url.clone(), self.wg_config.clone());
|
||||
|
||||
tracing::info!("Wireguard VPN Portal Starting");
|
||||
|
||||
@@ -224,9 +223,6 @@ impl WireGuardImpl {
|
||||
.await
|
||||
.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 peer_mgr = self.peer_mgr.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;
|
||||
|
||||
Ok(())
|
||||
@@ -324,7 +346,7 @@ PersistentKeepalive = 25
|
||||
"#,
|
||||
peer_secret_key = BASE64_STANDARD.encode(cfg.peer_secret_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,
|
||||
address = client_cidr.first_address().to_string() + "/32",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user