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
+94 -1
View File
@@ -3,7 +3,11 @@ package main
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
@@ -29,16 +33,30 @@ func main() {
// AttachConsole(ATTACH_PARENT_PROCESS) wires that up. No-op elsewhere.
attachToParentConsole()
// Open a debug log file at %LOCALAPPDATA%\Drover\debug.log so we have
// post-mortem visibility into engine startup failures even when the
// process was launched via UAC re-elevation (which detaches stderr
// from the parent terminal).
setupDebugLog()
// Detect if we need admin for the command in os.Args[1:]. If we do and
// we're not admin, re-launch via ShellExecute("runas", ...) and exit.
// CLI subcommands like "check", "version", "update" don't need admin
// and will run without UAC prompt.
if CmdNeedsAdmin(os.Args[1:]) && !IsAdmin() {
needsAdm := CmdNeedsAdmin(os.Args[1:])
isAdm := IsAdmin()
log.Printf("main: post-console admin=%v needsAdmin=%v args=%v", isAdm, needsAdm, os.Args[1:])
if needsAdm && !isAdm {
log.Printf("main: invoking ReElevate")
if err := ReElevate(os.Args[1:]); err != nil {
log.Printf("main: ReElevate returned err: %v", err)
fmt.Fprintf(os.Stderr, "failed to re-elevate: %v\n", err)
} else {
log.Printf("main: ReElevate returned ok, exiting parent")
}
os.Exit(0)
}
log.Printf("main: continuing in current process (no re-elevation needed)")
// Inject our build version so the updater package can stamp it on the
// User-Agent header it sends to git.okcu.io.
@@ -50,6 +68,39 @@ func main() {
}
}
// setupDebugLog wires the standard `log` package to write to both stderr
// and %LOCALAPPDATA%\Drover\debug.log. Survives UAC re-launch (each
// process opens its own append-mode handle).
func setupDebugLog() {
dir := os.Getenv("LOCALAPPDATA")
if dir == "" {
dir = os.Getenv("TEMP")
}
if dir == "" {
return
}
dir = filepath.Join(dir, "Drover")
_ = os.MkdirAll(dir, 0755)
// Truncate on each startup — keeps the log focused on the current
// run instead of accumulating past sessions. If you need history,
// rotate before launch.
f, err := os.OpenFile(filepath.Join(dir, "debug.log"), os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
// On a UAC-elevated launch (Start-Process -Verb RunAs) we have no
// parent console — os.Stderr points at an invalid handle. Writing
// to it via MultiWriter fails the *entire* write, so logs silently
// drop. Just write to the file; CLI subcommands launched from a
// real console can grep the file.
log.SetOutput(f)
_ = io.Discard // keep io import used
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Printf("=== drover %s start pid=%d args=%v admin=%v at %s ===",
Version, os.Getpid(), os.Args[1:], IsAdmin(), time.Now().Format(time.RFC3339))
}
func newRootCmd() *cobra.Command {
root := &cobra.Command{
Use: "drover",
@@ -76,10 +127,52 @@ 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",