feat: enable udpgw via tun2proxy CLI API — no fork needed (#271)

Uses tun2proxy_run_with_cli_args (the C API) via dlsym instead of
modifying the JNI run() signature. The upstream tun2proxy maintainer
recommended this path — the CLI API accepts --udpgw-server natively.

- Cargo.toml: enable udpgw feature, remove [patch.crates-io]
- MhrvVpnService.kt: build CLI args with --udpgw-server in full mode
- Native.kt + android_jni.rs: dlsym wrapper for the C API
- Tun2proxy.kt: reverted to upstream signature

No fork, no patch, no submodule.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yyoyoian-pixel
2026-04-26 20:13:20 +02:00
committed by GitHub
parent e81974c204
commit 536aa0078f
6 changed files with 74 additions and 21 deletions
Generated
+2 -1
View File
@@ -3961,7 +3961,8 @@ dependencies = [
[[package]]
name = "tun2proxy"
version = "0.7.21"
source = "git+https://github.com/yyoyoian-pixel/tun2proxy?branch=feat%2Fudpgw-jni-param#dfc24ed12cdee69987bdd321ea55c6b940f2d0f0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336ad07beb04a9e219972fcdc54a71d2586cdfd35ac03551a629e4ca328db3c"
dependencies = [
"android_logger",
"async-trait",
-6
View File
@@ -100,12 +100,6 @@ tun2proxy = { version = "0.7", default-features = false, features = ["udpgw"] }
# Used in mitm tests to sanity-check the cert extensions we emit.
x509-parser = "0.16"
# Temporary patch: adds udpgw_server parameter to the Android JNI run()
# function. Upstream PR: https://github.com/tun2proxy/tun2proxy/pull/247
# Remove this section once tun2proxy >= 0.8 ships with the change.
[patch.crates-io]
tun2proxy = { git = "https://github.com/yyoyoian-pixel/tun2proxy", branch = "feat/udpgw-jni-param" }
[profile.release]
panic = "abort"
codegen-units = 1
@@ -59,7 +59,6 @@ object Tun2proxy {
tunMtu: Char,
verbosity: Int,
dnsStrategy: Int,
udpgwServer: String,
): Int
/** Signals the running `run()` to shut down. Idempotent. */
@@ -259,21 +259,20 @@ class MhrvVpnService : VpnService() {
// the sole owner once it's running.
val detachedFd = parcelFd.detachFd()
tun2proxyRunning.set(true)
// In full mode, enable udpgw so UDP traffic (DNS, QUIC, …) is
// forwarded through the tunnel-node's native udpgw handler.
// 198.18.0.1:7300 is a magic address the tunnel-node intercepts.
val udpgwAddr = if (cfg.mode == Mode.FULL) "198.18.0.1:7300" else ""
// Use tun2proxy_run_with_cli_args C API via dlsym — gives full
// CLI flexibility including --udpgw-server, no fork needed.
val cliArgs = buildString {
append("tun2proxy")
append(" --proxy socks5://127.0.0.1:$socks5Port")
append(" --tun-fd $detachedFd")
append(" --dns virtual")
append(" --verbosity info")
append(" --close-fd-on-drop true")
if (cfg.mode == Mode.FULL) append(" --udpgw-server 198.18.0.1:7300")
}
val worker = Thread({
try {
val rc = Tun2proxy.run(
"socks5://127.0.0.1:$socks5Port",
detachedFd,
/* closeFdOnDrop = */ true,
MTU.toChar(),
/* verbosity = info */ 3,
/* dnsStrategy = virtual */ 0,
udpgwAddr,
)
val rc = Native.runTun2proxy(cliArgs, MTU)
Log.i(TAG, "tun2proxy exited rc=$rc")
} catch (t: Throwable) {
Log.e(TAG, "tun2proxy crashed: ${t.message}", t)
@@ -95,4 +95,14 @@ object Native {
* Cheap — just reads atomics. Safe to poll on a second-scale timer.
*/
external fun statsJson(handle: Long): String
/**
* Start tun2proxy via its CLI args C API (`tun2proxy_run_with_cli_args`).
* Resolved at runtime via dlsym from libtun2proxy.so — no fork needed.
*
* @param cliArgs full CLI string, e.g. "tun2proxy --proxy socks5://... --tun-fd 42 --udpgw-server 198.18.0.1:7300"
* @param tunMtu TUN MTU (typically 1500)
* @return 0 on normal shutdown, negative on error. BLOCKS.
*/
external fun runTun2proxy(cliArgs: String, tunMtu: Int): Int
}
+50
View File
@@ -482,3 +482,53 @@ pub extern "system" fn Java_com_therealaleph_mhrv_Native_statsJson<'a>(
}));
env.new_string(out).map(|s| s.into_raw()).unwrap_or(std::ptr::null_mut())
}
// ---------------------------------------------------------------------------
// tun2proxy CLI API wrapper (dlsym — no fork or patch needed)
// ---------------------------------------------------------------------------
/// `Native.runTun2proxy(cliArgs, tunMtu)` -> int
///
/// Calls `tun2proxy_run_with_cli_args` from libtun2proxy.so via dlsym.
/// This is the C API the tun2proxy maintainer recommends for callers that
/// need full CLI flexibility (e.g. --udpgw-server). BLOCKS until shutdown.
#[no_mangle]
pub extern "system" fn Java_com_therealaleph_mhrv_Native_runTun2proxy<'a>(
mut env: JNIEnv<'a>,
_class: JClass,
cli_args: JString,
tun_mtu: jni::sys::jint,
) -> jni::sys::jint {
safe(-1, AssertUnwindSafe(|| {
let args_str = jstring_to_string(&mut env, &cli_args);
tracing::info!("runTun2proxy: cli={}", args_str);
unsafe {
use std::ffi::{CStr, CString};
let lib = CString::new("libtun2proxy.so").unwrap();
let handle = libc::dlopen(lib.as_ptr(), libc::RTLD_NOW);
if handle.is_null() {
let err = CStr::from_ptr(libc::dlerror());
tracing::error!("dlopen libtun2proxy.so failed: {:?}", err);
return -10;
}
let sym = CString::new("tun2proxy_run_with_cli_args").unwrap();
let func = libc::dlsym(handle, sym.as_ptr());
if func.is_null() {
let err = CStr::from_ptr(libc::dlerror());
tracing::error!("dlsym tun2proxy_run_with_cli_args: {:?}", err);
libc::dlclose(handle);
return -11;
}
type RunFn = unsafe extern "C" fn(*const std::ffi::c_char, u16, bool) -> i32;
let run: RunFn = std::mem::transmute(func);
let c_args = CString::new(args_str).unwrap();
let rc = run(c_args.as_ptr(), tun_mtu as u16, false);
libc::dlclose(handle);
rc
}
}))
}