mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 17:27:54 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60c7c72036 | |||
| f81e8b6162 | |||
| e4ecd0d18a | |||
| 8bc2dc3102 | |||
| 55de231a37 | |||
| aab403fd9b |
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -1784,7 +1784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.24.3"
|
||||
version = "0.24.4"
|
||||
dependencies = [
|
||||
"aes 0.9.0",
|
||||
"aes-gcm",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user