diff --git a/bin/usb-client b/bin/usb-client index 799c9ee..9753511 100755 Binary files a/bin/usb-client and b/bin/usb-client differ diff --git a/bin/usb-relay b/bin/usb-relay index 94a7ab8..a381a93 100755 Binary files a/bin/usb-relay and b/bin/usb-relay differ diff --git a/internal/usb/device.go b/internal/usb/device.go index 4124bb9..8ca397c 100644 --- a/internal/usb/device.go +++ b/internal/usb/device.go @@ -24,13 +24,29 @@ type Device struct { // Interface represents a USB interface type Interface struct { - Number uint8 `json:"number"` - Class uint8 `json:"class"` - SubClass uint8 `json:"sub_class"` - Protocol uint8 `json:"protocol"` - Driver string `json:"driver"` + Number uint8 `json:"number"` + Class uint8 `json:"class"` + SubClass uint8 `json:"sub_class"` + Protocol uint8 `json:"protocol"` + Driver string `json:"driver"` + Endpoints []Endpoint `json:"endpoints"` } +// Endpoint represents a USB endpoint +type Endpoint struct { + Address uint8 `json:"address"` // bEndpointAddress (bit 7=direction, bits 3:0=number) + TransferType uint8 `json:"transfer_type"` // 0=control, 1=iso, 2=bulk, 3=interrupt + MaxPacketSize uint16 `json:"max_packet_size"` +} + +// USB transfer types (from bmAttributes) +const ( + TransferTypeControl = 0 + TransferTypeIsochronous = 1 + TransferTypeBulk = 2 + TransferTypeInterrupt = 3 +) + // DevID returns the USB/IP device ID (busnum << 16 | devnum) func (d *Device) DevID() uint32 { return (d.BusNum << 16) | d.DevNum diff --git a/internal/usb/enumerate_linux.go b/internal/usb/enumerate_linux.go index b296f85..752329a 100644 --- a/internal/usb/enumerate_linux.go +++ b/internal/usb/enumerate_linux.go @@ -127,12 +127,55 @@ func readInterfaces(sysPath, busID string) []Interface { iface.Driver = filepath.Base(driverLink) } + // Read endpoints + iface.Endpoints = readEndpoints(ifacePath) + ifaces = append(ifaces, iface) } return ifaces } +func readEndpoints(ifacePath string) []Endpoint { + entries, err := os.ReadDir(ifacePath) + if err != nil { + return nil + } + + var eps []Endpoint + for _, entry := range entries { + name := entry.Name() + if !strings.HasPrefix(name, "ep_") || name == "ep_00" { + continue + } + + epPath := filepath.Join(ifacePath, name) + addr, _ := strconv.ParseUint(readString(epPath, "bEndpointAddress"), 16, 8) + + var transferType uint8 + switch readString(epPath, "type") { + case "Control": + transferType = TransferTypeControl + case "Isoc": + transferType = TransferTypeIsochronous + case "Bulk": + transferType = TransferTypeBulk + case "Interrupt": + transferType = TransferTypeInterrupt + } + + maxPkt := readUint32(epPath, "wMaxPacketSize") + + eps = append(eps, Endpoint{ + Address: uint8(addr), + TransferType: transferType, + MaxPacketSize: uint16(maxPkt), + }) + } + + return eps +} + func readString(dir, attr string) string { data, err := os.ReadFile(filepath.Join(dir, attr)) if err != nil { diff --git a/internal/usb/usbdevfs.go b/internal/usb/usbdevfs.go index 7b9c103..7d4c45d 100644 --- a/internal/usb/usbdevfs.go +++ b/internal/usb/usbdevfs.go @@ -348,6 +348,109 @@ func (h *DeviceHandle) DiscardURB(urb *usbdevfsURB) error { return nil } +// SubmitISOURBParams holds parameters for async ISO URB submission +type SubmitISOURBParams struct { + Endpoint uint8 + Flags uint32 + Buffer []byte + NumberOfPackets int32 + PacketLengths []uint32 // length of each ISO packet + UserContext uintptr +} + +// SubmitISOURB submits an asynchronous isochronous URB. +// Returns the URB pointer and the backing memory slice (must be kept alive until reap). +func (h *DeviceHandle) SubmitISOURB(params *SubmitISOURBParams) (urb *usbdevfsURB, mem []byte, err error) { + urbSize := unsafe.Sizeof(usbdevfsURB{}) + isoDescSize := unsafe.Sizeof(usbdevfsISOPacketDesc{}) + totalSize := urbSize + uintptr(params.NumberOfPackets)*isoDescSize + + mem = make([]byte, totalSize) + urb = (*usbdevfsURB)(unsafe.Pointer(&mem[0])) + + var bufPtr uintptr + if len(params.Buffer) > 0 { + bufPtr = uintptr(unsafe.Pointer(¶ms.Buffer[0])) + } + + urb.Type = urbTypeISO + urb.Endpoint = params.Endpoint + urb.Flags = params.Flags + urb.Buffer = bufPtr + urb.BufferLength = int32(len(params.Buffer)) + urb.NumberOfPackets = params.NumberOfPackets + urb.UserContext = params.UserContext + + // Fill ISO packet descriptors (immediately following the URB in memory) + for i := int32(0); i < params.NumberOfPackets; i++ { + descOffset := urbSize + uintptr(i)*isoDescSize + desc := (*usbdevfsISOPacketDesc)(unsafe.Pointer(&mem[descOffset])) + if i < int32(len(params.PacketLengths)) { + desc.Length = params.PacketLengths[i] + } + } + + _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSubmitURB, uintptr(unsafe.Pointer(urb))) + if errno != 0 { + return nil, nil, fmt.Errorf("USBDEVFS_SUBMITURB (ISO): %w", errno) + } + return urb, mem, nil +} + +// ISOPacketResult holds the result of one ISO packet after reaping +type ISOPacketResult struct { + Length uint32 + ActualLength uint32 + Status uint32 +} + +// ReadISOResults reads the ISO packet results from a reaped ISO URB's backing memory. +func ReadISOResults(mem []byte, numPackets int32) []ISOPacketResult { + urbSize := unsafe.Sizeof(usbdevfsURB{}) + isoDescSize := unsafe.Sizeof(usbdevfsISOPacketDesc{}) + + results := make([]ISOPacketResult, numPackets) + for i := int32(0); i < numPackets; i++ { + offset := urbSize + uintptr(i)*isoDescSize + if int(offset+isoDescSize) > len(mem) { + break + } + desc := (*usbdevfsISOPacketDesc)(unsafe.Pointer(&mem[offset])) + results[i] = ISOPacketResult{ + Length: desc.Length, + ActualLength: desc.ActualLength, + Status: desc.Status, + } + } + return results +} + +// ReapedURBInfo holds exported fields from a reaped URB needed for response building +type ReapedURBInfo struct { + UserContext uintptr + Status int32 + ActualLength int32 + StartFrame int32 + ErrorCount int32 +} + +// ReapURBInfo blocks until a URB completes and returns exported info +func (h *DeviceHandle) ReapURBInfo() (*ReapedURBInfo, error) { + var urbPtr uintptr + _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURB, uintptr(unsafe.Pointer(&urbPtr))) + if errno != 0 { + return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno) + } + urb := (*usbdevfsURB)(unsafe.Pointer(urbPtr)) + return &ReapedURBInfo{ + UserContext: urb.UserContext, + Status: urb.Status, + ActualLength: urb.ActualLength, + StartFrame: urb.StartFrame, + ErrorCount: urb.ErrorCount, + }, nil +} + // GetFile returns an os.File wrapping the device fd (useful for epoll/select) func (h *DeviceHandle) GetFile() *os.File { return os.NewFile(uintptr(h.fd), h.devPath) diff --git a/internal/usbip/protocol.go b/internal/usbip/protocol.go index 791e217..5ec7836 100644 --- a/internal/usbip/protocol.go +++ b/internal/usbip/protocol.go @@ -304,6 +304,56 @@ func BuildRetSubmit(seqNum, devID, direction, endpoint uint32, status int32, dat 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, + 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 + } + + actualLen := uint32(0) + if direction == DirIn && packedData != nil { + actualLen = uint32(len(packedData)) + } + + body := &RetSubmitBody{ + Status: status, + ActualLength: actualLen, + 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{} diff --git a/internal/usbip/server.go b/internal/usbip/server.go index 48f096d..3083f5a 100644 --- a/internal/usbip/server.go +++ b/internal/usbip/server.go @@ -19,20 +19,25 @@ import ( // 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 + device *usb.Device + handle *usb.DeviceHandle + mu sync.Mutex pendingURBs map[uint32]*pendingURB // seqnum -> pending URB - closed bool + closed bool + epTypes map[uint8]uint8 // endpoint number (1-15) -> usbdevfs URB type } type pendingURB struct { - seqNum uint32 - devID uint32 - direction uint32 - endpoint uint32 - buffer []byte - urbPtr unsafe.Pointer // pointer to submitted usbdevfs_urb + 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) } // NewServer creates a USB/IP server for a specific device @@ -40,6 +45,7 @@ func NewServer(dev *usb.Device) *Server { return &Server{ device: dev, pendingURBs: make(map[uint32]*pendingURB), + epTypes: make(map[uint8]uint8), } } @@ -51,6 +57,9 @@ func (s *Server) Attach() error { } 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) @@ -108,6 +117,43 @@ func (s *Server) Detach() { s.handle = nil } +// buildEndpointTypeMap builds the endpoint number -> URB type map from device descriptors +func (s *Server) buildEndpointTypeMap() { + 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]) + } + } +} + +// getURBType returns the usbdevfs URB type for an endpoint number +func (s *Server) getURBType(endpoint uint8) uint8 { + if endpoint == 0 { + return 2 // control + } + if t, ok := s.epTypes[endpoint]; ok { + return t + } + return 3 // default: bulk +} + // BuildDeviceDescriptor creates a USB/IP device descriptor from our device info func (s *Server) BuildDeviceDescriptor() DeviceDescriptor { var desc DeviceDescriptor @@ -211,26 +257,21 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b } // Read ISO packet descriptors if present + var isoDescs []ISOPacketDescriptor + numPackets := int32(0) if body.NumberOfPackets != 0xFFFFFFFF && body.NumberOfPackets > 0 { - isoDescs := make([]ISOPacketDescriptor, body.NumberOfPackets) + 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) } - // TODO: handle ISO transfers properly } - // Determine URB type from endpoint endpoint := uint8(hdr.Endpoint) - var urbType uint8 - if endpoint == 0 { - urbType = 2 // control - } else { - urbType = 3 // bulk (most common, we'll detect interrupt from endpoint descriptor later) - } + urbType := s.getURBType(endpoint) // Handle control transfers specially (endpoint 0) if endpoint == 0 && hdr.Direction == DirIn { - // Control IN: send setup packet, receive data buf := make([]byte, body.TransferBufferLen) n, err := s.handle.ControlTransfer( body.Setup[0], body.Setup[1], @@ -253,7 +294,6 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b } if endpoint == 0 && hdr.Direction == DirOut { - // Control OUT buf := transferBuf if buf == nil { buf = make([]byte, 0) @@ -277,7 +317,17 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b return nil } - // For non-control transfers, submit asynchronously + ep := endpoint + if hdr.Direction == DirIn { + ep |= 0x80 + } + + // Handle isochronous transfers + if urbType == 0 && 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) @@ -285,20 +335,14 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b buf = transferBuf } - ep := endpoint - if hdr.Direction == DirIn { - ep |= 0x80 - } - urb, err := s.handle.SubmitURB(&usb.SubmitURBParams{ - Type: urbType, - Endpoint: ep, - Flags: 0, - Buffer: buf, - UserContext: uintptr(hdr.SeqNum), + Type: urbType, + Endpoint: ep, + Flags: 0, + Buffer: buf, + UserContext: uintptr(hdr.SeqNum), }) if err != nil { - // Submit failed - send error response immediately resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, nil) retChan <- resp return nil @@ -318,6 +362,73 @@ func (s *Server) handleCmdSubmit(r io.Reader, hdr *URBHeader, retChan chan<- []b return nil } +// 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 + } + } + } + + // Submit ISO URB + 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 { + // Submit failed - send error response + resp, _ := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, -32, nil) + 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 { @@ -333,13 +444,10 @@ func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []b var status int32 if exists && pending.urbPtr != nil { - // Try to discard the URB - // Note: we cast back to the URB type for the ioctl - urbForDiscard := (*usbDevfsURBForDiscard)(pending.urbPtr) + // Try to discard the URB via proper ioctl _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(s.handle.Fd()), uintptr(0x8000550B), // USBDEVFS_DISCARDURB uintptr(pending.urbPtr)) - _ = urbForDiscard if errno == 0 { status = -104 // -ECONNRESET } @@ -354,9 +462,6 @@ func (s *Server) handleCmdUnlink(r io.Reader, hdr *URBHeader, retChan chan<- []b return nil } -// usbDevfsURBForDiscard is a placeholder to make the Go compiler happy -type usbDevfsURBForDiscard struct{} - // reapLoop continuously reaps completed URBs and sends responses func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) { for { @@ -373,9 +478,8 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) { } s.mu.Unlock() - urb, err := s.handle.ReapURB() + urbInfo, err := s.handle.ReapURBInfo() if err != nil { - // Check if we should stop select { case <-done: return @@ -384,7 +488,7 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) { } } - seqNum := uint32(urb.UserContext) + seqNum := uint32(urbInfo.UserContext) s.mu.Lock() pending, exists := s.pendingURBs[seqNum] @@ -397,19 +501,23 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) { continue } - var data []byte - if pending.direction == DirIn && urb.ActualLength > 0 { - data = pending.buffer[:urb.ActualLength] + var resp []byte + if pending.isISO { + resp, err = s.buildISOResponse(urbInfo, pending) + } else { + var data []byte + if pending.direction == DirIn && urbInfo.ActualLength > 0 { + data = pending.buffer[:urbInfo.ActualLength] + } + resp, err = BuildRetSubmit( + pending.seqNum, + pending.devID, + pending.direction, + pending.endpoint, + urbInfo.Status, + data, + ) } - - resp, err := BuildRetSubmit( - pending.seqNum, - pending.devID, - pending.direction, - pending.endpoint, - urb.Status, - data, - ) if err != nil { continue } @@ -422,6 +530,54 @@ func (s *Server) reapLoop(retChan chan<- []byte, done <-chan struct{}) { } } +// 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)) + } + packedData = append(packedData, pending.buffer[bufOffset:end]...) + } + + bufOffset += pktLen + } + + return BuildRetSubmitISO( + pending.seqNum, + pending.devID, + pending.direction, + pending.endpoint, + urbInfo.Status, + 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()