The HID failure came down to the endpoint type map being indexed by endpoint number without the direction bit. A composite device can have endpoint 1 as both interrupt IN (0x81) and bulk OUT (0x01); the last one read won, so interrupt URBs were submitted as bulk and the kernel rejected them. The device attached and stayed silent. Endpoint data now comes from the raw descriptors read from /dev/bus/usb rather than sysfs, which only ever exposes the active alternate setting — a webcam's isochronous endpoints are invisible there because they only exist after SET_INTERFACE. Two sysfs parsing bugs fell out of that too: the numeric endpoint attributes are hex without a prefix (wMaxPacketSize "0040" was read as 40, not 64), and bInterval was never read at all. Reliability: three places could freeze the whole process. The share path fed io.Pipe from the WebSocket read loop, so one slow USB transfer stalled every tunnel and the keepalives with them. The relay wrote to client sockets while holding the hub lock, so one peer that stopped reading blocked routing and registration for everyone. Control transfers ran inline in the protocol loop behind a 5s timeout. Also fixed: a use-after- free where a discarded URB's memory could be collected while the kernel still owned it, a reap loop that spun at 100% CPU on ioctl errors, a missing attach timeout, a double close(done) panic, and Hash[:8] in the relay's log line, which let a client with a short hash take the server down. Adds mode "both", so one client can offer and consume devices at once. The tunnel and client-left callbacks became multicast for it: as plain fields the second manager to register silently unhooked the first. Tunnel traffic is now AES-256-GCM end to end, on the relay path as well as directly. The key is derived from the three tokens, not from the group hash — the relay is told the hash, so a key derived from it would protect nothing from the one party in the middle. Group IDs are unchanged, so existing setups keep working; only clients configured without the tokens drop to unencrypted, relay-only operation. Peers now try to connect directly, with the relay supplying the public address neither side can determine for itself. Candidates are raced because an unreachable address hangs until timeout rather than refusing. Falling back to the relay is not an error. Platform reach: cross-compiled targets for ARM, MIPS and RISC-V (the Linux client needed no code changes — usbdevfs is not architecture specific), multi-arch Docker images, an Android bridge that accepts devices over SCM_RIGHTS because apps cannot open /dev/bus/usb, and macOS builds via system_profiler enumeration. Adds a Windows KMDF filter driver under driver/windows with its Go side. UNTESTED: it has never been compiled or run, needs the WDK to build and an EV certificate to distribute. Treat it as a starting point. Adds "usb-client diag": says per machine whether sharing and using are possible, what stands in the way, and what fixes it. Reports can be uploaded to a relay to get them off machines that are awkward to copy from. 96 tests, all green under -race. Builds for linux, windows and darwin on amd64 and arm64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
715 lines
20 KiB
Go
715 lines
20 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/duffy/usb-server/internal/config"
|
|
"github.com/duffy/usb-server/internal/crypto"
|
|
"github.com/duffy/usb-server/internal/protocol"
|
|
"github.com/duffy/usb-server/internal/usbip"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// attachRequestTimeout bounds how long we wait for a share client to answer a
|
|
// device request before giving up.
|
|
const attachRequestTimeout = 30 * time.Second
|
|
|
|
// 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]*pendingRequest // requestID -> in-flight attach
|
|
forceDetachable map[string]bool // clientID -> allow_force_detach
|
|
}
|
|
|
|
// pendingRequest tracks an attach request waiting for the share client's reply.
|
|
// It carries the target so that a reply arriving after the caller gave up can
|
|
// still be undone — otherwise the share side would hold the device open for a
|
|
// user who is no longer waiting for it.
|
|
type pendingRequest struct {
|
|
clientID string
|
|
busID string
|
|
resp chan *protocol.DeviceGranted
|
|
}
|
|
|
|
type useTunnel struct {
|
|
id string
|
|
busID string
|
|
clientID string
|
|
conn net.Conn // our end of the socketpair
|
|
|
|
// codec seals outgoing and opens incoming payloads, whichever transport
|
|
// carries them.
|
|
codec *tunnelCodec
|
|
|
|
// send delivers an encoded frame; direct is non-nil when this tunnel
|
|
// bypasses the relay.
|
|
send tunnelSender
|
|
direct *directConn
|
|
|
|
// done is closed exactly once. Several paths can tear down the same
|
|
// tunnel — an explicit detach, a release from the share side, the peer
|
|
// leaving, a relay disconnect — and closing it twice would panic.
|
|
done chan struct{}
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
// tryDirect attempts a direct connection to the granting peer, returning nil
|
|
// if none can be established.
|
|
//
|
|
// Every failure here is benign: the tunnel simply runs through the relay, the
|
|
// way it always did. Only the latency improves when this succeeds.
|
|
func (um *UseManager) tryDirect(granted *protocol.DeviceGranted, secret *crypto.TunnelSecret) *directConn {
|
|
if um.cfg.DisableDirect || secret == nil || len(granted.Endpoints) == 0 {
|
|
return nil
|
|
}
|
|
|
|
conn, addr, err := dialDirect(granted.Endpoints, granted.TunnelID, secret.PeerToken(granted.TunnelID))
|
|
if err != nil {
|
|
log.Printf("[use] no direct route to %s (%v), using the relay", granted.BusID, err)
|
|
return nil
|
|
}
|
|
|
|
log.Printf("[use] direct connection to %s established for %s", addr, granted.BusID)
|
|
return conn
|
|
}
|
|
|
|
// 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]*pendingRequest),
|
|
forceDetachable: make(map[string]bool),
|
|
}
|
|
|
|
// Use-side messages only this manager handles.
|
|
client.OnDeviceList = um.handleDeviceList
|
|
client.OnDeviceGranted = um.handleDeviceGranted
|
|
client.OnDeviceDenied = um.handleDeviceDenied
|
|
client.OnDeviceReleased = um.handleDeviceReleased
|
|
|
|
// Shared with the share manager in "both" mode, hence multicast.
|
|
client.AddTunnelHandler(um.handleTunnelData)
|
|
client.AddClientLeftHandler(um.handleClientLeft)
|
|
client.AddDisconnectHandler(um.handleRelayDisconnect)
|
|
|
|
return um
|
|
}
|
|
|
|
// handleRelayDisconnect detaches everything after the relay link drops.
|
|
// The relay discarded those tunnels, so the devices are dead: without this
|
|
// they would stay listed as attached while no traffic could reach them.
|
|
func (um *UseManager) handleRelayDisconnect() {
|
|
um.mu.Lock()
|
|
defer um.mu.Unlock()
|
|
|
|
if n := len(um.attached); n > 0 {
|
|
log.Printf("[use] detaching %d device(s) (relay connection lost)", n)
|
|
for key, dev := range um.attached {
|
|
um.closeAttachedLocked(key, dev)
|
|
}
|
|
}
|
|
|
|
// The peers that advertised these are unreachable, and their device lists
|
|
// are re-sent on reconnect. Keeping stale entries would show devices the
|
|
// UI cannot actually attach.
|
|
um.available = make(map[string][]RemoteDevice)
|
|
um.forceDetachable = make(map[string]bool)
|
|
}
|
|
|
|
// closeAttachedLocked tears down one attached device: its tunnel, its VHCI
|
|
// port and its bookkeeping. Callers must hold um.mu.
|
|
func (um *UseManager) closeAttachedLocked(key string, dev *AttachedDevice) {
|
|
if tunnel, ok := um.tunnels[dev.TunnelID]; ok {
|
|
tunnel.closeOnce.Do(func() { close(tunnel.done) })
|
|
if tunnel.conn != nil {
|
|
tunnel.conn.Close()
|
|
}
|
|
if tunnel.direct != nil {
|
|
tunnel.direct.Close()
|
|
}
|
|
delete(um.tunnels, dev.TunnelID)
|
|
}
|
|
|
|
if dev.VHCIPort >= 0 {
|
|
if err := usbip.DetachDevice(dev.VHCIPort); err != nil {
|
|
log.Printf("[use] warning: VHCI detach error for %s: %v", key, err)
|
|
}
|
|
}
|
|
|
|
delete(um.attached, key)
|
|
}
|
|
|
|
// 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] = &pendingRequest{clientID: clientID, busID: busID, resp: 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 a grant or denial. Without the timeout a share client that
|
|
// never answers — because it crashed, or the relay dropped the message —
|
|
// would leave this call blocked forever, and with it the HTTP request or
|
|
// auto-connect goroutine that made it.
|
|
timer := time.NewTimer(attachRequestTimeout)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case granted, ok := <-respChan:
|
|
if !ok || granted == nil {
|
|
return fmt.Errorf("device request denied")
|
|
}
|
|
return um.setupVHCI(clientID, busID, granted)
|
|
case <-timer.C:
|
|
return fmt.Errorf("no response from %s for device %s after %s",
|
|
protocol.ShortID(clientID), busID, attachRequestTimeout)
|
|
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)
|
|
}
|
|
um.closeAttachedLocked(key, dev)
|
|
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()
|
|
|
|
// The granting side tells us whether it encrypts. Both ends must agree:
|
|
// a mismatch would turn ciphertext into garbage USB traffic.
|
|
secret := um.client.TunnelSecret()
|
|
if granted.Encrypted && secret == nil {
|
|
return fmt.Errorf("%s encrypts its tunnels but this client has no tokens configured, "+
|
|
"only a group hash — copy the three tokens over to connect", protocol.ShortID(clientID))
|
|
}
|
|
if !granted.Encrypted {
|
|
if secret != nil {
|
|
log.Printf("[use] warning: %s does not encrypt tunnel traffic for %s",
|
|
protocol.ShortID(clientID), busID)
|
|
}
|
|
secret = nil
|
|
}
|
|
|
|
codec, err := newTunnelCodec(secret, granted.TunnelID, crypto.DirUseToShare)
|
|
if err != nil {
|
|
return fmt.Errorf("setting up tunnel encryption: %w", err)
|
|
}
|
|
|
|
// Try to reach the peer directly before falling back to the relay. This
|
|
// is where the latency win comes from: two machines on the same network
|
|
// otherwise send every USB transfer out to the relay and back.
|
|
direct := um.tryDirect(granted, secret)
|
|
|
|
// Platform-specific VHCI attachment (Linux: socketpair+sysfs, Windows: TCP proxy+usbip.exe)
|
|
tunnelConn, vhciPort, err := createVHCIAttachment(um.client.ctx, granted, devInfo)
|
|
if err != nil {
|
|
if direct != nil {
|
|
direct.Close()
|
|
}
|
|
return fmt.Errorf("VHCI attachment: %w", err)
|
|
}
|
|
|
|
tunnel := &useTunnel{
|
|
id: granted.TunnelID,
|
|
busID: busID,
|
|
clientID: clientID,
|
|
conn: tunnelConn,
|
|
done: make(chan struct{}),
|
|
codec: codec,
|
|
direct: direct,
|
|
}
|
|
if direct != nil {
|
|
tunnel.send = directSender(direct)
|
|
} else {
|
|
tunnel.send = relaySender(um.client, granted.TunnelID)
|
|
}
|
|
|
|
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 -> peer)
|
|
go um.tunnelReadLoop(tunnel)
|
|
|
|
// On a direct connection, incoming frames arrive here instead of through
|
|
// the relay's tunnel-data callback.
|
|
if direct != nil {
|
|
go func() {
|
|
receiveLoop(direct, tunnel.codec, func(payload []byte) error {
|
|
_, err := tunnel.conn.Write(payload)
|
|
return err
|
|
}, tunnel.done, "use/"+busID)
|
|
|
|
// Losing the direct connection ends the tunnel: the USB/IP stream
|
|
// cannot be resumed on the relay mid-conversation.
|
|
select {
|
|
case <-tunnel.done:
|
|
default:
|
|
log.Printf("[use] direct connection for %s ended, detaching", key)
|
|
um.DetachDevice(clientID, busID)
|
|
}
|
|
}()
|
|
}
|
|
|
|
transport := "relay"
|
|
if direct != nil {
|
|
transport = "direct " + direct.RemoteAddr()
|
|
}
|
|
log.Printf("[use] device %s attached on VHCI port %d (devID=0x%08x speed=%d, %s, encrypted=%v)",
|
|
key, vhciPort, granted.DevID, granted.Speed, transport, codec.encrypted())
|
|
|
|
// Check device status and fix permissions on newly created device nodes
|
|
// VHCI-created devices don't get normal udev permissions
|
|
go func() {
|
|
logVHCIDeviceStatus(vhciPort)
|
|
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 protocol.Debug {
|
|
usbip.TraceRequest("use-tunnel", buf[:n])
|
|
}
|
|
|
|
if err := send(tunnel.codec, tunnel.send, 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, protocol.ShortID(msg.ClientID), 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,
|
|
})
|
|
}
|
|
|
|
// resolvePending hands a response to the waiting AttachDevice call and removes
|
|
// the request, so that a duplicate or late reply cannot reach the channel
|
|
// twice — a grant arriving after a denial closed it would panic.
|
|
func (um *UseManager) resolvePending(requestID string) (*pendingRequest, bool) {
|
|
um.mu.Lock()
|
|
defer um.mu.Unlock()
|
|
|
|
req, exists := um.pending[requestID]
|
|
if exists {
|
|
delete(um.pending, requestID)
|
|
}
|
|
return req, exists
|
|
}
|
|
|
|
func (um *UseManager) handleDeviceGranted(msg *protocol.DeviceGranted) {
|
|
req, exists := um.resolvePending(msg.RequestID)
|
|
if !exists {
|
|
// Nobody is waiting any more — the request timed out, or the caller
|
|
// gave up. The share client has already claimed the device for us, so
|
|
// hand it back instead of leaving it stuck in "in use".
|
|
log.Printf("[use] late grant for %s, releasing it again", msg.BusID)
|
|
um.releaseOrphanedGrant(msg)
|
|
return
|
|
}
|
|
|
|
// The channel is buffered with capacity 1 and we are the only sender for
|
|
// this request ID, so this never blocks.
|
|
req.resp <- msg
|
|
}
|
|
|
|
// releaseOrphanedGrant tells the share client to take back a device that was
|
|
// granted to a request nobody is waiting for.
|
|
func (um *UseManager) releaseOrphanedGrant(msg *protocol.DeviceGranted) {
|
|
// Find who owns this bus ID; the grant message does not name the sender.
|
|
um.mu.RLock()
|
|
var owner string
|
|
for clientID, devs := range um.available {
|
|
for _, d := range devs {
|
|
if d.BusID == msg.BusID {
|
|
owner = clientID
|
|
break
|
|
}
|
|
}
|
|
if owner != "" {
|
|
break
|
|
}
|
|
}
|
|
um.mu.RUnlock()
|
|
|
|
if owner == "" {
|
|
return
|
|
}
|
|
|
|
um.client.SendJSON(&protocol.ReleaseDevice{
|
|
Type: protocol.MsgReleaseDevice,
|
|
TargetClient: owner,
|
|
BusID: msg.BusID,
|
|
})
|
|
}
|
|
|
|
func (um *UseManager) handleDeviceDenied(msg *protocol.DeviceDenied) {
|
|
log.Printf("[use] device request denied: %s - %s", msg.BusID, msg.Reason)
|
|
|
|
if req, exists := um.resolvePending(msg.RequestID); exists {
|
|
close(req.resp) // a closed channel reads as a denial
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
um.closeAttachedLocked(key, dev)
|
|
log.Printf("[use] device %s cleaned up (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 {
|
|
// In "both" mode the share manager sees the same frames and owns the
|
|
// other tunnels, so an unknown ID here is normal, not an error.
|
|
return
|
|
}
|
|
|
|
// A tunnel running directly gets its frames from that connection; anything
|
|
// arriving via the relay for it is stale or spoofed.
|
|
if tunnel.direct != nil {
|
|
return
|
|
}
|
|
|
|
payload, err := tunnel.codec.decode(data)
|
|
if err != nil {
|
|
log.Printf("[use] tunnel %s: rejecting relayed frame: %v", protocol.ShortID(tunnelID), err)
|
|
tunnel.closeOnce.Do(func() { close(tunnel.done) })
|
|
tunnel.conn.Close()
|
|
return
|
|
}
|
|
|
|
if protocol.Debug {
|
|
usbip.TraceResponse("use-tunnel", payload)
|
|
}
|
|
|
|
// Write to the tunnel socket (peer -> VHCI). A failed write would desync
|
|
// the USB/IP stream permanently, so treat it as fatal for this tunnel.
|
|
if _, err := tunnel.conn.Write(payload); err != nil {
|
|
log.Printf("[use] tunnel %s write error: %v", protocol.ShortID(tunnelID), err)
|
|
tunnel.closeOnce.Do(func() { close(tunnel.done) })
|
|
tunnel.conn.Close()
|
|
}
|
|
}
|
|
|
|
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 {
|
|
um.closeAttachedLocked(key, dev)
|
|
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 {
|
|
um.closeAttachedLocked(key, dev)
|
|
log.Printf("[use] cleaned up device %s", key)
|
|
}
|
|
|
|
um.attached = make(map[string]*AttachedDevice)
|
|
um.tunnels = make(map[string]*useTunnel)
|
|
}
|