Files
drover-go/internal/sboxrun/install.go
T
root 48097f8671
Build / test (push) Failing after 31s
Build / build-windows (push) Has been skipped
pivot: replace WinDivert engine with embedded sing-box + wintun
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>
2026-05-01 23:02:12 +03:00

84 lines
2.2 KiB
Go

//go:build windows
package sboxrun
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
)
// AssetPaths records where the binaries landed after install.
type AssetPaths struct {
SingBoxExe string
WintunDLL string
WorkDir string // %PROGRAMDATA%\Drover\sboxrun
ConfigPath string // <workdir>\config.json
LogPath string // <workdir>\sing-box.log
}
// InstallAssets extracts sing-box.exe + wintun.dll into
// %PROGRAMDATA%\Drover\sboxrun\ (creating the directory if needed)
// and verifies SHA256. Idempotent — second runs skip if existing
// files match the embedded SHAs.
func InstallAssets() (*AssetPaths, error) {
pd := os.Getenv("ProgramData")
if pd == "" {
return nil, fmt.Errorf("ProgramData environment variable is not set")
}
dir := filepath.Join(pd, "Drover", "sboxrun")
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create %s: %w", dir, err)
}
exePath := filepath.Join(dir, "sing-box.exe")
dllPath := filepath.Join(dir, "wintun.dll")
if err := writeIfDifferent(exePath, singBoxExe, SingBoxSHA256); err != nil {
return nil, fmt.Errorf("install sing-box.exe: %w", err)
}
if err := writeIfDifferent(dllPath, wintunDLL, WintunSHA256); err != nil {
return nil, fmt.Errorf("install wintun.dll: %w", err)
}
return &AssetPaths{
SingBoxExe: exePath,
WintunDLL: dllPath,
WorkDir: dir,
ConfigPath: filepath.Join(dir, "config.json"),
LogPath: filepath.Join(dir, "sing-box.log"),
}, nil
}
func writeIfDifferent(path string, content []byte, expectedSHA string) error {
if existing, err := os.ReadFile(path); err == nil {
if strings.EqualFold(sha256Hex(existing), expectedSHA) {
return nil
}
}
tmp := path + ".new"
if err := os.WriteFile(tmp, content, 0644); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
_ = os.Remove(tmp)
return err
}
got, err := os.ReadFile(path)
if err != nil {
return err
}
if !strings.EqualFold(sha256Hex(got), expectedSHA) {
return fmt.Errorf("SHA256 mismatch after write at %s; antivirus may have tampered with the file. Add %%PROGRAMDATA%%\\Drover\\ to AV exclusions", path)
}
return nil
}
func sha256Hex(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}