experimental/windivert: P2.1+P2.2 with WinDivert NETWORK+SOCKET layers

WIP snapshot before pivot to sing-box+TUN. Reached:
- TCP redirect via streamdump pattern (swap+Outbound=0+reinject)
- SOCKET layer for SYN-stage flow detection (avoids FLOW Establish-too-late race)
- Lazy PID→name resolution (catches Update.exe inside procscan tick)
- UDP forward via SOCKS5 UDP ASSOCIATE relay + manual reinject
- Result: chat works, voice times out (Discord IP discovery / RTC handshake fails)

Reason for pivot: WinDivert NAT-reinject pattern has subtle layer-3
semantics issues that DLL-injection / TUN-based proxies sidestep
entirely. Going with embedded sing-box + wintun as the engine —
proven path for Discord voice through SOCKS5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 22:27:54 +03:00
parent 8ceb7775d7
commit 4074e68715
19 changed files with 2666 additions and 62 deletions
+80
View File
@@ -0,0 +1,80 @@
//go:build windows
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"git.okcu.io/root/drover-go/internal/engine"
)
// runProxy is the body of the `drover proxy` subcommand. It builds an
// engine.Engine from the supplied flags, calls Start, and blocks until
// the process receives SIGINT (Ctrl+C) or SIGTERM. On signal, it
// gracefully Stops the engine and exits.
//
// All output is mirrored to stderr (visible when launched from a
// console session) AND %LOCALAPPDATA%\Drover\debug.log. setupDebugLog
// in main.go has already wired the log package to write to both.
func runProxy(parent context.Context, host string, port int, auth bool, login, password string) error {
if host == "" || port == 0 {
return fmt.Errorf("--host and --port are required")
}
ctx, cancel := signal.NotifyContext(parent, os.Interrupt, syscall.SIGTERM)
defer cancel()
cfg := engine.Config{
ProxyAddr: fmt.Sprintf("%s:%d", host, port),
UseAuth: auth,
Login: login,
Password: password,
Targets: []string{"Discord.exe", "DiscordCanary.exe", "DiscordPTB.exe", "Update.exe"},
}
log.Printf("proxy: building engine (proxy=%s auth=%v targets=%v)", cfg.ProxyAddr, cfg.UseAuth, cfg.Targets)
e, err := engine.New(cfg)
if err != nil {
return fmt.Errorf("engine.New: %w", err)
}
startCtx, startCancel := context.WithTimeout(ctx, 15*time.Second)
defer startCancel()
if err := e.Start(startCtx); err != nil {
log.Printf("proxy: Start failed: %v", err)
return fmt.Errorf("engine.Start: %w", err)
}
log.Printf("proxy: engine status=%s — press Ctrl+C to stop", e.Status())
// Periodic status ping so the user sees the engine is alive.
statusTk := time.NewTicker(10 * time.Second)
defer statusTk.Stop()
for {
select {
case <-ctx.Done():
log.Printf("proxy: signal received, shutting down")
if err := e.Stop(); err != nil {
log.Printf("proxy: Stop returned: %v", err)
}
log.Printf("proxy: bye")
return nil
case <-statusTk.C:
if le := e.LastError(); le != nil {
log.Printf("proxy: heartbeat status=%s lastErr=%v", e.Status(), le)
} else {
log.Printf("proxy: heartbeat status=%s", e.Status())
}
if e.Status() == engine.StatusFailed {
log.Printf("proxy: engine entered Failed state, exiting")
_ = e.Stop()
return fmt.Errorf("engine failed: %v", e.LastError())
}
}
}
}