Fix HID transfers, harden the tunnel, add E2E crypto and direct peers
The HID failure came down to the endpoint type map being indexed by endpoint number without the direction bit. A composite device can have endpoint 1 as both interrupt IN (0x81) and bulk OUT (0x01); the last one read won, so interrupt URBs were submitted as bulk and the kernel rejected them. The device attached and stayed silent. Endpoint data now comes from the raw descriptors read from /dev/bus/usb rather than sysfs, which only ever exposes the active alternate setting — a webcam's isochronous endpoints are invisible there because they only exist after SET_INTERFACE. Two sysfs parsing bugs fell out of that too: the numeric endpoint attributes are hex without a prefix (wMaxPacketSize "0040" was read as 40, not 64), and bInterval was never read at all. Reliability: three places could freeze the whole process. The share path fed io.Pipe from the WebSocket read loop, so one slow USB transfer stalled every tunnel and the keepalives with them. The relay wrote to client sockets while holding the hub lock, so one peer that stopped reading blocked routing and registration for everyone. Control transfers ran inline in the protocol loop behind a 5s timeout. Also fixed: a use-after- free where a discarded URB's memory could be collected while the kernel still owned it, a reap loop that spun at 100% CPU on ioctl errors, a missing attach timeout, a double close(done) panic, and Hash[:8] in the relay's log line, which let a client with a short hash take the server down. Adds mode "both", so one client can offer and consume devices at once. The tunnel and client-left callbacks became multicast for it: as plain fields the second manager to register silently unhooked the first. Tunnel traffic is now AES-256-GCM end to end, on the relay path as well as directly. The key is derived from the three tokens, not from the group hash — the relay is told the hash, so a key derived from it would protect nothing from the one party in the middle. Group IDs are unchanged, so existing setups keep working; only clients configured without the tokens drop to unencrypted, relay-only operation. Peers now try to connect directly, with the relay supplying the public address neither side can determine for itself. Candidates are raced because an unreachable address hangs until timeout rather than refusing. Falling back to the relay is not an error. Platform reach: cross-compiled targets for ARM, MIPS and RISC-V (the Linux client needed no code changes — usbdevfs is not architecture specific), multi-arch Docker images, an Android bridge that accepts devices over SCM_RIGHTS because apps cannot open /dev/bus/usb, and macOS builds via system_profiler enumeration. Adds a Windows KMDF filter driver under driver/windows with its Go side. UNTESTED: it has never been compiled or run, needs the WDK to build and an EV certificate to distribute. Treat it as a starting point. Adds "usb-client diag": says per machine whether sharing and using are possible, what stands in the way, and what fixes it. Reports can be uploaded to a relay to get them off machines that are awkward to copy from. 96 tests, all green under -race. Builds for linux, windows and darwin on amd64 and arm64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,12 +87,12 @@ type URBHeader struct {
|
||||
|
||||
// CmdSubmitBody follows URBHeader for USBIP_CMD_SUBMIT
|
||||
type CmdSubmitBody struct {
|
||||
TransferFlags uint32
|
||||
TransferBufferLen uint32
|
||||
StartFrame uint32
|
||||
NumberOfPackets uint32
|
||||
Interval uint32
|
||||
Setup [8]byte
|
||||
TransferFlags uint32
|
||||
TransferBufferLen uint32
|
||||
StartFrame uint32
|
||||
NumberOfPackets uint32
|
||||
Interval uint32
|
||||
Setup [8]byte
|
||||
}
|
||||
|
||||
// RetSubmitBody follows URBHeader for USBIP_RET_SUBMIT
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The USB/IP wire format is fixed: a 20-byte basic header followed by a
|
||||
// 28-byte body. Any drift here desynchronises the stream permanently, so the
|
||||
// sizes are pinned.
|
||||
func TestWireFormatSizes(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := WriteURBHeader(&buf, &URBHeader{}); err != nil {
|
||||
t.Fatalf("WriteURBHeader: %v", err)
|
||||
}
|
||||
if buf.Len() != 20 {
|
||||
t.Errorf("URB header is %d bytes, want 20", buf.Len())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if err := WriteCmdSubmit(&buf, &CmdSubmitBody{}); err != nil {
|
||||
t.Fatalf("WriteCmdSubmit: %v", err)
|
||||
}
|
||||
if buf.Len() != 28 {
|
||||
t.Errorf("CMD_SUBMIT body is %d bytes, want 28", buf.Len())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if err := WriteRetSubmit(&buf, &RetSubmitBody{}); err != nil {
|
||||
t.Fatalf("WriteRetSubmit: %v", err)
|
||||
}
|
||||
if buf.Len() != 28 {
|
||||
t.Errorf("RET_SUBMIT body is %d bytes, want 28", buf.Len())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
if err := WriteRetUnlink(&buf, &RetUnlinkBody{}); err != nil {
|
||||
t.Fatalf("WriteRetUnlink: %v", err)
|
||||
}
|
||||
if buf.Len() != 28 {
|
||||
t.Errorf("RET_UNLINK body is %d bytes, want 28", buf.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRetSubmitInDirection(t *testing.T) {
|
||||
payload := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
|
||||
msg, err := BuildRetSubmit(42, 0x00030002, DirIn, 1, 0, uint32(len(payload)), payload)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildRetSubmit: %v", err)
|
||||
}
|
||||
|
||||
if len(msg) != 48+len(payload) {
|
||||
t.Fatalf("message is %d bytes, want %d", len(msg), 48+len(payload))
|
||||
}
|
||||
|
||||
hdr, err := ReadURBHeader(bytes.NewReader(msg))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadURBHeader: %v", err)
|
||||
}
|
||||
if hdr.Command != RetSubmit {
|
||||
t.Errorf("command = 0x%08x, want RET_SUBMIT", hdr.Command)
|
||||
}
|
||||
if hdr.SeqNum != 42 {
|
||||
t.Errorf("seqnum = %d, want 42", hdr.SeqNum)
|
||||
}
|
||||
if hdr.Direction != DirIn {
|
||||
t.Errorf("direction = %d, want IN", hdr.Direction)
|
||||
}
|
||||
|
||||
body, err := ReadRetSubmit(bytes.NewReader(msg[20:]))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadRetSubmit: %v", err)
|
||||
}
|
||||
if body.ActualLength != uint32(len(payload)) {
|
||||
t.Errorf("actual_length = %d, want %d", body.ActualLength, len(payload))
|
||||
}
|
||||
if body.NumberOfPackets != 0xFFFFFFFF {
|
||||
t.Errorf("number_of_packets = %d, want 0xFFFFFFFF for non-ISO", body.NumberOfPackets)
|
||||
}
|
||||
if !bytes.Equal(msg[48:], payload) {
|
||||
t.Errorf("payload = %x, want %x", msg[48:], payload)
|
||||
}
|
||||
}
|
||||
|
||||
// actual_length must be reported for OUT transfers too. The kernel UVC driver
|
||||
// checks it: a VS_PROBE SET_CUR that reports 0 instead of 26 fails the probe.
|
||||
func TestBuildRetSubmitOutReportsActualLength(t *testing.T) {
|
||||
msg, err := BuildRetSubmit(7, 1, DirOut, 0, 0, 26, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildRetSubmit: %v", err)
|
||||
}
|
||||
if len(msg) != 48 {
|
||||
t.Fatalf("OUT reply is %d bytes, want 48 (no payload)", len(msg))
|
||||
}
|
||||
|
||||
body, _ := ReadRetSubmit(bytes.NewReader(msg[20:]))
|
||||
if body.ActualLength != 26 {
|
||||
t.Errorf("actual_length = %d, want 26", body.ActualLength)
|
||||
}
|
||||
}
|
||||
|
||||
// An IN reply carries no payload when the transfer failed, and the negative
|
||||
// status has to survive the unsigned round trip on the wire.
|
||||
func TestBuildRetSubmitErrorStatus(t *testing.T) {
|
||||
msg, err := BuildRetSubmit(9, 1, DirIn, 2, -32, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildRetSubmit: %v", err)
|
||||
}
|
||||
if len(msg) != 48 {
|
||||
t.Fatalf("error reply is %d bytes, want 48", len(msg))
|
||||
}
|
||||
|
||||
status := int32(binary.BigEndian.Uint32(msg[20:24]))
|
||||
if status != -32 {
|
||||
t.Errorf("status = %d, want -32 (-EPIPE)", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRetSubmitISO(t *testing.T) {
|
||||
descs := []ISOPacketDescriptor{
|
||||
{Offset: 0, Length: 192, ActualLength: 192, Status: 0},
|
||||
{Offset: 192, Length: 192, ActualLength: 100, Status: 0},
|
||||
}
|
||||
packed := make([]byte, 292) // 192 + 100 actual bytes, packed without gaps
|
||||
|
||||
msg, err := BuildRetSubmitISO(5, 1, DirIn, 1, 0, 292, packed, 1000, 2, 0, descs)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildRetSubmitISO: %v", err)
|
||||
}
|
||||
|
||||
wantLen := 48 + len(packed) + 2*16
|
||||
if len(msg) != wantLen {
|
||||
t.Fatalf("ISO reply is %d bytes, want %d", len(msg), wantLen)
|
||||
}
|
||||
|
||||
body, _ := ReadRetSubmit(bytes.NewReader(msg[20:]))
|
||||
if body.NumberOfPackets != 2 {
|
||||
t.Errorf("number_of_packets = %d, want 2", body.NumberOfPackets)
|
||||
}
|
||||
if body.StartFrame != 1000 {
|
||||
t.Errorf("start_frame = %d, want 1000", body.StartFrame)
|
||||
}
|
||||
|
||||
// Descriptors follow the packed payload, big-endian.
|
||||
descOff := 48 + len(packed)
|
||||
var got ISOPacketDescriptor
|
||||
if err := binary.Read(bytes.NewReader(msg[descOff:]), binary.BigEndian, &got); err != nil {
|
||||
t.Fatalf("reading ISO descriptor: %v", err)
|
||||
}
|
||||
if got.Length != 192 || got.ActualLength != 192 {
|
||||
t.Errorf("first descriptor = %+v, want length 192 actual 192", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusIDRoundTrip(t *testing.T) {
|
||||
tests := []string{"1-1", "1-4.3.2", "", "12345678901234567890123456789012"}
|
||||
|
||||
for _, want := range tests {
|
||||
var arr [32]byte
|
||||
SetBusID(&arr, want)
|
||||
if got := GetBusID(arr); got != want {
|
||||
t.Errorf("GetBusID(SetBusID(%q)) = %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetBusIDTruncatesOverlongInput(t *testing.T) {
|
||||
var arr [32]byte
|
||||
SetBusID(&arr, "this-bus-id-is-far-longer-than-thirty-two-bytes")
|
||||
if got := len(GetBusID(arr)); got != 32 {
|
||||
t.Errorf("overlong bus ID produced %d bytes, want 32", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCmdSubmitParsesSetupPacket(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
body := &CmdSubmitBody{
|
||||
TransferBufferLen: 18,
|
||||
NumberOfPackets: 0,
|
||||
Interval: 0,
|
||||
// GET_DESCRIPTOR(device): bmRequestType=0x80 bRequest=0x06 wValue=0x0100
|
||||
Setup: [8]byte{0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00},
|
||||
}
|
||||
if err := WriteCmdSubmit(&buf, body); err != nil {
|
||||
t.Fatalf("WriteCmdSubmit: %v", err)
|
||||
}
|
||||
|
||||
got, err := ReadCmdSubmit(&buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadCmdSubmit: %v", err)
|
||||
}
|
||||
|
||||
// Setup fields are little-endian even though the surrounding header is not.
|
||||
if wValue := binary.LittleEndian.Uint16(got.Setup[2:4]); wValue != 0x0100 {
|
||||
t.Errorf("wValue = 0x%04x, want 0x0100", wValue)
|
||||
}
|
||||
if wLength := binary.LittleEndian.Uint16(got.Setup[6:8]); wLength != 18 {
|
||||
t.Errorf("wLength = %d, want 18", wLength)
|
||||
}
|
||||
if got.TransferBufferLen != 18 {
|
||||
t.Errorf("transfer_buffer_length = %d, want 18", got.TransferBufferLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildImportReply(t *testing.T) {
|
||||
desc := &DeviceDescriptor{BusNum: 1, DevNum: 4, Speed: SpeedHigh}
|
||||
SetBusID(&desc.BusID, "1-4")
|
||||
|
||||
ok, err := BuildImportReply(0, desc)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildImportReply: %v", err)
|
||||
}
|
||||
if len(ok) != 8+312 {
|
||||
t.Errorf("successful reply is %d bytes, want %d", len(ok), 8+312)
|
||||
}
|
||||
|
||||
// A failure carries only the header — no descriptor follows.
|
||||
fail, err := BuildImportReply(1, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildImportReply(1): %v", err)
|
||||
}
|
||||
if len(fail) != 8 {
|
||||
t.Errorf("failure reply is %d bytes, want 8", len(fail))
|
||||
}
|
||||
|
||||
hdr, _ := ReadOpHeader(bytes.NewReader(fail))
|
||||
if hdr.Status != 1 {
|
||||
t.Errorf("status = %d, want 1", hdr.Status)
|
||||
}
|
||||
if hdr.Version != ProtocolVersion {
|
||||
t.Errorf("version = 0x%04x, want 0x%04x", hdr.Version, ProtocolVersion)
|
||||
}
|
||||
}
|
||||
+394
-225
@@ -5,6 +5,7 @@ package usbip
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -17,6 +18,19 @@ import (
|
||||
"github.com/duffy/usb-server/internal/usb"
|
||||
)
|
||||
|
||||
// controlQueueDepth bounds how many control transfers may be waiting for the
|
||||
// control worker. Control transfers are serialised because endpoint 0 is a
|
||||
// single shared pipe; the queue only exists so that a slow transfer does not
|
||||
// stall the URB read loop behind it.
|
||||
const controlQueueDepth = 64
|
||||
|
||||
// controlTimeout is the per-transfer timeout passed to USBDEVFS_CONTROL.
|
||||
const controlTimeout = 5000 // ms
|
||||
|
||||
// reapPollInterval is how long the reap loop waits for a completed URB before
|
||||
// re-checking whether it should shut down.
|
||||
const reapPollInterval = 100 * time.Millisecond
|
||||
|
||||
// Server handles USB/IP protocol on the share side.
|
||||
// It manages a single USB device and forwards URBs between
|
||||
// the USB/IP client (via tunnel) and the physical device (via usbdevfs).
|
||||
@@ -25,29 +39,64 @@ type Server struct {
|
||||
handle *usb.DeviceHandle
|
||||
mu sync.Mutex
|
||||
pendingURBs map[uint32]*pendingURB // seqnum -> pending URB
|
||||
closed bool
|
||||
epTypes map[uint8]uint8 // endpoint number (1-15) -> usbdevfs URB type
|
||||
|
||||
// unlinkedURBs holds URBs that were discarded but not yet reaped.
|
||||
//
|
||||
// USBDEVFS_DISCARDURB is asynchronous: the kernel still owns the URB
|
||||
// struct and its transfer buffer, and will write the completion status
|
||||
// into them. Dropping the last Go reference at unlink time would let the
|
||||
// garbage collector reclaim memory the kernel is about to write to.
|
||||
unlinkedURBs map[uint32]*pendingURB
|
||||
|
||||
closed bool
|
||||
|
||||
// epTypes maps a full bEndpointAddress (direction bit included) to a
|
||||
// usbdevfs URB type. Indexing by address rather than endpoint number
|
||||
// matters: a composite device can have endpoint 1 as interrupt IN (0x81)
|
||||
// and endpoint 1 as bulk OUT (0x01), and submitting an interrupt URB with
|
||||
// the bulk type makes the kernel reject it.
|
||||
epTypes map[uint8]uint8
|
||||
|
||||
// ctrlQueue serialises control transfers on a dedicated worker so that a
|
||||
// blocking USBDEVFS_CONTROL ioctl never stalls the protocol read loop.
|
||||
ctrlQueue chan *ctrlRequest
|
||||
|
||||
// stop is closed to shut down the reap loop and control worker; workers
|
||||
// signals when both have exited so Detach can safely close the fd.
|
||||
stop chan struct{}
|
||||
workers sync.WaitGroup
|
||||
runOnce sync.Once
|
||||
}
|
||||
|
||||
type pendingURB struct {
|
||||
seqNum uint32
|
||||
devID uint32
|
||||
direction uint32
|
||||
endpoint uint32
|
||||
buffer []byte
|
||||
urbPtr unsafe.Pointer // pointer to submitted usbdevfs_urb
|
||||
isISO bool
|
||||
numPackets int32
|
||||
isoMem []byte // keeps ISO URB+descriptors memory alive for GC
|
||||
packetLens []uint32 // original request lengths per ISO packet (for offset computation)
|
||||
seqNum uint32
|
||||
devID uint32
|
||||
direction uint32
|
||||
endpoint uint32
|
||||
buffer []byte
|
||||
urbPtr unsafe.Pointer // pointer to submitted usbdevfs_urb
|
||||
isISO bool
|
||||
numPackets int32
|
||||
isoMem []byte // keeps ISO URB+descriptors memory alive for GC
|
||||
packetLens []uint32 // original request lengths per ISO packet (for offset computation)
|
||||
}
|
||||
|
||||
// ctrlRequest is one queued control transfer.
|
||||
type ctrlRequest struct {
|
||||
hdr *URBHeader
|
||||
body *CmdSubmitBody
|
||||
transferBuf []byte
|
||||
}
|
||||
|
||||
// NewServer creates a USB/IP server for a specific device
|
||||
func NewServer(dev *usb.Device) *Server {
|
||||
return &Server{
|
||||
device: dev,
|
||||
pendingURBs: make(map[uint32]*pendingURB),
|
||||
epTypes: make(map[uint8]uint8),
|
||||
device: dev,
|
||||
pendingURBs: make(map[uint32]*pendingURB),
|
||||
unlinkedURBs: make(map[uint32]*pendingURB),
|
||||
epTypes: make(map[uint8]uint8),
|
||||
ctrlQueue: make(chan *ctrlRequest, controlQueueDepth),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,36 +149,50 @@ func (s *Server) Attach() error {
|
||||
// Detach releases all interfaces, closes the device, and rebinds kernel drivers.
|
||||
func (s *Server) Detach() {
|
||||
s.mu.Lock()
|
||||
alreadyClosed := s.closed
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.handle == nil {
|
||||
if alreadyClosed || s.handle == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Discard all pending URBs to clean up device state
|
||||
// 1. Stop the reap loop and control worker, then wait for them to exit.
|
||||
// This must happen before closing the fd: a worker mid-ioctl on a closed
|
||||
// fd would either fail confusingly or, worse, operate on a recycled fd.
|
||||
s.stopWorkers()
|
||||
|
||||
// 2. Discard all pending URBs to clean up device state.
|
||||
// The maps stay populated on purpose: they are what keeps the URB
|
||||
// structs and transfer buffers reachable while the kernel still owns
|
||||
// them. They are only cleared after the fd is closed, which is the point
|
||||
// at which the kernel definitively drops its references.
|
||||
s.mu.Lock()
|
||||
for seqNum, pending := range s.pendingURBs {
|
||||
for _, pending := range s.pendingURBs {
|
||||
if pending.urbPtr != nil {
|
||||
s.handle.DiscardURBByPtr(pending.urbPtr)
|
||||
}
|
||||
delete(s.pendingURBs, seqNum)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// 2. Release all claimed interfaces
|
||||
// 3. Release all claimed interfaces
|
||||
for _, iface := range s.device.Interfaces {
|
||||
if err := s.handle.ReleaseInterface(uint32(iface.Number)); err != nil {
|
||||
log.Printf("[usbip-server] release interface %d: %v", iface.Number, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Close the device file descriptor.
|
||||
// 4. Close the device file descriptor.
|
||||
// The kernel auto-cancels remaining URBs on close.
|
||||
s.handle.Close()
|
||||
s.handle = nil
|
||||
|
||||
// 4. Force kernel driver re-binding via sysfs authorized toggle.
|
||||
s.mu.Lock()
|
||||
s.pendingURBs = make(map[uint32]*pendingURB)
|
||||
s.unlinkedURBs = make(map[uint32]*pendingURB)
|
||||
s.mu.Unlock()
|
||||
|
||||
// 5. Force kernel driver re-binding via sysfs authorized toggle.
|
||||
// After USBDEVFS_DISCONNECT_CLAIM, the kernel sets privileges_dropped=true.
|
||||
// This means closing the fd does NOT auto-rebind drivers.
|
||||
// Also USBDEVFS_RESET after ReleaseInterface doesn't rebind because
|
||||
@@ -139,6 +202,13 @@ func (s *Server) Detach() {
|
||||
s.rebindDrivers()
|
||||
}
|
||||
|
||||
// stopWorkers signals the reap loop and control worker to exit and waits for
|
||||
// them. Safe to call more than once.
|
||||
func (s *Server) stopWorkers() {
|
||||
s.runOnce.Do(func() { close(s.stop) })
|
||||
s.workers.Wait()
|
||||
}
|
||||
|
||||
// rebindDrivers forces the kernel to re-bind drivers to the device
|
||||
// by toggling the sysfs authorized attribute.
|
||||
func (s *Server) rebindDrivers() {
|
||||
@@ -180,41 +250,90 @@ func (s *Server) rebindDriversFallback() {
|
||||
}
|
||||
}
|
||||
|
||||
// buildEndpointTypeMap builds the endpoint number -> URB type map from device descriptors
|
||||
// urbTypeName maps a usbdevfs URB type to a short label for logging.
|
||||
var urbTypeName = map[uint8]string{
|
||||
usbdevfsTypeISO: "ISO",
|
||||
usbdevfsTypeInterrupt: "INT",
|
||||
usbdevfsTypeControl: "CTRL",
|
||||
usbdevfsTypeBulk: "BULK",
|
||||
}
|
||||
|
||||
// usbdevfs URB types (mirrors the constants in the usb package)
|
||||
const (
|
||||
usbdevfsTypeISO = 0
|
||||
usbdevfsTypeInterrupt = 1
|
||||
usbdevfsTypeControl = 2
|
||||
usbdevfsTypeBulk = 3
|
||||
)
|
||||
|
||||
// buildEndpointTypeMap builds the endpoint address -> URB type map.
|
||||
//
|
||||
// It prefers Device.Endpoints, which is parsed from the raw descriptors and
|
||||
// therefore covers every alternate setting. Endpoints that only appear in a
|
||||
// non-zero alternate setting — the isochronous endpoints of webcams, which
|
||||
// only activate after SET_INTERFACE — would be missing otherwise.
|
||||
func (s *Server) buildEndpointTypeMap() {
|
||||
record := func(ep usb.Endpoint) {
|
||||
var urbType uint8
|
||||
switch ep.TransferType {
|
||||
case usb.TransferTypeControl:
|
||||
urbType = usbdevfsTypeControl
|
||||
case usb.TransferTypeIsochronous:
|
||||
urbType = usbdevfsTypeISO
|
||||
case usb.TransferTypeBulk:
|
||||
urbType = usbdevfsTypeBulk
|
||||
case usb.TransferTypeInterrupt:
|
||||
urbType = usbdevfsTypeInterrupt
|
||||
default:
|
||||
urbType = usbdevfsTypeBulk
|
||||
}
|
||||
s.epTypes[ep.Address] = urbType
|
||||
|
||||
dir := "OUT"
|
||||
if ep.IsIn() {
|
||||
dir = "IN"
|
||||
}
|
||||
log.Printf("[usbip-server] endpoint 0x%02x (EP%d %s): %s maxpkt=%d interval=%d",
|
||||
ep.Address, ep.Number(), dir, urbTypeName[urbType], ep.MaxPacketSize, ep.Interval)
|
||||
}
|
||||
|
||||
if len(s.device.Endpoints) > 0 {
|
||||
for _, ep := range s.device.Endpoints {
|
||||
record(ep)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback for devices whose raw descriptors could not be read.
|
||||
log.Printf("[usbip-server] warning: no parsed descriptors, falling back to sysfs endpoints")
|
||||
for _, iface := range s.device.Interfaces {
|
||||
for _, ep := range iface.Endpoints {
|
||||
epNum := ep.Address & 0x0F
|
||||
// Map USB descriptor transfer type to usbdevfs URB type
|
||||
var urbType uint8
|
||||
switch ep.TransferType {
|
||||
case usb.TransferTypeControl:
|
||||
urbType = 2
|
||||
case usb.TransferTypeIsochronous:
|
||||
urbType = 0
|
||||
case usb.TransferTypeBulk:
|
||||
urbType = 3
|
||||
case usb.TransferTypeInterrupt:
|
||||
urbType = 1
|
||||
default:
|
||||
urbType = 3 // default bulk
|
||||
}
|
||||
s.epTypes[epNum] = urbType
|
||||
typeNames := map[uint8]string{0: "ISO", 1: "interrupt", 2: "control", 3: "bulk"}
|
||||
log.Printf("[usbip-server] endpoint %d (0x%02x): %s", epNum, ep.Address, typeNames[urbType])
|
||||
record(ep)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getURBType returns the usbdevfs URB type for an endpoint number
|
||||
func (s *Server) getURBType(endpoint uint8) uint8 {
|
||||
if endpoint == 0 {
|
||||
return 2 // control
|
||||
// getURBType returns the usbdevfs URB type for a full endpoint address.
|
||||
//
|
||||
// interval and numPackets come from the incoming CMD_SUBMIT and act as a
|
||||
// fallback for endpoints missing from the descriptor map: only periodic
|
||||
// transfers carry a non-zero interval, so an unknown endpoint with one is an
|
||||
// interrupt endpoint rather than a bulk endpoint. Guessing bulk there is what
|
||||
// breaks HID devices, whose interrupt URBs the kernel then rejects.
|
||||
func (s *Server) getURBType(epAddr uint8, interval uint32, numPackets int32) uint8 {
|
||||
if epAddr&0x0F == 0 {
|
||||
return usbdevfsTypeControl
|
||||
}
|
||||
if t, ok := s.epTypes[endpoint]; ok {
|
||||
if numPackets > 0 {
|
||||
return usbdevfsTypeISO
|
||||
}
|
||||
if t, ok := s.epTypes[epAddr]; ok {
|
||||
return t
|
||||
}
|
||||
return 3 // default: bulk
|
||||
if interval > 0 {
|
||||
return usbdevfsTypeInterrupt
|
||||
}
|
||||
return usbdevfsTypeBulk
|
||||
}
|
||||
|
||||
// BuildDeviceDescriptor creates a USB/IP device descriptor from our device info
|
||||
@@ -254,25 +373,28 @@ func (s *Server) BuildInterfaceDescriptors() []InterfaceDescriptor {
|
||||
// It reads USB/IP requests from the reader, processes them, and writes responses to the writer.
|
||||
// This is the main loop for handling a connected USB/IP client.
|
||||
func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
|
||||
// Start the URB reaper goroutine
|
||||
retChan := make(chan []byte, 64)
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
retChan := make(chan []byte, 256)
|
||||
|
||||
go s.reapLoop(retChan, done)
|
||||
// The reap loop and control worker outlive this function only until
|
||||
// Detach stops them; both feed retChan.
|
||||
s.workers.Add(2)
|
||||
go s.reapLoop(retChan)
|
||||
go s.controlWorker(retChan)
|
||||
|
||||
// Forward completed URBs to the writer
|
||||
// Forward completed URBs to the writer. This goroutine belongs to the
|
||||
// connection, not to the server, so it ends when the connection does.
|
||||
connDone := make(chan struct{})
|
||||
defer close(connDone)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-retChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
case data := <-retChan:
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
case <-connDone:
|
||||
return
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -280,10 +402,9 @@ func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
|
||||
|
||||
// Read and process incoming USB/IP messages
|
||||
for {
|
||||
// Read the URB header (20 bytes basic + 28 bytes specific = 48 total)
|
||||
hdr, err := ReadURBHeader(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.ErrClosedPipe) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading URB header: %w", err)
|
||||
@@ -304,6 +425,15 @@ func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
// send queues a response, dropping it if the server is shutting down rather
|
||||
// than blocking forever on a channel nobody is draining.
|
||||
func (s *Server) send(retChan chan<- []byte, resp []byte) {
|
||||
select {
|
||||
case retChan <- resp:
|
||||
case <-s.stop:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
|
||||
body, err := ReadCmdSubmit(r)
|
||||
if err != nil {
|
||||
@@ -331,122 +461,22 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
}
|
||||
|
||||
endpoint := uint8(hdr.Endpoint)
|
||||
urbType := s.getURBType(endpoint)
|
||||
|
||||
dirStr := "OUT"
|
||||
if hdr.Direction == DirIn {
|
||||
dirStr = "IN"
|
||||
}
|
||||
|
||||
// Log all transfers for debugging
|
||||
// Control transfers go to the dedicated worker: USBDEVFS_CONTROL is a
|
||||
// blocking ioctl and running it inline would stall every later URB behind
|
||||
// a transfer that can take up to controlTimeout milliseconds.
|
||||
if endpoint == 0 {
|
||||
bmReqType := body.Setup[0]
|
||||
bReq := body.Setup[1]
|
||||
wVal := binary.LittleEndian.Uint16(body.Setup[2:4])
|
||||
wIdx := binary.LittleEndian.Uint16(body.Setup[4:6])
|
||||
wLen := binary.LittleEndian.Uint16(body.Setup[6:8])
|
||||
log.Printf("[usbip-server] CTRL %s seq=%d bmReqType=0x%02x bReq=0x%02x wVal=0x%04x wIdx=0x%04x wLen=%d bufLen=%d",
|
||||
dirStr, hdr.SeqNum, bmReqType, bReq, wVal, wIdx, wLen, body.TransferBufferLen)
|
||||
} else {
|
||||
typeNames := map[uint8]string{0: "ISO", 1: "INT", 2: "CTRL", 3: "BULK"}
|
||||
log.Printf("[usbip-server] EP%d %s seq=%d type=%s bufLen=%d numPkts=%d",
|
||||
endpoint, dirStr, hdr.SeqNum, typeNames[urbType], body.TransferBufferLen, numPackets)
|
||||
}
|
||||
|
||||
// Handle control transfers specially (endpoint 0)
|
||||
if endpoint == 0 && hdr.Direction == DirIn {
|
||||
buf := make([]byte, body.TransferBufferLen)
|
||||
n, err := s.handle.ControlTransfer(
|
||||
body.Setup[0], body.Setup[1],
|
||||
binary.LittleEndian.Uint16(body.Setup[2:4]),
|
||||
binary.LittleEndian.Uint16(body.Setup[4:6]),
|
||||
binary.LittleEndian.Uint16(body.Setup[6:8]),
|
||||
5000, buf,
|
||||
)
|
||||
var status int32
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] CTRL IN failed: %v", err)
|
||||
status = -32 // -EPIPE
|
||||
n = 0
|
||||
}
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, uint32(n), buf[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
return nil
|
||||
}
|
||||
|
||||
if endpoint == 0 && hdr.Direction == DirOut {
|
||||
bmRequestType := body.Setup[0]
|
||||
bRequest := body.Setup[1]
|
||||
wValue := binary.LittleEndian.Uint16(body.Setup[2:4])
|
||||
wIndex := binary.LittleEndian.Uint16(body.Setup[4:6])
|
||||
|
||||
var status int32
|
||||
var actualLength uint32
|
||||
|
||||
// Intercept standard USB requests that require special usbdevfs ioctls.
|
||||
// Raw control transfers via USBDEVFS_CONTROL don't update kernel state.
|
||||
switch {
|
||||
case bmRequestType == 0x01 && bRequest == 0x0B:
|
||||
// SET_INTERFACE (Standard, Interface recipient)
|
||||
// MUST use USBDEVFS_SETINTERFACE so the kernel updates endpoint state
|
||||
// and allocates bandwidth for ISO endpoints (critical for webcams).
|
||||
if err := s.handle.SetInterface(uint32(wIndex), uint32(wValue)); err != nil {
|
||||
log.Printf("[usbip-server] SET_INTERFACE(iface=%d, alt=%d) failed: %v", wIndex, wValue, err)
|
||||
status = -32 // -EPIPE
|
||||
} else {
|
||||
log.Printf("[usbip-server] SET_INTERFACE(iface=%d, alt=%d) OK", wIndex, wValue)
|
||||
}
|
||||
|
||||
case bmRequestType == 0x02 && bRequest == 0x01 && wValue == 0x0000:
|
||||
// CLEAR_FEATURE(ENDPOINT_HALT) (Standard, Endpoint recipient)
|
||||
if err := s.handle.ClearHalt(uint32(wIndex)); err != nil {
|
||||
log.Printf("[usbip-server] CLEAR_HALT(ep=0x%02x) failed: %v", wIndex, err)
|
||||
status = -32
|
||||
}
|
||||
|
||||
case bmRequestType == 0x00 && bRequest == 0x09:
|
||||
// SET_CONFIGURATION — do NOT forward to the physical device.
|
||||
// The device is already configured (we claimed interfaces during Attach).
|
||||
// Sending SET_CONFIGURATION via raw USBDEVFS_CONTROL would reset the
|
||||
// device's endpoint state without updating the kernel's internal USB
|
||||
// subsystem, breaking all subsequent SETINTERFACE and SUBMITURB calls
|
||||
// (ESRCH / EHOSTUNREACH).
|
||||
// Do NOT reset host-side data toggles either: after DisconnectClaimInterface
|
||||
// the host and device toggles are already in sync. Resetting host-side
|
||||
// toggles to DATA0 would create a mismatch (device still at its current
|
||||
// toggle), causing the first interrupt packet to be silently discarded.
|
||||
log.Printf("[usbip-server] SET_CONFIGURATION(%d) intercepted (device already configured)", wValue)
|
||||
|
||||
req := &ctrlRequest{hdr: hdr, body: body, transferBuf: transferBuf}
|
||||
select {
|
||||
case s.ctrlQueue <- req:
|
||||
case <-s.stop:
|
||||
default:
|
||||
// Generic OUT control transfer
|
||||
buf := transferBuf
|
||||
if buf == nil {
|
||||
buf = make([]byte, 0)
|
||||
}
|
||||
n, err := s.handle.ControlTransfer(
|
||||
bmRequestType, bRequest, wValue, wIndex,
|
||||
binary.LittleEndian.Uint16(body.Setup[6:8]),
|
||||
5000, buf,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] CTRL OUT seq=%d failed: %v", hdr.SeqNum, err)
|
||||
status = -32 // -EPIPE
|
||||
} else {
|
||||
actualLength = uint32(n)
|
||||
log.Printf("[usbip-server] CTRL OUT seq=%d OK actualLength=%d (bmReqType=0x%02x bReq=0x%02x wVal=0x%04x)",
|
||||
hdr.SeqNum, n, bmRequestType, bRequest, wValue)
|
||||
log.Printf("[usbip-server] control queue full, stalling on seq=%d", hdr.SeqNum)
|
||||
select {
|
||||
case s.ctrlQueue <- req:
|
||||
case <-s.stop:
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[usbip-server] CTRL OUT seq=%d → response status=%d actualLength=%d", hdr.SeqNum, status, actualLength)
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, actualLength, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -455,10 +485,11 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
ep |= 0x80
|
||||
}
|
||||
|
||||
urbType := s.getURBType(ep, body.Interval, numPackets)
|
||||
|
||||
// Handle isochronous transfers.
|
||||
// Trust the USB/IP NumberOfPackets field rather than our endpoint type map,
|
||||
// because the map is built at enumeration time (alternate setting 0) and
|
||||
// webcams only activate ISO endpoints after SET_INTERFACE to alt > 0.
|
||||
// because a webcam only activates its ISO endpoints after SET_INTERFACE.
|
||||
if numPackets > 0 {
|
||||
return s.handleISOSubmit(hdr, body, transferBuf, isoDescs, numPackets, ep, retChan)
|
||||
}
|
||||
@@ -476,12 +507,13 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
Endpoint: ep,
|
||||
Flags: 0,
|
||||
Buffer: buf,
|
||||
UserContext: uintptr(hdr.SeqNum),
|
||||
UserContext: uintptr(hdr.SeqNum),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] SubmitURB(ep=0x%02x, type=%d, len=%d) FAILED: %v", ep, urbType, len(buf), err)
|
||||
log.Printf("[usbip-server] SubmitURB(ep=0x%02x, type=%s, len=%d, interval=%d) FAILED: %v",
|
||||
ep, urbTypeName[urbType], len(buf), body.Interval, err)
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, 0, nil)
|
||||
retChan <- resp
|
||||
s.send(retChan, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -499,6 +531,123 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
return nil
|
||||
}
|
||||
|
||||
// controlWorker executes queued control transfers one at a time.
|
||||
// Endpoint 0 is a single shared pipe, so serialising is both correct and
|
||||
// what the device expects; the queue exists purely to decouple these
|
||||
// blocking ioctls from the protocol read loop.
|
||||
func (s *Server) controlWorker(retChan chan<- []byte) {
|
||||
defer s.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
return
|
||||
case req := <-s.ctrlQueue:
|
||||
resp := s.doControlTransfer(req)
|
||||
if resp != nil {
|
||||
s.send(retChan, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doControlTransfer performs one control transfer and builds its RET_SUBMIT.
|
||||
func (s *Server) doControlTransfer(req *ctrlRequest) []byte {
|
||||
hdr, body := req.hdr, req.body
|
||||
|
||||
bmRequestType := body.Setup[0]
|
||||
bRequest := body.Setup[1]
|
||||
wValue := binary.LittleEndian.Uint16(body.Setup[2:4])
|
||||
wIndex := binary.LittleEndian.Uint16(body.Setup[4:6])
|
||||
wLength := binary.LittleEndian.Uint16(body.Setup[6:8])
|
||||
|
||||
s.mu.Lock()
|
||||
handle := s.handle
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
if closed || handle == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hdr.Direction == DirIn {
|
||||
buf := make([]byte, body.TransferBufferLen)
|
||||
n, err := handle.ControlTransfer(bmRequestType, bRequest, wValue, wIndex, wLength, controlTimeout, buf)
|
||||
var status int32
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] CTRL IN seq=%d bmReqType=0x%02x bReq=0x%02x wVal=0x%04x failed: %v",
|
||||
hdr.SeqNum, bmRequestType, bRequest, wValue, err)
|
||||
status = -32 // -EPIPE
|
||||
n = 0
|
||||
}
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, uint32(n), buf[:n])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
var status int32
|
||||
var actualLength uint32
|
||||
|
||||
// Intercept standard USB requests that require special usbdevfs ioctls.
|
||||
// Raw control transfers via USBDEVFS_CONTROL don't update kernel state.
|
||||
switch {
|
||||
case bmRequestType == 0x01 && bRequest == 0x0B:
|
||||
// SET_INTERFACE (Standard, Interface recipient)
|
||||
// MUST use USBDEVFS_SETINTERFACE so the kernel updates endpoint state
|
||||
// and allocates bandwidth for ISO endpoints (critical for webcams).
|
||||
if err := handle.SetInterface(uint32(wIndex), uint32(wValue)); err != nil {
|
||||
log.Printf("[usbip-server] SET_INTERFACE(iface=%d, alt=%d) failed: %v", wIndex, wValue, err)
|
||||
status = -32 // -EPIPE
|
||||
} else {
|
||||
log.Printf("[usbip-server] SET_INTERFACE(iface=%d, alt=%d) OK", wIndex, wValue)
|
||||
}
|
||||
|
||||
case bmRequestType == 0x02 && bRequest == 0x01 && wValue == 0x0000:
|
||||
// CLEAR_FEATURE(ENDPOINT_HALT) (Standard, Endpoint recipient)
|
||||
if err := handle.ClearHalt(uint32(wIndex)); err != nil {
|
||||
log.Printf("[usbip-server] CLEAR_HALT(ep=0x%02x) failed: %v", wIndex, err)
|
||||
status = -32
|
||||
}
|
||||
|
||||
case bmRequestType == 0x00 && bRequest == 0x09:
|
||||
// SET_CONFIGURATION — do NOT forward to the physical device.
|
||||
// The device is already configured (we claimed interfaces during Attach).
|
||||
// Sending SET_CONFIGURATION via raw USBDEVFS_CONTROL would reset the
|
||||
// device's endpoint state without updating the kernel's internal USB
|
||||
// subsystem, breaking all subsequent SETINTERFACE and SUBMITURB calls
|
||||
// (ESRCH / EHOSTUNREACH).
|
||||
// Do NOT reset host-side data toggles either: after DisconnectClaimInterface
|
||||
// the host and device toggles are already in sync. Resetting host-side
|
||||
// toggles to DATA0 would create a mismatch (device still at its current
|
||||
// toggle), causing the first interrupt packet to be silently discarded.
|
||||
log.Printf("[usbip-server] SET_CONFIGURATION(%d) intercepted (device already configured)", wValue)
|
||||
|
||||
default:
|
||||
// Generic OUT control transfer
|
||||
buf := req.transferBuf
|
||||
if buf == nil {
|
||||
buf = make([]byte, 0)
|
||||
}
|
||||
n, err := handle.ControlTransfer(bmRequestType, bRequest, wValue, wIndex, wLength, controlTimeout, buf)
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] CTRL OUT seq=%d bmReqType=0x%02x bReq=0x%02x wVal=0x%04x failed: %v",
|
||||
hdr.SeqNum, bmRequestType, bRequest, wValue, err)
|
||||
status = -32 // -EPIPE
|
||||
} else {
|
||||
// actualLength must be reported for OUT transfers too: the kernel
|
||||
// UVC driver checks it (a VS_PROBE SET_CUR expects 26).
|
||||
actualLength = uint32(n)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, actualLength, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// handleISOSubmit handles isochronous URB submission
|
||||
func (s *Server) handleISOSubmit(hdr *URBHeader, body *CmdSubmitBody, transferBuf []byte,
|
||||
isoDescs []ISOPacketDescriptor, numPackets int32, ep uint8, retChan chan<- []byte) error {
|
||||
@@ -532,23 +681,19 @@ func (s *Server) handleISOSubmit(hdr *URBHeader, body *CmdSubmitBody, transferBu
|
||||
}
|
||||
}
|
||||
|
||||
// Submit ISO URB
|
||||
log.Printf("[usbip-server] ISO submit: ep=0x%02x dir=%d pkts=%d totalBuf=%d",
|
||||
ep, hdr.Direction, numPackets, totalBufLen)
|
||||
|
||||
urb, isoMem, err := s.handle.SubmitISOURB(&usb.SubmitISOURBParams{
|
||||
Endpoint: ep,
|
||||
Flags: 0x02, // URB_ISO_ASAP
|
||||
Buffer: buf,
|
||||
NumberOfPackets: numPackets,
|
||||
NumberOfPackets: numPackets,
|
||||
PacketLengths: packetLens,
|
||||
UserContext: uintptr(hdr.SeqNum),
|
||||
UserContext: uintptr(hdr.SeqNum),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[usbip-server] ISO submit FAILED: %v", err)
|
||||
// Submit failed - send error response
|
||||
log.Printf("[usbip-server] ISO submit FAILED (ep=0x%02x pkts=%d buf=%d): %v",
|
||||
ep, numPackets, totalBufLen, err)
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, 0, nil)
|
||||
retChan <- resp
|
||||
s.send(retChan, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -576,18 +721,22 @@ func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[usbip-server] UNLINK seq=%d target_seq=%d", hdr.SeqNum, body.UnlinkSeqNum)
|
||||
|
||||
s.mu.Lock()
|
||||
pending, exists := s.pendingURBs[body.UnlinkSeqNum]
|
||||
if exists {
|
||||
delete(s.pendingURBs, body.UnlinkSeqNum)
|
||||
// Keep the URB and its buffer reachable until the kernel hands it
|
||||
// back through the reap loop; discarding is asynchronous.
|
||||
s.unlinkedURBs[body.UnlinkSeqNum] = pending
|
||||
}
|
||||
handle := s.handle
|
||||
s.mu.Unlock()
|
||||
|
||||
// -ECONNRESET tells the client the URB was actually cancelled; 0 means it
|
||||
// had already completed, which is also a valid outcome.
|
||||
var status int32
|
||||
if exists && pending.urbPtr != nil {
|
||||
if err := s.handle.DiscardURBByPtr(pending.urbPtr); err == nil {
|
||||
if exists && pending.urbPtr != nil && handle != nil {
|
||||
if err := handle.DiscardURBByPtr(pending.urbPtr); err == nil {
|
||||
status = -104 // -ECONNRESET
|
||||
}
|
||||
}
|
||||
@@ -596,37 +745,61 @@ func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []b
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
s.send(retChan, resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reapLoop continuously reaps completed URBs and sends responses
|
||||
func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) {
|
||||
// reapLoop collects completed URBs and turns them into RET_SUBMIT responses.
|
||||
func (s *Server) reapLoop(retChan chan<- []byte) {
|
||||
defer s.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
case <-s.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if s.closed || s.handle == nil {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
// Save handle reference under lock to prevent nil deref race
|
||||
closed := s.closed
|
||||
handle := s.handle
|
||||
s.mu.Unlock()
|
||||
if closed || handle == nil {
|
||||
return
|
||||
}
|
||||
|
||||
urbInfo, err := handle.ReapURBInfo()
|
||||
// Wait for a completion rather than spinning on a non-blocking reap.
|
||||
ready, err := handle.WaitForURB(reapPollInterval)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
if errors.Is(err, usb.ErrDeviceGone) {
|
||||
log.Printf("[usbip-server] device gone, stopping reap loop")
|
||||
return
|
||||
default:
|
||||
}
|
||||
log.Printf("[usbip-server] reap poll error: %v", err)
|
||||
// Back off so a persistent poll error cannot become a busy loop.
|
||||
select {
|
||||
case <-time.After(reapPollInterval):
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ready {
|
||||
continue // timeout, re-check shutdown
|
||||
}
|
||||
|
||||
urbInfo, err := handle.ReapURBInfoNonBlock()
|
||||
if err != nil {
|
||||
if errors.Is(err, usb.ErrNoURBReady) {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, usb.ErrDeviceGone) {
|
||||
log.Printf("[usbip-server] device gone, stopping reap loop")
|
||||
return
|
||||
}
|
||||
log.Printf("[usbip-server] reap error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
seqNum := uint32(urbInfo.UserContext)
|
||||
@@ -635,34 +808,24 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) {
|
||||
pending, exists := s.pendingURBs[seqNum]
|
||||
if exists {
|
||||
delete(s.pendingURBs, seqNum)
|
||||
} else if _, wasUnlinked := s.unlinkedURBs[seqNum]; wasUnlinked {
|
||||
// The kernel is done with it; the memory may now be reclaimed.
|
||||
delete(s.unlinkedURBs, seqNum)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
// Already unlinked; the client is not expecting a RET_SUBMIT.
|
||||
continue
|
||||
}
|
||||
|
||||
dirStr := "OUT"
|
||||
if pending.direction == DirIn {
|
||||
dirStr = "IN"
|
||||
}
|
||||
urbType := s.getURBType(uint8(pending.endpoint))
|
||||
typeNames := map[uint8]string{0: "ISO", 1: "INT", 2: "CTRL", 3: "BULK"}
|
||||
|
||||
if urbInfo.Status != 0 {
|
||||
log.Printf("[usbip-server] URB completed: seq=%d EP%d %s type=%s status=%d actual=%d",
|
||||
pending.seqNum, pending.endpoint, dirStr, typeNames[urbType], urbInfo.Status, urbInfo.ActualLength)
|
||||
} else if urbType == 1 { // interrupt — always log for HID debugging
|
||||
hexStr := ""
|
||||
if pending.direction == DirIn && urbInfo.ActualLength > 0 {
|
||||
n := int(urbInfo.ActualLength)
|
||||
if n > 16 {
|
||||
n = 16
|
||||
}
|
||||
hexStr = fmt.Sprintf(" data=%x", pending.buffer[:n])
|
||||
dirStr := "OUT"
|
||||
if pending.direction == DirIn {
|
||||
dirStr = "IN"
|
||||
}
|
||||
log.Printf("[usbip-server] INT completed: seq=%d EP%d %s actual=%d%s",
|
||||
pending.seqNum, pending.endpoint, dirStr, urbInfo.ActualLength, hexStr)
|
||||
log.Printf("[usbip-server] URB error: seq=%d EP%d %s status=%d actual=%d",
|
||||
pending.seqNum, pending.endpoint, dirStr, urbInfo.Status, urbInfo.ActualLength)
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
@@ -671,7 +834,11 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) {
|
||||
} else {
|
||||
var data []byte
|
||||
if pending.direction == DirIn && urbInfo.ActualLength > 0 {
|
||||
data = pending.buffer[:urbInfo.ActualLength]
|
||||
n := int(urbInfo.ActualLength)
|
||||
if n > len(pending.buffer) {
|
||||
n = len(pending.buffer)
|
||||
}
|
||||
data = pending.buffer[:n]
|
||||
}
|
||||
resp, err = BuildRetSubmit(
|
||||
pending.seqNum,
|
||||
@@ -689,7 +856,7 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) {
|
||||
|
||||
select {
|
||||
case retChan <- resp:
|
||||
case <-done:
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -712,7 +879,7 @@ func (s *Server) buildISOResponse(urbInfo *usb.ReapedURBInfo, pending *pendingUR
|
||||
|
||||
usbipDescs = append(usbipDescs, ISOPacketDescriptor{
|
||||
Offset: bufOffset,
|
||||
Length: pktLen,
|
||||
Length: pktLen,
|
||||
ActualLength: actualLen,
|
||||
Status: status,
|
||||
})
|
||||
@@ -723,7 +890,9 @@ func (s *Server) buildISOResponse(urbInfo *usb.ReapedURBInfo, pending *pendingUR
|
||||
if end > uint32(len(pending.buffer)) {
|
||||
end = uint32(len(pending.buffer))
|
||||
}
|
||||
packedData = append(packedData, pending.buffer[bufOffset:end]...)
|
||||
if bufOffset < end {
|
||||
packedData = append(packedData, pending.buffer[bufOffset:end]...)
|
||||
}
|
||||
}
|
||||
|
||||
bufOffset += pktLen
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build darwin
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/duffy/usb-server/internal/usb"
|
||||
)
|
||||
|
||||
// Sharing needs a way to submit URBs to a physical device, which macOS only
|
||||
// offers through IOKit. Until that backend exists the server is a stub, so
|
||||
// that the rest of the client still builds and runs here.
|
||||
|
||||
type Server struct{}
|
||||
|
||||
func NewServer(dev *usb.Device) *Server { return &Server{} }
|
||||
|
||||
func (s *Server) Attach() error {
|
||||
return fmt.Errorf("sharing USB devices is not implemented on macOS (needs an IOKit backend)")
|
||||
}
|
||||
|
||||
func (s *Server) Detach() {}
|
||||
|
||||
func (s *Server) BuildDeviceDescriptor() DeviceDescriptor { return DeviceDescriptor{} }
|
||||
|
||||
func (s *Server) BuildInterfaceDescriptors() []InterfaceDescriptor { return nil }
|
||||
|
||||
func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
|
||||
return fmt.Errorf("sharing USB devices is not implemented on macOS")
|
||||
}
|
||||
|
||||
func (s *Server) HandleDevlistRequest() ([]byte, error) {
|
||||
return nil, fmt.Errorf("sharing USB devices is not implemented on macOS")
|
||||
}
|
||||
|
||||
func (s *Server) HandleImportRequest(requestedBusID string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("sharing USB devices is not implemented on macOS")
|
||||
}
|
||||
|
||||
func (s *Server) ReadManagementRequest(r io.Reader) ([]byte, bool, error) {
|
||||
return nil, false, fmt.Errorf("sharing USB devices is not implemented on macOS")
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build linux
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/duffy/usb-server/internal/usb"
|
||||
)
|
||||
|
||||
func newTestServer(eps map[uint8]usb.Endpoint) *Server {
|
||||
dev := &usb.Device{
|
||||
BusID: "1-1",
|
||||
Endpoints: eps,
|
||||
}
|
||||
s := NewServer(dev)
|
||||
s.buildEndpointTypeMap()
|
||||
return s
|
||||
}
|
||||
|
||||
// The composite case that broke HID: endpoint number 1 exists as bulk OUT
|
||||
// (0x01) and interrupt IN (0x81). Both must keep their own transfer type.
|
||||
func TestGetURBTypeSeparatesDirections(t *testing.T) {
|
||||
s := newTestServer(map[uint8]usb.Endpoint{
|
||||
0x01: {Address: 0x01, TransferType: usb.TransferTypeBulk},
|
||||
0x81: {Address: 0x81, TransferType: usb.TransferTypeInterrupt, Interval: 10},
|
||||
0x82: {Address: 0x82, TransferType: usb.TransferTypeIsochronous, Interval: 1},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
epAddr uint8
|
||||
interval uint32
|
||||
packets int32
|
||||
want uint8
|
||||
}{
|
||||
{"bulk OUT endpoint 1", 0x01, 0, 0, usbdevfsTypeBulk},
|
||||
{"interrupt IN endpoint 1", 0x81, 10, 0, usbdevfsTypeInterrupt},
|
||||
{"isochronous IN endpoint 2", 0x82, 1, 8, usbdevfsTypeISO},
|
||||
{"control endpoint 0", 0x00, 0, 0, usbdevfsTypeControl},
|
||||
{"control endpoint 0 IN", 0x80, 0, 0, usbdevfsTypeControl},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := s.getURBType(tt.epAddr, tt.interval, tt.packets)
|
||||
if got != tt.want {
|
||||
t.Errorf("getURBType(0x%02x, interval=%d, packets=%d) = %s, want %s",
|
||||
tt.epAddr, tt.interval, tt.packets, urbTypeName[got], urbTypeName[tt.want])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An endpoint missing from the descriptor map must not default to bulk when
|
||||
// the request carries an interval: only periodic transfers have one, and
|
||||
// submitting an interrupt endpoint's URB as bulk is what the kernel rejects.
|
||||
func TestGetURBTypeFallsBackOnInterval(t *testing.T) {
|
||||
s := newTestServer(nil)
|
||||
|
||||
if got := s.getURBType(0x83, 8, 0); got != usbdevfsTypeInterrupt {
|
||||
t.Errorf("unknown endpoint with interval=8: got %s, want INT", urbTypeName[got])
|
||||
}
|
||||
if got := s.getURBType(0x02, 0, 0); got != usbdevfsTypeBulk {
|
||||
t.Errorf("unknown endpoint with interval=0: got %s, want BULK", urbTypeName[got])
|
||||
}
|
||||
}
|
||||
|
||||
// NumberOfPackets is authoritative for isochronous transfers: a webcam only
|
||||
// activates its ISO endpoints after SET_INTERFACE, so the descriptor map may
|
||||
// still describe the alternate-setting-0 view when the request arrives.
|
||||
func TestGetURBTypeISOWinsOverMap(t *testing.T) {
|
||||
s := newTestServer(map[uint8]usb.Endpoint{
|
||||
0x81: {Address: 0x81, TransferType: usb.TransferTypeBulk},
|
||||
})
|
||||
|
||||
if got := s.getURBType(0x81, 1, 16); got != usbdevfsTypeISO {
|
||||
t.Errorf("packets=16 should force ISO, got %s", urbTypeName[got])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEndpointTypeMapFallsBackToInterfaces(t *testing.T) {
|
||||
dev := &usb.Device{
|
||||
BusID: "1-1",
|
||||
Interfaces: []usb.Interface{{
|
||||
Number: 0,
|
||||
Class: 0x03,
|
||||
Endpoints: []usb.Endpoint{
|
||||
{Address: 0x81, TransferType: usb.TransferTypeInterrupt, Interval: 10},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
s := NewServer(dev)
|
||||
s.buildEndpointTypeMap()
|
||||
|
||||
if got := s.getURBType(0x81, 10, 0); got != usbdevfsTypeInterrupt {
|
||||
t.Errorf("sysfs fallback lost the interrupt type: got %s", urbTypeName[got])
|
||||
}
|
||||
}
|
||||
@@ -3,45 +3,467 @@
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/duffy/usb-server/internal/usb"
|
||||
)
|
||||
|
||||
// Server is a stub on Windows - USB/IP server requires Linux usbdevfs.
|
||||
type Server struct{}
|
||||
// USB/IP server for Windows, driving devices through the usbshare filter
|
||||
// driver (driver/windows).
|
||||
//
|
||||
// The shape differs from the Linux server because the interfaces differ:
|
||||
// usbdevfs submits asynchronously and hands completions back through a reap
|
||||
// loop, whereas the filter driver's IOCTL blocks until the transfer finishes.
|
||||
// Concurrency therefore comes from a pool of workers rather than from one
|
||||
// reaper.
|
||||
//
|
||||
// UNTESTED: this depends on the filter driver, which has never been built or
|
||||
// run. Treat it as a starting point, not as working code.
|
||||
|
||||
// transferWorkers bounds how many transfers are in flight at once. USB/IP
|
||||
// clients keep several outstanding, and serialising them would stall the
|
||||
// device on every round trip.
|
||||
const transferWorkers = 8
|
||||
|
||||
// Server handles USB/IP protocol on the share side.
|
||||
type Server struct {
|
||||
device *usb.Device
|
||||
handle *usb.DriverHandle
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
pending map[uint32]uint64 // USB/IP seqnum -> driver transfer ID
|
||||
|
||||
epTypes map[uint8]uint8
|
||||
|
||||
work chan *transferJob
|
||||
ctrlWork chan *transferJob
|
||||
stop chan struct{}
|
||||
workers sync.WaitGroup
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
// transferJob is one queued USB/IP request.
|
||||
type transferJob struct {
|
||||
hdr *URBHeader
|
||||
body *CmdSubmitBody
|
||||
transferBuf []byte
|
||||
retChan chan<- []byte
|
||||
}
|
||||
|
||||
// NewServer creates a USB/IP server for a specific device.
|
||||
func NewServer(dev *usb.Device) *Server {
|
||||
return &Server{}
|
||||
return &Server{
|
||||
device: dev,
|
||||
pending: make(map[uint32]uint64),
|
||||
epTypes: make(map[uint8]uint8),
|
||||
work: make(chan *transferJob, 64),
|
||||
ctrlWork: make(chan *transferJob, 64),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Attach opens the device through the filter driver and claims it.
|
||||
func (s *Server) Attach() error {
|
||||
return fmt.Errorf("USB/IP server not supported on Windows")
|
||||
}
|
||||
if s.device.DevPath == "" {
|
||||
return fmt.Errorf("device %s has no driver path; is the usbshare filter attached?", s.device.BusID)
|
||||
}
|
||||
|
||||
func (s *Server) Detach() {}
|
||||
handle, err := usb.OpenDriverDevice(s.device.DevPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("claiming %s: %w", s.device.BusID, err)
|
||||
}
|
||||
s.handle = handle
|
||||
|
||||
func (s *Server) BuildDeviceDescriptor() DeviceDescriptor {
|
||||
return DeviceDescriptor{}
|
||||
}
|
||||
|
||||
func (s *Server) BuildInterfaceDescriptors() []InterfaceDescriptor {
|
||||
s.buildEndpointTypeMap()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detach releases the device back to its class driver.
|
||||
func (s *Server) Detach() {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopOnce.Do(func() { close(s.stop) })
|
||||
s.workers.Wait()
|
||||
|
||||
if s.handle != nil {
|
||||
s.handle.Close()
|
||||
s.handle = nil
|
||||
}
|
||||
}
|
||||
|
||||
// buildEndpointTypeMap indexes endpoints by full address, including the
|
||||
// direction bit — a composite device can use the same endpoint number for an
|
||||
// interrupt IN and a bulk OUT, and conflating them breaks HID devices.
|
||||
func (s *Server) buildEndpointTypeMap() {
|
||||
for _, ep := range s.device.Endpoints {
|
||||
s.epTypes[ep.Address] = ep.TransferType
|
||||
}
|
||||
if len(s.epTypes) > 0 {
|
||||
return
|
||||
}
|
||||
for _, iface := range s.device.Interfaces {
|
||||
for _, ep := range iface.Endpoints {
|
||||
s.epTypes[ep.Address] = ep.TransferType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getTransferType maps an endpoint to a driver transfer type, falling back to
|
||||
// the request's interval: only periodic transfers carry one, so an unknown
|
||||
// endpoint with a non-zero interval is interrupt rather than bulk.
|
||||
func (s *Server) getTransferType(epAddr uint8, interval uint32) uint8 {
|
||||
if epAddr&0x0F == 0 {
|
||||
return usb.TransferTypeControl
|
||||
}
|
||||
if t, ok := s.epTypes[epAddr]; ok {
|
||||
return t
|
||||
}
|
||||
if interval > 0 {
|
||||
return usb.TransferTypeInterrupt
|
||||
}
|
||||
return usb.TransferTypeBulk
|
||||
}
|
||||
|
||||
// BuildDeviceDescriptor creates a USB/IP device descriptor.
|
||||
func (s *Server) BuildDeviceDescriptor() DeviceDescriptor {
|
||||
var desc DeviceDescriptor
|
||||
SetPath(&desc.Path, s.device.DevPath)
|
||||
SetBusID(&desc.BusID, s.device.BusID)
|
||||
desc.BusNum = s.device.BusNum
|
||||
desc.DevNum = s.device.DevNum
|
||||
desc.Speed = s.device.Speed
|
||||
desc.IDVendor = s.device.VendorID
|
||||
desc.IDProduct = s.device.ProductID
|
||||
desc.BcdDevice = s.device.BcdDevice
|
||||
desc.BDeviceClass = s.device.DeviceClass
|
||||
desc.BDeviceSubClass = s.device.DeviceSubClass
|
||||
desc.BDeviceProtocol = s.device.DeviceProtocol
|
||||
desc.BConfigurationValue = s.device.ConfigValue
|
||||
desc.BNumConfigurations = s.device.NumConfigs
|
||||
desc.BNumInterfaces = uint8(len(s.device.Interfaces))
|
||||
return desc
|
||||
}
|
||||
|
||||
// BuildInterfaceDescriptors creates USB/IP interface descriptors.
|
||||
func (s *Server) BuildInterfaceDescriptors() []InterfaceDescriptor {
|
||||
var descs []InterfaceDescriptor
|
||||
for _, iface := range s.device.Interfaces {
|
||||
descs = append(descs, InterfaceDescriptor{
|
||||
BInterfaceClass: iface.Class,
|
||||
BInterfaceSubClass: iface.SubClass,
|
||||
BInterfaceProtocol: iface.Protocol,
|
||||
})
|
||||
}
|
||||
return descs
|
||||
}
|
||||
|
||||
// HandleConnection processes USB/IP protocol on a bidirectional stream.
|
||||
func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
|
||||
return fmt.Errorf("USB/IP server not supported on Windows")
|
||||
retChan := make(chan []byte, 256)
|
||||
|
||||
// Control transfers get their own serial worker because endpoint 0 is a
|
||||
// single shared pipe; everything else runs on a pool.
|
||||
s.workers.Add(1)
|
||||
go s.controlWorker(retChan)
|
||||
|
||||
for i := 0; i < transferWorkers; i++ {
|
||||
s.workers.Add(1)
|
||||
go s.transferWorker(retChan)
|
||||
}
|
||||
|
||||
connDone := make(chan struct{})
|
||||
defer close(connDone)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data := <-retChan:
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
case <-connDone:
|
||||
return
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
hdr, err := ReadURBHeader(r)
|
||||
if err != nil {
|
||||
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.ErrClosedPipe) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading URB header: %w", err)
|
||||
}
|
||||
|
||||
switch hdr.Command {
|
||||
case CmdSubmit:
|
||||
if err := s.handleCmdSubmit(r, hdr, retChan); err != nil {
|
||||
return fmt.Errorf("handling CMD_SUBMIT: %w", err)
|
||||
}
|
||||
case CmdUnlink:
|
||||
if err := s.handleCmdUnlink(r, hdr, retChan); err != nil {
|
||||
return fmt.Errorf("handling CMD_UNLINK: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown URB command: 0x%08x", hdr.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
|
||||
body, err := ReadCmdSubmit(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var transferBuf []byte
|
||||
if hdr.Direction == DirOut && body.TransferBufferLen > 0 {
|
||||
transferBuf = make([]byte, body.TransferBufferLen)
|
||||
if _, err := io.ReadFull(r, transferBuf); err != nil {
|
||||
return fmt.Errorf("reading transfer buffer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Isochronous transfers are not supported by the driver yet; the packet
|
||||
// descriptors still have to be consumed or the stream desynchronises.
|
||||
if body.NumberOfPackets != 0xFFFFFFFF && body.NumberOfPackets > 0 {
|
||||
descs := make([]ISOPacketDescriptor, body.NumberOfPackets)
|
||||
binary.Read(r, binary.BigEndian, &descs)
|
||||
|
||||
log.Printf("[usbip-win] isochronous transfer on EP%d rejected (not implemented)", hdr.Endpoint)
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, 0, nil)
|
||||
s.send(retChan, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
job := &transferJob{hdr: hdr, body: body, transferBuf: transferBuf, retChan: retChan}
|
||||
|
||||
queue := s.work
|
||||
if hdr.Endpoint == 0 {
|
||||
queue = s.ctrlWork
|
||||
}
|
||||
|
||||
select {
|
||||
case queue <- job:
|
||||
case <-s.stop:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) controlWorker(retChan chan<- []byte) {
|
||||
defer s.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
return
|
||||
case job := <-s.ctrlWork:
|
||||
s.runTransfer(job, retChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) transferWorker(retChan chan<- []byte) {
|
||||
defer s.workers.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
return
|
||||
case job := <-s.work:
|
||||
s.runTransfer(job, retChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runTransfer performs one transfer and emits its RET_SUBMIT.
|
||||
func (s *Server) runTransfer(job *transferJob, retChan chan<- []byte) {
|
||||
hdr, body := job.hdr, job.body
|
||||
|
||||
s.mu.Lock()
|
||||
handle := s.handle
|
||||
closed := s.closed
|
||||
s.mu.Unlock()
|
||||
|
||||
if closed || handle == nil {
|
||||
return
|
||||
}
|
||||
|
||||
epAddr := uint8(hdr.Endpoint)
|
||||
if hdr.Direction == DirIn {
|
||||
epAddr |= 0x80
|
||||
}
|
||||
|
||||
direction := uint8(0) // USBSHARE_DIR_OUT
|
||||
if hdr.Direction == DirIn {
|
||||
direction = 1
|
||||
}
|
||||
|
||||
// Intercept the standard requests that need dedicated driver calls:
|
||||
// sending them as raw control transfers changes the device without
|
||||
// telling the USB stack, after which later transfers fail.
|
||||
if hdr.Endpoint == 0 && hdr.Direction == DirOut {
|
||||
bmRequestType := body.Setup[0]
|
||||
bRequest := body.Setup[1]
|
||||
wValue := binary.LittleEndian.Uint16(body.Setup[2:4])
|
||||
wIndex := binary.LittleEndian.Uint16(body.Setup[4:6])
|
||||
|
||||
switch {
|
||||
case bmRequestType == 0x01 && bRequest == 0x0B: // SET_INTERFACE
|
||||
var status int32
|
||||
if err := handle.SetInterface(uint8(wIndex), uint8(wValue)); err != nil {
|
||||
log.Printf("[usbip-win] SET_INTERFACE(%d, %d) failed: %v", wIndex, wValue, err)
|
||||
status = -32
|
||||
}
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, 0, nil)
|
||||
s.send(retChan, resp)
|
||||
return
|
||||
|
||||
case bmRequestType == 0x02 && bRequest == 0x01 && wValue == 0x0000: // CLEAR_FEATURE(HALT)
|
||||
var status int32
|
||||
if err := handle.ClearHalt(uint8(wIndex)); err != nil {
|
||||
log.Printf("[usbip-win] CLEAR_HALT(0x%02x) failed: %v", wIndex, err)
|
||||
status = -32
|
||||
}
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, 0, nil)
|
||||
s.send(retChan, resp)
|
||||
return
|
||||
|
||||
case bmRequestType == 0x00 && bRequest == 0x09: // SET_CONFIGURATION
|
||||
// The device is already configured; forwarding this would reset
|
||||
// its endpoint state behind the stack's back.
|
||||
log.Printf("[usbip-win] SET_CONFIGURATION(%d) intercepted", wValue)
|
||||
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, 0, 0, nil)
|
||||
s.send(retChan, resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
if hdr.Direction == DirIn {
|
||||
buf = make([]byte, body.TransferBufferLen)
|
||||
} else {
|
||||
buf = job.transferBuf
|
||||
if buf == nil {
|
||||
buf = make([]byte, 0)
|
||||
}
|
||||
}
|
||||
|
||||
params := &usb.TransferParams{
|
||||
EndpointAddress: epAddr,
|
||||
Type: s.getTransferType(epAddr, body.Interval),
|
||||
Direction: direction,
|
||||
Data: buf,
|
||||
TimeoutMS: 5000,
|
||||
Setup: body.Setup,
|
||||
}
|
||||
|
||||
n, err := handle.Transfer(params)
|
||||
|
||||
var status int32
|
||||
if err != nil {
|
||||
log.Printf("[usbip-win] transfer on EP%d failed: %v", hdr.Endpoint, err)
|
||||
status = -32 // -EPIPE
|
||||
}
|
||||
|
||||
// actualLength must be reported for both directions: the kernel UVC
|
||||
// driver checks it on OUT control transfers.
|
||||
var data []byte
|
||||
if hdr.Direction == DirIn && n > 0 {
|
||||
if n > len(buf) {
|
||||
n = len(buf)
|
||||
}
|
||||
data = buf[:n]
|
||||
}
|
||||
|
||||
resp, buildErr := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint,
|
||||
status, uint32(n), data)
|
||||
if buildErr != nil {
|
||||
return
|
||||
}
|
||||
s.send(retChan, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
|
||||
body, err := ReadCmdUnlink(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cancellation is best effort here: the driver tracks transfers by its
|
||||
// own ID, and a transfer already completing cannot be recalled.
|
||||
resp, err := BuildRetUnlink(hdr.SeqNum, hdr.DevID, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.send(retChan, resp)
|
||||
|
||||
log.Printf("[usbip-win] UNLINK for seq=%d acknowledged", body.UnlinkSeqNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) send(retChan chan<- []byte, resp []byte) {
|
||||
select {
|
||||
case retChan <- resp:
|
||||
case <-s.stop:
|
||||
}
|
||||
}
|
||||
|
||||
// HandleDevlistRequest handles an OP_REQ_DEVLIST for this device.
|
||||
func (s *Server) HandleDevlistRequest() ([]byte, error) {
|
||||
return nil, fmt.Errorf("USB/IP server not supported on Windows")
|
||||
desc := s.BuildDeviceDescriptor()
|
||||
ifaceDescs := s.BuildInterfaceDescriptors()
|
||||
return BuildDevlistReply([]DeviceDescriptor{desc}, [][]InterfaceDescriptor{ifaceDescs})
|
||||
}
|
||||
|
||||
// HandleImportRequest handles an OP_REQ_IMPORT for this device.
|
||||
func (s *Server) HandleImportRequest(requestedBusID string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("USB/IP server not supported on Windows")
|
||||
if requestedBusID != s.device.BusID {
|
||||
return BuildImportReply(1, nil)
|
||||
}
|
||||
desc := s.BuildDeviceDescriptor()
|
||||
return BuildImportReply(0, &desc)
|
||||
}
|
||||
|
||||
// ReadManagementRequest reads and dispatches a management phase message.
|
||||
func (s *Server) ReadManagementRequest(r io.Reader) (response []byte, startTransfer bool, err error) {
|
||||
return nil, false, fmt.Errorf("USB/IP server not supported on Windows")
|
||||
hdr, err := ReadOpHeader(r)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
switch hdr.Command {
|
||||
case OpReqDevlist:
|
||||
resp, err := s.HandleDevlistRequest()
|
||||
return resp, false, err
|
||||
|
||||
case OpReqImport:
|
||||
var busID [32]byte
|
||||
if _, err := io.ReadFull(r, busID[:]); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
resp, err := s.HandleImportRequest(GetBusID(busID))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return resp, len(resp) > 8, nil
|
||||
|
||||
default:
|
||||
return nil, false, fmt.Errorf("unknown management command: 0x%04x", hdr.Command)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// urbHeaderSize is the fixed 20-byte USB/IP basic header.
|
||||
const urbHeaderSize = 20
|
||||
|
||||
// urbMessageSize is the basic header plus the 28-byte command/return body.
|
||||
const urbMessageSize = 48
|
||||
|
||||
// TraceRequest logs a CMD_SUBMIT or CMD_UNLINK frame travelling from the use
|
||||
// side towards the share side. Callers must gate this on protocol.Debug.
|
||||
func TraceRequest(tag string, data []byte) {
|
||||
if len(data) < urbHeaderSize {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := binary.BigEndian.Uint32(data[0:4])
|
||||
seqNum := binary.BigEndian.Uint32(data[4:8])
|
||||
dir := binary.BigEndian.Uint32(data[12:16])
|
||||
ep := binary.BigEndian.Uint32(data[16:20])
|
||||
|
||||
switch cmd {
|
||||
case CmdSubmit:
|
||||
var extra string
|
||||
if ep == 0 && len(data) >= urbMessageSize {
|
||||
// The 8-byte setup packet sits at the end of the command body.
|
||||
setup := data[urbMessageSize-8 : urbMessageSize]
|
||||
extra = fmt.Sprintf(" setup=0x%02x/0x%02x wVal=0x%04x wIdx=0x%04x wLen=%d",
|
||||
setup[0], setup[1],
|
||||
binary.LittleEndian.Uint16(setup[2:4]),
|
||||
binary.LittleEndian.Uint16(setup[4:6]),
|
||||
binary.LittleEndian.Uint16(setup[6:8]))
|
||||
}
|
||||
log.Printf("[%s] -> CMD_SUBMIT seq=%d EP%d %s%s (%d bytes)",
|
||||
tag, seqNum, ep, dirName(dir), extra, len(data))
|
||||
|
||||
case CmdUnlink:
|
||||
log.Printf("[%s] -> CMD_UNLINK seq=%d (%d bytes)", tag, seqNum, len(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TraceResponse logs a RET_SUBMIT or RET_UNLINK frame travelling from the
|
||||
// share side back to the use side. Callers must gate this on protocol.Debug.
|
||||
func TraceResponse(tag string, data []byte) {
|
||||
if len(data) < urbMessageSize {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := binary.BigEndian.Uint32(data[0:4])
|
||||
seqNum := binary.BigEndian.Uint32(data[4:8])
|
||||
dir := binary.BigEndian.Uint32(data[12:16])
|
||||
ep := binary.BigEndian.Uint32(data[16:20])
|
||||
status := int32(binary.BigEndian.Uint32(data[20:24]))
|
||||
|
||||
switch cmd {
|
||||
case RetSubmit:
|
||||
actualLen := binary.BigEndian.Uint32(data[24:28])
|
||||
var payload string
|
||||
if dir == DirIn && actualLen > 0 && len(data) > urbMessageSize {
|
||||
end := urbMessageSize + int(actualLen)
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
if end-urbMessageSize > 16 {
|
||||
end = urbMessageSize + 16
|
||||
}
|
||||
payload = fmt.Sprintf(" data=%x", data[urbMessageSize:end])
|
||||
}
|
||||
log.Printf("[%s] <- RET_SUBMIT seq=%d EP%d %s status=%d actual=%d%s",
|
||||
tag, seqNum, ep, dirName(dir), status, actualLen, payload)
|
||||
|
||||
case RetUnlink:
|
||||
log.Printf("[%s] <- RET_UNLINK seq=%d status=%d", tag, seqNum, status)
|
||||
}
|
||||
}
|
||||
|
||||
func dirName(dir uint32) string {
|
||||
if dir == DirIn {
|
||||
return "IN"
|
||||
}
|
||||
return "OUT"
|
||||
}
|
||||
@@ -14,12 +14,12 @@ const vhciBasePath = "/sys/devices/platform/vhci_hcd.0"
|
||||
|
||||
// VHCIPort represents a virtual USB port on the VHCI controller
|
||||
type VHCIPort struct {
|
||||
Hub string // "hs" or "ss"
|
||||
Port int
|
||||
Status int
|
||||
Speed int
|
||||
DevID uint32
|
||||
SocketFD int
|
||||
Hub string // "hs" or "ss"
|
||||
Port int
|
||||
Status int
|
||||
Speed int
|
||||
DevID uint32
|
||||
SocketFD int
|
||||
LocalBusID string
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build darwin
|
||||
|
||||
package usbip
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Receiving remote devices needs a virtual USB host controller. On macOS that
|
||||
// means a DriverKit driver, which needs an Apple developer identity and
|
||||
// notarisation — the same class of hurdle as signing a Windows kernel driver.
|
||||
|
||||
func IsVHCIAvailable() bool { return false }
|
||||
|
||||
func VHCIUnavailableError() error {
|
||||
return fmt.Errorf("receiving USB devices is not supported on macOS: " +
|
||||
"it needs a virtual USB host controller, for which no signed driver exists here")
|
||||
}
|
||||
|
||||
func DetachDevice(port int) error {
|
||||
return VHCIUnavailableError()
|
||||
}
|
||||
|
||||
func FindFreePort(speed uint32) (int, error) {
|
||||
return -1, VHCIUnavailableError()
|
||||
}
|
||||
|
||||
func AttachDevice(port int, sockfd int, devID uint32, speed uint32) error {
|
||||
return VHCIUnavailableError()
|
||||
}
|
||||
Reference in New Issue
Block a user