Three follow-up fixes after the WinDivert→sing-box pivot:
1. Discord updater now routes through upstream. Previously only the
process-name rule matched, but sing-box's TUN-side process
detection on Windows mis-attributes the in-process Rust updater's
TLS connection to e.g. steam.exe — the connection went direct and
hit RKN block. Adding domain_suffix + ip_cidr rules for Cloudflare
(162.159/16, 104.16/13, 172.64/13) and Fastly (199.232/16,
151.101/16) catches updates.discord.com regardless of which PID
the kernel claims sent it. Verified via curl through mihomo:
updates.discord.com responds 400 in 393ms (i.e. TLS handshake
succeeds, only the path is wrong — proves the routing reaches it).
2. DiscordSystemHelper.exe added to TargetProcs alongside Update.exe
(modern Discord builds use it for elevated updates).
3. UDP voice quality test removed from the checker. The STUN-via-
relay burst measured private mihomo BND.ADDR (192.168.1.132)
which is unroutable from external clients, so the test reported
100% loss every time despite voice actually working through
sing-box's TUN+SOCKS5. The remaining 6 checks (TCP/greet/auth/
connect/UDP/api) cover what's actionable; voice quality is
verified empirically by joining a Discord call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After 5+ hours of WinDivert NETWORK-layer NAT-rewrite debugging
(streamdump pattern, SOCKET-layer SYN preemption, lazy PID resolution,
UDP ASSOCIATE relay + manual reinject), Discord voice still wouldn't
connect. The fundamental issue is that WinDivert reinjected UDP
packets don't always reach connect()-bound application sockets — the
demux happens at a layer above the reinject point.
dvp/force-proxy avoids this entirely via DLL injection (above the
kernel demux). We avoid it the other way: embed sing-box, let it run
TUN inbound + per-process routing rule + SOCKS5 outbound. TUN packets
are read by sing-box from kernel as a normal flow; the application
socket sees a normal flow back. No reinject hairpin, no SYN race, no
spoofing concerns.
What this commit does:
- Drops internal/divert, internal/engine, internal/redirect,
internal/socks5, internal/procscan, plus cmd/drover/{proxy,
debugflow}_*.go subcommands (all WinDivert-only).
- Adds internal/sboxrun — embed sing-box.exe (1.12.25) + wintun.dll
(0.14.1) via //go:embed, install to %PROGRAMDATA%\Drover\sboxrun\
with SHA256 verify, generate JSON config from form, spawn as
subprocess, manage lifecycle.
- Wires sboxrun into internal/gui/app.go: StartEngine/StopEngine
now call sboxrun.Engine instead of windivert engine.
- Fixes Wails binding: StartEngine(cfg) now passes the form config
(was zero-arg, hit ProxyHost-required validation silently).
Manual test: Discord chat + voice work end-to-end through mihomo
upstream. Yandex Music / svchost / etc continue direct via
process_name routing rule.
Binary grew from 12 MB → 49 MB (37 MB sing-box embedded), but ships
fully self-contained. AV-friendly: wintun is Microsoft-signed, no
DLL injection.
WinDivert work preserved on experimental/windivert branch in case we
ever want to come back to that path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stub flag-toggle with a real engine.Engine. GetStatus
now reports the engine's actual state machine value. Stats remain
randomised in P2.1 — real bytes-counters land in P2.4 with the tray
UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend now renders three-tier status (passed/warn/failed) instead of
two. Warn rows get a yellow exclamation-mark dot, expand by default to
show the hint, and contribute to a new "(with warnings)" suffix on the
"All checks passed" header.
Test catalog gains "voice-quality" and "voice-srv" rows replacing the
single "stun" row, in the same position (after udp, before api). RU
descriptions explain what each test actually probes.
useDrover's lastSummary now reports {total, failed, warnings} so the
Classic header can pick the right tier color.
App.go counts StatusWarn as passed in the final {passed, failed} summary
emitted via "check:done" — warn is a soft pass per spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RunCheck now drives internal/checker.Run instead of the fake 7-step
sleep loop. Streams checker.Result events as "check:result" with
Duration converted to milliseconds; emits "check:done" summary on
channel close. Re-running while a check is in flight cancels the
previous run and waits for its goroutine to drain so two emitter
goroutines never race on event order.
- New CancelCheck method (no-op when nothing is running) is bound
through wailsjs/go/gui/App.js and surfaced in useDrover as
cancelCheck() with a "check cancelled by user" log line.
- Classic.jsx: while phase==='checking', the Check button collapses to
a disabled "Checking…" pill side-by-side with a Cancel button (uses
Stop's secondary visual weight, t.danger on hover). The expanded
failure row now renders r.rawHex (truncated to 64 chars) on its own
mono line and the copy button includes raw=<hex> when present.
- CheckResult struct gains RawHex (json:"rawHex") and Attempt fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.go: App struct with stub bindings (RunCheck/StartEngine/
StopEngine/GetStatus/Version) — emits check:result, check:done,
engine:status, stats:update events. Real backend lands in Phase 1.
- run.go: wails.Run() with frameless 480x640 fixed window, Classic
dark bg matching theme.
- embed.go: //go:embed all:frontend/dist for the Vite build output.
- frontend/: Vite + React project derived from `wails init -t react`.
Removed default template assets and wired Classic variant from
docs/design/v2/.
- components/Classic.jsx: variant 1 with custom title bar
(drag region, sun/moon theme toggle, min/close hooked to
Wails WindowMinimise/Quit).
- components/shared.jsx: useDrover hook adapted to call Wails
bindings and listen on backend events instead of mock SCENARIOS.
Added IconSun + IconMoon for the title-bar toggle.
- App.jsx: owns mode state, wraps setMode in
document.startViewTransition so the title-bar toggle gives a
circle-reveal sweep from the cursor.
- style.css: clean reset (overflow hidden, no scrollbars, brand
background) — replaces the wails-react-template defaults.
- wailsjs/go/gui/App.js: hand-written bindings since our App
struct lives in package gui rather than the standard top-level
main; `wails generate module` would have written package main
bindings here.
- build/: standard wails artifacts (icon, manifest); will be
consumed by `wails build` once we wire it through CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>