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>
969 lines
29 KiB
Go
969 lines
29 KiB
Go
//go:build linux
|
|
|
|
package usbip
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"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).
|
|
type Server struct {
|
|
device *usb.Device
|
|
handle *usb.DeviceHandle
|
|
mu sync.Mutex
|
|
pendingURBs map[uint32]*pendingURB // seqnum -> pending URB
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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),
|
|
unlinkedURBs: make(map[uint32]*pendingURB),
|
|
epTypes: make(map[uint8]uint8),
|
|
ctrlQueue: make(chan *ctrlRequest, controlQueueDepth),
|
|
stop: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Attach opens the device, disconnects the kernel driver, and claims all interfaces
|
|
func (s *Server) Attach() error {
|
|
handle, err := usb.OpenDevice(s.device.DevPath, s.device.BusID)
|
|
if err != nil {
|
|
return fmt.Errorf("opening device: %w", err)
|
|
}
|
|
s.handle = handle
|
|
|
|
// Build endpoint type map from device info (must be done before driver detach)
|
|
s.buildEndpointTypeMap()
|
|
|
|
// For each interface: disconnect kernel driver and claim it
|
|
for _, iface := range s.device.Interfaces {
|
|
ifnum := uint32(iface.Number)
|
|
|
|
// Try atomic disconnect+claim first (USBDEVFS_DISCONNECT_CLAIM, Linux 3.7+)
|
|
err := handle.DisconnectClaimInterface(ifnum)
|
|
if err == nil {
|
|
log.Printf("[usbip-server] interface %d: disconnect+claim OK", ifnum)
|
|
continue
|
|
}
|
|
|
|
// Fallback: disconnect driver per interface, then claim
|
|
if disconnErr := handle.DisconnectDriverForInterface(ifnum); disconnErr != nil {
|
|
log.Printf("[usbip-server] interface %d: disconnect warning: %v", ifnum, disconnErr)
|
|
}
|
|
|
|
if claimErr := handle.ClaimInterface(ifnum); claimErr != nil {
|
|
log.Printf("[usbip-server] error: could not claim interface %d: %v", ifnum, claimErr)
|
|
// This is a critical error - clean up and fail
|
|
for _, prev := range s.device.Interfaces {
|
|
if uint32(prev.Number) < ifnum {
|
|
handle.ReleaseInterface(uint32(prev.Number))
|
|
}
|
|
}
|
|
handle.ConnectDriver()
|
|
handle.Close()
|
|
s.handle = nil
|
|
return fmt.Errorf("claiming interface %d: %w", ifnum, claimErr)
|
|
}
|
|
log.Printf("[usbip-server] interface %d: fallback disconnect+claim OK", ifnum)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 alreadyClosed || s.handle == nil {
|
|
return
|
|
}
|
|
|
|
// 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 _, pending := range s.pendingURBs {
|
|
if pending.urbPtr != nil {
|
|
s.handle.DiscardURBByPtr(pending.urbPtr)
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 4. Close the device file descriptor.
|
|
// The kernel auto-cancels remaining URBs on close.
|
|
s.handle.Close()
|
|
s.handle = nil
|
|
|
|
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
|
|
// the kernel sets needs_binding=false on release.
|
|
// The reliable solution: toggle authorized 0->1 which forces complete
|
|
// re-enumeration and driver binding.
|
|
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() {
|
|
authPath := filepath.Join(s.device.SysPath, "authorized")
|
|
|
|
// Deauthorize: kernel disconnects device, unbinds all drivers
|
|
if err := os.WriteFile(authPath, []byte("0"), 0644); err != nil {
|
|
log.Printf("[usbip-server] sysfs deauthorize failed: %v, trying fallback", err)
|
|
s.rebindDriversFallback()
|
|
return
|
|
}
|
|
|
|
// Brief delay for the kernel to process the deauthorization
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Re-authorize: kernel re-enumerates device, binds drivers
|
|
if err := os.WriteFile(authPath, []byte("1"), 0644); err != nil {
|
|
log.Printf("[usbip-server] sysfs re-authorize failed: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("[usbip-server] device re-authorized, kernel drivers re-bound")
|
|
}
|
|
|
|
// rebindDriversFallback tries alternative methods to rebind drivers
|
|
func (s *Server) rebindDriversFallback() {
|
|
// Try writing bus_id to each original driver's bind file
|
|
for _, iface := range s.device.Interfaces {
|
|
if iface.Driver == "" {
|
|
continue
|
|
}
|
|
ifaceName := fmt.Sprintf("%s:%d.%d", s.device.BusID, s.device.ConfigValue, iface.Number)
|
|
bindPath := filepath.Join("/sys/bus/usb/drivers", iface.Driver, "bind")
|
|
if err := os.WriteFile(bindPath, []byte(ifaceName), 0644); err != nil {
|
|
log.Printf("[usbip-server] bind %s to %s failed: %v", ifaceName, iface.Driver, err)
|
|
} else {
|
|
log.Printf("[usbip-server] re-bound %s to driver %s", ifaceName, iface.Driver)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
record(ep)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 numPackets > 0 {
|
|
return usbdevfsTypeISO
|
|
}
|
|
if t, ok := s.epTypes[epAddr]; ok {
|
|
return t
|
|
}
|
|
if interval > 0 {
|
|
return usbdevfsTypeInterrupt
|
|
}
|
|
return usbdevfsTypeBulk
|
|
}
|
|
|
|
// BuildDeviceDescriptor creates a USB/IP device descriptor from our device info
|
|
func (s *Server) BuildDeviceDescriptor() DeviceDescriptor {
|
|
var desc DeviceDescriptor
|
|
SetPath(&desc.Path, s.device.SysPath)
|
|
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.
|
|
// 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 {
|
|
retChan := make(chan []byte, 256)
|
|
|
|
// 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. 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 := <-retChan:
|
|
if _, err := w.Write(data); err != nil {
|
|
return
|
|
}
|
|
case <-connDone:
|
|
return
|
|
case <-s.stop:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Read and process incoming USB/IP messages
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
return err
|
|
}
|
|
|
|
// Read transfer buffer for OUT direction
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Read ISO packet descriptors if present
|
|
var isoDescs []ISOPacketDescriptor
|
|
numPackets := int32(0)
|
|
if body.NumberOfPackets != 0xFFFFFFFF && body.NumberOfPackets > 0 {
|
|
numPackets = int32(body.NumberOfPackets)
|
|
isoDescs = make([]ISOPacketDescriptor, numPackets)
|
|
if err := binary.Read(r, binary.BigEndian, &isoDescs); err != nil {
|
|
return fmt.Errorf("reading ISO descriptors: %w", err)
|
|
}
|
|
}
|
|
|
|
endpoint := uint8(hdr.Endpoint)
|
|
|
|
// 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 {
|
|
req := &ctrlRequest{hdr: hdr, body: body, transferBuf: transferBuf}
|
|
select {
|
|
case s.ctrlQueue <- req:
|
|
case <-s.stop:
|
|
default:
|
|
log.Printf("[usbip-server] control queue full, stalling on seq=%d", hdr.SeqNum)
|
|
select {
|
|
case s.ctrlQueue <- req:
|
|
case <-s.stop:
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
ep := endpoint
|
|
if hdr.Direction == DirIn {
|
|
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 a webcam only activates its ISO endpoints after SET_INTERFACE.
|
|
if numPackets > 0 {
|
|
return s.handleISOSubmit(hdr, body, transferBuf, isoDescs, numPackets, ep, retChan)
|
|
}
|
|
|
|
// Bulk and interrupt transfers
|
|
var buf []byte
|
|
if hdr.Direction == DirIn {
|
|
buf = make([]byte, body.TransferBufferLen)
|
|
} else {
|
|
buf = transferBuf
|
|
}
|
|
|
|
urb, err := s.handle.SubmitURB(&usb.SubmitURBParams{
|
|
Type: urbType,
|
|
Endpoint: ep,
|
|
Flags: 0,
|
|
Buffer: buf,
|
|
UserContext: uintptr(hdr.SeqNum),
|
|
})
|
|
if err != nil {
|
|
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)
|
|
s.send(retChan, resp)
|
|
return nil
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.pendingURBs[hdr.SeqNum] = &pendingURB{
|
|
seqNum: hdr.SeqNum,
|
|
devID: hdr.DevID,
|
|
direction: hdr.Direction,
|
|
endpoint: hdr.Endpoint,
|
|
buffer: buf,
|
|
urbPtr: unsafe.Pointer(urb),
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
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 {
|
|
|
|
// Collect packet lengths and compute total buffer size
|
|
packetLens := make([]uint32, numPackets)
|
|
var totalBufLen uint32
|
|
for i := int32(0); i < numPackets; i++ {
|
|
packetLens[i] = isoDescs[i].Length
|
|
totalBufLen += isoDescs[i].Length
|
|
}
|
|
|
|
// Prepare buffer
|
|
var buf []byte
|
|
if hdr.Direction == DirIn {
|
|
buf = make([]byte, totalBufLen)
|
|
} else {
|
|
// For OUT: the incoming data is packed (compact), expand to sequential layout
|
|
buf = make([]byte, totalBufLen)
|
|
if transferBuf != nil {
|
|
srcOff := uint32(0)
|
|
dstOff := uint32(0)
|
|
for i := int32(0); i < numPackets; i++ {
|
|
pktLen := packetLens[i]
|
|
if srcOff+pktLen <= uint32(len(transferBuf)) {
|
|
copy(buf[dstOff:dstOff+pktLen], transferBuf[srcOff:srcOff+pktLen])
|
|
}
|
|
srcOff += pktLen
|
|
dstOff += pktLen
|
|
}
|
|
}
|
|
}
|
|
|
|
urb, isoMem, err := s.handle.SubmitISOURB(&usb.SubmitISOURBParams{
|
|
Endpoint: ep,
|
|
Flags: 0x02, // URB_ISO_ASAP
|
|
Buffer: buf,
|
|
NumberOfPackets: numPackets,
|
|
PacketLengths: packetLens,
|
|
UserContext: uintptr(hdr.SeqNum),
|
|
})
|
|
if err != nil {
|
|
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)
|
|
s.send(retChan, resp)
|
|
return nil
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.pendingURBs[hdr.SeqNum] = &pendingURB{
|
|
seqNum: hdr.SeqNum,
|
|
devID: hdr.DevID,
|
|
direction: hdr.Direction,
|
|
endpoint: hdr.Endpoint,
|
|
buffer: buf,
|
|
urbPtr: unsafe.Pointer(urb),
|
|
isISO: true,
|
|
numPackets: numPackets,
|
|
isoMem: isoMem,
|
|
packetLens: packetLens,
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
|
|
body, err := ReadCmdUnlink(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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 && handle != nil {
|
|
if err := handle.DiscardURBByPtr(pending.urbPtr); err == nil {
|
|
status = -104 // -ECONNRESET
|
|
}
|
|
}
|
|
|
|
resp, err := BuildRetUnlink(hdr.SeqNum, hdr.DevID, status)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.send(retChan, resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 <-s.stop:
|
|
return
|
|
default:
|
|
}
|
|
|
|
s.mu.Lock()
|
|
closed := s.closed
|
|
handle := s.handle
|
|
s.mu.Unlock()
|
|
if closed || handle == nil {
|
|
return
|
|
}
|
|
|
|
// Wait for a completion rather than spinning on a non-blocking reap.
|
|
ready, err := handle.WaitForURB(reapPollInterval)
|
|
if err != nil {
|
|
if errors.Is(err, usb.ErrDeviceGone) {
|
|
log.Printf("[usbip-server] device gone, stopping reap loop")
|
|
return
|
|
}
|
|
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)
|
|
|
|
s.mu.Lock()
|
|
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
|
|
}
|
|
|
|
if urbInfo.Status != 0 {
|
|
dirStr := "OUT"
|
|
if pending.direction == DirIn {
|
|
dirStr = "IN"
|
|
}
|
|
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
|
|
if pending.isISO {
|
|
resp, err = s.buildISOResponse(urbInfo, pending)
|
|
} else {
|
|
var data []byte
|
|
if pending.direction == DirIn && urbInfo.ActualLength > 0 {
|
|
n := int(urbInfo.ActualLength)
|
|
if n > len(pending.buffer) {
|
|
n = len(pending.buffer)
|
|
}
|
|
data = pending.buffer[:n]
|
|
}
|
|
resp, err = BuildRetSubmit(
|
|
pending.seqNum,
|
|
pending.devID,
|
|
pending.direction,
|
|
pending.endpoint,
|
|
urbInfo.Status,
|
|
uint32(urbInfo.ActualLength),
|
|
data,
|
|
)
|
|
}
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case retChan <- resp:
|
|
case <-s.stop:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildISOResponse builds a RET_SUBMIT for a completed ISO URB
|
|
func (s *Server) buildISOResponse(urbInfo *usb.ReapedURBInfo, pending *pendingURB) ([]byte, error) {
|
|
// Read ISO packet results from the URB memory
|
|
isoResults := usb.ReadISOResults(pending.isoMem, pending.numPackets)
|
|
|
|
// Build USB/IP ISO descriptors and pack transfer data
|
|
var usbipDescs []ISOPacketDescriptor
|
|
var packedData []byte
|
|
bufOffset := uint32(0) // offset in our sequential buffer
|
|
|
|
for i := int32(0); i < pending.numPackets; i++ {
|
|
pktLen := pending.packetLens[i]
|
|
actualLen := isoResults[i].ActualLength
|
|
status := isoResults[i].Status
|
|
|
|
usbipDescs = append(usbipDescs, ISOPacketDescriptor{
|
|
Offset: bufOffset,
|
|
Length: pktLen,
|
|
ActualLength: actualLen,
|
|
Status: status,
|
|
})
|
|
|
|
// Pack actual data (compact, no gaps) for IN direction
|
|
if pending.direction == DirIn && actualLen > 0 {
|
|
end := bufOffset + actualLen
|
|
if end > uint32(len(pending.buffer)) {
|
|
end = uint32(len(pending.buffer))
|
|
}
|
|
if bufOffset < end {
|
|
packedData = append(packedData, pending.buffer[bufOffset:end]...)
|
|
}
|
|
}
|
|
|
|
bufOffset += pktLen
|
|
}
|
|
|
|
return BuildRetSubmitISO(
|
|
pending.seqNum,
|
|
pending.devID,
|
|
pending.direction,
|
|
pending.endpoint,
|
|
urbInfo.Status,
|
|
uint32(urbInfo.ActualLength),
|
|
packedData,
|
|
uint32(urbInfo.StartFrame),
|
|
pending.numPackets,
|
|
urbInfo.ErrorCount,
|
|
usbipDescs,
|
|
)
|
|
}
|
|
|
|
// HandleDevlistRequest handles an OP_REQ_DEVLIST for this device
|
|
func (s *Server) HandleDevlistRequest() ([]byte, error) {
|
|
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) {
|
|
if requestedBusID != s.device.BusID {
|
|
return BuildImportReply(1, nil) // device not found
|
|
}
|
|
desc := s.BuildDeviceDescriptor()
|
|
return BuildImportReply(0, &desc)
|
|
}
|
|
|
|
// ReadManagementRequest reads and dispatches a management phase message.
|
|
// Returns the response bytes and whether we should transition to transfer phase.
|
|
func (s *Server) ReadManagementRequest(r io.Reader) (response []byte, startTransfer bool, err error) {
|
|
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
|
|
}
|
|
reqBusID := GetBusID(busID)
|
|
resp, err := s.HandleImportRequest(reqBusID)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if import was successful (status in response)
|
|
var checkBuf bytes.Buffer
|
|
checkBuf.Write(resp)
|
|
checkHdr, _ := ReadOpHeader(&checkBuf)
|
|
if checkHdr != nil && checkHdr.Status == 0 {
|
|
return resp, true, nil // successful import -> transfer phase
|
|
}
|
|
return resp, false, nil
|
|
|
|
default:
|
|
return nil, false, fmt.Errorf("unknown management command: 0x%04x", hdr.Command)
|
|
}
|
|
}
|