diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 9f56422..01aa4c2 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -57,10 +57,8 @@ program // Build upstream URL from individual components if provided if (options.host && options.proxyPort && options.type) { - const protocol = - options.type === "socks4" || options.type === "socks5" - ? options.type - : "http"; + // Preserve provided scheme (http, https, socks4, socks5) + const protocol = String(options.type).toLowerCase(); const auth = options.username && options.password ? `${encodeURIComponent(options.username)}:${encodeURIComponent( diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index efd64ba..41108e3 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1491,11 +1491,18 @@ pub async fn launch_browser_profile( // Store the internal proxy settings for passing to launch_browser let mut internal_proxy_settings: Option = None; + // Resolve the most up-to-date profile from disk by name to avoid using stale proxy_id/browser state + let profile_for_launch = browser_runner + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))? + .into_iter() + .find(|p| p.name == profile.name) + .unwrap_or_else(|| profile.clone()); + // Always start a local proxy before launching (non-Camoufox handled here; Camoufox has its own flow) - let profile_for_launch = profile.clone(); if profile.browser != "camoufox" { // Determine upstream proxy if configured; otherwise use DIRECT - let upstream_proxy = profile + let upstream_proxy = profile_for_launch .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); @@ -1518,11 +1525,13 @@ pub async fn launch_browser_profile( // For Firefox-based browsers, apply PAC/user.js to point to the local proxy if matches!( - profile.browser.as_str(), + profile_for_launch.browser.as_str(), "firefox" | "firefox-developer" | "zen" | "tor-browser" | "mullvad-browser" ) { let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + let profile_path = profiles_dir + .join(profile_for_launch.id.to_string()) + .join("profile"); // Provide a dummy upstream (ignored when internal proxy is provided) let dummy_upstream = ProxySettings { @@ -1540,7 +1549,7 @@ pub async fn launch_browser_profile( println!( "Local proxy prepared for profile: {} on port: {} (upstream: {})", - profile.name, + profile_for_launch.name, internal_proxy.port, upstream_proxy .as_ref() diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index e78a977..be4ffef 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -18,6 +18,8 @@ pub struct ProxyInfo { pub upstream_port: u16, pub upstream_type: String, pub local_port: u16, + // Optional profile name to which this proxy instance is logically tied + pub profile_name: Option, } // Stored proxy configuration with name and ID for reuse @@ -51,7 +53,9 @@ pub struct ProxyManager { active_proxies: Mutex>, // Maps browser process ID to proxy info // Store proxy info by profile name for persistence across browser restarts profile_proxies: Mutex>, // Maps profile name to proxy settings - stored_proxies: Mutex>, // Maps proxy ID to stored proxy + // Track active proxy IDs by profile name for targeted cleanup + profile_active_proxy_ids: Mutex>, // Maps profile name to proxy id + stored_proxies: Mutex>, // Maps proxy ID to stored proxy base_dirs: BaseDirs, } @@ -61,6 +65,7 @@ impl ProxyManager { let manager = Self { active_proxies: Mutex::new(HashMap::new()), profile_proxies: Mutex::new(HashMap::new()), + profile_active_proxy_ids: Mutex::new(HashMap::new()), stored_proxies: Mutex::new(HashMap::new()), base_dirs, }; @@ -257,20 +262,94 @@ impl ProxyManager { browser_pid: u32, profile_name: Option<&str>, ) -> Result { - // Check if we already have a proxy for this browser + // First, proactively cleanup any dead proxies so we don't accidentally reuse stale ones + let _ = self.cleanup_dead_proxies(app_handle.clone()).await; + + // If we have a previous proxy tied to this profile, and the upstream settings are changing, + // stop it before starting a new one so the change takes effect immediately. + if let Some(name) = profile_name { + // Check if we have an active proxy recorded for this profile + let maybe_existing_id = { + let map = self.profile_active_proxy_ids.lock().unwrap(); + map.get(name).cloned() + }; + + if let Some(existing_id) = maybe_existing_id { + // Find the existing proxy info + let existing_info = { + let proxies = self.active_proxies.lock().unwrap(); + proxies.values().find(|p| p.id == existing_id).cloned() + }; + + if let Some(existing) = existing_info { + let desired_type = proxy_settings + .map(|p| p.proxy_type.as_str()) + .unwrap_or("DIRECT"); + let desired_host = proxy_settings.map(|p| p.host.as_str()).unwrap_or("DIRECT"); + let desired_port = proxy_settings.map(|p| p.port).unwrap_or(0); + + let is_same_upstream = existing.upstream_type == desired_type + && existing.upstream_host == desired_host + && existing.upstream_port == desired_port; + + if !is_same_upstream { + // Stop the previous proxy tied to this profile (best effort) + // We don't know the original PID mapping that created it; iterate to find its key + let pid_to_stop = { + let proxies = self.active_proxies.lock().unwrap(); + proxies.iter().find_map(|(pid, info)| { + if info.id == existing_id { + Some(*pid) + } else { + None + } + }) + }; + if let Some(pid) = pid_to_stop { + let _ = self.stop_proxy(app_handle.clone(), pid).await; + } + } + } + } + } + // Check if we already have a proxy for this browser PID. If it exists but the upstream + // settings don't match the newly requested ones, stop it and create a new proxy so that + // changes take effect immediately. + let mut needs_restart = false; { let proxies = self.active_proxies.lock().unwrap(); - if let Some(proxy) = proxies.get(&browser_pid) { - return Ok(ProxySettings { - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility - port: proxy.local_port, - username: None, - password: None, - }); + if let Some(existing) = proxies.get(&browser_pid) { + let desired_type = proxy_settings + .map(|p| p.proxy_type.as_str()) + .unwrap_or("DIRECT"); + let desired_host = proxy_settings.map(|p| p.host.as_str()).unwrap_or("DIRECT"); + let desired_port = proxy_settings.map(|p| p.port).unwrap_or(0); + + let is_same_upstream = existing.upstream_type == desired_type + && existing.upstream_host == desired_host + && existing.upstream_port == desired_port; + + if is_same_upstream { + // Reuse existing local proxy + return Ok(ProxySettings { + proxy_type: "http".to_string(), + host: "127.0.0.1".to_string(), + port: existing.local_port, + username: None, + password: None, + }); + } else { + // Upstream changed; we must restart the local proxy so that traffic is routed correctly + needs_restart = true; + } } } + if needs_restart { + // Best-effort stop of the old proxy for this PID before starting a new one + let _ = self.stop_proxy(app_handle.clone(), browser_pid).await; + } + // Check if we have a preferred port for this profile let preferred_port = if let Some(name) = profile_name { let profile_proxies = self.profile_proxies.lock().unwrap(); @@ -367,6 +446,7 @@ impl ProxyManager { .map(|p| p.proxy_type.clone()) .unwrap_or_else(|| "DIRECT".to_string()), local_port, + profile_name: profile_name.map(|s| s.to_string()), }; // Store the proxy info @@ -381,6 +461,9 @@ impl ProxyManager { let mut profile_proxies = self.profile_proxies.lock().unwrap(); profile_proxies.insert(name.to_string(), proxy_settings.clone()); } + // Also record the active proxy id for this profile for quick cleanup on changes + let mut map = self.profile_active_proxy_ids.lock().unwrap(); + map.insert(name.to_string(), proxy_info.id.clone()); } // Return proxy settings for the browser @@ -399,10 +482,10 @@ impl ProxyManager { app_handle: tauri::AppHandle, browser_pid: u32, ) -> Result<(), String> { - let proxy_id = { + let (proxy_id, profile_name): (String, Option) = { let mut proxies = self.active_proxies.lock().unwrap(); match proxies.remove(&browser_pid) { - Some(proxy) => proxy.id, + Some(proxy) => (proxy.id, proxy.profile_name.clone()), None => return Ok(()), // No proxy to stop } }; @@ -415,7 +498,7 @@ impl ProxyManager { .arg("proxy") .arg("stop") .arg("--id") - .arg(proxy_id); + .arg(&proxy_id); let output = nodecar.output().await.unwrap(); @@ -425,6 +508,16 @@ impl ProxyManager { // We still return Ok since we've already removed the proxy from our tracking } + // Clear profile-to-proxy mapping if it references this proxy + if let Some(name) = profile_name { + let mut map = self.profile_active_proxy_ids.lock().unwrap(); + if let Some(current_id) = map.get(&name) { + if current_id == &proxy_id { + map.remove(&name); + } + } + } + Ok(()) } @@ -624,6 +717,7 @@ mod tests { upstream_port: 3128, upstream_type: "http".to_string(), local_port: (8000 + i) as u16, + profile_name: None, }; // Add proxy diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 1a0ac35..ff16e6e 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -132,8 +132,8 @@ export function ProfilesDataTable({ async (profileName: string, proxyId: string | null) => { try { await invoke("update_profile_proxy", { - profileName, - proxyId, + profileName: profileName, + proxy_id: proxyId, }); setProxyOverrides((prev) => ({ ...prev, [profileName]: proxyId })); } catch (error) {