Skip to content

feat(desktop): proxy mode selection (tun / proxy-only / system / pac) - #65

Merged
loss-and-quick merged 4 commits into
mainfrom
feat/proxy-mode
Jul 14, 2026
Merged

feat(desktop): proxy mode selection (tun / proxy-only / system / pac)#65
loss-and-quick merged 4 commits into
mainfrom
feat/proxy-mode

Conversation

@loss-and-quick

@loss-and-quick loss-and-quick commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Summary

Add a desktop proxy mode selector, so the client can run without a TUN device — like v2rayN. Both cores already expose a local socks/http inbound, so a non-tun mode just runs the core on that inbound.

A single mutually-exclusive proxyMode setting (tun | proxy-only | system | pac), because "TUN + system proxy" is meaningless — a tun already captures everything, so one selector makes the impossible state unrepresentable rather than offering two orthogonal axes.

  • tun (default) — today's system-wide tun + routing. Unchanged.
  • proxy-only — core up on 127.0.0.1 socks/http only; OS untouched.
  • system — sets the OS proxy at that inbound; cleared on stop/exit.
  • pac — a tiny loopback PAC server + the OS pointed at http://127.0.0.1:10811/proxy.pac.

Re-port note. This PR was originally written against the pre-helper architecture (whole-GUI elevation) and has been re-implemented on the current one rather than rebased: 283 commits of drift, and the privhelper split (#143) + the TUN-engine abstraction (#137#141) rewrote every seam it touched. The old relaunch elevated commit is gone — the GUI never elevates now, so there is nothing to relaunch.

Per-layer:

  • core (kasumi-core) — the proxyMode setting; any non-tun mode builds sing-box socks-only (no_tun, composed with the existing external-TUN-engine condition; xray is inbound-only already).
  • backend (kasumi-backend)mode rides StartDataPath; resolve_and_write_config returns the whole struct now instead of a growing tuple. A new Platform::supports_proxy_modes (default false) normalizes the mode to tun at config build and start for platforms without support — so a desktop backup restored on Android can't strip the tun inbound out from under the root module.
  • desktop data path — the privileged helper still owns the data path in every mode (see "Why the helper stays" below); mode travels over the privhelper wire with #[serde(default)] so a stale helper/service build degrades to tun instead of erroring. Non-tun modes spawn the core and stop: no tun device, no routing rewrite, no uplink-bind escape (nothing to escape). A dedicated no-tun marker keeps the watchdog from expecting a tun helper for an xray core (the absent-marker fallback would otherwise flag the data-path unhealthy and rebuild it in a loop).
  • OS proxy (system/pac) — applied through new Platform::{set_os_proxy, clear_os_proxy} hooks that the Service calls around the lifecycle; RemotePlatform answers them locally instead of forwarding: the OS proxy lives in the logged-in user's session (gsettings / D-Bus / HKCU), which the root helper's isn't. Linux applies a layered stack (gsettings guarded by schema presence; KDE kioslaverc + reparseSlaveConfiguration so KIO apps reload; environment.d + a live systemctl --user/dbus-update-activation-environment push — the only layer reaching CLI tools and XFCE/LXQt). Windows writes the WinINET registry keys + InternetSetOption refresh. Cleared on stop, on mode switch, on a failed bring-up, and on app exit.

Why the helper stays in non-tun modes

Skipping the helper for non-tun modes (the original PR's elevation gating) is deliberately not ported: the helper writes the data-path runtime files as root/LocalSystem, so alternating a privileged and an unprivileged data-path owner over the same state files turns mode switches into silent EACCES misbehaviour. On packaged installs the helper is promptless anyway (caps wrapper / installed service); a lazy helper bring-up gated on the saved mode is a follow-up that needs a helper-side file-ownership sweep first.

Affected layer

  • frontend/ — React Web UI
  • crates/ · src-tauri/ — Rust core / backend / Tauri desktop
  • module/ — Android installable zip (thin launcher over the Rust daemon)
  • scripts/ — build / release helpers
  • CI / .github/
  • Docs only

Verification

Web UI (frontend/):

  • bun run check — Biome lint + format clean
  • bun run test — vitest green (69 passed)
  • bun run buildtsc -b + vite build succeed
  • bun run check:i18n — locale dictionaries in sync (651 keys × 8 locales)

Rust (crates/ · src-tauri/):

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace — one failure: net::tests::tcp_ping_fails_on_unreachable_host, the known environment flake (an active VPN tun on the dev machine captures the TEST-NET-1 connect); fails identically on main
  • Codegen drift: cargo run -p kasumi-desktop --bin codegen leaves git clean

New tests: non-tun modes build sing-box socks-only (non_tun_proxy_mode_is_socks_only), the no-tun marker suppresses the helper expectation incl. the xray fallback (running_external_engine_*), mode normalization for non-supporting platforms (resolve_and_write_config_*), proto round-trip with the new field, and the PAC server serves/stops/re-binds (serves_the_pac_and_stops).

Windows #[cfg(windows)] (registry/WinINET) compiles only under the Windows CI job — the windows-sys 0.59 signatures match the vendored crate (unchanged from the original PR's check).

Checklist

  • Title is a scoped Conventional Commit; commits are logically split
  • No build artifacts committed (module/bin/<abi>/, geoip/geosite, built module/webroot/, src-tauri/gen/ — all gitignored on purpose)
  • Generated frontend/src/generated/ was regenerated from Rust, not hand-edited
  • If user-visible strings changed: i18n/en.ts and every locale file updated (no partial translations)
  • Renames touching the project id were grepped in all case forms (kasumi-proxy, Kasumi Proxy, camelCase)

Notes for reviewers

  • The OS-proxy hooks run on every lifecycle edge the Service owns — start success, start failure, stop, resume-restart (it re-enters run_lifecycle), and RunEvent::Exit — so no path leaves the OS pointed at a dead port. The uncovered case is a hard GUI crash: the helper reaps the data-path (GUI-gone teardown), but the OS proxy stays until the next start/stop. Same class of gap as v2rayN; fixing it needs a boot-time "did we set this" record.
  • clear_system_proxy doesn't save/restore a proxy the user had configured before the app ran — the app assumes it owns the OS proxy setting while in system/pac mode.
  • sing-box serves http on its mixed socks inbound, so system/pac use socks_port for both there; xray keeps its separate http inbound port. Decided desktop-side in set_os_proxy, not in the shared Service.
  • The PAC port (10811) is fixed; if it can't bind, the OS proxy is left cleared rather than pointed at whatever bound it.

@loss-and-quick loss-and-quick added platform:android Android (root module) platform:linux Linux desktop (Tauri) platform:windows Windows desktop (Tauri) frontend React Web UI (frontend/) backend Rust core/backend/desktop (crates/, src-tauri/) labels Jun 21, 2026
@loss-and-quick
loss-and-quick force-pushed the feat/proxy-mode branch 5 times, most recently from fcb6959 to ba8d4b3 Compare June 22, 2026 13:52
Default tun preserves current behaviour; non-tun modes drop the sing-box tun
inbound whatever the resolved TUN engine (xray is already inbound-only).
Non-tun modes spawn the core on its local socks/http inbound alone — no tun
device, no routing rewrite. The privileged helper still owns the data path; the
mode travels over the privhelper wire (defaulting to tun for a stale helper).
A dedicated no-tun marker keeps the watchdog from expecting a tun helper, and
platforms without proxy-mode support (Android) normalize the mode to tun at
config build and start. Adds the desktop-only selector (i18n ×8).
gsettings + KDE kioslaverc + environment.d on Linux (layered — each reaches a
disjoint set of apps), WinINET registry on Windows. Applied by the Service
through new Platform hooks that run in the GUI process, never the privileged
helper — the OS proxy lives in the logged-in user's session. Cleared on every
stop, mode switch and app exit, so the OS is never left pointing at a dead
port.
Loopback PAC server + the OS pointed at its URL (gsettings auto / kioslaverc
ProxyType 2 / WinINET AutoConfigURL); torn down with the data path. The env-var
layer can't express a PAC, so it is cleared in this mode.
@loss-and-quick
loss-and-quick merged commit 392100c into main Jul 14, 2026
10 checks passed
@loss-and-quick
loss-and-quick deleted the feat/proxy-mode branch July 14, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Rust core/backend/desktop (crates/, src-tauri/) frontend React Web UI (frontend/) platform:android Android (root module) platform:linux Linux desktop (Tauri) platform:windows Windows desktop (Tauri)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant