Compare commits

...

6 Commits

Author SHA1 Message Date
zhom 60c7c72036 chore: versiom bump 2026-05-26 04:42:31 +04:00
zhom f81e8b6162 refactor: more robust camoufox proxy handling 2026-05-26 04:40:19 +04:00
github-actions[bot] e4ecd0d18a chore: update flake.nix for v0.24.3 [skip ci] (#383)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-25 00:02:17 +00:00
github-actions[bot] 8bc2dc3102 docs: update CHANGELOG.md and README.md for v0.24.3 [skip ci] (#382)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-25 00:01:55 +00:00
zhom 55de231a37 docs: readme 2026-05-25 03:38:01 +04:00
zhom aab403fd9b docs: update preview 2026-05-25 02:31:06 +04:00
12 changed files with 179 additions and 51 deletions
+35
View File
@@ -1,6 +1,41 @@
# Changelog
## v0.24.3 (2026-05-25)
### Features
- add shortcuts
### Bug Fixes
- track gecko_id for extension groups
### Refactoring
- cleanup
- cleanup, korean translation
- reduce token usage
### Maintenance
- chore: version bump
- chore: linting
- chore: update pnpm
- chore: make telegram releases ai-generated
- chore: workflow cleanup
- ci(deps): bump the github-actions group with 6 updates
- chore: use less tokens
- chore: improve issue validation
- ci(deps): bump the github-actions group across 1 directory with 6 updates
- chore: update flake.nix for v0.24.2 [skip ci] (#370)
### Other
- deps(rust)(deps): bump the rust-dependencies group
- deps(rust)(deps): bump the rust-dependencies group
## v0.24.2 (2026-05-16)
### Features
+6 -8
View File
@@ -19,9 +19,6 @@
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/network/members" target="_blank">
<img src="https://img.shields.io/github/forks/zhom/donutbrowser?style=social" alt="GitHub forks">
</a>
<a style="text-decoration: none;" href="https://github.com/zhom/donutbrowser/releases" target="_blank">
<img src="https://img.shields.io/github/downloads/zhom/donutbrowser/total" alt="Downloads">
</a>
</p>
<img alt="Donut Browser Preview" src="assets/donut-preview.png" />
@@ -30,6 +27,7 @@
- **Unlimited browser profiles** — each fully isolated with its own fingerprint, cookies, extensions, and data
- **Chromium & Firefox engines** — Chromium powered by [Wayfern](https://wayfern.com), Firefox powered by [Camoufox](https://camoufox.com), both with advanced fingerprint spoofing
- **DNS AdBlocker** - block ads, trackers, and other unwanted content with per-profile DNS blocking
- **Proxy support** — HTTP, HTTPS, SOCKS4, SOCKS5 per profile, with dynamic proxy URLs
- **VPN support** — WireGuard configs per profile
- **Local API & MCP** — REST API and [Model Context Protocol](https://modelcontextprotocol.io) server for integration with Claude, automation tools, and custom workflows
@@ -48,7 +46,7 @@
| | Apple Silicon | Intel |
|---|---|---|
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_x64.dmg) |
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_x64.dmg) |
Or install via Homebrew:
@@ -58,15 +56,15 @@ brew install --cask donut
### Windows
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_x64-portable.zip)
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_x64-portable.zip)
### Linux
| Format | x86_64 | ARM64 |
|---|---|---|
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut-0.24.2-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut-0.24.2-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_aarch64.AppImage) |
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut-0.24.3-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut-0.24.3-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_aarch64.AppImage) |
<!-- install-links-end -->
Or install via package manager:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 KiB

After

Width:  |  Height:  |  Size: 508 KiB

+5 -5
View File
@@ -94,17 +94,17 @@
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
);
releaseVersion = "0.24.2";
releaseVersion = "0.24.3";
releaseAppImage =
if system == "x86_64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_amd64.AppImage";
hash = "sha256-140PSB/1BLGUB4sI/RgfYe7uUjwRFWXtdSnUZz6Wr0U=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_amd64.AppImage";
hash = "sha256-4RXEpNiD10hhZhBJ96lhvRG+K6ZrsEF+atwfkAicnhc=";
}
else if system == "aarch64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.24.2/Donut_0.24.2_aarch64.AppImage";
hash = "sha256-QPGV6XO0ugPAJSbPJrVwDsEb9lw3dcL6IdU17UCYH4E=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.24.3/Donut_0.24.3_aarch64.AppImage";
hash = "sha256-EmyJwfUnEQ3vtS2N99QrGrsNESHmiqIdGCrTYvTlMTI=";
}
else
null;
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.24.3",
"version": "0.24.4",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
+1 -1
View File
@@ -1784,7 +1784,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.24.3"
version = "0.24.4"
dependencies = [
"aes 0.9.0",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.24.3"
version = "0.24.4"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
+38 -3
View File
@@ -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");
+39 -23
View File
@@ -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)
+49 -5
View File
@@ -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" => {
+3 -3
View File
@@ -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",
];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.24.3",
"version": "0.24.4",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",