Files
世界 5da1096a91 usbip: use a dedicated overlapped event per VBoxUSB ioctl
Device and Monitor shared one manual-reset event across all in-flight
IOCTLs, while the session layer runs one goroutine per endpoint: the
first completion released every GetOverlappedResult waiter with the
first operation's byte count, returning before the driver finished
writing the other buffers. Each overlappedIoctl call now owns its
event, making concurrent URBs (and aborts) on one handle safe.
2026-06-10 09:22:41 +08:00

173 lines
5.2 KiB
Go

//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. Methods are safe for
// concurrent use: each IOCTL carries its own OVERLAPPED + event and
// the driver serializes filter mutations internally.
type Monitor struct {
handle 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")
}
return &Monitor{handle: handle}, nil
}
func (m *Monitor) Close() error {
m.closing.Do(func() {
if m.handle != 0 {
m.closeErr = windows.CloseHandle(m.handle)
m.handle = 0
}
})
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) {
return overlappedIoctl(m.handle, code, in, out)
}
// 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 (4 = 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 = 4 // UsbFilterType.CAPTURE (5 is END, the enum sentinel)
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
}