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:
+94
-1
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user