Files
sing-box/common/vboxusb/driver_windows.go
T
世界 6a4ae5263e usbip: extract URBEngine + userspaceURBSession from darwin backend
Both the existing darwin backend and the upcoming windows backend
drive USB devices from user space (IOUSBHost CGO calls vs. VBoxUSB
IOCTLs). Refactor the per-attachment URB loop out of host_darwin.go
into a platform-agnostic userspaceURBSession that talks to a URBEngine
interface; the darwin-specific dispatch becomes a 30-line
darwinIOUSBHostEngine. Linux's kernelHandoffSession is untouched.

Move hex8 into shared.go and add usbipStatusEIO so the shared session
does not depend on golang.org/x/sys/unix (Windows has no equivalent).
2026-06-09 10:42:35 +08:00

261 lines
6.9 KiB
Go

//go:build windows
package vboxusb
import (
"errors"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// EnsureDrivers extracts the bundled VBoxUSB + VBoxUSBMon binaries and
// makes both drivers available to PnP:
//
// 1. VBoxUSBMon is registered as a SERVICE_KERNEL_DRIVER demand-start
// service. The actual handle to \\.\VBoxUSBMon is opened separately
// via OpenMonitor.
// 2. VBoxUSB.inf is copied into the driver store via SetupCopyOEMInfW
// so PnP knows the driver exists; actual per-device binding is
// orchestrated later via VBoxUSBMon's ADD_FILTER mechanism.
//
// Requires Administrator (SeLoadDriverPrivilege). Safe to call from
// multiple processes concurrently; a named global mutex serializes
// the install.
func EnsureDrivers() error {
driverOnce.Do(func() {
driverErr = installDrivers()
})
return driverErr
}
var (
driverOnce sync.Once
driverErr error
)
func installDrivers() error {
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
return E.New("vboxusb: unsupported GOARCH ", runtime.GOARCH)
}
dir, err := ensureExtracted()
if err != nil {
return err
}
mutexName, _ := windows.UTF16PtrFromString("Global\\SingBoxVBoxUSBInstallMutex")
mutex, err := windows.CreateMutex(nil, false, mutexName)
if err != nil {
return E.Cause(err, "vboxusb: create install mutex")
}
defer windows.CloseHandle(mutex)
_, err = windows.WaitForSingleObject(mutex, windows.INFINITE)
if err != nil {
return E.Cause(err, "vboxusb: wait install mutex")
}
defer windows.ReleaseMutex(mutex)
err = installMonitorService(dir)
if err != nil {
return err
}
err = installVBoxUSBInf(dir)
if err != nil {
return err
}
return nil
}
func installMonitorService(dir string) error {
sysPath := filepath.Join(dir, "VBoxUSBMon.sys")
sysPathW, err := windows.UTF16PtrFromString(sysPath)
if err != nil {
return E.Cause(err, "vboxusb: utf16 monitor path")
}
manager, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_ALL_ACCESS)
if err != nil {
return E.Cause(err, "vboxusb: open SCM")
}
defer windows.CloseServiceHandle(manager)
serviceNameW, _ := windows.UTF16PtrFromString(MonitorServiceName)
service, err := windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS)
if err != nil {
service, err = windows.CreateService(
manager,
serviceNameW,
serviceNameW,
windows.SERVICE_ALL_ACCESS,
windows.SERVICE_KERNEL_DRIVER,
windows.SERVICE_DEMAND_START,
windows.SERVICE_ERROR_NORMAL,
sysPathW,
nil, nil, nil, nil, nil,
)
if err != nil {
if errors.Is(err, windows.ERROR_SERVICE_EXISTS) {
service, err = windows.OpenService(manager, serviceNameW, windows.SERVICE_ALL_ACCESS)
}
if err != nil {
return wrapInstallError(err)
}
}
}
defer windows.CloseServiceHandle(service)
err = windows.StartService(service, 0, nil)
if err != nil && errors.Is(err, windows.ERROR_SERVICE_DISABLED) {
// A prior process called DeleteService on a still-loaded
// driver: SCM marks the record for deletion and flips
// START_TYPE to DISABLED until the last handle closes.
// Re-enable so we can start instead of waiting for a reboot.
err = windows.ChangeServiceConfig(
service,
windows.SERVICE_NO_CHANGE,
windows.SERVICE_DEMAND_START,
windows.SERVICE_NO_CHANGE,
nil, nil, nil, nil, nil, nil, nil,
)
if err != nil {
return E.Cause(err, "vboxusb: re-enable disabled monitor service")
}
err = windows.StartService(service, 0, nil)
}
if err != nil && !errors.Is(err, windows.ERROR_SERVICE_ALREADY_RUNNING) {
return E.Cause(err, "vboxusb: start monitor service")
}
return nil
}
func wrapInstallError(err error) error {
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return E.Cause(err, "vboxusb: installing the kernel driver requires Administrator privileges")
}
return E.Cause(err, "vboxusb: create monitor service")
}
// installVBoxUSBInf copies VBoxUSB.inf into the Windows driver store
// via SetupCopyOEMInfW so PnP knows the driver is available. The
// actual per-device binding is triggered later by VBoxUSBMon's
// ADD_FILTER + a port cycle.
//
// SetupCopyOEMInfW is not exposed by golang.org/x/sys/windows, so we
// resolve it manually via the setupapi DLL.
func installVBoxUSBInf(dir string) error {
infPath := filepath.Join(dir, "VBoxUSB.inf")
infPathW, err := windows.UTF16PtrFromString(infPath)
if err != nil {
return E.Cause(err, "vboxusb: utf16 inf path")
}
dirW, err := windows.UTF16PtrFromString(dir)
if err != nil {
return E.Cause(err, "vboxusb: utf16 inf dir")
}
const (
spostPath = 1 // SPOST_PATH
spCopyNoStyle = 0
)
ret, _, callErr := procSetupCopyOEMInfW.Call(
uintptr(unsafe.Pointer(infPathW)),
uintptr(unsafe.Pointer(dirW)),
uintptr(spostPath),
uintptr(spCopyNoStyle),
0, 0, 0, 0,
)
if ret == 0 {
if errors.Is(callErr, windows.ERROR_FILE_EXISTS) {
// Already present in driver store; not an error.
return nil
}
return E.Cause(callErr, "vboxusb: SetupCopyOEMInfW")
}
return nil
}
var (
modSetupAPI = windows.NewLazyDLL("setupapi.dll")
procSetupCopyOEMInfW = modSetupAPI.NewProc("SetupCopyOEMInfW")
)
type assetFile struct {
name string
data []byte
}
var (
extractOnce sync.Once
extractErr error
extractDir string
)
// The on-disk copy is protected by Authenticode signature enforcement
// at SCM StartService time; any tampering with the .sys is rejected
// by the kernel loader before we ever see it.
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("vboxusb: unsupported architecture ", runtime.GOARCH)
}
base, err := os.UserCacheDir()
if err != nil {
return "", E.Cause(err, "vboxusb: locate user cache dir")
}
dir := filepath.Join(base, "sing-box", "vboxusb", "v"+AssetVersion)
err = os.MkdirAll(dir, 0o755)
if err != nil {
return "", E.Cause(err, "vboxusb: mkdir ", dir)
}
for _, asset := range files {
err = ensureAsset(dir, asset)
if err != nil {
return "", err
}
}
return dir, nil
}
// Concurrent sing-box processes race on os.Rename (atomic on NTFS);
// whichever wins creates the final file. Writers that lose the race
// silently 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, "vboxusb: stat ", asset.name)
}
tmp := target + ".tmp-" + strconv.Itoa(os.Getpid())
err = os.WriteFile(tmp, asset.data, 0o644)
if err != nil {
return E.Cause(err, "vboxusb: 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, "vboxusb: rename ", asset.name)
}
return nil
}