Skip to content

feat(desktop): Windows support (wintun tun + routing, UAC, portable) - #52

Merged
loss-and-quick merged 13 commits into
mainfrom
feat/windows-support
Jun 21, 2026
Merged

feat(desktop): Windows support (wintun tun + routing, UAC, portable)#52
loss-and-quick merged 13 commits into
mainfrom
feat/windows-support

Conversation

@loss-and-quick

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

Copy link
Copy Markdown
Owner

Summary

Brings Windows back as a desktop target (it was dropped because the backend and the
desktop data-path were POSIX-only). One product, one data-path shape on every desktop OS —
Windows mirrors the Linux model rather than reinventing it.

What was POSIX-only, and how it's split now:

  • crates/kasumi-backend/src/proc.rs — process identity. POSIX matched a pid by
    /proc/<pid>/exe dev/ino and killed via libc::kill; Windows now matches by full image
    path (QueryFullProcessImageNameW) and ends via TerminateProcess, behind a cfg-gated
    imp module. run/silent/spawn_logged/read_pidfile stay shared.
  • src-tauri/src/desktop/ — was Linux-only and compiled unconditionally. Split into
    desktop/{linux,windows}/, each providing a DesktopPlatform re-exported by
    cfg(target_os). The DNS/address helpers, the xray-config server parsing, and the
    sing-box config finalisation move into the shared desktop/net.rs + desktop/singbox.rs.

Windows data-path (same model as Linux): sing-box drives its own wintun tun
(auto_route); the xray path bridges a wintun device through tun2socks and installs the
server-bypass + split-default by hand via route/netsh, reading the default route and
adapter index through Get-NetRoute/Get-NetAdapter. spawn_tun2socks drops -fwmark on
Windows (a Linux SO_MARK feature — the bypass is a host route).

No console flashing. A GUI process spawning a console-subsystem child pops a black
console window on Windows (tauri-apps/tauri#13230). The data-path spawns many — powershell,
route, netsh, and the cores — so CREATE_NO_WINDOW is set on every child spawned
through proc.rs. (Combined with the app's own windowed subsystem, fixed separately in #55.)

Elevation: a Windows arm — UAC via ShellExecuteW(runas) + IsUserAnAdmin, mirroring
the Linux pkexec whole-process re-exec.

Portable build: a portable.dat marker next to the exe pins all state beside the app
(run-from-anywhere, nothing in %APPDATA%/registry); installed builds (NSIS/MSI) fall back
to the roaming/local profile dirs.

Packaging/CI: fetch-cores-desktop.sh pulls the pinned wintun.dll (tun2socks loads it
from disk; sing-box embeds its own), declared as a Tauri bundle resource next to the exe.
The nightly desktop job gains a windows-latest matrix row producing the NSIS/MSI installers
plus a portable zip, and a desktop-windows compile-smoke runs per-PR. Release promotion
(release.yml) is deferred until the bundle is validated on real hardware.

Affected layer

  • frontend/ — React Web UI
  • module/ — Magisk/KernelSU/APatch payload
  • scripts/ — build / release helpers
  • CI / .github/
  • Docs only

Verification

  • cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings,
    cargo test --workspace — clean (Linux, nix devshell). Codegen drift gate empty.
  • Windows compile (cross, locally): cargo clippy -p kasumi-backend and
    cargo clippy -p kasumi-desktop clean against x86_64-pc-windows-gnu, exercising every
    #[cfg(windows)] path. The msvc bundle + on-hardware run go through the nightly job.
  • actionlint clean; shellcheck -s bash clean.

Checklist

  • Title is a scoped Conventional Commit; commits are logically split
  • No build artifacts committed (cores + wintun.dll are fetched, gitignored)
  • If user-visible strings changed: n/a
  • Renames touching the project id were grepped in all case forms — n/a

Notes for reviewers

  • Not the v2rayN model. v2rayN runs sing-box as a universal TUN provider and chains xray
    as a pre-socks upstream; that would split the data-path shape between OSes, force the
    backend to build a tun-shim config, and run two cores per xray profile. This PR keeps one
    tun2socks-based shape on both desktops.
  • wintun loading verified in both cores' sources + binary inspection: tun2socks loads
    wintun.dll from its own dir (LoadLibraryEx); sing-box embeds it (go:embed). So the
    bundled DLL is only needed for the xray/tun2socks path — enforced at start_xray.
  • Stubs to follow up: Windows per-interface byte counters report 0/0 (needs iphlpapi)
    and the uplink monitor polls the default route instead of an iphlpapi change callback.
  • Depends on the desktop fixes merged in fix(desktop): harden against tauri build shipping debug_assertions #55 (codegen-on-startup + windowed subsystem); this
    branch is rebased on top.

proc.rs was POSIX-only: pid matching read /proc/<pid>/exe dev/ino and
termination went through libc::kill. Split the OS-specific half into a
cfg-gated `imp` module so the crate builds on Windows too:

- POSIX keeps the /proc inode match + SIGTERM/SIGKILL teardown.
- Windows matches a pid by its full image path (QueryFullProcessImageNameW)
  and ends it with TerminateProcess; there is no SIGTERM, but the wintun
  driver reclaims a core's adapter on process exit, so the hard kill stays
  clean for a graceful stop.

run/silent/spawn_logged/read_pidfile were already portable and stay shared.
libc moves under cfg(unix); windows-sys (Win32 Threading + Foundation) comes
in under cfg(windows). Shell/proc-dependent unit tests are gated to unix.
The `-fwmark` flag is a Linux SO_MARK feature (an `ip rule` keeps the marked
upstream socket out of the tunnel). Windows has no fwmark — its server bypass
is a host route — so `spawn_tun2socks` now takes `Option<u32>` and omits the
flag when `None`. Linux/Android callers pass `Some(FWMARK)` unchanged.
The desktop module was Linux-only and compiled unconditionally. Split it so
each OS owns its data-path while the OS-neutral parts stay shared:

- desktop/{linux,windows}/ each provide a `DesktopPlatform`, re-exported by
  cfg(target_os) in desktop/mod.rs.
- The DNS/address helpers and the xray-config server parsing (collect servers
  -> resolve -> bypass CIDRs) move into the shared desktop/net.rs; sing-box
  config finalisation moves into shared desktop/singbox.rs (pure JSON). Each
  routing back-end only supplies its OS-specific resolver source (/etc/resolv.conf
  vs Get-DnsClientServerAddress).

Windows mirrors the Linux model: sing-box drives its own wintun tun
(auto_route); the xray path bridges a wintun device through tun2socks and
installs the bypass + split-default by hand via `route`/`netsh`, reading the
default route and adapter index through Get-NetRoute/Get-NetAdapter. Traffic
counters are stubbed (0/0) until iphlpapi is wired; the tun stack is "system"
(wintun), not gvisor. The uplink monitor polls the default route instead of
`ip monitor route`.

Paths honour a portable layout: a `portable.dat` marker next to the exe pins
all state beside the app (run-from-anywhere, nothing in %APPDATA%/registry);
installed builds fall back to %APPDATA%/%LOCALAPPDATA%.

Elevation gains a Windows arm: UAC via ShellExecuteW(runas) + IsUserAnAdmin,
mirroring the Linux pkexec whole-process re-exec. libc moves under cfg(unix);
windows-sys (UI Shell) comes in under cfg(windows).
sing-box's tun inbound and tun2socks both dlopen wintun.dll from the app
directory, so the Windows desktop bundle needs it next to the cores.
fetch-cores-desktop.sh now pulls the pinned wintun build (new WINTUN_VERSION
in core-versions.sh) and stages wintun.dll without a target suffix — it ships
as a Tauri bundle resource (placed next to the exe), not an externalBin
sidecar, since those only handle executables.
Add a windows-latest matrix row to the desktop job so each nightly also
produces the Windows installers. wintun.dll is declared as a Tauri bundle
resource (placed next to the exe) in tauri.windows.conf.json. Artifact
collection now also picks up the NSIS setup .exe and .msi, and a Windows-only
step assembles a portable zip (app exe + suffix-stripped cores + wintun.dll +
a portable.dat marker) for run-from-anywhere use.

Release promotion is deferred until the bundle is validated on real hardware.
The `rust` job runs on Linux and never compiles the #[cfg(windows)] Platform +
Win32 code, so a Windows-build break would only surface in the next nightly.
Add a desktop-windows job mirroring desktop-linux (compile-only, no bundle) so
it's caught in the PR. The Android daemon needs no equivalent: it has no
cfg(target_os) gates, so the workspace `rust` job already compiles it per-PR.
Verified against both cores' sources: sing-box embeds wintun.dll (go:embed,
loaded from memory) so its tun comes up without a DLL on disk, while tun2socks
loads it via LoadLibraryEx(LOAD_LIBRARY_SEARCH_APPLICATION_DIR) — i.e. next to
the exe. So the earlier "both need it" assumption was wrong:

- start_singbox no longer fails when wintun.dll is absent.
- start_xray still requires it (the tun2socks path genuinely does).
- capabilities.tun is now always true on Windows (sing-box can always tun);
  the xray path's DLL requirement is enforced at start instead.

The bundled wintun.dll stays — it's what the tun2socks/xray path loads.
tauri-build validates declared bundle.resources at compile time, so the
desktop-windows job (which doesn't fetch cores) failed with "resource path
binaries\wintun.dll doesn't exist". Drop a placeholder before the compile-only
build; the real wintun.dll is staged by fetch-cores for the nightly/release
bundle.
Windows desktop is now a supported target; reflect it in the README platform
badge alongside Android and Linux.
A GUI process spawning a console-subsystem child on Windows flashes a black
console window for each spawn (tauri-apps/tauri#13230). The Windows data-path
spawns many — powershell (Get-Net*), route, netsh, and the xray/sing-box/
tun2socks cores — so Start/Stop would blink consoles repeatedly. Set
CREATE_NO_WINDOW on every child spawned through proc.rs (run + spawn_logged);
a no-op on other platforms.
With a second `[[bin]]` (`codegen`) in the crate, `cargo tauri build` picks the
alphabetically-first binary (`codegen` < `kasumi-desktop`) as the app and bundles
THAT (tauri-apps/tauri#4807, #13420). The shipped `kasumi-desktop.exe` was actually
`codegen`, which unconditionally runs `export_generated()` → panics writing to a
missing build-tree path, and (a plain bin without `main.rs`'s `windows_subsystem`
attribute) runs as a console app → black window. Pin the app binary with
`default-run = "kasumi-desktop"` so the bundle ships the real app.
Spawns cleared the child env and set only our own vars. On Windows that drops
SystemRoot/windir, without which ws2_32 can't load its Winsock provider catalog,
so xray/sing-box failed to open any socket ("The requested service provider could
not be loaded or initialized"). Clear only on POSIX (the Android/Linux isolation
intent); on Windows overlay our vars onto the inherited environment.
current_exe/%APPDATA% give backslashes, so joining sub-paths with `/` produced
mixed paths like `C:\…\Kasumi-Proxy/kasumi-proxy\singbox.log`. Build with `\`.
@loss-and-quick
loss-and-quick merged commit 18ec4f1 into main Jun 21, 2026
5 checks passed
@loss-and-quick
loss-and-quick deleted the feat/windows-support branch June 21, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant