//go:build with_usbip && (linux || (darwin && cgo) || windows) package main import ( "cmp" "fmt" "os" "slices" "strconv" "strings" "github.com/sagernet/sing-usbip" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) func runAPIUsbipDeviceList() error { devices, err := listLocalUSBDevices() if err != nil { return err } table := tableWriter{ header: []string{"BUSID", "VID:PID", "PRODUCT", "SERIAL"}, emptyMessage: "no local USB devices", } for _, device := range devices { table.addRow( device.Entry.Info.BusIDString(), usbipVendorProduct(device.Entry.Info.IDVendor, device.Entry.Info.IDProduct), device.Entry.Product, device.Entry.Serial, ) } table.flush() return nil } func runAPIUsbipDeviceShow(selector string) error { devices, err := listLocalUSBDevices() if err != nil { return err } device, err := resolveLocalUSBDevice(devices, selector) if err != nil { return err } info := device.Entry.Info var block blockWriter block.addLine("busid", info.BusIDString()) block.addLine("stable id", device.StableID) block.addLine("backend", device.Backend.String()) if device.Entry.Product != "" { block.addLine("product", device.Entry.Product) } if device.Entry.Serial != "" { block.addLine("serial", device.Entry.Serial) } block.addLine("vendor id", fmt.Sprintf("%04x", info.IDVendor)) block.addLine("product id", fmt.Sprintf("%04x", info.IDProduct)) block.addLine("device version", fmt.Sprintf("%x.%02x", info.BCDDevice>>8, info.BCDDevice&0xff)) block.addLine("bus", F.ToString(info.BusNum)) block.addLine("device", F.ToString(info.DevNum)) block.addLine("speed", usbipSpeedString(info.Speed)) block.addLine("device class", usbipDeviceClassString(info.BDeviceClass)) block.addLine("device subclass", fmt.Sprintf("%02x", info.BDeviceSubClass)) block.addLine("device protocol", fmt.Sprintf("%02x", info.BDeviceProtocol)) if info.BNumConfigurations > 0 { block.addLine("configurations", F.ToString(info.BNumConfigurations, " (active #", info.BConfigurationValue, ")")) } for index, deviceInterface := range device.Entry.Interfaces { block.addLine(F.ToString("interface ", index), fmt.Sprintf( "class %02x subclass %02x protocol %02x", deviceInterface.BInterfaceClass, deviceInterface.BInterfaceSubClass, deviceInterface.BInterfaceProtocol, )) } block.flush() return nil } // ListLocalDevices filters hubs on linux and darwin but not on windows. func listLocalUSBDevices() ([]usbip.LocalDeviceInfo, error) { devices, err := usbip.ListLocalDevices() if err != nil { return nil, err } devices = common.Filter(devices, func(it usbip.LocalDeviceInfo) bool { return it.Entry.Info.BDeviceClass != 0x09 }) slices.SortFunc(devices, func(a usbip.LocalDeviceInfo, b usbip.LocalDeviceInfo) int { return compareBusID(a.Entry.Info.BusIDString(), b.Entry.Info.BusIDString()) }) return devices, nil } func resolveLocalUSBDevice(devices []usbip.LocalDeviceInfo, selector string) (usbip.LocalDeviceInfo, error) { index := slices.IndexFunc(devices, func(it usbip.LocalDeviceInfo) bool { return it.Entry.Info.BusIDString() == selector }) if index != -1 { return devices[index], nil } vendorID, productID, serial, valid := parseLocalUSBDeviceSelector(selector) if !valid { return usbip.LocalDeviceInfo{}, E.New("no local USB device matches ", selector) } matches := common.Filter(devices, func(it usbip.LocalDeviceInfo) bool { if it.Entry.Info.IDVendor != vendorID || it.Entry.Info.IDProduct != productID { return false } return serial == "" || it.Entry.Serial == serial }) switch len(matches) { case 0: return usbip.LocalDeviceInfo{}, E.New("no local USB device matches ", selector) case 1: return matches[0], nil } writeLocalUSBDeviceMatches(matches) return usbip.LocalDeviceInfo{}, E.New(selector, " matches ", len(matches), " devices") } func parseLocalUSBDeviceSelector(selector string) (vendorID uint16, productID uint16, serial string, valid bool) { parts := strings.SplitN(selector, ":", 3) if len(parts) < 2 { return 0, 0, "", false } vendorID, valid = parseUSBIdentifier(parts[0]) if !valid { return 0, 0, "", false } productID, valid = parseUSBIdentifier(parts[1]) if !valid { return 0, 0, "", false } if len(parts) == 3 { serial = parts[2] if serial == "" { return 0, 0, "", false } } return vendorID, productID, serial, true } func parseUSBIdentifier(value string) (uint16, bool) { if len(value) != 4 { return 0, false } parsed, err := strconv.ParseUint(value, 16, 16) if err != nil { return 0, false } return uint16(parsed), true } func writeLocalUSBDeviceMatches(matches []usbip.LocalDeviceInfo) { busIDWidth := 0 productWidth := 0 for _, device := range matches { busIDWidth = max(busIDWidth, len(device.Entry.Info.BusIDString())) productWidth = max(productWidth, len(device.Entry.Product)) } var output strings.Builder for _, device := range matches { output.WriteString(" ") output.WriteString(padUSBIPCell(device.Entry.Info.BusIDString(), busIDWidth)) output.WriteString(" ") output.WriteString(usbipVendorProduct(device.Entry.Info.IDVendor, device.Entry.Info.IDProduct)) if device.Entry.Product != "" || device.Entry.Serial != "" { output.WriteString(" ") output.WriteString(padUSBIPCell(device.Entry.Product, productWidth)) } if device.Entry.Serial != "" { output.WriteString(" (serial ") output.WriteString(device.Entry.Serial) output.WriteString(")") } output.WriteString("\n") } os.Stderr.WriteString(output.String()) } func padUSBIPCell(value string, width int) string { if len(value) >= width { return value } return value + strings.Repeat(" ", width-len(value)) } func compareBusID(a string, b string) int { for len(a) > 0 && len(b) > 0 { aDigits := usbipDigitPrefix(a) bDigits := usbipDigitPrefix(b) if aDigits > 0 && bDigits > 0 { aValue, aErr := strconv.ParseUint(a[:aDigits], 10, 64) bValue, bErr := strconv.ParseUint(b[:bDigits], 10, 64) if aErr == nil && bErr == nil { if aValue != bValue { return cmp.Compare(aValue, bValue) } a = a[aDigits:] b = b[bDigits:] continue } } if a[0] != b[0] { return cmp.Compare(a[0], b[0]) } a = a[1:] b = b[1:] } return cmp.Compare(len(a), len(b)) } func usbipDigitPrefix(value string) int { index := 0 for index < len(value) && value[index] >= '0' && value[index] <= '9' { index++ } return index } func usbipSpeedString(speed uint32) string { switch speed { case usbip.SpeedLow: return "low (1.5 Mbps)" case usbip.SpeedFull: return "full (12 Mbps)" case usbip.SpeedHigh: return "high (480 Mbps)" case usbip.SpeedSuper: return "super (5 Gbps)" case usbip.SpeedSuperPlus: return "super+ (10 Gbps)" default: return F.ToString(speed) } } var usbipDeviceClassNames = map[uint8]string{ 0x00: "defined at interface level", 0x01: "audio", 0x02: "communications", 0x03: "human interface device", 0x05: "physical", 0x06: "image", 0x07: "printer", 0x08: "mass storage", 0x09: "hub", 0x0a: "cdc data", 0x0b: "smart card", 0x0d: "content security", 0x0e: "video", 0x0f: "personal healthcare", 0x10: "audio/video", 0x11: "billboard", 0x12: "usb type-c bridge", 0xdc: "diagnostic", 0xe0: "wireless controller", 0xef: "miscellaneous", 0xfe: "application specific", 0xff: "vendor specific", } func usbipDeviceClassString(deviceClass uint8) string { name, found := usbipDeviceClassNames[deviceClass] if !found { return fmt.Sprintf("%02x", deviceClass) } return fmt.Sprintf("%02x (%s)", deviceClass, name) }