From 56edbd3656d6d71e26fd08dd7be79d710dd4b950 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Mon, 10 Aug 2026 00:15:17 +0800 Subject: [PATCH] fix(connection): keep healthy tunnel ports stable Closes #5716 --- crates/dbx-core/src/connection.rs | 61 +++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index c20dbc3a8..7c752c97c 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -3995,8 +3995,11 @@ impl AppState { } } + let mut detached_pool_keys = Vec::new(); for (key, client, replace_runtime) in failed_agent_checks { - self.detach_agent_pool_if_current(&key, &client, replace_runtime).await; + if self.detach_agent_pool_if_current(&key, &client, replace_runtime).await { + detached_pool_keys.push(key); + } } // Remove dead pools @@ -4020,13 +4023,20 @@ impl AppState { } } drop(conns); + detached_pool_keys.extend(removed.iter().map(|(key, _)| key.clone())); self.pool_routing_control().finish_detach(removed).await; } - // Re-establish SSH tunnels that have died - let tunnel_connection_ids: Vec = { + // Only failed pools may require a fresh transport. Healthy tunnel listeners must keep + // their local ports stable across app resume and visibility-triggered health checks. + let tunnel_connection_ids: HashSet = { let configs = self.configs.read().await; - configs.iter().filter(|(_, c)| c.has_effective_transport_layers()).map(|(id, _)| id.clone()).collect() + detached_pool_keys + .iter() + .filter_map(|pool_key| config_for_pool_key(pool_key, &configs)) + .filter(|config| config.has_effective_transport_layers()) + .map(|config| config.id.clone()) + .collect() }; for connection_id in tunnel_connection_ids { self.reset_connection_transport(&connection_id).await; @@ -7218,6 +7228,21 @@ for line in sys.stdin: let (state, dir) = test_app_state().await; let (runtime, target_client, sibling_client) = replace_runtime_on_error_clients(&dir, "validate_connection").await; + let mut config = mysql_config(None); + config.transport_layers = vec![TransportLayerConfig::Proxy(ProxyTunnelConfig { + profile_id: String::new(), + id: "proxy".to_string(), + name: String::new(), + enabled: true, + proxy_type: ProxyType::Socks5, + host: "127.0.0.1".to_string(), + port: 65000, + username: String::new(), + password: String::new(), + test_target: None, + })]; + state.configs.write().await.insert(config.id.clone(), config.clone()); + state.connection_host_port(&config.id, &config).await.unwrap(); { let mut connections = state.connections.write().await; connections.insert("conn:analytics".to_string(), PoolKind::Agent(target_client)); @@ -7227,10 +7252,38 @@ for line in sys.stdin: state.refresh_connections().await; assert!(state.connections.read().await.is_empty()); + assert!(state.proxy_tunnels.local_port("conn:transport:0").await.is_none()); assert!(runtime.is_failed()); let _ = std::fs::remove_dir_all(dir); } + #[tokio::test] + async fn global_health_preserves_transport_for_connections_without_failed_pools() { + let (state, dir) = test_app_state().await; + let mut config = mysql_config(None); + config.id = "proxied-connection".to_string(); + config.transport_layers = vec![TransportLayerConfig::Proxy(ProxyTunnelConfig { + profile_id: String::new(), + id: "proxy".to_string(), + name: String::new(), + enabled: true, + proxy_type: ProxyType::Socks5, + host: "127.0.0.1".to_string(), + port: 65000, + username: String::new(), + password: String::new(), + test_target: None, + })]; + state.configs.write().await.insert(config.id.clone(), config.clone()); + let (_, local_port) = state.connection_host_port(&config.id, &config).await.unwrap(); + + state.refresh_connections().await; + + assert_eq!(state.proxy_tunnels.local_port("proxied-connection:transport:0").await, Some(local_port)); + state.reset_connection_transport(&config.id).await; + let _ = std::fs::remove_dir_all(dir); + } + #[tokio::test] async fn stale_agent_failure_preserves_newer_pool_generation() { let (state, dir) = test_app_state().await;