mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
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:
parent
e59e14d89d
commit
6a4ae5263e
24 changed files with 2110 additions and 467 deletions
BIN
common/vboxusb/assets/amd64/VBoxUSB.cat
Normal file
BIN
common/vboxusb/assets/amd64/VBoxUSB.cat
Normal file
Binary file not shown.
103
common/vboxusb/assets/amd64/VBoxUSB.inf
Normal file
103
common/vboxusb/assets/amd64/VBoxUSB.inf
Normal 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
|
||||
|
||||
BIN
common/vboxusb/assets/amd64/VBoxUSB.sys
Normal file
BIN
common/vboxusb/assets/amd64/VBoxUSB.sys
Normal file
Binary file not shown.
BIN
common/vboxusb/assets/amd64/VBoxUSBMon.sys
Normal file
BIN
common/vboxusb/assets/amd64/VBoxUSBMon.sys
Normal file
Binary file not shown.
BIN
common/vboxusb/assets/arm64/VBoxUSB.cat
Normal file
BIN
common/vboxusb/assets/arm64/VBoxUSB.cat
Normal file
Binary file not shown.
103
common/vboxusb/assets/arm64/VBoxUSB.inf
Normal file
103
common/vboxusb/assets/arm64/VBoxUSB.inf
Normal 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
|
||||
|
||||
BIN
common/vboxusb/assets/arm64/VBoxUSB.sys
Normal file
BIN
common/vboxusb/assets/arm64/VBoxUSB.sys
Normal file
Binary file not shown.
BIN
common/vboxusb/assets/arm64/VBoxUSBMon.sys
Normal file
BIN
common/vboxusb/assets/arm64/VBoxUSBMon.sys
Normal file
Binary file not shown.
26
common/vboxusb/assets_windows_amd64.go
Normal file
26
common/vboxusb/assets_windows_amd64.go
Normal 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
common/vboxusb/assets_windows_arm64.go
Normal file
26
common/vboxusb/assets_windows_arm64.go
Normal 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},
|
||||
}
|
||||
}
|
||||
5
common/vboxusb/assets_windows_unsupported.go
Normal file
5
common/vboxusb/assets_windows_unsupported.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//go:build windows && !amd64 && !arm64
|
||||
|
||||
package vboxusb
|
||||
|
||||
func assetFiles() []assetFile { return nil }
|
||||
310
common/vboxusb/device_windows.go
Normal file
310
common/vboxusb/device_windows.go
Normal 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
common/vboxusb/driver_windows.go
Normal file
260
common/vboxusb/driver_windows.go
Normal 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
common/vboxusb/monitor_windows.go
Normal file
216
common/vboxusb/monitor_windows.go
Normal 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
common/vboxusb/pnp_windows.go
Normal file
265
common/vboxusb/pnp_windows.go
Normal 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,
|
||||
¶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,
|
||||
}
|
||||
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
common/vboxusb/privilege_windows.go
Normal file
56
common/vboxusb/privilege_windows.go
Normal 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
common/vboxusb/vboxusb.go
Normal file
175
common/vboxusb/vboxusb.go
Normal 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
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ const (
|
|||
maxUSBIPIsoPackets = 1024
|
||||
nonIsoPacketCount = -1
|
||||
usbipTransferFlagIsoASAP = 0x0002
|
||||
usbipStatusEIO = -5
|
||||
usbipStatusECONNRESET = -104
|
||||
)
|
||||
|
||||
|
|
|
|||
39
service/usbip/engine_darwin.go
Normal file
39
service/usbip/engine_darwin.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
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.
|
||||
type darwinIOUSBHostEngine struct {
|
||||
device *darwinUSBHostDevice
|
||||
}
|
||||
|
||||
func newDarwinIOUSBHostEngine(device *darwinUSBHostDevice) *darwinIOUSBHostEngine {
|
||||
return &darwinIOUSBHostEngine{device: device}
|
||||
}
|
||||
|
||||
func (e *darwinIOUSBHostEngine) Submit(req URBRequest) URBResponse {
|
||||
command := req.Command
|
||||
switch {
|
||||
case command.Header.Endpoint == 0:
|
||||
status, actual, outBuf, err := e.device.control(command.Setup, req.Buffer)
|
||||
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, Error: err}
|
||||
case command.NumberOfPackets > 0:
|
||||
asap := command.TransferFlags&usbipTransferFlagIsoASAP != 0
|
||||
status, actual, outBuf, isoOut, err := e.device.iso(req.Endpoint, req.Buffer, command.StartFrame, asap, req.IsoPackets)
|
||||
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, IsoPackets: isoOut, Error: err}
|
||||
default:
|
||||
status, actual, outBuf, err := e.device.io(req.Endpoint, req.Buffer)
|
||||
return URBResponse{Status: status, ActualLength: actual, Buffer: outBuf, Error: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *darwinIOUSBHostEngine) AbortEndpoint(endpoint uint8) error {
|
||||
return e.device.abortEndpoint(endpoint)
|
||||
}
|
||||
|
||||
func (e *darwinIOUSBHostEngine) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@ package usbip
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"slices"
|
||||
|
|
@ -14,10 +12,7 @@ import (
|
|||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func newPlatformExportHost(ctx context.Context, logger log.ContextLogger, matches []option.USBIPDeviceMatch) (ExportHost, error) {
|
||||
|
|
@ -348,7 +343,7 @@ func (e *darwinExport) NewServerDataSession(ctx context.Context, conn net.Conn)
|
|||
if e.device == nil {
|
||||
return nil, E.New("darwin export ", e.busid, " has no device handle")
|
||||
}
|
||||
return newDarwinServerDataSession(ctx, e.logger, conn, e.device), nil
|
||||
return newUserspaceURBSession(ctx, e.logger, conn, newDarwinIOUSBHostEngine(e.device)), nil
|
||||
}
|
||||
|
||||
type darwinImportHost struct {
|
||||
|
|
@ -372,456 +367,3 @@ func (h *darwinImportHost) Attach(ctx context.Context, info DeviceInfoTruncated,
|
|||
}
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
var _ DataSession = (*darwinServerDataSession)(nil)
|
||||
|
||||
type darwinServerDataSession struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
conn net.Conn
|
||||
device *darwinUSBHostDevice
|
||||
writeAccess sync.Mutex
|
||||
access sync.Mutex
|
||||
pending map[uint32]darwinServerSubmitState
|
||||
endpoints map[uint8]*darwinServerEndpointState
|
||||
wg sync.WaitGroup
|
||||
|
||||
done chan struct{}
|
||||
doneOnce sync.Once
|
||||
runErr error
|
||||
|
||||
stateAccess sync.Mutex
|
||||
started bool
|
||||
closed bool
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
type darwinServerSubmitState struct {
|
||||
command SubmitCommand
|
||||
endpoint uint8
|
||||
started bool
|
||||
unlinked bool
|
||||
drained chan struct{}
|
||||
}
|
||||
|
||||
type darwinServerEndpointState struct {
|
||||
active uint32
|
||||
queued []uint32
|
||||
}
|
||||
|
||||
type darwinServerNextSubmit struct {
|
||||
sequence uint32
|
||||
command SubmitCommand
|
||||
}
|
||||
|
||||
func newDarwinServerDataSession(ctx context.Context, logger log.ContextLogger, conn net.Conn, device *darwinUSBHostDevice) *darwinServerDataSession {
|
||||
return &darwinServerDataSession{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
conn: conn,
|
||||
device: device,
|
||||
pending: make(map[uint32]darwinServerSubmitState),
|
||||
endpoints: make(map[uint8]*darwinServerEndpointState),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) Done() <-chan struct{} {
|
||||
return s.done
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) Err() error {
|
||||
return s.runErr
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) Start() error {
|
||||
s.stateAccess.Lock()
|
||||
defer s.stateAccess.Unlock()
|
||||
if s.started || s.closed {
|
||||
return nil
|
||||
}
|
||||
s.started = true
|
||||
go s.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) Close() error {
|
||||
s.closeOnce.Do(func() {
|
||||
s.closeErr = common.Close(s.conn)
|
||||
})
|
||||
s.stateAccess.Lock()
|
||||
started := s.started
|
||||
s.closed = true
|
||||
s.stateAccess.Unlock()
|
||||
if started {
|
||||
<-s.done
|
||||
} else {
|
||||
s.markDone(nil)
|
||||
}
|
||||
return s.closeErr
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) markDone(err error) {
|
||||
s.doneOnce.Do(func() {
|
||||
s.runErr = err
|
||||
close(s.done)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) run() {
|
||||
err := s.serve()
|
||||
if err != nil && (errors.Is(err, io.EOF) || E.IsClosedOrCanceled(err)) {
|
||||
err = nil
|
||||
}
|
||||
s.markDone(err)
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) serve() error {
|
||||
stopCloseOnCancel := closeConnOnContextDone(s.ctx, s.conn)
|
||||
defer stopCloseOnCancel()
|
||||
defer func() {
|
||||
s.abortPendingSubmits()
|
||||
s.wg.Wait()
|
||||
}()
|
||||
for {
|
||||
header, err := ReadDataHeader(s.conn)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
switch header.Command {
|
||||
case CmdSubmit:
|
||||
command, err := ReadSubmitCommandBody(s.conn, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next, shouldStart := s.enqueueSubmit(command)
|
||||
if shouldStart {
|
||||
s.startSubmit(next)
|
||||
}
|
||||
case CmdUnlink:
|
||||
command, err := ReadUnlinkCommandBody(s.conn, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := int32(0)
|
||||
endpoint, drained, shouldAbort, found := s.unlinkSubmit(command.SeqNum)
|
||||
if found {
|
||||
if shouldAbort {
|
||||
abortErr := s.device.abortEndpoint(endpoint)
|
||||
if abortErr != nil {
|
||||
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", abortErr)
|
||||
}
|
||||
}
|
||||
<-drained
|
||||
status = usbipStatusECONNRESET
|
||||
}
|
||||
s.writeAccess.Lock()
|
||||
err = WriteUnlinkResponse(s.conn, UnlinkResponse{
|
||||
Header: DataHeader{Command: RetUnlink, SeqNum: header.SeqNum, DevID: header.DevID, Direction: header.Direction, Endpoint: header.Endpoint},
|
||||
Status: status,
|
||||
})
|
||||
s.writeAccess.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unexpected USB/IP command ", fmt.Sprintf("0x%08x", header.Command))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) enqueueSubmit(command SubmitCommand) (darwinServerNextSubmit, bool) {
|
||||
endpoint := submitScheduleEndpoint(command)
|
||||
sequence := command.Header.SeqNum
|
||||
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
|
||||
state := darwinServerSubmitState{
|
||||
command: command,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
endpointState, found := s.endpoints[endpoint]
|
||||
if !found {
|
||||
endpointState = &darwinServerEndpointState{}
|
||||
s.endpoints[endpoint] = endpointState
|
||||
}
|
||||
if endpointState.active == 0 {
|
||||
state.started = true
|
||||
s.pending[sequence] = state
|
||||
endpointState.active = sequence
|
||||
return darwinServerNextSubmit{
|
||||
sequence: sequence,
|
||||
command: command,
|
||||
}, true
|
||||
}
|
||||
s.pending[sequence] = state
|
||||
endpointState.queued = append(endpointState.queued, sequence)
|
||||
return darwinServerNextSubmit{}, false
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) startSubmit(next darwinServerNextSubmit) {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
response := s.handleSubmit(next.command)
|
||||
shouldSend, followUp, hasFollowUp := s.finishSubmit(next.sequence)
|
||||
if shouldSend {
|
||||
s.writeAccess.Lock()
|
||||
err := WriteSubmitResponse(s.conn, response)
|
||||
s.writeAccess.Unlock()
|
||||
if err != nil {
|
||||
_ = s.conn.Close()
|
||||
}
|
||||
}
|
||||
if hasFollowUp {
|
||||
s.startSubmit(followUp)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) handleSubmit(command SubmitCommand) SubmitResponse {
|
||||
response := SubmitResponse{
|
||||
Header: DataHeader{
|
||||
Command: RetSubmit,
|
||||
SeqNum: command.Header.SeqNum,
|
||||
DevID: command.Header.DevID,
|
||||
Direction: command.Header.Direction,
|
||||
Endpoint: command.Header.Endpoint,
|
||||
},
|
||||
StartFrame: command.StartFrame,
|
||||
NumberOfPackets: command.NumberOfPackets,
|
||||
IsoPackets: slices.Clone(command.IsoPackets),
|
||||
}
|
||||
buffer := command.Buffer
|
||||
if command.Header.Direction == USBIPDirIn && command.TransferBufferLength > 0 {
|
||||
buffer = make([]byte, int(command.TransferBufferLength))
|
||||
}
|
||||
var (
|
||||
status int32
|
||||
actual int32
|
||||
err error
|
||||
)
|
||||
endpoint := commandEndpoint(command)
|
||||
switch {
|
||||
case command.Header.Endpoint == 0:
|
||||
status, actual, buffer, err = s.device.control(command.Setup, buffer)
|
||||
case command.NumberOfPackets > 0:
|
||||
asap := command.TransferFlags&usbipTransferFlagIsoASAP != 0
|
||||
status, actual, buffer, response.IsoPackets, err = s.device.iso(endpoint, buffer, command.StartFrame, asap, response.IsoPackets)
|
||||
default:
|
||||
status, actual, buffer, err = s.device.io(endpoint, buffer)
|
||||
}
|
||||
if err != nil {
|
||||
s.logger.Debug("submit seq ", command.Header.SeqNum, " endpoint 0x", hex8(endpoint), ": ", err)
|
||||
response.Status = -int32(unix.EIO)
|
||||
return response
|
||||
}
|
||||
response.Status = status
|
||||
if actual < 0 {
|
||||
actual = 0
|
||||
}
|
||||
response.ActualLength = actual
|
||||
if command.Header.Direction == USBIPDirIn && actual > 0 {
|
||||
if command.NumberOfPackets > 0 {
|
||||
response.Buffer = packIsoInResponseBuffer(buffer, response.IsoPackets)
|
||||
response.ActualLength = int32(len(response.Buffer))
|
||||
} else {
|
||||
response.Buffer = buffer[:min(int(actual), len(buffer))]
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func packIsoInResponseBuffer(buffer []byte, packets []IsoPacketDescriptor) []byte {
|
||||
var total int
|
||||
for i := range packets {
|
||||
length := int(packets[i].ActualLength)
|
||||
if length <= 0 {
|
||||
packets[i].ActualLength = 0
|
||||
continue
|
||||
}
|
||||
offset := int(packets[i].Offset)
|
||||
if offset < 0 || offset >= len(buffer) {
|
||||
packets[i].ActualLength = 0
|
||||
continue
|
||||
}
|
||||
if offset+length > len(buffer) {
|
||||
length = len(buffer) - offset
|
||||
packets[i].ActualLength = int32(length)
|
||||
}
|
||||
total += length
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
packed := make([]byte, 0, total)
|
||||
for i := range packets {
|
||||
length := int(packets[i].ActualLength)
|
||||
if length <= 0 {
|
||||
continue
|
||||
}
|
||||
offset := int(packets[i].Offset)
|
||||
packed = append(packed, buffer[offset:offset+length]...)
|
||||
}
|
||||
return packed
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) unlinkSubmit(seq uint32) (uint8, <-chan struct{}, bool, bool) {
|
||||
var drained chan struct{}
|
||||
|
||||
s.access.Lock()
|
||||
pending, found := s.pending[seq]
|
||||
if !found {
|
||||
s.access.Unlock()
|
||||
return 0, nil, false, false
|
||||
}
|
||||
if pending.drained == nil {
|
||||
pending.drained = make(chan struct{})
|
||||
}
|
||||
drained = pending.drained
|
||||
if !pending.started {
|
||||
endpointState := s.endpoints[pending.endpoint]
|
||||
if endpointState != nil {
|
||||
endpointState.queued = removeQueuedSequence(endpointState.queued, seq)
|
||||
if endpointState.active == 0 && len(endpointState.queued) == 0 {
|
||||
delete(s.endpoints, pending.endpoint)
|
||||
}
|
||||
}
|
||||
delete(s.pending, seq)
|
||||
s.access.Unlock()
|
||||
close(drained)
|
||||
return pending.endpoint, drained, false, true
|
||||
}
|
||||
shouldAbort := !pending.unlinked
|
||||
pending.unlinked = true
|
||||
s.pending[seq] = pending
|
||||
s.access.Unlock()
|
||||
return pending.endpoint, drained, shouldAbort, true
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) finishSubmit(seq uint32) (bool, darwinServerNextSubmit, bool) {
|
||||
var drained chan struct{}
|
||||
var followUp darwinServerNextSubmit
|
||||
var hasFollowUp bool
|
||||
|
||||
s.access.Lock()
|
||||
pending, found := s.pending[seq]
|
||||
if !found {
|
||||
s.access.Unlock()
|
||||
return true, darwinServerNextSubmit{}, false
|
||||
}
|
||||
endpointState := s.endpoints[pending.endpoint]
|
||||
if endpointState != nil && endpointState.active == seq {
|
||||
endpointState.active = 0
|
||||
}
|
||||
delete(s.pending, seq)
|
||||
if endpointState != nil {
|
||||
for len(endpointState.queued) > 0 {
|
||||
nextSequence := endpointState.queued[0]
|
||||
endpointState.queued = endpointState.queued[1:]
|
||||
nextPending, nextFound := s.pending[nextSequence]
|
||||
if !nextFound {
|
||||
continue
|
||||
}
|
||||
nextPending.started = true
|
||||
s.pending[nextSequence] = nextPending
|
||||
endpointState.active = nextSequence
|
||||
followUp = darwinServerNextSubmit{
|
||||
sequence: nextSequence,
|
||||
command: nextPending.command,
|
||||
}
|
||||
hasFollowUp = true
|
||||
break
|
||||
}
|
||||
if endpointState.active == 0 && len(endpointState.queued) == 0 {
|
||||
delete(s.endpoints, pending.endpoint)
|
||||
}
|
||||
}
|
||||
drained = pending.drained
|
||||
unlinked := pending.unlinked
|
||||
s.access.Unlock()
|
||||
if drained != nil {
|
||||
close(drained)
|
||||
}
|
||||
return !unlinked, followUp, hasFollowUp
|
||||
}
|
||||
|
||||
func (s *darwinServerDataSession) abortPendingSubmits() {
|
||||
var (
|
||||
activeEndpoints []uint8
|
||||
drained []chan struct{}
|
||||
)
|
||||
|
||||
s.access.Lock()
|
||||
seen := make(map[uint8]struct{})
|
||||
for seq, pending := range s.pending {
|
||||
if !pending.started {
|
||||
delete(s.pending, seq)
|
||||
if pending.drained != nil {
|
||||
drained = append(drained, pending.drained)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !pending.unlinked {
|
||||
seen[pending.endpoint] = struct{}{}
|
||||
}
|
||||
pending.unlinked = true
|
||||
s.pending[seq] = pending
|
||||
}
|
||||
for endpoint := range s.endpoints {
|
||||
endpointState := s.endpoints[endpoint]
|
||||
if endpointState != nil {
|
||||
endpointState.queued = nil
|
||||
}
|
||||
}
|
||||
s.access.Unlock()
|
||||
|
||||
for _, drainedChannel := range drained {
|
||||
close(drainedChannel)
|
||||
}
|
||||
activeEndpoints = make([]uint8, 0, len(seen))
|
||||
for endpoint := range seen {
|
||||
activeEndpoints = append(activeEndpoints, endpoint)
|
||||
}
|
||||
slices.Sort(activeEndpoints)
|
||||
if s.device == nil {
|
||||
return
|
||||
}
|
||||
for _, endpoint := range activeEndpoints {
|
||||
err := s.device.abortEndpoint(endpoint)
|
||||
if err != nil {
|
||||
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeQueuedSequence(queue []uint32, sequence uint32) []uint32 {
|
||||
for index, current := range queue {
|
||||
if current != sequence {
|
||||
continue
|
||||
}
|
||||
return append(queue[:index], queue[index+1:]...)
|
||||
}
|
||||
return queue
|
||||
}
|
||||
|
||||
func submitScheduleEndpoint(command SubmitCommand) uint8 {
|
||||
if command.Header.Endpoint == 0 {
|
||||
return 0
|
||||
}
|
||||
return commandEndpoint(command)
|
||||
}
|
||||
|
||||
func commandEndpoint(command SubmitCommand) uint8 {
|
||||
endpoint := uint8(command.Header.Endpoint & 0x0f)
|
||||
if command.Header.Direction == USBIPDirIn {
|
||||
endpoint |= 0x80
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
|
|
|||
470
service/usbip/session_userspace.go
Normal file
470
service/usbip/session_userspace.go
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
//go:build linux || (darwin && cgo)
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
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
|
||||
conn net.Conn
|
||||
engine URBEngine
|
||||
|
||||
writeAccess sync.Mutex
|
||||
access sync.Mutex
|
||||
pending map[uint32]userspaceSubmitState
|
||||
endpoints map[uint8]*userspaceEndpointState
|
||||
wg sync.WaitGroup
|
||||
|
||||
done chan struct{}
|
||||
doneOnce sync.Once
|
||||
runErr error
|
||||
|
||||
stateAccess sync.Mutex
|
||||
started bool
|
||||
closed bool
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
type userspaceSubmitState struct {
|
||||
command SubmitCommand
|
||||
endpoint uint8
|
||||
started bool
|
||||
unlinked bool
|
||||
drained chan struct{}
|
||||
}
|
||||
|
||||
type userspaceEndpointState struct {
|
||||
active uint32
|
||||
queued []uint32
|
||||
}
|
||||
|
||||
type userspaceNextSubmit struct {
|
||||
sequence uint32
|
||||
command SubmitCommand
|
||||
}
|
||||
|
||||
func newUserspaceURBSession(ctx context.Context, logger log.ContextLogger, conn net.Conn, engine URBEngine) *userspaceURBSession {
|
||||
return &userspaceURBSession{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
conn: conn,
|
||||
engine: engine,
|
||||
pending: make(map[uint32]userspaceSubmitState),
|
||||
endpoints: make(map[uint8]*userspaceEndpointState),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) Done() <-chan struct{} {
|
||||
return s.done
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) Err() error {
|
||||
return s.runErr
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) Start() error {
|
||||
s.stateAccess.Lock()
|
||||
defer s.stateAccess.Unlock()
|
||||
if s.started || s.closed {
|
||||
return nil
|
||||
}
|
||||
s.started = true
|
||||
go s.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) Close() error {
|
||||
s.closeOnce.Do(func() {
|
||||
s.closeErr = common.Close(s.conn)
|
||||
})
|
||||
s.stateAccess.Lock()
|
||||
started := s.started
|
||||
s.closed = true
|
||||
s.stateAccess.Unlock()
|
||||
if started {
|
||||
<-s.done
|
||||
} else {
|
||||
s.markDone(nil)
|
||||
}
|
||||
_ = s.engine.Close()
|
||||
return s.closeErr
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) markDone(err error) {
|
||||
s.doneOnce.Do(func() {
|
||||
s.runErr = err
|
||||
close(s.done)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) run() {
|
||||
err := s.serve()
|
||||
if err != nil && (errors.Is(err, io.EOF) || E.IsClosedOrCanceled(err)) {
|
||||
err = nil
|
||||
}
|
||||
s.markDone(err)
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) serve() error {
|
||||
stopCloseOnCancel := closeConnOnContextDone(s.ctx, s.conn)
|
||||
defer stopCloseOnCancel()
|
||||
defer func() {
|
||||
s.abortPendingSubmits()
|
||||
s.wg.Wait()
|
||||
}()
|
||||
for {
|
||||
header, err := ReadDataHeader(s.conn)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
switch header.Command {
|
||||
case CmdSubmit:
|
||||
command, err := ReadSubmitCommandBody(s.conn, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next, shouldStart := s.enqueueSubmit(command)
|
||||
if shouldStart {
|
||||
s.startSubmit(next)
|
||||
}
|
||||
case CmdUnlink:
|
||||
command, err := ReadUnlinkCommandBody(s.conn, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := int32(0)
|
||||
endpoint, drained, shouldAbort, found := s.unlinkSubmit(command.SeqNum)
|
||||
if found {
|
||||
if shouldAbort {
|
||||
abortErr := s.engine.AbortEndpoint(endpoint)
|
||||
if abortErr != nil {
|
||||
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", abortErr)
|
||||
}
|
||||
}
|
||||
<-drained
|
||||
status = usbipStatusECONNRESET
|
||||
}
|
||||
s.writeAccess.Lock()
|
||||
err = WriteUnlinkResponse(s.conn, UnlinkResponse{
|
||||
Header: DataHeader{Command: RetUnlink, SeqNum: header.SeqNum, DevID: header.DevID, Direction: header.Direction, Endpoint: header.Endpoint},
|
||||
Status: status,
|
||||
})
|
||||
s.writeAccess.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return E.New("unexpected USB/IP command ", fmt.Sprintf("0x%08x", header.Command))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) enqueueSubmit(command SubmitCommand) (userspaceNextSubmit, bool) {
|
||||
endpoint := submitScheduleEndpoint(command)
|
||||
sequence := command.Header.SeqNum
|
||||
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
|
||||
state := userspaceSubmitState{
|
||||
command: command,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
endpointState, found := s.endpoints[endpoint]
|
||||
if !found {
|
||||
endpointState = &userspaceEndpointState{}
|
||||
s.endpoints[endpoint] = endpointState
|
||||
}
|
||||
if endpointState.active == 0 {
|
||||
state.started = true
|
||||
s.pending[sequence] = state
|
||||
endpointState.active = sequence
|
||||
return userspaceNextSubmit{
|
||||
sequence: sequence,
|
||||
command: command,
|
||||
}, true
|
||||
}
|
||||
s.pending[sequence] = state
|
||||
endpointState.queued = append(endpointState.queued, sequence)
|
||||
return userspaceNextSubmit{}, false
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) startSubmit(next userspaceNextSubmit) {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
|
||||
response := s.handleSubmit(next.command)
|
||||
shouldSend, followUp, hasFollowUp := s.finishSubmit(next.sequence)
|
||||
if shouldSend {
|
||||
s.writeAccess.Lock()
|
||||
err := WriteSubmitResponse(s.conn, response)
|
||||
s.writeAccess.Unlock()
|
||||
if err != nil {
|
||||
_ = s.conn.Close()
|
||||
}
|
||||
}
|
||||
if hasFollowUp {
|
||||
s.startSubmit(followUp)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) handleSubmit(command SubmitCommand) SubmitResponse {
|
||||
response := SubmitResponse{
|
||||
Header: DataHeader{
|
||||
Command: RetSubmit,
|
||||
SeqNum: command.Header.SeqNum,
|
||||
DevID: command.Header.DevID,
|
||||
Direction: command.Header.Direction,
|
||||
Endpoint: command.Header.Endpoint,
|
||||
},
|
||||
StartFrame: command.StartFrame,
|
||||
NumberOfPackets: command.NumberOfPackets,
|
||||
IsoPackets: slices.Clone(command.IsoPackets),
|
||||
}
|
||||
buffer := command.Buffer
|
||||
if command.Header.Direction == USBIPDirIn && command.TransferBufferLength > 0 {
|
||||
buffer = make([]byte, int(command.TransferBufferLength))
|
||||
}
|
||||
endpoint := commandEndpoint(command)
|
||||
result := s.engine.Submit(URBRequest{
|
||||
Command: command,
|
||||
Endpoint: endpoint,
|
||||
Buffer: buffer,
|
||||
IsoPackets: response.IsoPackets,
|
||||
})
|
||||
if result.Error != nil {
|
||||
s.logger.Debug("submit seq ", command.Header.SeqNum, " endpoint 0x", hex8(endpoint), ": ", result.Error)
|
||||
response.Status = usbipStatusEIO
|
||||
return response
|
||||
}
|
||||
response.Status = result.Status
|
||||
if result.IsoPackets != nil {
|
||||
response.IsoPackets = result.IsoPackets
|
||||
}
|
||||
actual := result.ActualLength
|
||||
if actual < 0 {
|
||||
actual = 0
|
||||
}
|
||||
response.ActualLength = actual
|
||||
if command.Header.Direction == USBIPDirIn && actual > 0 {
|
||||
if command.NumberOfPackets > 0 {
|
||||
response.Buffer = packIsoInResponseBuffer(result.Buffer, response.IsoPackets)
|
||||
response.ActualLength = int32(len(response.Buffer))
|
||||
} else {
|
||||
response.Buffer = result.Buffer[:min(int(actual), len(result.Buffer))]
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func packIsoInResponseBuffer(buffer []byte, packets []IsoPacketDescriptor) []byte {
|
||||
var total int
|
||||
for i := range packets {
|
||||
length := int(packets[i].ActualLength)
|
||||
if length <= 0 {
|
||||
packets[i].ActualLength = 0
|
||||
continue
|
||||
}
|
||||
offset := int(packets[i].Offset)
|
||||
if offset < 0 || offset >= len(buffer) {
|
||||
packets[i].ActualLength = 0
|
||||
continue
|
||||
}
|
||||
if offset+length > len(buffer) {
|
||||
length = len(buffer) - offset
|
||||
packets[i].ActualLength = int32(length)
|
||||
}
|
||||
total += length
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
packed := make([]byte, 0, total)
|
||||
for i := range packets {
|
||||
length := int(packets[i].ActualLength)
|
||||
if length <= 0 {
|
||||
continue
|
||||
}
|
||||
offset := int(packets[i].Offset)
|
||||
packed = append(packed, buffer[offset:offset+length]...)
|
||||
}
|
||||
return packed
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) unlinkSubmit(seq uint32) (uint8, <-chan struct{}, bool, bool) {
|
||||
var drained chan struct{}
|
||||
|
||||
s.access.Lock()
|
||||
pending, found := s.pending[seq]
|
||||
if !found {
|
||||
s.access.Unlock()
|
||||
return 0, nil, false, false
|
||||
}
|
||||
if pending.drained == nil {
|
||||
pending.drained = make(chan struct{})
|
||||
}
|
||||
drained = pending.drained
|
||||
if !pending.started {
|
||||
endpointState := s.endpoints[pending.endpoint]
|
||||
if endpointState != nil {
|
||||
endpointState.queued = removeQueuedSequence(endpointState.queued, seq)
|
||||
if endpointState.active == 0 && len(endpointState.queued) == 0 {
|
||||
delete(s.endpoints, pending.endpoint)
|
||||
}
|
||||
}
|
||||
delete(s.pending, seq)
|
||||
s.access.Unlock()
|
||||
close(drained)
|
||||
return pending.endpoint, drained, false, true
|
||||
}
|
||||
shouldAbort := !pending.unlinked
|
||||
pending.unlinked = true
|
||||
s.pending[seq] = pending
|
||||
s.access.Unlock()
|
||||
return pending.endpoint, drained, shouldAbort, true
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) finishSubmit(seq uint32) (bool, userspaceNextSubmit, bool) {
|
||||
var drained chan struct{}
|
||||
var followUp userspaceNextSubmit
|
||||
var hasFollowUp bool
|
||||
|
||||
s.access.Lock()
|
||||
pending, found := s.pending[seq]
|
||||
if !found {
|
||||
s.access.Unlock()
|
||||
return true, userspaceNextSubmit{}, false
|
||||
}
|
||||
endpointState := s.endpoints[pending.endpoint]
|
||||
if endpointState != nil && endpointState.active == seq {
|
||||
endpointState.active = 0
|
||||
}
|
||||
delete(s.pending, seq)
|
||||
if endpointState != nil {
|
||||
for len(endpointState.queued) > 0 {
|
||||
nextSequence := endpointState.queued[0]
|
||||
endpointState.queued = endpointState.queued[1:]
|
||||
nextPending, nextFound := s.pending[nextSequence]
|
||||
if !nextFound {
|
||||
continue
|
||||
}
|
||||
nextPending.started = true
|
||||
s.pending[nextSequence] = nextPending
|
||||
endpointState.active = nextSequence
|
||||
followUp = userspaceNextSubmit{
|
||||
sequence: nextSequence,
|
||||
command: nextPending.command,
|
||||
}
|
||||
hasFollowUp = true
|
||||
break
|
||||
}
|
||||
if endpointState.active == 0 && len(endpointState.queued) == 0 {
|
||||
delete(s.endpoints, pending.endpoint)
|
||||
}
|
||||
}
|
||||
drained = pending.drained
|
||||
unlinked := pending.unlinked
|
||||
s.access.Unlock()
|
||||
if drained != nil {
|
||||
close(drained)
|
||||
}
|
||||
return !unlinked, followUp, hasFollowUp
|
||||
}
|
||||
|
||||
func (s *userspaceURBSession) abortPendingSubmits() {
|
||||
var (
|
||||
activeEndpoints []uint8
|
||||
drained []chan struct{}
|
||||
)
|
||||
|
||||
s.access.Lock()
|
||||
seen := make(map[uint8]struct{})
|
||||
for seq, pending := range s.pending {
|
||||
if !pending.started {
|
||||
delete(s.pending, seq)
|
||||
if pending.drained != nil {
|
||||
drained = append(drained, pending.drained)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !pending.unlinked {
|
||||
seen[pending.endpoint] = struct{}{}
|
||||
}
|
||||
pending.unlinked = true
|
||||
s.pending[seq] = pending
|
||||
}
|
||||
for endpoint := range s.endpoints {
|
||||
endpointState := s.endpoints[endpoint]
|
||||
if endpointState != nil {
|
||||
endpointState.queued = nil
|
||||
}
|
||||
}
|
||||
s.access.Unlock()
|
||||
|
||||
for _, drainedChannel := range drained {
|
||||
close(drainedChannel)
|
||||
}
|
||||
activeEndpoints = make([]uint8, 0, len(seen))
|
||||
for endpoint := range seen {
|
||||
activeEndpoints = append(activeEndpoints, endpoint)
|
||||
}
|
||||
slices.Sort(activeEndpoints)
|
||||
for _, endpoint := range activeEndpoints {
|
||||
err := s.engine.AbortEndpoint(endpoint)
|
||||
if err != nil {
|
||||
s.logger.Debug("abort endpoint 0x", hex8(endpoint), ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeQueuedSequence(queue []uint32, sequence uint32) []uint32 {
|
||||
for index, current := range queue {
|
||||
if current != sequence {
|
||||
continue
|
||||
}
|
||||
return append(queue[:index], queue[index+1:]...)
|
||||
}
|
||||
return queue
|
||||
}
|
||||
|
||||
func submitScheduleEndpoint(command SubmitCommand) uint8 {
|
||||
if command.Header.Endpoint == 0 {
|
||||
return 0
|
||||
}
|
||||
return commandEndpoint(command)
|
||||
}
|
||||
|
||||
func commandEndpoint(command SubmitCommand) uint8 {
|
||||
endpoint := uint8(command.Header.Endpoint & 0x0f)
|
||||
if command.Header.Direction == USBIPDirIn {
|
||||
endpoint |= 0x80
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
|
@ -42,6 +42,11 @@ func closeConnOnContextDone(ctx context.Context, conn net.Conn) func() {
|
|||
}
|
||||
}
|
||||
|
||||
func hex8(v uint8) string {
|
||||
const hexdigits = "0123456789abcdef"
|
||||
return string([]byte{hexdigits[(v>>4)&0xf], hexdigits[v&0xf]})
|
||||
}
|
||||
|
||||
func describeMatch(m option.USBIPDeviceMatch) string {
|
||||
var parts []string
|
||||
if m.BusID != "" {
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
//go:build darwin && cgo
|
||||
|
||||
package usbip
|
||||
|
||||
func hex8(v uint8) string {
|
||||
const hexdigits = "0123456789abcdef"
|
||||
return string([]byte{hexdigits[(v>>4)&0xf], hexdigits[v&0xf]})
|
||||
}
|
||||
49
service/usbip/urb_engine.go
Normal file
49
service/usbip/urb_engine.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//go:build linux || (darwin && cgo)
|
||||
|
||||
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.
|
||||
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.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// URBRequest carries one decoded CMD_SUBMIT plus session-owned buffers.
|
||||
// Buffer holds the OUT payload on entry, or a pre-allocated zero buffer
|
||||
// for IN transfers. IsoPackets is pre-cloned from the wire command so
|
||||
// the engine may overwrite descriptors in place during iso completion.
|
||||
type URBRequest struct {
|
||||
Command SubmitCommand
|
||||
Endpoint uint8
|
||||
Buffer []byte
|
||||
IsoPackets []IsoPacketDescriptor
|
||||
}
|
||||
|
||||
// URBResponse is the engine's verdict on one URB. Status follows USBIP
|
||||
// convention (negated errno, 0 on success). ActualLength is the number
|
||||
// of payload bytes valid in Buffer. Error is engine-internal failure
|
||||
// distinct from a USB-level error: on Error the session emits
|
||||
// Status = usbipStatusEIO and logs at Debug.
|
||||
type URBResponse struct {
|
||||
Status int32
|
||||
ActualLength int32
|
||||
Buffer []byte
|
||||
IsoPackets []IsoPacketDescriptor
|
||||
Error error
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue