//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: " " 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 }