package client import ( "fmt" "log" "net" "strings" "sync" "github.com/duffy/usb-server/internal/config" "github.com/duffy/usb-server/internal/protocol" "github.com/duffy/usb-server/internal/usbip" "github.com/google/uuid" ) // RemoteDevice represents a USB device available from a share client type RemoteDevice struct { protocol.USBDevice ClientID string `json:"client_id"` ClientName string `json:"client_name"` } // AttachedDevice represents a device currently attached via VHCI type AttachedDevice struct { RemoteDevice TunnelID string `json:"tunnel_id"` VHCIPort int `json:"vhci_port"` } // UseManager handles receiving/using remote USB devices type UseManager struct { client *Client cfg *config.Config cfgPath string mu sync.RWMutex available map[string][]RemoteDevice // clientID -> devices attached map[string]*AttachedDevice // busID@clientID -> attached info tunnels map[string]*useTunnel // tunnelID -> tunnel pending map[string]chan *protocol.DeviceGranted // requestID -> response channel forceDetachable map[string]bool // clientID -> allow_force_detach } type useTunnel struct { id string busID string clientID string conn net.Conn // our end of the socketpair done chan struct{} } // NewUseManager creates a use manager func NewUseManager(client *Client, cfg *config.Config, cfgPath string) *UseManager { um := &UseManager{ client: client, cfg: cfg, cfgPath: cfgPath, available: make(map[string][]RemoteDevice), attached: make(map[string]*AttachedDevice), tunnels: make(map[string]*useTunnel), pending: make(map[string]chan *protocol.DeviceGranted), forceDetachable: make(map[string]bool), } client.OnDeviceList = um.handleDeviceList client.OnDeviceGranted = um.handleDeviceGranted client.OnDeviceDenied = um.handleDeviceDenied client.OnDeviceReleased = um.handleDeviceReleased client.OnTunnelData = um.handleTunnelData client.OnClientLeft = um.handleClientLeft return um } // GetAvailableDevices returns all available remote devices func (um *UseManager) GetAvailableDevices() []RemoteDevice { um.mu.RLock() defer um.mu.RUnlock() var all []RemoteDevice for _, devs := range um.available { all = append(all, devs...) } return all } // GetAttachedDevices returns currently attached devices func (um *UseManager) GetAttachedDevices() []*AttachedDevice { um.mu.RLock() defer um.mu.RUnlock() var result []*AttachedDevice for _, dev := range um.attached { result = append(result, dev) } return result } // AttachDevice requests and attaches a remote USB device func (um *UseManager) AttachDevice(clientID, busID string) error { // Check if VHCI is available if err := usbip.VHCIUnavailableError(); err != nil { return err } key := busID + "@" + clientID um.mu.RLock() if _, already := um.attached[key]; already { um.mu.RUnlock() return fmt.Errorf("device %s already attached", key) } um.mu.RUnlock() // Create request requestID := uuid.New().String() respChan := make(chan *protocol.DeviceGranted, 1) um.mu.Lock() um.pending[requestID] = respChan um.mu.Unlock() defer func() { um.mu.Lock() delete(um.pending, requestID) um.mu.Unlock() }() // Send request to relay err := um.client.SendJSON(&protocol.RequestDevice{ Type: protocol.MsgRequestDevice, TargetClient: clientID, BusID: busID, RequestID: requestID, }) if err != nil { return fmt.Errorf("sending request: %w", err) } log.Printf("[use] requesting device %s from %s", busID, clientID) // Wait for response (with timeout via context) select { 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") } } // DetachDevice releases an attached device func (um *UseManager) DetachDevice(clientID, busID string) error { key := busID + "@" + clientID um.mu.Lock() dev, exists := um.attached[key] if !exists { um.mu.Unlock() return fmt.Errorf("device %s not attached", key) } // Clean up tunnel if tunnel, ok := um.tunnels[dev.TunnelID]; ok { close(tunnel.done) if tunnel.conn != nil { tunnel.conn.Close() } delete(um.tunnels, dev.TunnelID) } // Detach from VHCI if dev.VHCIPort >= 0 { if err := usbip.DetachDevice(dev.VHCIPort); err != nil { log.Printf("[use] warning: VHCI detach error: %v", err) } } delete(um.attached, key) um.mu.Unlock() // Notify share client um.client.SendJSON(&protocol.ReleaseDevice{ Type: protocol.MsgReleaseDevice, TargetClient: clientID, BusID: busID, }) log.Printf("[use] device %s detached", key) return nil } func (um *UseManager) setupVHCI(clientID, busID string, granted *protocol.DeviceGranted) error { // Look up device info from available list (needed for Windows management phase) var devInfo *RemoteDevice um.mu.RLock() for _, d := range um.available[clientID] { if d.BusID == busID { cp := d devInfo = &cp break } } um.mu.RUnlock() // Platform-specific VHCI attachment (Linux: socketpair+sysfs, Windows: TCP proxy+usbip.exe) tunnelConn, vhciPort, err := createVHCIAttachment(um.client.ctx, granted, devInfo) if err != nil { return fmt.Errorf("VHCI attachment: %w", err) } tunnel := &useTunnel{ id: granted.TunnelID, busID: busID, clientID: clientID, conn: tunnelConn, done: make(chan struct{}), } key := busID + "@" + clientID remDev := RemoteDevice{ USBDevice: protocol.USBDevice{BusID: busID}, ClientID: clientID, } if devInfo != nil { remDev = *devInfo } um.mu.Lock() um.tunnels[granted.TunnelID] = tunnel um.attached[key] = &AttachedDevice{ RemoteDevice: remDev, TunnelID: granted.TunnelID, VHCIPort: vhciPort, } um.mu.Unlock() // Start reading from the tunnel socket (VHCI -> relay) go um.tunnelReadLoop(tunnel) log.Printf("[use] device %s attached on VHCI port %d", key, vhciPort) // Fix permissions on newly created device nodes (e.g. /dev/video*) // VHCI-created devices don't get normal udev permissions go fixVHCIDevicePermissions(vhciPort) return nil } // tunnelReadLoop reads from the VHCI socket and sends to relay func (um *UseManager) tunnelReadLoop(tunnel *useTunnel) { buf := make([]byte, 65536) for { select { case <-tunnel.done: return default: } n, err := tunnel.conn.Read(buf) if err != nil { select { case <-tunnel.done: return default: log.Printf("[use] tunnel read error: %v", err) return } } if err := um.client.SendTunnelData(tunnel.id, buf[:n]); err != nil { log.Printf("[use] tunnel send error: %v", err) return } } } func (um *UseManager) handleDeviceList(msg *protocol.DeviceList) { um.mu.Lock() var remoteDevs []RemoteDevice for _, dev := range msg.Devices { remoteDevs = append(remoteDevs, RemoteDevice{ USBDevice: dev, ClientID: msg.ClientID, ClientName: msg.ClientName, }) } um.available[msg.ClientID] = remoteDevs um.forceDetachable[msg.ClientID] = msg.AllowForceDetach // Collect devices to auto-connect (while holding the lock to check attached map) var toAutoConnect []RemoteDevice for _, dev := range remoteDevs { if dev.Status != protocol.StatusAvailable { continue } key := dev.BusID + "@" + msg.ClientID if _, attached := um.attached[key]; attached { continue } if um.matchesAutoConnect(dev) { toAutoConnect = append(toAutoConnect, dev) } } um.mu.Unlock() log.Printf("[use] received device list from %s (%s): %d devices", msg.ClientName, msg.ClientID[:8], len(msg.Devices)) // Auto-connect matching devices (outside lock, each in its own goroutine) for _, dev := range toAutoConnect { log.Printf("[use] auto-connecting %s (%s:%s) from %s", dev.Name, dev.VendorID, dev.ProductID, msg.ClientName) go um.AttachDevice(msg.ClientID, dev.BusID) } } // matchesAutoConnect checks if a device matches any auto-connect rule. // Must be called with um.mu held (at least RLock). func (um *UseManager) matchesAutoConnect(dev RemoteDevice) bool { for _, rule := range um.cfg.AutoConnect { if rule.VendorID != "" && !strings.EqualFold(rule.VendorID, dev.VendorID) { continue } if rule.ProductID != "" && !strings.EqualFold(rule.ProductID, dev.ProductID) { continue } if rule.BusID != "" && rule.BusID != dev.BusID { continue } if rule.ClientName != "" && rule.ClientName != dev.ClientName { continue } return true // all specified fields match } return false } // SetAutoConnect adds or removes an auto-connect rule for a VendorID:ProductID pair. func (um *UseManager) SetAutoConnect(vendorID, productID string, enabled bool) error { um.mu.Lock() defer um.mu.Unlock() if enabled { // Check if rule already exists for _, rule := range um.cfg.AutoConnect { if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) { return nil // already exists } } um.cfg.AutoConnect = append(um.cfg.AutoConnect, config.AutoConnectRule{ VendorID: vendorID, ProductID: productID, }) } else { // Remove matching rule filtered := um.cfg.AutoConnect[:0] for _, rule := range um.cfg.AutoConnect { if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) { continue } filtered = append(filtered, rule) } um.cfg.AutoConnect = filtered } if err := um.cfg.Save(um.cfgPath); err != nil { return fmt.Errorf("saving config: %w", err) } log.Printf("[use] auto-connect %s:%s = %v", vendorID, productID, enabled) return nil } // IsAutoConnect checks if there is an auto-connect rule for this VendorID:ProductID. func (um *UseManager) IsAutoConnect(vendorID, productID string) bool { um.mu.RLock() defer um.mu.RUnlock() for _, rule := range um.cfg.AutoConnect { if strings.EqualFold(rule.VendorID, vendorID) && strings.EqualFold(rule.ProductID, productID) { return true } } return false } // IsForceDetachable checks if a share client allows force-detach func (um *UseManager) IsForceDetachable(clientID string) bool { um.mu.RLock() defer um.mu.RUnlock() return um.forceDetachable[clientID] } // ForceDetachDevice sends a force-release request to the share client func (um *UseManager) ForceDetachDevice(clientID, busID string) error { return um.client.SendJSON(&protocol.ForceRelease{ Type: protocol.MsgForceRelease, TargetClient: clientID, BusID: busID, }) } func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) { um.mu.RLock() ch, exists := um.pending[msg.RequestID] um.mu.RUnlock() if exists { ch <- msg } } func (um *UseManager) handleDeviceDenied(msg *protocol.DeviceDenied) { log.Printf("[use] device request denied: %s - %s", msg.BusID, msg.Reason) um.mu.RLock() ch, exists := um.pending[msg.RequestID] um.mu.RUnlock() if exists { close(ch) // signal denial by closing channel } } func (um *UseManager) handleDeviceReleased(msg *protocol.DeviceReleased) { log.Printf("[use] device released by share client: %s", msg.BusID) um.mu.Lock() // Find and clean up any attached device matching this BusID (and ClientID if provided) for key, dev := range um.attached { if dev.BusID != msg.BusID { continue } if msg.ClientID != "" && dev.ClientID != msg.ClientID { continue } // Clean up tunnel if tunnel, ok := um.tunnels[dev.TunnelID]; ok { close(tunnel.done) if tunnel.conn != nil { tunnel.conn.Close() } delete(um.tunnels, dev.TunnelID) } // Detach from VHCI if dev.VHCIPort >= 0 { if err := usbip.DetachDevice(dev.VHCIPort); err != nil { log.Printf("[use] warning: VHCI detach error for force-released device: %v", err) } } delete(um.attached, key) log.Printf("[use] device %s cleaned up (force-released by share client)", key) break } um.mu.Unlock() } func (um *UseManager) handleTunnelData(tunnelID string, data []byte) { um.mu.RLock() tunnel, exists := um.tunnels[tunnelID] um.mu.RUnlock() if !exists { log.Printf("[use] tunnel data for unknown tunnel %s (%d bytes)", tunnelID[:8], len(data)) return } // Write to the tunnel socket (relay -> VHCI) n, err := tunnel.conn.Write(data) if err != nil { log.Printf("[use] tunnel write error: %v", err) } else if n != len(data) { log.Printf("[use] tunnel short write: %d/%d", n, len(data)) } } func (um *UseManager) handleClientLeft(msg *protocol.ClientLeft) { um.mu.Lock() delete(um.available, msg.ClientID) delete(um.forceDetachable, msg.ClientID) // Detach any devices from this client for key, dev := range um.attached { if dev.ClientID == msg.ClientID { if tunnel, ok := um.tunnels[dev.TunnelID]; ok { close(tunnel.done) tunnel.conn.Close() delete(um.tunnels, dev.TunnelID) } if dev.VHCIPort >= 0 { usbip.DetachDevice(dev.VHCIPort) } delete(um.attached, key) log.Printf("[use] device %s auto-detached (client left)", key) } } um.mu.Unlock() } // Cleanup releases all attached devices func (um *UseManager) Cleanup() { um.mu.Lock() defer um.mu.Unlock() for key, dev := range um.attached { if tunnel, ok := um.tunnels[dev.TunnelID]; ok { close(tunnel.done) tunnel.conn.Close() } if dev.VHCIPort >= 0 { usbip.DetachDevice(dev.VHCIPort) } log.Printf("[use] cleaned up device %s", key) } um.attached = make(map[string]*AttachedDevice) um.tunnels = make(map[string]*useTunnel) }