From b7151880c0ffa5ff004a184befcb59f4a98bce4b Mon Sep 17 00:00:00 2001 From: lbl <1791778603@qq.com> Date: Sat, 22 Aug 2026 21:55:00 +0800 Subject: [PATCH] feat(android): add JNI build and VPN support --- .github/workflows/rust.yml | 104 +++++++++++++++++++++++++++++++- Cargo.lock | 6 +- vnt-core/Cargo.toml | 6 +- vnt-core/src/core/mod.rs | 1 + vnt-core/src/tun/general.rs | 16 ++++- vnt-core/src/utils/device_id.rs | 1 + vnt-jni/src/lib.rs | 77 ++++++++++++----------- 7 files changed, 163 insertions(+), 48 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 024f808..0239ee3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 99d9e49..7b9fd98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/vnt-core/Cargo.toml b/vnt-core/Cargo.toml index e3d52e1..15e6d83 100644 --- a/vnt-core/Cargo.toml +++ b/vnt-core/Cargo.toml @@ -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" diff --git a/vnt-core/src/core/mod.rs b/vnt-core/src/core/mod.rs index 79137ad..ab82420 100644 --- a/vnt-core/src/core/mod.rs +++ b/vnt-core/src/core/mod.rs @@ -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; diff --git a/vnt-core/src/tun/general.rs b/vnt-core/src/tun/general.rs index eb1255d..39d5614 100644 --- a/vnt-core/src/tun/general.rs +++ b/vnt-core/src/tun/general.rs @@ -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, Option<(Ipv4Addr, u8)>)>>; pub struct DeviceTask { + #[cfg_attr(target_os = "android", allow(dead_code))] device: Arc, 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 { + #[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 { _ = dev.set_tx_queue_len(1000); } Ok(dev) + } } fn create( task_group: &TaskGroup, diff --git a/vnt-core/src/utils/device_id.rs b/vnt-core/src/utils/device_id.rs index 6d9e7fb..7dc23aa 100644 --- a/vnt-core/src/utils/device_id.rs +++ b/vnt-core/src/utils/device_id.rs @@ -2,6 +2,7 @@ use anyhow::Context; use std::fs; use std::path::PathBuf; pub fn get_device_id() -> anyhow::Result { + #[cfg(not(target_os = "android"))] match machine_uid::get() { Ok(id) => return Ok(id), Err(e) => { diff --git a/vnt-jni/src/lib.rs b/vnt-jni/src/lib.rs index 4d79f2d..f59f237 100644 --- a/vnt-jni/src/lib.rs +++ b/vnt-jni/src/lib.rs @@ -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实例