Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/ironrdp-activex/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["cam
ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" }
ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.3.0" }
ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.8" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.3" }
ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" }
ironrdp-core = { path = "../ironrdp-core", version = "0.3" }
ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" }
ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9" }
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4" }
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.5" }
ironrdp-input = { path = "../ironrdp-input", version = "0.8" }
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" }
ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.2" }
Expand All @@ -39,7 +39,7 @@ ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" }
ironrdp-rail = { path = "../ironrdp-rail", version = "0.2" }
ironrdp-rdpdr-native = { path = "../ironrdp-rdpdr-native", version = "0.8" }
ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" }
ironrdp-session = { path = "../ironrdp-session", version = "0.12" }
ironrdp-session = { path = "../ironrdp-session", version = "0.13" }
ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" }
ironrdp-tls = { path = "../ironrdp-tls", version = "0.3" }
png = "0.18"
Expand Down
4 changes: 2 additions & 2 deletions crates/ironrdp-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test = false
[dependencies]
# Configuration model and codecs
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.3" }
ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1" }
ironrdp-input = { path = "../ironrdp-input", version = "0.8" }
ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" }
Expand All @@ -38,7 +38,7 @@ clap = { version = "4.6", features = ["derive", "cargo", "env"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", features = ["rustls"] }
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.3", features = ["rustls"] }

# Inline terminal screenshots
base64 = "0.22"
Expand Down
8 changes: 4 additions & 4 deletions crates/ironrdp-capture-replay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ categories.workspace = true
aes-gcm = "0.10"
hmac = "0.12"
ironrdp-core = { version = "0.3.0", path = "../ironrdp-core" }
ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public
ironrdp-egfx = { version = "0.4.0", path = "../ironrdp-egfx" }
ironrdp-graphics = { version = "0.10.0", path = "../ironrdp-graphics" }
ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public
ironrdp-egfx = { version = "0.5.0", path = "../ironrdp-egfx" }
ironrdp-graphics = { version = "0.10.1", path = "../ironrdp-graphics" }
ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu" } # public
ironrdp-session = { version = "0.12.0", path = "../ironrdp-session" }
ironrdp-session = { version = "0.13.0", path = "../ironrdp-session" }
ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" }
pcap-parser = "0.17"
png = "0.18"
Expand Down
171 changes: 171 additions & 0 deletions crates/ironrdp-cfg/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,177 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cfg-v0.2.0...ironrdp-cfg-v0.3.0)] - 2026-10-23

### <!-- 1 -->Features

- Redirect USB devices over URBDRC with nusb and RemoteFX USB backends ([#2181](https://github.com/Devolutions/IronRDP/issues/2181)) ([35247071a3](https://github.com/Devolutions/IronRDP/commit/35247071a32a718094fc4df1de60e220e3730f4b))

Adds host-device backends for the MS-RDPEUSB (URBDRC) client and wires
them into `ironrdp-client` and 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 in
`ironrdp-rdpeusb` found by redirecting a device to Windows Server 2025.

## Device coverage

- **Windows, with RemoteFX USB redirection allowed**: any device,
including HID, mass storage, smart card readers and cameras, through
`TsUsbFlt` and `TsUsbGD`, the drivers mstsc uses. Isochronous transfers
and the cancellation of a single request are supported. The
`fUsbRedirectionEnableMode` policy ("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.
- **Windows, otherwise**, through [nusb](https://docs.rs/nusb): only
devices bound to WinUSB, or composite devices whose interfaces all use
WinUSB. Firmware that requests WinUSB works out of the box; other
devices need Zadig or similar.
- **Linux**, through nusb: broad. Kernel drivers are detached while the
device is redirected and reattached afterwards; the user needs udev
access to `/dev/bus/usb`.
- **macOS**, through nusb: only devices not claimed by a kernel or
system extension.

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

- **New crate `ironrdp-rdpeusb-native`**, publishable like
`ironrdp-rdpeusb`. It implements `DeviceManagerBackend` and
`UrbdrcDeviceBackend`. Both backends share device selection,
announcement and unplug handling through a small `RedirectableDevice`
trait.
- **nusb backend** (all platforms):
- lists and filters devices, opens them and claims every interface
before announcing them to the server;
- translates control, bulk and interrupt URBs, plus feature, status,
descriptor, vendor/class and Microsoft OS descriptor requests;
- handles cancel, abort pipe and clear stall, configuration and
interface selection, port IOCTLs and device text;
- follows plug and unplug during the session, including devices
unplugged before their channel opens;
- rejects isochronous transfers packet by packet;
- bounds the data each device holds in flight (64 MiB) and each IN
transfer (16 MiB).

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_URB`s are re-encoded and submitted as overlapped I/O on a
completion port; the driver writes each `TS_URB_RESULT` over its
request.
- IO controls are forwarded, and a single request can be cancelled.
- A device is given back to its own driver when its channel closes, when
the server retracts it, or when the connection ends.

The driver interface is undocumented and was reverse-engineered from the
Windows 11 binaries.
- **Device selection**: FreeRDP `/usb` rules (`id:VID:PID#...`,
`addr:BUS:ADDR#...`, `auto`, `filter:CLASS#...` / `filter:none`) and the
`.rdp` `usbdevicestoredirect` property (`*`, `USB\VID_xxxx&PID_yyyy...`,
device instance IDs, `-` exclusions). `auto` and `*` 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`**: new `usb` feature, included in `all`, that
registers the URBDRC listener.
- Backend events go through the unbounded ordered queue the clipboard
already uses (`RdpInputSender::send_urbdrc`, new `RdpInputEvent::Urbdrc`
variant). The bounded input queue drops events when full, and a lost
completion would leave the server driver waiting forever.
- Events are scoped to their connection and device channel, so a
reconnection that reuses channel IDs never receives stale ones.
- USB redirection is suppressed when the RD Gateway's device-redirection
policy disables Plug and Play or all devices, over both the HTTP and
RPCH transports.
- New `ConfigBuilder::with_usb_redirection`. `usbdevicestoredirect` is
mapped in `with_property_set` through a new `ironrdp-cfg` accessor.
- The backend crate is re-exported as `ironrdp_client::usb`, and
`ironrdp` gains a `client-usb` feature.
- **`ironrdp-rdpeusb`** (client side):
- `CANCEL_REQUEST` keeps the request pending until the backend completes
it, so the server-side IRP finishes; FreeRDP does the same.
- `DeviceInfo::isochronous`: `ADD_DEVICE` advertises a nonzero no-ack
isochronous jitter buffer only when the backend performs isochronous
transfers.
- The listener takes the device before allocating its interface ID, so a
refused channel doesn't use one up.
- `TsUrbInPacket` and `TsUrbOutPacket` gain `encode_ts_urb` and
`decode_ts_urb_result`, for backends that hand `TS_URB`s to the
operating system as they are.
- Interop fixes found against Windows Server 2025:
- `REGISTER_REQUEST_CALLBACK` accepted 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.
- Windows sends control requests without a data stage, including OUT
ones such as HID `SET_IDLE`, in `TRANSFER_IN_REQUEST` messages 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-initiated `DYNVC_CLOSE` now tolerates data
that races it, with bounded bookkeeping that keeps the close order and
lets a channel ID be reused. `close_channel_batch` returns the Soft-Sync
tunnel the channel used, so the close travels on it.
- **Viewer**: `--usb RULES`, repeatable, overrides the `.rdp` value.
`--usb list` prints the local devices with the values the rules match
against and, on Windows, each device's driver.
- **ActiveX**: `RedirectDevices` is left as `E_NOTIMPL`. It switches
Plug and Play device redirection (MS-RDPEPNP), not URBDRC.

## Testing

- `cargo xtask check fmt -v`, `cargo xtask check lints -v` and `cargo
xtask check locks -v` pass.
- Unit and integration tests pass:
- `ironrdp-rdpeusb-native` (57);
- `ironrdp-client` with all features (68);
- the rdpeusb/dvc suites in `ironrdp-testsuite-core` (144);
- the client config suite in `ironrdp-testsuite-extra` (70).

They cover URB translation, cancellation and resumed transfers,
select-configuration results, completion status and HRESULT, IOCTLs,
filter and `.rdp` parsing, the config and gateway policy mapping, the
RemoteFX USB request buffers and status mapping, and regression tests
for each interop fix.
- On Windows, `--usb list` enumerates 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:
- A YubiKey 5 was claimed, redirected, and enumerated on the server with
its composite device, OTP keyboard, FIDO and CCID interfaces all OK.
- A CTAPHID `INIT` sent to its FIDO interface in the session got its
nonce echoed back, which exercises interrupt OUT and IN transfers end to
end.
- The session stayed up for about an hour, and the YubiKey went back to
its own drivers when the session ended.
- Unplugging the YubiKey during the session closed its device channel;
plugging it back in claimed it again on a new channel, and the CTAPHID
ping passed on the new device.
- The server needed the RD Session Host role and `fDisablePNPRedir = 0`
to offer URBDRC; it isn't offered in remote administration mode.
- With the nusb backend, URBDRC negotiates; no WinUSB device was
available to redirect.
- **Not verified**:
- a WinUSB device redirected through the nusb backend;
- smart card access in the session: the reader enumerates, but the
server's smart card service wasn't reachable;
- OTP keystrokes reach the server's console rather than the session, as
with mstsc.

πŸ€– Generated with [Claude Code](https://claude.com/claude-code)

---------



## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-cfg-v0.1.0...ironrdp-cfg-v0.2.0)] - 2026-10-23

### <!-- 1 -->Features
Expand Down
2 changes: 1 addition & 1 deletion crates/ironrdp-cfg/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ironrdp-cfg"
version = "0.2.0"
version = "0.3.0"
readme = "README.md"
description = "IronRDP utilities for ironrdp-cfgstore"
edition.workspace = true
Expand Down
10 changes: 5 additions & 5 deletions crates/ironrdp-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,19 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public
ironrdp-svc = { path = "../ironrdp-svc", version = "0.9" } # public
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.9" } # public
ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public
ironrdp-session = { path = "../ironrdp-session", version = "0.12" } # public
ironrdp-session = { path = "../ironrdp-session", version = "0.13" } # public
ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public
ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9" }
ironrdp-echo = { path = "../ironrdp-echo", version = "0.5" }
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4" } # public
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.5" } # public
ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.2" } # public
ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1", optional = true }
ironrdp-rdpeudp-tokio = { path = "../ironrdp-rdpeudp-tokio", version = "0.1", optional = true }
ironrdp-tls = { path = "../ironrdp-tls", version = "0.3" } # public
ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.11", features = ["reqwest"] }
ironrdp-rdcleanpath = { path = "../ironrdp-rdcleanpath", version = "0.3" }
ironrdp-vmconnect = { path = "../ironrdp-vmconnect", version = "0.1", optional = true }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.3" }
ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } # public
ironrdp-rail = { path = "../ironrdp-rail", version = "0.2" }

Expand All @@ -110,15 +110,15 @@ ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8", optional = tru
ironrdp-rdpdr = { path = "../ironrdp-rdpdr", version = "0.8", optional = true } # public
ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10", optional = true }
ironrdp-rdpeai = { path = "../ironrdp-rdpeai", version = "0.1", optional = true }
ironrdp-rdpel = { path = "../ironrdp-rdpel", version = "0.0.1", optional = true }
ironrdp-rdpel = { path = "../ironrdp-rdpel", version = "0.0.2", optional = true }
ironrdp-rdpegt = { path = "../ironrdp-rdpegt", version = "0.0.0", optional = true }
ironrdp-rdpevor = { path = "../ironrdp-rdpevor", version = "0.0.0", optional = true }
ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = true }

# Optional backend crates (activated by features above)
ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.7", optional = true } # public
ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.8", optional = true }
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", optional = true }
ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.3", optional = true }
ironrdp-dvc-pipe-proxy = { path = "../ironrdp-dvc-pipe-proxy", version = "0.6", optional = true }
ironrdp-rdpeusb-native = { path = "../ironrdp-rdpeusb-native", version = "0.1", optional = true } # public

Expand Down
2 changes: 1 addition & 1 deletion crates/ironrdp-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ categories.workspace = true

[dependencies]
ironrdp-client = { path = "../ironrdp-client", version = "0.1", features = ["rustls", "dvc-pipe-proxy", "rdpdr", "smartcard", "vmconnect", "gateway", "clipboard", "video"] } # public
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" }
ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.3" }
ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8" }
ironrdp-cliprdr-format = { path = "../ironrdp-cliprdr-format", version = "0.3" }
ironrdp-input = { path = "../ironrdp-input", version = "0.8" }
Expand Down
Loading
Loading