Files
sing-box/common/usbipvhci/driver_windows.go
T
世界 4e9eedc1e8 usbip: upgrade outdated usbip-win2 drivers via capability probe
The install fast path treated an openable VHCI interface as 'driver
present', but the interface GUID is identical across all usbip-win2
releases while PLUGIN_HARDWARE_ONCE and STOP_ATTACH_ATTEMPTS only exist
since 0.9.7.5 — a machine with an older community release passed the
check and then failed every Plugin forever. The fast path now probes
STOP_ATTACH_ATTEMPTS and falls through to the (in-place, FORCE) install
when the driver lacks it. The probe's empty location doubles as
cleanup, discarding ghost reattach attempts left by a previous process.
2026-06-10 09:24:18 +08:00

298 lines
8.8 KiB
Go

//go:build windows
package usbipvhci
import (
"encoding/binary"
"errors"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"time"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// classGUIDDevClassUSB is GUID_DEVCLASS_USB (devguid.h) — the setup class
// the VHCI devnode is created under, matching usbip2_ude.inf Class=USB.
var classGUIDDevClassUSB = windows.GUID{
Data1: 0x36fc9e60,
Data2: 0xc465,
Data3: 0x11cf,
Data4: [8]byte{0x80, 0x56, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
}
// EnsureDriver extracts the bundled usbip-win2 UDE driver, registers the
// upper-filter package in the driver store, and creates the
// root-enumerated VHCI devnode when it is not already present. Requires
// Administrator. Idempotent; a named global mutex serializes concurrent
// installs, and an already-present interface is the fast-path no-op.
func EnsureDriver() error {
driverOnce.Do(func() {
driverErr = installDriver()
})
return driverErr
}
var (
driverOnce sync.Once
driverErr error
)
func installDriver() error {
err := probeDriver()
if err == nil {
return nil
}
dir, err := ensureExtracted()
if err != nil {
return err
}
mutexName, _ := windows.UTF16PtrFromString(`Global\SingBoxUSBIPVHCIInstallMutex`)
mutex, err := windows.CreateMutex(nil, false, mutexName)
if err != nil {
return E.Cause(err, "usbipvhci: create install mutex")
}
defer windows.CloseHandle(mutex)
_, err = windows.WaitForSingleObject(mutex, windows.INFINITE)
if err != nil {
return E.Cause(err, "usbipvhci: wait install mutex")
}
defer windows.ReleaseMutex(mutex)
// Another process may have completed the install while we waited.
err = probeDriver()
if err == nil {
return nil
}
// The upper filter is an extension INF: copying it into the driver
// store is enough; PnP applies it once the VHCI root hub appears.
err = addToDriverStore(filepath.Join(dir, "usbip2_filter.inf"))
if err != nil {
return E.Cause(err, "usbipvhci: register upper-filter driver")
}
err = createDevnodeAndInstall(filepath.Join(dir, "usbip2_ude.inf"))
if err != nil {
return E.Cause(err, "usbipvhci: create VHCI devnode")
}
return waitForInterface(20 * time.Second)
}
// probeDriver verifies not just that the VHCI interface exists but that
// the bound driver speaks the bundled ABI. The interface GUID is
// identical across all usbip-win2 releases, while PLUGIN_HARDWARE_ONCE
// and STOP_ATTACH_ATTEMPTS only exist since 0.9.7.5 — an installed
// community release older than that opens fine and then fails every
// Plugin, so interface presence alone must not short-circuit the
// install. The probe doubles as cleanup: the empty location means
// "cancel all scheduled attach attempts", discarding ghost reconnects
// left over from a previous process toward dead loopback ports.
func probeDriver() error {
controller, err := Open()
if err != nil {
return err
}
defer controller.Close()
_, err = controller.StopAttachAttempts("", "", "")
if err != nil {
return E.Cause(err, "usbipvhci: installed driver lacks STOP_ATTACH_ATTEMPTS (older than 0.9.7.5); upgrading")
}
return nil
}
// addToDriverStore imports an INF (and its catalog-verified payload) into
// the Windows driver store. SetupCopyOEMInfW rejects unsigned or tampered
// packages; ERROR_FILE_EXISTS means the package is already present.
func addToDriverStore(infPath string) error {
infW, err := windows.UTF16PtrFromString(infPath)
if err != nil {
return E.Cause(err, "usbipvhci: utf16 inf path")
}
dirW, err := windows.UTF16PtrFromString(filepath.Dir(infPath))
if err != nil {
return E.Cause(err, "usbipvhci: utf16 inf dir")
}
const spostPath = 1
ret, _, callErr := procSetupCopyOEMInfW.Call(
uintptr(unsafe.Pointer(infW)),
uintptr(unsafe.Pointer(dirW)),
uintptr(spostPath),
0, 0, 0, 0, 0,
)
if ret == 0 && !errors.Is(callErr, windows.ERROR_FILE_EXISTS) {
if errors.Is(callErr, windows.ERROR_ACCESS_DENIED) {
return E.Cause(callErr, "SetupCopyOEMInfW (Administrator required)")
}
return E.Cause(callErr, "SetupCopyOEMInfW")
}
return nil
}
// createDevnodeAndInstall creates the root-enumerated VHCI devnode and
// installs usbip2_ude.inf onto it — the native equivalent of usbip-win2's
// "devnode install". infPath must be absolute. If a VHCI devnode already
// exists (e.g. the driver was removed but the root node lingers), the
// driver is reinstalled onto it instead of creating a duplicate.
func createDevnodeAndInstall(infPath string) error {
err := updateDriverForPlugAndPlayDevices(udeHardwareID, infPath)
if err == nil {
return nil
}
devInfoSet, err := windows.SetupDiCreateDeviceInfoListEx(&classGUIDDevClassUSB, 0, "")
if err != nil {
return E.Cause(err, "SetupDiCreateDeviceInfoListEx")
}
defer devInfoSet.Close()
devInfoData, err := windows.SetupDiCreateDeviceInfo(devInfoSet, "USB", &classGUIDDevClassUSB, "", 0, windows.DICD_GENERATE_ID)
if err != nil {
return E.Cause(err, "SetupDiCreateDeviceInfo")
}
err = windows.SetupDiSetDeviceRegistryProperty(devInfoSet, devInfoData, windows.SPDRP_HARDWAREID, multiSzUTF16(udeHardwareID))
if err != nil {
return E.Cause(err, "SetupDiSetDeviceRegistryProperty")
}
err = windows.SetupDiCallClassInstaller(windows.DIF_REGISTERDEVICE, devInfoSet, devInfoData)
if err != nil {
return E.Cause(err, "SetupDiCallClassInstaller(DIF_REGISTERDEVICE)")
}
return updateDriverForPlugAndPlayDevices(udeHardwareID, infPath)
}
// updateDriverForPlugAndPlayDevices binds the INF driver to every present
// device matching hardwareID (the devnode just created). INSTALLFLAG_FORCE
// installs even when the bundled driver is not strictly newer.
func updateDriverForPlugAndPlayDevices(hardwareID, infPath string) error {
hardwareIDW, err := windows.UTF16PtrFromString(hardwareID)
if err != nil {
return E.Cause(err, "usbipvhci: utf16 hardware id")
}
infW, err := windows.UTF16PtrFromString(infPath)
if err != nil {
return E.Cause(err, "usbipvhci: utf16 inf path")
}
const installFlagForce = 0x00000001
var rebootRequired int32
ret, _, callErr := procUpdateDriverForPlugAndPlayDevicesW.Call(
0,
uintptr(unsafe.Pointer(hardwareIDW)),
uintptr(unsafe.Pointer(infW)),
uintptr(installFlagForce),
uintptr(unsafe.Pointer(&rebootRequired)),
)
if ret == 0 {
return E.Cause(callErr, "UpdateDriverForPlugAndPlayDevices")
}
return nil
}
func waitForInterface(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for {
err := probeDriver()
if err == nil {
return nil
}
if time.Now().After(deadline) {
return E.Cause(err, "usbipvhci: VHCI interface did not appear after install (the devnode may have failed to start; check Device Manager)")
}
time.Sleep(200 * time.Millisecond)
}
}
// multiSzUTF16 encodes s as a REG_MULTI_SZ (UTF-16LE, double-NUL terminated).
func multiSzUTF16(s string) []byte {
u16, err := windows.UTF16FromString(s)
if err != nil {
return nil
}
u16 = append(u16, 0) // second NUL ends the list
buf := make([]byte, len(u16)*2)
for i, v := range u16 {
binary.LittleEndian.PutUint16(buf[i*2:], v)
}
return buf
}
var (
extractOnce sync.Once
extractErr error
extractDir string
)
func ensureExtracted() (string, error) {
extractOnce.Do(func() {
extractDir, extractErr = extractImpl()
})
return extractDir, extractErr
}
func extractImpl() (string, error) {
files := assetFiles()
if len(files) == 0 {
return "", E.New("usbipvhci: no bundled driver for ", runtime.GOARCH)
}
base, err := os.UserCacheDir()
if err != nil {
return "", E.Cause(err, "usbipvhci: locate user cache dir")
}
dir := filepath.Join(base, "sing-box", "usbipvhci", "v"+assetVersion())
err = os.MkdirAll(dir, 0o755)
if err != nil {
return "", E.Cause(err, "usbipvhci: mkdir ", dir)
}
for _, asset := range files {
err = ensureAsset(dir, asset)
if err != nil {
return "", err
}
}
return dir, nil
}
// ensureAsset writes one asset atomically: concurrent sing-box processes
// race on os.Rename (atomic on NTFS) and losers discard their temp copy.
func ensureAsset(dir string, asset assetFile) error {
target := filepath.Join(dir, asset.name)
_, err := os.Stat(target)
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return E.Cause(err, "usbipvhci: stat ", asset.name)
}
tmp := target + ".tmp-" + strconv.Itoa(os.Getpid())
err = os.WriteFile(tmp, asset.data, 0o644)
if err != nil {
return E.Cause(err, "usbipvhci: write ", asset.name)
}
err = os.Rename(tmp, target)
if err != nil {
os.Remove(tmp)
_, statErr := os.Stat(target)
if statErr == nil {
return nil
}
return E.Cause(err, "usbipvhci: rename ", asset.name)
}
return nil
}
var (
modSetupAPI = windows.NewLazyDLL("setupapi.dll")
procSetupCopyOEMInfW = modSetupAPI.NewProc("SetupCopyOEMInfW")
modNewDev = windows.NewLazyDLL("newdev.dll")
procUpdateDriverForPlugAndPlayDevicesW = modNewDev.NewProc("UpdateDriverForPlugAndPlayDevicesW")
)