From 277052172c5829caef71da18447a18e35b6ee754 Mon Sep 17 00:00:00 2001 From: ssrlive <30760636+ssrlive@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:40:52 +0800 Subject: [PATCH] Android implementation --- .github/workflows/rust.yml | 22 +++++++ Cargo.toml | 4 ++ cbindgen.toml | 8 ++- src/android.rs | 130 +++++++++++++++++++++++++++++++++++++ src/config.rs | 16 +++++ src/lib.rs | 1 + 6 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/android.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cee061d..4d28bb8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,3 +26,25 @@ jobs: run: cargo clippy --all-targets --all-features -- -D warnings - name: Build run: cargo build --verbose --tests --all-features + + build_android: + strategy: + fail-fast: false + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Install cargo ndk and rust compiler for android target + if: ${{ !cancelled() }} + run: | + cargo install --locked cargo-ndk + rustup target add x86_64-linux-android + - name: clippy + if: ${{ !cancelled() }} + run: cargo ndk -t x86_64 clippy --all-features -- -D warnings + - name: Build + if: ${{ !cancelled() }} + run: cargo ndk -t x86_64 rustc --verbose --all-features --lib --crate-type=cdylib + - name: Abort on error + if: ${{ failure() }} + run: echo "Android build job failed" && false diff --git a/Cargo.toml b/Cargo.toml index 3ee275e..d74bb47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,7 @@ rand = "0.8" socks5-impl = "0.5" tokio = { version = "1", features = ["full"] } tokio-util = "0.7" + +[target.'cfg(target_os="android")'.dependencies] +android_logger = "0.14" +jni = { version = "0.21", default-features = false } diff --git a/cbindgen.toml b/cbindgen.toml index 5e1bff7..b7b4af8 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -2,7 +2,13 @@ language = "C" cpp_compat = true [export] -include = ["dns2socks_start", "dns2socks_stop", "dns2socks_set_log_callback"] +include = [ + "dns2socks_start", + "dns2socks_stop", + "dns2socks_set_log_callback", + "Java_com_github_shadowsocks_bg_Dns2socks_start", + "Java_com_github_shadowsocks_bg_Dns2socks_stop", +] exclude = [] [export.rename] diff --git a/src/android.rs b/src/android.rs new file mode 100644 index 0000000..537da2b --- /dev/null +++ b/src/android.rs @@ -0,0 +1,130 @@ +#![cfg(target_os = "android")] + +use crate::{main_entry, ArgVerbosity, Config}; +use jni::{ + objects::{JClass, JString}, + sys::{jboolean, jint}, + JNIEnv, +}; + +static TUN_QUIT: std::sync::Mutex> = std::sync::Mutex::new(None); + +/// # Safety +/// +/// Start dns2socks +/// Parameters: +/// - listen_addr: the listen address, e.g. "172.19.0.1:53", or null to use the default value +/// - dns_remote_server: the dns remote server, e.g. "8.8.8.8:53", or null to use the default value +/// - socks5_server: the socks5 server, e.g. "127.0.0.1:1080", or null to use the default value +/// - username: the username for socks5 authentication, or null to use the default value +/// - password: the password for socks5 authentication, or null to use the default value +/// - force_tcp: whether to force tcp, true or false, default is false +/// - cache_records: whether to cache dns records, true or false, default is false +/// - verbosity: the verbosity level, see ArgVerbosity enum, default is ArgVerbosity::Info +/// - timeout: the timeout in seconds, default is 5 +#[no_mangle] +pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Dns2socks_start( + mut env: JNIEnv, + _clazz: JClass, + listen_addr: JString, + dns_remote_server: JString, + socks5_server: JString, + username: JString, + password: JString, + force_tcp: jboolean, + cache_records: jboolean, + verbosity: jint, + timeout: jint, +) -> jint { + let verbosity: ArgVerbosity = verbosity.try_into().unwrap_or_default(); + let filter_str = &format!("off,dns2socks={verbosity}"); + let filter = android_logger::FilterBuilder::new().parse(filter_str).build(); + android_logger::init_once( + android_logger::Config::default() + .with_tag("dns2socks") + .with_max_level(log::LevelFilter::Trace) + .with_filter(filter), + ); + + let listen_addr = match get_java_string(&mut env, &listen_addr) { + Ok(addr) => addr, + Err(_e) => "0.0.0.0:53".to_string(), + }; + let dns_remote_server = match get_java_string(&mut env, &dns_remote_server) { + Ok(addr) => addr, + Err(_e) => "8.8.8.8:53".to_string(), + }; + let socks5_server = match get_java_string(&mut env, &socks5_server) { + Ok(addr) => addr, + Err(_e) => "127.0.0.1:1080".to_string(), + }; + let username = match get_java_string(&mut env, &username) { + Ok(addr) => Some(addr), + Err(_e) => None, + }; + let password = match get_java_string(&mut env, &password) { + Ok(addr) => Some(addr), + Err(_e) => None, + }; + let force_tcp = force_tcp != 0; + let cache_records = cache_records != 0; + let timeout = if timeout < 3 { 5 } else { timeout as u64 }; + + let shutdown_token = tokio_util::sync::CancellationToken::new(); + if let Ok(mut lock) = TUN_QUIT.lock() { + if lock.is_some() { + return -1; + } + *lock = Some(shutdown_token.clone()); + } else { + return -2; + } + + let main_loop = async move { + let mut cfg = Config::default(); + cfg.verbosity(verbosity) + .timeout(timeout) + .force_tcp(force_tcp) + .cache_records(cache_records) + .listen_addr(listen_addr.parse()?) + .dns_remote_server(dns_remote_server.parse()?) + .socks5_server(socks5_server.parse()?) + .username(username) + .password(password); + + if let Err(err) = main_entry(cfg, shutdown_token).await { + log::error!("main loop error: {}", err); + return Err(err); + } + Ok(()) + }; + + let exit_code = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { + Err(_e) => -3, + Ok(rt) => match rt.block_on(main_loop) { + Ok(_) => 0, + Err(_e) => -4, + }, + }; + + exit_code +} + +/// # Safety +/// +/// Shutdown dns2socks +#[no_mangle] +pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Dns2socks_stop(_env: JNIEnv, _: JClass) -> jint { + if let Ok(mut lock) = TUN_QUIT.lock() { + if let Some(shutdown_token) = lock.take() { + shutdown_token.cancel(); + return 0; + } + } + -1 +} + +fn get_java_string(env: &mut JNIEnv, string: &JString) -> std::io::Result { + use std::io::{Error, ErrorKind::Other}; + Ok(env.get_string(string).map_err(|e| Error::new(Other, e))?.into()) +} diff --git a/src/config.rs b/src/config.rs index c5ad53d..5be605c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -157,3 +157,19 @@ impl From for log::LevelFilter { } } } + +impl TryFrom for ArgVerbosity { + type Error = std::io::Error; + + fn try_from(value: i32) -> Result>::Error> { + match value { + 0 => Ok(ArgVerbosity::Off), + 1 => Ok(ArgVerbosity::Error), + 2 => Ok(ArgVerbosity::Warn), + 3 => Ok(ArgVerbosity::Info), + 4 => Ok(ArgVerbosity::Debug), + 5 => Ok(ArgVerbosity::Trace), + _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid verbosity level")), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7bd186d..5cb320c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod android; mod api; mod config; mod dns;