diff --git a/ostp-client/src/app.rs b/ostp-client/src/app.rs index 0b6c80f..3b6f6b9 100644 --- a/ostp-client/src/app.rs +++ b/ostp-client/src/app.rs @@ -40,6 +40,9 @@ pub enum BridgeCommand { ToggleTunnel, NextProfile, ReloadConfig, + /// Triggered by Android NetworkCallback when the active network changes (WiFi→LTE, etc.). + /// Causes an immediate background reconnect without waiting for stall detection. + NetworkChanged, Shutdown, } diff --git a/ostp-client/src/bridge.rs b/ostp-client/src/bridge.rs index 7b59dbf..d98d0d5 100644 --- a/ostp-client/src/bridge.rs +++ b/ostp-client/src/bridge.rs @@ -338,6 +338,73 @@ impl Bridge { tx.send(UiEvent::ProfileChanged(self.profile)).await.ok(); tx.send(UiEvent::Log(format!("Obfuscation profile switched to {:?}", self.profile))).await.ok(); } + Some(BridgeCommand::NetworkChanged) => { + if self.running { + // Network changed (e.g. WiFi→LTE): IP address changed, existing UDP + // socket is dead. Trigger immediate reconnect without waiting for stall. + let _ = tx.send(UiEvent::Log("Network changed — starting immediate reconnect".to_string())).await; + self.metrics.connection_state.store(1, Ordering::Relaxed); + self.last_valid_recv = Instant::now() - Duration::from_secs(100); // force stall path + + let session_count = if self.mux_enabled { self.mux_sessions.max(1) } else { 1 }; + let (udp_tx, udp_rx) = mpsc::channel(100000); + let mut new_sessions = Vec::with_capacity(session_count); + let mut successful_sessions = 0; + let mut rtt_sum = 0.0; + + for idx in 0..session_count { + let session_id: u32 = rand::thread_rng().gen(); + match self.perform_handshake_with_id(&tx, session_id).await { + Ok((sock, mach, rtt)) => { + let session_index = new_sessions.len(); + let socket = Arc::new(sock); + let socket_clone = socket.clone(); + let udp_tx_clone = udp_tx.clone(); + let is_turn = self.turn_enabled; + tokio::spawn(async move { + let mut buf = vec![0_u8; 65535]; + loop { + match socket_clone.recv(&mut buf).await { + Ok(n) => { + let inbound = if is_turn && n >= 4 && buf[0] == 0x40 && buf[1] == 0x00 { + let len = u16::from_be_bytes([buf[2], buf[3]]) as usize; + if 4 + len <= n { Bytes::copy_from_slice(&buf[4..4+len]) } else { Bytes::copy_from_slice(&buf[..n]) } + } else { + Bytes::copy_from_slice(&buf[..n]) + }; + if udp_tx_clone.send((session_index, inbound)).await.is_err() { break; } + } + Err(e) => { + tracing::warn!("UDP recv error (network-change session {}): {}", session_index, e); + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + } + } + }); + new_sessions.push(SessionState { socket, machine: mach }); + rtt_sum += rtt; + successful_sessions += 1; + } + Err(err) => { + let _ = tx.send(UiEvent::Log(format!("NetworkChanged reconnect session {}/{} failed: {}", idx + 1, session_count, err))).await; + } + } + } + + if !new_sessions.is_empty() { + sessions_opt = Some(new_sessions); + udp_rx_opt = Some(udp_rx); + self.last_rtt_ms = rtt_sum / successful_sessions as f64; + self.last_valid_recv = Instant::now(); + stream_map.clear(); + self.reset_proxy_streams(&tx, &proxy_tx, "network changed"); + self.metrics.connection_state.store(2, Ordering::Relaxed); + let _ = tx.send(UiEvent::Log("NetworkChanged reconnect successful!".to_string())).await; + } else { + let _ = tx.send(UiEvent::Log("NetworkChanged reconnect failed — will retry on keepalive tick".to_string())).await; + } + } + } Some(BridgeCommand::ReloadConfig) => { match ClientConfig::reload_from_json_near_binary() { Ok(cfg) => { @@ -374,7 +441,7 @@ impl Bridge { _ = keepalive_tick.tick() => { if self.running { // 1. Connection Liveness Check & Silent Background Reconnect - if self.last_valid_recv.elapsed().as_secs() > 25 { + if self.last_valid_recv.elapsed().as_secs() > 8 { let elapsed = self.last_valid_recv.elapsed().as_secs(); if elapsed > 180 { // Hard timeout after 3 minutes of total silence diff --git a/ostp-jni/OstpClientSdk.kt b/ostp-jni/OstpClientSdk.kt index f29d8b4..4d8d7e3 100644 --- a/ostp-jni/OstpClientSdk.kt +++ b/ostp-jni/OstpClientSdk.kt @@ -36,6 +36,7 @@ class OstpClientSdk private constructor(private val context: Context) { private external fun nativeStopClient(): Boolean private external fun nativeGetMetrics(): String private external fun nativeGetLogs(): String + private external fun notifyNetworkChanged() // ── Public data models ──────────────────────────────────────────────────── @@ -263,28 +264,19 @@ class OstpClientSdk private constructor(private val context: Context) { val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - // Network came back (e.g. switched from WiFi to LTE) - // If we're not connected, trigger a reconnect by bouncing the native client - if (_state.value !is TunnelState.Connected && started.get()) { - emitLog("Network available — triggering reconnect") - scope.launch { - nativeStopClient() - delay(500L) - val json = config.toNativeJson() - val ok = nativeStartClient(json) - if (!ok) { - _state.value = TunnelState.Failed("Reconnect failed after network change") - } else { - _state.value = TunnelState.Connecting - } - } - } + if (!started.get()) return + // Network became available (WiFi→LTE, tower switch, etc.) + // Send a lightweight BridgeCommand::NetworkChanged to Rust so the bridge + // immediately reconnects on the new interface without a full stop/start. + emitLog("Network available — signalling Rust bridge for immediate reconnect") + _state.value = TunnelState.Connecting + notifyNetworkChanged() } override fun onLost(network: Network) { if (_state.value is TunnelState.Connected) { _state.value = TunnelState.Reconnecting("Network lost", 0) - emitLog("Network lost — waiting for reconnect") + emitLog("Network lost — waiting for new network") } } } diff --git a/ostp-jni/src/lib.rs b/ostp-jni/src/lib.rs index 3d4119c..b0c3f49 100644 --- a/ostp-jni/src/lib.rs +++ b/ostp-jni/src/lib.rs @@ -353,3 +353,23 @@ pub extern "system" fn Java_net_ostp_client_OstpClientSdk_addLog( add_log(text); } } + +/// Called by Android NetworkCallback when the active network changes (WiFi→LTE, etc.). +/// Sends BridgeCommand::NetworkChanged to trigger an immediate reconnect in the Rust bridge. +#[no_mangle] +pub extern "system" fn Java_net_ostp_client_OstpClientSdk_notifyNetworkChanged( + _env: JNIEnv, + _class: JClass, +) { + let state = match STATE.lock() { + Ok(s) => s, + Err(_) => return, + }; + + if let Some(ref cmd_tx) = state.cmd_tx { + // Use try_send since we're likely on a background thread from Android's ConnectivityManager + let _ = cmd_tx.try_send(ostp_client::app::BridgeCommand::NetworkChanged); + add_log("notifyNetworkChanged: BridgeCommand::NetworkChanged sent".to_string()); + } +} +