diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index 2d2229f..3ba975e 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -659,6 +659,56 @@ impl CamoufoxManager { } } + // Write explicit proxy prefs to user.js so Firefox always uses the local + // donut-proxy and never falls back to stale proxy settings baked into prefs.js + // from a previous session. user.js values override prefs.js on every launch. + if let Some(proxy_str) = &config.proxy { + let user_js_path = profile_path.join("user.js"); + let mut prefs = String::new(); + + // Preserve existing user.js content (ephemeral prefs, etc.) + if let Ok(existing) = std::fs::read_to_string(&user_js_path) { + // Strip old proxy prefs so we don't duplicate + for line in existing.lines() { + if !line.contains("network.proxy.") { + prefs.push_str(line); + prefs.push('\n'); + } + } + } + + if let Ok(parsed) = url::Url::parse(proxy_str) { + let host = parsed.host_str().unwrap_or("127.0.0.1"); + let port = parsed.port().unwrap_or(8080); + let scheme = parsed.scheme(); + + if scheme == "socks5" || scheme == "socks4" { + prefs.push_str(&format!( + "user_pref(\"network.proxy.type\", 1);\n\ + user_pref(\"network.proxy.socks\", \"{host}\");\n\ + user_pref(\"network.proxy.socks_port\", {port});\n\ + user_pref(\"network.proxy.socks_version\", {});\n\ + user_pref(\"network.proxy.socks_remote_dns\", true);\n", + if scheme == "socks5" { 5 } else { 4 } + )); + } else { + // HTTP/HTTPS proxy + prefs.push_str(&format!( + "user_pref(\"network.proxy.type\", 1);\n\ + user_pref(\"network.proxy.http\", \"{host}\");\n\ + user_pref(\"network.proxy.http_port\", {port});\n\ + user_pref(\"network.proxy.ssl\", \"{host}\");\n\ + user_pref(\"network.proxy.ssl_port\", {port});\n\ + user_pref(\"network.proxy.no_proxies_on\", \"\");\n" + )); + } + + if let Err(e) = std::fs::write(&user_js_path, prefs) { + log::warn!("Failed to write proxy prefs to user.js: {e}"); + } + } + } + self .launch_camoufox( &app_handle, diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 0081473..d4970a2 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -1005,7 +1005,19 @@ impl ProxyManager { Ok(proxy_config) => { let local_url = format!("http://127.0.0.1:{}", proxy_config.local_port.unwrap_or(0)); let config_id = proxy_config.id.clone(); - let result = ip_utils::fetch_public_ip(Some(&local_url)).await; + // Wrap in a timeout so the check worker doesn't stay alive indefinitely + // if the upstream is slow or unreachable. + let result = tokio::time::timeout( + std::time::Duration::from_secs(30), + ip_utils::fetch_public_ip(Some(&local_url)), + ) + .await + .unwrap_or_else(|_| { + Err(ip_utils::IpError::Network( + "Proxy check timed out after 30s".to_string(), + )) + }); + // Always stop the worker — even if the check failed or timed out let _ = crate::proxy_runner::stop_proxy_process(&config_id).await; result } @@ -1996,11 +2008,58 @@ impl ProxyManager { "Cleaning up orphaned proxy config: {} (proxy process is dead)", config.id ); - // Just delete the config file - the process is already dead use crate::proxy_storage::delete_proxy_config; delete_proxy_config(&config.id); } + // Kill stale profileless proxy workers — these are check workers + // (from check_proxy_validity or similar) that were never cleaned up. + // Profile-associated proxies are left alone to avoid the regression + // where killing proxies for "dead" browsers on Linux also killed + // proxies for running browsers (due to launcher-vs-browser PID mismatch). + { + use crate::proxy_storage::{is_process_running, list_proxy_configs}; + use std::time::{SystemTime, UNIX_EPOCH}; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let all_configs = list_proxy_configs(); + for config in all_configs { + // Only target proxies WITHOUT a profile_id (check workers) + if config.profile_id.is_some() { + continue; + } + + // Must have a running process to kill + let Some(pid) = config.pid else { continue }; + if !is_process_running(pid) { + continue; + } + + // Check age: only kill if older than 5 minutes + let proxy_age = config + .id + .strip_prefix("proxy_") + .and_then(|s| s.split('_').next()) + .and_then(|s| s.parse::().ok()) + .map(|created_at| now.saturating_sub(created_at)) + .unwrap_or(0); + + if proxy_age > 300 { + log::info!( + "Killing stale profileless proxy {} (PID {}, age {}s)", + config.id, + pid, + proxy_age + ); + let _ = crate::proxy_runner::stop_proxy_process(&config.id).await; + } + } + } + // Clean up orphaned VPN worker configs where the worker process is dead { use crate::proxy_storage::is_process_running;