Launch
This commit is contained in:
318
internal/wintunloader/loader_windows.go
Normal file
318
internal/wintunloader/loader_windows.go
Normal file
@@ -0,0 +1,318 @@
|
||||
//go:build windows
|
||||
|
||||
package wintunloader
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:embed assets/wintun/windows/amd64/* assets/wintun/windows/arm64/*
|
||||
var embeddedWintun embed.FS
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
// Prepare makes Wintun available before tun2socks/WireGuard tries to load it.
|
||||
// Windows cannot create a real TUN adapter without the signed wintun.dll layer,
|
||||
// so this function validates the DLL architecture, extracts embedded assets when
|
||||
// present, and updates the current process DLL search path.
|
||||
func Prepare(logger Logger) error {
|
||||
arch := runtime.GOARCH
|
||||
if arch != "amd64" && arch != "arm64" {
|
||||
return fmt.Errorf("Wintun embedded loader only supports amd64/arm64, current GOARCH=%s", arch)
|
||||
}
|
||||
|
||||
exeDir := executableDir()
|
||||
workDir := workingDir()
|
||||
|
||||
candidateDirs := uniqueNonEmpty([]string{
|
||||
exeDir,
|
||||
workDir,
|
||||
filepath.Join(exeDir, "tools", "wintun", arch),
|
||||
filepath.Join(workDir, "tools", "wintun", arch),
|
||||
filepath.Join(exeDir, "tools", "wintun"),
|
||||
filepath.Join(workDir, "tools", "wintun"),
|
||||
})
|
||||
|
||||
for _, dir := range candidateDirs {
|
||||
path := filepath.Join(dir, "wintun.dll")
|
||||
if ok, reason := validDLLForArch(path, arch); ok {
|
||||
installDir, installPath, err := ensureProcessDLL(path, exeDir, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activateDLLDirectory(installDir, append(candidateDirs, installDir)...)
|
||||
if logger != nil {
|
||||
logger.Add("info", "Wintun DLL ready: %s", installPath)
|
||||
}
|
||||
return nil
|
||||
} else if fileExists(path) && logger != nil {
|
||||
logger.Add("warn", "Ignoring invalid Wintun DLL at %s: %s", path, reason)
|
||||
}
|
||||
}
|
||||
|
||||
embeddedPath := filepath.ToSlash(filepath.Join("assets", "wintun", "windows", arch, "wintun.dll"))
|
||||
data, err := embeddedWintun.ReadFile(embeddedPath)
|
||||
if err == nil {
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return fmt.Errorf("embedded wintun.dll is invalid for %s: %w", arch, err)
|
||||
}
|
||||
installDir, installPath, err := writeEmbeddedDLL(data, exeDir, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activateDLLDirectory(installDir, append(candidateDirs, installDir)...)
|
||||
if logger != nil {
|
||||
logger.Add("info", "Embedded Wintun extracted: %s", installPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("read embedded wintun.dll failed: %w", err)
|
||||
}
|
||||
|
||||
activateDLLDirectory("", candidateDirs...)
|
||||
return fmt.Errorf("wintun.dll was not found. The TUN engine is compiled into the app, but Windows still requires the signed Wintun DLL. Put the official %s wintun.dll at tools/wintun/%s/wintun.dll, run scripts/embed_wintun_from_tools.ps1, and rebuild", arch, arch)
|
||||
}
|
||||
|
||||
func ensureProcessDLL(source, exeDir, arch string) (string, string, error) {
|
||||
if ok, reason := validDLLForArch(source, arch); !ok {
|
||||
return "", "", fmt.Errorf("source Wintun DLL is invalid: %s: %s", source, reason)
|
||||
}
|
||||
if source == filepath.Join(exeDir, "wintun.dll") {
|
||||
return exeDir, source, nil
|
||||
}
|
||||
installDir := preferredInstallDir(exeDir, arch)
|
||||
installPath := filepath.Join(installDir, "wintun.dll")
|
||||
if samePath(source, installPath) {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if ok, _ := validDLLForArch(installPath, arch); ok {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if err := copyFile(source, installPath); err != nil {
|
||||
// Fall back to the source directory if we cannot copy beside the exe.
|
||||
return filepath.Dir(source), source, nil
|
||||
}
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
|
||||
func writeEmbeddedDLL(data []byte, exeDir, arch string) (string, string, error) {
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
installDir := preferredInstallDir(exeDir, arch)
|
||||
installPath := filepath.Join(installDir, "wintun.dll")
|
||||
if ok, _ := validDLLForArch(installPath, arch); ok {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if err := os.MkdirAll(installDir, 0o755); err != nil {
|
||||
return "", "", fmt.Errorf("create Wintun install directory failed: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(installPath, data, 0o644); err != nil {
|
||||
cacheDir := filepath.Join(userCacheDir(), "SocksRevivePC", "wintun", arch)
|
||||
cachePath := filepath.Join(cacheDir, "wintun.dll")
|
||||
if err2 := os.MkdirAll(cacheDir, 0o755); err2 != nil {
|
||||
return "", "", fmt.Errorf("write embedded Wintun failed: %w; cache fallback mkdir failed: %v", err, err2)
|
||||
}
|
||||
if err2 := os.WriteFile(cachePath, data, 0o644); err2 != nil {
|
||||
return "", "", fmt.Errorf("write embedded Wintun failed: %w; cache fallback write failed: %v", err, err2)
|
||||
}
|
||||
return cacheDir, cachePath, nil
|
||||
}
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
|
||||
func preferredInstallDir(exeDir, arch string) string {
|
||||
if exeDir != "" {
|
||||
return exeDir
|
||||
}
|
||||
return filepath.Join(userCacheDir(), "SocksRevivePC", "wintun", arch)
|
||||
}
|
||||
|
||||
func executableDir() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil || exe == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
|
||||
func workingDir() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return wd
|
||||
}
|
||||
|
||||
func userCacheDir() string {
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil || dir == "" {
|
||||
return os.TempDir()
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func activateDLLDirectory(primary string, dirs ...string) {
|
||||
if primary != "" {
|
||||
_ = setDLLDirectory(primary)
|
||||
}
|
||||
prependPath(dirs...)
|
||||
}
|
||||
|
||||
func setDLLDirectory(dir string) error {
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
return nil
|
||||
}
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
proc := kernel32.NewProc("SetDllDirectoryW")
|
||||
ptr, err := syscall.UTF16PtrFromString(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r1, _, callErr := proc.Call(uintptr(unsafe.Pointer(ptr)))
|
||||
if r1 == 0 {
|
||||
return callErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prependPath(dirs ...string) {
|
||||
dirs = uniqueNonEmpty(dirs)
|
||||
if len(dirs) == 0 {
|
||||
return
|
||||
}
|
||||
current := os.Getenv("PATH")
|
||||
parts := strings.Split(current, string(os.PathListSeparator))
|
||||
var cleaned []string
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" && !containsPath(dirs, p) {
|
||||
cleaned = append(cleaned, p)
|
||||
}
|
||||
}
|
||||
os.Setenv("PATH", strings.Join(append(dirs, cleaned...), string(os.PathListSeparator)))
|
||||
}
|
||||
|
||||
func uniqueNonEmpty(in []string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if !containsPath(out, s) {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsPath(paths []string, p string) bool {
|
||||
for _, existing := range paths {
|
||||
if samePath(existing, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func samePath(a, b string) bool {
|
||||
if a == "" || b == "" {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(filepath.Clean(a), filepath.Clean(b))
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
st, err := os.Stat(path)
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
func validDLLForArch(path, arch string) (bool, string) {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if st.IsDir() {
|
||||
return false, "path is a directory"
|
||||
}
|
||||
if st.Size() <= 1024 {
|
||||
return false, "file is too small"
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func validateDLLBytes(data []byte, arch string) error {
|
||||
if len(data) <= 1024 {
|
||||
return fmt.Errorf("file is too small")
|
||||
}
|
||||
if len(data) < 0x40 || data[0] != 'M' || data[1] != 'Z' {
|
||||
return fmt.Errorf("not a Windows PE DLL")
|
||||
}
|
||||
peOffset := int(binary.LittleEndian.Uint32(data[0x3c:0x40]))
|
||||
if peOffset <= 0 || len(data) < peOffset+6 {
|
||||
return fmt.Errorf("invalid PE header")
|
||||
}
|
||||
if string(data[peOffset:peOffset+4]) != "PE\x00\x00" {
|
||||
return fmt.Errorf("missing PE signature")
|
||||
}
|
||||
machine := binary.LittleEndian.Uint16(data[peOffset+4 : peOffset+6])
|
||||
want := uint16(0x8664) // IMAGE_FILE_MACHINE_AMD64
|
||||
if arch == "arm64" {
|
||||
want = 0xaa64 // IMAGE_FILE_MACHINE_ARM64
|
||||
}
|
||||
if machine != want {
|
||||
return fmt.Errorf("wrong architecture machine=0x%04x expected=0x%04x for %s", machine, want, arch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
if ok, reason := validDLLForArch(source, runtime.GOARCH); !ok {
|
||||
return fmt.Errorf("source DLL is missing or invalid: %s: %s", source, reason)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
in, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(out, in)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(target)
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = os.Remove(target)
|
||||
return closeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user