diff --git a/src-tauri/src/extension_manager.rs b/src-tauri/src/extension_manager.rs index 6485231..6d12b2c 100644 --- a/src-tauri/src/extension_manager.rs +++ b/src-tauri/src/extension_manager.rs @@ -362,7 +362,7 @@ impl ExtensionManager { } } - extensions.sort_by(|a, b| a.created_at.cmp(&b.created_at)); + extensions.sort_by_key(|a| a.created_at); Ok(extensions) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bd65a59..631a9ee 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1422,11 +1422,43 @@ pub fn run() { // Without this cleanup, users on Windows accumulate dozens of idle workers // (one per profile launch) that the periodic cleanup won't touch because // profile-associated workers are deliberately skipped to avoid regressions. + // + // Preserves workers whose associated profile still has a running browser + // process — if the app crashed while a browser was running, its detached + // browser keeps going and needs the proxy/VPN worker to stay alive. tauri::async_runtime::spawn(async move { use crate::proxy_storage::{delete_proxy_config, is_process_running, list_proxy_configs}; use crate::vpn_worker_storage::{delete_vpn_worker_config, list_vpn_worker_configs}; + // Build sets of (profile_id, vpn_id) whose browsers are still running + let profile_manager = crate::profile::ProfileManager::instance(); + let profiles = profile_manager.list_profiles().unwrap_or_default(); + + let running_profile_ids: std::collections::HashSet = profiles + .iter() + .filter(|p| p.process_id.is_some_and(is_process_running)) + .map(|p| p.id.to_string()) + .collect(); + + let running_vpn_ids: std::collections::HashSet = profiles + .iter() + .filter(|p| p.process_id.is_some_and(is_process_running)) + .filter_map(|p| p.vpn_id.clone()) + .collect(); + for config in list_proxy_configs() { + let has_running_browser = config + .profile_id + .as_ref() + .is_some_and(|pid| running_profile_ids.contains(pid)); + if has_running_browser { + log::info!( + "Startup: preserving proxy worker {} (profile browser still running)", + config.id + ); + continue; + } + if let Some(pid) = config.pid { if is_process_running(pid) { log::info!( @@ -1442,6 +1474,15 @@ pub fn run() { } for worker in list_vpn_worker_configs() { + if running_vpn_ids.contains(&worker.vpn_id) { + log::info!( + "Startup: preserving VPN worker {} (profile browser using vpn_id {} still running)", + worker.id, + worker.vpn_id + ); + continue; + } + if let Some(pid) = worker.pid { if is_process_running(pid) { log::info!( diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs index 1376ae5..e3e72c3 100644 --- a/src-tauri/src/sync/engine.rs +++ b/src-tauri/src/sync/engine.rs @@ -230,11 +230,7 @@ impl SyncProgressTracker { let elapsed = self.start_time.elapsed().as_secs_f64().max(0.1); let speed = (completed_bytes as f64 / elapsed) as u64; let remaining_bytes = self.total_bytes.saturating_sub(completed_bytes); - let eta = if speed > 0 { - remaining_bytes / speed - } else { - 0 - }; + let eta = remaining_bytes.checked_div(speed).unwrap_or(0); let _ = events::emit( "profile-sync-progress", diff --git a/src-tauri/src/vpn/config.rs b/src-tauri/src/vpn/config.rs index d7827ac..2a36830 100644 --- a/src-tauri/src/vpn/config.rs +++ b/src-tauri/src/vpn/config.rs @@ -141,11 +141,15 @@ pub fn parse_wireguard_config(content: &str) -> Result = HashMap::new(); let mut current_section: Option<&str> = None; + // Strip a UTF-8 BOM if present — some editors/tools emit one and it would + // otherwise prepend invisible bytes to the first section header + let content = content.strip_prefix('\u{feff}').unwrap_or(content); + for line in content.lines() { let line = line.trim(); // Skip empty lines and comments - if line.is_empty() || line.starts_with('#') { + if line.is_empty() || line.starts_with('#') || line.starts_with(';') { continue; } @@ -159,7 +163,7 @@ pub fn parse_wireguard_config(content: &str) -> Result Result Result Result Result Result<(), VpnError> { + use base64::Engine; + + let decoded = base64::engine::general_purpose::STANDARD + .decode(key) + .map_err(|e| { + let preview: String = key.chars().take(8).collect(); + VpnError::InvalidWireGuard(format!( + "{field} is not valid base64 (starts with {preview:?}): {e}. \ + Expected a 32-byte base64-encoded key (44 chars ending with '=')." + )) + })?; + if decoded.len() != 32 { + return Err(VpnError::InvalidWireGuard(format!( + "{field} decoded to {} bytes (expected 32). The config may be truncated or malformed.", + decoded.len() + ))); + } + Ok(()) +} + /// Parse an OpenVPN configuration file pub fn parse_openvpn_config(content: &str) -> Result { let mut remote_host = String::new(); @@ -250,31 +283,23 @@ pub fn parse_openvpn_config(content: &str) -> Result { if parts.len() >= 2 { remote_host = parts[1].to_string(); } - if parts.len() >= 3 { - if let Ok(port) = parts[2].parse() { - remote_port = port; - } + if let Some(port) = parts.get(2).and_then(|p| p.parse().ok()) { + remote_port = port; } if parts.len() >= 4 { protocol = parts[3].to_string(); } } - "proto" => { - if parts.len() >= 2 { - protocol = parts[1].to_string(); - } + "proto" if parts.len() >= 2 => { + protocol = parts[1].to_string(); } "port" => { - if parts.len() >= 2 { - if let Ok(port) = parts[1].parse() { - remote_port = port; - } + if let Some(port) = parts.get(1).and_then(|p| p.parse().ok()) { + remote_port = port; } } - "dev" => { - if parts.len() >= 2 { - dev_type = parts[1].to_string(); - } + "dev" if parts.len() >= 2 => { + dev_type = parts[1].to_string(); } _ => {} } @@ -348,13 +373,13 @@ mod tests { fn test_parse_wireguard_config() { let content = r#" [Interface] -PrivateKey = WGTestPrivateKey123456789012345678901234567890 +PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE= Address = 10.0.0.2/24 DNS = 1.1.1.1 MTU = 1420 [Peer] -PublicKey = WGTestPublicKey1234567890123456789012345678901 +PublicKey = YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI= Endpoint = vpn.example.com:51820 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 25 @@ -363,14 +388,14 @@ PersistentKeepalive = 25 let config = parse_wireguard_config(content).unwrap(); assert_eq!( config.private_key, - "WGTestPrivateKey123456789012345678901234567890" + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" ); assert_eq!(config.address, "10.0.0.2/24"); assert_eq!(config.dns, Some("1.1.1.1".to_string())); assert_eq!(config.mtu, Some(1420)); assert_eq!( config.peer_public_key, - "WGTestPublicKey1234567890123456789012345678901" + "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=" ); assert_eq!(config.peer_endpoint, "vpn.example.com:51820"); assert_eq!(config.allowed_ips, vec!["0.0.0.0/0", "::/0"]); @@ -381,20 +406,26 @@ PersistentKeepalive = 25 fn test_parse_wireguard_config_minimal() { let content = r#" [Interface] -PrivateKey = minimalkey +PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE= Address = 10.0.0.2/32 [Peer] -PublicKey = peerpubkey +PublicKey = YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI= Endpoint = 1.2.3.4:51820 "#; let config = parse_wireguard_config(content).unwrap(); - assert_eq!(config.private_key, "minimalkey"); + assert_eq!( + config.private_key, + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" + ); assert_eq!(config.address, "10.0.0.2/32"); assert!(config.dns.is_none()); assert!(config.mtu.is_none()); - assert_eq!(config.peer_public_key, "peerpubkey"); + assert_eq!( + config.peer_public_key, + "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=" + ); assert_eq!(config.peer_endpoint, "1.2.3.4:51820"); } diff --git a/src-tauri/src/vpn/socks5_server.rs b/src-tauri/src/vpn/socks5_server.rs index ee2b5c4..79eb91a 100644 --- a/src-tauri/src/vpn/socks5_server.rs +++ b/src-tauri/src/vpn/socks5_server.rs @@ -622,12 +622,10 @@ impl WireGuardSocks5Server { // smoltcp → Client if socket.can_recv() { match socket.recv(|data| (data.len(), data.to_vec())) { - Ok(data) if !data.is_empty() => { - if conn.tcp_stream.try_write(&data).is_err() { - socket.close(); - completed.push(idx); - continue; - } + Ok(data) if !data.is_empty() && conn.tcp_stream.try_write(&data).is_err() => { + socket.close(); + completed.push(idx); + continue; } _ => {} } diff --git a/src-tauri/tests/vpn_integration.rs b/src-tauri/tests/vpn_integration.rs index 83cb5b6..0b472f4 100644 --- a/src-tauri/tests/vpn_integration.rs +++ b/src-tauri/tests/vpn_integration.rs @@ -144,7 +144,7 @@ Endpoint = 1.2.3.4:51820 fn test_wireguard_config_missing_peer() { let config = r#" [Interface] -PrivateKey = somekey +PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE= Address = 10.0.0.2/24 "#; let result = parse_wireguard_config(config);