first commit
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
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
|
||||
func BuildRetSubmit(seqNum, devID, direction, endpoint uint32, status int32, 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
|
||||
}
|
||||
|
||||
actualLen := uint32(0)
|
||||
if direction == DirIn && data != nil {
|
||||
actualLen = uint32(len(data))
|
||||
}
|
||||
|
||||
body := &RetSubmitBody{
|
||||
Status: status,
|
||||
ActualLength: actualLen,
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
//go:build linux
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/duffy/usb-server/internal/usb"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// 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
|
||||
closed bool
|
||||
}
|
||||
|
||||
type pendingURB struct {
|
||||
seqNum uint32
|
||||
devID uint32
|
||||
direction uint32
|
||||
endpoint uint32
|
||||
buffer []byte
|
||||
urbPtr unsafe.Pointer // pointer to submitted usbdevfs_urb
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Disconnect kernel drivers from all interfaces
|
||||
for _, iface := range s.device.Interfaces {
|
||||
if iface.Driver != "" && iface.Driver != "(none)" {
|
||||
// Try to disconnect - ignore errors for already-disconnected interfaces
|
||||
handle.DisconnectDriver()
|
||||
}
|
||||
}
|
||||
|
||||
// Claim all interfaces
|
||||
for _, iface := range s.device.Interfaces {
|
||||
if err := handle.ClaimInterface(uint32(iface.Number)); err != nil {
|
||||
log.Printf("[usbip-server] warning: could not claim interface %d: %v", iface.Number, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detach releases all interfaces, reconnects kernel driver, and closes the device
|
||||
func (s *Server) Detach() {
|
||||
s.mu.Lock()
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
|
||||
if s.handle == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Release all interfaces
|
||||
for _, iface := range s.device.Interfaces {
|
||||
s.handle.ReleaseInterface(uint32(iface.Number))
|
||||
}
|
||||
|
||||
// Reconnect kernel driver
|
||||
s.handle.ConnectDriver()
|
||||
|
||||
s.handle.Close()
|
||||
s.handle = nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Start the URB reaper goroutine
|
||||
retChan := make(chan []byte, 64)
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
go s.reapLoop(retChan, done)
|
||||
|
||||
// Forward completed URBs to the writer
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-retChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read and process incoming USB/IP messages
|
||||
for {
|
||||
// Read the URB header (20 bytes basic + 28 bytes specific = 48 total)
|
||||
hdr, err := ReadURBHeader(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
if body.NumberOfPackets != 0xFFFFFFFF && body.NumberOfPackets > 0 {
|
||||
isoDescs := make([]ISOPacketDescriptor, body.NumberOfPackets)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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],
|
||||
binary.LittleEndian.Uint16(body.Setup[2:4]),
|
||||
binary.LittleEndian.Uint16(body.Setup[4:6]),
|
||||
binary.LittleEndian.Uint16(body.Setup[6:8]),
|
||||
5000, buf,
|
||||
)
|
||||
var status int32
|
||||
if err != nil {
|
||||
status = -32 // -EPIPE
|
||||
n = 0
|
||||
}
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, buf[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
return nil
|
||||
}
|
||||
|
||||
if endpoint == 0 && hdr.Direction == DirOut {
|
||||
// Control OUT
|
||||
buf := transferBuf
|
||||
if buf == nil {
|
||||
buf = make([]byte, 0)
|
||||
}
|
||||
_, err := s.handle.ControlTransfer(
|
||||
body.Setup[0], body.Setup[1],
|
||||
binary.LittleEndian.Uint16(body.Setup[2:4]),
|
||||
binary.LittleEndian.Uint16(body.Setup[4:6]),
|
||||
binary.LittleEndian.Uint16(body.Setup[6:8]),
|
||||
5000, buf,
|
||||
)
|
||||
var status int32
|
||||
if err != nil {
|
||||
status = -32 // -EPIPE
|
||||
}
|
||||
resp, err := BuildRetSubmit(hdr.SeqNum, hdr.DevID, hdr.Direction, hdr.Endpoint, status, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
return nil
|
||||
}
|
||||
|
||||
// For non-control transfers, submit asynchronously
|
||||
var buf []byte
|
||||
if hdr.Direction == DirIn {
|
||||
buf = make([]byte, body.TransferBufferLen)
|
||||
} else {
|
||||
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),
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
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)
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(s.handle.Fd()),
|
||||
uintptr(0x8000550B), // USBDEVFS_DISCARDURB
|
||||
uintptr(pending.urbPtr))
|
||||
_ = urbForDiscard
|
||||
if errno == 0 {
|
||||
status = -104 // -ECONNRESET
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := BuildRetUnlink(hdr.SeqNum, hdr.DevID, status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retChan <- resp
|
||||
|
||||
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 {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if s.closed || s.handle == nil {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
urb, err := s.handle.ReapURB()
|
||||
if err != nil {
|
||||
// Check if we should stop
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
seqNum := uint32(urb.UserContext)
|
||||
|
||||
s.mu.Lock()
|
||||
pending, exists := s.pendingURBs[seqNum]
|
||||
if exists {
|
||||
delete(s.pendingURBs, seqNum)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if pending.direction == DirIn && urb.ActualLength > 0 {
|
||||
data = pending.buffer[:urb.ActualLength]
|
||||
}
|
||||
|
||||
resp, err := BuildRetSubmit(
|
||||
pending.seqNum,
|
||||
pending.devID,
|
||||
pending.direction,
|
||||
pending.endpoint,
|
||||
urb.Status,
|
||||
data,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case retChan <- resp:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//go:build linux
|
||||
|
||||
package usbip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const vhciBasePath = "/sys/devices/platform/vhci_hcd.0"
|
||||
|
||||
// VHCIPort represents a virtual USB port on the VHCI controller
|
||||
type VHCIPort struct {
|
||||
Hub string // "hs" or "ss"
|
||||
Port int
|
||||
Status int
|
||||
Speed int
|
||||
DevID uint32
|
||||
SocketFD int
|
||||
LocalBusID string
|
||||
}
|
||||
|
||||
// VHCI status constants
|
||||
const (
|
||||
VDevStNull = 0x04
|
||||
VDevStNotAssigned = 0x05
|
||||
VDevStUsed = 0x06
|
||||
VDevStError = 0x07
|
||||
)
|
||||
|
||||
// ReadVHCIStatus reads the current VHCI port status
|
||||
func ReadVHCIStatus() ([]VHCIPort, error) {
|
||||
// Try status file directly, then status.0, status.1, etc.
|
||||
var allPorts []VHCIPort
|
||||
|
||||
paths := []string{
|
||||
filepath.Join(vhciBasePath, "status"),
|
||||
}
|
||||
|
||||
// Check for multi-controller status files
|
||||
for i := 0; i < 16; i++ {
|
||||
p := filepath.Join(vhciBasePath, fmt.Sprintf("status.%d", i))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
paths = append(paths, p)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
ports, err := parseStatusFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
allPorts = append(allPorts, ports...)
|
||||
}
|
||||
|
||||
if len(allPorts) == 0 {
|
||||
return nil, fmt.Errorf("vhci-hcd module not loaded or no ports found (check: modprobe vhci-hcd)")
|
||||
}
|
||||
|
||||
return allPorts, nil
|
||||
}
|
||||
|
||||
func parseStatusFile(path string) ([]VHCIPort, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
var ports []VHCIPort
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Skip header lines
|
||||
if strings.HasPrefix(line, "hub") || strings.HasPrefix(line, "prt") || line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
port := VHCIPort{Hub: fields[0]}
|
||||
|
||||
if v, err := strconv.Atoi(fields[1]); err == nil {
|
||||
port.Port = v
|
||||
}
|
||||
if v, err := strconv.Atoi(fields[2]); err == nil {
|
||||
port.Status = v
|
||||
}
|
||||
if v, err := strconv.Atoi(fields[3]); err == nil {
|
||||
port.Speed = v
|
||||
}
|
||||
if v, err := strconv.ParseUint(fields[4], 16, 32); err == nil {
|
||||
port.DevID = uint32(v)
|
||||
}
|
||||
if v, err := strconv.Atoi(fields[5]); err == nil {
|
||||
port.SocketFD = v
|
||||
}
|
||||
port.LocalBusID = fields[6]
|
||||
|
||||
ports = append(ports, port)
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// FindFreePort finds an available VHCI port for the given speed
|
||||
func FindFreePort(speed uint32) (int, error) {
|
||||
ports, err := ReadVHCIStatus()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
// Determine desired hub type based on speed
|
||||
wantHub := "hs" // high-speed and below
|
||||
if speed >= SpeedSuper {
|
||||
wantHub = "ss" // super-speed
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
if port.Status == VDevStNull && port.Hub == wantHub {
|
||||
return port.Port, nil
|
||||
}
|
||||
}
|
||||
|
||||
return -1, fmt.Errorf("no free VHCI port available for hub type %s", wantHub)
|
||||
}
|
||||
|
||||
// AttachDevice writes to the VHCI attach file to create a virtual USB device.
|
||||
// sockfd must be a valid TCP socket file descriptor connected to the USB/IP server.
|
||||
func AttachDevice(port int, sockfd int, devID uint32, speed uint32) error {
|
||||
attachPath := filepath.Join(vhciBasePath, "attach")
|
||||
|
||||
// Format: "<port> <sockfd> <devid> <speed>"
|
||||
data := fmt.Sprintf("%d %d %d %d", port, sockfd, devID, speed)
|
||||
|
||||
if err := os.WriteFile(attachPath, []byte(data), 0); err != nil {
|
||||
return fmt.Errorf("writing to VHCI attach: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetachDevice writes to the VHCI detach file to remove a virtual USB device
|
||||
func DetachDevice(port int) error {
|
||||
detachPath := filepath.Join(vhciBasePath, "detach")
|
||||
|
||||
data := fmt.Sprintf("%d", port)
|
||||
|
||||
if err := os.WriteFile(detachPath, []byte(data), 0); err != nil {
|
||||
return fmt.Errorf("writing to VHCI detach: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsVHCIAvailable checks if the vhci-hcd kernel module is loaded
|
||||
func IsVHCIAvailable() bool {
|
||||
_, err := os.Stat(vhciBasePath)
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user