//go:build linux package diag import ( "fmt" "os" "strings" "github.com/duffy/usb-server/internal/usb" "golang.org/x/sys/unix" ) func isPrivileged() bool { return os.Geteuid() == 0 } func collectPlatform(r *Report) { r.System.KernelVersion = kernelVersion() checkUsbdevfs(r) checkVHCI(r) collectLinuxDevices(r) assessLinuxCapabilities(r) } func kernelVersion() string { var uname unix.Utsname if err := unix.Uname(&uname); err != nil { return "" } return fmt.Sprintf("%s %s", nullTerminated(uname.Sysname[:]), nullTerminated(uname.Release[:])) } func nullTerminated(b []byte) string { if i := strings.IndexByte(string(b), 0); i >= 0 { return string(b[:i]) } return string(b) } // checkUsbdevfs verifies that device nodes exist and are usable. // // Being able to list devices through sysfs proves nothing: sharing needs to // open the node under /dev/bus/usb, and that is where permissions bite. func checkUsbdevfs(r *Report) { if _, err := os.Stat("/dev/bus/usb"); err != nil { detail := "/dev/bus/usb is missing" fix := "check that usbcore is loaded and devtmpfs is mounted" if r.System.Container { detail += " — this is a container, so it was probably not passed through" fix = "add - /dev/bus/usb:/dev/bus/usb to the container's volumes, and run it privileged" } r.addCheck("usbdevfs device nodes", false, detail, fix) return } if _, err := os.Stat("/sys/bus/usb/devices"); err != nil { r.addCheck("usbdevfs device nodes", false, "/sys/bus/usb is not mounted, so devices cannot be enumerated", "mount sysfs, or in a container add - /sys/bus/usb:/sys/bus/usb") return } r.addCheck("usbdevfs device nodes", true, "/dev/bus/usb and /sys/bus/usb are present", "") if !isPrivileged() { r.addCheck("privileges", false, "not running as root — devices can be listed but not claimed", "run the client with sudo, or install it as a system service") } else { r.addCheck("privileges", true, "running as root", "") } } // checkVHCI verifies the kernel module needed to receive remote devices. func checkVHCI(r *Report) { if _, err := os.Stat("/sys/devices/platform/vhci_hcd.0"); err == nil { r.addCheck("vhci-hcd module", true, "loaded — remote devices can be attached", "") return } detail := "not loaded — remote devices cannot be attached" if r.System.Container { detail += " (a container cannot load modules; this must happen on the host)" } r.addCheck("vhci-hcd module", false, detail, "sudo modprobe vhci-hcd (persist with: echo vhci-hcd | sudo tee /etc/modules-load.d/vhci-hcd.conf)") } func collectLinuxDevices(r *Report) { devices, err := usb.Enumerate() if err != nil { r.note("device enumeration failed: %v", err) return } for _, dev := range devices { info := DeviceInfo{ BusID: dev.BusID, VendorID: fmt.Sprintf("%04x", dev.VendorID), ProductID: fmt.Sprintf("%04x", dev.ProductID), Name: dev.DisplayName(), Class: fmt.Sprintf("%02x", dev.DeviceClass), Speed: speedName(dev.Speed), } if len(dev.Interfaces) > 0 { info.Driver = dev.Interfaces[0].Driver } // Sharing needs write access to the node, so test exactly that. if err := unix.Access(dev.DevPath, unix.R_OK|unix.W_OK); err != nil { info.Shareable = false info.Blocker = fmt.Sprintf("no write access to %s (%v)", dev.DevPath, err) } else { info.Shareable = true } for _, ep := range dev.Endpoints { info.Endpoints = append(info.Endpoints, endpointInfo(ep)) } // An empty endpoint map means the raw descriptors could not be read, // which is what makes transfer types guesswork later. if len(dev.Endpoints) == 0 { r.note("no endpoint descriptors for %s — could not read %s; "+ "transfer types will be guessed from the request interval", dev.BusID, dev.DevPath) } r.Devices = append(r.Devices, info) } } func assessLinuxCapabilities(r *Report) { shareable := 0 for _, d := range r.Devices { if d.Shareable { shareable++ } } switch { case shareable > 0: r.Sharing = Capability{Available: true, Mechanism: "usbdevfs"} case len(r.Devices) > 0: r.Sharing = Capability{ Available: false, Reason: "devices found, but none can be opened (permissions)", Mechanism: "usbdevfs", } default: r.Sharing = Capability{ Available: false, Reason: "no USB devices found", Mechanism: "usbdevfs", } } if _, err := os.Stat("/sys/devices/platform/vhci_hcd.0"); err == nil { r.Using = Capability{Available: true, Mechanism: "vhci-hcd"} } else { r.Using = Capability{ Available: false, Reason: "vhci-hcd is not loaded", Mechanism: "vhci-hcd", } } }