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:
+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
|
||||
|
||||
Reference in New Issue
Block a user