Compare commits

...

25 Commits

Author SHA1 Message Date
sijie.sun
f272a1775e tmp 2025-09-21 22:35:56 +08:00
Sijie.Sun
a102a8bfc7 fix macos bind failed when addr is v6 (#1398) 2025-09-21 21:47:03 +08:00
Sijie.Sun
c9e8c35e77 fix log dir not work; fix stun config from file not work; (#1393) 2025-09-20 00:20:08 +08:00
Sijie.Sun
1a1be8138a bump version to v2.4.4 (#1386) 2025-09-18 19:49:10 +08:00
Sijie.Sun
e06e8a9e8a allow enable log with cli, limit log size (#1384)
* impl logger rpc
* use size based appender
* add log args
2025-09-18 16:35:12 +08:00
Sijie.Sun
56fd6e4ab6 fix wireguard listener (#1382)
* listen both v4 and v6 for wireguard portal
* fix panic when getting udp local addr

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-17 23:45:05 +08:00
Sijie.Sun
215db09925 avoid packets sending to non-exist peer causing route loop (#1378) 2025-09-17 09:52:53 +08:00
fanyang
9fff5e4fec Add config validation flag (#1376)
Add `--check-config` CLI option to validate configuration without
starting network
2025-09-16 22:58:07 +08:00
Sijie.Sun
802d3f78d7 distinct v6 and v4 tunnel in gui and cli (#1373) 2025-09-16 07:24:31 +08:00
Sijie.Sun
3593035eb9 fix networksetup timeout on some machine (#1372) 2025-09-15 23:33:43 +08:00
Sijie.Sun
757d76c9da fix stun server list empty when config is from web (#1371) 2025-09-15 22:52:58 +08:00
fanyang
445e68ddd1 Read config from stdin (#1354) 2025-09-13 21:21:30 +08:00
Sijie.Sun
b540ec3f46 improve uptime (#1365) 2025-09-13 19:14:13 +08:00
Sijie.Sun
5c90431876 fix smoltcp attempt to subtract sequence numbers with underflow (#1360) 2025-09-13 15:03:04 +08:00
Sijie.Sun
793889c3b7 fix ospf ipv4 map error when ipv4 conflicted and changed (#1359) 2025-09-13 08:48:50 +08:00
Sijie.Sun
eb42086f9c set correct route policy for udp/icmp (#1361) 2025-09-13 08:48:37 +08:00
Sijie.Sun
d0efc40efb fix foreign network direct conn with mapped listeners (#1363) 2025-09-13 08:48:12 +08:00
fanyang
ae704d1d5f Fix jemalloc warning on macOS (#1344)
fix:
```
-> % easytier-core
<jemalloc>: option background_thread currently supports pthread only
```

Reference: https://github.com/apache/arrow/pull/5729
2025-09-08 21:53:40 +08:00
fanyang
525dfd9fc1 cli: improve peer table display with shorter columns for small display (#1342)
- Add short column names for latency, loss rate, rx/tx bytes, tunnel protocol and NAT type
- Format loss rate as percentage with one decimal place
- Change table style from modern to markdown for better readability
2025-09-08 21:52:53 +08:00
Sijie.Sun
18bd178bbd update readme to fix script installation command (#1341) 2025-09-06 17:02:31 +08:00
fanyang
088155f6f3 core: hide default STUN servers from cli (#1334) 2025-09-06 15:53:34 +08:00
Sijie.Sun
b750faa66f add android jni (#1340) 2025-09-06 13:49:42 +08:00
Sijie.Sun
ef3309814d fix cli add port forward failed if initial forward list is empty (#1324) 2025-09-02 22:03:57 +08:00
fanyang
b87a05b457 refactor: update custom STUN server settings (#1310)
* refactor: update global context STUN server initialization

Modified global context initialization to use a single StunInfoCollector
instance with properly configured IPv4 and IPv6 servers instead of
creating separate instances.

feat: add IPv6 STUN server configuration support

Added interface methods and config struct fields to support both IPv4
and IPv6 STUN server configuration. Modified getter and setter methods
to handle Option<Vec<String>> type for both server types.

feat: enhance StunInfoCollector with IPv6 support

Updated StunInfoCollector to support both IPv4 and IPv6 STUN servers.
Added new constructor that accepts both server types and methods to set
them independently.

feat: add CLI argument for IPv6 STUN servers

Added command line argument support for configuring IPv6 STUN servers.
Updated configuration setup to handle both IPv4 and IPv6 STUN server
settings.

docs: add localization for STUN server configuration

Added English and Chinese localization strings for the new STUN server
configuration options, including both IPv4 and IPv6 variants.
2025-09-02 21:46:37 +08:00
Joel Stodolski
754439f03c feat(gui): add macOS dock icon visibility control (#1328) 2025-09-02 17:32:18 +08:00
72 changed files with 4845 additions and 209 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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.

View File

@@ -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'

92
Cargo.lock generated
View File

@@ -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",
@@ -2208,11 +2226,25 @@ dependencies = [
"windows-service",
"windows-sys 0.52.0",
"winreg 0.52.0",
"xmltree",
"zerocopy",
"zip",
"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 +2259,7 @@ dependencies = [
[[package]]
name = "easytier-gui"
version = "2.4.3"
version = "2.4.4"
dependencies = [
"anyhow",
"chrono",
@@ -2314,7 +2346,7 @@ dependencies = [
[[package]]
name = "easytier-web"
version = "2.4.3"
version = "2.4.4"
dependencies = [
"anyhow",
"async-trait",
@@ -2517,6 +2549,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 +2637,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 +3406,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 +7898,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 +8844,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 +9481,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"
@@ -11113,6 +11142,15 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26"
[[package]]
name = "xmltree"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b619f8c85654798007fb10afa5125590b43b088c225a25fc2fec100a9fad0fc6"
dependencies = [
"xml-rs",
]
[[package]]
name = "yansi"
version = "1.0.1"

View File

@@ -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 = [

View File

@@ -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:

View File

@@ -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~ |
```
您可以测试节点之间的连通性:

View 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" }

View 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/)

View 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 类的方法"

View 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

View File

@@ -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))
}
}

View File

@@ -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>
)
}

View File

@@ -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()
}
}

View File

@@ -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 组件。

View 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(),
}
}

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
[package]
name = "easytier-gui"
version = "2.4.3"
version = "2.4.4"
description = "EasyTier GUI"
authors = ["you"]
edition = "2021"

View File

@@ -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"))]

View File

@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false
},
"productName": "easytier-gui",
"version": "2.4.3",
"version": "2.4.4",
"identifier": "com.kkrainbow.easytier",
"plugins": {},
"app": {

View 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
}

View File

@@ -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',

View 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."

View File

@@ -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(() => {

View File

@@ -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地址更改

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}
}
}

View File

@@ -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",
@@ -218,6 +217,9 @@ version-compare = "0.2.0"
hmac = "0.12.1"
sha2 = "0.10.8"
# upnp igd
xmltree = "0.11.0"
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3"
@@ -258,11 +260,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 +298,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"

View File

@@ -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:

View File

@@ -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");

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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");
}
}

View File

@@ -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> {

View 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.
~~~~

View 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

View 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::*;

View File

@@ -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");

View File

@@ -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");
@@ -943,9 +969,16 @@ impl NetworkOptions {
cfg.set_udp_whitelist(old_udp_whitelist);
if let Some(stun_servers) = &self.stun_servers {
cfg.set_stun_servers(stun_servers.clone());
let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
old_stun_servers.extend(stun_servers.iter().cloned());
cfg.set_stun_servers(Some(old_stun_servers));
}
if let Some(stun_servers_v6) = &self.stun_servers_v6 {
let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
}
Ok(())
}
}
@@ -962,6 +995,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 +1071,18 @@ fn win_service_event_loop(
});
}
fn parse_cli() -> Cli {
let mut cli = Cli::parse();
// for --stun-servers="", we want vec![], but clap will give vec![""], hack for that
if let Some(stun_servers) = &mut cli.network_options.stun_servers {
stun_servers.retain(|s| !s.trim().is_empty());
}
if let Some(stun_servers_v6) = &mut cli.network_options.stun_servers_v6 {
stun_servers_v6.retain(|s| !s.trim().is_empty());
}
cli
}
#[cfg(target_os = "windows")]
fn win_service_main(arg: Vec<std::ffi::OsString>) {
use std::sync::Arc;
@@ -1046,7 +1093,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 +1125,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 +1185,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 +1308,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 +1340,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(())
}

View File

@@ -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");

View File

@@ -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)

View File

@@ -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();

View File

@@ -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(())
}

View File

@@ -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() {

View File

@@ -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(

View 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(&current_level_str);
Ok(GetLoggerConfigResponse {
level: level.into(),
})
}
}

View File

@@ -6,3 +6,7 @@ pub mod listeners;
#[cfg(feature = "tun")]
pub mod virtual_nic;
pub mod logger_rpc_service;
pub mod upnp_igd;

View File

@@ -0,0 +1,157 @@
use super::super::PortMappingProtocol;
use std::net::SocketAddr;
// Content of the request.
pub const SEARCH_REQUEST: &str = "M-SEARCH * HTTP/1.1\r
Host:239.255.255.250:1900\r
ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1\r
Man:\"ssdp:discover\"\r
MX:3\r\n\r\n";
pub const GET_EXTERNAL_IP_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress""#;
pub const ADD_ANY_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddAnyPortMapping""#;
pub const ADD_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping""#;
pub const DELETE_PORT_MAPPING_HEADER: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping""#;
pub const GET_GENERIC_PORT_MAPPING_ENTRY: &str =
r#""urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry""#;
const MESSAGE_HEAD: &str = r#"<?xml version="1.0"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>"#;
const MESSAGE_TAIL: &str = r#"</s:Body>
</s:Envelope>"#;
fn format_message(body: String) -> String {
format!("{MESSAGE_HEAD}{body}{MESSAGE_TAIL}")
}
pub fn format_get_external_ip_message() -> String {
r#"<?xml version="1.0"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<m:GetExternalIPAddress xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
</m:GetExternalIPAddress>
</s:Body>
</s:Envelope>"#
.into()
}
pub fn format_add_any_port_mapping_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewEnabled" => 1.to_string(),
"NewExternalPort" => external_port.to_string(),
"NewInternalClient" => local_addr.ip().to_string(),
"NewInternalPort" => local_addr.port().to_string(),
"NewLeaseDuration" => lease_duration.to_string(),
"NewPortMappingDescription" => description.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>"))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:AddAnyPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:AddAnyPortMapping>"#,
))
}
pub fn format_add_port_mapping_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewEnabled" => 1.to_string(),
"NewExternalPort" => external_port.to_string(),
"NewInternalClient" => local_addr.ip().to_string(),
"NewInternalPort" => local_addr.port().to_string(),
"NewLeaseDuration" => lease_duration.to_string(),
"NewPortMappingDescription" => description.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>",))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:AddPortMapping>"#
))
}
pub fn format_delete_port_message(
schema: &[String],
protocol: PortMappingProtocol,
external_port: u16,
) -> String {
let args = schema
.iter()
.filter_map(|argument| {
let value = match argument.as_str() {
"NewExternalPort" => external_port.to_string(),
"NewProtocol" => protocol.to_string(),
"NewRemoteHost" => "".to_string(),
unknown => {
tracing::warn!("Unknown argument: {}", unknown);
return None;
}
};
Some(format!("<{argument}>{value}</{argument}>",))
})
.collect::<Vec<_>>()
.join("\n");
format_message(format!(
r#"<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
{args}
</u:DeletePortMapping>"#
))
}
pub fn formate_get_generic_port_mapping_entry_message(port_mapping_index: u32) -> String {
format_message(format!(
r#"<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewPortMappingIndex>{port_mapping_index}</NewPortMappingIndex>
</u:GetGenericPortMappingEntry>"#
))
}

View File

@@ -0,0 +1,11 @@
pub mod messages;
pub mod options;
pub mod parsing;
use rand::Rng;
pub use self::options::SearchOptions;
pub fn random_port() -> u16 {
rand::thread_rng().gen_range(32_768_u16..65_535_u16)
}

View File

@@ -0,0 +1,45 @@
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
/// Default timeout for a gateway search.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
/// Timeout for each broadcast response during a gateway search.
#[allow(dead_code)]
pub const RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
/// Gateway search configuration
///
/// SearchOptions::default() should suffice for most situations.
///
/// # Example
/// To customize only a few options you can use `Default::default()` or `SearchOptions::default()` and the
/// [struct update syntax](https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax).
/// ```
/// # use std::time::Duration;
/// # use igd_next::SearchOptions;
/// let opts = SearchOptions {
/// timeout: Some(Duration::from_secs(60)),
/// ..Default::default()
/// };
/// ```
pub struct SearchOptions {
/// Bind address for UDP socket (defaults to all `0.0.0.0`)
pub bind_addr: SocketAddr,
/// Broadcast address for discovery packets (defaults to `239.255.255.250:1900`)
pub broadcast_address: SocketAddr,
/// Timeout for a search iteration (defaults to 10s)
pub timeout: Option<Duration>,
/// Timeout for a single search response (defaults to 5s)
pub single_search_timeout: Option<Duration>,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
bind_addr: (IpAddr::from([0, 0, 0, 0]), 0).into(),
broadcast_address: "239.255.255.250:1900".parse().unwrap(),
timeout: Some(DEFAULT_TIMEOUT),
single_search_timeout: Some(RESPONSE_TIMEOUT),
}
}
}

View File

@@ -0,0 +1,756 @@
use std::collections::HashMap;
use std::io;
use std::net::{IpAddr, SocketAddr};
use anyhow::Context;
use url::Url;
use xmltree::{self, Element};
use super::super::PortMappingProtocol;
// Parse the result.
pub fn parse_search_result(text: &str) -> anyhow::Result<(SocketAddr, String)> {
for line in text.lines() {
let line = line.trim();
if line.to_ascii_lowercase().starts_with("location:") {
if let Some(colon) = line.find(':') {
let url_text = &line[colon + 1..].trim();
let url = Url::parse(url_text).map_err(|_| anyhow::anyhow!("Invalid response"))?;
let addr: IpAddr = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
.and_then(|s| s.parse().map_err(|_| anyhow::anyhow!("Invalid response")))?;
let port: u16 = url
.port_or_known_default()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))?;
return Ok((SocketAddr::new(addr, port), url.path().to_string()));
}
}
}
Err(anyhow::anyhow!("Invalid response"))
}
pub fn parse_control_urls<R>(resp: R) -> anyhow::Result<(String, String)>
where
R: io::Read,
{
let root = Element::parse(resp)?;
let mut urls = root.children.iter().filter_map(|child| {
let child = child.as_element()?;
if child.name == "device" {
Some(parse_device(child)?)
} else {
None
}
});
urls.next()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
}
fn parse_device(device: &Element) -> Option<(String, String)> {
let services = device.get_child("serviceList").and_then(|service_list| {
service_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "service" {
parse_service(child)
} else {
None
}
})
.next()
});
let devices = device.get_child("deviceList").and_then(parse_device_list);
services.or(devices)
}
fn parse_device_list(device_list: &Element) -> Option<(String, String)> {
device_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "device" {
parse_device(child)
} else {
None
}
})
.next()
}
fn parse_service(service: &Element) -> Option<(String, String)> {
let service_type = service.get_child("serviceType")?;
let service_type = service_type
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into());
if [
"urn:schemas-upnp-org:service:WANPPPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:1",
"urn:schemas-upnp-org:service:WANIPConnection:2",
]
.contains(&service_type.as_str())
{
let scpd_url = service.get_child("SCPDURL");
let control_url = service.get_child("controlURL");
if let (Some(scpd_url), Some(control_url)) = (scpd_url, control_url) {
Some((
scpd_url
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into()),
control_url
.get_text()
.map(|s| s.into_owned())
.unwrap_or_else(|| "".into()),
))
} else {
None
}
} else {
None
}
}
pub fn parse_schemas<R>(resp: R) -> anyhow::Result<HashMap<String, Vec<String>>>
where
R: io::Read,
{
let root = Element::parse(resp)?;
let mut schema = root.children.iter().filter_map(|child| {
let child = child.as_element()?;
if child.name == "actionList" {
parse_action_list(child)
} else {
None
}
});
schema
.next()
.ok_or_else(|| anyhow::anyhow!("Invalid response"))
}
fn parse_action_list(action_list: &Element) -> Option<HashMap<String, Vec<String>>> {
Some(
action_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "action" {
parse_action(child)
} else {
None
}
})
.collect(),
)
}
fn parse_action(action: &Element) -> Option<(String, Vec<String>)> {
Some((
action.get_child("name")?.get_text()?.into_owned(),
parse_argument_list(action.get_child("argumentList")?)?,
))
}
fn parse_argument_list(argument_list: &Element) -> Option<Vec<String>> {
Some(
argument_list
.children
.iter()
.filter_map(|child| {
let child = child.as_element()?;
if child.name == "argument" {
parse_argument(child)
} else {
None
}
})
.collect(),
)
}
fn parse_argument(action: &Element) -> Option<String> {
if action
.get_child("direction")?
.get_text()?
.into_owned()
.as_str()
== "in"
{
Some(action.get_child("name")?.get_text()?.into_owned())
} else {
None
}
}
pub struct RequestReponse {
text: String,
xml: xmltree::Element,
}
pub type RequestResult = anyhow::Result<RequestReponse>;
pub fn parse_response(text: String, ok: &str) -> RequestResult {
let mut xml = match xmltree::Element::parse(text.as_bytes()) {
Ok(xml) => xml,
Err(..) => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
let body = match xml.get_mut_child("Body") {
Some(body) => body,
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
if let Some(ok) = body.take_child(ok) {
return Ok(RequestReponse { text, xml: ok });
}
let upnp_error = match body
.get_child("Fault")
.and_then(|e| e.get_child("detail"))
.and_then(|e| e.get_child("UPnPError"))
{
Some(upnp_error) => upnp_error,
None => return Err(anyhow::anyhow!("Invalid response: {}", text)),
};
match (
upnp_error.get_child("errorCode"),
upnp_error.get_child("errorDescription"),
) {
(Some(e), Some(d)) => match (e.get_text().as_ref(), d.get_text().as_ref()) {
(Some(et), Some(dt)) => match et.parse::<u16>() {
Ok(en) => Err(anyhow::anyhow!("Error code {}: {}", en, dt)),
Err(..) => Err(anyhow::anyhow!("Invalid response: {}", text)),
},
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
},
_ => Err(anyhow::anyhow!("Invalid response: {}", text)),
}
}
pub fn parse_get_external_ip_response(result: RequestResult) -> anyhow::Result<Option<IpAddr>> {
if let Ok(resp) = &result {
let child = resp.xml.get_child("NewExternalIPAddress");
if let Some(child) = child {
let text = child.get_text();
println!("text {:?}", text);
}
let child_empty = resp.xml.get_child("NewExternalIPAddressFuck");
println!("child_empty {:?}", child_empty);
}
match result {
Ok(resp) => {
let child = resp.xml.get_child("NewExternalIPAddress");
if let Some(child) = child {
match child.get_text() {
Some(text) => {
Ok(Some(text.parse::<IpAddr>().with_context(|| {
format!("Invalid IP address: {}", text)
})?))
}
None => Ok(None),
}
} else {
anyhow::bail!("Invalid response: {}", resp.text);
}
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else {
Err(e)
}
}
}
}
pub fn parse_add_any_port_mapping_response(result: RequestResult) -> anyhow::Result<u16> {
match result {
Ok(resp) => {
match resp
.xml
.get_child("NewReservedPort")
.and_then(|e| e.get_text())
.and_then(|t| t.parse::<u16>().ok())
{
Some(port) => Ok(port),
None => Err(anyhow::anyhow!("Invalid response: {}", resp.text)),
}
}
Err(err) => {
let error_msg = err.to_string();
if error_msg.contains("Error code 605") {
Err(anyhow::anyhow!("Description too long"))
} else if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 728") {
Err(anyhow::anyhow!("No ports available"))
} else {
Err(err)
}
}
}
}
pub fn convert_add_random_port_mapping_error(error: anyhow::Error) -> Option<anyhow::Error> {
let error_msg = error.to_string();
if error_msg.contains("Error code 724") {
None
} else if error_msg.contains("Error code 605") {
Some(anyhow::anyhow!("Description too long"))
} else if error_msg.contains("Error code 606") {
Some(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 718") {
Some(anyhow::anyhow!("No ports available"))
} else if error_msg.contains("Error code 725") {
Some(anyhow::anyhow!("Only permanent leases supported"))
} else {
Some(error)
}
}
pub fn convert_add_same_port_mapping_error(error: anyhow::Error) -> anyhow::Error {
let error_msg = error.to_string();
if error_msg.contains("Error code 606") {
anyhow::anyhow!("Action not authorized")
} else if error_msg.contains("Error code 718") {
anyhow::anyhow!("External port in use")
} else if error_msg.contains("Error code 725") {
anyhow::anyhow!("Only permanent leases supported")
} else {
error
}
}
pub fn convert_add_port_error(err: anyhow::Error) -> anyhow::Error {
let error_msg = err.to_string();
if error_msg.contains("Error code 605") {
anyhow::anyhow!("Description too long")
} else if error_msg.contains("Error code 606") {
anyhow::anyhow!("Action not authorized")
} else if error_msg.contains("Error code 718") {
anyhow::anyhow!("Port in use")
} else if error_msg.contains("Error code 724") {
anyhow::anyhow!("Same port values required")
} else if error_msg.contains("Error code 725") {
anyhow::anyhow!("Only permanent leases supported")
} else {
err
}
}
pub fn parse_delete_port_mapping_response(result: RequestResult) -> anyhow::Result<()> {
match result {
Ok(_) => Ok(()),
Err(err) => {
let error_msg = err.to_string();
if error_msg.contains("Error code 606") {
Err(anyhow::anyhow!("Action not authorized"))
} else if error_msg.contains("Error code 714") {
Err(anyhow::anyhow!("No such port mapping"))
} else {
Err(err)
}
}
}
}
/// One port mapping entry as returned by GetGenericPortMappingEntry
pub struct PortMappingEntry {
/// The remote host for which the mapping is valid
/// Can be an IP address or a host name
pub remote_host: String,
/// The external port of the mapping
pub external_port: u16,
/// The protocol of the mapping
pub protocol: PortMappingProtocol,
/// The internal (local) port
pub internal_port: u16,
/// The internal client of the port mapping
/// Can be an IP address or a host name
pub internal_client: String,
/// A flag whether this port mapping is enabled
pub enabled: bool,
/// A description for this port mapping
pub port_mapping_description: String,
/// The lease duration of this port mapping in seconds
pub lease_duration: u32,
}
pub fn parse_get_generic_port_mapping_entry(
result: RequestResult,
) -> anyhow::Result<PortMappingEntry> {
let response = result?;
let xml = response.xml;
let make_err = |msg: String| move || anyhow::anyhow!("Invalid response: {}", msg);
let extract_field = |field: &str| {
xml.get_child(field)
.ok_or_else(make_err(format!("{field} is missing")))
};
let remote_host = extract_field("NewRemoteHost")?
.get_text()
.map(|c| c.into_owned())
.unwrap_or_else(|| "".into());
let external_port = extract_field("NewExternalPort")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field NewExternalPort is invalid".into()))?;
let protocol = match extract_field("NewProtocol")?.get_text() {
Some(std::borrow::Cow::Borrowed("UDP")) => PortMappingProtocol::Udp,
Some(std::borrow::Cow::Borrowed("TCP")) => PortMappingProtocol::Tcp,
_ => {
return Err(anyhow::anyhow!(
"Invalid response: Field NewProtocol is invalid"
))
}
};
let internal_port = extract_field("NewInternalPort")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field NewInternalPort is invalid".into()))?;
let internal_client = extract_field("NewInternalClient")?
.get_text()
.map(|c| c.into_owned())
.ok_or_else(make_err("Field NewInternalClient is empty".into()))?;
let enabled = match extract_field("NewEnabled")?
.get_text()
.and_then(|t| t.parse::<u16>().ok())
.ok_or_else(make_err("Field Enabled is invalid".into()))?
{
0 => false,
1 => true,
_ => {
return Err(anyhow::anyhow!(
"Invalid response: Field NewEnabled is invalid"
))
}
};
let port_mapping_description = extract_field("NewPortMappingDescription")?
.get_text()
.map(|c| c.into_owned())
.unwrap_or_else(|| "".into());
let lease_duration = extract_field("NewLeaseDuration")?
.get_text()
.and_then(|t| t.parse::<u32>().ok())
.ok_or_else(make_err("Field NewLeaseDuration is invalid".into()))?;
Ok(PortMappingEntry {
remote_host,
external_port,
protocol,
internal_port,
internal_client,
enabled,
port_mapping_description,
lease_duration,
})
}
#[test]
fn test_parse_search_result_case_insensitivity() {
assert!(parse_search_result("location:http://0.0.0.0:0/control_url").is_ok());
assert!(parse_search_result("LOCATION:http://0.0.0.0:0/control_url").is_ok());
}
#[test]
fn test_parse_search_result_ok() {
let result = parse_search_result("location:http://0.0.0.0:0/control_url").unwrap();
assert_eq!(
result.0.ip(),
IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0))
);
assert_eq!(result.0.port(), 0);
assert_eq!(&result.1[..], "/control_url");
}
#[test]
fn test_parse_search_result_fail() {
assert!(parse_search_result("content-type:http://0.0.0.0:0/control_url").is_err());
}
#[test]
fn test_parse_device1() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber>1</modelNumber>
<serialNumber>00000000</serialNumber>
<UDN></UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:Layer3Forwarding1</serviceId>
<controlURL>/ctl/L3F</controlURL>
<eventSubURL>/evt/L3F</eventSubURL>
<SCPDURL>/L3F.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>WAN Device</modelDescription>
<modelName>WAN Device</modelName>
<modelNumber>20180615</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>00000000</serialNumber>
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
<UPC>MINIUPNPD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/ctl/CmnIfCfg</controlURL>
<eventSubURL>/evt/CmnIfCfg</eventSubURL>
<SCPDURL>/WANCfg.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice</friendlyName>
<manufacturer>MiniUPnP</manufacturer>
<manufacturerURL>http://miniupnp.free.fr/</manufacturerURL>
<modelDescription>MiniUPnP daemon</modelDescription>
<modelName>MiniUPnPd</modelName>
<modelNumber>20180615</modelNumber>
<modelURL>http://miniupnp.free.fr/</modelURL>
<serialNumber>00000000</serialNumber>
<UDN>uuid:804e2e56-7bfe-4733-bae0-04bf6d569692</UDN>
<UPC>MINIUPNPD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/ctl/IPConn</controlURL>
<eventSubURL>/evt/IPConn</eventSubURL>
<SCPDURL>/WANIPCn.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://192.168.0.1/</presentationURL>
</device>
</root>"#;
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
assert_eq!(control_url, "/ctl/IPConn");
assert_eq!(control_schema_url, "/WANIPCn.xml");
}
#[test]
fn test_parse_device2() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>http://www.avm.de</manufacturerURL>
<modelDescription>FRITZ!Box 7430</modelDescription>
<modelName>FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>http://www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<iconList>
<icon>
<mimetype>image/gif</mimetype>
<width>118</width>
<height>119</height>
<depth>8</depth>
<url>/ligd.gif</url>
</icon>
</iconList>
<serviceList>
<service>
<serviceType>urn:schemas-any-com:service:Any:1</serviceType>
<serviceId>urn:any-com:serviceId:any1</serviceId>
<controlURL>/igdupnp/control/any</controlURL>
<eventSubURL>/igdupnp/control/any</eventSubURL>
<SCPDURL>/any.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice - FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>www.avm.de</manufacturerURL>
<modelDescription>WANDevice - FRITZ!Box 7430</modelDescription>
<modelName>WANDevice - FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>AVM IGD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/igdupnp/control/WANCommonIFC1</controlURL>
<eventSubURL>/igdupnp/control/WANCommonIFC1</eventSubURL>
<SCPDURL>/igdicfgSCPD.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice - FRITZ!Box 7430</friendlyName>
<manufacturer>AVM Berlin</manufacturer>
<manufacturerURL>www.avm.de</manufacturerURL>
<modelDescription>WANConnectionDevice - FRITZ!Box 7430</modelDescription>
<modelName>WANConnectionDevice - FRITZ!Box 7430</modelName>
<modelNumber>avm</modelNumber>
<modelURL>www.avm.de</modelURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>AVM IGD</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
<controlURL>/igdupnp/control/WANDSLLinkC1</controlURL>
<eventSubURL>/igdupnp/control/WANDSLLinkC1</eventSubURL>
<SCPDURL>/igddslSCPD.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/igdupnp/control/WANIPConn1</controlURL>
<eventSubURL>/igdupnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/igdconnSCPD.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPv6FirewallControl:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPv6Firewall1</serviceId>
<controlURL>/igd2upnp/control/WANIPv6Firewall1</controlURL>
<eventSubURL>/igd2upnp/control/WANIPv6Firewall1</eventSubURL>
<SCPDURL>/igd2ipv6fwcSCPD.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
<presentationURL>http://fritz.box</presentationURL>
</device>
</root>
"#;
let result = parse_control_urls(text.as_bytes());
assert!(result.is_ok());
let (control_schema_url, control_url) = result.unwrap();
assert_eq!(control_url, "/igdupnp/control/WANIPConn1");
assert_eq!(control_schema_url, "/igdconnSCPD.xml");
}
#[test]
fn test_parse_device3() {
let text = r#"<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device xmlns="urn:schemas-upnp-org:device-1-0">
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.1</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<iconList>
<icon>
<mimetype>image/png</mimetype>
<width>16</width>
<height>16</height>
<depth>8</depth>
<url>/ligd.png</url>
</icon>
</iconList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.254</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/upnp/control/WANCommonIFC1</controlURL>
<eventSubURL>/upnp/control/WANCommonIFC1</eventSubURL>
<SCPDURL>/332b484d/wancomicfgSCPD.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName></friendlyName>
<manufacturer></manufacturer>
<manufacturerURL></manufacturerURL>
<modelDescription></modelDescription>
<modelName></modelName>
<modelNumber></modelNumber>
<modelURL></modelURL>
<serialNumber></serialNumber>
<presentationURL>http://192.168.1.254</presentationURL>
<UDN>uuid:00000000-0000-0000-0000-000000000000</UDN>
<UPC>999999999001</UPC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/control/WANIPConn1</eventSubURL>
<SCPDURL>/332b484d/wanipconnSCPD.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>"#;
let (control_schema_url, control_url) = parse_control_urls(text.as_bytes()).unwrap();
assert_eq!(control_url, "/upnp/control/WANIPConn1");
assert_eq!(control_schema_url, "/332b484d/wanipconnSCPD.xml");
}

View File

@@ -0,0 +1,323 @@
use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::net::{IpAddr, SocketAddr};
use super::Provider;
use super::common::{self, messages, parsing, parsing::RequestReponse};
use super::PortMappingProtocol;
/// This structure represents a gateway found by the search functions.
#[derive(Clone, Debug)]
pub struct Gateway<P> {
/// Socket address of the gateway
pub addr: SocketAddr,
/// Root url of the device
pub root_url: String,
/// Control url of the device
pub control_url: String,
/// Url to get schema data from
pub control_schema_url: String,
/// Control schema for all actions
pub control_schema: HashMap<String, Vec<String>>,
/// Executor provider
pub provider: P,
}
impl<P: Provider> Gateway<P> {
async fn perform_request(
&self,
header: &str,
body: &str,
ok: &str,
) -> anyhow::Result<RequestReponse> {
let url = format!("{self}");
let text = P::send_async(&url, header, body).await?;
parsing::parse_response(text, ok)
}
/// Get the external IP address of the gateway in a tokio compatible way
pub async fn get_external_ip(&self) -> anyhow::Result<Option<IpAddr>> {
let result = self
.perform_request(
messages::GET_EXTERNAL_IP_HEADER,
&messages::format_get_external_ip_message(),
"GetExternalIPAddressResponse",
)
.await;
parsing::parse_get_external_ip_response(result)
}
/// Get an external socket address with our external ip and any port. This is a convenience
/// function that calls `get_external_ip` followed by `add_any_port`
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
///
/// # Returns
///
/// The external address that was mapped on success. Otherwise an error.
pub async fn get_any_address(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<SocketAddr> {
let description = description.to_owned();
let ip = self
.get_external_ip()
.await?
.ok_or_else(|| anyhow::anyhow!("Router does not have an external IP address"))?;
let port = self
.add_any_port(protocol, local_addr, lease_duration, &description)
.await?;
Ok(SocketAddr::new(ip, port))
}
/// Add a port mapping.with any external port.
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
///
/// # Returns
///
/// The external port that was mapped on success. Otherwise an error.
pub async fn add_any_port(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
// This function first attempts to call AddAnyPortMapping on the IGD with a random port
// number. If that fails due to the method being unknown it attempts to call AddPortMapping
// instead with a random port number. If that fails due to ConflictInMappingEntry it retrys
// with another port up to a maximum of 20 times. If it fails due to SamePortValuesRequired
// it retrys once with the same port values.
if local_addr.port() == 0 {
return Err(anyhow::anyhow!("Internal port zero is invalid"));
}
let schema = self.control_schema.get("AddAnyPortMapping");
if let Some(schema) = schema {
let external_port = common::random_port();
let description = description.to_owned();
let resp = self
.perform_request(
messages::ADD_ANY_PORT_MAPPING_HEADER,
&messages::format_add_any_port_mapping_message(
schema,
protocol,
external_port,
local_addr,
lease_duration,
&description,
),
"AddAnyPortMappingResponse",
)
.await;
parsing::parse_add_any_port_mapping_response(resp)
} else {
// The router does not have the AddAnyPortMapping method.
// Fall back to using AddPortMapping with a random port.
self.retry_add_random_port_mapping(protocol, local_addr, lease_duration, description)
.await
}
}
async fn retry_add_random_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
for _ in 0u8..20u8 {
match self
.add_random_port_mapping(protocol, local_addr, lease_duration, description)
.await
{
Ok(port) => return Ok(port),
Err(_) => continue,
}
}
Err(anyhow::anyhow!("No ports available"))
}
async fn add_random_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
let description = description.to_owned();
let external_port = common::random_port();
let res = self
.add_port_mapping(
protocol,
external_port,
local_addr,
lease_duration,
&description,
)
.await;
match res {
Ok(_) => Ok(external_port),
Err(_) => {
self.add_same_port_mapping(protocol, local_addr, lease_duration, &description)
.await
}
}
}
async fn add_same_port_mapping(
&self,
protocol: PortMappingProtocol,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<u16> {
let res = self
.add_port_mapping(
protocol,
local_addr.port(),
local_addr,
lease_duration,
description,
)
.await;
match res {
Ok(_) => Ok(local_addr.port()),
Err(err) => Err(anyhow::anyhow!("Add same port mapping failed: {}", err)),
}
}
async fn add_port_mapping(
&self,
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<()> {
self.perform_request(
messages::ADD_PORT_MAPPING_HEADER,
&messages::format_add_port_mapping_message(
self.control_schema
.get("AddPortMapping")
.ok_or_else(|| anyhow::anyhow!("Unsupported action: AddPortMapping"))?,
protocol,
external_port,
local_addr,
lease_duration,
description,
),
"AddPortMappingResponse",
)
.await?;
Ok(())
}
/// Add a port mapping.
///
/// The local_addr is the address where the traffic is sent to.
/// The lease_duration parameter is in seconds. A value of 0 is infinite.
pub async fn add_port(
&self,
protocol: PortMappingProtocol,
external_port: u16,
local_addr: SocketAddr,
lease_duration: u32,
description: &str,
) -> anyhow::Result<()> {
if external_port == 0 {
return Err(anyhow::anyhow!("External port zero is invalid"));
}
if local_addr.port() == 0 {
return Err(anyhow::anyhow!("Internal port zero is invalid"));
}
let res = self
.add_port_mapping(
protocol,
external_port,
local_addr,
lease_duration,
description,
)
.await;
if let Err(err) = res {
return Err(anyhow::anyhow!("Add port mapping failed: {}", err));
};
Ok(())
}
/// Remove a port mapping.
pub async fn remove_port(
&self,
protocol: PortMappingProtocol,
external_port: u16,
) -> anyhow::Result<()> {
let res = self
.perform_request(
messages::DELETE_PORT_MAPPING_HEADER,
&messages::format_delete_port_message(
self.control_schema
.get("DeletePortMapping")
.ok_or_else(|| anyhow::anyhow!("Unsupported action: DeletePortMapping"))?,
protocol,
external_port,
),
"DeletePortMappingResponse",
)
.await;
parsing::parse_delete_port_mapping_response(res)
}
/// Get one port mapping entry
///
/// Gets one port mapping entry by its index.
/// Not all existing port mappings might be visible to this client.
/// If the index is out of bound, GetGenericPortMappingEntryError::SpecifiedArrayIndexInvalid will be returned
pub async fn get_generic_port_mapping_entry(
&self,
index: u32,
) -> anyhow::Result<parsing::PortMappingEntry> {
let result = self
.perform_request(
messages::GET_GENERIC_PORT_MAPPING_ENTRY,
&messages::formate_get_generic_port_mapping_entry_message(index),
"GetGenericPortMappingEntryResponse",
)
.await;
parsing::parse_get_generic_port_mapping_entry(result)
}
}
impl<P> fmt::Display for Gateway<P> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "http://{}{}", self.addr, self.control_url)
}
}
impl<P> PartialEq for Gateway<P> {
fn eq(&self, other: &Gateway<P>) -> bool {
self.addr == other.addr && self.control_url == other.control_url
}
}
impl<P> Eq for Gateway<P> {}
impl<P> Hash for Gateway<P> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.addr.hash(state);
self.control_url.hash(state);
}
}

View File

@@ -0,0 +1,64 @@
mod common;
mod gateway;
mod search;
use std::fmt;
pub(crate) const MAX_RESPONSE_SIZE: usize = 1500;
pub(crate) const HEADER_NAME: &str = "SOAPAction";
/// Trait to allow abstracting over `tokio`.
#[async_trait::async_trait]
pub trait Provider {
/// Send an async request over the executor.
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String>;
}
/// Represents the protocols available for port mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortMappingProtocol {
/// TCP protocol
Tcp,
/// UDP protocol
Udp,
}
impl fmt::Display for PortMappingProtocol {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
PortMappingProtocol::Tcp => "TCP",
PortMappingProtocol::Udp => "UDP",
}
)
}
}
#[cfg(test)]
mod tests {
use crate::instance::upnp_igd::{
common::SearchOptions, search::search_gateway, PortMappingProtocol,
};
#[tokio::test]
async fn test_search_device() {
let ret = search_gateway(SearchOptions::default()).await.unwrap();
println!("{:?}", ret);
let external_ip = ret.get_external_ip().await.unwrap();
println!("{:?}", external_ip);
let add_port_ret = ret
.add_port(
PortMappingProtocol::Tcp,
51010,
"10.147.223.128:11010".parse().unwrap(),
1000,
"test",
)
.await;
println!("{:?}", add_port_ret);
}
}

View File

@@ -0,0 +1,250 @@
//! Tokio abstraction for the aio [`Gateway`].
use std::collections::HashMap;
use std::net::SocketAddr;
use http_req::response::Headers;
use tokio::{net::UdpSocket, time::timeout};
use super::common::options::{DEFAULT_TIMEOUT, RESPONSE_TIMEOUT};
use super::common::{messages, parsing, SearchOptions};
use super::gateway::Gateway;
use super::{Provider, HEADER_NAME, MAX_RESPONSE_SIZE};
use tracing::debug;
/// Tokio provider for the [`Gateway`].
#[derive(Debug, Clone)]
pub struct Tokio;
#[async_trait::async_trait]
impl Provider for Tokio {
async fn send_async(url: &str, action: &str, body: &str) -> anyhow::Result<String> {
use http_req::request;
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let url_owned = url.to_string();
let body_clone = body.to_string();
let action_clone = action.to_string();
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url_owned.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
println!("body: {body_clone}, action: {action_clone}");
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::POST)
.header(HEADER_NAME, &action_clone)
.header("Content-Type", "text/xml; charset=\"utf-8\"")
.body(body_clone.as_bytes())
.send(&mut response_body);
if response.is_err() {
if response_body.is_empty() {
anyhow::bail!("HTTP request error: {}", response.unwrap_err());
} else {
anyhow::bail!(
"HTTP request error: {} with response body: {}",
response.unwrap_err(),
String::from_utf8_lossy(&response_body)
);
}
}
let response = response.unwrap();
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
if response_body.is_empty() {
return Err(anyhow::anyhow!(
"HTTP error with empty body: {}",
response.status_code()
));
}
}
let string = String::from_utf8(response_body)
.map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
Ok(string)
}
}
/// Search for a gateway with the provided options.
pub async fn search_gateway(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
let search_timeout = options.timeout.unwrap_or(DEFAULT_TIMEOUT);
match timeout(search_timeout, search_gateway_inner(options)).await {
Ok(Ok(gateway)) => Ok(gateway),
Ok(Err(err)) => Err(err),
Err(_err) => {
// Timeout
Err(anyhow::anyhow!("No response within timeout"))
}
}
}
async fn search_gateway_inner(options: SearchOptions) -> anyhow::Result<Gateway<Tokio>> {
// Create socket for future calls
let mut socket = UdpSocket::bind(&options.bind_addr)
.await
.map_err(|e| anyhow::anyhow!("Failed to bind socket: {}", e))?;
send_search_request(&mut socket, options.broadcast_address).await?;
let response_timeout = options.single_search_timeout.unwrap_or(RESPONSE_TIMEOUT);
loop {
let search_response = receive_search_response(&mut socket);
// Receive search response
let (response_body, from) = match timeout(response_timeout, search_response).await {
Ok(Ok(v)) => v,
Ok(Err(err)) => {
debug!("error while receiving broadcast response: {err}");
continue;
}
Err(_) => {
debug!("timeout while receiving broadcast response");
continue;
}
};
let (addr, root_url) = match handle_broadcast_resp(&from, &response_body) {
Ok(v) => v,
Err(e) => {
debug!("error handling broadcast response: {}", e);
continue;
}
};
let (control_schema_url, control_url) = match get_control_urls(&addr, &root_url).await {
Ok(v) => v,
Err(e) => {
debug!("error getting control URLs: {}", e);
continue;
}
};
let control_schema = match get_control_schemas(&addr, &control_schema_url).await {
Ok(v) => v,
Err(e) => {
debug!("error getting control schemas: {}", e);
continue;
}
};
return Ok(Gateway {
addr,
root_url,
control_url,
control_schema_url,
control_schema,
provider: Tokio,
});
}
}
// Create a new search.
async fn send_search_request(socket: &mut UdpSocket, addr: SocketAddr) -> anyhow::Result<()> {
debug!(
"sending broadcast request to: {} on interface: {:?}",
addr,
socket.local_addr()
);
socket
.send_to(messages::SEARCH_REQUEST.as_bytes(), &addr)
.await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("Failed to send search request: {}", e))
}
async fn receive_search_response(socket: &mut UdpSocket) -> anyhow::Result<(Vec<u8>, SocketAddr)> {
let mut buff = [0u8; MAX_RESPONSE_SIZE];
let (n, from) = socket
.recv_from(&mut buff)
.await
.map_err(|e| anyhow::anyhow!("Failed to receive response: {}", e))?;
debug!("received broadcast response from: {}", from);
Ok((buff[..n].to_vec(), from))
}
// Handle a UDP response message.
fn handle_broadcast_resp(from: &SocketAddr, data: &[u8]) -> anyhow::Result<(SocketAddr, String)> {
debug!("handling broadcast response from: {}", from);
// Convert response to text.
let text =
std::str::from_utf8(data).map_err(|e| anyhow::anyhow!("UTF-8 conversion error: {}", e))?;
// Parse socket address and path.
let (addr, root_url) = parsing::parse_search_result(text)?;
Ok((addr, root_url))
}
async fn get_control_urls(addr: &SocketAddr, path: &str) -> anyhow::Result<(String, String)> {
use http_req::request;
let url = format!("http://{addr}{path}");
debug!("requesting control url from: {}", url);
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::GET)
.send(&mut response_body)
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
}
debug!("handling control response from: {addr}");
let c = std::io::Cursor::new(&response_body);
parsing::parse_control_urls(c)
}
async fn get_control_schemas(
addr: &SocketAddr,
control_schema_url: &str,
) -> anyhow::Result<HashMap<String, Vec<String>>> {
use http_req::request;
let url = format!("http://{addr}{control_schema_url}");
debug!("requesting control schema from: {}", url);
// Run the blocking HTTP request in a separate thread to avoid blocking the async runtime
let (response, response_body) = tokio::task::spawn_blocking(move || {
let uri = http_req::uri::Uri::try_from(url.as_str())
.map_err(|e| anyhow::anyhow!("URI parse error: {}", e))?;
let mut response_body = Vec::new();
let response = request::Request::new(&uri)
.method(request::Method::GET)
.send(&mut response_body)
.map_err(|e| anyhow::anyhow!("HTTP GET error: {}", e))?;
Ok::<_, anyhow::Error>((response, response_body))
})
.await
.map_err(|e| anyhow::anyhow!("Task join error: {}", e))??;
if !response.status_code().is_success() {
return Err(anyhow::anyhow!("HTTP error: {}", response.status_code()));
}
debug!("handling schema response from: {addr}");
let c = std::io::Cursor::new(&response_body);
parsing::parse_schemas(c)
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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()));

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -384,7 +384,11 @@ pub(crate) fn setup_sokcet2_ext(
unsafe {
let dev_idx = nix::libc::if_nametoindex(dev_name.as_str().as_ptr() as *const i8);
tracing::warn!(?dev_idx, ?dev_name, "bind device");
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
if bind_addr.is_ipv4() {
socket2_socket.bind_device_by_index_v4(std::num::NonZeroU32::new(dev_idx))?;
} else {
socket2_socket.bind_device_by_index_v6(std::num::NonZeroU32::new(dev_idx))?;
}
tracing::warn!(?dev_idx, ?dev_name, "bind device doen");
}
}

View File

@@ -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();

View File

@@ -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,25 @@ 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 dir = file_config.dir.as_deref().unwrap_or(".");
let file = file_config.file.as_deref().unwrap_or("easytier.log");
let path = std::path::Path::new(dir).join(file);
let path_str = path.to_string_lossy().into_owned();
let builder = RollingFileAppenderBase::builder();
let file_appender = builder
.filename(path_str)
.condition_daily()
.max_filecount(file_config.count.unwrap_or(10))
.condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024)
.build()
.unwrap();
let wrapper = FileAppenderWrapper::new(file_appender);
// Create a simple wrapper that implements MakeWriter
file_layer = Some(
l.with_writer(file_appender)
l.with_writer(wrapper)
.with_timer(get_logger_timer_rfc3339())
.with_filter(file_filter),
);

View File

@@ -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",
);