usbip: remove dead code, ceremonial guards, and WHAT comments
This commit is contained in:
@@ -177,13 +177,6 @@ type IsoPacket struct {
|
||||
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):
|
||||
@@ -206,9 +199,6 @@ const urbStructSize = 104
|
||||
// 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)
|
||||
@@ -277,14 +267,14 @@ func (e *URBStatusError) Error() string {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return overlappedIoctl(d.handle, code, in, out, d.event)
|
||||
}
|
||||
|
||||
func overlappedIoctl(handle windows.Handle, code uint32, in []byte, out []byte, event windows.Handle) (uint32, error) {
|
||||
var overlapped windows.Overlapped
|
||||
overlapped.HEvent = d.event
|
||||
_ = windows.ResetEvent(d.event)
|
||||
overlapped.HEvent = event
|
||||
_ = windows.ResetEvent(event)
|
||||
var inPtr *byte
|
||||
var inLen uint32
|
||||
if len(in) > 0 {
|
||||
@@ -298,11 +288,11 @@ func (d *Device) ioctl(code uint32, in []byte, out []byte) (uint32, error) {
|
||||
outLen = uint32(len(out))
|
||||
}
|
||||
var returned uint32
|
||||
err := windows.DeviceIoControl(d.handle, code, inPtr, inLen, outPtr, outLen, &returned, &overlapped)
|
||||
err := windows.DeviceIoControl(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)
|
||||
err = windows.GetOverlappedResult(handle, &overlapped, &returned, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -42,10 +42,6 @@ var (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -126,31 +126,7 @@ func (m *Monitor) RemoveFilter(id uint64) error {
|
||||
}
|
||||
|
||||
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
|
||||
return overlappedIoctl(m.handle, code, in, out, m.event)
|
||||
}
|
||||
|
||||
// encodeFilter builds a 312-byte USBFILTER packed struct matching the
|
||||
|
||||
+17
-111
@@ -3,19 +3,30 @@
|
||||
package vboxusb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var MonitorAccessGUID = windows.GUID{
|
||||
Data1: 0x00873fdf,
|
||||
Data2: 0xCAFE,
|
||||
Data3: 0x80EE,
|
||||
Data4: [8]byte{0xaa, 0x5e, 0x00, 0xc0, 0x4f, 0xb1, 0x72, 0x0b},
|
||||
}
|
||||
|
||||
var USBDeviceInterfaceGUID = windows.GUID{
|
||||
Data1: 0xa5dcbf10,
|
||||
Data2: 0x6530,
|
||||
Data3: 0x11d2,
|
||||
Data4: [8]byte{0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9, 0x51, 0xed},
|
||||
}
|
||||
|
||||
// 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>")
|
||||
@@ -36,13 +47,9 @@ type USBDeviceInfo struct {
|
||||
// one record per attached USB device. Hubs are not filtered out here;
|
||||
// the caller (export host) skips DeviceClass == 0x09.
|
||||
func EnumerateUSBDevices() ([]USBDeviceInfo, error) {
|
||||
guid := USBDeviceInterfaceGUID
|
||||
devInfo, err := windows.SetupDiGetClassDevsEx(
|
||||
&windows.GUID{
|
||||
Data1: USBDeviceInterfaceGUID.Data1,
|
||||
Data2: USBDeviceInterfaceGUID.Data2,
|
||||
Data3: USBDeviceInterfaceGUID.Data3,
|
||||
Data4: USBDeviceInterfaceGUID.Data4,
|
||||
},
|
||||
&guid,
|
||||
"",
|
||||
0,
|
||||
windows.DIGCF_PRESENT|windows.DIGCF_DEVICEINTERFACE,
|
||||
@@ -87,62 +94,8 @@ func EnumerateUSBDevices() ([]USBDeviceInfo, error) {
|
||||
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,
|
||||
¶ms[0], uint32(len(params)),
|
||||
¶ms[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,
|
||||
}
|
||||
guid := MonitorAccessGUID
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
paths, err := windows.CM_Get_Device_Interface_List(instanceID, &guid, windows.CM_GET_DEVICE_INTERFACE_LIST_PRESENT)
|
||||
@@ -160,39 +113,6 @@ func WaitForVBoxUSBInterface(instanceID string, timeout time.Duration) (string,
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
@@ -215,9 +135,6 @@ func toUint32(value any) uint32 {
|
||||
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_")
|
||||
@@ -252,14 +169,3 @@ func extractHex16(s, prefix string) uint16 {
|
||||
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
|
||||
)
|
||||
|
||||
@@ -43,46 +43,6 @@ const (
|
||||
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:
|
||||
@@ -108,11 +68,6 @@ const (
|
||||
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
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ type ClientService struct {
|
||||
assignedWorkers []*clientAssignedWorker
|
||||
allWorkers map[string]context.CancelFunc
|
||||
|
||||
remoteAccess sync.Mutex
|
||||
remoteDevicesV2 map[string]DeviceInfoV2
|
||||
remoteAccess sync.Mutex
|
||||
remoteDevices map[string]ControlDeviceInfo
|
||||
}
|
||||
|
||||
func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) {
|
||||
|
||||
@@ -9,9 +9,6 @@ import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
// clientAssignment runs in two modes that share active-busid tracking:
|
||||
// matched (len(targets) > 0) binds each target to at most one busid;
|
||||
// import-all (len(targets) == 0) marks every advertised busid as desired.
|
||||
type clientAssignment struct {
|
||||
access sync.Mutex
|
||||
|
||||
@@ -82,9 +79,6 @@ func (a *clientAssignment) ApplyMatched(entries []DeviceEntry, knownKeys map[str
|
||||
return nextAssigned, prev
|
||||
}
|
||||
|
||||
// ApplyAll keeps no-longer-desired-but-active busids registered so the
|
||||
// runBusIDLoop exits naturally after the active session ends, via
|
||||
// IsRetryDesired returning false.
|
||||
func (a *clientAssignment) ApplyAll(entries []DeviceEntry) (start []string, stop []string) {
|
||||
desired := make(map[string]struct{}, len(entries))
|
||||
for i := range entries {
|
||||
|
||||
@@ -42,7 +42,6 @@ type darwinVirtualController struct {
|
||||
runErr error
|
||||
eventStarted atomic.Bool
|
||||
|
||||
stateAccess sync.Mutex
|
||||
powered bool
|
||||
connected bool
|
||||
nextAddress uint8
|
||||
@@ -136,7 +135,6 @@ func (c *darwinVirtualController) enqueueEvent(event darwinControllerEvent) {
|
||||
}
|
||||
|
||||
func (c *darwinVirtualController) eventLoop() {
|
||||
c.eventStarted.Store(true)
|
||||
defer close(c.eventDone)
|
||||
defer c.teardownIOUSBHostState()
|
||||
for {
|
||||
@@ -200,27 +198,21 @@ func (c *darwinVirtualController) handleDeviceCreate(message darwinCIMessage) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.stateAccess.Lock()
|
||||
address := c.nextAddress
|
||||
c.nextAddress++
|
||||
c.devices[address] = device
|
||||
c.stateAccess.Unlock()
|
||||
return device.respondCreate(message, ciStatusSuccess, address)
|
||||
}
|
||||
|
||||
func (c *darwinVirtualController) handleDeviceCommand(message darwinCIMessage) error {
|
||||
address := message.deviceAddress()
|
||||
c.stateAccess.Lock()
|
||||
device := c.devices[address]
|
||||
c.stateAccess.Unlock()
|
||||
if device == nil {
|
||||
return nil
|
||||
}
|
||||
err := device.respond(message, ciStatusSuccess)
|
||||
if message.messageType() == ciMsgDeviceDestroy {
|
||||
c.stateAccess.Lock()
|
||||
delete(c.devices, address)
|
||||
c.stateAccess.Unlock()
|
||||
device.Close()
|
||||
}
|
||||
return err
|
||||
@@ -233,21 +225,17 @@ func (c *darwinVirtualController) handleEndpointCreate(message darwinCIMessage)
|
||||
}
|
||||
key := darwinEndpointKey{device: message.deviceAddress(), endpoint: message.endpointAddress()}
|
||||
endpoint := newDarwinEndpoint(c.ctx, c.logger, sm, c.peer, c.CurrentFrame, c.info.DevID(), key)
|
||||
c.stateAccess.Lock()
|
||||
c.endpoints[key] = endpoint
|
||||
c.stateAccess.Unlock()
|
||||
return sm.respond(message, ciStatusSuccess)
|
||||
}
|
||||
|
||||
func (c *darwinVirtualController) handleEndpointCommand(message darwinCIMessage) error {
|
||||
key := darwinEndpointKey{device: message.deviceAddress(), endpoint: message.endpointAddress()}
|
||||
destroy := message.messageType() == ciMsgEndpointDestroy
|
||||
c.stateAccess.Lock()
|
||||
endpoint := c.endpoints[key]
|
||||
if destroy {
|
||||
delete(c.endpoints, key)
|
||||
}
|
||||
c.stateAccess.Unlock()
|
||||
if endpoint == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -263,9 +251,7 @@ func (c *darwinVirtualController) handleDoorbell(doorbell uint32) {
|
||||
device: uint8(doorbell & 0xff),
|
||||
endpoint: uint8((doorbell >> 8) & 0xff),
|
||||
}
|
||||
c.stateAccess.Lock()
|
||||
endpoint := c.endpoints[key]
|
||||
c.stateAccess.Unlock()
|
||||
if endpoint == nil {
|
||||
return
|
||||
}
|
||||
@@ -273,7 +259,6 @@ func (c *darwinVirtualController) handleDoorbell(doorbell uint32) {
|
||||
}
|
||||
|
||||
func (c *darwinVirtualController) teardownIOUSBHostState() {
|
||||
c.stateAccess.Lock()
|
||||
endpoints := make([]*darwinEndpoint, 0, len(c.endpoints))
|
||||
for _, endpoint := range c.endpoints {
|
||||
endpoints = append(endpoints, endpoint)
|
||||
@@ -286,7 +271,6 @@ func (c *darwinVirtualController) teardownIOUSBHostState() {
|
||||
c.devices = make(map[uint8]*darwinUSBHostDeviceSM)
|
||||
controller := c.controller
|
||||
c.controller = nil
|
||||
c.stateAccess.Unlock()
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Close()
|
||||
@@ -294,7 +278,5 @@ func (c *darwinVirtualController) teardownIOUSBHostState() {
|
||||
for _, device := range devices {
|
||||
device.Close()
|
||||
}
|
||||
if controller != nil {
|
||||
controller.Close()
|
||||
}
|
||||
controller.Close()
|
||||
}
|
||||
|
||||
@@ -115,10 +115,6 @@ func (c *ClientService) run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic export discovery and hotplug updates are provided by the sing-box
|
||||
// USB/IP control extensions. Standard USB/IP implementations expose only a
|
||||
// static DEVLIST snapshot, so we seed assignments once and do not keep polling
|
||||
// for newly exported devices.
|
||||
func (c *ClientService) runStandardStaticMode() error {
|
||||
err := c.syncRemoteStateContext(c.ctx)
|
||||
if err != nil {
|
||||
@@ -199,10 +195,10 @@ func (c *ClientService) runControlSession() error {
|
||||
if err != nil {
|
||||
return E.Cause(errImmediateReconnect, "read device snapshot: ", err)
|
||||
}
|
||||
devices := deviceInfoV2Map(snapshot.Devices)
|
||||
values := sortedDeviceInfoV2Values(devices)
|
||||
devices := controlDeviceInfoMap(snapshot.Devices)
|
||||
values := sortedControlDeviceInfoValues(devices)
|
||||
c.remoteAccess.Lock()
|
||||
c.remoteDevicesV2 = devices
|
||||
c.remoteDevices = devices
|
||||
c.remoteAccess.Unlock()
|
||||
c.applyRemoteDeviceState(values)
|
||||
case controlFramePong:
|
||||
@@ -249,8 +245,8 @@ func (c *ClientService) syncRemoteStateContext(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClientService) applyRemoteDeviceState(devices []DeviceInfoV2) {
|
||||
availableEntries := deviceInfoV2ToEntries(devices, true)
|
||||
func (c *ClientService) applyRemoteDeviceState(devices []ControlDeviceInfo) {
|
||||
availableEntries := controlDeviceInfoToEntries(devices, true)
|
||||
if !c.assignment.Matched() {
|
||||
c.applyRemoteExports(availableEntries)
|
||||
return
|
||||
|
||||
@@ -16,9 +16,9 @@ const (
|
||||
|
||||
controlFrameHello uint8 = 1
|
||||
controlFrameAck uint8 = 2
|
||||
controlFramePing uint8 = 4
|
||||
controlFramePong uint8 = 5
|
||||
controlFrameDeviceSnapshot uint8 = 6
|
||||
controlFramePing uint8 = 3
|
||||
controlFramePong uint8 = 4
|
||||
controlFrameDeviceSnapshot uint8 = 5
|
||||
|
||||
controlPrefaceSize = 8
|
||||
controlFrameSize = 4
|
||||
@@ -46,40 +46,39 @@ type controlMessage struct {
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type DeviceInterfaceV2 struct {
|
||||
type ControlDeviceInterface struct {
|
||||
Class uint8 `json:"class"`
|
||||
SubClass uint8 `json:"subclass"`
|
||||
Protocol uint8 `json:"protocol"`
|
||||
}
|
||||
|
||||
type DeviceInfoV2 struct {
|
||||
BusID string `json:"busid"`
|
||||
StableID string `json:"stable_id,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
VendorID uint16 `json:"vendor_id"`
|
||||
ProductID uint16 `json:"product_id"`
|
||||
BCDDevice uint16 `json:"bcd_device,omitempty"`
|
||||
Speed uint32 `json:"speed"`
|
||||
DeviceClass uint8 `json:"device_class"`
|
||||
DeviceSubClass uint8 `json:"device_subclass"`
|
||||
DeviceProtocol uint8 `json:"device_protocol"`
|
||||
ConfigurationValue uint8 `json:"configuration_value"`
|
||||
NumConfigurations uint8 `json:"num_configurations"`
|
||||
NumInterfaces uint8 `json:"num_interfaces"`
|
||||
Interfaces []DeviceInterfaceV2 `json:"interfaces,omitempty"`
|
||||
State string `json:"state"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
type ControlDeviceInfo struct {
|
||||
BusID string `json:"busid"`
|
||||
StableID string `json:"stable_id,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
VendorID uint16 `json:"vendor_id"`
|
||||
ProductID uint16 `json:"product_id"`
|
||||
BCDDevice uint16 `json:"bcd_device,omitempty"`
|
||||
Speed uint32 `json:"speed"`
|
||||
DeviceClass uint8 `json:"device_class"`
|
||||
DeviceSubClass uint8 `json:"device_subclass"`
|
||||
DeviceProtocol uint8 `json:"device_protocol"`
|
||||
ConfigurationValue uint8 `json:"configuration_value"`
|
||||
NumConfigurations uint8 `json:"num_configurations"`
|
||||
NumInterfaces uint8 `json:"num_interfaces"`
|
||||
Interfaces []ControlDeviceInterface `json:"interfaces,omitempty"`
|
||||
State string `json:"state"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
StatusReason string `json:"status_reason,omitempty"`
|
||||
}
|
||||
|
||||
type controlDeviceSnapshot struct {
|
||||
Devices []DeviceInfoV2 `json:"devices"`
|
||||
Devices []ControlDeviceInfo `json:"devices"`
|
||||
}
|
||||
|
||||
// controlReader reuses its payload scratch across successive reads on a
|
||||
// single connection. The returned payload is only valid until the next call.
|
||||
// The returned payload is only valid until the next call.
|
||||
type controlReader struct {
|
||||
scratch []byte
|
||||
}
|
||||
@@ -151,10 +150,10 @@ func unmarshalControlPayload(payload []byte, value any) error {
|
||||
return json.Unmarshal(payload, value)
|
||||
}
|
||||
|
||||
func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, state string, statusCode int, statusReason string) DeviceInfoV2 {
|
||||
interfaces := make([]DeviceInterfaceV2, len(entry.Interfaces))
|
||||
func controlDeviceInfoFromEntry(entry DeviceEntry, backend string, stableID string, state string, statusCode int, statusReason string) ControlDeviceInfo {
|
||||
interfaces := make([]ControlDeviceInterface, len(entry.Interfaces))
|
||||
for i := range entry.Interfaces {
|
||||
interfaces[i] = DeviceInterfaceV2{
|
||||
interfaces[i] = ControlDeviceInterface{
|
||||
Class: entry.Interfaces[i].BInterfaceClass,
|
||||
SubClass: entry.Interfaces[i].BInterfaceSubClass,
|
||||
Protocol: entry.Interfaces[i].BInterfaceProtocol,
|
||||
@@ -167,7 +166,7 @@ func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, s
|
||||
if serial == "" {
|
||||
serial = entry.Info.SerialString()
|
||||
}
|
||||
return DeviceInfoV2{
|
||||
return ControlDeviceInfo{
|
||||
BusID: entry.Info.BusIDString(),
|
||||
StableID: stableID,
|
||||
Backend: backend,
|
||||
@@ -190,8 +189,8 @@ func deviceInfoV2FromEntry(entry DeviceEntry, backend string, stableID string, s
|
||||
}
|
||||
}
|
||||
|
||||
func deviceInfoV2Map(devices []DeviceInfoV2) map[string]DeviceInfoV2 {
|
||||
out := make(map[string]DeviceInfoV2, len(devices))
|
||||
func controlDeviceInfoMap(devices []ControlDeviceInfo) map[string]ControlDeviceInfo {
|
||||
out := make(map[string]ControlDeviceInfo, len(devices))
|
||||
for _, device := range devices {
|
||||
if device.BusID == "" {
|
||||
continue
|
||||
@@ -201,20 +200,20 @@ func deviceInfoV2Map(devices []DeviceInfoV2) map[string]DeviceInfoV2 {
|
||||
return out
|
||||
}
|
||||
|
||||
func sortedDeviceInfoV2Values(devices map[string]DeviceInfoV2) []DeviceInfoV2 {
|
||||
func sortedControlDeviceInfoValues(devices map[string]ControlDeviceInfo) []ControlDeviceInfo {
|
||||
busids := make([]string, 0, len(devices))
|
||||
for busid := range devices {
|
||||
busids = append(busids, busid)
|
||||
}
|
||||
slices.Sort(busids)
|
||||
out := make([]DeviceInfoV2, 0, len(busids))
|
||||
out := make([]ControlDeviceInfo, 0, len(busids))
|
||||
for _, busid := range busids {
|
||||
out = append(out, devices[busid])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func deviceInfoV2ToEntries(devices []DeviceInfoV2, availableOnly bool) []DeviceEntry {
|
||||
func controlDeviceInfoToEntries(devices []ControlDeviceInfo, availableOnly bool) []DeviceEntry {
|
||||
entries := make([]DeviceEntry, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if availableOnly && device.State != "" && device.State != deviceStateAvailable {
|
||||
@@ -246,7 +245,7 @@ func deviceInfoV2ToEntries(devices []DeviceInfoV2, availableOnly bool) []DeviceE
|
||||
return entries
|
||||
}
|
||||
|
||||
func deviceInfoV2Equal(a, b DeviceInfoV2) bool {
|
||||
func controlDeviceInfoEqual(a, b ControlDeviceInfo) bool {
|
||||
if a.BusID != b.BusID ||
|
||||
a.StableID != b.StableID ||
|
||||
a.Backend != b.Backend ||
|
||||
@@ -267,13 +266,5 @@ func deviceInfoV2Equal(a, b DeviceInfoV2) bool {
|
||||
a.StatusReason != b.StatusReason {
|
||||
return false
|
||||
}
|
||||
if len(a.Interfaces) != len(b.Interfaces) {
|
||||
return false
|
||||
}
|
||||
for i := range a.Interfaces {
|
||||
if a.Interfaces[i] != b.Interfaces[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return slices.Equal(a.Interfaces, b.Interfaces)
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ func (s *darwinFakeUSBIPServer) handleControlConn(conn net.Conn) {
|
||||
Type: controlFrameDeviceSnapshot,
|
||||
Version: controlProtocolVersion,
|
||||
}, controlDeviceSnapshot{
|
||||
Devices: []DeviceInfoV2{deviceInfoV2FromEntry(s.entry, "darwin-fake", "darwin-fake:"+s.entry.Info.BusIDString(), deviceStateAvailable, 0, "available")},
|
||||
Devices: []ControlDeviceInfo{controlDeviceInfoFromEntry(s.entry, "darwin-fake", "darwin-fake:"+s.entry.Info.BusIDString(), deviceStateAvailable, 0, "available")},
|
||||
})
|
||||
for {
|
||||
message, err := cr.read(conn)
|
||||
|
||||
@@ -202,8 +202,6 @@ func (e *darwinEndpoint) finalizePending(pending *pendingTransfer) {
|
||||
}
|
||||
}
|
||||
|
||||
// validateResponse reconciles a RET_SUBMIT against the original request shape.
|
||||
// All wire-shape rules live here so accept can assume well-formed input.
|
||||
func (p *pendingTransfer) validateResponse(response SubmitResponse) (int32, error) {
|
||||
if response.ActualLength < 0 {
|
||||
return -int32(unix.EPROTO), E.New("RET_SUBMIT actual_length is negative: ", response.ActualLength)
|
||||
@@ -226,10 +224,7 @@ func (p *pendingTransfer) validateResponse(response SubmitResponse) (int32, erro
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// accept validates a RET_SUBMIT against the original request and, for IN
|
||||
// transfers, scatters the payload into the Apple-owned buffer. A non-nil err
|
||||
// signals a protocol violation; the caller is expected to cancel the endpoint
|
||||
// so no further wire-corrupt completions are delivered to IOUSBHost.
|
||||
// A non-nil err signals a protocol violation; the caller must cancel the endpoint.
|
||||
func (p *pendingTransfer) accept(response SubmitResponse) (int32, int, error) {
|
||||
errStatus, err := p.validateResponse(response)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
package usbip
|
||||
|
||||
// darwinIOUSBHostEngine drives one captured IOUSBHost device for the
|
||||
// duration of one USBIP attachment. The device handle itself is owned
|
||||
// by darwinExportHost (kept across attachments for re-capture), so
|
||||
// Close here is intentionally a no-op.
|
||||
// Close is a no-op: darwinExportHost owns the device handle across attachments.
|
||||
type darwinIOUSBHostEngine struct {
|
||||
device *darwinUSBHostDevice
|
||||
}
|
||||
|
||||
@@ -3,15 +3,10 @@
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/sagernet/sing-box/common/vboxusb"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
// vboxusbEngine adapts a vboxusb.Device into the URBEngine interface.
|
||||
// It owns the device handle for the lifetime of one userspaceURBSession
|
||||
// (closed via Close).
|
||||
type vboxusbEngine struct {
|
||||
device *vboxusb.Device
|
||||
}
|
||||
@@ -52,9 +47,6 @@ func (e *vboxusbEngine) Close() error {
|
||||
return e.device.Close()
|
||||
}
|
||||
|
||||
// trapStandardControl returns (response, true) when the EP0 transfer
|
||||
// is one VBoxUSB requires us to translate. (zero, false) means the
|
||||
// caller should proceed with a normal control SEND_URB.
|
||||
func (e *vboxusbEngine) trapStandardControl(command SubmitCommand) (URBResponse, bool) {
|
||||
bmRequestType := command.Setup[0]
|
||||
bRequest := command.Setup[1]
|
||||
@@ -147,10 +139,6 @@ func (e *vboxusbEngine) bulkSubmit(req URBRequest, transferType vboxusb.Transfer
|
||||
return resp
|
||||
}
|
||||
|
||||
// isoSubmit currently rejects iso transfers exceeding VBoxUSB's 8-
|
||||
// packet-per-URB limit. usbipd-win splits these into multiple
|
||||
// parallel SEND_URB calls sharing one pinned buffer; that splitter is
|
||||
// a Phase C follow-up. Single-shot iso under 8 packets does work.
|
||||
func (e *vboxusbEngine) isoSubmit(req URBRequest) URBResponse {
|
||||
command := req.Command
|
||||
if len(command.IsoPackets) > vboxusb.MaxIsoPacketsPerURB {
|
||||
@@ -215,22 +203,16 @@ func flagsFromCommand(transferFlags int32, usbipDir uint32) vboxusb.TransferFlag
|
||||
return vboxusb.TransferFlagShortOK
|
||||
}
|
||||
|
||||
// classifyURBError returns (status, ok). ok=true means the URB completed
|
||||
// (with possibly a non-zero device-level status); ok=false means a
|
||||
// transport failure that the caller surfaces as URBResponse.Error.
|
||||
func classifyURBError(err error) (int32, bool) {
|
||||
if err == nil {
|
||||
return 0, true
|
||||
}
|
||||
var statusErr *vboxusb.URBStatusError
|
||||
if errors.As(err, &statusErr) {
|
||||
if statusErr, isStatus := E.Cast[*vboxusb.URBStatusError](err); isStatus {
|
||||
return vboxusbStatusToUSBIP(statusErr.Code), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// vboxusbStatusToUSBIP maps USBSUP_ERROR onto the USBIP (Linux errno)
|
||||
// wire status convention.
|
||||
func vboxusbStatusToUSBIP(code vboxusb.URBError) int32 {
|
||||
switch code {
|
||||
case vboxusb.URBOK:
|
||||
|
||||
@@ -24,7 +24,7 @@ type exportLedger struct {
|
||||
broadcastAccess sync.Mutex
|
||||
nextSubID uint64
|
||||
subs map[uint64]*exportSubscriber
|
||||
state map[string]DeviceInfoV2
|
||||
state map[string]ControlDeviceInfo
|
||||
|
||||
inventoryAccess sync.Mutex
|
||||
exports map[string]Export
|
||||
@@ -47,7 +47,7 @@ func newExportLedger(logger log.ContextLogger, now func() time.Time) *exportLedg
|
||||
logger: logger,
|
||||
now: now,
|
||||
subs: make(map[uint64]*exportSubscriber),
|
||||
state: make(map[string]DeviceInfoV2),
|
||||
state: make(map[string]ControlDeviceInfo),
|
||||
exports: make(map[string]Export),
|
||||
busy: make(map[string]bool),
|
||||
}
|
||||
@@ -70,8 +70,6 @@ func (l *exportLedger) withInventoryRead(body func()) {
|
||||
body()
|
||||
}
|
||||
|
||||
// withInventoryWriteQuiet is for mutations whose broadcast is the
|
||||
// caller's responsibility (paired with BroadcastIfChanged or shutdown).
|
||||
func (l *exportLedger) withInventoryWriteQuiet(body func()) {
|
||||
l.inventoryAccess.Lock()
|
||||
defer l.inventoryAccess.Unlock()
|
||||
@@ -115,22 +113,22 @@ func (l *exportLedger) ApplyHostSnapshot(snapshot map[string]Export, released []
|
||||
}
|
||||
|
||||
func (l *exportLedger) SeedBroadcastState() {
|
||||
nextState := deviceInfoV2Map(l.snapshotDeviceState())
|
||||
nextState := controlDeviceInfoMap(l.snapshotDeviceState())
|
||||
l.broadcastAccess.Lock()
|
||||
l.state = nextState
|
||||
l.broadcastAccess.Unlock()
|
||||
}
|
||||
|
||||
func (l *exportLedger) BroadcastIfChanged() bool {
|
||||
nextState := deviceInfoV2Map(l.snapshotDeviceState())
|
||||
nextState := controlDeviceInfoMap(l.snapshotDeviceState())
|
||||
|
||||
l.broadcastAccess.Lock()
|
||||
if maps.EqualFunc(l.state, nextState, deviceInfoV2Equal) {
|
||||
if maps.EqualFunc(l.state, nextState, controlDeviceInfoEqual) {
|
||||
l.broadcastAccess.Unlock()
|
||||
return false
|
||||
}
|
||||
l.state = nextState
|
||||
devices := sortedDeviceInfoV2Values(nextState)
|
||||
devices := sortedControlDeviceInfoValues(nextState)
|
||||
targets := make([]*exportSubscriber, 0, len(l.subs))
|
||||
for _, sub := range l.subs {
|
||||
targets = append(targets, sub)
|
||||
@@ -145,8 +143,7 @@ func (l *exportLedger) BroadcastIfChanged() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// TryReserveForImport atomically reserves an exported busid. The single
|
||||
// critical section under inventoryAccess closes the TOCTOU window between
|
||||
// The single critical section closes the TOCTOU window between
|
||||
// availability check and busy mark. Caller must pair success with
|
||||
// ReleaseImport and broadcast once the session is wired up.
|
||||
func (l *exportLedger) TryReserveForImport(busid string) (Export, bool, string) {
|
||||
@@ -201,21 +198,18 @@ func (l *exportLedger) Subscribe(conn net.Conn) *exportSubscriber {
|
||||
l.enqueuePayload(sub, controlFrame{
|
||||
Type: controlFrameDeviceSnapshot,
|
||||
Version: controlProtocolVersion,
|
||||
}, controlDeviceSnapshot{Devices: sortedDeviceInfoV2Values(l.state)})
|
||||
}, controlDeviceSnapshot{Devices: sortedControlDeviceInfoValues(l.state)})
|
||||
l.subs[sub.id] = sub
|
||||
return sub
|
||||
}
|
||||
|
||||
// Unsubscribe leaves the subscriber's send channel for the GC to
|
||||
// reclaim; the transport read loop has already exited.
|
||||
func (l *exportLedger) Unsubscribe(sub *exportSubscriber) {
|
||||
l.broadcastAccess.Lock()
|
||||
delete(l.subs, sub.id)
|
||||
l.broadcastAccess.Unlock()
|
||||
}
|
||||
|
||||
// CloseAllSubscribers returns the underlying connections so the caller
|
||||
// can close them outside any lock.
|
||||
// Returns connections to close outside the lock.
|
||||
func (l *exportLedger) CloseAllSubscribers() []net.Conn {
|
||||
l.broadcastAccess.Lock()
|
||||
conns := make([]net.Conn, 0, len(l.subs))
|
||||
@@ -234,7 +228,7 @@ func (l *exportLedger) ResetForClose() {
|
||||
})
|
||||
}
|
||||
|
||||
func (l *exportLedger) snapshotDeviceState() []DeviceInfoV2 {
|
||||
func (l *exportLedger) snapshotDeviceState() []ControlDeviceInfo {
|
||||
type entry struct {
|
||||
export Export
|
||||
busy bool
|
||||
@@ -252,13 +246,13 @@ func (l *exportLedger) snapshotDeviceState() []DeviceInfoV2 {
|
||||
slices.SortFunc(entries, func(a, b entry) int {
|
||||
return strings.Compare(a.export.BusID(), b.export.BusID())
|
||||
})
|
||||
out := make([]DeviceInfoV2, 0, len(entries))
|
||||
out := make([]ControlDeviceInfo, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
snapshot := e.export.Snapshot(e.busy)
|
||||
if snapshot.State == deviceStateUnavailable && snapshot.Entry.Info.BusIDString() == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, deviceInfoV2FromEntry(snapshot.Entry, snapshot.Backend, snapshot.StableID, snapshot.State, snapshot.RawStatus, snapshot.StatusReason))
|
||||
out = append(out, controlDeviceInfoFromEntry(snapshot.Entry, snapshot.Backend, snapshot.StableID, snapshot.State, snapshot.RawStatus, snapshot.StatusReason))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -165,10 +165,6 @@ func (h *kernelHandoffSession) Start() error {
|
||||
}
|
||||
|
||||
func (h *kernelHandoffSession) runDirect(ctx context.Context, logger log.ContextLogger, side string, busid string, file *os.File) {
|
||||
if file == nil {
|
||||
h.markDone(nil)
|
||||
return
|
||||
}
|
||||
closeFile := sync.OnceFunc(func() {
|
||||
_ = file.Close()
|
||||
})
|
||||
|
||||
@@ -254,10 +254,6 @@ func (h *darwinExportHost) snapshotSelf() map[string]Export {
|
||||
return snapshotDarwinExports(h.exports)
|
||||
}
|
||||
|
||||
// snapshotDarwinExports returns every tracked export, including stale
|
||||
// ones, matching the ExportSnapshot contract: stale exports surface
|
||||
// to the ledger so they broadcast as State: unavailable updates
|
||||
// instead of disappearing.
|
||||
func snapshotDarwinExports(exports map[string]*darwinExport) map[string]Export {
|
||||
out := make(map[string]Export, len(exports))
|
||||
for busid, exp := range exports {
|
||||
@@ -329,9 +325,6 @@ func (e *darwinExport) DeviceInfo() (DeviceInfoTruncated, error) {
|
||||
}
|
||||
|
||||
func (e *darwinExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
|
||||
if e.device == nil {
|
||||
return nil, E.New("darwin export ", e.busid, " has no device handle")
|
||||
}
|
||||
return newUserspaceURBSession(ctx, e.logger, conn, newDarwinIOUSBHostEngine(e.device)), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -100,16 +100,10 @@ func (i linuxExportIdentity) Equal(other linuxExportIdentity) bool {
|
||||
i.ConfigValue != other.ConfigValue ||
|
||||
i.NumConfigs != other.NumConfigs ||
|
||||
i.NumInterfaces != other.NumInterfaces ||
|
||||
i.Serial != other.Serial ||
|
||||
len(i.Interfaces) != len(other.Interfaces) {
|
||||
i.Serial != other.Serial {
|
||||
return false
|
||||
}
|
||||
for index := range i.Interfaces {
|
||||
if i.Interfaces[index] != other.Interfaces[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return slices.Equal(i.Interfaces, other.Interfaces)
|
||||
}
|
||||
|
||||
type linuxExportHost struct {
|
||||
@@ -311,11 +305,7 @@ func (h *linuxExportHost) Reconcile(isReserved func(busid string) bool) (map[str
|
||||
var reconcileErrors []error
|
||||
|
||||
for _, busid := range plan.toStale {
|
||||
exp, found := committed[busid]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
cloned := cloneLinuxExport(exp)
|
||||
cloned := cloneLinuxExport(committed[busid])
|
||||
cloned.stale = true
|
||||
committed[busid] = cloned
|
||||
}
|
||||
@@ -413,11 +403,6 @@ func (h *linuxExportHost) snapshotSelf() map[string]Export {
|
||||
return snapshotLinuxExports(h.exports)
|
||||
}
|
||||
|
||||
// snapshotLinuxExports returns every tracked export, including stale
|
||||
// ones. The ledger treats stale entries as broadcastable State:
|
||||
// unavailable updates via Export.Snapshot, which is what the
|
||||
// ExportSnapshot contract requires; filtering here would surface a
|
||||
// removed device instead of an updated one.
|
||||
func snapshotLinuxExports(exports map[string]*linuxExport) map[string]Export {
|
||||
out := make(map[string]Export, len(exports))
|
||||
for busid, exp := range exports {
|
||||
@@ -427,9 +412,6 @@ func snapshotLinuxExports(exports map[string]*linuxExport) map[string]Export {
|
||||
}
|
||||
|
||||
func cloneLinuxExport(exp *linuxExport) *linuxExport {
|
||||
if exp == nil {
|
||||
return nil
|
||||
}
|
||||
clone := *exp
|
||||
clone.descriptor.Interfaces = slices.Clone(exp.descriptor.Interfaces)
|
||||
clone.identity.Interfaces = slices.Clone(exp.identity.Interfaces)
|
||||
|
||||
@@ -19,18 +19,10 @@ func newPlatformExportHost(ctx context.Context, logger log.ContextLogger, matche
|
||||
return newWindowsExportHost(ctx, logger, matches), nil
|
||||
}
|
||||
|
||||
// newPlatformImportHost: usbip-client is not yet implemented on
|
||||
// Windows. A virtual host controller driver is required to drive
|
||||
// imported devices into the local USB stack — that is a separate
|
||||
// effort tracked outside this PR.
|
||||
func newPlatformImportHost(_ log.ContextLogger) (ImportHost, error) {
|
||||
return nil, E.New("usbip-client service is not yet implemented on Windows")
|
||||
}
|
||||
|
||||
// windowsExportHost manages one VBoxUSBMon handle and one filter id
|
||||
// per actively-matched device. Reconcile is called by ServerService
|
||||
// on a timer (Events delivers ticks because CM_Register_Notification
|
||||
// is not yet wired up — see vboxusb/pnp_windows.go).
|
||||
type windowsExportHost struct {
|
||||
logger log.ContextLogger
|
||||
matches []option.USBIPDeviceMatch
|
||||
@@ -79,9 +71,7 @@ func (h *windowsExportHost) Start() error {
|
||||
return E.New("windows usbip: VBoxUSBMon major version ", major, " (need ", vboxusb.DriverMajorVersion, ")")
|
||||
}
|
||||
h.logger.Info("VBoxUSBMon ", major, ".", minor, " ready")
|
||||
h.access.Lock()
|
||||
h.monitor = monitor
|
||||
h.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,10 +102,6 @@ func (h *windowsExportHost) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Events returns a channel that the server polls for topology changes.
|
||||
// On Windows, CM_Register_Notification wiring is a Phase B follow-up;
|
||||
// for now we tick on a fixed interval so Reconcile runs periodically.
|
||||
// Once the notification hook lands, this becomes edge-triggered.
|
||||
func (h *windowsExportHost) Events() (<-chan struct{}, error) {
|
||||
ch := make(chan struct{}, 1)
|
||||
go func() {
|
||||
@@ -209,10 +195,6 @@ func (h *windowsExportHost) FinishImport(busid string) (bool, error) {
|
||||
_ = exp.device.Close()
|
||||
exp.device = nil
|
||||
}
|
||||
// TODO(phase B follow-up): once vboxusb.RestartDevice is implemented,
|
||||
// call it here (or CycleHubPort) so Windows re-binds the device's
|
||||
// natural function driver. Until then, the device stays in VBoxUSB's
|
||||
// captured state until physical unplug.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -226,10 +208,6 @@ func (h *windowsExportHost) snapshotSelf() map[string]Export {
|
||||
return out
|
||||
}
|
||||
|
||||
// installFilterLocked is called from Reconcile under the assumption
|
||||
// the caller already holds (or does not need) the lock for this
|
||||
// h.filters mutation — but we re-lock briefly to keep the mutation
|
||||
// safe.
|
||||
func (h *windowsExportHost) installFilterLocked(monitor *vboxusb.Monitor, busid string, info vboxusb.USBDeviceInfo) {
|
||||
if monitor == nil {
|
||||
return
|
||||
@@ -266,8 +244,6 @@ func (h *windowsExportHost) removeFilterLocked(monitor *vboxusb.Monitor, busid s
|
||||
}
|
||||
}
|
||||
|
||||
// windowsExport carries the per-device state needed to satisfy the
|
||||
// Export interface and to drive the claim sequence in NewServerDataSession.
|
||||
type windowsExport struct {
|
||||
info vboxusb.USBDeviceInfo
|
||||
entry DeviceEntry
|
||||
@@ -313,16 +289,6 @@ func (e *windowsExport) DeviceInfo() (DeviceInfoTruncated, error) {
|
||||
return e.entry.Info, nil
|
||||
}
|
||||
|
||||
// NewServerDataSession executes the VBoxUSB claim sequence:
|
||||
//
|
||||
// 1. Wait for the VBoxUSB-bound device interface to appear under
|
||||
// GUID_CLASS_VBOXUSB. This only resolves if VBoxUSBMon's filter has
|
||||
// already triggered (next PnP arrival after AddFilter); for devices
|
||||
// already plugged in when sing-box started, the user must physically
|
||||
// unplug/replug, or RestartDevice must complete (Phase B follow-up).
|
||||
// 2. Open the device handle and verify driver version.
|
||||
// 3. USB_CLAIM_DEVICE to take exclusive ownership.
|
||||
// 4. Wrap as vboxusbEngine and hand to userspaceURBSession.
|
||||
func (e *windowsExport) NewServerDataSession(ctx context.Context, conn net.Conn) (DataSession, error) {
|
||||
path, err := vboxusb.WaitForVBoxUSBInterface(e.info.InstanceID, 10*time.Second)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,11 +6,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
// EncodeIsoSubmit fills the isochronous SUBMIT fields on base. When asap is
|
||||
// true, the wire-level ASAP flag is set and StartFrame is zeroed. Otherwise
|
||||
// RebaseFrame recovers the absolute frame number from the controller's
|
||||
// monotonic counter and ciFrame's 8 bits, then StartFrame carries the low 32
|
||||
// bits across the wire.
|
||||
func EncodeIsoSubmit(currentFrame uint64, base SubmitCommand, ciFrame uint8, asap bool) SubmitCommand {
|
||||
if asap {
|
||||
base.TransferFlags |= usbipTransferFlagIsoASAP
|
||||
|
||||
@@ -34,9 +34,6 @@ func matches(m option.USBIPDeviceMatch, d DeviceKey) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectMatches returns the indexes of keys that match at least one
|
||||
// non-zero pattern. Indexes are deduplicated and returned in ascending
|
||||
// order so callers iterate devices in a stable sequence.
|
||||
func SelectMatches(patterns []option.USBIPDeviceMatch, keys []DeviceKey) []int {
|
||||
if len(patterns) == 0 || len(keys) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -18,11 +18,6 @@ import (
|
||||
|
||||
var _ DataSession = (*userspaceURBSession)(nil)
|
||||
|
||||
// userspaceURBSession drives one USBIP attachment when the per-URB I/O
|
||||
// happens in user space (Darwin IOUSBHost, Windows VBoxUSB). It reads
|
||||
// CMD_SUBMIT/CMD_UNLINK from conn, serializes per endpoint, dispatches
|
||||
// to a URBEngine, frames RET_SUBMIT/RET_UNLINK back over conn. Linux
|
||||
// kernel-handoff sessions use kernelHandoffSession instead.
|
||||
type userspaceURBSession struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
|
||||
@@ -66,9 +66,6 @@ func (d *sysfsDevice) toProtocol() DeviceInfoTruncated {
|
||||
return info
|
||||
}
|
||||
|
||||
// vhciStatusRecord is one row of /sys/devices/platform/vhci_hcd.0/status
|
||||
// or status.N. The kernel emits globally unique port numbers across every
|
||||
// status* file.
|
||||
type vhciStatusRecord struct {
|
||||
hub string
|
||||
port int
|
||||
@@ -180,10 +177,7 @@ func readUsbipStatus(busid string) (int, error) {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// finishImportStatusTimeout is the upper bound for waitForUsbipStatusCleared.
|
||||
// It is a var (not const) so interop tests can shrink it without changing the
|
||||
// polling cadence.
|
||||
var finishImportStatusTimeout = 2 * time.Second
|
||||
const finishImportStatusTimeout = 2 * time.Second
|
||||
|
||||
const finishImportStatusPollInterval = 25 * time.Millisecond
|
||||
|
||||
@@ -209,10 +203,6 @@ func waitForUsbipStatusCleared(ctx context.Context, busid string) {
|
||||
}
|
||||
}
|
||||
|
||||
// readPrimaryVHCIStatus reads every status* file under
|
||||
// /sys/devices/platform/vhci_hcd.0 and concatenates the rows in lexical
|
||||
// order. Port numbers are already globally unique across controllers — no
|
||||
// remapping is needed.
|
||||
func readPrimaryVHCIStatus() ([]vhciStatusRecord, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(sysVHCIControllerV0, "status*"))
|
||||
if err != nil {
|
||||
|
||||
@@ -2,25 +2,12 @@
|
||||
|
||||
package usbip
|
||||
|
||||
// URBEngine executes USB Request Blocks against an already-claimed
|
||||
// device. The session layer (session_userspace.go) handles framing,
|
||||
// per-endpoint ordering, and unlink bookkeeping; the engine performs
|
||||
// the per-URB I/O and per-endpoint aborts only.
|
||||
//
|
||||
// Submit is called from per-endpoint goroutines; the session never
|
||||
// issues two Submits concurrently for the same endpoint, so the engine
|
||||
// does not need its own cross-endpoint serialization.
|
||||
// Submit is called from per-endpoint goroutines; the session serializes
|
||||
// per-endpoint so the engine needs no cross-endpoint lock.
|
||||
type URBEngine interface {
|
||||
Submit(request URBRequest) URBResponse
|
||||
// AbortEndpoint cancels all in-flight submits on the given raw
|
||||
// endpoint address (direction bit included). It is invoked once per
|
||||
// pending sequence at CMD_UNLINK time and once per active endpoint
|
||||
// at session shutdown.
|
||||
AbortEndpoint(endpoint uint8) error
|
||||
// Close releases engine-owned resources. For engines that own the
|
||||
// underlying device handle (e.g. Windows VBoxUSB), this releases it.
|
||||
// For engines where the host manages the device handle separately
|
||||
// (e.g. Darwin IOUSBHost capture), Close may be a no-op. Idempotent.
|
||||
// Idempotent.
|
||||
Close() error
|
||||
}
|
||||
|
||||
|
||||
@@ -21,15 +21,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ciStatusSuccess = 1
|
||||
ciStatusOffline = 2
|
||||
ciStatusNotPermitted = 3
|
||||
ciStatusBadArgument = 4
|
||||
ciStatusTimeout = 5
|
||||
ciStatusNoResources = 6
|
||||
ciStatusEndpointStopped = 7
|
||||
ciStatusStallError = 11
|
||||
ciStatusError = 13
|
||||
ciStatusSuccess = 1
|
||||
|
||||
ciMsgControllerPowerOn = 0x10
|
||||
ciMsgControllerPowerOff = 0x11
|
||||
@@ -89,7 +81,6 @@ func (c cgoCallbackHandle) token() C.uintptr_t {
|
||||
return C.uintptr_t(c.handle)
|
||||
}
|
||||
|
||||
// deleteRaw rolls back a failed C-side create without a drain.
|
||||
func (c cgoCallbackHandle) deleteRaw() {
|
||||
c.handle.Delete()
|
||||
}
|
||||
@@ -171,7 +162,7 @@ func darwinWatchUSBHostDevices(callback func()) (*darwinUSBHostDeviceWatcher, er
|
||||
}
|
||||
|
||||
func (w *darwinUSBHostDeviceWatcher) Close() {
|
||||
if w == nil || w.handle == nil {
|
||||
if w.handle == nil {
|
||||
return
|
||||
}
|
||||
w.callback.closeAfter(func() {
|
||||
@@ -192,7 +183,7 @@ func darwinCreateUSBHostController(controller *darwinVirtualController, portCoun
|
||||
}
|
||||
|
||||
func (c *darwinUSBHostController) Close() {
|
||||
if c == nil || c.handle == nil {
|
||||
if c.handle == nil {
|
||||
return
|
||||
}
|
||||
c.callback.closeAfter(func() {
|
||||
@@ -239,7 +230,7 @@ func (c *darwinUSBHostController) createDeviceSM(message darwinCIMessage) (*darw
|
||||
}
|
||||
|
||||
func (s *darwinUSBHostDeviceSM) Close() {
|
||||
if s == nil || s.handle == nil {
|
||||
if s.handle == nil {
|
||||
return
|
||||
}
|
||||
C.box_usbhost_device_sm_destroy(s.handle)
|
||||
@@ -275,7 +266,7 @@ func (c *darwinUSBHostController) createEndpointSM(message darwinCIMessage) (*da
|
||||
}
|
||||
|
||||
func (s *darwinUSBHostEndpointSM) Close() {
|
||||
if s == nil || s.handle == nil {
|
||||
if s.handle == nil {
|
||||
return
|
||||
}
|
||||
C.box_usbhost_endpoint_sm_destroy(s.handle)
|
||||
@@ -403,7 +394,7 @@ func box_usbip_darwin_usb_event(ref C.uintptr_t) {
|
||||
}
|
||||
|
||||
func (d *darwinUSBHostDevice) Close() {
|
||||
if d == nil || d.handle == nil {
|
||||
if d.handle == nil {
|
||||
return
|
||||
}
|
||||
C.box_usbhost_device_close(d.handle)
|
||||
@@ -411,9 +402,6 @@ func (d *darwinUSBHostDevice) Close() {
|
||||
}
|
||||
|
||||
func (d *darwinUSBHostDevice) control(setup [8]byte, buffer []byte) (int32, int32, []byte, error) {
|
||||
if d == nil || d.handle == nil {
|
||||
return -int32(unix.ENODEV), 0, nil, E.New("IOUSBHostDevice control: closed")
|
||||
}
|
||||
var actual C.size_t
|
||||
var status C.int32_t
|
||||
var errorPtr *C.char
|
||||
@@ -429,9 +417,6 @@ func (d *darwinUSBHostDevice) control(setup [8]byte, buffer []byte) (int32, int3
|
||||
}
|
||||
|
||||
func (d *darwinUSBHostDevice) io(endpoint uint8, buffer []byte) (int32, int32, []byte, error) {
|
||||
if d == nil || d.handle == nil {
|
||||
return -int32(unix.ENODEV), 0, nil, E.New("IOUSBHostPipe IO: closed")
|
||||
}
|
||||
var actual C.size_t
|
||||
var status C.int32_t
|
||||
var errorPtr *C.char
|
||||
@@ -446,9 +431,6 @@ func (d *darwinUSBHostDevice) io(endpoint uint8, buffer []byte) (int32, int32, [
|
||||
}
|
||||
|
||||
func (d *darwinUSBHostDevice) iso(endpoint uint8, buffer []byte, startFrame int32, asap bool, packets []IsoPacketDescriptor) (int32, int32, []byte, []IsoPacketDescriptor, error) {
|
||||
if d == nil || d.handle == nil {
|
||||
return -int32(unix.ENODEV), 0, nil, nil, E.New("IOUSBHostPipe isochronous IO: closed")
|
||||
}
|
||||
var actual C.size_t
|
||||
var status C.int32_t
|
||||
var errorPtr *C.char
|
||||
@@ -478,9 +460,6 @@ func (d *darwinUSBHostDevice) iso(endpoint uint8, buffer []byte, startFrame int3
|
||||
}
|
||||
|
||||
func (d *darwinUSBHostDevice) abortEndpoint(endpoint uint8) error {
|
||||
if d == nil || d.handle == nil {
|
||||
return nil
|
||||
}
|
||||
var errorPtr *C.char
|
||||
if !bool(C.box_usbhost_device_abort_endpoint(d.handle, C.uint8_t(endpoint), &errorPtr)) {
|
||||
return darwinCError(errorPtr)
|
||||
@@ -574,9 +553,7 @@ func darwinIOReturnToUSBIPStatus(status int32) int32 {
|
||||
}
|
||||
}
|
||||
|
||||
// darwinUSBIPStatusToCIStatus maps USB/IP transfer completion status to the
|
||||
// corresponding IOUSBHostCI completion status. This is only used for real
|
||||
// transfer completion, not for EndpointPause-driven state machine events.
|
||||
// Only for real transfer completion, not for EndpointPause-driven state machine events.
|
||||
func darwinUSBIPStatusToCIStatus(status int32) int {
|
||||
if status == 0 {
|
||||
return int(C.IOUSBHostCIMessageStatusSuccess)
|
||||
|
||||
Reference in New Issue
Block a user