usbip: restart device nodes so VBoxUSB capture and release take effect

VBoxUSBMon only rewrites a device's IDs while PnP enumerates it, so
adding a filter for an already-plugged device captured nothing until a
physical replug, and releasing one left it dead under VBoxUSB.sys.
Capture and release now drive the cfgmgr32 restart sequence
(query-and-remove, hub port cycle, re-setup) around the filter change,
mirroring usbipd-win's RestartingDevice.

Capture also changes the devnode's identity to the VBox stub ID, which
broke everything keyed on it: enumeration now reads the true
vendor/product/class/speed from the parent hub's descriptor cache (the
registry hardware ID reads as 80EE:CAFE once captured), data sessions
locate the VBoxUSB interface by bus/address instead of the original
instance ID, and Reconcile keeps exports alive through the
re-enumeration window with a short absence grace.
This commit is contained in:
世界
2026-06-10 09:22:55 +08:00
parent 5da1096a91
commit bb60072e01
4 changed files with 436 additions and 68 deletions
+85 -10
View File
@@ -27,10 +27,24 @@ var USBDeviceInterfaceGUID = windows.GUID{
Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed},
}
// vboxStubVendorID/vboxStubProductID are the IDs VBoxUSBMon rewrites a
// captured device's hardware ID to (so VBoxUSB.inf binds VBoxUSB.sys).
// Their presence marks a device currently owned by VBoxUSB; the true
// identity is then only available from the parent hub's descriptor.
const (
vboxStubVendorID uint16 = 0x80EE
vboxStubProductID uint16 = 0xCAFE
)
// USBDeviceInfo describes one USB device enumerated by Windows
// regardless of which function driver currently owns it. Bus/Address
// values are normalized into a stable bus-id string ("<bus>-<address>")
// matching Linux usbip conventions.
//
// VendorID/ProductID/Revision/DeviceClass come from the parent hub's
// cached device descriptor when available (stable across VBoxUSB
// capture), falling back to the registry hardware ID (which reads as
// the VBox stub ID once captured).
type USBDeviceInfo struct {
InstanceID string
HardwareID string
@@ -42,6 +56,14 @@ type USBDeviceInfo struct {
BusID string // "<bus>-<address>"
DeviceClass uint8
Speed DeviceSpeed
Captured bool // currently owned by VBoxUSB.sys
}
// IdentityIsStub reports whether VendorID/ProductID still carry the
// VBoxUSB stub identity, i.e. the device is captured and the hub
// descriptor (the only source of the true identity) was unavailable.
func (i USBDeviceInfo) IdentityIsStub() bool {
return i.VendorID == vboxStubVendorID && i.ProductID == vboxStubProductID
}
// EnumerateUSBDevices walks GUID_DEVINTERFACE_USB_DEVICE and returns
@@ -93,31 +115,84 @@ func EnumerateUSBDevices() ([]USBDeviceInfo, error) {
info.Address = toUint32(addressValue)
}
info.BusID = strconv.FormatUint(uint64(info.BusNumber), 10) + "-" + strconv.FormatUint(uint64(info.Address), 10)
info.Speed = probe.speedOf(devInfo, data, info.Address)
info.Captured = info.VendorID == vboxStubVendorID && info.ProductID == vboxStubProductID
descriptor, speed := probe.describe(devInfo, data, info.Address)
info.Speed = speed
if descriptor != nil {
info.VendorID = descriptor.vendorID
info.ProductID = descriptor.productID
info.Revision = descriptor.bcdDevice
info.DeviceClass = descriptor.deviceClass
}
out = append(out, info)
}
return out, nil
}
func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string, error) {
guid := MonitorAccessGUID
// WaitForCapturedDevice polls for a VBoxUSB-owned device at the given
// bus location and returns its VBoxUSB interface path. Capture changes
// the device's instance ID (VBoxUSBMon rewrites it to the stub ID), so
// the location — which survives the rewrite — is the only stable key.
func WaitForCapturedDevice(busNumber, address uint32, timeout time.Duration) (string, error) {
deadline := time.Now().Add(timeout)
for {
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
path, err := findCapturedDevice(busNumber, address)
if err == nil {
for _, p := range paths {
if p != "" {
return p, nil
}
}
return path, nil
}
if time.Now().After(deadline) {
return "", E.New("vboxusb: VBoxUSB interface for ", instanceID, " did not appear within ", timeout)
return "", E.Cause(err, "vboxusb: VBoxUSB interface for ", busNumber, "-", address, " did not appear within ", timeout)
}
time.Sleep(100 * time.Millisecond)
}
}
func findCapturedDevice(busNumber, address uint32) (string, error) {
guid := MonitorAccessGUID
devInfo, err := windows.SetupDiGetClassDevsEx(
&guid,
"",
0,
windows.DIGCF_PRESENT|windows.DIGCF_DEVICEINTERFACE,
0,
"",
)
if err != nil {
return "", E.Cause(err, "vboxusb: SetupDiGetClassDevsEx(VBoxUSB)")
}
defer devInfo.Close()
for i := 0; ; i++ {
data, err := windows.SetupDiEnumDeviceInfo(devInfo, i)
if err != nil {
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
return "", E.New("vboxusb: no captured device at ", busNumber, "-", address)
}
return "", E.Cause(err, "vboxusb: SetupDiEnumDeviceInfo[", i, "]")
}
busNumberValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_BUSNUMBER)
if err != nil || toUint32(busNumberValue) != busNumber {
continue
}
addressValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_ADDRESS)
if err != nil || toUint32(addressValue) != address {
continue
}
instanceID, err := windows.SetupDiGetDeviceInstanceId(devInfo, data)
if err != nil {
continue
}
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
if err != nil {
continue
}
for _, p := range paths {
if p != "" {
return p, nil
}
}
}
}
func firstString(value any) string {
switch v := value.(type) {
case string:
+156
View File
@@ -0,0 +1,156 @@
//go:build windows
package vboxusb
import (
"encoding/binary"
"time"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// DeviceRestart drives the cfgmgr32 restart-device sequence VBoxUSBMon
// depends on: the monitor only rewrites a device's IDs (and thereby
// hands it to VBoxUSB.sys, or back to its function driver) while PnP
// re-enumerates the device, which never happens for a device that is
// already sitting configured on the bus. Begin removes the devnode
// without restarting it; the caller mutates filters in between; Finish
// cycles the hub port (a software unplug/replug, so drivers see a
// clean device) and re-enables the devnode.
//
// Mirrors usbipd-win's RestartingDevice.
type DeviceRestart struct {
devInst uint32
hubPath string
port uint32
}
const (
cmLocateDevNodeNormal = 0x00000000
cmRemoveUINotOK = 0x00000001
cmRemoveNoRestart = 0x00000002
cmSetupDevNodeReady = 0x00000000
maxDeviceIDLength = 200
ioctlUSBHubCyclePort = 0x0022_0444 // CTL_CODE(FILE_DEVICE_USB, USB_HUB_CYCLE_PORT=273, METHOD_BUFFERED, FILE_ANY_ACCESS)
deviceRestartSettleTime = 100 * time.Millisecond
)
// BeginDeviceRestart resolves the devnode and its parent hub, then
// removes the device subtree without restart. Call Finish to bring the
// device back; the pair must not be left half-open.
func BeginDeviceRestart(instanceID string, port uint32) (*DeviceRestart, error) {
instanceW, err := windows.UTF16PtrFromString(instanceID)
if err != nil {
return nil, E.Cause(err, "vboxusb: utf16 instance id")
}
var devInst uint32
ret, _, _ := procCMLocateDevNodeW.Call(
uintptr(unsafe.Pointer(&devInst)),
uintptr(unsafe.Pointer(instanceW)),
cmLocateDevNodeNormal,
)
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
return nil, E.New("vboxusb: CM_Locate_DevNode(", instanceID, ") CR=", ret)
}
hubPath := parentHubInterfacePath(devInst)
var vetoType uint32
var vetoName [260]uint16
ret, _, _ = procCMQueryAndRemoveSubTreeW.Call(
uintptr(devInst),
uintptr(unsafe.Pointer(&vetoType)),
uintptr(unsafe.Pointer(&vetoName[0])),
uintptr(len(vetoName)),
cmRemoveNoRestart|cmRemoveUINotOK,
)
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
return nil, E.New("vboxusb: CM_Query_And_Remove_SubTree(", instanceID, ") CR=", ret,
" veto=", vetoType, " by ", windows.UTF16ToString(vetoName[:]))
}
return &DeviceRestart{devInst: devInst, hubPath: hubPath, port: port}, nil
}
// Finish re-enumerates the removed device. Errors are swallowed by
// design (mirrors upstream): the device may have been physically
// unplugged meanwhile, or re-enumerated by someone else already.
func (r *DeviceRestart) Finish() {
// Give the just-switched driver stack time to settle; upstream
// found flash drives fail to re-initialize without this.
time.Sleep(deviceRestartSettleTime)
cycleHubPort(r.hubPath, r.port)
_, _, _ = procCMSetupDevNode.Call(uintptr(r.devInst), cmSetupDevNodeReady)
}
// parentHubInterfacePath resolves the USB hub interface path of the
// device's parent before removal (afterwards the parent link may be
// unreliable). Empty on failure; Finish then skips the port cycle.
func parentHubInterfacePath(devInst uint32) string {
var parent uint32
ret, _, _ := procCMGetParent.Call(
uintptr(unsafe.Pointer(&parent)),
uintptr(devInst),
0,
)
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
return ""
}
var parentID [maxDeviceIDLength + 1]uint16
ret, _, _ = procCMGetDeviceIDW.Call(
uintptr(parent),
uintptr(unsafe.Pointer(&parentID[0])),
uintptr(len(parentID)),
0,
)
if windows.CONFIGRET(ret) != windows.CR_SUCCESS {
return ""
}
paths, err := windows.CM_Get_Device_Interface_List(
windows.UTF16ToString(parentID[:]),
&usbHubInterfaceGUID,
windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT,
)
if err != nil {
return ""
}
for _, p := range paths {
if p != "" {
return p
}
}
return ""
}
// cycleHubPort issues IOCTL_USB_HUB_CYCLE_PORT — a software
// unplug/replug of the given port. Best effort.
func cycleHubPort(hubPath string, port uint32) {
if hubPath == "" || port == 0 {
return
}
hub := openHub(hubPath)
if hub == windows.InvalidHandle {
return
}
defer windows.CloseHandle(hub)
// USB_CYCLE_PORT_PARAMS: ConnectionIndex (in) + StatusReturned (out).
var params [8]byte
binary.LittleEndian.PutUint32(params[0:4], port)
var returned uint32
_ = windows.DeviceIoControl(
hub,
ioctlUSBHubCyclePort,
&params[0], uint32(len(params)),
&params[0], uint32(len(params)),
&returned, nil,
)
}
var (
modCfgMgr32 = windows.NewLazySystemDLL("cfgmgr32.dll")
procCMLocateDevNodeW = modCfgMgr32.NewProc("CM_Locate_DevNodeW")
procCMGetParent = modCfgMgr32.NewProc("CM_Get_Parent")
procCMGetDeviceIDW = modCfgMgr32.NewProc("CM_Get_Device_IDW")
procCMQueryAndRemoveSubTreeW = modCfgMgr32.NewProc("CM_Query_And_Remove_SubTreeW")
procCMSetupDevNode = modCfgMgr32.NewProc("CM_Setup_DevNode")
)
+41 -17
View File
@@ -69,13 +69,25 @@ const (
nodeConnInfoExV2FlagSuperSpeedPlus = 0x4
)
// hubSpeedProbe resolves the negotiated link speed of devices by querying their
// parent hub. Open hub handles are cached for the lifetime of one enumeration;
// a nil/InvalidHandle entry caches a failure so it is not retried per device.
// hubSpeedProbe resolves devices' negotiated link speed and cached device
// descriptor by querying their parent hub. Open hub handles are cached for
// the lifetime of one enumeration; a nil/InvalidHandle entry caches a
// failure so it is not retried per device.
type hubSpeedProbe struct {
hubs map[string]windows.Handle
}
// hubDeviceDescriptor carries the identity fields of the
// USB_DEVICE_DESCRIPTOR embedded in USB_NODE_CONNECTION_INFORMATION_EX.
// The hub reports the real descriptor regardless of which function
// driver owns the device, so these survive VBoxUSB capture.
type hubDeviceDescriptor struct {
vendorID uint16
productID uint16
bcdDevice uint16
deviceClass uint8
}
func newHubSpeedProbe() *hubSpeedProbe {
return &hubSpeedProbe{hubs: make(map[string]windows.Handle)}
}
@@ -89,15 +101,16 @@ func (p *hubSpeedProbe) close() {
p.hubs = nil
}
// speedOf returns the device's link speed, or SpeedUnknown if the parent hub
// could not be opened or did not answer. port is the hub port index
// (SPDRP_ADDRESS) the device is attached to.
func (p *hubSpeedProbe) speedOf(devInfo windows.DevInfo, data *windows.DevInfoData, port uint32) DeviceSpeed {
// describe returns the device's descriptor identity and link speed, or
// (nil, SpeedUnknown) if the parent hub could not be opened or did not
// answer. port is the hub port index (SPDRP_ADDRESS) the device is
// attached to.
func (p *hubSpeedProbe) describe(devInfo windows.DevInfo, data *windows.DevInfoData, port uint32) (*hubDeviceDescriptor, DeviceSpeed) {
hub := p.parentHub(devInfo, data)
if hub == windows.InvalidHandle {
return SpeedUnknown
return nil, SpeedUnknown
}
return querySpeed(hub, port)
return queryNodeConnection(hub, port)
}
func (p *hubSpeedProbe) parentHub(devInfo windows.DevInfo, data *windows.DevInfoData) windows.Handle {
@@ -144,7 +157,7 @@ func openHub(hubPath string) windows.Handle {
return handle
}
func querySpeed(hub windows.Handle, port uint32) DeviceSpeed {
func queryNodeConnection(hub windows.Handle, port uint32) (*hubDeviceDescriptor, DeviceSpeed) {
buffer := make([]byte, nodeConnInfoExBufferSize)
binary.LittleEndian.PutUint32(buffer[0:4], port)
var returned uint32
@@ -156,23 +169,34 @@ func querySpeed(hub windows.Handle, port uint32) DeviceSpeed {
&returned, nil,
)
if err != nil || returned <= nodeConnInfoExSpeedOffset {
return SpeedUnknown
return nil, SpeedUnknown
}
// USB_DEVICE_DESCRIPTOR starts at offset 4 (after ConnectionIndex):
// bDeviceClass at +4, idVendor at +8, idProduct at +10, bcdDevice at +12.
descriptor := &hubDeviceDescriptor{
deviceClass: buffer[8],
vendorID: binary.LittleEndian.Uint16(buffer[12:14]),
productID: binary.LittleEndian.Uint16(buffer[14:16]),
bcdDevice: binary.LittleEndian.Uint16(buffer[16:18]),
}
var speed DeviceSpeed
switch buffer[nodeConnInfoExSpeedOffset] {
case usbDeviceSpeedLow:
return SpeedLow
speed = SpeedLow
case usbDeviceSpeedFull:
return SpeedFull
speed = SpeedFull
case usbDeviceSpeedHigh:
return SpeedHigh
speed = SpeedHigh
case usbDeviceSpeedSuper:
if superSpeedPlus(hub, port) {
return SpeedSuperPlus
speed = SpeedSuperPlus
} else {
speed = SpeedSuper
}
return SpeedSuper
default:
return SpeedUnknown
speed = SpeedUnknown
}
return descriptor, speed
}
func superSpeedPlus(hub windows.Handle, port uint32) bool {