Files
usb-server/driver/windows/public.h
T
duffyduckandClaude Opus 5 9ed473a965 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>
2026-08-11 22:02:04 +02:00

163 lines
4.8 KiB
C

/*
* usbshare - public interface
*
* Shared between the kernel driver and the user mode client. Keep this file
* in sync with internal/usb/driver_windows.go: both sides marshal the same
* structures, and a mismatch corrupts memory rather than failing cleanly.
*/
#pragma once
#include <initguid.h>
/*
* Device interface GUID. User mode enumerates this to find devices that have
* the filter attached.
*
* Generate a fresh GUID if you fork this driver: two drivers exposing the
* same interface would be indistinguishable to clients.
*/
// {8F3D2A14-6C7B-4E59-9A1D-3F5B7C8E2D40}
DEFINE_GUID(GUID_DEVINTERFACE_USBSHARE,
0x8f3d2a14, 0x6c7b, 0x4e59, 0x9a, 0x1d, 0x3f, 0x5b, 0x7c, 0x8e, 0x2d, 0x40);
#define USBSHARE_DEVICE_TYPE 0x8000
#define USBSHARE_IOCTL(index) \
CTL_CODE(USBSHARE_DEVICE_TYPE, 0x800 + (index), METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
* Take exclusive control of the device.
*
* While claimed the filter stops passing the class driver's requests down, so
* the device stops responding to the local system and answers only to URBs
* submitted here. The claim is bound to the file handle: closing it — or the
* process dying — releases the device, which is what stops a crashed client
* from leaving hardware permanently stuck.
*
* Input: none
* Output: USBSHARE_DEVICE_INFO
*/
#define IOCTL_USBSHARE_CLAIM USBSHARE_IOCTL(0)
/* Release the device back to its class driver. Input/output: none. */
#define IOCTL_USBSHARE_RELEASE USBSHARE_IOCTL(1)
/*
* Read the raw descriptor blob: device descriptor followed by every
* configuration descriptor, the same layout a Linux usbdevfs read returns.
*
* Input: none
* Output: raw bytes; STATUS_BUFFER_TOO_SMALL reports the needed size
*/
#define IOCTL_USBSHARE_GET_DESCRIPTORS USBSHARE_IOCTL(2)
/*
* Submit a transfer. Completion is asynchronous: the request stays pending
* until the device answers.
*
* Input: USBSHARE_TRANSFER followed by the payload for OUT transfers
* Output: USBSHARE_TRANSFER_RESULT followed by the payload for IN transfers
*/
#define IOCTL_USBSHARE_SUBMIT USBSHARE_IOCTL(3)
/*
* Cancel a previously submitted transfer.
*
* Input: USBSHARE_CANCEL
* Output: none
*/
#define IOCTL_USBSHARE_CANCEL USBSHARE_IOCTL(4)
/* Select an alternate setting. Input: USBSHARE_SET_INTERFACE. */
#define IOCTL_USBSHARE_SET_INTERFACE USBSHARE_IOCTL(5)
/* Clear a stall on an endpoint. Input: USBSHARE_CLEAR_HALT. */
#define IOCTL_USBSHARE_CLEAR_HALT USBSHARE_IOCTL(6)
/* Reset the port. Input/output: none. */
#define IOCTL_USBSHARE_RESET USBSHARE_IOCTL(7)
#pragma pack(push, 1)
/* Transfer types, matching the USB endpoint attribute values. */
#define USBSHARE_TRANSFER_CONTROL 0
#define USBSHARE_TRANSFER_ISOCHRONOUS 1
#define USBSHARE_TRANSFER_BULK 2
#define USBSHARE_TRANSFER_INTERRUPT 3
/* Direction, taken from the endpoint address bit 7. */
#define USBSHARE_DIR_OUT 0
#define USBSHARE_DIR_IN 1
typedef struct _USBSHARE_DEVICE_INFO {
USHORT VendorId;
USHORT ProductId;
USHORT BcdDevice;
UCHAR DeviceClass;
UCHAR DeviceSubClass;
UCHAR DeviceProtocol;
UCHAR ConfigurationValue;
UCHAR NumConfigurations;
/* USB_DEVICE_SPEED_* from usbdi.h, translated by the client. */
ULONG Speed;
/* Hub port number, used to build a stable bus ID. */
ULONG PortNumber;
} USBSHARE_DEVICE_INFO, *PUSBSHARE_DEVICE_INFO;
typedef struct _USBSHARE_TRANSFER {
/* Caller-assigned, unique among outstanding transfers. Used to cancel. */
ULONG64 Id;
/* Full bEndpointAddress including the direction bit. */
UCHAR EndpointAddress;
/* USBSHARE_TRANSFER_* */
UCHAR Type;
/* USBSHARE_DIR_*, redundant with the address bit but explicit. */
UCHAR Direction;
UCHAR Reserved;
/* Bytes of payload following this header (OUT), or expected (IN). */
ULONG BufferLength;
/* Milliseconds; 0 means no timeout. */
ULONG Timeout;
/*
* Setup packet for control transfers, in USB wire order (little endian).
* Ignored for other types.
*/
UCHAR Setup[8];
} USBSHARE_TRANSFER, *PUSBSHARE_TRANSFER;
typedef struct _USBSHARE_TRANSFER_RESULT {
ULONG64 Id;
/* NTSTATUS from the USB stack; 0 means success. */
LONG Status;
/* USBD_STATUS, kept separate because it distinguishes stall from timeout. */
ULONG UsbdStatus;
/* Bytes actually transferred. Meaningful for OUT transfers too. */
ULONG ActualLength;
} USBSHARE_TRANSFER_RESULT, *PUSBSHARE_TRANSFER_RESULT;
typedef struct _USBSHARE_CANCEL {
ULONG64 Id;
} USBSHARE_CANCEL, *PUSBSHARE_CANCEL;
typedef struct _USBSHARE_SET_INTERFACE {
UCHAR InterfaceNumber;
UCHAR AlternateSetting;
} USBSHARE_SET_INTERFACE, *PUSBSHARE_SET_INTERFACE;
typedef struct _USBSHARE_CLEAR_HALT {
UCHAR EndpointAddress;
} USBSHARE_CLEAR_HALT, *PUSBSHARE_CLEAR_HALT;
#pragma pack(pop)