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:
2026-08-11 22:02:04 +02:00
co-authored by Claude Opus 5
parent 54178dce75
commit 9ed473a965
95 changed files with 12181 additions and 892 deletions
+394 -225
View File
@@ -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