mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-30 11:59:29 +02:00
refactor: more robust camoufox proxy handling
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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<dyn std::error::Error + Send + Sync> {
|
||||
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<dyn std::error::Error + Send + Sync> {
|
||||
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<dyn std::error::Error + Send + Sync> {
|
||||
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<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to disable proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
|
||||
// Emit profile update event so frontend UIs can refresh immediately (e.g. proxy manager)
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user