Files
usb-server/internal/usbip/server_windows.go
T
duffyduckandClaude Opus 5 9ed473a965 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>
2026-08-11 22:02:04 +02:00

470 lines
12 KiB
Go

//go:build windows
package usbip
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"sync"
"github.com/duffy/usb-server/internal/usb"
)
// USB/IP server for Windows, driving devices through the usbshare filter
// driver (driver/windows).
//
// The shape differs from the Linux server because the interfaces differ:
// usbdevfs submits asynchronously and hands completions back through a reap
// loop, whereas the filter driver's IOCTL blocks until the transfer finishes.
// Concurrency therefore comes from a pool of workers rather than from one
// reaper.
//
// UNTESTED: this depends on the filter driver, which has never been built or
// run. Treat it as a starting point, not as working code.
// transferWorkers bounds how many transfers are in flight at once. USB/IP
// clients keep several outstanding, and serialising them would stall the
// device on every round trip.
const transferWorkers = 8
// Server handles USB/IP protocol on the share side.
type Server struct {
device *usb.Device
handle *usb.DriverHandle
mu sync.Mutex
closed bool
pending map[uint32]uint64 // USB/IP seqnum -> driver transfer ID
epTypes map[uint8]uint8
work chan *transferJob
ctrlWork chan *transferJob
stop chan struct{}
workers sync.WaitGroup
stopOnce sync.Once
}
// transferJob is one queued USB/IP request.
type transferJob struct {
hdr *URBHeader
body *CmdSubmitBody
transferBuf []byte
retChan chan<- []byte
}
// NewServer creates a USB/IP server for a specific device.
func NewServer(dev *usb.Device) *Server {
return &Server{
device: dev,
pending: make(map[uint32]uint64),
epTypes: make(map[uint8]uint8),
work: make(chan *transferJob, 64),
ctrlWork: make(chan *transferJob, 64),
stop: make(chan struct{}),
}
}
// Attach opens the device through the filter driver and claims it.
func (s *Server) Attach() error {
if s.device.DevPath == "" {
return fmt.Errorf("device %s has no driver path; is the usbshare filter attached?", s.device.BusID)
}
handle, err := usb.OpenDriverDevice(s.device.DevPath)
if err != nil {
return fmt.Errorf("claiming %s: %w", s.device.BusID, err)
}
s.handle = handle
s.buildEndpointTypeMap()
return nil
}
// Detach releases the device back to its class driver.
func (s *Server) Detach() {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return
}
s.closed = true
s.mu.Unlock()
s.stopOnce.Do(func() { close(s.stop) })
s.workers.Wait()
if s.handle != nil {
s.handle.Close()
s.handle = nil
}
}
// buildEndpointTypeMap indexes endpoints by full address, including the
// direction bit — a composite device can use the same endpoint number for an
// interrupt IN and a bulk OUT, and conflating them breaks HID devices.
func (s *Server) buildEndpointTypeMap() {
for _, ep := range s.device.Endpoints {
s.epTypes[ep.Address] = ep.TransferType
}
if len(s.epTypes) > 0 {
return
}
for _, iface := range s.device.Interfaces {
for _, ep := range iface.Endpoints {
s.epTypes[ep.Address] = ep.TransferType
}
}
}
// getTransferType maps an endpoint to a driver transfer type, falling back to
// the request's interval: only periodic transfers carry one, so an unknown
// endpoint with a non-zero interval is interrupt rather than bulk.
func (s *Server) getTransferType(epAddr uint8, interval uint32) uint8 {
if epAddr&0x0F == 0 {
return usb.TransferTypeControl
}
if t, ok := s.epTypes[epAddr]; ok {
return t
}
if interval > 0 {
return usb.TransferTypeInterrupt
}
return usb.TransferTypeBulk
}
// BuildDeviceDescriptor creates a USB/IP device descriptor.
func (s *Server) BuildDeviceDescriptor() DeviceDescriptor {
var desc DeviceDescriptor
SetPath(&desc.Path, s.device.DevPath)
SetBusID(&desc.BusID, s.device.BusID)
desc.BusNum = s.device.BusNum
desc.DevNum = s.device.DevNum
desc.Speed = s.device.Speed
desc.IDVendor = s.device.VendorID
desc.IDProduct = s.device.ProductID
desc.BcdDevice = s.device.BcdDevice
desc.BDeviceClass = s.device.DeviceClass
desc.BDeviceSubClass = s.device.DeviceSubClass
desc.BDeviceProtocol = s.device.DeviceProtocol
desc.BConfigurationValue = s.device.ConfigValue
desc.BNumConfigurations = s.device.NumConfigs
desc.BNumInterfaces = uint8(len(s.device.Interfaces))
return desc
}
// BuildInterfaceDescriptors creates USB/IP interface descriptors.
func (s *Server) BuildInterfaceDescriptors() []InterfaceDescriptor {
var descs []InterfaceDescriptor
for _, iface := range s.device.Interfaces {
descs = append(descs, InterfaceDescriptor{
BInterfaceClass: iface.Class,
BInterfaceSubClass: iface.SubClass,
BInterfaceProtocol: iface.Protocol,
})
}
return descs
}
// HandleConnection processes USB/IP protocol on a bidirectional stream.
func (s *Server) HandleConnection(r io.Reader, w io.Writer) error {
retChan := make(chan []byte, 256)
// Control transfers get their own serial worker because endpoint 0 is a
// single shared pipe; everything else runs on a pool.
s.workers.Add(1)
go s.controlWorker(retChan)
for i := 0; i < transferWorkers; i++ {
s.workers.Add(1)
go s.transferWorker(retChan)
}
connDone := make(chan struct{})
defer close(connDone)
go func() {
for {
select {
case data := <-retChan:
if _, err := w.Write(data); err != nil {
return
}
case <-connDone:
return
case <-s.stop:
return
}
}
}()
for {
hdr, err := ReadURBHeader(r)
if err != nil {
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.ErrClosedPipe) {
return nil
}
return fmt.Errorf("reading URB header: %w", err)
}
switch hdr.Command {
case CmdSubmit:
if err := s.handleCmdSubmit(r, hdr, retChan); err != nil {
return fmt.Errorf("handling CMD_SUBMIT: %w", err)
}
case CmdUnlink:
if err := s.handleCmdUnlink(r, hdr, retChan); err != nil {
return fmt.Errorf("handling CMD_UNLINK: %w", err)
}
default:
return fmt.Errorf("unknown URB command: 0x%08x", hdr.Command)
}
}
}
func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
body, err := ReadCmdSubmit(r)
if err != nil {
return err
}
var transferBuf []byte
if hdr.Direction == DirOut && body.TransferBufferLen > 0 {
transferBuf = make([]byte, body.TransferBufferLen)
if _, err := io.ReadFull(r, transferBuf); err != nil {
return fmt.Errorf("reading transfer buffer: %w", err)
}
}
// Isochronous transfers are not supported by the driver yet; the packet
// descriptors still have to be consumed or the stream desynchronises.
if body.NumberOfPackets != 0xFFFFFFFF && body.NumberOfPackets > 0 {
descs := make([]ISOPacketDescriptor, body.NumberOfPackets)
binary.Read(r, binary.BigEndian, &descs)
log.Printf("[usbip-win] isochronous transfer on EP%d rejected (not implemented)", hdr.Endpoint)
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, 0, nil)
s.send(retChan, resp)
return nil
}
job := &transferJob{hdr: hdr, body: body, transferBuf: transferBuf, retChan: retChan}
queue := s.work
if hdr.Endpoint == 0 {
queue = s.ctrlWork
}
select {
case queue <- job:
case <-s.stop:
}
return nil
}
func (s *Server) controlWorker(retChan chan<- []byte) {
defer s.workers.Done()
for {
select {
case <-s.stop:
return
case job := <-s.ctrlWork:
s.runTransfer(job, retChan)
}
}
}
func (s *Server) transferWorker(retChan chan<- []byte) {
defer s.workers.Done()
for {
select {
case <-s.stop:
return
case job := <-s.work:
s.runTransfer(job, retChan)
}
}
}
// runTransfer performs one transfer and emits its RET_SUBMIT.
func (s *Server) runTransfer(job *transferJob, retChan chan<- []byte) {
hdr, body := job.hdr, job.body
s.mu.Lock()
handle := s.handle
closed := s.closed
s.mu.Unlock()
if closed || handle == nil {
return
}
epAddr := uint8(hdr.Endpoint)
if hdr.Direction == DirIn {
epAddr |= 0x80
}
direction := uint8(0) // USBSHARE_DIR_OUT
if hdr.Direction == DirIn {
direction = 1
}
// Intercept the standard requests that need dedicated driver calls:
// sending them as raw control transfers changes the device without
// telling the USB stack, after which later transfers fail.
if hdr.Endpoint == 0 && hdr.Direction == DirOut {
bmRequestType := body.Setup[0]
bRequest := body.Setup[1]
wValue := binary.LittleEndian.Uint16(body.Setup[2:4])
wIndex := binary.LittleEndian.Uint16(body.Setup[4:6])
switch {
case bmRequestType == 0x01 && bRequest == 0x0B: // SET_INTERFACE
var status int32
if err := handle.SetInterface(uint8(wIndex), uint8(wValue)); err != nil {
log.Printf("[usbip-win] SET_INTERFACE(%d, %d) failed: %v", wIndex, wValue, err)
status = -32
}
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, 0, nil)
s.send(retChan, resp)
return
case bmRequestType == 0x02 && bRequest == 0x01 && wValue == 0x0000: // CLEAR_FEATURE(HALT)
var status int32
if err := handle.ClearHalt(uint8(wIndex)); err != nil {
log.Printf("[usbip-win] CLEAR_HALT(0x%02x) failed: %v", wIndex, err)
status = -32
}
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, 0, nil)
s.send(retChan, resp)
return
case bmRequestType == 0x00 && bRequest == 0x09: // SET_CONFIGURATION
// The device is already configured; forwarding this would reset
// its endpoint state behind the stack's back.
log.Printf("[usbip-win] SET_CONFIGURATION(%d) intercepted", wValue)
resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, 0, 0, nil)
s.send(retChan, resp)
return
}
}
var buf []byte
if hdr.Direction == DirIn {
buf = make([]byte, body.TransferBufferLen)
} else {
buf = job.transferBuf
if buf == nil {
buf = make([]byte, 0)
}
}
params := &usb.TransferParams{
EndpointAddress: epAddr,
Type: s.getTransferType(epAddr, body.Interval),
Direction: direction,
Data: buf,
TimeoutMS: 5000,
Setup: body.Setup,
}
n, err := handle.Transfer(params)
var status int32
if err != nil {
log.Printf("[usbip-win] transfer on EP%d failed: %v", hdr.Endpoint, err)
status = -32 // -EPIPE
}
// actualLength must be reported for both directions: the kernel UVC
// driver checks it on OUT control transfers.
var data []byte
if hdr.Direction == DirIn && n > 0 {
if n > len(buf) {
n = len(buf)
}
data = buf[:n]
}
resp, buildErr := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint,
status, uint32(n), data)
if buildErr != nil {
return
}
s.send(retChan, resp)
}
func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []byte) error {
body, err := ReadCmdUnlink(r)
if err != nil {
return err
}
// Cancellation is best effort here: the driver tracks transfers by its
// own ID, and a transfer already completing cannot be recalled.
resp, err := BuildRetUnlink(hdr.SeqNum, hdr.DevID, 0)
if err != nil {
return err
}
s.send(retChan, resp)
log.Printf("[usbip-win] UNLINK for seq=%d acknowledged", body.UnlinkSeqNum)
return nil
}
func (s *Server) send(retChan chan<- []byte, resp []byte) {
select {
case retChan <- resp:
case <-s.stop:
}
}
// HandleDevlistRequest handles an OP_REQ_DEVLIST for this device.
func (s *Server) HandleDevlistRequest() ([]byte, error) {
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)
}
desc := s.BuildDeviceDescriptor()
return BuildImportReply(0, &desc)
}
// ReadManagementRequest reads and dispatches a management phase message.
func (s *Server) ReadManagementRequest(r io.Reader) (response []byte, startTransfer bool, err error) {
hdr, err := ReadOpHeader(r)
if err != nil {
return nil, false, err
}
switch hdr.Command {
case OpReqDevlist:
resp, err := s.HandleDevlistRequest()
return resp, false, err
case OpReqImport:
var busID [32]byte
if _, err := io.ReadFull(r, busID[:]); err != nil {
return nil, false, err
}
resp, err := s.HandleImportRequest(GetBusID(busID))
if err != nil {
return nil, false, err
}
return resp, len(resp) > 8, nil
default:
return nil, false, fmt.Errorf("unknown management command: 0x%04x", hdr.Command)
}
}