add android jni (#1340)

This commit is contained in:
Sijie.Sun
2025-09-06 13:49:42 +08:00
committed by GitHub
parent ef3309814d
commit b750faa66f
13 changed files with 1342 additions and 3 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

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

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

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

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