diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index aeda09f..f8c27b1 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -270,13 +270,33 @@ impl CamoufoxManager { args ); - // Spawn the browser process + // Spawn the browser process. Camoufox prints NSS/PSM and proxy failures + // to stderr (e.g. cert errors, CONNECT failures) and the user otherwise + // sees only an opaque "Secure Connection Failed" page — capture stderr + // to a per-launch file so diagnostics survive without a TTY. + let stderr_log_path = std::env::temp_dir().join(format!("camoufox-stderr-{}.log", profile.id)); let mut command = TokioCommand::new(&executable_path); command .args(&args) .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); + .stdout(Stdio::null()); + + match std::fs::File::create(&stderr_log_path) { + Ok(file) => { + log::info!( + "Camoufox stderr will be logged to: {}", + stderr_log_path.display() + ); + command.stderr(Stdio::from(file)); + } + Err(e) => { + log::warn!( + "Failed to open Camoufox stderr log {}: {e}", + stderr_log_path.display() + ); + command.stderr(Stdio::null()); + } + } // Add environment variables for (key, value) in &env_vars { @@ -708,6 +728,8 @@ impl CamoufoxManager { // re-emit so they never duplicate. let managed_keys = [ "network.proxy.", + "network.http.http3.enable", + "network.http.http3.enabled", "xpinstall.signatures.required", "extensions.startupScanScopes", "browser.sessionhistory.max_entries", @@ -741,6 +763,19 @@ impl CamoufoxManager { user_pref(\"extensions.startupScanScopes\", 1);\n", ); + // Disable HTTP/3 / QUIC. Camoufox always sits behind the local + // donut-proxy, and Firefox-150's QUIC stack bypasses configured HTTP + // proxies and goes direct UDP to the remote host. With an upstream + // proxy that's the only allowed egress, that traffic silently fails + // and pages won't load. (Chromium suppresses QUIC under a proxy on + // its own, so Wayfern doesn't need the equivalent toggle.) Both + // pref names are emitted because they've been renamed across FF + // versions and either could be the active one at runtime. + prefs.push_str( + "user_pref(\"network.http.http3.enable\", false);\n\ + user_pref(\"network.http.http3.enabled\", false);\n", + ); + if let Some(proxy_str) = &config.proxy { if let Ok(parsed) = url::Url::parse(proxy_str) { let host = parsed.host_str().unwrap_or("127.0.0.1"); diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index ff7d2a0..fbc3278 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -377,9 +377,18 @@ impl ProfileManager { log::info!("Profile '{name}' created successfully with ID: {profile_id}"); - // Create user.js with common Firefox preferences and apply proxy settings if provided - // Skip for ephemeral profiles since the data dir is created at launch time - if !ephemeral { + // `apply_proxy_settings_to_profile` writes a Firefox-style user.js + // with the upstream proxy host. That is wrong for both supported + // browser types: + // - Camoufox: camoufox_manager rewrites user.js at every launch with + // the local donut-proxy host; writing the upstream here leaves a + // stale, wrong proxy in user.js until the next launch. + // - Wayfern: Chromium gets its proxy via `--proxy-pac-url=` at launch + // (see wayfern_manager.rs) and never reads user.js. + // So we only call it for any unrecognized browser type that might be + // a true Firefox-family target (none currently). Ephemeral profiles + // skip regardless because their data dir is created at launch time. + if !ephemeral && !matches!(browser, "camoufox" | "wayfern") { if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?; @@ -1236,18 +1245,34 @@ impl ProfileManager { } } - // Update on-disk browser profile config immediately - if let Some(proxy_id_ref) = &proxy_id { - if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; + // Update on-disk browser profile config immediately. + // Both supported browser types ignore this write (Camoufox rewrites + // user.js at launch with the local donut-proxy host, Wayfern takes its + // proxy via `--proxy-pac-url=` and never reads user.js), and for + // Camoufox specifically writing the upstream host here would leave a + // stale, wrong proxy in user.js until the next launch. + if !matches!(profile.browser.as_str(), "camoufox" | "wayfern") { + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; + } else { + // Proxy ID provided but proxy not found, disable proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .disable_proxy_settings_in_profile(&profile_path) + .map_err(|e| -> Box { + format!("Failed to disable proxy settings: {e}").into() + })?; + } } else { - // Proxy ID provided but proxy not found, disable proxy + // No proxy ID provided, disable proxy let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); self @@ -1256,15 +1281,6 @@ impl ProfileManager { format!("Failed to disable proxy settings: {e}").into() })?; } - } else { - // No proxy ID provided, disable proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .disable_proxy_settings_in_profile(&profile_path) - .map_err(|e| -> Box { - format!("Failed to disable proxy settings: {e}").into() - })?; } // Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager) diff --git a/src-tauri/src/proxy_server.rs b/src-tauri/src/proxy_server.rs index cfe56b3..ddd7c84 100644 --- a/src-tauri/src/proxy_server.rs +++ b/src-tauri/src/proxy_server.rs @@ -1147,14 +1147,17 @@ pub async fn handle_proxy_connection( } } - let _ = handle_connect_from_buffer( + if let Err(e) = handle_connect_from_buffer( stream, full_request, upstream_url, bypass_matcher, blocklist_matcher, ) - .await; + .await + { + log::warn!("CONNECT tunnel ended with error: {e}"); + } return; } @@ -1449,6 +1452,13 @@ async fn handle_connect_from_buffer( tracker.record_request(&domain, 0, 0); } + log::info!( + "CONNECT {}:{} (upstream={})", + target_host, + target_port, + upstream_url.as_deref().unwrap_or("DIRECT") + ); + // Connect to target (directly or via upstream proxy). // Returns a BoxedAsyncStream so all upstream types (plain TCP, SOCKS, // Shadowsocks) share the same bidirectional-copy tunnel code below. @@ -1503,12 +1513,46 @@ async fn handle_connect_from_buffer( let mut buffer = [0u8; 4096]; let n = proxy_stream.read(&mut buffer).await?; - let response = String::from_utf8_lossy(&buffer[..n]); + let response_full = String::from_utf8_lossy(&buffer[..n]).to_string(); + let status_line = response_full.lines().next().unwrap_or("").to_string(); - if !response.starts_with("HTTP/1.1 200") && !response.starts_with("HTTP/1.0 200") { - return Err(format!("Upstream proxy CONNECT failed: {}", response).into()); + if !response_full.starts_with("HTTP/1.1 200") + && !response_full.starts_with("HTTP/1.0 200") + { + log::warn!( + "Upstream CONNECT to {}:{} via {}:{} rejected: {}", + target_host, + target_port, + proxy_host, + proxy_port, + status_line + ); + return Err(format!("Upstream proxy CONNECT failed: {response_full}").into()); } + // Detect the buffer-drop race where the upstream returned the + // 200 response coalesced with destination bytes — those bytes + // would otherwise be silently discarded and the browser would + // see a TLS stream missing its first record. + let header_end_in_buffer = response_full.find("\r\n\r\n").map(|i| i + 4); + if let Some(end) = header_end_in_buffer { + if end < n { + log::warn!( + "Upstream CONNECT response coalesced {} byte(s) of payload — these would be dropped without forwarding", + n - end + ); + } + } + + log::info!( + "Upstream CONNECT to {}:{} via {}:{} accepted ({})", + target_host, + target_port, + proxy_host, + proxy_port, + status_line + ); + Box::new(proxy_stream) } "socks4" | "socks5" => { diff --git a/src-tauri/src/sync/manifest.rs b/src-tauri/src/sync/manifest.rs index 229d9f5..d45bd0d 100644 --- a/src-tauri/src/sync/manifest.rs +++ b/src-tauri/src/sync/manifest.rs @@ -62,9 +62,9 @@ pub const DEFAULT_EXCLUDE_PATTERNS: &[&str] = &[ "**/BrowserMetrics*", "**/.DS_Store", ".donut-sync/**", - // Local-only marker recording when Wayfern last refreshed this profile's - // fingerprint. Each device decides its own refresh cadence, so syncing - // this would cause one device's refresh to silence others. + // Orphaned local-only marker from earlier rollover-based fingerprint + // regeneration. Keep excluding it so any markers left on disk from + // prior builds never get uploaded. ".last-fp-refresh", ];