Files
usb-server/internal/usbip/protocol.go
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

418 lines
9.6 KiB
Go

package usbip
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
// Protocol version
const ProtocolVersion = 0x0111
// Management phase opcodes
const (
OpReqDevlist = 0x8005
OpRepDevlist = 0x0005
OpReqImport = 0x8003
OpRepImport = 0x0003
)
// Data transfer phase commands
const (
CmdSubmit = 0x00000001
CmdUnlink = 0x00000002
RetSubmit = 0x00000003
RetUnlink = 0x00000004
)
// Transfer directions
const (
DirOut = 0
DirIn = 1
)
// USB device speeds
const (
SpeedUnknown = 0
SpeedLow = 1
SpeedFull = 2
SpeedHigh = 3
SpeedWireless = 4
SpeedSuper = 5
SpeedSuperPlus = 6
)
// OpHeader is the 8-byte header for management messages
type OpHeader struct {
Version uint16
Command uint16
Status uint32
}
// DeviceDescriptor describes a USB device in USB/IP protocol
type DeviceDescriptor struct {
Path [256]byte
BusID [32]byte
BusNum uint32
DevNum uint32
Speed uint32
IDVendor uint16
IDProduct uint16
BcdDevice uint16
BDeviceClass uint8
BDeviceSubClass uint8
BDeviceProtocol uint8
BConfigurationValue uint8
BNumConfigurations uint8
BNumInterfaces uint8
}
// InterfaceDescriptor describes a USB interface
type InterfaceDescriptor struct {
BInterfaceClass uint8
BInterfaceSubClass uint8
BInterfaceProtocol uint8
Padding uint8
}
// URBHeader is the 48-byte common header for USB/IP transfer messages
type URBHeader struct {
Command uint32
SeqNum uint32
DevID uint32
Direction uint32
Endpoint uint32
}
// CmdSubmitBody follows URBHeader for USBIP_CMD_SUBMIT
type CmdSubmitBody struct {
TransferFlags uint32
TransferBufferLen uint32
StartFrame uint32
NumberOfPackets uint32
Interval uint32
Setup [8]byte
}
// RetSubmitBody follows URBHeader for USBIP_RET_SUBMIT
type RetSubmitBody struct {
Status int32
ActualLength uint32
StartFrame uint32
NumberOfPackets uint32
ErrorCount uint32
Padding [8]byte
}
// CmdUnlinkBody follows URBHeader for USBIP_CMD_UNLINK
type CmdUnlinkBody struct {
UnlinkSeqNum uint32
Padding [24]byte
}
// RetUnlinkBody follows URBHeader for USBIP_RET_UNLINK
type RetUnlinkBody struct {
Status int32
Padding [24]byte
}
// ISOPacketDescriptor for isochronous transfers
type ISOPacketDescriptor struct {
Offset uint32
Length uint32
ActualLength uint32
Status uint32
}
// --- Encoding/Decoding helpers ---
// WriteOpHeader writes an operation header
func WriteOpHeader(w io.Writer, cmd uint16, status uint32) error {
h := OpHeader{Version: ProtocolVersion, Command: cmd, Status: status}
return binary.Write(w, binary.BigEndian, &h)
}
// ReadOpHeader reads an operation header
func ReadOpHeader(r io.Reader) (*OpHeader, error) {
h := &OpHeader{}
if err := binary.Read(r, binary.BigEndian, h); err != nil {
return nil, err
}
return h, nil
}
// WriteDeviceDescriptor writes a device descriptor
func WriteDeviceDescriptor(w io.Writer, d *DeviceDescriptor) error {
return binary.Write(w, binary.BigEndian, d)
}
// ReadDeviceDescriptor reads a device descriptor
func ReadDeviceDescriptor(r io.Reader) (*DeviceDescriptor, error) {
d := &DeviceDescriptor{}
if err := binary.Read(r, binary.BigEndian, d); err != nil {
return nil, err
}
return d, nil
}
// WriteInterfaceDescriptor writes an interface descriptor
func WriteInterfaceDescriptor(w io.Writer, d *InterfaceDescriptor) error {
return binary.Write(w, binary.BigEndian, d)
}
// ReadURBHeader reads a URB header
func ReadURBHeader(r io.Reader) (*URBHeader, error) {
h := &URBHeader{}
if err := binary.Read(r, binary.BigEndian, h); err != nil {
return nil, err
}
return h, nil
}
// WriteURBHeader writes a URB header
func WriteURBHeader(w io.Writer, h *URBHeader) error {
return binary.Write(w, binary.BigEndian, h)
}
// ReadCmdSubmit reads a CMD_SUBMIT body (after URB header)
func ReadCmdSubmit(r io.Reader) (*CmdSubmitBody, error) {
b := &CmdSubmitBody{}
if err := binary.Read(r, binary.BigEndian, b); err != nil {
return nil, err
}
return b, nil
}
// WriteCmdSubmit writes a CMD_SUBMIT body
func WriteCmdSubmit(w io.Writer, b *CmdSubmitBody) error {
return binary.Write(w, binary.BigEndian, b)
}
// ReadRetSubmit reads a RET_SUBMIT body
func ReadRetSubmit(r io.Reader) (*RetSubmitBody, error) {
b := &RetSubmitBody{}
if err := binary.Read(r, binary.BigEndian, b); err != nil {
return nil, err
}
return b, nil
}
// WriteRetSubmit writes a RET_SUBMIT body
func WriteRetSubmit(w io.Writer, b *RetSubmitBody) error {
return binary.Write(w, binary.BigEndian, b)
}
// ReadCmdUnlink reads a CMD_UNLINK body
func ReadCmdUnlink(r io.Reader) (*CmdUnlinkBody, error) {
b := &CmdUnlinkBody{}
if err := binary.Read(r, binary.BigEndian, b); err != nil {
return nil, err
}
return b, nil
}
// WriteRetUnlink writes a RET_UNLINK body
func WriteRetUnlink(w io.Writer, b *RetUnlinkBody) error {
return binary.Write(w, binary.BigEndian, b)
}
// --- High-level message builders ---
// BuildDevlistReply builds a complete OP_REP_DEVLIST response
func BuildDevlistReply(devices []DeviceDescriptor, interfaces [][]InterfaceDescriptor) ([]byte, error) {
buf := &bytes.Buffer{}
// Header
if err := WriteOpHeader(buf, OpRepDevlist, 0); err != nil {
return nil, err
}
// Number of devices
if err := binary.Write(buf, binary.BigEndian, uint32(len(devices))); err != nil {
return nil, err
}
// Each device + its interfaces
for i, dev := range devices {
if err := WriteDeviceDescriptor(buf, &dev); err != nil {
return nil, err
}
if i < len(interfaces) {
for _, iface := range interfaces[i] {
if err := WriteInterfaceDescriptor(buf, &iface); err != nil {
return nil, err
}
}
}
}
return buf.Bytes(), nil
}
// BuildImportReply builds an OP_REP_IMPORT response
func BuildImportReply(status uint32, dev *DeviceDescriptor) ([]byte, error) {
buf := &bytes.Buffer{}
if err := WriteOpHeader(buf, OpRepImport, status); err != nil {
return nil, err
}
if status == 0 && dev != nil {
if err := WriteDeviceDescriptor(buf, dev); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// BuildRetSubmit builds a RET_SUBMIT message.
// actualLength must be set for BOTH directions: it reports how many bytes
// were actually transferred. For OUT transfers the kernel driver checks this
// (e.g. UVC probe control expects actualLength == 26).
func BuildRetSubmit(seqNum, devID, direction, endpoint uint32, status int32, actualLength uint32, data []byte) ([]byte, error) {
buf := &bytes.Buffer{}
hdr := &URBHeader{
Command: RetSubmit,
SeqNum: seqNum,
DevID: devID,
Direction: direction,
Endpoint: endpoint,
}
if err := WriteURBHeader(buf, hdr); err != nil {
return nil, err
}
body := &RetSubmitBody{
Status: status,
ActualLength: actualLength,
NumberOfPackets: 0xFFFFFFFF,
}
if err := WriteRetSubmit(buf, body); err != nil {
return nil, err
}
// Transfer buffer for IN direction
if direction == DirIn && len(data) > 0 {
buf.Write(data)
}
return buf.Bytes(), nil
}
// BuildRetSubmitISO builds a RET_SUBMIT message for isochronous transfers.
// The transfer data must already be packed (compact, no gaps).
func BuildRetSubmitISO(seqNum, devID, direction, endpoint uint32, status int32,
actualLength uint32, packedData []byte, startFrame uint32, numPackets int32, errorCount int32,
isoDescs []ISOPacketDescriptor) ([]byte, error) {
buf := &bytes.Buffer{}
hdr := &URBHeader{
Command: RetSubmit,
SeqNum: seqNum,
DevID: devID,
Direction: direction,
Endpoint: endpoint,
}
if err := WriteURBHeader(buf, hdr); err != nil {
return nil, err
}
body := &RetSubmitBody{
Status: status,
ActualLength: actualLength,
StartFrame: startFrame,
NumberOfPackets: uint32(numPackets),
ErrorCount: uint32(errorCount),
}
if err := WriteRetSubmit(buf, body); err != nil {
return nil, err
}
// Transfer buffer (packed) for IN direction
if direction == DirIn && len(packedData) > 0 {
buf.Write(packedData)
}
// ISO packet descriptors
for _, desc := range isoDescs {
if err := binary.Write(buf, binary.BigEndian, &desc); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
// BuildRetUnlink builds a RET_UNLINK message
func BuildRetUnlink(seqNum, devID uint32, status int32) ([]byte, error) {
buf := &bytes.Buffer{}
hdr := &URBHeader{
Command: RetUnlink,
SeqNum: seqNum,
DevID: devID,
Direction: 0,
Endpoint: 0,
}
if err := WriteURBHeader(buf, hdr); err != nil {
return nil, err
}
body := &RetUnlinkBody{Status: status}
if err := WriteRetUnlink(buf, body); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// SetBusID sets a bus ID string in a fixed-size byte array
func SetBusID(arr *[32]byte, busID string) {
copy(arr[:], busID)
}
// SetPath sets a path string in a fixed-size byte array
func SetPath(arr *[256]byte, path string) {
copy(arr[:], path)
}
// GetBusID extracts a bus ID string from a fixed-size byte array
func GetBusID(arr [32]byte) string {
n := bytes.IndexByte(arr[:], 0)
if n < 0 {
n = 32
}
return string(arr[:n])
}
// GetPath extracts a path string from a fixed-size byte array
func GetPath(arr [256]byte) string {
n := bytes.IndexByte(arr[:], 0)
if n < 0 {
n = 256
}
return string(arr[:n])
}
// SpeedString returns a human-readable speed name
func SpeedString(speed uint32) string {
switch speed {
case SpeedLow:
return "1.5 Mbps (Low)"
case SpeedFull:
return "12 Mbps (Full)"
case SpeedHigh:
return "480 Mbps (High)"
case SpeedSuper:
return "5 Gbps (Super)"
case SpeedSuperPlus:
return "10 Gbps (Super+)"
default:
return fmt.Sprintf("Unknown (%d)", speed)
}
}