Files
sing-box/common/vboxusb/device_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

311 lines
9.8 KiB
Go

//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
}