diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index d44aee97..3d0e8093 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -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
diff --git a/.github/workflows/install_rust.sh b/.github/workflows/install_rust.sh
index 0c7176cb..778a78b7 100644
--- a/.github/workflows/install_rust.sh
+++ b/.github/workflows/install_rust.sh
@@ -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.
diff --git a/Cargo.lock b/Cargo.lock
index 5aa60888..6f2f51d7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
@@ -2213,6 +2231,19 @@ dependencies = [
"zstd",
]
+[[package]]
+name = "easytier-android-jni"
+version = "0.1.0"
+dependencies = [
+ "android_logger",
+ "easytier",
+ "jni",
+ "log",
+ "once_cell",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "easytier-ffi"
version = "0.1.0"
@@ -2517,6 +2548,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "log",
+ "regex",
+]
+
[[package]]
name = "equivalent"
version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 2101797c..0768f4f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 = [
diff --git a/easytier-contrib/easytier-android-jni/Cargo.toml b/easytier-contrib/easytier-android-jni/Cargo.toml
new file mode 100644
index 00000000..86aa0837
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/Cargo.toml
@@ -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" }
\ No newline at end of file
diff --git a/easytier-contrib/easytier-android-jni/README.md b/easytier-contrib/easytier-android-jni/README.md
new file mode 100644
index 00000000..1062f400
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/README.md
@@ -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
+
+
+
+```
+
+## 使用示例
+
+### 基本使用
+
+```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 infos = EasyTierJNI.collectNetworkInfosAsMap(10);
+ for (Map.Entry 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 |
+| `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/)
\ No newline at end of file
diff --git a/easytier-contrib/easytier-android-jni/build.sh b/easytier-contrib/easytier-android-jni/build.sh
new file mode 100755
index 00000000..42a81dec
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/build.sh
@@ -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 类的方法"
\ No newline at end of file
diff --git a/easytier-contrib/easytier-android-jni/example_config.toml b/easytier-contrib/easytier-android-jni/example_config.toml
new file mode 100644
index 00000000..0f85246e
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/example_config.toml
@@ -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
\ No newline at end of file
diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt
new file mode 100644
index 00000000..8f1b0efe
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt
@@ -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?): 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))
+ }
+}
diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierManager.kt b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierManager.kt
new file mode 100644
index 00000000..e8330ec6
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierManager.kt
@@ -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 = 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()
+ 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) {
+ try {
+ // 先停止现有的 VpnService
+ stopVpnService()
+
+ // 启动新的 VpnService
+ startVpnService(ipv4, proxyCidrs)
+ } catch (e: Exception) {
+ Log.e(TAG, "重启 VpnService 时发生异常", e)
+ }
+ }
+
+ /** 启动 VpnService */
+ private fun startVpnService(ipv4: String, proxyCidrs: List) {
+ 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
+ )
+}
diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierVpnService.t.kt b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierVpnService.t.kt
new file mode 100644
index 00000000..596582c7
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierVpnService.t.kt
@@ -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) {
+ 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 {
+ 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 {
+ 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()
+ }
+}
diff --git a/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/README.md b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/README.md
new file mode 100644
index 00000000..d1ae26d9
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/README.md
@@ -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 组件。
\ No newline at end of file
diff --git a/easytier-contrib/easytier-android-jni/src/lib.rs b/easytier-contrib/easytier-android-jni/src/lib.rs
new file mode 100644
index 00000000..4f32a969
--- /dev/null
+++ b/easytier-contrib/easytier-android-jni/src/lib.rs
@@ -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 {
+ 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 {
+ 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::(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(),
+ }
+}