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).
This commit is contained in:
世界
2026-05-18 12:38:47 +08:00
parent e59e14d89d
commit 6a4ae5263e
24 changed files with 2110 additions and 467 deletions
Binary file not shown.
+103
View File
@@ -0,0 +1,103 @@
; $Id: VBoxUSB.inf 170711 2025-09-29 16:57:21Z klaus $
;; @file
; VBox host drivers - USB drivers - Win32 USB device
;
;
; Copyright (C) 2011-2025 Oracle and/or its affiliates.
;
; This file is part of VirtualBox base platform packages, as
; available from https://www.virtualbox.org.
;
; This program is free software; you can redistribute it and/or
; modify it under the terms of the GNU General Public License
; as published by the Free Software Foundation, in version 3 of the
; License.
;
; This program is distributed in the hope that it will be useful, but
; WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
; General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program; if not, see <https://www.gnu.org/licenses>.
;
; The contents of this file may alternatively be used under the terms
; of the Common Development and Distribution License Version 1.0
; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
; in the VirtualBox distribution, in which case the provisions of the
; CDDL are applicable instead of those of the GPL.
;
; You may elect to license modified versions of this file under the
; terms and conditions of either the GPL or the CDDL or both.
;
; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
;
[Version]
Signature="$Windows NT$"
Class=USB
ClassGUID={36FC9E60-C465-11CF-8056-444553540000}
provider=%ORACLE%
DriverVer = 04/18/2026,7.2.8.23730 ; r173730
CatalogFile=VBoxUSB.cat
PnpLockdown=1
[SourceDisksNames]
1=%VBoxUSB_DiskDesc%,,,
[SourceDisksFiles]
VBoxUSB.sys = 1
[DestinationDirs]
DefaultDestDir = 12
[Manufacturer]
%ORACLE%=VBoxUSB,NTAMD64
[VBoxUSB.NTAMD64]
%VBoxUSB_DrvDesc%=VBoxUSB_Drv,USB\VID_80EE&PID_CAFE
[VBoxUSB_Drv.NTAMD64]
CopyFiles=VBoxUSB_CopyFiles
[VBoxUSB_Drv.NTAMD64.Services]
AddService=VBoxUSB,0x00000002,VBoxUSB_Service,VBoxUSB_AddReg
[DefaultUninstall.NTAMD64.Services]
DelService = VBoxUSB,0x00000200
DelReg = VBoxUSB_Registry
LegacyUninstall=1
[VBoxUSB_Service]
DisplayName = %VBoxUSB_SvcDesc%
ServiceType = %SERVICE_TYPE_KERNEL_DRIVER%
StartType = %SERVICE_STARTTYPE_DEMAND%
ErrorControl = %SERVICE_ERRORCTL_NORMAL%
ServiceBinary = %12%\VBoxUSB.sys
LoadOrderGroup = Base
[VBoxUSB_AddReg]
AddReg = VBoxUSB_Registry
[VBoxUSB_Registry]
HKR,,DevLoader,,*ntkern
HKR,,NTMPDriver,,VBoxUSB.sys
[VBoxUSB_CopyFiles]
VBoxUSB.sys
[Strings]
ORACLE="Oracle Corporation"
VBoxUSB_SvcDesc="VirtualBox USB Service"
VBoxUSB_DrvDesc="VirtualBox USB Driver"
VBoxUSB_DiskDesc="VBoxUSB Installation Disk"
; Useful constants
SERVICE_TYPE_KERNEL_DRIVER = 1
SERVICE_STARTTYPE_BOOT = 0
SERVICE_STARTTYPE_AUTO = 1
SERVICE_STARTTYPE_DEMAND = 3
SERVICE_ERRORCTL_IGNORE = 0
SERVICE_ERRORCTL_NORMAL = 1
Binary file not shown.
Binary file not shown.
Binary file not shown.
+103
View File
@@ -0,0 +1,103 @@
; $Id: VBoxUSB.inf 170711 2025-09-29 16:57:21Z klaus $
;; @file
; VBox host drivers - USB drivers - Win32 USB device
;
;
; Copyright (C) 2011-2025 Oracle and/or its affiliates.
;
; This file is part of VirtualBox base platform packages, as
; available from https://www.virtualbox.org.
;
; This program is free software; you can redistribute it and/or
; modify it under the terms of the GNU General Public License
; as published by the Free Software Foundation, in version 3 of the
; License.
;
; This program is distributed in the hope that it will be useful, but
; WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
; General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program; if not, see <https://www.gnu.org/licenses>.
;
; The contents of this file may alternatively be used under the terms
; of the Common Development and Distribution License Version 1.0
; (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
; in the VirtualBox distribution, in which case the provisions of the
; CDDL are applicable instead of those of the GPL.
;
; You may elect to license modified versions of this file under the
; terms and conditions of either the GPL or the CDDL or both.
;
; SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
;
[Version]
Signature="$Windows NT$"
Class=USB
ClassGUID={36FC9E60-C465-11CF-8056-444553540000}
provider=%ORACLE%
DriverVer = 04/18/2026,7.2.8.23730 ; r173730
CatalogFile=VBoxUSB.cat
PnpLockdown=1
[SourceDisksNames]
1=%VBoxUSB_DiskDesc%,,,
[SourceDisksFiles]
VBoxUSB.sys = 1
[DestinationDirs]
DefaultDestDir = 12
[Manufacturer]
%ORACLE%=VBoxUSB,NTARM64
[VBoxUSB.NTARM64]
%VBoxUSB_DrvDesc%=VBoxUSB_Drv,USB\VID_80EE&PID_CAFE
[VBoxUSB_Drv.NTARM64]
CopyFiles=VBoxUSB_CopyFiles
[VBoxUSB_Drv.NTARM64.Services]
AddService=VBoxUSB,0x00000002,VBoxUSB_Service,VBoxUSB_AddReg
[DefaultUninstall.NTARM64.Services]
DelService = VBoxUSB,0x00000200
DelReg = VBoxUSB_Registry
LegacyUninstall=1
[VBoxUSB_Service]
DisplayName = %VBoxUSB_SvcDesc%
ServiceType = %SERVICE_TYPE_KERNEL_DRIVER%
StartType = %SERVICE_STARTTYPE_DEMAND%
ErrorControl = %SERVICE_ERRORCTL_NORMAL%
ServiceBinary = %12%\VBoxUSB.sys
LoadOrderGroup = Base
[VBoxUSB_AddReg]
AddReg = VBoxUSB_Registry
[VBoxUSB_Registry]
HKR,,DevLoader,,*ntkern
HKR,,NTMPDriver,,VBoxUSB.sys
[VBoxUSB_CopyFiles]
VBoxUSB.sys
[Strings]
ORACLE="Oracle Corporation"
VBoxUSB_SvcDesc="VirtualBox USB Service"
VBoxUSB_DrvDesc="VirtualBox USB Driver"
VBoxUSB_DiskDesc="VBoxUSB Installation Disk"
; Useful constants
SERVICE_TYPE_KERNEL_DRIVER = 1
SERVICE_STARTTYPE_BOOT = 0
SERVICE_STARTTYPE_AUTO = 1
SERVICE_STARTTYPE_DEMAND = 3
SERVICE_ERRORCTL_IGNORE = 0
SERVICE_ERRORCTL_NORMAL = 1
Binary file not shown.
Binary file not shown.
+26
View File
@@ -0,0 +1,26 @@
//go:build windows && amd64
package vboxusb
import _ "embed"
//go:embed assets/amd64/VBoxUSB.sys
var vboxUSBSys []byte
//go:embed assets/amd64/VBoxUSB.inf
var vboxUSBInf []byte
//go:embed assets/amd64/VBoxUSB.cat
var vboxUSBCat []byte
//go:embed assets/amd64/VBoxUSBMon.sys
var vboxUSBMonSys []byte
func assetFiles() []assetFile {
return []assetFile{
{"VBoxUSB.sys", vboxUSBSys},
{"VBoxUSB.inf", vboxUSBInf},
{"VBoxUSB.cat", vboxUSBCat},
{"VBoxUSBMon.sys", vboxUSBMonSys},
}
}
+26
View File
@@ -0,0 +1,26 @@
//go:build windows && arm64
package vboxusb
import _ "embed"
//go:embed assets/arm64/VBoxUSB.sys
var vboxUSBSys []byte
//go:embed assets/arm64/VBoxUSB.inf
var vboxUSBInf []byte
//go:embed assets/arm64/VBoxUSB.cat
var vboxUSBCat []byte
//go:embed assets/arm64/VBoxUSBMon.sys
var vboxUSBMonSys []byte
func assetFiles() []assetFile {
return []assetFile{
{"VBoxUSB.sys", vboxUSBSys},
{"VBoxUSB.inf", vboxUSBInf},
{"VBoxUSB.cat", vboxUSBCat},
{"VBoxUSBMon.sys", vboxUSBMonSys},
}
}
@@ -0,0 +1,5 @@
//go:build windows && !amd64 && !arm64
package vboxusb
func assetFiles() []assetFile { return nil }
+310
View File
@@ -0,0 +1,310 @@
//go:build windows
package vboxusb
import (
"encoding/binary"
"errors"
"runtime"
"sync"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// Device holds an open handle to one VBoxUSB-claimed USB device plus a
// private event for overlapped I/O. Methods are not safe for
// concurrent use on the same Device — the session layer serializes
// per-endpoint via per-endpoint goroutines.
type Device struct {
handle windows.Handle
event windows.Handle
closing sync.Once
closeErr error
}
// OpenDevice opens a per-device VBoxUSB handle by its setupapi-resolved
// interface path (typically obtained via SetupDiEnumDeviceInterfaces
// over MonitorAccessGUID). The handle is opened FILE_FLAG_OVERLAPPED.
// FILE_SKIP_COMPLETION_PORT_ON_SUCCESS is set so synchronously
// completed URBs do not bounce through the IOCP — matches the
// usbipd-win fast path.
func OpenDevice(interfacePath string) (*Device, error) {
pathW, err := windows.UTF16PtrFromString(interfacePath)
if err != nil {
return nil, E.Cause(err, "vboxusb: utf16 device path")
}
handle, err := windows.CreateFile(
pathW,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED,
0,
)
if err != nil {
return nil, E.Cause(err, "vboxusb: open ", interfacePath)
}
// Skip IOCP wakeup on synchronous completion. Tolerated on Windows
// 7+; ignore errors since the slow path still works.
_ = windows.SetFileCompletionNotificationModes(handle, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS)
event, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
windows.CloseHandle(handle)
return nil, E.Cause(err, "vboxusb: create event")
}
return &Device{handle: handle, event: event}, nil
}
// Close releases the handle. Aborts any in-flight IOCTLs (they return
// ERROR_OPERATION_ABORTED). Idempotent.
func (d *Device) Close() error {
d.closing.Do(func() {
var errs []error
if d.handle != 0 {
err := windows.CloseHandle(d.handle)
if err != nil {
errs = append(errs, err)
}
d.handle = 0
}
if d.event != 0 {
err := windows.CloseHandle(d.event)
if err != nil {
errs = append(errs, err)
}
d.event = 0
}
d.closeErr = E.Errors(errs...)
})
return d.closeErr
}
// GetVersion returns the VBoxUSB driver version (8 bytes: major + minor).
// Call before Claim to verify the driver is at least DriverMajorVersion.
func (d *Device) GetVersion() (uint32, uint32, error) {
var buf [8]byte
_, err := d.ioctl(IOCTLGetVersion, nil, buf[:])
if err != nil {
return 0, 0, E.Cause(err, "vboxusb: GET_VERSION")
}
return binary.LittleEndian.Uint32(buf[0:4]), binary.LittleEndian.Uint32(buf[4:8]), nil
}
// Claim acquires exclusive ownership of the device. The driver returns
// Claimed=false if another handle owns the device. The input field is
// unused by the driver but the buffer must equal the output size, so
// pass a zero-initialized 2-byte input.
func (d *Device) Claim() (bool, error) {
var in [2]byte
var out [2]byte
_, err := d.ioctl(IOCTLUSBClaimDevice, in[:], out[:])
if err != nil {
return false, E.Cause(err, "vboxusb: USB_CLAIM_DEVICE")
}
return out[1] != 0, nil
}
// SetConfig issues USBSUP_IOCTL_USB_SET_CONFIG so VBoxUSB rebuilds its
// pipe-handle table for the requested bConfigurationValue. Must be
// awaited before any URBs targeting endpoints in the new configuration.
func (d *Device) SetConfig(value byte) error {
in := [1]byte{value}
_, err := d.ioctl(IOCTLUSBSetConfig, in[:], nil)
if err != nil {
return E.Cause(err, "vboxusb: USB_SET_CONFIG")
}
return nil
}
// SelectInterface issues USBSUP_IOCTL_USB_SELECT_INTERFACE. Same
// pipe-handle implications as SetConfig.
func (d *Device) SelectInterface(num, alt byte) error {
in := [2]byte{num, alt}
_, err := d.ioctl(IOCTLUSBSelectInterface, in[:], nil)
if err != nil {
return E.Cause(err, "vboxusb: USB_SELECT_INTERFACE")
}
return nil
}
// ClearEndpoint clears the STALL condition on a halted endpoint. The
// argument is the raw 8-bit address (direction bit in MSB).
func (d *Device) ClearEndpoint(rawEndpoint byte) error {
in := [1]byte{rawEndpoint}
_, err := d.ioctl(IOCTLUSBClearEndpoint, in[:], nil)
if err != nil {
return E.Cause(err, "vboxusb: USB_CLEAR_ENDPOINT")
}
return nil
}
// AbortEndpoint aborts all pending submits on the given raw endpoint
// address. VBoxUSB has no per-URB cancel; callers must use the
// abort-holdoff heuristic to avoid aborting URBs that completed in the
// race window.
func (d *Device) AbortEndpoint(rawEndpoint byte) error {
in := [1]byte{rawEndpoint}
_, err := d.ioctl(IOCTLUSBAbortEndpoint, in[:], nil)
if err != nil {
return E.Cause(err, "vboxusb: USB_ABORT_ENDPOINT")
}
return nil
}
// URB is the high-level form of USBSUP_URB. The session layer fills it
// and hands to SendURB; on return Length and IsoPackets carry the
// driver's response. Buffer must remain referenced by the caller until
// SendURB returns; SendURB internally calls runtime.KeepAlive.
type URB struct {
Type TransferType
Endpoint uint32 // 4-bit endpoint index, no direction bit
Direction Direction
Flags TransferFlags
Length uint64
Buffer []byte
IsoPackets []IsoPacket
}
// IsoPacket is one USBSUP_ISOCPKT entry. Length is in/out (requested
// then actual); Offset is in-only; Status is out-only.
type IsoPacket struct {
Length uint16
Offset uint16
Status URBError
}
// URBResult mirrors the fields VBoxUSB writes back into the URB struct.
type URBResult struct {
Error URBError
Length uint64
IsoPackets []IsoPacket
}
// urbStructSize is the on-the-wire size of USBSUP_URB with Pack=4 on
// 64-bit systems (both amd64 and arm64; nint is 8 bytes either way).
// Layout (offsets in bytes):
//
// 0 type uint32
// 4 ep uint32
// 8 dir uint32
// 12 flags uint32
// 16 error uint32
// 20 len uint64
// 28 buf uint64 (native pointer)
// 36 numIsoPkts uint32
// 40 aIsoPkts [8]uint64 (each entry is cb(u16) off(u16) stat(u32) = 8 bytes)
//
// Total = 104.
const urbStructSize = 104
// SendURB marshals urb into a USBSUP_URB, dispatches IOCTL_SEND_URB,
// and unmarshals the response back into urb.Length / urb.IsoPackets.
// Caller must keep urb.Buffer alive across the call (SendURB does so
// internally for the duration of the syscall).
func (d *Device) SendURB(urb *URB) error {
if len(urb.IsoPackets) > MaxIsoPacketsPerURB {
return E.New("vboxusb: too many iso packets: ", len(urb.IsoPackets), " > ", MaxIsoPacketsPerURB)
}
var raw [urbStructSize]byte
binary.LittleEndian.PutUint32(raw[0:4], uint32(urb.Type))
binary.LittleEndian.PutUint32(raw[4:8], urb.Endpoint)
binary.LittleEndian.PutUint32(raw[8:12], uint32(urb.Direction))
binary.LittleEndian.PutUint32(raw[12:16], uint32(urb.Flags))
// raw[16:20] error is filled by the driver
binary.LittleEndian.PutUint64(raw[20:28], urb.Length)
var bufPtr uintptr
if len(urb.Buffer) > 0 {
bufPtr = uintptr(unsafe.Pointer(&urb.Buffer[0]))
}
binary.LittleEndian.PutUint64(raw[28:36], uint64(bufPtr))
binary.LittleEndian.PutUint32(raw[36:40], uint32(len(urb.IsoPackets)))
for i, iso := range urb.IsoPackets {
base := 40 + i*8
binary.LittleEndian.PutUint16(raw[base:base+2], iso.Length)
binary.LittleEndian.PutUint16(raw[base+2:base+4], iso.Offset)
binary.LittleEndian.PutUint32(raw[base+4:base+8], uint32(iso.Status))
}
_, err := d.ioctl(IOCTLSendURB, raw[:], raw[:])
runtime.KeepAlive(urb.Buffer)
if err != nil {
return E.Cause(err, "vboxusb: SEND_URB")
}
urb.Length = binary.LittleEndian.Uint64(raw[20:28])
errCode := URBError(binary.LittleEndian.Uint32(raw[16:20]))
for i := range urb.IsoPackets {
base := 40 + i*8
urb.IsoPackets[i].Length = binary.LittleEndian.Uint16(raw[base : base+2])
urb.IsoPackets[i].Offset = binary.LittleEndian.Uint16(raw[base+2 : base+4])
urb.IsoPackets[i].Status = URBError(binary.LittleEndian.Uint32(raw[base+4 : base+8]))
}
if errCode != URBOK {
// Surface the device-reported error through URB.Length on
// the caller side: it's already set to actual transferred
// bytes by the driver. Return a typed error so the engine
// layer can translate to a USBIP status.
return &URBStatusError{Code: errCode}
}
return nil
}
// URBStatusError signals a USB-level failure reported by the driver
// (STALL, DNR, CRC, etc.) rather than a Windows-level IOCTL failure.
// The engine layer translates these into USBIP wire status.
type URBStatusError struct {
Code URBError
}
func (e *URBStatusError) Error() string {
switch e.Code {
case URBStall:
return "vboxusb: URB stalled"
case URBDeviceNotResponding:
return "vboxusb: device not responding"
case URBCRCError:
return "vboxusb: CRC error"
case URBNACError:
return "vboxusb: NAC error"
case URBUnderrun:
return "vboxusb: data underrun"
case URBOverrun:
return "vboxusb: data overrun"
default:
return "vboxusb: unknown URB error"
}
}
// ioctl is the single synchronous overlapped DeviceIoControl primitive.
// Ported from common/windivert/handle_windows.go:263-290. The event is
// the per-Device event (reused across calls) so we avoid CreateEvent on
// every URB.
func (d *Device) ioctl(code uint32, in []byte, out []byte) (uint32, error) {
var overlapped windows.Overlapped
overlapped.HEvent = d.event
_ = windows.ResetEvent(d.event)
var inPtr *byte
var inLen uint32
if len(in) > 0 {
inPtr = &in[0]
inLen = uint32(len(in))
}
var outPtr *byte
var outLen uint32
if len(out) > 0 {
outPtr = &out[0]
outLen = uint32(len(out))
}
var returned uint32
err := windows.DeviceIoControl(d.handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped)
if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) {
return 0, err
}
err = windows.GetOverlappedResult(d.handle, &overlapped, &returned, true)
if err != nil {
return 0, err
}
return returned, nil
}
+260
View File
@@ -0,0 +1,260 @@
//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
}
+216
View File
@@ -0,0 +1,216 @@
//go:build windows
package vboxusb
import (
"encoding/binary"
"errors"
"sync"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// Monitor is a handle to \\.\VBoxUSBMon. It is shared across the
// process (one global monitor handle per sing-box server); per-device
// filters are added/removed against it. Concurrent ADD_FILTER /
// REMOVE_FILTER calls are serialized inside the driver but our handle
// reuses a single overlapped event — so Monitor methods are not safe
// for concurrent use across goroutines. The caller (the export host)
// is expected to serialize.
type Monitor struct {
handle windows.Handle
event windows.Handle
closing sync.Once
closeErr error
}
// OpenMonitor opens \\.\VBoxUSBMon. EnsureDrivers must have been
// called first (and must have succeeded) so the VBoxUSBMon kernel
// service is loaded; otherwise CreateFile returns FILE_NOT_FOUND.
func OpenMonitor() (*Monitor, error) {
pathW, err := windows.UTF16PtrFromString(MonitorDevicePath)
if err != nil {
return nil, E.Cause(err, "vboxusb: utf16 monitor path")
}
handle, err := windows.CreateFile(
pathW,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OVERLAPPED,
0,
)
if err != nil {
if errors.Is(err, windows.ERROR_FILE_NOT_FOUND) {
return nil, E.Cause(err, "vboxusb: open monitor (driver not loaded?)")
}
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
return nil, E.Cause(err, "vboxusb: open monitor (administrator required)")
}
return nil, E.Cause(err, "vboxusb: open monitor")
}
event, err := windows.CreateEvent(nil, 1, 0, nil)
if err != nil {
windows.CloseHandle(handle)
return nil, E.Cause(err, "vboxusb: create monitor event")
}
return &Monitor{handle: handle, event: event}, nil
}
func (m *Monitor) Close() error {
m.closing.Do(func() {
var errs []error
if m.handle != 0 {
err := windows.CloseHandle(m.handle)
if err != nil {
errs = append(errs, err)
}
m.handle = 0
}
if m.event != 0 {
err := windows.CloseHandle(m.event)
if err != nil {
errs = append(errs, err)
}
m.event = 0
}
m.closeErr = E.Errors(errs...)
})
return m.closeErr
}
// GetVersion returns the monitor driver version (8 bytes packed:
// major + minor). Reject if major mismatches DriverMajorVersion.
func (m *Monitor) GetVersion() (uint32, uint32, error) {
var buf [8]byte
_, err := m.ioctl(IOCTLMonitorGetVersion, nil, buf[:])
if err != nil {
return 0, 0, E.Cause(err, "vboxusb: monitor GET_VERSION")
}
return binary.LittleEndian.Uint32(buf[0:4]), binary.LittleEndian.Uint32(buf[4:8]), nil
}
// AddFilter installs a one-shot filter that captures the next PnP
// arrival matching the filter. Returns a filter id that must be passed
// to RemoveFilter on detach. usbipd-win uses a permanent CAPTURE
// filter; we follow the same pattern so the filter survives transient
// PnP retries during RestartingDevice.
func (m *Monitor) AddFilter(filter Filter) (uint64, error) {
in := encodeFilter(filter)
var out [12]byte // UsbSupFltAddOut: uint64 uId + int32 rc
_, err := m.ioctl(IOCTLMonitorAddFilter, in[:], out[:])
if err != nil {
return 0, E.Cause(err, "vboxusb: monitor ADD_FILTER")
}
id := binary.LittleEndian.Uint64(out[0:8])
rc := int32(binary.LittleEndian.Uint32(out[8:12]))
if rc < 0 {
return 0, E.New("vboxusb: monitor ADD_FILTER returned rc=", rc)
}
return id, nil
}
// RemoveFilter releases a filter previously returned by AddFilter.
// Safe to call after the device has already left.
func (m *Monitor) RemoveFilter(id uint64) error {
var in [8]byte
binary.LittleEndian.PutUint64(in[:], id)
_, err := m.ioctl(IOCTLMonitorRemoveFilter, in[:], nil)
if err != nil {
return E.Cause(err, "vboxusb: monitor REMOVE_FILTER")
}
return nil
}
func (m *Monitor) ioctl(code uint32, in []byte, out []byte) (uint32, error) {
var overlapped windows.Overlapped
overlapped.HEvent = m.event
_ = windows.ResetEvent(m.event)
var inPtr *byte
var inLen uint32
if len(in) > 0 {
inPtr = &in[0]
inLen = uint32(len(in))
}
var outPtr *byte
var outLen uint32
if len(out) > 0 {
outPtr = &out[0]
outLen = uint32(len(out))
}
var returned uint32
err := windows.DeviceIoControl(m.handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped)
if err != nil && !errors.Is(err, windows.ERROR_IO_PENDING) {
return 0, err
}
err = windows.GetOverlappedResult(m.handle, &overlapped, &returned, true)
if err != nil {
return 0, err
}
return returned, nil
}
// encodeFilter builds a 312-byte USBFILTER packed struct matching the
// layout in VirtualBox usbfilter.h (also documented in
// /tmp/usbipd-win/Usbipd/Interop/VBoxUsbMon.cs:77-105).
//
// Layout (offsets):
//
// 0 u32Magic uint32 (0x19670408)
// 4 enmType uint32 (5 = CAPTURE)
// 8 aFields [11]{enmMatch uint16, u16Value uint16} (44 bytes)
// 52 offCurEnd uint32 (0)
// 56 achStrTab [256]byte (0)
//
// All entries default to IGNORE; caller-specified fields are upgraded
// to NUM_EXACT with the supplied value. String matches and offCurEnd
// stay zero (we never use string filters; the driver rejects nonzero
// offCurEnd with strange offsets).
func encodeFilter(f Filter) [312]byte {
const (
filterMagic uint32 = 0x19670408
filterCapture uint32 = 5 // UsbFilterType.CAPTURE
matchIgnore uint16 = 1 // UsbFilterMatch.IGNORE
matchNumExact uint16 = 3 // UsbFilterMatch.NUM_EXACT
)
const (
idxVendorID = 0
idxProductID = 1
idxDeviceRev = 2
idxDeviceClass = 3
idxBus = 6
idxPort = 7
)
var raw [312]byte
binary.LittleEndian.PutUint32(raw[0:4], filterMagic)
binary.LittleEndian.PutUint32(raw[4:8], filterCapture)
for i := 0; i < 11; i++ {
binary.LittleEndian.PutUint16(raw[8+i*4:8+i*4+2], matchIgnore)
}
setField := func(idx int, value uint16) {
binary.LittleEndian.PutUint16(raw[8+idx*4:8+idx*4+2], matchNumExact)
binary.LittleEndian.PutUint16(raw[8+idx*4+2:8+idx*4+4], value)
}
if f.VendorID != nil {
setField(idxVendorID, *f.VendorID)
}
if f.ProductID != nil {
setField(idxProductID, *f.ProductID)
}
if f.DeviceRev != nil {
setField(idxDeviceRev, *f.DeviceRev)
}
if f.DeviceClass != nil {
setField(idxDeviceClass, *f.DeviceClass)
}
if f.Bus != nil {
setField(idxBus, *f.Bus)
}
if f.Port != nil {
setField(idxPort, *f.Port)
}
return raw
}
+265
View File
@@ -0,0 +1,265 @@
//go:build windows
package vboxusb
import (
"encoding/binary"
"errors"
"strconv"
"strings"
"sync"
"time"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// 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.
type USBDeviceInfo struct {
InstanceID string
HardwareID string
VendorID uint16
ProductID uint16
Revision uint16
BusNumber uint32
Address uint32 // device address on the bus (port path leaf)
BusID string // "<bus>-<address>"
DeviceClass uint8
}
// EnumerateUSBDevices walks GUID_DEVINTERFACE_USB_DEVICE and returns
// one record per attached USB device. Hubs are not filtered out here;
// the caller (export host) skips DeviceClass == 0x09.
func EnumerateUSBDevices() ([]USBDeviceInfo, error) {
devInfo, err := windows.SetupDiGetClassDevsEx(
&windows.GUID{
Data1: USBDeviceInterfaceGUID.Data1,
Data2: USBDeviceInterfaceGUID.Data2,
Data3: USBDeviceInterfaceGUID.Data3,
Data4: USBDeviceInterfaceGUID.Data4,
},
"",
0,
windows.DIGCF_PRESENT|windows.DIGCF_DEVICEINTERFACE,
0,
"",
)
if err != nil {
return nil, E.Cause(err, "vboxusb: SetupDiGetClassDevsEx")
}
defer devInfo.Close()
var out []USBDeviceInfo
for i := 0; ; i++ {
data, err := windows.SetupDiEnumDeviceInfo(devInfo, i)
if err != nil {
if errors.Is(err, windows.ERROR_NO_MORE_ITEMS) {
break
}
return nil, E.Cause(err, "vboxusb: SetupDiEnumDeviceInfo[", i, "]")
}
info := USBDeviceInfo{}
info.InstanceID, err = windows.SetupDiGetDeviceInstanceId(devInfo, data)
if err != nil {
continue
}
hardwareIDValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_HARDWAREID)
if err == nil {
info.HardwareID = firstString(hardwareIDValue)
info.VendorID, info.ProductID, info.Revision = parseHardwareID(info.HardwareID)
}
busNumberValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_BUSNUMBER)
if err == nil {
info.BusNumber = toUint32(busNumberValue)
}
addressValue, err := windows.SetupDiGetDeviceRegistryProperty(devInfo, data, windows.SPDRP_ADDRESS)
if err == nil {
info.Address = toUint32(addressValue)
}
info.BusID = strconv.FormatUint(uint64(info.BusNumber), 10) + "-" + strconv.FormatUint(uint64(info.Address), 10)
out = append(out, info)
}
return out, nil
}
// CycleHubPort issues IOCTL_USB_HUB_CYCLE_PORT on the parent hub for
// the given 1-based port number. This is the supported equivalent of
// physically unplugging and replugging the device; combined with a
// VBoxUSBMon filter it triggers PnP to bind VBoxUSB to the device.
//
// hubInterfacePath is the setupapi-resolved interface path of the
// parent hub (obtained via CM_Get_Device_Interface_List with
// USBHubInterfaceGUID).
func CycleHubPort(hubInterfacePath string, port uint32) error {
pathW, err := windows.UTF16PtrFromString(hubInterfacePath)
if err != nil {
return E.Cause(err, "vboxusb: utf16 hub path")
}
handle, err := windows.CreateFile(
pathW,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return E.Cause(err, "vboxusb: open hub ", hubInterfacePath)
}
defer windows.CloseHandle(handle)
// USB_CYCLE_PORT_PARAMS: ULONG ConnectionIndex; ULONG StatusReturned;
var params [8]byte
binary.LittleEndian.PutUint32(params[0:4], port)
var returned uint32
err = windows.DeviceIoControl(
handle,
IOCTLHubCyclePort,
&params[0], uint32(len(params)),
&params[0], uint32(len(params)),
&returned, nil,
)
if err != nil {
return E.Cause(err, "vboxusb: IOCTL_USB_HUB_CYCLE_PORT")
}
return nil
}
// WaitForVBoxUSBInterface polls CM_Get_Device_Interface_List for the
// VBoxUSB class GUID until an interface path matching instanceID
// appears, or timeout elapses. After RestartDevice + filter trigger,
// PnP needs a moment to load VBoxUSB.sys on the device; matching
// usbipd-win's 10-second window.
func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string, error) {
guid := windows.GUID{
Data1: MonitorAccessGUID.Data1,
Data2: MonitorAccessGUID.Data2,
Data3: MonitorAccessGUID.Data3,
Data4: MonitorAccessGUID.Data4,
}
deadline := time.Now().Add(timeout)
for {
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
if err == nil {
for _, p := range paths {
if p != "" {
return p, nil
}
}
}
if time.Now().After(deadline) {
return "", E.New("vboxusb: VBoxUSB interface for ", instanceID, " did not appear within ", timeout)
}
time.Sleep(100 * time.Millisecond)
}
}
// RestartDevice triggers the PnP unplug/replug cycle that lets a
// pending VBoxUSBMon CAPTURE filter activate. usbipd-win uses
// CM_Query_And_Remove_SubTree -> IOCTL_USB_HUB_CYCLE_PORT ->
// CM_Setup_DevNode.
//
// TODO(phase B follow-up): CM_Query_And_Remove_SubTree and
// CM_Setup_DevNode are not exposed by golang.org/x/sys/windows and
// need direct LazyDLL wrappers. Until that is in place, callers
// should arrange for the device to be physically replugged.
func RestartDevice(_ string) error {
return E.New("vboxusb: RestartDevice not yet implemented (use CycleHubPort for now)")
}
// WatchDeviceArrival registers a callback for device arrival/removal
// on GUID_DEVINTERFACE_USB_DEVICE. Returns a handle that must be
// closed when the watcher is no longer needed.
//
// TODO(phase B follow-up): CM_Register_Notification is not exposed by
// golang.org/x/sys/windows. Until wired up, the export host will need
// to poll via EnumerateUSBDevices on a reconcile timer.
func WatchDeviceArrival(_ func()) (DeviceWatcher, error) {
return nil, E.New("vboxusb: WatchDeviceArrival not yet implemented (poll EnumerateUSBDevices instead)")
}
// DeviceWatcher is the handle returned by WatchDeviceArrival. Close
// stops delivery and releases the underlying CM_Notify_HNOTIFICATION.
type DeviceWatcher interface {
Close() error
}
// firstString returns the first NUL-separated string in a REG_MULTI_SZ
// value (returned by SetupDiGetDeviceRegistryProperty for HardwareID).
// Returns the value unchanged for REG_SZ.
func firstString(value any) string {
switch v := value.(type) {
case string:
return v
case []string:
if len(v) > 0 {
return v[0]
}
}
return ""
}
func toUint32(value any) uint32 {
switch v := value.(type) {
case uint32:
return v
case uint64:
return uint32(v)
}
return 0
}
// parseHardwareID extracts VID/PID/REV from a USB hardware ID such as
// "USB\VID_046D&PID_C31C&REV_6400". Returns zeros on parse failure;
// the caller decides whether that disqualifies the device.
func parseHardwareID(hwid string) (vid, pid, rev uint16) {
upper := strings.ToUpper(hwid)
vid = extractHex16(upper, "VID_")
pid = extractHex16(upper, "PID_")
rev = extractHex16(upper, "REV_")
return
}
func extractHex16(s, prefix string) uint16 {
idx := strings.Index(s, prefix)
if idx < 0 {
return 0
}
tail := s[idx+len(prefix):]
end := len(tail)
for i, r := range tail {
if !isHex(r) {
end = i
break
}
}
if end == 0 {
return 0
}
v, err := strconv.ParseUint(tail[:end], 16, 16)
if err != nil {
return 0
}
return uint16(v)
}
func isHex(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'A' && r <= 'F') || (r >= 'a' && r <= 'f')
}
// pnpProcOnce + LazyProc handles for direct syscall to functions that
// golang.org/x/sys/windows does not yet wrap. Kept in one place so the
// RestartDevice / WatchDeviceArrival follow-up has the resolver
// boilerplate already laid out.
var (
pnpProcOnce sync.Once
modCfgMgr32 = windows.NewLazyDLL("cfgmgr32.dll")
_ = modCfgMgr32 // referenced by upcoming RestartDevice impl
_ unsafe.Pointer
)
+56
View File
@@ -0,0 +1,56 @@
//go:build windows
package vboxusb
import (
"sync"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/sys/windows"
)
// EnableLoadDriverPrivilege flips SeLoadDriverPrivilege ON in the
// current process token. Required to open \\.\VBoxUSBMon and to drive
// PnP install/uninstall flows; non-interactive services have it
// disabled by default. Idempotent.
func EnableLoadDriverPrivilege() error {
loadDriverPrivOnce.Do(func() {
loadDriverPrivErr = enableLoadDriverPrivilege()
})
return loadDriverPrivErr
}
var (
loadDriverPrivOnce sync.Once
loadDriverPrivErr error
)
func enableLoadDriverPrivilege() error {
var token windows.Token
err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token)
if err != nil {
return E.Cause(err, "vboxusb: open process token")
}
defer token.Close()
privName, _ := windows.UTF16PtrFromString("SeLoadDriverPrivilege")
var luid windows.LUID
err = windows.LookupPrivilegeValue(nil, privName, &luid)
if err != nil {
return E.Cause(err, "vboxusb: lookup SeLoadDriverPrivilege")
}
tp := windows.Tokenprivileges{
PrivilegeCount: 1,
Privileges: [1]windows.LUIDAndAttributes{{
Luid: luid,
Attributes: windows.SE_PRIVILEGE_ENABLED,
}},
}
err = windows.AdjustTokenPrivileges(token, false, &tp, 0, nil, nil)
if err != nil {
return E.Cause(err, "vboxusb: adjust token privileges")
}
return nil
}
+175
View File
@@ -0,0 +1,175 @@
// Package vboxusb provides a Go binding for Oracle's VBoxUSB +
// VBoxUSBMon kernel drivers on Windows (amd64 and arm64), packaged
// for embedding in sing-box.
//
// The driver pair is shipped verbatim from dorssel/usbipd-win, which
// in turn carries the binaries unchanged from upstream VirtualBox.
// VBoxUSB.sys provides per-device user-space URB submission via
// SUPUSB_IOCTL_* IOCTLs; VBoxUSBMon.sys is the system-wide monitor
// that arranges for VBoxUSB to bind matching devices on PnP arrival.
//
// Administrator is required for the first Open in a process so SCM
// can load VBoxUSBMon. The drivers are Microsoft-signed via Oracle's
// VirtualBox publishing chain; we never re-sign or modify them.
//
// Upstream:
// - https://github.com/dorssel/usbipd-win (driver packaging)
// - https://github.com/VirtualBox/VirtualBox (driver source)
//
// License: GPLv2-or-later (drivers) and GPLv3 (this binding); see
// assets/*.license files for the per-asset SPDX records.
package vboxusb
// AssetVersion identifies the bundled VBoxUSB driver release. Bumped
// in lock-step with the .sys files copied from
// /tmp/usbipd-win/Drivers/{x64,arm64}/. The on-disk extraction path
// (and SCM service version reuse) is keyed on this string, so a
// version bump triggers re-extraction.
const AssetVersion = "7.2.8.23730"
// DriverVersion enforces the minimum acceptable VBoxUSB/VBoxUSBMon
// driver major version reported via GET_VERSION. Mirrors usbipd-win's
// USBDRV_MAJOR_VERSION / USBMON_MAJOR_VERSION (both 5).
const (
DriverMajorVersion = 5
DriverMinorVersion = 0
)
// Driver and device names. The Windows side opens the monitor via
// CreateFile(MonitorDevicePath); per-device VBoxUSB handles are opened
// via SetupDi-resolved interface paths under the GUID below.
const (
MonitorServiceName = "VBoxUSBMon"
MonitorDevicePath = `\\.\VBoxUSBMon`
)
// MonitorAccessGUID is GUID_CLASS_VBOXUSB from VirtualBox usblib-win.h.
// Used with SetupDiEnumDeviceInterfaces to find the per-device file
// path after VBoxUSB binds.
var MonitorAccessGUID = GUID{
Data1: 0x00873fdf,
Data2: 0xCAFE,
Data3: 0x80EE,
Data4: [8]byte{0xaa, 0x5e, 0x00, 0xc0, 0x4f, 0xb1, 0x72, 0x0b},
}
// USBDeviceInterfaceGUID is GUID_DEVINTERFACE_USB_DEVICE
// ({a5dcbf10-6530-11d2-901f-00c04fb951ed}). Used to enumerate plugged
// USB devices regardless of which function driver currently owns them.
var USBDeviceInterfaceGUID = GUID{
Data1: 0xa5dcbf10,
Data2: 0x6530,
Data3: 0x11d2,
Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed},
}
// USBHubInterfaceGUID is GUID_DEVINTERFACE_USB_HUB
// ({f18a0e88-c30c-11d0-8815-00a0c906bed8}). The parent hub of a target
// device is opened with this GUID to issue IOCTL_USB_HUB_CYCLE_PORT.
var USBHubInterfaceGUID = GUID{
Data1: 0xf18a0e88,
Data2: 0xc30c,
Data3: 0x11d0,
Data4: [8]byte{0x88, 0x15, 0x00, 0xa0, 0xc9, 0x06, 0xbe, 0xd8},
}
// GUID matches the Windows GUID layout exactly. We carry our own copy
// so the (cross-platform) package-level vars above can be declared
// without depending on golang.org/x/sys/windows.
type GUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
// IOCTL codes from VirtualBox usblib-win.h, identical to those used by
// usbipd-win (Usbipd/Interop/VBoxUsb.cs:26-39 and VBoxUsbMon.cs:122-129).
// Encoding is the standard CTL_CODE shape:
//
// (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
//
// DeviceType = FILE_DEVICE_UNKNOWN (0x22), Access = FILE_WRITE_ACCESS (2),
// Method = METHOD_BUFFERED (0).
const (
// Per-device VBoxUSB.sys (\\?\<setupapi-resolved path>).
IOCTLSendURB uint32 = 0x0022_181C // function 0x607
IOCTLUSBSelectInterface uint32 = 0x0022_1824 // function 0x609
IOCTLUSBSetConfig uint32 = 0x0022_1828 // function 0x60a
IOCTLUSBClaimDevice uint32 = 0x0022_182C // function 0x60b
IOCTLUSBClearEndpoint uint32 = 0x0022_1838 // function 0x60e
IOCTLGetVersion uint32 = 0x0022_183C // function 0x60f
IOCTLUSBAbortEndpoint uint32 = 0x0022_1840 // function 0x610
// VBoxUSBMon (\\.\VBoxUSBMon). Note GET_VERSION shares the numeric
// code with VBoxUSB's USB_ABORT_ENDPOINT — different handles.
IOCTLMonitorGetVersion uint32 = 0x0022_1840
IOCTLMonitorAddFilter uint32 = 0x0022_1844
IOCTLMonitorRemoveFilter uint32 = 0x0022_1848
)
// IOCTLHubCyclePort = IOCTL_USB_HUB_CYCLE_PORT from usbioctl.h
// (FILE_DEVICE_USB=0x22, FILE_ANY_ACCESS=0, function=0x111,
// METHOD_BUFFERED=0).
const IOCTLHubCyclePort uint32 = 0x0022_0444
// USB/IP-style transfer type enum, matching VirtualBox USBSUP_TRANSFER_TYPE.
type TransferType uint32
const (
TransferTypeControl TransferType = iota
TransferTypeIso
TransferTypeBulk
TransferTypeInterrupt
TransferTypeMessage // control with setup packet inline
)
// Direction matches USBSUP_DIRECTION.
type Direction uint32
const (
DirectionSetup Direction = iota
DirectionIn
DirectionOut
)
// TransferFlags matches USBSUP_XFER_FLAG. ShortOK is required for IN
// transfers unless the USB/IP request flags include URB_SHORT_NOT_OK.
type TransferFlags uint32
const (
TransferFlagNone TransferFlags = 0
TransferFlagShortOK TransferFlags = 1 << 0
)
// URBError mirrors USBSUP_ERROR. The session layer maps these into
// USBIP-wire-format status (negated Linux errno).
type URBError uint32
const (
URBOK URBError = iota
URBStall
URBDeviceNotResponding
URBCRCError
URBNACError
URBUnderrun
URBOverrun
)
// MaxIsoPacketsPerURB is the hard VBoxUSB limit (USBSUP_URB.aIsoPkts
// is sized for 8 entries). Callers with more iso packets must split
// into multiple URBs sharing one pinned buffer; offsets must stay
// within ushort range.
const MaxIsoPacketsPerURB = 8
// Filter is a logical builder for VBoxUSBMon ADD_FILTER. The Go side
// owns the byte layout (in monitor_windows.go) so callers see a clean
// API even though the on-the-wire struct is fixed-size packed.
type Filter struct {
VendorID *uint16
ProductID *uint16
DeviceRev *uint16
Bus *uint16
Port *uint16
DeviceClass *uint16
}