mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 17:27:54 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b57523fa1e | |||
| d637b3036b | |||
| a1170b586a | |||
| c4c6ec9dfd | |||
| 3152e0de59 | |||
| 8284b62e34 | |||
| 1bd3a9d123 | |||
| adb1335564 | |||
| 0f2d0b1b3b | |||
| 9f4bb594e4 | |||
| f338d08be1 | |||
| e293c36b97 | |||
| ba796f1cea | |||
| bd052cec38 |
@@ -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
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -58,4 +58,4 @@ nodecar/nodecar-bin
|
||||
.env
|
||||
|
||||
# next
|
||||
next-env.d.ts
|
||||
**/next-env.d.ts
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./dist/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.20.4",
|
||||
"version": "0.21.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 12341",
|
||||
|
||||
Generated
+21
-32
@@ -169,7 +169,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -180,7 +180,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1714,7 +1714,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1789,7 +1789,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.20.4"
|
||||
version = "0.21.1"
|
||||
dependencies = [
|
||||
"aes 0.9.0",
|
||||
"aes-gcm",
|
||||
@@ -1831,7 +1831,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"playwright",
|
||||
"quick-xml 0.39.2",
|
||||
"rand 0.10.0",
|
||||
"rand 0.10.1",
|
||||
"regex-lite",
|
||||
"reqwest 0.13.2",
|
||||
"resvg",
|
||||
@@ -2082,7 +2082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3085,7 +3085,7 @@ dependencies = [
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.6.1",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3100,7 +3100,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4548,7 +4548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4923,7 +4923,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
[[package]]
|
||||
name = "playwright"
|
||||
version = "0.0.23"
|
||||
source = "git+https://github.com/sctg-development/playwright-rust?branch=master#77d7a9729bc6c45b899a61eb4fb84adf075315e2"
|
||||
source = "git+https://github.com/zhom/playwright-rust?branch=master#95a6c94d87c88376502ce2f33d4c61c09fc008a6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -5307,9 +5307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"chacha20 0.10.0",
|
||||
"getrandom 0.4.2",
|
||||
@@ -5848,7 +5848,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5875,9 +5875,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.11"
|
||||
version = "0.103.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4"
|
||||
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -6550,7 +6550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7097,7 +7097,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.3",
|
||||
"windows-registry",
|
||||
"windows-result 0.3.4",
|
||||
]
|
||||
|
||||
@@ -7348,7 +7348,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7957,7 +7957,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8551,7 +8551,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8731,17 +8731,6 @@ dependencies = [
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -9080,7 +9069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.20.4"
|
||||
version = "0.21.1"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
@@ -76,7 +76,7 @@ chrono-tz = "0.10"
|
||||
axum = { version = "0.8.8", features = ["ws"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
rand = "0.10.0"
|
||||
rand = "0.10.1"
|
||||
utoipa = { version = "5", features = ["axum_extras", "chrono"] }
|
||||
utoipa-axum = "0.2"
|
||||
argon2 = "0.5"
|
||||
@@ -93,7 +93,7 @@ clap = { version = "4", features = ["derive"] }
|
||||
async-socks5 = "0.6"
|
||||
|
||||
# Camoufox/Playwright integration
|
||||
playwright = { git = "https://github.com/sctg-development/playwright-rust", branch = "master" }
|
||||
playwright = { git = "https://github.com/zhom/playwright-rust", branch = "master" }
|
||||
|
||||
# Wayfern CDP integration
|
||||
tokio-tungstenite = { version = "0.29", features = ["native-tls"] }
|
||||
|
||||
@@ -198,7 +198,12 @@ async fn main() {
|
||||
.required(true)
|
||||
.help("Local SOCKS5 port"),
|
||||
)
|
||||
.arg(Arg::new("action").required(true).help("Action (start)")),
|
||||
.arg(Arg::new("action").required(true).help("Action (start)"))
|
||||
.arg(
|
||||
Arg::new("config-path")
|
||||
.long("config-path")
|
||||
.help("Direct path to the VPN worker config JSON file"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("mcp-bridge")
|
||||
@@ -391,6 +396,7 @@ async fn main() {
|
||||
let port = *vpn_matches
|
||||
.get_one::<u16>("port")
|
||||
.expect("port is required");
|
||||
let config_path = vpn_matches.get_one::<String>("config-path");
|
||||
|
||||
if action == "start" {
|
||||
set_high_priority();
|
||||
@@ -398,8 +404,37 @@ async fn main() {
|
||||
log::info!("VPN worker starting, config id: {}", id);
|
||||
log::info!("Process PID: {}", std::process::id());
|
||||
|
||||
// Retry config loading to handle file system race condition
|
||||
let config = {
|
||||
let config = if let Some(path) = config_path {
|
||||
// Load config directly from the provided path
|
||||
log::info!("Loading VPN worker config from: {}", path);
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(content) => match serde_json::from_str::<
|
||||
donutbrowser_lib::vpn_worker_storage::VpnWorkerConfig,
|
||||
>(&content)
|
||||
{
|
||||
Ok(config) => {
|
||||
log::info!(
|
||||
"Found VPN worker config: id={}, vpn_type={}, vpn_id={}",
|
||||
config.id,
|
||||
config.vpn_type,
|
||||
config.vpn_id
|
||||
);
|
||||
config
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse VPN worker config from {}: {}", path, e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!("Failed to read VPN worker config from {}: {}", path, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: discover config by ID with retries
|
||||
let storage_dir = donutbrowser_lib::proxy_storage::get_storage_dir();
|
||||
log::info!("Looking for VPN worker config in: {:?}", storage_dir);
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
if let Some(config) = donutbrowser_lib::vpn_worker_storage::get_vpn_worker_config(id) {
|
||||
@@ -412,20 +447,21 @@ async fn main() {
|
||||
break config;
|
||||
}
|
||||
attempts += 1;
|
||||
if attempts >= 10 {
|
||||
if attempts >= 50 {
|
||||
log::error!(
|
||||
"VPN worker configuration {} not found after {} attempts",
|
||||
"VPN worker configuration {} not found after {} attempts in {:?}",
|
||||
id,
|
||||
attempts
|
||||
attempts,
|
||||
storage_dir
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
log::info!(
|
||||
"VPN worker config {} not found yet, retrying ({}/10)...",
|
||||
"VPN worker config {} not found yet, retrying ({}/50)...",
|
||||
id,
|
||||
attempts
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -659,6 +659,56 @@ impl CamoufoxManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Write explicit proxy prefs to user.js so Firefox always uses the local
|
||||
// donut-proxy and never falls back to stale proxy settings baked into prefs.js
|
||||
// from a previous session. user.js values override prefs.js on every launch.
|
||||
if let Some(proxy_str) = &config.proxy {
|
||||
let user_js_path = profile_path.join("user.js");
|
||||
let mut prefs = String::new();
|
||||
|
||||
// Preserve existing user.js content (ephemeral prefs, etc.)
|
||||
if let Ok(existing) = std::fs::read_to_string(&user_js_path) {
|
||||
// Strip old proxy prefs so we don't duplicate
|
||||
for line in existing.lines() {
|
||||
if !line.contains("network.proxy.") {
|
||||
prefs.push_str(line);
|
||||
prefs.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(proxy_str) {
|
||||
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
||||
let port = parsed.port().unwrap_or(8080);
|
||||
let scheme = parsed.scheme();
|
||||
|
||||
if scheme == "socks5" || scheme == "socks4" {
|
||||
prefs.push_str(&format!(
|
||||
"user_pref(\"network.proxy.type\", 1);\n\
|
||||
user_pref(\"network.proxy.socks\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.socks_port\", {port});\n\
|
||||
user_pref(\"network.proxy.socks_version\", {});\n\
|
||||
user_pref(\"network.proxy.socks_remote_dns\", true);\n",
|
||||
if scheme == "socks5" { 5 } else { 4 }
|
||||
));
|
||||
} else {
|
||||
// HTTP/HTTPS proxy
|
||||
prefs.push_str(&format!(
|
||||
"user_pref(\"network.proxy.type\", 1);\n\
|
||||
user_pref(\"network.proxy.http\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.http_port\", {port});\n\
|
||||
user_pref(\"network.proxy.ssl\", \"{host}\");\n\
|
||||
user_pref(\"network.proxy.ssl_port\", {port});\n\
|
||||
user_pref(\"network.proxy.no_proxies_on\", \"\");\n"
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(e) = std::fs::write(&user_js_path, prefs) {
|
||||
log::warn!("Failed to write proxy prefs to user.js: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
.launch_camoufox(
|
||||
&app_handle,
|
||||
|
||||
@@ -198,11 +198,20 @@ impl CookieManager {
|
||||
|
||||
match profile.browser.as_str() {
|
||||
"wayfern" => {
|
||||
let path = profile_data_path.join("Default").join("Cookies");
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
let network_path = profile_data_path
|
||||
.join("Default")
|
||||
.join("Network")
|
||||
.join("Cookies");
|
||||
let legacy_path = profile_data_path.join("Default").join("Cookies");
|
||||
if network_path.exists() {
|
||||
Ok(network_path)
|
||||
} else if legacy_path.exists() {
|
||||
Ok(legacy_path)
|
||||
} else {
|
||||
Err(format!("Cookie database not found at: {}", path.display()))
|
||||
Err(format!(
|
||||
"Cookie database not found at: {}",
|
||||
network_path.display()
|
||||
))
|
||||
}
|
||||
}
|
||||
"camoufox" => {
|
||||
@@ -232,11 +241,21 @@ impl CookieManager {
|
||||
|
||||
match profile.browser.as_str() {
|
||||
"wayfern" => {
|
||||
let path = profile_data_path.join("Default").join("Cookies");
|
||||
if !path.exists() {
|
||||
Self::create_empty_chrome_cookies_db(&path)?;
|
||||
let network_path = profile_data_path
|
||||
.join("Default")
|
||||
.join("Network")
|
||||
.join("Cookies");
|
||||
let legacy_path = profile_data_path.join("Default").join("Cookies");
|
||||
if network_path.exists() {
|
||||
Ok(network_path)
|
||||
} else if legacy_path.exists() {
|
||||
Ok(legacy_path)
|
||||
} else {
|
||||
let dir = network_path.parent().unwrap();
|
||||
std::fs::create_dir_all(dir).map_err(|e| format!("Failed to create Network dir: {e}"))?;
|
||||
Self::create_empty_chrome_cookies_db(&network_path)?;
|
||||
Ok(network_path)
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
"camoufox" => {
|
||||
let path = profile_data_path.join("cookies.sqlite");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
{
|
||||
|
||||
@@ -1005,7 +1005,19 @@ impl ProxyManager {
|
||||
Ok(proxy_config) => {
|
||||
let local_url = format!("http://127.0.0.1:{}", proxy_config.local_port.unwrap_or(0));
|
||||
let config_id = proxy_config.id.clone();
|
||||
let result = ip_utils::fetch_public_ip(Some(&local_url)).await;
|
||||
// Wrap in a timeout so the check worker doesn't stay alive indefinitely
|
||||
// if the upstream is slow or unreachable.
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
ip_utils::fetch_public_ip(Some(&local_url)),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
Err(ip_utils::IpError::Network(
|
||||
"Proxy check timed out after 30s".to_string(),
|
||||
))
|
||||
});
|
||||
// Always stop the worker — even if the check failed or timed out
|
||||
let _ = crate::proxy_runner::stop_proxy_process(&config_id).await;
|
||||
result
|
||||
}
|
||||
@@ -1356,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;
|
||||
};
|
||||
@@ -1996,11 +2012,132 @@ impl ProxyManager {
|
||||
"Cleaning up orphaned proxy config: {} (proxy process is dead)",
|
||||
config.id
|
||||
);
|
||||
// Just delete the config file - the process is already dead
|
||||
use crate::proxy_storage::delete_proxy_config;
|
||||
delete_proxy_config(&config.id);
|
||||
}
|
||||
|
||||
// Kill stale profileless proxy workers — these are check workers
|
||||
// (from check_proxy_validity or similar) that were never cleaned up.
|
||||
// Profile-associated proxies are left alone to avoid the regression
|
||||
// where killing proxies for "dead" browsers on Linux also killed
|
||||
// proxies for running browsers (due to launcher-vs-browser PID mismatch).
|
||||
{
|
||||
use crate::proxy_storage::{is_process_running, list_proxy_configs};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let all_configs = list_proxy_configs();
|
||||
for config in all_configs {
|
||||
// Only target proxies WITHOUT a profile_id (check workers)
|
||||
if config.profile_id.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Must have a running process to kill
|
||||
let Some(pid) = config.pid else { continue };
|
||||
if !is_process_running(pid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check age: only kill if older than 5 minutes
|
||||
let proxy_age = config
|
||||
.id
|
||||
.strip_prefix("proxy_")
|
||||
.and_then(|s| s.split('_').next())
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.map(|created_at| now.saturating_sub(created_at))
|
||||
.unwrap_or(0);
|
||||
|
||||
if proxy_age > 300 {
|
||||
log::info!(
|
||||
"Killing stale profileless proxy {} (PID {}, age {}s)",
|
||||
config.id,
|
||||
pid,
|
||||
proxy_age
|
||||
);
|
||||
let _ = crate::proxy_runner::stop_proxy_process(&config.id).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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",
|
||||
@@ -2344,7 +2340,18 @@ impl SyncEngine {
|
||||
|
||||
// Verify critical files after download
|
||||
let os_crypt_key_path = profile_dir.join("profile").join("os_crypt_key");
|
||||
let cookies_path = profile_dir.join("profile").join("Default").join("Cookies");
|
||||
let cookies_path = {
|
||||
let network = profile_dir
|
||||
.join("profile")
|
||||
.join("Default")
|
||||
.join("Network")
|
||||
.join("Cookies");
|
||||
if network.exists() {
|
||||
network
|
||||
} else {
|
||||
profile_dir.join("profile").join("Default").join("Cookies")
|
||||
}
|
||||
};
|
||||
if os_crypt_key_path.exists() {
|
||||
let key_data = fs::read(&os_crypt_key_path).unwrap_or_default();
|
||||
log::info!(
|
||||
|
||||
+57
-26
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::proxy_runner::find_sidecar_executable;
|
||||
use crate::proxy_storage::is_process_running;
|
||||
use crate::vpn_worker_storage::{
|
||||
delete_vpn_worker_config, find_vpn_worker_by_vpn_id, generate_vpn_worker_id,
|
||||
get_vpn_worker_config, list_vpn_worker_configs, save_vpn_worker_config, VpnWorkerConfig,
|
||||
get_vpn_worker_config, list_vpn_worker_configs, save_vpn_worker_config, vpn_worker_config_path,
|
||||
VpnWorkerConfig,
|
||||
};
|
||||
use std::process::Stdio;
|
||||
|
||||
@@ -175,6 +176,8 @@ pub async fn start_vpn_worker(vpn_id: &str) -> Result<VpnWorkerConfig, Box<dyn s
|
||||
);
|
||||
save_vpn_worker_config(&config)?;
|
||||
|
||||
let config_json_path = vpn_worker_config_path(&id);
|
||||
|
||||
// Spawn detached VPN worker process
|
||||
let exe = find_sidecar_executable("donut-proxy")?;
|
||||
|
||||
@@ -190,6 +193,8 @@ pub async fn start_vpn_worker(vpn_id: &str) -> Result<VpnWorkerConfig, Box<dyn s
|
||||
cmd.arg(&id);
|
||||
cmd.arg("--port");
|
||||
cmd.arg(local_port.to_string());
|
||||
cmd.arg("--config-path");
|
||||
cmd.arg(&config_json_path);
|
||||
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
@@ -235,6 +240,8 @@ pub async fn start_vpn_worker(vpn_id: &str) -> Result<VpnWorkerConfig, Box<dyn s
|
||||
cmd.arg(&id);
|
||||
cmd.arg("--port");
|
||||
cmd.arg(local_port.to_string());
|
||||
cmd.arg("--config-path");
|
||||
cmd.arg(&config_json_path);
|
||||
|
||||
cmd.stdin(Stdio::null());
|
||||
cmd.stdout(Stdio::null());
|
||||
@@ -249,7 +256,8 @@ pub async fn start_vpn_worker(vpn_id: &str) -> Result<VpnWorkerConfig, Box<dyn s
|
||||
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
|
||||
|
||||
let child = cmd.spawn()?;
|
||||
let pid = child.id();
|
||||
|
||||
@@ -27,6 +27,10 @@ impl VpnWorkerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vpn_worker_config_path(id: &str) -> std::path::PathBuf {
|
||||
get_storage_dir().join(format!("vpn_worker_{}.json", id))
|
||||
}
|
||||
|
||||
pub fn save_vpn_worker_config(config: &VpnWorkerConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let storage_dir = get_storage_dir();
|
||||
fs::create_dir_all(&storage_dir)?;
|
||||
|
||||
@@ -519,7 +519,17 @@ impl WayfernManager {
|
||||
{
|
||||
let profile_path_buf = std::path::PathBuf::from(profile_path);
|
||||
let key_path = profile_path_buf.join("os_crypt_key");
|
||||
let cookies_path = profile_path_buf.join("Default").join("Cookies");
|
||||
let cookies_path = {
|
||||
let network = profile_path_buf
|
||||
.join("Default")
|
||||
.join("Network")
|
||||
.join("Cookies");
|
||||
if network.exists() {
|
||||
network
|
||||
} else {
|
||||
profile_path_buf.join("Default").join("Cookies")
|
||||
}
|
||||
};
|
||||
|
||||
if key_path.exists() {
|
||||
let key_text = std::fs::read_to_string(&key_path).unwrap_or_default();
|
||||
@@ -819,6 +829,29 @@ impl WayfernManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Playwright's emulation overrides that cause tampering detection
|
||||
for target in &page_targets {
|
||||
if let Some(ws_url) = &target.websocket_debugger_url {
|
||||
let _ = self
|
||||
.send_cdp_command(ws_url, "Emulation.clearDeviceMetricsOverride", json!({}))
|
||||
.await;
|
||||
let _ = self
|
||||
.send_cdp_command(
|
||||
ws_url,
|
||||
"Emulation.setFocusEmulationEnabled",
|
||||
json!({ "enabled": false }),
|
||||
)
|
||||
.await;
|
||||
let _ = self
|
||||
.send_cdp_command(
|
||||
ws_url,
|
||||
"Emulation.setEmulatedMedia",
|
||||
json!({ "media": "", "features": [] }),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let instance = WayfernInstance {
|
||||
id: id.clone(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut",
|
||||
"version": "0.20.4",
|
||||
"version": "0.21.1",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -67,7 +67,12 @@ const ChartContainer = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer minWidth={1} minHeight={1}>
|
||||
<RechartsPrimitive.ResponsiveContainer
|
||||
width="100%"
|
||||
height="100%"
|
||||
minWidth={1}
|
||||
minHeight={1}
|
||||
>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "通常",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Обычный",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user