//go:build linux package client import ( "context" "fmt" "log" "net" "os" "path/filepath" "strings" "time" "github.com/duffy/usb-server/internal/protocol" "github.com/duffy/usb-server/internal/usbip" "golang.org/x/sys/unix" ) // createVHCIAttachment creates a VHCI attachment on Linux using socketpair + sysfs. // Returns the tunnel connection (our end of the socketpair), the VHCI port number, and any error. func createVHCIAttachment(_ context.Context, granted *protocol.DeviceGranted, _ *RemoteDevice) (net.Conn, int, error) { // Create a socketpair - one end for VHCI, one for our tunnel fds, err := createSocketPair() if err != nil { return nil, -1, fmt.Errorf("creating socketpair: %w", err) } vhciFD := fds[0] tunnelFD := fds[1] // Find a free VHCI port port, err := usbip.FindFreePort(granted.Speed) if err != nil { closeFDs(fds) return nil, -1, fmt.Errorf("finding free VHCI port: %w", err) } // Attach to VHCI if err := usbip.AttachDevice(port, vhciFD, granted.DevID, granted.Speed); err != nil { closeFDs(fds) return nil, -1, fmt.Errorf("VHCI attach: %w", err) } // The VHCI driver holds a kernel reference to the socket via sockfd_lookup, // so we can close our copy of the fd to avoid leaking it. unix.Close(vhciFD) // Create a net.Conn from the tunnel FD tunnelFile := fdToFile(tunnelFD, "usb-tunnel") tunnelConn, err := net.FileConn(tunnelFile) tunnelFile.Close() // FileConn dups the fd if err != nil { usbip.DetachDevice(port) return nil, -1, fmt.Errorf("creating tunnel conn: %w", err) } return tunnelConn, port, nil } // createSocketPair creates a Unix domain socket pair func createSocketPair() ([2]int, error) { fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0) if err != nil { return [2]int{}, fmt.Errorf("socketpair: %w", err) } return fds, nil } func closeFDs(fds [2]int) { unix.Close(fds[0]) unix.Close(fds[1]) } func fdToFile(fd int, name string) *os.File { return os.NewFile(uintptr(fd), name) } // logVHCIDeviceStatus reads the VHCI sysfs tree to check what happened // with a newly attached device. Logs driver binding, device class, etc. // // This is diagnostics only, so it stays behind USBSRV_DEBUG: it waits three // seconds and then walks the whole sysfs tree on every attach. func logVHCIDeviceStatus(port int) { if !protocol.Debug { return } time.Sleep(3 * time.Second) // wait for enumeration basePath := "/sys/devices/platform/vhci_hcd.0" entries, err := os.ReadDir(basePath) if err != nil { log.Printf("[use-diag] cannot read VHCI sysfs: %v", err) return } // Find the USB device for this port (usbN/N-M pattern) for _, entry := range entries { if !strings.HasPrefix(entry.Name(), "usb") { continue } usbPath := filepath.Join(basePath, entry.Name()) devEntries, err := os.ReadDir(usbPath) if err != nil { continue } for _, devEntry := range devEntries { devName := devEntry.Name() // Device dirs look like "3-1", not "3-1:1.0" if !strings.Contains(devName, "-") || strings.Contains(devName, ":") { continue } devPath := filepath.Join(usbPath, devName) // Read device info readAttr := func(name string) string { data, err := os.ReadFile(filepath.Join(devPath, name)) if err != nil { return "" } return strings.TrimSpace(string(data)) } vid := readAttr("idVendor") pid := readAttr("idProduct") product := readAttr("product") manufacturer := readAttr("manufacturer") speed := readAttr("speed") devClass := readAttr("bDeviceClass") if vid == "" { continue // not a real device } log.Printf("[use-diag] VHCI device: %s %s:%s speed=%s class=%s %s %s", devName, vid, pid, speed, devClass, manufacturer, product) // Check interfaces and their drivers ifEntries, _ := os.ReadDir(devPath) for _, ifEntry := range ifEntries { ifName := ifEntry.Name() if !strings.Contains(ifName, ":") { continue } ifPath := filepath.Join(devPath, ifName) ifClass, _ := os.ReadFile(filepath.Join(ifPath, "bInterfaceClass")) ifProto, _ := os.ReadFile(filepath.Join(ifPath, "bInterfaceProtocol")) driverLink, err := os.Readlink(filepath.Join(ifPath, "driver")) driver := "(no driver)" if err == nil { driver = filepath.Base(driverLink) } log.Printf("[use-diag] interface %s: class=%s proto=%s driver=%s", ifName, strings.TrimSpace(string(ifClass)), strings.TrimSpace(string(ifProto)), driver) // Check for input devices under this interface filepath.WalkDir(ifPath, func(path string, d os.DirEntry, err error) error { if err != nil { return nil } if strings.HasPrefix(d.Name(), "event") && strings.Contains(path, "/input/input") { log.Printf("[use-diag] → /dev/input/%s", d.Name()) } if strings.HasPrefix(d.Name(), "hidraw") && filepath.Base(filepath.Dir(path)) == "hidraw" { log.Printf("[use-diag] → /dev/%s", d.Name()) } return nil }) } } } } // fixVHCIDevicePermissions waits for the VHCI-attached device to create // device nodes (e.g. /dev/video*, /dev/input/event*, /dev/hidraw*) and sets // them to world-accessible. VHCI-created devices don't get normal udev // rules applied, so they default to root-only access. func fixVHCIDevicePermissions(port int) { // Wait for the device to finish enumerating and create device nodes. // The kernel needs time to enumerate descriptors and bind drivers. for attempt := 0; attempt < 15; attempt++ { time.Sleep(500 * time.Millisecond) found := false // Walk the VHCI sysfs tree to find device nodes at any depth. // Paths look like: vhci_hcd.0/usb3/3-1/3-1:1.0/video4linux/video0 filepath.WalkDir("/sys/devices/platform/vhci_hcd.0", func(path string, d os.DirEntry, err error) error { if err != nil { return nil } dir := filepath.Dir(path) parent := filepath.Base(dir) // video4linux devices → /dev/videoN if parent == "video4linux" && strings.HasPrefix(d.Name(), "video") { devPath := "/dev/" + d.Name() if err := os.Chmod(devPath, 0666); err == nil { log.Printf("[use] set permissions 0666 on %s", devPath) found = true } else { log.Printf("[use] chmod %s failed: %v", devPath, err) } } // sound devices → /dev/snd/* if parent == "sound" && strings.HasPrefix(d.Name(), "card") { sndDir := filepath.Join(path, "device") if _, err := os.Stat(sndDir); err == nil { filepath.WalkDir("/dev/snd", func(sndPath string, sd os.DirEntry, err error) error { if err == nil && !sd.IsDir() { os.Chmod(sndPath, 0666) } return nil }) } } // input devices → /dev/input/eventN if strings.HasPrefix(d.Name(), "event") && strings.Contains(path, "/input/input") { devPath := "/dev/input/" + d.Name() if err := os.Chmod(devPath, 0666); err == nil { log.Printf("[use] set permissions 0666 on %s", devPath) found = true } else { log.Printf("[use] chmod %s failed: %v", devPath, err) } } // hidraw devices → /dev/hidrawN if parent == "hidraw" && strings.HasPrefix(d.Name(), "hidraw") { devPath := "/dev/" + d.Name() if err := os.Chmod(devPath, 0666); err == nil { log.Printf("[use] set permissions 0666 on %s", devPath) found = true } else { log.Printf("[use] chmod %s failed: %v", devPath, err) } } return nil }) if attempt >= 2 && found { return } } log.Printf("[use] fixVHCIDevicePermissions: no device nodes found after 7.5s (port %d)", port) }