pivot: replace WinDivert engine with embedded sing-box + wintun
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped

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>
This commit is contained in:
2026-05-01 23:02:12 +03:00
parent 4074e68715
commit 48097f8671
51 changed files with 522 additions and 4438 deletions
-42
View File
@@ -127,52 +127,10 @@ func newRootCmd() *cobra.Command {
root.AddCommand(newUpdateCmd())
root.AddCommand(newServiceCmd())
root.AddCommand(newGUICmd())
root.AddCommand(newProxyCmd())
root.AddCommand(newDebugFlowCmd())
return root
}
// newDebugFlowCmd opens a WinDivert FLOW handle with filter "tcp"
// (capture all TCP flow events from any process) and logs every event
// for 30 seconds. Useful to verify the FLOW layer is working at all
// without process-targeting interference.
func newDebugFlowCmd() *cobra.Command {
return &cobra.Command{
Use: "debug-flow",
Short: "[debug] open broad FLOW handle, log events for 30s",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDebugFlow(cmd.Context())
},
}
}
// newProxyCmd is the headless engine-only mode: no Wails, no tray —
// just spin up the WinDivert + SOCKS5 pipeline against the configured
// upstream and block on Ctrl+C. Useful for debugging without the GUI
// stack in the way; everything still goes to %LOCALAPPDATA%\Drover\debug.log.
func newProxyCmd() *cobra.Command {
var host, login, password string
var port int
var auth bool
cmd := &cobra.Command{
Use: "proxy",
Short: "Run the WinDivert+SOCKS5 engine in headless mode (no GUI, blocks until Ctrl+C)",
RunE: func(cmd *cobra.Command, args []string) error {
return runProxy(cmd.Context(), host, port, auth, login, password)
},
}
cmd.Flags().StringVar(&host, "host", "", "upstream SOCKS5 host (required)")
cmd.Flags().IntVar(&port, "port", 0, "upstream SOCKS5 port (required)")
cmd.Flags().BoolVar(&auth, "auth", false, "enable user/pass auth")
cmd.Flags().StringVar(&login, "login", "", "SOCKS5 login (when --auth)")
cmd.Flags().StringVar(&password, "password", "", "SOCKS5 password (when --auth)")
_ = cmd.MarkFlagRequired("host")
_ = cmd.MarkFlagRequired("port")
return cmd
}
func newGUICmd() *cobra.Command {
return &cobra.Command{
Use: "gui",