Compare commits

..

9 Commits

Author SHA1 Message Date
zhom b57523fa1e refactor: better cleanup 2026-04-19 22:43:16 +04:00
zhom d637b3036b chore: version bump 2026-04-19 21:44:13 +04:00
zhom a1170b586a chore: linting 2026-04-19 21:07:10 +04:00
zhom c4c6ec9dfd refactor: proxy cleanup 2026-04-19 19:40:55 +04:00
zhom 3152e0de59 feat: shadowsocks 2026-04-19 19:40:55 +04:00
andy 8284b62e34 Merge pull request #291 from zhom/dependabot/github_actions/github-actions-2ccc4691dc
ci(deps): bump the github-actions group with 3 updates
2026-04-18 12:06:18 +02:00
dependabot[bot] 1bd3a9d123 ci(deps): bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup), [anomalyco/opencode](https://github.com/anomalyco/opencode) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `pnpm/action-setup` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/08c4be7e2e672a47d11bd04269e27e5f3e8529cb...078e9d416474b29c0c387560859308974f7e9c53)

Updates `anomalyco/opencode` from 1.4.3 to 1.4.11
- [Release notes](https://github.com/anomalyco/opencode/releases)
- [Commits](https://github.com/anomalyco/opencode/compare/877be7e8e04142cd8fbebcb5e6c4b9617bf28cce...a35b8a95c27d28e979a3826e1289d7ee87f40251)

Updates `crate-ci/typos` from 1.45.0 to 1.45.1
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/02ea592e44b3a53c302f697cddca7641cd051c3d...cf5f1c29a8ac336af8568821ec41919923b05a83)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: anomalyco/opencode
  dependency-version: 1.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 09:04:48 +00:00
github-actions[bot] adb1335564 chore: update flake.nix for v0.21.0 [skip ci] (#289)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-16 13:23:52 +00:00
github-actions[bot] 0f2d0b1b3b docs: update CHANGELOG.md and README.md for v0.21.0 [skip ci] (#288)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-16 13:23:32 +00:00
32 changed files with 349 additions and 86 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+1 -1
View File
@@ -327,7 +327,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Run opencode
uses: anomalyco/opencode/github@877be7e8e04142cd8fbebcb5e6c4b9617bf28cce #v1.4.3
uses: anomalyco/opencode/github@a35b8a95c27d28e979a3826e1289d7ee87f40251 #v1.4.11
env:
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+1 -1
View File
@@ -108,7 +108,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+1 -1
View File
@@ -107,7 +107,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
- name: Checkout Actions Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Spell Check Repo
uses: crate-ci/typos@02ea592e44b3a53c302f697cddca7641cd051c3d #v1.45.0
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 #v1.45.1
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Install pnpm
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
@@ -94,7 +94,7 @@ jobs:
done
- name: Install pnpm
uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb #v6.0.0
uses: pnpm/action-setup@078e9d416474b29c0c387560859308974f7e9c53 #v6.0.1
with:
run_install: false
+33
View File
@@ -1,6 +1,39 @@
# Changelog
## v0.21.0 (2026-04-16)
### Features
- shadowsocks
### Bug Fixes
- vpn config discovery
### Refactoring
- cleanup
- stricter proxy cleanup
- wayfern launch
- better error handling
- self-updates
- x64 performance
### Maintenance
- chore: version bump
- chore: proper formatting
- chore: remove pre-installed aws cli
- chore: update flake.nix for v0.20.4 [skip ci] (#283)
### Other
- deps(rust)(deps): bump rand from 0.10.0 to 0.10.1 in /src-tauri (#285)
- style: button should not become bigger on hover
- style: scrollbars
## v0.20.4 (2026-04-11)
### Refactoring
+5 -5
View File
@@ -51,7 +51,7 @@
| | Apple Silicon | Intel |
|---|---|---|
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_x64.dmg) |
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_x64.dmg) |
Or install via Homebrew:
@@ -61,15 +61,15 @@ brew install --cask donut
### Windows
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_x64-portable.zip)
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_x64-portable.zip)
### Linux
| Format | x86_64 | ARM64 |
|---|---|---|
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut-0.20.4-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut-0.20.4-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_aarch64.AppImage) |
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut-0.21.0-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut-0.21.0-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_aarch64.AppImage) |
<!-- install-links-end -->
Or install via package manager:
+5 -5
View File
@@ -94,17 +94,17 @@
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
);
releaseVersion = "0.20.4";
releaseVersion = "0.21.0";
releaseAppImage =
if system == "x86_64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_amd64.AppImage";
hash = "sha256-Ag+MmIc2VqTpbUpd1MPq0DPn+npzguE9pp3Hq4RQERM=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_amd64.AppImage";
hash = "sha256-Qrg+8uh9RTDMHUNqWChWBHIIsy2Dgzu5wOH+FuPN35k=";
}
else if system == "aarch64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.20.4/Donut_0.20.4_aarch64.AppImage";
hash = "sha256-pYDaN445X2g7gNVTzbdie8Mv4V1vi3bREvRRBqZ50qA=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.21.0/Donut_0.21.0_aarch64.AppImage";
hash = "sha256-UBGer3/8xleadHaZ/5OY2KaC03OE40SOewCAdcxw2CM=";
}
else
null;
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.21.0",
"version": "0.21.1",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
+1 -1
View File
@@ -1789,7 +1789,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.21.0"
version = "0.21.1"
dependencies = [
"aes 0.9.0",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.21.0"
version = "0.21.1"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
+1 -1
View File
@@ -4,7 +4,7 @@ use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProxySettings {
pub proxy_type: String, // "http", "https", "socks4", or "socks5"
pub proxy_type: String, // "http", "https", "socks4", "socks5", or "ss" (Shadowsocks)
pub host: String,
pub port: u16,
pub username: Option<String>,
+1 -1
View File
@@ -362,7 +362,7 @@ impl ExtensionManager {
}
}
extensions.sort_by(|a, b| a.created_at.cmp(&b.created_at));
extensions.sort_by_key(|a| a.created_at);
Ok(extensions)
}
+82
View File
@@ -1416,6 +1416,88 @@ pub fn run() {
}
}
// Kill orphaned proxy and VPN worker processes from previous app runs.
// Since active_proxies is an in-memory map that starts empty, any running
// donut-proxy workers on disk must be orphans the current app can't track.
// Without this cleanup, users on Windows accumulate dozens of idle workers
// (one per profile launch) that the periodic cleanup won't touch because
// profile-associated workers are deliberately skipped to avoid regressions.
//
// Preserves workers whose associated profile still has a running browser
// process — if the app crashed while a browser was running, its detached
// browser keeps going and needs the proxy/VPN worker to stay alive.
tauri::async_runtime::spawn(async move {
use crate::proxy_storage::{delete_proxy_config, is_process_running, list_proxy_configs};
use crate::vpn_worker_storage::{delete_vpn_worker_config, list_vpn_worker_configs};
// Build sets of (profile_id, vpn_id) whose browsers are still running
let profile_manager = crate::profile::ProfileManager::instance();
let profiles = profile_manager.list_profiles().unwrap_or_default();
let running_profile_ids: std::collections::HashSet<String> = profiles
.iter()
.filter(|p| p.process_id.is_some_and(is_process_running))
.map(|p| p.id.to_string())
.collect();
let running_vpn_ids: std::collections::HashSet<String> = profiles
.iter()
.filter(|p| p.process_id.is_some_and(is_process_running))
.filter_map(|p| p.vpn_id.clone())
.collect();
for config in list_proxy_configs() {
let has_running_browser = config
.profile_id
.as_ref()
.is_some_and(|pid| running_profile_ids.contains(pid));
if has_running_browser {
log::info!(
"Startup: preserving proxy worker {} (profile browser still running)",
config.id
);
continue;
}
if let Some(pid) = config.pid {
if is_process_running(pid) {
log::info!(
"Startup: killing orphaned proxy worker {} (PID {})",
config.id,
pid
);
let _ = crate::proxy_runner::stop_proxy_process(&config.id).await;
continue;
}
}
delete_proxy_config(&config.id);
}
for worker in list_vpn_worker_configs() {
if running_vpn_ids.contains(&worker.vpn_id) {
log::info!(
"Startup: preserving VPN worker {} (profile browser using vpn_id {} still running)",
worker.id,
worker.vpn_id
);
continue;
}
if let Some(pid) = worker.pid {
if is_process_running(pid) {
log::info!(
"Startup: killing orphaned VPN worker {} (PID {})",
worker.id,
pid
);
let _ = crate::vpn_worker_runner::stop_vpn_worker(&worker.id).await;
continue;
}
}
delete_vpn_worker_config(&worker.id);
}
});
// Immediately bump non-running profiles to the latest installed browser version.
// This runs synchronously before any network calls so profiles are updated on launch.
{
+78
View File
@@ -1368,6 +1368,10 @@ impl ProxyManager {
("socks5", rest)
} else if let Some(rest) = line.strip_prefix("socks://") {
("socks5", rest) // Default socks to socks5
} else if let Some(rest) = line.strip_prefix("ss://") {
("ss", rest)
} else if let Some(rest) = line.strip_prefix("shadowsocks://") {
("ss", rest)
} else {
return None;
};
@@ -2060,6 +2064,80 @@ impl ProxyManager {
}
}
// Kill proxy workers whose browser process has died.
//
// active_proxies is keyed by the EXACT browser PID that was recorded in
// update_proxy_pid(). Checking that PID against a single process-table
// snapshot is deterministic: either the PID refers to a live process or
// it doesn't. This avoids the fuzzy launcher-vs-browser detection used
// by check_browser_status (which historically had false negatives on
// Linux and was the reason profile-associated workers were left alone
// in the other cleanup branches).
//
// Without this, every time a user closes their browser via the window's
// X button (bypassing Donut's stop flow) or the browser crashes, the
// worker keeps running forever. On Windows users reported dozens of
// donut-proxy processes accumulating this way.
{
// Snapshot current active entries first so we don't hold the mutex
// while running the (expensive on Windows) sysinfo scan.
let snapshot: Vec<(u32, String, Option<String>)> = {
let proxies = self.active_proxies.lock().unwrap();
proxies
.iter()
.map(|(&browser_pid, info)| (browser_pid, info.id.clone(), info.profile_id.clone()))
.collect()
};
if !snapshot.is_empty() {
// One process-table scan for all candidates
let system = sysinfo::System::new_with_specifics(
sysinfo::RefreshKind::nothing().with_processes(sysinfo::ProcessRefreshKind::everything()),
);
let dead_browser_entries: Vec<(u32, String, Option<String>)> = snapshot
.into_iter()
.filter(|(browser_pid, _, _)| {
// The sentinel PID=0 is used as a placeholder during launch,
// before update_proxy_pid has recorded the real browser PID.
*browser_pid != 0
&& system
.process(sysinfo::Pid::from_u32(*browser_pid))
.is_none()
})
.collect();
for (browser_pid, proxy_id, profile_id) in dead_browser_entries {
log::info!(
"Cleanup: browser PID {} is dead, stopping proxy worker {} (profile={:?})",
browser_pid,
proxy_id,
profile_id
);
{
let mut proxies = self.active_proxies.lock().unwrap();
// Re-check the entry still maps to the same proxy_id — another
// thread may have replaced it with a new proxy since we snapshotted.
if let Some(current) = proxies.get(&browser_pid) {
if current.id != proxy_id {
continue;
}
} else {
continue;
}
proxies.remove(&browser_pid);
}
if let Some(ref pid) = profile_id {
let mut map = self.profile_active_proxy_ids.lock().unwrap();
if map.get(pid) == Some(&proxy_id) {
map.remove(pid);
}
}
let _ = crate::proxy_runner::stop_proxy_process(&proxy_id).await;
}
}
}
// Clean up orphaned VPN worker configs where the worker process is dead
{
use crate::proxy_storage::is_process_running;
+1 -5
View File
@@ -230,11 +230,7 @@ impl SyncProgressTracker {
let elapsed = self.start_time.elapsed().as_secs_f64().max(0.1);
let speed = (completed_bytes as f64 / elapsed) as u64;
let remaining_bytes = self.total_bytes.saturating_sub(completed_bytes);
let eta = if speed > 0 {
remaining_bytes / speed
} else {
0
};
let eta = remaining_bytes.checked_div(speed).unwrap_or(0);
let _ = events::emit(
"profile-sync-progress",
+57 -26
View File
@@ -141,11 +141,15 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
let mut peer: HashMap<String, String> = HashMap::new();
let mut current_section: Option<&str> = None;
// Strip a UTF-8 BOM if present — some editors/tools emit one and it would
// otherwise prepend invisible bytes to the first section header
let content = content.strip_prefix('\u{feff}').unwrap_or(content);
for line in content.lines() {
let line = line.trim();
// Skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
@@ -159,7 +163,7 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
continue;
}
// Parse key-value pairs
// Parse key-value pairs (split on the first `=` so base64 padding is preserved)
if let Some((key, value)) = line.split_once('=') {
let key = key.trim().to_string();
let value = value.trim().to_string();
@@ -181,6 +185,7 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
.get("PrivateKey")
.ok_or_else(|| VpnError::InvalidWireGuard("Missing PrivateKey in [Interface]".to_string()))?
.clone();
validate_wireguard_key(&private_key, "PrivateKey")?;
let address = interface
.get("Address")
@@ -191,6 +196,7 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
.get("PublicKey")
.ok_or_else(|| VpnError::InvalidWireGuard("Missing PublicKey in [Peer]".to_string()))?
.clone();
validate_wireguard_key(&peer_public_key, "PublicKey")?;
let peer_endpoint = peer
.get("Endpoint")
@@ -207,6 +213,9 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
let dns = interface.get("DNS").cloned();
let mtu = interface.get("MTU").and_then(|s| s.parse().ok());
let preshared_key = peer.get("PresharedKey").cloned();
if let Some(ref psk) = preshared_key {
validate_wireguard_key(psk, "PresharedKey")?;
}
Ok(WireGuardConfig {
private_key,
@@ -221,6 +230,30 @@ pub fn parse_wireguard_config(content: &str) -> Result<WireGuardConfig, VpnError
})
}
/// Validate that a WireGuard key is a base64-encoded 32-byte value.
/// Reports the field name and a short preview of the bad value so users can
/// see exactly what went wrong (e.g. a redacted/masked key).
fn validate_wireguard_key(key: &str, field: &str) -> Result<(), VpnError> {
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD
.decode(key)
.map_err(|e| {
let preview: String = key.chars().take(8).collect();
VpnError::InvalidWireGuard(format!(
"{field} is not valid base64 (starts with {preview:?}): {e}. \
Expected a 32-byte base64-encoded key (44 chars ending with '=')."
))
})?;
if decoded.len() != 32 {
return Err(VpnError::InvalidWireGuard(format!(
"{field} decoded to {} bytes (expected 32). The config may be truncated or malformed.",
decoded.len()
)));
}
Ok(())
}
/// Parse an OpenVPN configuration file
pub fn parse_openvpn_config(content: &str) -> Result<OpenVpnConfig, VpnError> {
let mut remote_host = String::new();
@@ -250,31 +283,23 @@ pub fn parse_openvpn_config(content: &str) -> Result<OpenVpnConfig, VpnError> {
if parts.len() >= 2 {
remote_host = parts[1].to_string();
}
if parts.len() >= 3 {
if let Ok(port) = parts[2].parse() {
remote_port = port;
}
if let Some(port) = parts.get(2).and_then(|p| p.parse().ok()) {
remote_port = port;
}
if parts.len() >= 4 {
protocol = parts[3].to_string();
}
}
"proto" => {
if parts.len() >= 2 {
protocol = parts[1].to_string();
}
"proto" if parts.len() >= 2 => {
protocol = parts[1].to_string();
}
"port" => {
if parts.len() >= 2 {
if let Ok(port) = parts[1].parse() {
remote_port = port;
}
if let Some(port) = parts.get(1).and_then(|p| p.parse().ok()) {
remote_port = port;
}
}
"dev" => {
if parts.len() >= 2 {
dev_type = parts[1].to_string();
}
"dev" if parts.len() >= 2 => {
dev_type = parts[1].to_string();
}
_ => {}
}
@@ -348,13 +373,13 @@ mod tests {
fn test_parse_wireguard_config() {
let content = r#"
[Interface]
PrivateKey = WGTestPrivateKey123456789012345678901234567890
PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=
Address = 10.0.0.2/24
DNS = 1.1.1.1
MTU = 1420
[Peer]
PublicKey = WGTestPublicKey1234567890123456789012345678901
PublicKey = YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
@@ -363,14 +388,14 @@ PersistentKeepalive = 25
let config = parse_wireguard_config(content).unwrap();
assert_eq!(
config.private_key,
"WGTestPrivateKey123456789012345678901234567890"
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
);
assert_eq!(config.address, "10.0.0.2/24");
assert_eq!(config.dns, Some("1.1.1.1".to_string()));
assert_eq!(config.mtu, Some(1420));
assert_eq!(
config.peer_public_key,
"WGTestPublicKey1234567890123456789012345678901"
"YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI="
);
assert_eq!(config.peer_endpoint, "vpn.example.com:51820");
assert_eq!(config.allowed_ips, vec!["0.0.0.0/0", "::/0"]);
@@ -381,20 +406,26 @@ PersistentKeepalive = 25
fn test_parse_wireguard_config_minimal() {
let content = r#"
[Interface]
PrivateKey = minimalkey
PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=
Address = 10.0.0.2/32
[Peer]
PublicKey = peerpubkey
PublicKey = YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI=
Endpoint = 1.2.3.4:51820
"#;
let config = parse_wireguard_config(content).unwrap();
assert_eq!(config.private_key, "minimalkey");
assert_eq!(
config.private_key,
"YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
);
assert_eq!(config.address, "10.0.0.2/32");
assert!(config.dns.is_none());
assert!(config.mtu.is_none());
assert_eq!(config.peer_public_key, "peerpubkey");
assert_eq!(
config.peer_public_key,
"YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI="
);
assert_eq!(config.peer_endpoint, "1.2.3.4:51820");
}
+4 -6
View File
@@ -622,12 +622,10 @@ impl WireGuardSocks5Server {
// smoltcp → Client
if socket.can_recv() {
match socket.recv(|data| (data.len(), data.to_vec())) {
Ok(data) if !data.is_empty() => {
if conn.tcp_stream.try_write(&data).is_err() {
socket.close();
completed.push(idx);
continue;
}
Ok(data) if !data.is_empty() && conn.tcp_stream.try_write(&data).is_err() => {
socket.close();
completed.push(idx);
continue;
}
_ => {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.21.0",
"version": "0.21.1",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
+1 -1
View File
@@ -144,7 +144,7 @@ Endpoint = 1.2.3.4:51820
fn test_wireguard_config_missing_peer() {
let config = r#"
[Interface]
PrivateKey = somekey
PrivateKey = YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=
Address = 10.0.0.2/24
"#;
let result = parse_wireguard_config(config);
+32 -8
View File
@@ -94,6 +94,19 @@ export function ProxyFormDialog({
return;
}
if (
form.proxy_type === "ss" &&
(!form.username.trim() || !form.password.trim())
) {
toast.error(
t(
"proxies.form.ssCipherRequired",
"Cipher and password are required for Shadowsocks",
),
);
return;
}
setIsSubmitting(true);
try {
const payload = {
@@ -136,7 +149,12 @@ export function ProxyFormDialog({
}, [isSubmitting, onClose]);
const isFormValid =
form.name.trim() && form.host.trim() && form.port > 0 && form.port <= 65535;
form.name.trim() &&
form.host.trim() &&
form.port > 0 &&
form.port <= 65535 &&
(form.proxy_type !== "ss" ||
(form.username.trim() && form.password.trim()));
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
@@ -174,9 +192,9 @@ export function ProxyFormDialog({
<SelectValue placeholder="Select proxy type" />
</SelectTrigger>
<SelectContent>
{["http", "https", "socks4", "socks5"].map((type) => (
{["http", "https", "socks4", "socks5", "ss"].map((type) => (
<SelectItem key={type} value={type}>
{type.toUpperCase()}
{type === "ss" ? "Shadowsocks" : type.toUpperCase()}
</SelectItem>
))}
</SelectContent>
@@ -220,8 +238,9 @@ export function ProxyFormDialog({
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="proxy-username">
{t("proxies.form.username")} (
{t("proxies.form.usernamePlaceholder")})
{form.proxy_type === "ss"
? t("proxies.form.cipher")
: `${t("proxies.form.username")} (${t("proxies.form.usernamePlaceholder")})`}
</Label>
<Input
id="proxy-username"
@@ -229,15 +248,20 @@ export function ProxyFormDialog({
onChange={(e) => {
setForm({ ...form, username: e.target.value });
}}
placeholder={t("proxies.form.usernamePlaceholder")}
placeholder={
form.proxy_type === "ss"
? t("proxies.form.cipherPlaceholder")
: t("proxies.form.usernamePlaceholder")
}
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="proxy-password">
{t("proxies.form.password")} (
{t("proxies.form.passwordPlaceholder")})
{form.proxy_type === "ss"
? t("proxies.form.password")
: `${t("proxies.form.password")} (${t("proxies.form.passwordPlaceholder")})`}
</Label>
<Input
id="proxy-password"
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "Username",
"usernamePlaceholder": "Optional",
"password": "Password",
"passwordPlaceholder": "Optional"
"passwordPlaceholder": "Optional",
"cipher": "Cipher",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "Regular",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "Usuario",
"usernamePlaceholder": "Opcional",
"password": "Contraseña",
"passwordPlaceholder": "Opcional"
"passwordPlaceholder": "Opcional",
"cipher": "Cifrado",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "Regular",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "Nom d'utilisateur",
"usernamePlaceholder": "Optionnel",
"password": "Mot de passe",
"passwordPlaceholder": "Optionnel"
"passwordPlaceholder": "Optionnel",
"cipher": "Chiffrement",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "Standard",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "ユーザー名",
"usernamePlaceholder": "任意",
"password": "パスワード",
"passwordPlaceholder": "任意"
"passwordPlaceholder": "任意",
"cipher": "暗号方式",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "通常",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "Usuário",
"usernamePlaceholder": "Opcional",
"password": "Senha",
"passwordPlaceholder": "Opcional"
"passwordPlaceholder": "Opcional",
"cipher": "Cifra",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "Regular",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "Имя пользователя",
"usernamePlaceholder": "Необязательно",
"password": "Пароль",
"passwordPlaceholder": "Необязательно"
"passwordPlaceholder": "Необязательно",
"cipher": "Шифр",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "Обычный",
+5 -2
View File
@@ -278,13 +278,16 @@
"username": "用户名",
"usernamePlaceholder": "可选",
"password": "密码",
"passwordPlaceholder": "可选"
"passwordPlaceholder": "可选",
"cipher": "加密方式",
"cipherPlaceholder": "aes-256-gcm"
},
"types": {
"http": "HTTP",
"https": "HTTPS",
"socks4": "SOCKS4",
"socks5": "SOCKS5"
"socks5": "SOCKS5",
"ss": "Shadowsocks"
},
"tabs": {
"regular": "常规",
+1 -1
View File
@@ -1,5 +1,5 @@
export interface ProxySettings {
proxy_type: string; // "http", "https", "socks4", or "socks5"
proxy_type: string; // "http", "https", "socks4", "socks5", or "ss" (Shadowsocks)
host: string;
port: number;
username?: string;