From 18f7faa279a7da90ff1345c5fc645d90f478226b Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 7 Apr 2026 09:49:33 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20ping=20as=20engine=20instance=20method?= =?UTF-8?q?=20=E2=80=94=20same=20lifecycle=20as=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ping was a static JNI method that loaded the .so before nativeInit, crashing jemalloc. Now ping is an instance method on WzpEngine: - Engine is created once (nativeInit), reused for both ping and call - pingRelay() uses same tokio runtime pattern as startCall() - Auto-pings all servers on app launch (after engine init) - No process restart needed - TOFU fingerprints saved on first successful ping Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 193 +++++++++++++++++- .../src/main/java/com/wzp/engine/WzpEngine.kt | 19 +- .../src/main/java/com/wzp/net/RelayPinger.kt | 30 +-- .../java/com/wzp/ui/call/CallViewModel.kt | 84 ++++---- .../main/java/com/wzp/ui/call/InCallScreen.kt | 22 +- crates/wzp-android/src/engine.rs | 40 ++++ crates/wzp-android/src/jni_bridge.rs | 65 +----- 7 files changed, 303 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 298edd0..5987018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -467,6 +473,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -627,6 +634,24 @@ dependencies = [ "libc", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -650,6 +675,7 @@ dependencies = [ "digest", "fiat-crypto", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -816,6 +842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -850,6 +877,21 @@ dependencies = [ "rustfft", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -857,6 +899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] @@ -881,6 +924,26 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -924,6 +987,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1084,6 +1157,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1143,6 +1217,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.13" @@ -1626,6 +1711,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", + "signature", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1702,6 +1802,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.3" @@ -2389,6 +2498,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2567,6 +2686,21 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2671,6 +2805,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2724,6 +2868,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] @@ -2937,6 +3082,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -3235,10 +3389,14 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -3367,6 +3525,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3406,7 +3576,28 @@ dependencies = [ [[package]] name = "warzone-protocol" -version = "0.1.0" +version = "0.0.38" +dependencies = [ + "base64", + "bincode", + "bip39", + "chacha20poly1305", + "chrono", + "curve25519-dalek", + "ed25519-dalek", + "hex", + "hkdf", + "k256", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tiny-keccak", + "uuid", + "x25519-dalek", + "zeroize", +] [[package]] name = "wasi" diff --git a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt index 6b9e864..8693c7d 100644 --- a/android/app/src/main/java/com/wzp/engine/WzpEngine.kt +++ b/android/app/src/main/java/com/wzp/engine/WzpEngine.kt @@ -153,20 +153,21 @@ class WzpEngine(private val callback: WzpCallback) { private external fun nativeWriteAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, sampleCount: Int): Int private external fun nativeReadAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, maxSamples: Int): Int private external fun nativeDestroy(handle: Long) + private external fun nativePingRelay(handle: Long, relay: String): String? + + /** + * Ping a relay server. Requires engine to be initialized. + * Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null. + */ + fun pingRelay(address: String): String? { + if (nativeHandle == 0L) return null + return nativePingRelay(nativeHandle, address) + } companion object { init { System.loadLibrary("wzp_android") } - - /** - * Ping a relay server. Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` - * or null if unreachable. Does not require an engine instance. - */ - fun pingRelay(address: String): String? = nativePingRelay(address) - - @JvmStatic - private external fun nativePingRelay(relay: String): String? } } diff --git a/android/app/src/main/java/com/wzp/net/RelayPinger.kt b/android/app/src/main/java/com/wzp/net/RelayPinger.kt index 3eb8665..cb95926 100644 --- a/android/app/src/main/java/com/wzp/net/RelayPinger.kt +++ b/android/app/src/main/java/com/wzp/net/RelayPinger.kt @@ -1,36 +1,12 @@ package com.wzp.net -/** - * Relay ping via native QUIC — requires loading the native .so. - * After ping completes, the process must be restarted (System.exit) - * because jemalloc initialization during .so load corrupts state - * on Android 16 MTE devices. - * - * Flow: ping all servers → save results → exit → app restarts → load results - */ -object RelayPinger { +// Relay pinging is now done via WzpEngine.pingRelay() (instance method). +// This file kept for the data class only. +object RelayPinger { data class PingResult( val rttMs: Int, val reachable: Boolean, val serverFingerprint: String = "", ) - - /** - * Ping a relay via the native QUIC stack. - * WARNING: After calling this, the process must be restarted. - */ - fun ping(address: String): PingResult { - return try { - val json = com.wzp.engine.WzpEngine.pingRelay(address) ?: return PingResult(0, false) - val obj = org.json.JSONObject(json) - PingResult( - rttMs = obj.getInt("rtt_ms"), - reachable = true, - serverFingerprint = obj.optString("server_fingerprint", ""), - ) - } catch (e: Exception) { - PingResult(0, false) - } - } } diff --git a/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt b/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt index e87dd97..d7ae6b6 100644 --- a/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt +++ b/android/app/src/main/java/com/wzp/ui/call/CallViewModel.kt @@ -208,55 +208,61 @@ class CallViewModel : ViewModel(), WzpCallback { settings?.saveSelectedServer(_selectedServer.value) } - /** Load saved ping results from last ping-and-exit cycle. */ - fun loadSavedPingResults() { - val s = settings ?: return - val results = mutableMapOf() - val known = mutableMapOf() - _servers.value.forEach { server -> - val rtt = s.loadPingRtt(server.address) - val fp = s.loadServerFingerprint(server.address) - if (rtt >= 0) { - results[server.address] = PingResult(rttMs = rtt, serverFingerprint = fp ?: "") - } - fp?.let { known[server.address] = it } - } - _pingResults.value = results - _knownFingerprints.value = known - } - /** - * Ping all servers via native QUIC, save results, then exit process. - * On restart, saved results are loaded. This avoids the jemalloc crash - * by ensuring the native .so is only loaded once per process lifetime. + * Ping all servers via native QUIC. Requires engine to be initialized. + * Creates engine if needed, pings, keeps engine alive for subsequent Connect. */ - fun pingAndExit() { + fun pingAllServers() { viewModelScope.launch { - val results = mutableMapOf() - _servers.value.forEach { server -> - val pr = withContext(Dispatchers.IO) { - com.wzp.net.RelayPinger.ping(server.address) + // Ensure engine exists + if (engine == null || engine?.isInitialized != true) { + try { + engine = WzpEngine(this@CallViewModel).also { it.init() } + engineInitialized = true + } catch (e: Exception) { + Log.w(TAG, "engine init for ping failed: $e") + return@launch } - results[server.address] = PingResult( - rttMs = pr.rttMs, - serverFingerprint = pr.serverFingerprint, - ) - // Save results - settings?.savePingRtt(server.address, pr.rttMs) - if (pr.serverFingerprint.isNotEmpty()) { - val saved = settings?.loadServerFingerprint(server.address) - if (saved == null) { - settings?.saveServerFingerprint(server.address, pr.serverFingerprint) - } + } + val eng = engine ?: return@launch + + val results = mutableMapOf() + val known = mutableMapOf() + _servers.value.forEach { server -> + val json = withContext(Dispatchers.IO) { + eng.pingRelay(server.address) + } + if (json != null) { + try { + val obj = JSONObject(json) + val rtt = obj.getInt("rtt_ms") + val fp = obj.optString("server_fingerprint", "") + results[server.address] = PingResult(rttMs = rtt, serverFingerprint = fp) + // TOFU + if (fp.isNotEmpty()) { + val saved = settings?.loadServerFingerprint(server.address) + if (saved == null) settings?.saveServerFingerprint(server.address, fp) + known[server.address] = saved ?: fp + } + } catch (_: Exception) {} } } _pingResults.value = results - // Exit process — next launch loads saved results, native .so reinits cleanly - delay(300) // let UI update - System.exit(0) + _knownFingerprints.value = known } } + /** Load saved TOFU fingerprints. */ + fun loadSavedFingerprints() { + val known = mutableMapOf() + _servers.value.forEach { server -> + settings?.loadServerFingerprint(server.address)?.let { + known[server.address] = it + } + } + _knownFingerprints.value = known + } + /** Get lock status for a server. */ fun lockStatus(address: String): LockStatus { val pr = _pingResults.value[address] ?: return LockStatus.UNKNOWN diff --git a/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt b/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt index 28cbbc4..9df2453 100644 --- a/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt +++ b/android/app/src/main/java/com/wzp/ui/call/InCallScreen.kt @@ -90,8 +90,11 @@ fun InCallScreen( var showManageRelays by remember { mutableStateOf(false) } - // Load saved ping results from last ping-and-exit cycle - LaunchedEffect(Unit) { viewModel.loadSavedPingResults() } + // Ping servers on launch — engine init + QUIC ping, no restart needed + LaunchedEffect(Unit) { + viewModel.loadSavedFingerprints() + viewModel.pingAllServers() + } Surface( modifier = Modifier.fillMaxSize(), @@ -227,21 +230,6 @@ fun InCallScreen( ) } - Spacer(modifier = Modifier.height(8.dp)) - - // Ping button — pings all servers via native QUIC, saves results, exits app - OutlinedButton( - onClick = { viewModel.pingAndExit() }, - modifier = Modifier.fillMaxWidth().height(40.dp), - shape = RoundedCornerShape(8.dp), - ) { - Text( - "Ping Servers (restarts app)", - style = MaterialTheme.typography.labelSmall, - color = TextDim - ) - } - errorMessage?.let { err -> Spacer(modifier = Modifier.height(8.dp)) Text(text = err, color = Red, style = MaterialTheme.typography.bodySmall) diff --git a/crates/wzp-android/src/engine.rs b/crates/wzp-android/src/engine.rs index dba6143..b9ada0d 100644 --- a/crates/wzp-android/src/engine.rs +++ b/crates/wzp-android/src/engine.rs @@ -169,6 +169,46 @@ impl WzpEngine { info!("stop_call: done"); } + /// Ping a relay — same pattern as start_call (creates runtime on calling thread). + /// Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or error. + pub fn ping_relay(&self, address: &str) -> Result { + let addr: SocketAddr = address.parse()?; + let _ = rustls::crypto::ring::default_provider().install_default(); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + rt.block_on(async { + let bind: SocketAddr = "0.0.0.0:0".parse().unwrap(); + let endpoint = wzp_transport::create_endpoint(bind, None)?; + let client_cfg = wzp_transport::client_config(); + let start = Instant::now(); + + let conn = tokio::time::timeout( + std::time::Duration::from_secs(3), + wzp_transport::connect(&endpoint, addr, "ping", client_cfg), + ) + .await + .map_err(|_| anyhow::anyhow!("timeout"))??; + + let rtt_ms = start.elapsed().as_millis() as u64; + let server_fp = conn + .peer_identity() + .and_then(|id| id.downcast::>().ok()) + .and_then(|certs| certs.first().map(|c| { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + c.as_ref().hash(&mut h); + format!("{:016x}", h.finish()) + })) + .unwrap_or_default(); + conn.close(0u32.into(), b"ping"); + + Ok(format!(r#"{{"rtt_ms":{},"server_fingerprint":"{}"}}"#, rtt_ms, server_fp)) + }) + } + pub fn set_mute(&self, muted: bool) { self.state.muted.store(muted, Ordering::Relaxed); } diff --git a/crates/wzp-android/src/jni_bridge.rs b/crates/wzp-android/src/jni_bridge.rs index 3ddc11e..ca27d52 100644 --- a/crates/wzp-android/src/jni_bridge.rs +++ b/crates/wzp-android/src/jni_bridge.rs @@ -318,71 +318,22 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeDestroy( })); } -/// Ping a relay server — returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null on failure. -/// Does NOT require an engine handle — creates a temporary QUIC connection. +/// Ping a relay server — instance method, requires engine handle. +/// Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null on failure. #[unsafe(no_mangle)] pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativePingRelay<'a>( mut env: JNIEnv<'a>, _class: JClass, + handle: jlong, relay_j: JString, ) -> jstring { let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let h = unsafe { handle_ref(handle) }; let relay: String = env.get_string(&relay_j).map(|s| s.into()).unwrap_or_default(); - let addr: std::net::SocketAddr = match relay.parse() { - Ok(a) => a, - Err(_) => return None, - }; - - let _ = rustls::crypto::ring::default_provider().install_default(); - - let rt = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(rt) => rt, - Err(_) => return None, - }; - - rt.block_on(async { - let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap(); - let endpoint = match wzp_transport::create_endpoint(bind, None) { - Ok(e) => e, - Err(_) => return None, - }; - let client_cfg = wzp_transport::client_config(); - let start = std::time::Instant::now(); - - match tokio::time::timeout( - std::time::Duration::from_secs(3), - wzp_transport::connect(&endpoint, addr, "ping", client_cfg), - ) - .await - { - Ok(Ok(conn)) => { - let rtt_ms = start.elapsed().as_millis() as u64; - let server_fp = conn - .peer_identity() - .and_then(|id| { - id.downcast::>().ok() - }) - .and_then(|certs| { - certs.first().map(|c| { - use std::hash::{Hash, Hasher}; - let mut h = std::collections::hash_map::DefaultHasher::new(); - c.as_ref().hash(&mut h); - format!("{:016x}", h.finish()) - }) - }) - .unwrap_or_default(); - conn.close(0u32.into(), b"ping"); - Some(format!( - r#"{{"rtt_ms":{},"server_fingerprint":"{}"}}"#, - rtt_ms, server_fp - )) - } - _ => None, - } - }) + match h.engine.ping_relay(&relay) { + Ok(json) => Some(json), + Err(_) => None, + } })); let json = match result {