feat(android): add JNI build and VPN support

This commit is contained in:
lbl
2026-08-22 21:55:00 +08:00
parent 60a4403f0e
commit b7151880c0
7 changed files with 163 additions and 48 deletions
+102 -2
View File
@@ -86,7 +86,6 @@ jobs:
NAME: Vnt
TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }}
FEATURES: ${{ matrix.FEATURES }}
steps:
- uses: actions/checkout@v5
- name: Replace Web static files with current frontend build
@@ -295,10 +294,103 @@ jobs:
name: Vnt-${{ matrix.TARGET }}
path: |
./artifacts/*
build-android-jni:
name: Build Android JNI (${{ matrix.abi }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-linux-android
abi: arm64-v8a
clang: aarch64-linux-android24-clang
cargo_env: AARCH64_LINUX_ANDROID
cc_env: CC_aarch64_linux_android
ar_env: AR_aarch64_linux_android
- target: armv7-linux-androideabi
abi: armeabi-v7a
clang: armv7a-linux-androideabi24-clang
cargo_env: ARMV7_LINUX_ANDROIDEABI
cc_env: CC_armv7_linux_androideabi
ar_env: AR_armv7_linux_androideabi
- target: x86_64-linux-android
abi: x86_64
clang: x86_64-linux-android24-clang
cargo_env: X86_64_LINUX_ANDROID
cc_env: CC_x86_64_linux_android
ar_env: AR_x86_64_linux_android
steps:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Rust target
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Set up Android SDK
uses: android-actions/setup-android@v4
- name: Install Android NDK r27d
run: sdkmanager "ndk;27.3.13750724"
- name: Cache Cargo dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: android-jni-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}
restore-keys: android-jni-${{ matrix.target }}-
- name: Build vnt-jni
env:
TARGET: ${{ matrix.target }}
CLANG: ${{ matrix.clang }}
CARGO_ENV: ${{ matrix.cargo_env }}
CC_ENV: ${{ matrix.cc_env }}
AR_ENV: ${{ matrix.ar_env }}
run: |
set -euo pipefail
NDK_HOME="$ANDROID_SDK_ROOT/ndk/27.3.13750724"
toolchain="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
export PATH="$toolchain:$PATH"
export "CARGO_TARGET_${CARGO_ENV}_LINKER=$toolchain/$CLANG"
export "CARGO_TARGET_${CARGO_ENV}_RUSTFLAGS=-C link-arg=-Wl,-z,max-page-size=16384"
export "$CC_ENV=$toolchain/$CLANG"
export "$AR_ENV=$toolchain/llvm-ar"
cargo build --locked --release -p vnt-jni --target "$TARGET"
- name: Verify and package native library
env:
TARGET: ${{ matrix.target }}
ABI: ${{ matrix.abi }}
run: |
set -euo pipefail
NDK_HOME="$ANDROID_SDK_ROOT/ndk/27.3.13750724"
library="target/$TARGET/release/libvnt_jni.so"
readelf="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
nm="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm"
test -s "$library"
"$nm" -D --defined-only "$library" | grep -q Java_com_vnt_VntManager_nativeInit
"$readelf" -l "$library" | awk '/ LOAD / { found=1; if ($NF != "0x4000") bad=1 } END { exit (!found || bad) }'
mkdir -p artifacts
asset="artifacts/vnt-jni-${ABI}-${GITHUB_REF_NAME}.so"
cp "$library" "$asset"
(cd artifacts && sha256sum "$(basename "$asset")" > "$(basename "$asset").sha256")
- name: Upload Android JNI artifact
uses: actions/upload-artifact@v4
with:
name: Vnt-Android-${{ matrix.abi }}
path: artifacts/vnt-jni-${{ matrix.abi }}-${{ github.ref_name }}.so*
if-no-files-found: error
# deploys to github releases on tag
deploy:
if: startsWith(github.ref, 'refs/tags/')
needs: build
needs: [build, build-android-jni]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
@@ -315,6 +407,14 @@ jobs:
tag: ${{ github.ref }}
overwrite: true
file_glob: true
- name: Release Android JNI libraries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./artifacts/Vnt-Android-*/*.so*
tag: ${{ github.ref }}
overwrite: true
file_glob: true
desktop-windows:
name: Build and publish VNT Desktop for Windows
Generated
+3 -3
View File
@@ -2805,9 +2805,9 @@ dependencies = [
[[package]]
name = "machine-uid"
version = "0.5.4"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7217d573cdb141d6da43113b098172e057d39915d79c4bdedbc3aacd46bd96"
checksum = "cfe0d6336d341b10ae80e099b8f3c59f34bf8aef66ef25e83aa98bf2955c0ae7"
dependencies = [
"libc",
"windows-registry",
@@ -6351,7 +6351,7 @@ dependencies = [
"uuid",
"widestring",
"winapi",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"zerocopy",
]
+4 -2
View File
@@ -44,7 +44,6 @@ reed-solomon-erasure = "6.0"
serde.workspace = true
rcgen = "0.14.6"
machine-uid = "0.5.4"
zerocopy = { version = "0.8.31", features = ["derive"] }
socket2 = { version = "0.6.1", features = ["all"] }
@@ -52,11 +51,14 @@ socket2 = { version = "0.6.1", features = ["all"] }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3.9", features = ["winreg"] }
widestring = "1.2"
windows-sys = { version = "0.60.2", features = ["Win32_Networking_WinSock"] }
windows-sys = { version = "0.61", features = ["Win32_Networking_WinSock"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(not(target_os = "android"))'.dependencies]
machine-uid = "0.6"
[build-dependencies]
prost-build = "0.14"
protoc-bin-vendored = "3"
+1
View File
@@ -23,6 +23,7 @@ use crate::tunnel_core::server::rpc::ServerRPC;
use crate::utils::task_control::TaskGroup;
use anyhow::bail;
use ipnet::Ipv4Net;
#[cfg(not(target_os = "android"))]
use std::net::Ipv4Addr;
pub const DEFAULT_MTU: u16 = 1380;
+15 -1
View File
@@ -10,7 +10,9 @@ use std::net::Ipv4Addr;
use std::sync::Arc;
use tokio::sync::mpsc::{Receiver, Sender};
use tun_rs::async_framed::{Decoder, DeviceFramedRead, DeviceFramedWrite, Encoder};
use tun_rs::{AsyncDevice, DeviceBuilder};
use tun_rs::AsyncDevice;
#[cfg(not(target_os = "android"))]
use tun_rs::DeviceBuilder;
#[derive(Clone)]
pub struct DeviceIOManager {
@@ -19,6 +21,7 @@ pub struct DeviceIOManager {
}
type DeviceMutex = Arc<tokio::sync::Mutex<(Option<DeviceTask>, Option<(Ipv4Addr, u8)>)>>;
pub struct DeviceTask {
#[cfg_attr(target_os = "android", allow(dead_code))]
device: Arc<AsyncDevice>,
task: SubTask,
}
@@ -104,6 +107,7 @@ impl DeviceIOManager {
bail!("device doesn't exist")
}
}
#[cfg(not(target_os = "android"))]
pub async fn set_network(&self, ip: Ipv4Addr, prefix_len: u8) -> anyhow::Result<()> {
let mut guard = self.device.lock().await;
let Some(dev) = guard.0.as_ref() else {
@@ -124,6 +128,15 @@ impl DeviceIOManager {
}
fn create_tun(config: DeviceConfig) -> anyhow::Result<AsyncDevice> {
#[cfg(target_os = "android")]
{
let fd = config.tun_fd.context("Android requires a VpnService TUN fd")?;
// SAFETY: The fd comes directly from ParcelFileDescriptor returned by
// VpnService.Builder.establish and remains open for the network lifetime.
return unsafe { Ok(AsyncDevice::from_fd(fd)?) };
}
#[cfg(not(target_os = "android"))]
{
#[cfg(unix)]
if let Some(fd) = config.tun_fd {
// SAFETY: Caller must ensure fd is a valid, open file descriptor for a TUN device.
@@ -151,6 +164,7 @@ fn create_tun(config: DeviceConfig) -> anyhow::Result<AsyncDevice> {
_ = dev.set_tx_queue_len(1000);
}
Ok(dev)
}
}
fn create(
task_group: &TaskGroup,
+1
View File
@@ -2,6 +2,7 @@ use anyhow::Context;
use std::fs;
use std::path::PathBuf;
pub fn get_device_id() -> anyhow::Result<String> {
#[cfg(not(target_os = "android"))]
match machine_uid::get() {
Ok(id) => return Ok(id),
Err(e) => {
+37 -40
View File
@@ -300,54 +300,51 @@ pub extern "system" fn Java_com_vnt_VntNetwork_nativeSetNetworkIp<'local>(
ip: JString<'local>,
prefix_len: jint,
) -> jboolean {
jni_guard!(env, 0, {
let result: anyhow::Result<()> = (|| {
let (network_manager_arc, runtime) = {
let mut global_state = GLOBAL_STATE.lock();
let state = global_state.as_mut().context("VNT not initialized")?;
#[cfg(target_os = "android")]
{
let _ = (handle, ip, prefix_len);
let _ = env.throw("set_network_ip is not supported on Android");
0
}
#[cfg(not(target_os = "android"))]
{
jni_guard!(env, 0, {
let result: anyhow::Result<()> = (|| {
let (network_manager_arc, runtime) = {
let mut global_state = GLOBAL_STATE.lock();
let state = global_state.as_mut().context("VNT not initialized")?;
let network_manager_arc = state
.network_managers
.get(&handle)
.context("Invalid handle")?
.clone();
let network_manager_arc = state
.network_managers
.get(&handle)
.context("Invalid handle")?
.clone();
let runtime = state.runtime.clone();
(network_manager_arc, runtime)
};
let runtime = state.runtime.clone();
(network_manager_arc, runtime)
};
let ip_str: String = env.get_string(&ip)?.into();
let ip_addr: Ipv4Addr = ip_str.parse().context("Invalid IP address")?;
let manager_lock = network_manager_arc.lock();
let manager = manager_lock
.as_ref()
.context("Network manager already destroyed")?;
#[cfg(not(target_os = "android"))]
{
let ip_str: String = env.get_string(&ip)?.into();
let ip_addr: Ipv4Addr = ip_str.parse().context("Invalid IP address")?;
let manager_lock = network_manager_arc.lock();
let manager = manager_lock
.as_ref()
.context("Network manager already destroyed")?;
runtime.block_on(async {
manager.set_tun_network_ip(ip_addr, prefix_len as u8).await
})?;
}
Ok(())
})();
#[cfg(target_os = "android")]
{
let _ = (ip_addr, prefix_len); // 避免未使用警告
anyhow::bail!("set_network_ip is not supported on Android");
match result {
Ok(_) => 1,
Err(e) => {
let _ = env.throw(format!("Failed to set network IP: {:?}", e));
0
}
}
Ok(())
})();
match result {
Ok(_) => 1,
Err(e) => {
let _ = env.throw(format!("Failed to set network IP: {:?}", e));
0
}
}
})
})
}
}
/// 获取VntApi实例