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>
418 lines
9.6 KiB
Go
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)
|
|
}
|
|
}
|