mirror of https://github.com/ospab/ostp.git
feat: NetworkChanged command for instant mobile reconnect, lower stall threshold 25s->8s
This commit is contained in:
parent
baff58c7fb
commit
0cc5cf47ef
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue