Fix HID transfers, harden the tunnel, add E2E crypto and direct peers

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>
This commit is contained in:
2026-08-11 22:02:04 +02:00
co-authored by Claude Opus 5
parent 54178dce75
commit 9ed473a965
95 changed files with 12181 additions and 892 deletions
+83
View File
@@ -0,0 +1,83 @@
//go:build linux
package usb
import (
"fmt"
"sync"
"golang.org/x/sys/unix"
)
// Externally supplied file descriptors, keyed by bus ID.
//
// Android is the reason this exists. Apps there cannot open /dev/bus/usb:
// access goes through the framework, which shows a permission dialog and
// returns an already-open descriptor. A small Java shim obtains it and passes
// it to this process, which then drives the device through the same usbdevfs
// ioctls as anywhere else — the kernel interface is identical, only the way
// the descriptor is obtained differs.
var (
adoptedMu sync.Mutex
adoptedFDs = make(map[string]int)
)
// AdoptDeviceFD registers an already-open usbdevfs file descriptor for a bus
// ID. The next OpenDevice for that bus ID takes it instead of opening a path.
//
// Ownership transfers: the descriptor is closed when the resulting handle is
// closed, or by ReleaseAdoptedFDs if it is never claimed.
func AdoptDeviceFD(busID string, fd int) error {
if busID == "" {
return fmt.Errorf("bus ID is required")
}
if fd < 0 {
return fmt.Errorf("invalid file descriptor %d", fd)
}
// Reject a descriptor that is not actually usable, so the failure is
// reported here rather than as a confusing ioctl error much later.
if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil {
return fmt.Errorf("file descriptor %d is not open: %w", fd, err)
}
adoptedMu.Lock()
defer adoptedMu.Unlock()
if old, exists := adoptedFDs[busID]; exists && old != fd {
unix.Close(old)
}
adoptedFDs[busID] = fd
return nil
}
// takeAdoptedFD removes and returns a registered descriptor, if any.
func takeAdoptedFD(busID string) (int, bool) {
adoptedMu.Lock()
defer adoptedMu.Unlock()
fd, ok := adoptedFDs[busID]
if ok {
delete(adoptedFDs, busID)
}
return fd, ok
}
// HasAdoptedFD reports whether a descriptor is registered for a bus ID.
func HasAdoptedFD(busID string) bool {
adoptedMu.Lock()
defer adoptedMu.Unlock()
_, ok := adoptedFDs[busID]
return ok
}
// ReleaseAdoptedFDs closes every registered descriptor that was never claimed.
func ReleaseAdoptedFDs() {
adoptedMu.Lock()
defer adoptedMu.Unlock()
for busID, fd := range adoptedFDs {
unix.Close(fd)
delete(adoptedFDs, busID)
}
}
+18
View File
@@ -0,0 +1,18 @@
//go:build !linux
package usb
import "fmt"
// Adopting an external file descriptor only makes sense where devices are
// driven through usbdevfs, which is Linux-only.
func AdoptDeviceFD(busID string, fd int) error {
return fmt.Errorf("adopting USB file descriptors is only supported on Linux")
}
func takeAdoptedFD(busID string) (int, bool) { return 0, false }
func HasAdoptedFD(busID string) bool { return false }
func ReleaseAdoptedFDs() {}
+169
View File
@@ -0,0 +1,169 @@
package usb
import (
"encoding/binary"
"fmt"
)
// USB descriptor types
const (
DescTypeDevice = 0x01
DescTypeConfiguration = 0x02
DescTypeInterface = 0x04
DescTypeEndpoint = 0x05
)
// ParsedDescriptors holds everything we extract from a device's raw
// descriptor blob (device descriptor followed by all configuration
// descriptors, as returned by reading a usbdevfs device file).
type ParsedDescriptors struct {
VendorID uint16
ProductID uint16
BcdDevice uint16
DeviceClass uint8
DeviceSubClass uint8
DeviceProtocol uint8
NumConfigs uint8
// Configs holds every configuration, each with every interface
// alternate setting and its endpoints.
Configs []ConfigDescriptor
}
// ConfigDescriptor is one USB configuration
type ConfigDescriptor struct {
Value uint8 // bConfigurationValue
Interfaces []Interface // every alternate setting, in descriptor order
}
// ParseDescriptors parses a raw descriptor blob: an 18-byte device
// descriptor followed by one or more complete configuration descriptors.
//
// Reading a usbdevfs file (/dev/bus/usb/BBB/DDD) from offset 0 yields
// exactly this layout, which is the only way to see interface alternate
// settings — sysfs only exposes the currently active one.
func ParseDescriptors(data []byte) (*ParsedDescriptors, error) {
if len(data) < 18 {
return nil, fmt.Errorf("descriptor blob too short: %d bytes", len(data))
}
if data[1] != DescTypeDevice {
return nil, fmt.Errorf("first descriptor is type 0x%02x, expected device (0x01)", data[1])
}
pd := &ParsedDescriptors{
DeviceClass: data[4],
DeviceSubClass: data[5],
DeviceProtocol: data[6],
VendorID: binary.LittleEndian.Uint16(data[8:10]),
ProductID: binary.LittleEndian.Uint16(data[10:12]),
BcdDevice: binary.LittleEndian.Uint16(data[12:14]),
NumConfigs: data[17],
}
// Walk the remaining descriptors. Configuration descriptors start a new
// config; interface descriptors start a new alternate setting; endpoint
// descriptors attach to the most recent interface. Class-specific
// descriptors (HID, UVC, audio) are skipped by their bLength.
pos := int(data[0]) // skip the device descriptor using its own bLength
if pos < 18 {
pos = 18
}
var curConfig *ConfigDescriptor
var curIface *Interface
for pos+2 <= len(data) {
bLength := int(data[pos])
bType := data[pos+1]
// A zero-length descriptor would loop forever; a descriptor running
// past the end of the blob means the device returned garbage.
if bLength < 2 || pos+bLength > len(data) {
break
}
switch bType {
case DescTypeConfiguration:
if bLength >= 9 {
pd.Configs = append(pd.Configs, ConfigDescriptor{Value: data[pos+5]})
curConfig = &pd.Configs[len(pd.Configs)-1]
curIface = nil
}
case DescTypeInterface:
if bLength >= 9 && curConfig != nil {
curConfig.Interfaces = append(curConfig.Interfaces, Interface{
Number: data[pos+2],
AltSetting: data[pos+3],
Class: data[pos+5],
SubClass: data[pos+6],
Protocol: data[pos+7],
})
curIface = &curConfig.Interfaces[len(curConfig.Interfaces)-1]
}
case DescTypeEndpoint:
if bLength >= 7 && curIface != nil {
curIface.Endpoints = append(curIface.Endpoints, Endpoint{
Address: data[pos+2],
TransferType: data[pos+3] & 0x03,
MaxPacketSize: binary.LittleEndian.Uint16(data[pos+4 : pos+6]),
Interval: data[pos+6],
})
}
}
pos += bLength
}
if len(pd.Configs) == 0 {
return nil, fmt.Errorf("no configuration descriptor found")
}
return pd, nil
}
// FindConfig returns the configuration with the given bConfigurationValue,
// or nil if the device has no such configuration.
func (pd *ParsedDescriptors) FindConfig(value uint8) *ConfigDescriptor {
for i := range pd.Configs {
if pd.Configs[i].Value == value {
return &pd.Configs[i]
}
}
return nil
}
// AllEndpoints returns every endpoint across every alternate setting of the
// given configuration, keyed by full bEndpointAddress (direction bit
// included). Endpoints only present in a non-zero alternate setting — the
// isochronous endpoints of webcams, for example — are included, which is
// what makes the endpoint type map correct after a SET_INTERFACE.
func (c *ConfigDescriptor) AllEndpoints() map[uint8]Endpoint {
eps := make(map[uint8]Endpoint)
for _, iface := range c.Interfaces {
for _, ep := range iface.Endpoints {
// Alternate settings reuse addresses with identical transfer
// types in practice; keep the first one we see so alt 0 wins.
if _, seen := eps[ep.Address]; !seen {
eps[ep.Address] = ep
}
}
}
return eps
}
// ActiveInterfaces returns one Interface per interface number, using
// alternate setting 0 — the set of interfaces that must be claimed.
func (c *ConfigDescriptor) ActiveInterfaces() []Interface {
var result []Interface
seen := make(map[uint8]bool)
for _, iface := range c.Interfaces {
if iface.AltSetting != 0 || seen[iface.Number] {
continue
}
seen[iface.Number] = true
result = append(result, iface)
}
return result
}
+238
View File
@@ -0,0 +1,238 @@
package usb
import "testing"
// buildDescriptorBlob assembles a device descriptor followed by raw
// configuration bytes, the way a usbdevfs file read returns them.
func buildDescriptorBlob(numConfigs uint8, configs ...[]byte) []byte {
dev := []byte{
18, // bLength
0x01, // bDescriptorType = DEVICE
0x00, 0x02, // bcdUSB 2.00
0x00, // bDeviceClass (per-interface)
0x00, // bDeviceSubClass
0x00, // bDeviceProtocol
64, // bMaxPacketSize0
0x6d, 0x04, // idVendor 046d
0x1c, 0xc0, // idProduct c01c
0x10, 0x02, // bcdDevice 0210
1, 2, 3, // string indices
numConfigs,
}
blob := dev
for _, c := range configs {
blob = append(blob, c...)
}
return blob
}
func ifaceDesc(number, alt, numEndpoints, class, subclass, protocol uint8) []byte {
return []byte{9, 0x04, number, alt, numEndpoints, class, subclass, protocol, 0}
}
func endpointDesc(addr, attrs uint8, maxPacket uint16, interval uint8) []byte {
return []byte{7, 0x05, addr, attrs, byte(maxPacket), byte(maxPacket >> 8), interval}
}
func configDesc(value uint8, body []byte) []byte {
total := 9 + len(body)
cfg := []byte{9, 0x02, byte(total), byte(total >> 8), 1, value, 0, 0x80, 250}
return append(cfg, body...)
}
func TestParseDescriptorsDeviceFields(t *testing.T) {
blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 0, 3, 1, 1)))
pd, err := ParseDescriptors(blob)
if err != nil {
t.Fatalf("ParseDescriptors: %v", err)
}
if pd.VendorID != 0x046d {
t.Errorf("VendorID = %04x, want 046d", pd.VendorID)
}
if pd.ProductID != 0xc01c {
t.Errorf("ProductID = %04x, want c01c", pd.ProductID)
}
if pd.BcdDevice != 0x0210 {
t.Errorf("BcdDevice = %04x, want 0210", pd.BcdDevice)
}
if pd.NumConfigs != 1 {
t.Errorf("NumConfigs = %d, want 1", pd.NumConfigs)
}
if len(pd.Configs) != 1 {
t.Fatalf("got %d configs, want 1", len(pd.Configs))
}
}
// A composite device where endpoint number 1 appears twice with different
// directions and different transfer types. Keying the endpoint map by number
// alone collapses these two into one, which is what made the server submit
// interrupt URBs with the bulk type and broke HID devices.
func TestAllEndpointsKeepsDirectionsSeparate(t *testing.T) {
body := ifaceDesc(0, 0, 2, 0x08, 0x06, 0x50) // mass storage
body = append(body, endpointDesc(0x01, 0x02, 512, 0)...) // bulk OUT, EP1
body = append(body, endpointDesc(0x82, 0x02, 512, 0)...) // bulk IN, EP2
body = append(body, ifaceDesc(1, 0, 1, 0x03, 0x01, 0x01)...) // HID keyboard
body = append(body, endpointDesc(0x81, 0x03, 8, 10)...) // interrupt IN, EP1
blob := buildDescriptorBlob(1, configDesc(1, body))
pd, err := ParseDescriptors(blob)
if err != nil {
t.Fatalf("ParseDescriptors: %v", err)
}
eps := pd.Configs[0].AllEndpoints()
if len(eps) != 3 {
t.Fatalf("got %d endpoints, want 3: %+v", len(eps), eps)
}
if got := eps[0x01].TransferType; got != TransferTypeBulk {
t.Errorf("EP 0x01 type = %d, want bulk (%d)", got, TransferTypeBulk)
}
if got := eps[0x81].TransferType; got != TransferTypeInterrupt {
t.Errorf("EP 0x81 type = %d, want interrupt (%d) — direction bit must not collapse", got, TransferTypeInterrupt)
}
if got := eps[0x81].Interval; got != 10 {
t.Errorf("EP 0x81 interval = %d, want 10", got)
}
if got := eps[0x82].MaxPacketSize; got != 512 {
t.Errorf("EP 0x82 maxpacket = %d, want 512", got)
}
}
// A webcam's isochronous endpoints only exist in a non-zero alternate
// setting. sysfs shows only the active setting, so an endpoint map built from
// it would classify these as bulk after a SET_INTERFACE.
func TestAllEndpointsIncludesAlternateSettings(t *testing.T) {
body := ifaceDesc(1, 0, 0, 0x0e, 0x02, 0x00) // video streaming, alt 0: no endpoints
body = append(body, ifaceDesc(1, 1, 1, 0x0e, 0x02, 0x00)...)
body = append(body, endpointDesc(0x81, 0x05, 1024, 1)...) // isochronous IN
body = append(body, ifaceDesc(1, 2, 1, 0x0e, 0x02, 0x00)...)
body = append(body, endpointDesc(0x81, 0x05, 2048, 1)...)
blob := buildDescriptorBlob(1, configDesc(1, body))
pd, err := ParseDescriptors(blob)
if err != nil {
t.Fatalf("ParseDescriptors: %v", err)
}
cfg := pd.Configs[0]
if len(cfg.Interfaces) != 3 {
t.Fatalf("got %d interface descriptors, want 3 (alt 0,1,2)", len(cfg.Interfaces))
}
eps := cfg.AllEndpoints()
ep, ok := eps[0x81]
if !ok {
t.Fatal("EP 0x81 missing — endpoints from non-zero alternate settings were dropped")
}
if ep.TransferType != TransferTypeIsochronous {
t.Errorf("EP 0x81 type = %d, want isochronous (%d)", ep.TransferType, TransferTypeIsochronous)
}
}
func TestActiveInterfacesOnlyAltZero(t *testing.T) {
body := ifaceDesc(0, 0, 0, 0x01, 0x01, 0x00)
body = append(body, ifaceDesc(1, 0, 0, 0x01, 0x02, 0x00)...)
body = append(body, ifaceDesc(1, 1, 1, 0x01, 0x02, 0x00)...)
body = append(body, endpointDesc(0x81, 0x05, 192, 1)...)
blob := buildDescriptorBlob(1, configDesc(1, body))
pd, _ := ParseDescriptors(blob)
active := pd.Configs[0].ActiveInterfaces()
if len(active) != 2 {
t.Fatalf("got %d active interfaces, want 2 (one per interface number)", len(active))
}
for _, iface := range active {
if iface.AltSetting != 0 {
t.Errorf("interface %d has alt setting %d, want 0", iface.Number, iface.AltSetting)
}
}
}
// Class-specific descriptors (HID, UVC, audio) sit between the standard ones
// and must be skipped by bLength rather than confusing the walk.
func TestParseDescriptorsSkipsClassSpecific(t *testing.T) {
hidDesc := []byte{9, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x3f, 0x00}
body := ifaceDesc(0, 0, 1, 0x03, 0x01, 0x01)
body = append(body, hidDesc...)
body = append(body, endpointDesc(0x81, 0x03, 8, 10)...)
blob := buildDescriptorBlob(1, configDesc(1, body))
pd, err := ParseDescriptors(blob)
if err != nil {
t.Fatalf("ParseDescriptors: %v", err)
}
eps := pd.Configs[0].AllEndpoints()
if _, ok := eps[0x81]; !ok {
t.Fatal("endpoint after a HID descriptor was not parsed")
}
if len(pd.Configs[0].Interfaces[0].Endpoints) != 1 {
t.Errorf("got %d endpoints on the interface, want 1",
len(pd.Configs[0].Interfaces[0].Endpoints))
}
}
func TestFindConfigSelectsByValue(t *testing.T) {
blob := buildDescriptorBlob(2,
configDesc(1, ifaceDesc(0, 0, 0, 0x03, 0, 0)),
configDesc(2, ifaceDesc(0, 0, 0, 0x08, 0, 0)),
)
pd, err := ParseDescriptors(blob)
if err != nil {
t.Fatalf("ParseDescriptors: %v", err)
}
cfg := pd.FindConfig(2)
if cfg == nil {
t.Fatal("FindConfig(2) returned nil")
}
if cfg.Interfaces[0].Class != 0x08 {
t.Errorf("got interface class %02x, want 08 — wrong configuration selected", cfg.Interfaces[0].Class)
}
if pd.FindConfig(9) != nil {
t.Error("FindConfig(9) should return nil for a configuration that does not exist")
}
}
func TestParseDescriptorsRejectsGarbage(t *testing.T) {
tests := []struct {
name string
data []byte
}{
{"empty", nil},
{"too short", []byte{18, 0x01, 0x00}},
{"not a device descriptor", append([]byte{9, 0x02}, make([]byte, 20)...)},
{"no configuration", buildDescriptorBlob(1)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := ParseDescriptors(tt.data); err == nil {
t.Error("expected an error, got nil")
}
})
}
}
// A truncated or zero-length descriptor must terminate the walk instead of
// looping forever or reading past the buffer.
func TestParseDescriptorsHandlesTruncation(t *testing.T) {
blob := buildDescriptorBlob(1, configDesc(1, ifaceDesc(0, 0, 1, 3, 1, 1)))
blob = append(blob, 0x00, 0x05) // zero bLength would spin forever
blob = append(blob, 9, 0x04) // interface descriptor claiming 9 bytes, only 2 present
done := make(chan struct{})
go func() {
defer close(done)
if _, err := ParseDescriptors(blob); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
<-done
}
+44 -27
View File
@@ -2,43 +2,60 @@ package usb
// Device represents a USB device
type Device struct {
BusID string `json:"bus_id"` // e.g. "1-1.4"
BusNum uint32 `json:"bus_num"`
DevNum uint32 `json:"dev_num"`
Speed uint32 `json:"speed"`
VendorID uint16 `json:"vendor_id"`
ProductID uint16 `json:"product_id"`
BcdDevice uint16 `json:"bcd_device"`
DeviceClass uint8 `json:"device_class"`
DeviceSubClass uint8 `json:"device_sub_class"`
DeviceProtocol uint8 `json:"device_protocol"`
ConfigValue uint8 `json:"config_value"`
NumConfigs uint8 `json:"num_configs"`
Manufacturer string `json:"manufacturer"`
Product string `json:"product"`
Serial string `json:"serial"`
SysPath string `json:"sys_path"` // sysfs path
DevPath string `json:"dev_path"` // /dev/bus/usb path
Interfaces []Interface `json:"interfaces"`
BusID string `json:"bus_id"` // e.g. "1-1.4"
BusNum uint32 `json:"bus_num"`
DevNum uint32 `json:"dev_num"`
Speed uint32 `json:"speed"`
VendorID uint16 `json:"vendor_id"`
ProductID uint16 `json:"product_id"`
BcdDevice uint16 `json:"bcd_device"`
DeviceClass uint8 `json:"device_class"`
DeviceSubClass uint8 `json:"device_sub_class"`
DeviceProtocol uint8 `json:"device_protocol"`
ConfigValue uint8 `json:"config_value"`
NumConfigs uint8 `json:"num_configs"`
Manufacturer string `json:"manufacturer"`
Product string `json:"product"`
Serial string `json:"serial"`
SysPath string `json:"sys_path"` // sysfs path
DevPath string `json:"dev_path"` // /dev/bus/usb path
// Interfaces holds one entry per interface number at alternate setting 0.
// These are the interfaces that get claimed when sharing the device.
Interfaces []Interface `json:"interfaces"`
// Endpoints holds every endpoint of the active configuration across all
// alternate settings, keyed by full bEndpointAddress (direction bit
// included). Endpoints that only exist in a non-zero alternate setting
// are included, so the transfer type stays correct after SET_INTERFACE.
Endpoints map[uint8]Endpoint `json:"endpoints"`
}
// Interface represents a USB interface
// Interface represents a USB interface at one alternate setting
type Interface struct {
Number uint8 `json:"number"`
Class uint8 `json:"class"`
SubClass uint8 `json:"sub_class"`
Protocol uint8 `json:"protocol"`
Driver string `json:"driver"`
Endpoints []Endpoint `json:"endpoints"`
Number uint8 `json:"number"`
AltSetting uint8 `json:"alt_setting"`
Class uint8 `json:"class"`
SubClass uint8 `json:"sub_class"`
Protocol uint8 `json:"protocol"`
Driver string `json:"driver"`
Endpoints []Endpoint `json:"endpoints"`
}
// Endpoint represents a USB endpoint
type Endpoint struct {
Address uint8 `json:"address"` // bEndpointAddress (bit 7=direction, bits 3:0=number)
TransferType uint8 `json:"transfer_type"` // 0=control, 1=iso, 2=bulk, 3=interrupt
Address uint8 `json:"address"` // bEndpointAddress (bit 7=direction, bits 3:0=number)
TransferType uint8 `json:"transfer_type"` // 0=control, 1=iso, 2=bulk, 3=interrupt
MaxPacketSize uint16 `json:"max_packet_size"`
Interval uint8 `json:"interval"` // bInterval
}
// IsIn reports whether this is an IN (device-to-host) endpoint.
func (e Endpoint) IsIn() bool { return e.Address&0x80 != 0 }
// Number returns the endpoint number without the direction bit.
func (e Endpoint) Number() uint8 { return e.Address & 0x0F }
// USB transfer types (from bmAttributes)
const (
TransferTypeControl = 0
+320
View File
@@ -0,0 +1,320 @@
//go:build windows
package usb
import (
"encoding/binary"
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
// Interface to the usbshare filter driver (driver/windows).
//
// The structure layouts and IOCTL codes here must match public.h exactly.
// They are marshalled by hand on both sides, so a mismatch corrupts memory
// rather than failing cleanly — change one, change the other.
// GUID_DEVINTERFACE_USBSHARE from public.h.
var guidDevInterfaceUsbShare = windows.GUID{
Data1: 0x8f3d2a14,
Data2: 0x6c7b,
Data3: 0x4e59,
Data4: [8]byte{0x9a, 0x1d, 0x3f, 0x5b, 0x7c, 0x8e, 0x2d, 0x40},
}
// IOCTL codes, mirroring the USBSHARE_IOCTL macro.
const (
fileDeviceUsbShare = 0x8000
methodBuffered = 0
fileAnyAccess = 0
)
func usbShareIOCTL(index uint32) uint32 {
return (fileDeviceUsbShare << 16) | (fileAnyAccess << 14) | ((0x800 + index) << 2) | methodBuffered
}
var (
ioctlClaim = usbShareIOCTL(0)
ioctlRelease = usbShareIOCTL(1)
ioctlGetDescriptors = usbShareIOCTL(2)
ioctlSubmit = usbShareIOCTL(3)
ioctlCancel = usbShareIOCTL(4)
ioctlSetInterface = usbShareIOCTL(5)
ioctlClearHalt = usbShareIOCTL(6)
ioctlReset = usbShareIOCTL(7)
)
// Transfer types, matching USBSHARE_TRANSFER_* in public.h.
const (
winTransferControl = 0
winTransferIsochronous = 1
winTransferBulk = 2
winTransferInterrupt = 3
)
// Directions, matching USBSHARE_DIR_*.
const (
winDirOut = 0
winDirIn = 1
)
// winDeviceInfo mirrors USBSHARE_DEVICE_INFO (packed).
type winDeviceInfo struct {
VendorID uint16
ProductID uint16
BcdDevice uint16
DeviceClass uint8
DeviceSubClass uint8
DeviceProtocol uint8
ConfigurationValue uint8
NumConfigurations uint8
Speed uint32
PortNumber uint32
}
// winTransferHeader mirrors USBSHARE_TRANSFER (packed).
type winTransferHeader struct {
ID uint64
EndpointAddress uint8
Type uint8
Direction uint8
Reserved uint8
BufferLength uint32
Timeout uint32
Setup [8]byte
}
// winTransferResult mirrors USBSHARE_TRANSFER_RESULT (packed).
type winTransferResult struct {
ID uint64
Status int32
UsbdStatus uint32
ActualLength uint32
}
const (
winTransferHeaderSize = 8 + 1 + 1 + 1 + 1 + 4 + 4 + 8 // 28
winTransferResultSize = 8 + 4 + 4 + 4 // 20
)
// DriverHandle is an open handle to a device claimed through the filter driver.
type DriverHandle struct {
handle windows.Handle
info winDeviceInfo
nextID uint64
}
// OpenDriverDevice opens the filter driver's interface for a device path and
// claims the device.
//
// Claiming stops the class driver from talking to the device, which is what
// lets us drive it — and it is released automatically if this process dies,
// because the driver ties the claim to the handle.
func OpenDriverDevice(devicePath string) (*DriverHandle, error) {
pathPtr, err := windows.UTF16PtrFromString(devicePath)
if err != nil {
return nil, fmt.Errorf("invalid device path: %w", err)
}
handle, err := windows.CreateFile(
pathPtr,
windows.GENERIC_READ|windows.GENERIC_WRITE,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE,
nil,
windows.OPEN_EXISTING,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, fmt.Errorf("opening %s: %w (is the usbshare driver installed?)", devicePath, err)
}
h := &DriverHandle{handle: handle}
if err := h.claim(); err != nil {
windows.CloseHandle(handle)
return nil, err
}
return h, nil
}
func (h *DriverHandle) claim() error {
out := make([]byte, unsafe.Sizeof(winDeviceInfo{}))
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlClaim,
nil, 0,
&out[0], uint32(len(out)),
&returned, nil)
if err != nil {
return fmt.Errorf("claiming device: %w", err)
}
h.info = *(*winDeviceInfo)(unsafe.Pointer(&out[0]))
return nil
}
// Close releases the device and closes the handle.
func (h *DriverHandle) Close() error {
var returned uint32
windows.DeviceIoControl(h.handle, ioctlRelease, nil, 0, nil, 0, &returned, nil)
return windows.CloseHandle(h.handle)
}
// Info returns the device information reported at claim time.
func (h *DriverHandle) Info() winDeviceInfo { return h.info }
// Descriptors reads the raw descriptor blob: device descriptor followed by
// the configuration descriptors, the same layout Linux usbdevfs returns. It
// is parsed by the same code on both platforms.
func (h *DriverHandle) Descriptors() ([]byte, error) {
// Ask with a generous buffer first; grow if the driver reports more.
buf := make([]byte, 4096)
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlGetDescriptors,
nil, 0, &buf[0], uint32(len(buf)), &returned, nil)
if err == windows.ERROR_INSUFFICIENT_BUFFER || err == windows.ERROR_MORE_DATA {
buf = make([]byte, returned)
err = windows.DeviceIoControl(h.handle, ioctlGetDescriptors,
nil, 0, &buf[0], uint32(len(buf)), &returned, nil)
}
if err != nil {
return nil, fmt.Errorf("reading descriptors: %w", err)
}
return buf[:returned], nil
}
// Transfer performs one USB transfer and blocks until it completes.
//
// For IN transfers data is the buffer to fill; for OUT transfers it holds the
// payload to send. The returned count is how many bytes actually moved, which
// matters for both directions.
func (h *DriverHandle) Transfer(params *TransferParams) (int, error) {
h.nextID++
header := winTransferHeader{
ID: h.nextID,
EndpointAddress: params.EndpointAddress,
Type: params.Type,
Direction: params.Direction,
BufferLength: uint32(len(params.Data)),
Timeout: params.TimeoutMS,
Setup: params.Setup,
}
// Input: header followed by the payload for OUT transfers.
input := make([]byte, winTransferHeaderSize+len(params.Data))
marshalTransferHeader(input, &header)
if params.Direction == winDirOut && len(params.Data) > 0 {
copy(input[winTransferHeaderSize:], params.Data)
}
// Output: result header followed by the payload for IN transfers.
output := make([]byte, winTransferResultSize+len(params.Data))
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlSubmit,
&input[0], uint32(len(input)),
&output[0], uint32(len(output)),
&returned, nil)
if err != nil {
return 0, fmt.Errorf("submitting transfer: %w", err)
}
if returned < winTransferResultSize {
return 0, fmt.Errorf("driver returned %d bytes, expected at least %d",
returned, winTransferResultSize)
}
result := unmarshalTransferResult(output)
if result.Status != 0 {
return int(result.ActualLength), fmt.Errorf(
"transfer failed: status 0x%08x, usbd 0x%08x",
uint32(result.Status), result.UsbdStatus)
}
if params.Direction == winDirIn && result.ActualLength > 0 {
n := int(result.ActualLength)
if n > len(params.Data) {
n = len(params.Data)
}
copy(params.Data, output[winTransferResultSize:winTransferResultSize+n])
}
return int(result.ActualLength), nil
}
// TransferParams describes one transfer.
type TransferParams struct {
EndpointAddress uint8
Type uint8
Direction uint8
Data []byte
TimeoutMS uint32
Setup [8]byte
}
// SetInterface selects an alternate setting through the driver, so the USB
// stack re-opens the pipes and reserves bandwidth for isochronous endpoints.
func (h *DriverHandle) SetInterface(iface, alt uint8) error {
input := []byte{iface, alt}
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlSetInterface,
&input[0], uint32(len(input)), nil, 0, &returned, nil)
if err != nil {
return fmt.Errorf("setting interface %d to alt %d: %w", iface, alt, err)
}
return nil
}
// ClearHalt clears a stall condition on an endpoint.
func (h *DriverHandle) ClearHalt(endpoint uint8) error {
input := []byte{endpoint}
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlClearHalt,
&input[0], 1, nil, 0, &returned, nil)
if err != nil {
return fmt.Errorf("clearing halt on endpoint 0x%02x: %w", endpoint, err)
}
return nil
}
// Reset resets the device's port.
func (h *DriverHandle) Reset() error {
var returned uint32
err := windows.DeviceIoControl(h.handle, ioctlReset, nil, 0, nil, 0, &returned, nil)
if err != nil {
return fmt.Errorf("resetting device: %w", err)
}
return nil
}
// marshalTransferHeader writes the header in the driver's packed layout.
// Done field by field rather than by casting a struct: Go inserts padding
// that the packed C structure does not have.
func marshalTransferHeader(buf []byte, h *winTransferHeader) {
binary.LittleEndian.PutUint64(buf[0:8], h.ID)
buf[8] = h.EndpointAddress
buf[9] = h.Type
buf[10] = h.Direction
buf[11] = h.Reserved
binary.LittleEndian.PutUint32(buf[12:16], h.BufferLength)
binary.LittleEndian.PutUint32(buf[16:20], h.Timeout)
copy(buf[20:28], h.Setup[:])
}
func unmarshalTransferResult(buf []byte) winTransferResult {
return winTransferResult{
ID: binary.LittleEndian.Uint64(buf[0:8]),
Status: int32(binary.LittleEndian.Uint32(buf[8:12])),
UsbdStatus: binary.LittleEndian.Uint32(buf[12:16]),
ActualLength: binary.LittleEndian.Uint32(buf[16:20]),
}
}
+119
View File
@@ -0,0 +1,119 @@
//go:build darwin
package usb
import (
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
)
// Enumerate lists USB devices on macOS via system_profiler.
//
// This is enough to see and report what is attached, which is what the
// diagnostics need. It is not enough to share anything: that requires opening
// devices through IOKit, which has no equivalent here — see the platform
// table in the README.
//
// Going through the command keeps the client cgo-free and therefore
// cross-compilable from any machine.
func Enumerate() ([]Device, error) {
if external := ExternalDevices(); len(external) > 0 {
// Devices handed in from outside are usable; report them first.
return external, nil
}
out, err := exec.Command("system_profiler", "-json", "SPUSBDataType").Output()
if err != nil {
return nil, fmt.Errorf("running system_profiler: %w", err)
}
var report struct {
Items []spUSBItem `json:"SPUSBDataType"`
}
if err := json.Unmarshal(out, &report); err != nil {
return nil, fmt.Errorf("parsing system_profiler output: %w", err)
}
var devices []Device
for _, item := range report.Items {
collectItem(&devices, item)
}
return devices, nil
}
type spUSBItem struct {
Name string `json:"_name"`
VendorID string `json:"vendor_id"`
ProductID string `json:"product_id"`
Speed string `json:"device_speed"`
Manufacturer string `json:"manufacturer"`
SerialNumber string `json:"serial_num"`
LocationID string `json:"location_id"`
Items []spUSBItem `json:"_items"`
}
func collectItem(devices *[]Device, item spUSBItem) {
if item.VendorID != "" {
dev := Device{
BusID: locationToBusID(item.LocationID),
VendorID: parseHexID(item.VendorID),
ProductID: parseHexID(item.ProductID),
Speed: parseSpeedName(item.Speed),
Manufacturer: item.Manufacturer,
Product: item.Name,
Serial: item.SerialNumber,
}
*devices = append(*devices, dev)
}
for _, child := range item.Items {
collectItem(devices, child)
}
}
// parseHexID turns "0x046d (Logitech Inc.)" into 0x046d.
func parseHexID(id string) uint16 {
id = strings.TrimSpace(id)
if i := strings.Index(id, " "); i > 0 {
id = id[:i]
}
id = strings.TrimPrefix(id, "0x")
v, err := strconv.ParseUint(id, 16, 16)
if err != nil {
return 0
}
return uint16(v)
}
// locationToBusID derives an identifier from the location ID, which encodes
// the device's position in the port tree.
func locationToBusID(locationID string) string {
locationID = strings.TrimSpace(locationID)
if i := strings.Index(locationID, " "); i > 0 {
locationID = locationID[:i]
}
return strings.TrimPrefix(locationID, "0x")
}
// parseSpeedName maps system_profiler's wording onto USB/IP speed codes.
func parseSpeedName(speed string) uint32 {
switch {
case strings.Contains(speed, "low_speed"):
return 1
case strings.Contains(speed, "full_speed"):
return 2
case strings.Contains(speed, "high_speed"):
return 3
case strings.Contains(speed, "super_speed_plus"):
return 6
case strings.Contains(speed, "super_speed"):
return 5
default:
return 0
}
}
+67 -21
View File
@@ -12,10 +12,17 @@ import (
const sysfsUSBDevices = "/sys/bus/usb/devices"
// Enumerate lists all USB devices by reading sysfs
// Enumerate lists all USB devices by reading sysfs, plus any device that was
// registered from outside the process (see RegisterExternalDevice).
func Enumerate() ([]Device, error) {
entries, err := os.ReadDir(sysfsUSBDevices)
if err != nil {
// On Android sysfs is not readable by an app, but devices handed in
// through the bridge still work. Only report a failure when there is
// nothing at all to go on.
if external := ExternalDevices(); len(external) > 0 {
return external, nil
}
return nil, fmt.Errorf("reading sysfs: %w", err)
}
@@ -46,7 +53,7 @@ func Enumerate() ([]Device, error) {
devices = append(devices, *dev)
}
return devices, nil
return mergeExternal(devices), nil
}
func isDevicePath(name string) bool {
@@ -93,12 +100,63 @@ func readDevice(busID string) (*Device, error) {
// Compute dev path
dev.DevPath = fmt.Sprintf("/dev/bus/usb/%03d/%03d", dev.BusNum, dev.DevNum)
// Read interfaces
// Read interfaces from sysfs. This gives us the bound kernel driver per
// interface, which the raw descriptors don't contain.
dev.Interfaces = readInterfaces(sysPath, busID)
// Overlay the raw descriptors from the usbdevfs file. Only these expose
// interface alternate settings and correct endpoint attributes; sysfs
// shows just the active alternate setting. Without the non-zero alternate
// settings the endpoint type map is wrong for webcams and audio devices.
applyRawDescriptors(dev)
return dev, nil
}
// applyRawDescriptors reads the device's descriptor blob from its usbdevfs
// file and fills in Endpoints plus any interface data sysfs did not provide.
// Failure is not fatal: reading /dev/bus/usb requires permissions we may not
// have when merely listing devices, and the sysfs data alone is enough for
// that. Sharing a device opens the same file anyway and would fail earlier.
func applyRawDescriptors(dev *Device) {
data, err := os.ReadFile(dev.DevPath)
if err != nil {
return
}
pd, err := ParseDescriptors(data)
if err != nil {
return
}
cfg := pd.FindConfig(dev.ConfigValue)
if cfg == nil {
// The device is unconfigured, or sysfs and the descriptors disagree.
// Fall back to the first configuration.
if len(pd.Configs) == 0 {
return
}
cfg = &pd.Configs[0]
}
dev.Endpoints = cfg.AllEndpoints()
// Merge: keep the driver names from sysfs, take everything else from the
// descriptors (which are authoritative and include endpoint intervals).
drivers := make(map[uint8]string, len(dev.Interfaces))
for _, iface := range dev.Interfaces {
drivers[iface.Number] = iface.Driver
}
ifaces := cfg.ActiveInterfaces()
for i := range ifaces {
ifaces[i].Driver = drivers[ifaces[i].Number]
}
if len(ifaces) > 0 {
dev.Interfaces = ifaces
}
}
func readInterfaces(sysPath, busID string) []Interface {
entries, err := os.ReadDir(sysPath)
if err != nil {
@@ -150,26 +208,14 @@ func readEndpoints(ifacePath string) []Endpoint {
}
epPath := filepath.Join(ifacePath, name)
addr, _ := strconv.ParseUint(readString(epPath, "bEndpointAddress"), 16, 8)
var transferType uint8
switch readString(epPath, "type") {
case "Control":
transferType = TransferTypeControl
case "Isoc":
transferType = TransferTypeIsochronous
case "Bulk":
transferType = TransferTypeBulk
case "Interrupt":
transferType = TransferTypeInterrupt
}
maxPkt := readUint32(epPath, "wMaxPacketSize")
// Every numeric endpoint attribute in sysfs is hex, without a 0x
// prefix — wMaxPacketSize "0040" means 64, not 40.
eps = append(eps, Endpoint{
Address: uint8(addr),
TransferType: transferType,
MaxPacketSize: uint16(maxPkt),
Address: readHex8(epPath, "bEndpointAddress"),
TransferType: readHex8(epPath, "bmAttributes") & 0x03,
MaxPacketSize: readHex16(epPath, "wMaxPacketSize"),
Interval: readHex8(epPath, "bInterval"),
})
}
+215 -3
View File
@@ -2,9 +2,221 @@
package usb
import "fmt"
import (
"fmt"
"log"
"strings"
"unsafe"
// Enumerate lists all USB devices (Windows stub)
"golang.org/x/sys/windows"
)
var (
modsetupapi = windows.NewLazySystemDLL("setupapi.dll")
procSetupDiGetClassDevsW = modsetupapi.NewProc("SetupDiGetClassDevsW")
procSetupDiEnumDeviceInterfaces = modsetupapi.NewProc("SetupDiEnumDeviceInterfaces")
procSetupDiGetDeviceInterfaceDetailW = modsetupapi.NewProc("SetupDiGetDeviceInterfaceDetailW")
procSetupDiDestroyDeviceInfoList = modsetupapi.NewProc("SetupDiDestroyDeviceInfoList")
)
const (
digcfPresent = 0x00000002
digcfDeviceInterface = 0x00000010
)
type spDeviceInterfaceData struct {
CbSize uint32
InterfaceClassGuid windows.GUID
Flags uint32
Reserved uintptr
}
// Enumerate lists USB devices reachable through the usbshare filter driver,
// plus any device registered from outside this process.
//
// Only devices with the filter attached appear: Windows has no equivalent of
// walking /sys/bus/usb, and without the filter there is no way to drive a
// device from user mode anyway, so listing the others would only offer
// devices that cannot actually be shared.
func Enumerate() ([]Device, error) {
return nil, fmt.Errorf("USB enumeration not yet implemented on Windows")
devices, err := enumerateFiltered()
if err != nil {
if external := ExternalDevices(); len(external) > 0 {
return external, nil
}
return nil, err
}
return mergeExternal(devices), nil
}
func enumerateFiltered() ([]Device, error) {
handle, _, _ := procSetupDiGetClassDevsW.Call(
uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
0, 0,
uintptr(digcfPresent|digcfDeviceInterface),
)
if handle == uintptr(windows.InvalidHandle) {
return nil, fmt.Errorf("no USB devices with the usbshare filter found " +
"(install driver/windows/usbshare.inf and attach it to the devices you want to share)")
}
defer procSetupDiDestroyDeviceInfoList.Call(handle)
var devices []Device
for index := uint32(0); ; index++ {
var ifaceData spDeviceInterfaceData
ifaceData.CbSize = uint32(unsafe.Sizeof(ifaceData))
ret, _, _ := procSetupDiEnumDeviceInterfaces.Call(
handle, 0,
uintptr(unsafe.Pointer(&guidDevInterfaceUsbShare)),
uintptr(index),
uintptr(unsafe.Pointer(&ifaceData)),
)
if ret == 0 {
break // no more interfaces
}
devicePath, err := interfaceDetailPath(handle, &ifaceData)
if err != nil {
continue
}
dev, err := describeFilteredDevice(devicePath)
if err != nil {
log.Printf("[usb] skipping %s: %v", devicePath, err)
continue
}
devices = append(devices, *dev)
}
return devices, nil
}
// interfaceDetailPath resolves an interface to the device path used to open it.
func interfaceDetailPath(handle uintptr, ifaceData *spDeviceInterfaceData) (string, error) {
// First call determines the size.
var required uint32
procSetupDiGetDeviceInterfaceDetailW.Call(
handle,
uintptr(unsafe.Pointer(ifaceData)),
0, 0,
uintptr(unsafe.Pointer(&required)),
0,
)
if required == 0 {
return "", fmt.Errorf("could not determine the interface detail size")
}
buf := make([]byte, required)
// SP_DEVICE_INTERFACE_DETAIL_DATA_W starts with cbSize, which must be set
// to the size of the fixed part — 8 on 64-bit, counting the alignment of
// the WCHAR array that follows — not the size of the whole buffer.
*(*uint32)(unsafe.Pointer(&buf[0])) = 8
ret, _, err := procSetupDiGetDeviceInterfaceDetailW.Call(
handle,
uintptr(unsafe.Pointer(ifaceData)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(required),
uintptr(unsafe.Pointer(&required)),
0,
)
if ret == 0 {
return "", fmt.Errorf("reading interface detail: %w", err)
}
// The path is a null-terminated WCHAR string starting after cbSize.
pathPtr := (*uint16)(unsafe.Pointer(&buf[4]))
return windows.UTF16PtrToString(pathPtr), nil
}
// describeFilteredDevice opens a device briefly to read its descriptors.
//
// Claiming it here means the class driver stops seeing it for the duration.
// Enumeration therefore releases immediately: holding the claim would make
// merely listing devices disrupt whatever is using them.
func describeFilteredDevice(devicePath string) (*Device, error) {
handle, err := OpenDriverDevice(devicePath)
if err != nil {
return nil, err
}
defer handle.Close()
descriptors, err := handle.Descriptors()
if err != nil {
return nil, fmt.Errorf("reading descriptors: %w", err)
}
parsed, err := ParseDescriptors(descriptors)
if err != nil {
return nil, fmt.Errorf("parsing descriptors: %w", err)
}
info := handle.Info()
cfg := parsed.FindConfig(info.ConfigurationValue)
if cfg == nil {
cfg = &parsed.Configs[0]
}
dev := &Device{
BusID: busIDFromPath(devicePath),
BusNum: 0,
DevNum: uint32(info.PortNumber),
Speed: translateWindowsSpeed(info.Speed),
VendorID: parsed.VendorID,
ProductID: parsed.ProductID,
BcdDevice: parsed.BcdDevice,
DeviceClass: parsed.DeviceClass,
DeviceSubClass: parsed.DeviceSubClass,
DeviceProtocol: parsed.DeviceProtocol,
ConfigValue: cfg.Value,
NumConfigs: parsed.NumConfigs,
DevPath: devicePath,
Interfaces: cfg.ActiveInterfaces(),
Endpoints: cfg.AllEndpoints(),
}
return dev, nil
}
// busIDFromPath derives a stable identifier from a Windows device path.
//
// Paths look like \\?\usb#vid_046d&pid_c52b#5&1a2b3c4d&0&2#{guid}. The
// instance part is stable for as long as the device stays in the same port,
// which is what peers need: they request devices by this ID.
func busIDFromPath(devicePath string) string {
trimmed := strings.TrimPrefix(devicePath, `\\?\`)
if idx := strings.LastIndex(trimmed, "#{"); idx > 0 {
trimmed = trimmed[:idx]
}
// '#' separates the parts; '&' appears inside them. Neither is a problem
// for transport, but a shorter, more readable ID helps in the UI.
parts := strings.Split(trimmed, "#")
if len(parts) >= 3 {
return strings.ReplaceAll(parts[2], "&", "-")
}
return strings.ReplaceAll(trimmed, "#", "-")
}
// translateWindowsSpeed maps USB_DEVICE_SPEED onto the USB/IP speed codes.
func translateWindowsSpeed(speed uint32) uint32 {
switch speed {
case 0: // UsbLowSpeed
return 1
case 1: // UsbFullSpeed
return 2
case 2: // UsbHighSpeed
return 3
case 3: // UsbSuperSpeed
return 5
default:
return 0
}
}
+127
View File
@@ -0,0 +1,127 @@
package usb
import (
"fmt"
"sync"
)
// Externally registered devices.
//
// Normally devices are found by walking sysfs. That is not available to an
// unprivileged Android app, which must go through the framework: it enumerates
// devices itself, asks the user for permission, and receives an already-open
// file descriptor plus the raw descriptor blob. Those devices are registered
// here and merged into the enumeration, so everything above this layer works
// the same whether a device came from sysfs or from outside.
var (
externalMu sync.RWMutex
externalDevices = make(map[string]Device)
)
// RegisterExternalDevice adds a device that was discovered outside this
// process. descriptors is the raw blob (device descriptor followed by
// configuration descriptors), exactly what a usbdevfs file read returns and
// what Android's UsbDeviceConnection.getRawDescriptors() provides.
func RegisterExternalDevice(busID string, descriptors []byte, meta ExternalDeviceMeta) error {
if busID == "" {
return fmt.Errorf("bus ID is required")
}
parsed, err := ParseDescriptors(descriptors)
if err != nil {
return fmt.Errorf("parsing descriptors for %s: %w", busID, err)
}
cfg := parsed.FindConfig(meta.ConfigValue)
if cfg == nil {
cfg = &parsed.Configs[0]
}
dev := Device{
BusID: busID,
BusNum: meta.BusNum,
DevNum: meta.DevNum,
Speed: meta.Speed,
VendorID: parsed.VendorID,
ProductID: parsed.ProductID,
BcdDevice: parsed.BcdDevice,
DeviceClass: parsed.DeviceClass,
DeviceSubClass: parsed.DeviceSubClass,
DeviceProtocol: parsed.DeviceProtocol,
ConfigValue: cfg.Value,
NumConfigs: parsed.NumConfigs,
Manufacturer: meta.Manufacturer,
Product: meta.Product,
Serial: meta.Serial,
Interfaces: cfg.ActiveInterfaces(),
Endpoints: cfg.AllEndpoints(),
}
externalMu.Lock()
externalDevices[busID] = dev
externalMu.Unlock()
return nil
}
// ExternalDeviceMeta carries the fields that cannot be read from the
// descriptor blob because they describe the device's place on the bus or come
// from string descriptors the caller already resolved.
type ExternalDeviceMeta struct {
BusNum uint32
DevNum uint32
Speed uint32
ConfigValue uint8
Manufacturer string
Product string
Serial string
}
// UnregisterExternalDevice removes a device registered from outside.
func UnregisterExternalDevice(busID string) {
externalMu.Lock()
delete(externalDevices, busID)
externalMu.Unlock()
}
// ExternalDevices returns a snapshot of the externally registered devices.
func ExternalDevices() []Device {
externalMu.RLock()
defer externalMu.RUnlock()
result := make([]Device, 0, len(externalDevices))
for _, dev := range externalDevices {
result = append(result, dev)
}
return result
}
// HasExternalDevices reports whether any device came from outside.
func HasExternalDevices() bool {
externalMu.RLock()
defer externalMu.RUnlock()
return len(externalDevices) > 0
}
// mergeExternal appends externally registered devices to a list from sysfs,
// letting the external entry win on a bus ID collision — it carries a file
// descriptor we can actually use, which the sysfs entry may not.
func mergeExternal(devices []Device) []Device {
externalMu.RLock()
defer externalMu.RUnlock()
if len(externalDevices) == 0 {
return devices
}
result := make([]Device, 0, len(devices)+len(externalDevices))
for _, dev := range devices {
if _, overridden := externalDevices[dev.BusID]; !overridden {
result = append(result, dev)
}
}
for _, dev := range externalDevices {
result = append(result, dev)
}
return result
}
+116 -14
View File
@@ -3,8 +3,10 @@
package usb
import (
"errors"
"fmt"
"os"
"time"
"unsafe"
"golang.org/x/sys/unix"
@@ -81,7 +83,7 @@ type usbdevfsBulkTransfer struct {
}
type usbdevfsSetIntf struct {
Interface uint32
Interface uint32
AltSetting uint32
}
@@ -117,7 +119,7 @@ type usbdevfsURB struct {
NumberOfPackets int32 // or StreamID
ErrorCount int32
Signr uint32
UserContext uintptr
UserContext uintptr
// ISO packet descriptors follow in memory if Type == urbTypeISO
}
@@ -126,10 +128,25 @@ type DeviceHandle struct {
fd int
busID string
devPath string
// adopted marks a descriptor handed to us from outside rather than
// opened here. It is closed on Close like any other, but the distinction
// matters for diagnostics: an adopted descriptor means the host process
// could not have opened the device itself.
adopted bool
}
// OpenDevice opens a USB device file for direct access
// OpenDevice opens a USB device file for direct access.
//
// If an external file descriptor has been registered for this device (see
// AdoptDeviceFD) it is used instead of opening the path. That is how Android
// works: apps cannot open /dev/bus/usb themselves, so a small Java shim asks
// the system for permission and hands the resulting descriptor down.
func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
if fd, ok := takeAdoptedFD(busID); ok {
return &DeviceHandle{fd: fd, busID: busID, devPath: devPath, adopted: true}, nil
}
fd, err := unix.Open(devPath, unix.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", devPath, err)
@@ -293,7 +310,7 @@ type SubmitURBParams struct {
Endpoint uint8
Flags uint32
Buffer []byte
UserContext uintptr
UserContext uintptr
}
// SubmitURB submits an asynchronous URB
@@ -310,7 +327,7 @@ func (h *DeviceHandle) SubmitURB(params *SubmitURBParams) (*usbdevfsURB, error)
Buffer: bufPtr,
BufferLength: int32(len(params.Buffer)),
NumberOfPackets: -1, // 0xFFFFFFFF for non-ISO
UserContext: params.UserContext,
UserContext: params.UserContext,
}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsSubmitURB, uintptr(unsafe.Pointer(urb)))
@@ -320,6 +337,20 @@ func (h *DeviceHandle) SubmitURB(params *SubmitURBParams) (*usbdevfsURB, error)
return urb, nil
}
// urbFromKernelPtr converts the uintptr USBDEVFS_REAPURB writes back into a
// *usbdevfsURB.
//
// go vet flags this as "possible misuse of unsafe.Pointer", correctly in
// general: the garbage collector cannot see a pointer stored in a uintptr, so
// the object could be collected before the conversion. It is safe here because
// the kernel only ever returns a pointer we submitted ourselves, and the
// caller keeps that URB reachable — in pendingURBs or unlinkedURBs on the
// server — from submission until after it has been reaped.
func urbFromKernelPtr(p uintptr) *usbdevfsURB {
//nolint:govet // see the comment above
return (*usbdevfsURB)(unsafe.Pointer(p))
}
// ReapURB blocks until a URB completes, then returns it
func (h *DeviceHandle) ReapURB() (*usbdevfsURB, error) {
var urbPtr uintptr
@@ -327,7 +358,7 @@ func (h *DeviceHandle) ReapURB() (*usbdevfsURB, error) {
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
}
return (*usbdevfsURB)(unsafe.Pointer(urbPtr)), nil
return urbFromKernelPtr(urbPtr), nil
}
// ReapURBNonBlock tries to reap a URB without blocking
@@ -337,7 +368,7 @@ func (h *DeviceHandle) ReapURBNonBlock() (*usbdevfsURB, error) {
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURBNDELAY: %w", errno)
}
return (*usbdevfsURB)(unsafe.Pointer(urbPtr)), nil
return urbFromKernelPtr(urbPtr), nil
}
// DiscardURB cancels a submitted URB
@@ -450,10 +481,81 @@ func ReadISOResults(mem []byte, numPackets int32) []ISOPacketResult {
// ReapedURBInfo holds exported fields from a reaped URB needed for response building
type ReapedURBInfo struct {
UserContext uintptr
Status int32
Status int32
ActualLength int32
StartFrame int32
ErrorCount int32
StartFrame int32
ErrorCount int32
}
// ErrNoURBReady is returned by ReapURBInfoNonBlock when no URB has completed.
var ErrNoURBReady = errors.New("no completed URB available")
// ErrDeviceGone is returned when the device has been unplugged or the file
// descriptor is no longer usable.
var ErrDeviceGone = errors.New("device gone")
// WaitForURB waits up to timeout for at least one URB to complete.
// It returns true if a URB is ready to be reaped, false on timeout.
//
// usbdevfs signals completed URBs via POLLOUT, so polling lets the reap loop
// stay responsive to shutdown without either spinning on a non-blocking ioctl
// or blocking indefinitely in USBDEVFS_REAPURB. The latter matters: a blocking
// reap can only be broken by closing the fd, which races with the fd being
// reused by another goroutine.
func (h *DeviceHandle) WaitForURB(timeout time.Duration) (bool, error) {
fds := []unix.PollFd{{Fd: int32(h.fd), Events: unix.POLLOUT}}
ms := int(timeout.Milliseconds())
if ms < 0 {
ms = 0
}
for {
n, err := unix.Poll(fds, ms)
if err == unix.EINTR {
continue // interrupted by a signal, not an error
}
if err != nil {
return false, fmt.Errorf("poll: %w", err)
}
if n == 0 {
return false, nil // timeout
}
// POLLERR/POLLHUP/POLLNVAL mean the device is gone or the fd was closed.
if fds[0].Revents&(unix.POLLERR|unix.POLLHUP|unix.POLLNVAL) != 0 {
return false, ErrDeviceGone
}
return fds[0].Revents&unix.POLLOUT != 0, nil
}
}
// ReapURBInfoNonBlock reaps one completed URB without blocking.
// Returns ErrNoURBReady if none has completed, ErrDeviceGone if the device
// has been disconnected.
func (h *DeviceHandle) ReapURBInfoNonBlock() (*ReapedURBInfo, error) {
var urbPtr uintptr
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(h.fd), usbdevfsReapURBNDelay, uintptr(unsafe.Pointer(&urbPtr)))
if errno != 0 {
switch errno {
case unix.EAGAIN:
return nil, ErrNoURBReady
case unix.ENODEV, unix.ESHUTDOWN, unix.EBADF, unix.ENOENT:
return nil, ErrDeviceGone
default:
return nil, fmt.Errorf("USBDEVFS_REAPURBNDELAY: %w", errno)
}
}
if urbPtr == 0 {
return nil, ErrNoURBReady
}
urb := urbFromKernelPtr(urbPtr)
return &ReapedURBInfo{
UserContext: urb.UserContext,
Status: urb.Status,
ActualLength: urb.ActualLength,
StartFrame: urb.StartFrame,
ErrorCount: urb.ErrorCount,
}, nil
}
// ReapURBInfo blocks until a URB completes and returns exported info
@@ -463,13 +565,13 @@ func (h *DeviceHandle) ReapURBInfo() (*ReapedURBInfo, error) {
if errno != 0 {
return nil, fmt.Errorf("USBDEVFS_REAPURB: %w", errno)
}
urb := (*usbdevfsURB)(unsafe.Pointer(urbPtr))
urb := urbFromKernelPtr(urbPtr)
return &ReapedURBInfo{
UserContext: urb.UserContext,
Status: urb.Status,
Status: urb.Status,
ActualLength: urb.ActualLength,
StartFrame: urb.StartFrame,
ErrorCount: urb.ErrorCount,
StartFrame: urb.StartFrame,
ErrorCount: urb.ErrorCount,
}, nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build darwin
package usb
import "fmt"
// Device access on macOS would go through IOKit, which has no counterpart to
// usbdevfs: there is no device node to open and drive with ioctls. Providing
// it means writing an IOKit backend with cgo, which is a separate piece of
// work — see the platform table in the README.
//
// These stubs exist so the client builds and its other functions (listing
// devices, diagnostics, the relay, the web UI) work on macOS.
type DeviceHandle struct{}
func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
return nil, fmt.Errorf("sharing USB devices is not implemented on macOS " +
"(needs an IOKit backend); this machine can still run the relay")
}
func (h *DeviceHandle) Close() error { return nil }
func (h *DeviceHandle) Fd() int { return -1 }
func (h *DeviceHandle) DisconnectDriver() error { return fmt.Errorf("not implemented on macOS") }
func (h *DeviceHandle) ConnectDriver() error { return fmt.Errorf("not implemented on macOS") }
func (h *DeviceHandle) ClaimInterface(uint32) error { return fmt.Errorf("not implemented on macOS") }
func (h *DeviceHandle) ReleaseInterface(uint32) error { return fmt.Errorf("not implemented on macOS") }
+1 -1
View File
@@ -11,7 +11,7 @@ func OpenDevice(devPath string, busID string) (*DeviceHandle, error) {
return nil, fmt.Errorf("USB device access not yet implemented on Windows")
}
func (h *DeviceHandle) Close() error { return nil }
func (h *DeviceHandle) Close() error { return nil }
func (h *DeviceHandle) Fd() int { return -1 }
func (h *DeviceHandle) DisconnectDriver() error { return fmt.Errorf("not implemented") }
func (h *DeviceHandle) ConnectDriver() error { return fmt.Errorf("not implemented") }