diff --git a/bin/usb-client b/bin/usb-client index acdfd0c..0e01fa2 100755 Binary files a/bin/usb-client and b/bin/usb-client differ diff --git a/bin/usb-relay b/bin/usb-relay index fc31968..9a263dd 100755 Binary files a/bin/usb-relay and b/bin/usb-relay differ diff --git a/internal/client/socket_linux.go b/internal/client/socket_linux.go index 5807ab5..65a0769 100644 --- a/internal/client/socket_linux.go +++ b/internal/client/socket_linux.go @@ -4,7 +4,10 @@ package client import ( "fmt" + "log" "os" + "path/filepath" + "time" "golang.org/x/sys/unix" ) @@ -26,3 +29,55 @@ func closeFDs(fds [2]int) { func fdToFile(fd int, name string) *os.File { return os.NewFile(uintptr(fd), name) } + +// fixVHCIDevicePermissions waits for the VHCI-attached device to create +// device nodes (e.g. /dev/video*) 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 < 10; attempt++ { + time.Sleep(500 * time.Millisecond) + + // Find device nodes created by this VHCI port + // VHCI creates devices under /sys/devices/platform/vhci_hcd.0/usbN/ + // We look for video4linux devices + matches, _ := filepath.Glob("/sys/devices/platform/vhci_hcd.0/usb*/*/video4linux/video*") + for _, m := range matches { + devName := filepath.Base(m) + devPath := "/dev/" + devName + if _, err := os.Stat(devPath); err == nil { + if err := os.Chmod(devPath, 0666); err == nil { + log.Printf("[use] set permissions 0666 on %s", devPath) + } + } + } + + // Also check for other common device types (input, sound, etc.) + for _, subsys := range []string{"sound/card*", "input/input*/event*"} { + sysMatches, _ := filepath.Glob("/sys/devices/platform/vhci_hcd.0/usb*/*/" + subsys) + for _, m := range sysMatches { + devName := filepath.Base(m) + devPath := "/dev/" + devName + // For sound devices, also check /dev/snd/ + if _, err := os.Stat(devPath); err == nil { + os.Chmod(devPath, 0666) + } + devPath = "/dev/snd/" + devName + if _, err := os.Stat(devPath); err == nil { + os.Chmod(devPath, 0666) + } + } + } + + if attempt == 0 { + continue // first iteration: always wait more + } + + // Check if we found any video devices + if len(matches) > 0 { + return + } + } +} diff --git a/internal/client/socket_windows.go b/internal/client/socket_windows.go index 2d2cef4..26e0a74 100644 --- a/internal/client/socket_windows.go +++ b/internal/client/socket_windows.go @@ -16,3 +16,7 @@ func closeFDs(fds [2]int) {} func fdToFile(fd int, name string) *os.File { return nil } + +func fixVHCIDevicePermissions(port int) { + // Not applicable on Windows +} diff --git a/internal/client/use.go b/internal/client/use.go index 1f5ca6b..37e6919 100644 --- a/internal/client/use.go +++ b/internal/client/use.go @@ -132,7 +132,10 @@ func (um *UseManager) AttachDevice(clientID, busID string) error { // Wait for response (with timeout via context) select { - case granted := <-respChan: + case granted, ok := <-respChan: + if !ok || granted == nil { + return fmt.Errorf("device request denied") + } return um.setupVHCI(clientID, busID, granted) case <-um.client.ctx.Done(): return fmt.Errorf("client shutting down") @@ -239,6 +242,11 @@ func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.Device go um.tunnelReadLoop(tunnel) log.Printf("[use] device %s attached on VHCI port %d", key, port) + + // Fix permissions on newly created device nodes (e.g. /dev/video*) + // VHCI-created devices don't get normal udev permissions + go fixVHCIDevicePermissions(port) + return nil }