Repository navigation
feat: redirect USB devices over URBDRC with nusb and RemoteFX USB backends - #2181
Marc-André Moreau (mamoreau-devolutions) merged 44 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Compilation, device-selection safety, allocation bounds, and USB lifecycle correctness issues remain unresolved.
6 open issues
Cancel submitted control transfers on task abort · New Bound server-supplied transfer lengths before allocation · New Preserve USB instance-specific device identity · New Import or qualify missing Pin type · New Correct feature descriptor interface handling · New Start hotplug watcher for address-only rules · New
What changed in this PR
Adds an nusb-backed USB redirection backend and integrates it into the client and viewer, keeping host I/O outside the protocol crate.
Changes:
- Implements device discovery, filtering, hotplug handling, and USB request processing.
- Adds client feature flags, configuration APIs, and ordered backend events.
- Exposes viewer USB selection and device listing, with platform-limit documentation.
| File | Description |
|---|---|
| crates/ironrdp/Cargo.toml | Exposes client-usb. |
| crates/ironrdp-viewer/src/cli.rs | Adds USB selection and listing. |
| crates/ironrdp-rdpeusb/README.md | Links the native backend. |
| crates/ironrdp-rdpeusb-native/src/urb.rs | Translates requests and builds completions. |
| crates/ironrdp-rdpeusb-native/src/manager.rs | Manages discovery and hotplug events. |
| crates/ironrdp-rdpeusb-native/src/lib.rs | Exposes listener and backend events. |
| crates/ironrdp-rdpeusb-native/src/filter.rs | Parses and matches device-selection rules. |
| crates/ironrdp-rdpeusb-native/src/device.rs | Implements host-device workers and transfers. |
| crates/ironrdp-rdpeusb-native/README.md | Documents usage and platform limitations. |
| crates/ironrdp-rdpeusb-native/LICENSE-MIT | Links the MIT license. |
| crates/ironrdp-rdpeusb-native/LICENSE-APACHE | Links the Apache license. |
| crates/ironrdp-rdpeusb-native/Cargo.toml | Defines backend dependencies. |
| crates/ironrdp-client/src/urbdrc.rs | Routes backend events through device channels. |
| crates/ironrdp-client/src/rdp.rs | Registers USB support and processes events. |
| crates/ironrdp-client/src/lib.rs | Re-exports the backend. |
| crates/ironrdp-client/src/config.rs | Adds USB configuration and mapping tests. |
| crates/ironrdp-client/README.md | Documents optional USB support. |
| crates/ironrdp-client/Cargo.toml | Adds the USB feature and dependencies. |
| crates/ironrdp-cfg/src/lib.rs | Adds USB property accessors. |
| Cargo.lock | Records backend dependencies. |
Review effort: Balanced
There was a problem hiding this comment.
USB redirection over URBDRC with an nusb backend: protocol review found two spec deviations (isochronous capability advertised but unsupported; IN overrun not terminating the channel); structural review confirmed the published client now depends on publish=false crates in its public API, blocking device opens on the session loop, broken LICENSE blobs, a process exit in config construction, and a colliding Windows bus-id fold; plus six behavior-preserving compression opportunities. The unmanaged-MSRV claim was rejected: dozens of existing workspace crates already declare rust-version = "1.94".
- [skeptical] `--usb list` terminates the process from inside ViewerConfig::from_args — low 🟡 — crates/ironrdp-viewer/src/cli.rs
`--usb list` makes print_usb_devices run and then calls std::process::exit(0) inside ViewerConfig::from_args, a config-construction function that now carries a process-terminating side effect: destructors of already-built values are skipped, and callers cannot compose or test from_args without risking an exit. Listing mode is CLI dispatch and belongs at the argument-parsing layer alongside the other early-exit surfaces.
Push a commit after addressing these findings. If no code change is needed, you may resolve inline threads and comment @github-actions review-ready to request human review.
Add UsbRedirectionConfig::all for devices present at connection, alongside auto for hotplug, instead of storing UsbDeviceMatch::Any in the device rules and scanning for it. Any remains for exclusions (-*), and as a device rule it now selects every device like any explicit rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 2180 makes ironrdp-rdpeusb publishable with the same edit minus the readme key, which Cargo infers from README.md anyway. Keeping the two identical lets them merge in either order without a conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drive redirection change added ViewerConfig::named_drives, which the early return for --usb list did not set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsistent - A bulk or interrupt transfer that nusb cancels only because the server cancelled another request on the endpoint now resumes where it stopped, instead of completing as a successful short transfer. Transfers that the server cancelled or whose pipe was aborted complete as cancelled, even when some data moved. - Spawned control requests complete through the device worker, after leaving the running requests, so that a request reusing the ID can still be cancelled. - A device unplugged before its device channel exists keeps its place in the announcement queue, so the channel created for it is refused rather than handed the next device. The listener takes the device before allocating an interface ID, and the client skips announcing a device that is already unplugged. - Hotplugged devices are opened right away, with retries in their own task, so unplugs are still followed while a driver loads; devices are identified once instead of twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Opening a plugged device stops when it is unplugged, and a replug replaces the attempts in progress instead of racing them for its interfaces. - A device that is already redirected, for example by the enumeration at connection time, is not opened again, which only failed and warned. - IN transfers reuse nusb's buffer unless they resumed after a cancellation, and a transfer cancelled before moving any data runs again with the same buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st configuration Master replaced ViewerConfig's redirect_printers flag with optional printer options and added the audio device listing flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REGISTER_REQUEST_CALLBACK rejected request completion interface IDs below 4, as if they clashed with the control channel's default interfaces. The server allocates this ID in its own space and only uses it to receive completions, and Windows Server 2025 picks one below 4: the device channel failed to decode, which ended the whole session right after a device was announced. Found by redirecting a YubiKey to Windows Server 2025. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_REQUEST Windows submits control requests without a data stage, including OUT ones such as a HID SET_IDLE, as TRANSFER_IN_REQUESTs whose transfer flags keep the OUT direction. Decoding rejected them, which ended the session as soon as a HID device was redirected to Windows Server 2025. Control, control-ex and vendor or class requests are no longer checked for the IN direction when decoding a TRANSFER_IN_REQUEST; bulk, interrupt and isochronous transfers still are. The nusb backend now sends a vendor or class request whose transfer flags say OUT as an OUT request without data, instead of an IN one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EST carries Decoding now accepts OUT control requests without data in a TRANSFER_IN_REQUEST, as Windows sends them. Encoding still rejected them, so a backend that hands the TS_URB to the operating system, such as the Windows RemoteFX USB redirection driver, could not pass them on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evict late-data history by close order instead of channel ID. Clear it only after a restored channel is successfully attached. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Scope unplug events and registry cleanup to the device channel. Keep cancelled controls charged until native completion. Use task identity to reject results for reused request IDs. Report partial transfer progress on failures and cancellations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The client library disables its default unit-test target. Run USB property mapping checks in the extra integration suite. Enable the USB feature explicitly rather than relying on unification. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Master added the RPCH gateway transport, whose redirection flags are mapped onto the HTTP tunnel ones. Its pnp_disabled flag was not mapped, so USB redirection stayed on through an RPCH gateway that disables Plug and Play device redirection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backends that hand TS_URBs to the operating system as they are, such as the Windows RemoteFX USB redirection driver, need the wire form of a transfer request back and must decode the TS_URB_RESULT it produces. TsUrbInPacket and TsUrbOutPacket gain encode_ts_urb and decode_ts_urb_result for that, reusing the server's knowledge of which result each request expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…teFX USB Windows ships the drivers mstsc redirects USB devices with: a filter on every USB hub (TsUsbFlt), through which a process claims devices, and a generic redirection driver (TsUsbGD) that the claimed devices are re-enumerated with. When the fUsbRedirectionEnableMode policy lets the current process claim devices, the backend now uses them instead of nusb, which reaches class devices (HID, storage, smart cards, cameras) and not only WinUSB ones, and supports isochronous transfers and the cancellation of a single request. - Devices are selected by the redirection rules and described from their descriptors before they are claimed, then claimed by their proprietary ID; claimed devices are found as device interfaces of the per-handle claim GUID and opened under it. - TS_URBs are re-encoded and submitted as overlapped I/O on a completion port; the driver writes the TS_URB_RESULT over the request. - A device is released when its channel closes, when the server retracts it, or when the connection ends. - The device manager now serves any RedirectableDevice, so both backends share announcement and unplug handling. The driver interface is undocumented and was reverse-engineered from the Windows 11 binaries. A YubiKey 5 redirected to Windows Server 2025 enumerated with all of its interfaces, and answered a CTAPHID ping in the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # crates/ironrdp-client/Cargo.toml
A failed request completes with a header-only TS_URB_RESULT, whatever result its success carries: Windows does so, and so do both IronRDP backends. Decoding rejected it for select-configuration, select-interface, current-frame and isochronous requests, which failed the server-side device channel and made the RemoteFX USB backend report REQUEST_FAILED instead of the real status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A channel the client closed while the server was choosing tunnels is still listed in the Soft-Sync request, and the server may already send its data on the tunnel. That data failed the connection; the closed channel is now remembered on the tunnel too, and its data dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n order nusb cancels every transfer of an endpoint at once, and transfers cancelled along with another one run again later. A transfer after them that finished meanwhile was reported first. Each transfer now gets its position on the endpoint, and is reported once every earlier one finished, which also orders zero-length IN requests completed without the device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…those not ready Unplugging a device closed its channel, whose release then kept the device from being claimed again during the connection. An unplugged device is now forgotten without that, and only devices given back while still plugged in stay unclaimed. A device just plugged in may not be known to the hub filter yet, and a claim may fail; such devices were never tried again. They are now retried at the next scans, up to five times, under the rules that selected them, and a failed claim is rolled back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sUsbGD - The HRESULT of a transfer follows its USBD status instead of always being S_OK, so server drivers see failures and recover. - An isochronous packet whose bytes are not in the buffer is reported as failed, so the packed data matches the successful packets' lengths. - Writes without acknowledgement are not tracked for cancellation, and a request only removes its own tracking entry, so a reused request ID stays cancellable. - Removing the last request in flight wakes the shutdown wait on every path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Adds host-device backends for the MS-RDPEUSB (URBDRC) client and wires them into
ironrdp-clientand the viewer, so local USB devices can be redirected to the server. On Windows it redirects USB the way mstsc does, through the in-box RemoteFX USB drivers, when policy allows it, and falls back to nusb otherwise. It also fixes three interop bugs inironrdp-rdpeusbfound by redirecting a device to Windows Server 2025.Device coverage
TsUsbFltandTsUsbGD, the drivers mstsc uses. Isochronous transfers and the cancellation of a single request are supported. ThefUsbRedirectionEnableModepolicy ("Allow RDP redirection of other supported RemoteFX USB devices from this computer") decides who may claim devices: nobody by default, elevated administrators, or every user./dev/bus/usb.nusb was chosen for the portable backend because it's pure Rust and supports hotplug on Windows, Linux and macOS. The full list of limits is in
crates/ironrdp-rdpeusb-native/README.md.Changes
ironrdp-rdpeusb-native, publishable likeironrdp-rdpeusb. It implementsDeviceManagerBackendandUrbdrcDeviceBackend. Both backends share device selection, announcement and unplug handling through a smallRedirectableDevicetrait.nusb backend (all platforms):
The translation code has no I/O and is unit-tested. Each device gets its own worker thread with a single-threaded Tokio runtime and one ordered queue per endpoint. nusb can only cancel a whole endpoint, so transfers cancelled only because another request was cancelled resume where they stopped.
RemoteFX USB backend (Windows): claims the selected devices through the hub filter's control object and finds them, re-enumerated under the generic redirection driver, as interfaces of the per-connection claim GUID.
TS_URBs are re-encoded and submitted as overlapped I/O on a completion port; the driver writes eachTS_URB_RESULTover its request.The driver interface is undocumented and was reverse-engineered from the Windows 11 binaries.
/usbrules (id:VID:PID#...,addr:BUS:ADDR#...,auto,filter:CLASS#.../filter:none) and the.rdpusbdevicestoredirectproperty (*,USB\VID_xxxx&PID_yyyy..., device instance IDs,-exclusions).autoand*keep audio, HID, mass storage, hubs and smart cards local by default, as FreeRDP does. Both backends select devices with the same rules.ironrdp-client: newusbfeature, included inall, that registers the URBDRC listener.RdpInputSender::send_urbdrc, newRdpInputEvent::Urbdrcvariant). The bounded input queue drops events when full, and a lost completion would leave the server driver waiting forever.ConfigBuilder::with_usb_redirection.usbdevicestoredirectis mapped inwith_property_setthrough a newironrdp-cfgaccessor.ironrdp_client::usb, andironrdpgains aclient-usbfeature.ironrdp-rdpeusb(client side):CANCEL_REQUESTkeeps the request pending until the backend completes it, so the server-side IRP finishes; FreeRDP does the same.DeviceInfo::isochronous:ADD_DEVICEadvertises a nonzero no-ack isochronous jitter buffer only when the backend performs isochronous transfers.TsUrbInPacketandTsUrbOutPacketgainencode_ts_urbanddecode_ts_urb_result, for backends that handTS_URBs to the operating system as they are.REGISTER_REQUEST_CALLBACKaccepted only request completion interface IDs of 4 or more, but Windows picks one below 4. The device channel failed to decode and the whole session ended right after a device was announced.SET_IDLE, inTRANSFER_IN_REQUESTmessages whose flags keep the OUT direction. They were rejected when decoded and when encoded. Bulk, interrupt and isochronous transfers are still checked.ironrdp-dvc: a client-initiatedDYNVC_CLOSEnow tolerates data that races it, with bounded bookkeeping that keeps the close order and lets a channel ID be reused.close_channel_batchreturns the Soft-Sync tunnel the channel used, so the close travels on it.--usb RULES, repeatable, overrides the.rdpvalue.--usb listprints the local devices with the values the rules match against and, on Windows, each device's driver.RedirectDevicesis left asE_NOTIMPL. It switches Plug and Play device redirection (MS-RDPEPNP), not URBDRC.Testing
cargo xtask check fmt -v,cargo xtask check lints -vandcargo xtask check locks -vpass.Unit and integration tests pass:
ironrdp-rdpeusb-native(57);ironrdp-clientwith all features (68);ironrdp-testsuite-core(144);ironrdp-testsuite-extra(70).They cover URB translation, cancellation and resumed transfers, select-configuration results, completion status and HRESULT, IOCTLs, filter and
.rdpparsing, the config and gateway policy mapping, the RemoteFX USB request buffers and status mapping, and regression tests for each interop fix.On Windows,
--usb listenumerates the local devices.Live, against Windows Server 2025, with the RemoteFX USB backend and the policy set to every user, repeated on this PR's head:
INITsent to its FIDO interface in the session got its nonce echoed back, which exercises interrupt OUT and IN transfers end to end.fDisablePNPRedir = 0to offer URBDRC; it isn't offered in remote administration mode.Not verified:
🤖 Generated with Claude Code