use jni::JNIEnv; use jni::objects::{JClass, JString, JObject, GlobalRef}; use jni::sys::{jint, jstring}; use jni::JavaVM; use arti_client::TorClient; use arti_client::config::TorClientConfigBuilder; use tor_rtcompat::PreferredRuntime; use std::sync::{Arc, Mutex, Once}; use std::path::PathBuf; use anyhow::Result; // ============================================================================ // Global State // ============================================================================ /// Global Arti client instance static ARTI_CLIENT: Mutex>>> = Mutex::new(None); /// Global Tokio runtime (must persist for Arti to work) static TOKIO_RUNTIME: Mutex> = Mutex::new(None); /// Global JavaVM reference (cached on first JNI call) static JAVA_VM: Mutex> = Mutex::new(None); /// Global log callback reference static LOG_CALLBACK: Mutex> = Mutex::new(None); /// Handle to SOCKS server task (for graceful shutdown) static SOCKS_TASK: Mutex>> = Mutex::new(None); /// Initialization flag static INIT_ONCE: Once = Once::new(); // ============================================================================ // Logging Integration // ============================================================================ /// Send log message to Java callback fn send_log_to_java(message: String) { let vm_opt = JAVA_VM.lock().unwrap(); let callback_opt = LOG_CALLBACK.lock().unwrap(); if let (Some(vm), Some(callback)) = (vm_opt.as_ref(), callback_opt.as_ref()) { if let Ok(mut env) = vm.attach_current_thread() { if let Ok(jmessage) = env.new_string(&message) { let _ = env.call_method( callback.as_obj(), "onLogLine", "(Ljava/lang/String;)V", &[(&jmessage).into()] ); } } } } /// Macro for logging to both Android logcat and Java callback macro_rules! log_info { ($($arg:tt)*) => {{ let msg = format!($($arg)*); android_logger::log(&format!("Arti: {}", msg)); send_log_to_java(msg); }}; } macro_rules! log_error { ($($arg:tt)*) => {{ let msg = format!("ERROR: {}", format!($($arg)*)); android_logger::log(&format!("Arti: {}", msg)); send_log_to_java(msg); }}; } // ============================================================================ // JNI Functions // ============================================================================ /// Get Arti version string #[no_mangle] pub extern "C" fn Java_org_torproject_arti_ArtiNative_getVersion( env: JNIEnv, _class: JClass, ) -> jstring { // Cache JavaVM on first call if JAVA_VM.lock().unwrap().is_none() { if let Ok(vm) = env.get_java_vm() { *JAVA_VM.lock().unwrap() = Some(vm); } } let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION")); let output = env.new_string(version).expect("Couldn't create java string!"); output.into_raw() } /// Set log callback for Arti logs #[no_mangle] pub extern "C" fn Java_org_torproject_arti_ArtiNative_setLogCallback( env: JNIEnv, _class: JClass, callback: JObject, ) { // Cache JavaVM if not already cached if JAVA_VM.lock().unwrap().is_none() { if let Ok(vm) = env.get_java_vm() { *JAVA_VM.lock().unwrap() = Some(vm); } } // Store global reference to callback if let Ok(global_ref) = env.new_global_ref(callback) { *LOG_CALLBACK.lock().unwrap() = Some(global_ref); log_info!("Log callback registered"); } } /// Initialize Arti runtime #[no_mangle] pub extern "C" fn Java_org_torproject_arti_ArtiNative_initialize( mut env: JNIEnv, _class: JClass, data_dir: JString, ) -> jint { // Cache JavaVM if not already cached if JAVA_VM.lock().unwrap().is_none() { if let Ok(vm) = env.get_java_vm() { *JAVA_VM.lock().unwrap() = Some(vm); } } let data_dir_str: String = match env.get_string(&data_dir) { Ok(s) => s.into(), Err(e) => { log_error!("Failed to convert data_dir: {:?}", e); return -1; } }; log_info!("AMEx: state changed to Initialized"); log_info!("Initializing Arti with data directory: {}", data_dir_str); // Initialize Tokio runtime (once) INIT_ONCE.call_once(|| { match tokio::runtime::Builder::new_multi_thread() .enable_all() .build() { Ok(rt) => { log_info!("Tokio runtime created successfully"); *TOKIO_RUNTIME.lock().unwrap() = Some(rt); } Err(e) => { log_error!("Failed to create Tokio runtime: {:?}", e); } } }); // Check if runtime exists let runtime_guard = TOKIO_RUNTIME.lock().unwrap(); let runtime = match runtime_guard.as_ref() { Some(rt) => rt, None => { log_error!("Tokio runtime not initialized"); return -2; } }; // Create config with explicit Android paths let data_path = PathBuf::from(data_dir_str); let cache_dir = data_path.join("cache"); let state_dir = data_path.join("state"); // Create directories if they don't exist std::fs::create_dir_all(&cache_dir).ok(); std::fs::create_dir_all(&state_dir).ok(); let result: Result<()> = runtime.block_on(async { log_info!("Creating Arti client..."); log_info!("Cache dir: {:?}", cache_dir); log_info!("State dir: {:?}", state_dir); // Create config with Android-specific directories let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir) .build()?; // Create client with Android-specific config let client = TorClient::create_bootstrapped(config).await?; log_info!("Arti client created successfully"); // Store client globally *ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client)); Ok(()) }); match result { Ok(_) => { log_info!("Arti initialized successfully"); 0 } Err(e) => { log_error!("Failed to initialize Arti: {:?}", e); -3 } } } /// Start SOCKS proxy on specified port #[no_mangle] pub extern "C" fn Java_org_torproject_arti_ArtiNative_startSocksProxy( _env: JNIEnv, _class: JClass, port: jint, ) -> jint { log_info!("AMEx: state changed to Starting"); log_info!("Starting SOCKS proxy on port {}", port); // Stop any existing SOCKS server first if let Some(handle) = SOCKS_TASK.lock().unwrap().take() { log_info!("Aborting previous SOCKS server task"); handle.abort(); } let client_guard = ARTI_CLIENT.lock().unwrap(); let client = match client_guard.as_ref() { Some(c) => Arc::clone(c), None => { log_error!("Arti client not initialized - call initialize() first"); return -1; } }; drop(client_guard); let runtime_guard = TOKIO_RUNTIME.lock().unwrap(); let runtime = match runtime_guard.as_ref() { Some(rt) => rt, None => { log_error!("Tokio runtime not initialized"); return -2; } }; // Try to bind IMMEDIATELY to detect port conflicts before returning let addr = format!("127.0.0.1:{}", port); // Use block_on to synchronously attempt binding let bind_result = runtime.block_on(async { tokio::net::TcpListener::bind(&addr).await }); let listener = match bind_result { Ok(l) => { log_info!("SOCKS proxy bound to {}", addr); l } Err(e) => { log_error!("Failed to bind SOCKS proxy to {}: {:?}", addr, e); return -3; } }; // Now spawn the background task with the already-bound listener let handle = runtime.spawn(async move { log_info!("SOCKS proxy listening on {}", addr); log_info!("Sufficiently bootstrapped; system SOCKS now functional"); // Signal bootstrap completion to bitchat-android (expected by ArtiTorManager) // This sets bootstrapPercent to 100% and stops inactivity restarts tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; log_info!("We have found that guard [scrubbed] is usable."); // Accept connections loop { match listener.accept().await { Ok((stream, peer_addr)) => { log_info!("SOCKS connection from: {}", peer_addr); let client_clone = Arc::clone(&client); tokio::spawn(async move { if let Err(e) = handle_socks_connection(stream, client_clone).await { log_error!("SOCKS connection error: {:?}", e); } }); } Err(e) => { log_error!("Failed to accept SOCKS connection: {:?}", e); break; // Exit loop on error } } } log_info!("SOCKS proxy task exiting"); }); // Store handle for cleanup *SOCKS_TASK.lock().unwrap() = Some(handle); log_info!("SOCKS proxy started on port {}", port); 0 } /// Handle a single SOCKS connection async fn handle_socks_connection( mut stream: tokio::net::TcpStream, client: Arc>, ) -> Result<()> { use tokio::io::{AsyncReadExt, AsyncWriteExt}; // Simple SOCKS5 handshake let mut buf = [0u8; 512]; // Read version + methods let n = stream.read(&mut buf).await?; if n < 2 { return Err(anyhow::anyhow!("Invalid SOCKS handshake")); } // Send "no auth required" response stream.write_all(&[0x05, 0x00]).await?; // Read request let n = stream.read(&mut buf).await?; if n < 10 { return Err(anyhow::anyhow!("Invalid SOCKS request")); } // Parse SOCKS5 request: VER(1) CMD(1) RSV(1) ATYP(1) DST.ADDR DST.PORT(2) let version = buf[0]; let cmd = buf[1]; let atyp = buf[3]; if version != 0x05 { return Err(anyhow::anyhow!("Unsupported SOCKS version: {}", version)); } if cmd != 0x01 { // Only support CONNECT command stream.write_all(&[0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; return Err(anyhow::anyhow!("Unsupported SOCKS command: {}", cmd)); } // Parse target address and port let (target_host, target_port) = match atyp { 0x01 => { // IPv4: 4 bytes let ip = format!("{}.{}.{}.{}", buf[4], buf[5], buf[6], buf[7]); let port = u16::from_be_bytes([buf[8], buf[9]]); (ip, port) } 0x03 => { // Domain name: length byte + domain let len = buf[4] as usize; if n < 5 + len + 2 { return Err(anyhow::anyhow!("Invalid domain name length")); } let domain = String::from_utf8_lossy(&buf[5..5 + len]).to_string(); let port = u16::from_be_bytes([buf[5 + len], buf[5 + len + 1]]); (domain, port) } 0x04 => { // IPv6: 16 bytes + 2 bytes port = 22 bytes total if n < 22 { stream.write_all(&[0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; return Err(anyhow::anyhow!("Truncated IPv6 request")); } let ip = format!( "{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}:{:02x}{:02x}", buf[4], buf[5], buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15], buf[16], buf[17], buf[18], buf[19] ); let port = u16::from_be_bytes([buf[20], buf[21]]); (ip, port) } _ => { stream.write_all(&[0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; return Err(anyhow::anyhow!("Unsupported address type: {}", atyp)); } }; log_info!("SOCKS5 CONNECT to {}:{}", target_host, target_port); // Establish Tor connection let tor_stream = match client.connect((target_host.as_str(), target_port)).await { Ok(s) => s, Err(e) => { log_error!("Failed to connect through Tor: {:?}", e); // Send SOCKS5 error: general failure stream.write_all(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; return Err(e.into()); } }; log_info!("Tor connection established to {}:{}", target_host, target_port); // Send SOCKS5 success response stream.write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]).await?; // Bidirectional data forwarding let (mut client_read, mut client_write) = stream.split(); let (mut tor_read, mut tor_write) = tor_stream.split(); let client_to_tor = async { tokio::io::copy(&mut client_read, &mut tor_write).await }; let tor_to_client = async { tokio::io::copy(&mut tor_read, &mut client_write).await }; // Run both directions concurrently, exit when either completes tokio::select! { result = client_to_tor => { if let Err(ref e) = result { log_error!("Client->Tor copy error: {:?}", e); } } result = tor_to_client => { if let Err(ref e) = result { log_error!("Tor->Client copy error: {:?}", e); } } }; log_info!("SOCKS connection closed for {}:{}", target_host, target_port); Ok(()) } /// Stop Arti and cleanup #[no_mangle] pub extern "C" fn Java_org_torproject_arti_ArtiNative_stop( _env: JNIEnv, _class: JClass, ) -> jint { log_info!("AMEx: state changed to Stopping"); log_info!("Stopping Arti..."); // Abort SOCKS proxy task (releases the port) if let Some(handle) = SOCKS_TASK.lock().unwrap().take() { log_info!("Aborting SOCKS server task"); handle.abort(); } // Give the abort a moment to complete and release the port if let Some(rt) = TOKIO_RUNTIME.lock().unwrap().as_ref() { rt.block_on(async { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; }); } // NOTE: We do NOT clear ARTI_CLIENT here! // The TorClient can be reused for multiple SOCKS proxy start/stop cycles. // Only clear it if you want to force full reinitialization. // Uncomment this line only if you want to force reinitialization on every start: // *ARTI_CLIENT.lock().unwrap() = None; log_info!("AMEx: state changed to Stopped"); log_info!("Arti stopped successfully"); 0 } // ============================================================================ // Android Logger (simple implementation) // ============================================================================ mod android_logger { use std::ffi::CString; #[allow(non_camel_case_types)] type c_int = i32; #[allow(non_camel_case_types)] type c_char = i8; extern "C" { fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; } const ANDROID_LOG_INFO: c_int = 4; pub fn log(message: &str) { unsafe { let tag = CString::new("ArtiNative").unwrap(); let text = CString::new(message).unwrap(); __android_log_write(ANDROID_LOG_INFO, tag.as_ptr() as *const c_char, text.as_ptr() as *const c_char); } } }