From b750faa66fa6298ff9b0b4aef76b8d071330d50e Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 6 Sep 2025 13:49:42 +0800 Subject: [PATCH] add android jni (#1340) --- .github/workflows/core.yml | 2 +- .github/workflows/install_rust.sh | 4 +- Cargo.lock | 41 +++ Cargo.toml | 1 + .../easytier-android-jni/Cargo.toml | 16 + .../easytier-android-jni/README.md | 267 +++++++++++++++ .../easytier-android-jni/build.sh | 125 +++++++ .../easytier-android-jni/example_config.toml | 56 +++ .../kotlin/com/easytier/jni/EasyTierJNI.kt | 78 +++++ .../com/easytier/jni/EasyTierManager.kt | 252 ++++++++++++++ .../com/easytier/jni/EasyTierVpnService.t.kt | 143 ++++++++ .../kotlin/com/easytier/jni/README.md | 41 +++ .../easytier-android-jni/src/lib.rs | 319 ++++++++++++++++++ 13 files changed, 1342 insertions(+), 3 deletions(-) create mode 100644 easytier-contrib/easytier-android-jni/Cargo.toml create mode 100644 easytier-contrib/easytier-android-jni/README.md create mode 100755 easytier-contrib/easytier-android-jni/build.sh create mode 100644 easytier-contrib/easytier-android-jni/example_config.toml create mode 100644 easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierJNI.kt create mode 100644 easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierManager.kt create mode 100644 easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/EasyTierVpnService.t.kt create mode 100644 easytier-contrib/easytier-android-jni/kotlin/com/easytier/jni/README.md create mode 100644 easytier-contrib/easytier-android-jni/src/lib.rs diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index d44aee9..3d0e809 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 0c7176c..778a78b 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 5aa6088..6f2f51d 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 2101797..0768f4f 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 0000000..86aa083 --- /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 0000000..1062f40 --- /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 0000000..42a81de --- /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 0000000..0f85246 --- /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 0000000..8f1b0ef --- /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 0000000..e8330ec --- /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 0000000..596582c --- /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 0000000..d1ae26d --- /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 0000000..4f32a96 --- /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(), + } +}