From 9763f76b235b6ced5140ad80da1db169289de90e Mon Sep 17 00:00:00 2001 From: devolutionsbot <31221910+devolutionsbot@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:27:37 -0400 Subject: [PATCH] chore(release): prepare for publishing --- Cargo.lock | 24 +-- crates/ironrdp-activex/Cargo.toml | 6 +- crates/ironrdp-agent/Cargo.toml | 4 +- crates/ironrdp-capture-replay/Cargo.toml | 8 +- crates/ironrdp-cfg/CHANGELOG.md | 171 ++++++++++++++++++++ crates/ironrdp-cfg/Cargo.toml | 2 +- crates/ironrdp-client/Cargo.toml | 10 +- crates/ironrdp-daemon/Cargo.toml | 2 +- crates/ironrdp-dvc-com-plugin/CHANGELOG.md | 22 +++ crates/ironrdp-dvc-com-plugin/Cargo.toml | 2 +- crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md | 22 +++ crates/ironrdp-dvc-pipe-proxy/Cargo.toml | 2 +- crates/ironrdp-dvc/CHANGELOG.md | 171 ++++++++++++++++++++ crates/ironrdp-dvc/Cargo.toml | 2 +- crates/ironrdp-egfx-native/Cargo.toml | 2 +- crates/ironrdp-egfx/CHANGELOG.md | 27 ++++ crates/ironrdp-egfx/Cargo.toml | 2 +- crates/ironrdp-graphics/CHANGELOG.md | 12 ++ crates/ironrdp-graphics/Cargo.toml | 2 +- crates/ironrdp-mstsgu/CHANGELOG.md | 22 +++ crates/ironrdp-mstsgu/Cargo.toml | 2 +- crates/ironrdp-rdpecam/Cargo.toml | 2 +- crates/ironrdp-rdpegt/Cargo.toml | 2 +- crates/ironrdp-rdpel/CHANGELOG.md | 4 + crates/ironrdp-rdpel/Cargo.toml | 4 +- crates/ironrdp-rdpeusb-native/CHANGELOG.md | 178 +++++++++++++++++++++ crates/ironrdp-rdpevor/Cargo.toml | 4 +- crates/ironrdp-server/Cargo.toml | 2 +- crates/ironrdp-session/CHANGELOG.md | 27 ++++ crates/ironrdp-session/Cargo.toml | 4 +- crates/ironrdp-str/CHANGELOG.md | 22 +++ crates/ironrdp-str/Cargo.toml | 2 +- crates/ironrdp-viewer/Cargo.toml | 2 +- crates/ironrdp-vmconnect/Cargo.toml | 2 +- crates/ironrdp/Cargo.toml | 4 +- fuzz/Cargo.lock | 10 +- 36 files changed, 732 insertions(+), 54 deletions(-) create mode 100644 crates/ironrdp-rdpeusb-native/CHANGELOG.md diff --git a/Cargo.lock b/Cargo.lock index 1d51a310a5..aade5af84a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2793,7 +2793,7 @@ dependencies = [ [[package]] name = "ironrdp-cfg" -version = "0.2.0" +version = "0.3.0" dependencies = [ "ironrdp-propertyset", ] @@ -2965,7 +2965,7 @@ dependencies = [ [[package]] name = "ironrdp-dvc" -version = "0.9.0" +version = "0.9.1" dependencies = [ "ironrdp-bulk", "ironrdp-core 0.3.0", @@ -2976,7 +2976,7 @@ dependencies = [ [[package]] name = "ironrdp-dvc-com-plugin" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ironrdp-core 0.3.0", "ironrdp-dvc", @@ -2989,7 +2989,7 @@ dependencies = [ [[package]] name = "ironrdp-dvc-pipe-proxy" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "ironrdp-core 0.3.0", @@ -3012,7 +3012,7 @@ dependencies = [ [[package]] name = "ironrdp-egfx" -version = "0.4.0" +version = "0.5.0" dependencies = [ "arbitrary", "bit_field", @@ -3078,7 +3078,7 @@ dependencies = [ [[package]] name = "ironrdp-graphics" -version = "0.10.0" +version = "0.10.1" dependencies = [ "bit_field", "bitflags 2.13.2", @@ -3106,7 +3106,7 @@ dependencies = [ [[package]] name = "ironrdp-mstsgu" -version = "0.0.2" +version = "0.0.3" dependencies = [ "base64 0.22.1", "bitflags 2.13.2", @@ -3267,7 +3267,7 @@ dependencies = [ [[package]] name = "ironrdp-rdpel" -version = "0.0.1" +version = "0.0.2" dependencies = [ "ironrdp-core 0.3.0", "ironrdp-dvc", @@ -3475,7 +3475,7 @@ dependencies = [ [[package]] name = "ironrdp-session" -version = "0.12.0" +version = "0.13.0" dependencies = [ "ironrdp-bulk", "ironrdp-core 0.3.0", @@ -3499,7 +3499,7 @@ version = "0.0.0" [[package]] name = "ironrdp-str" -version = "0.2.0" +version = "0.2.1" dependencies = [ "bytemuck", "ironrdp-core 0.3.0", @@ -6291,9 +6291,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "smithay-client-toolkit" diff --git a/crates/ironrdp-activex/Cargo.toml b/crates/ironrdp-activex/Cargo.toml index cc10ec63d3..251f64e46a 100644 --- a/crates/ironrdp-activex/Cargo.toml +++ b/crates/ironrdp-activex/Cargo.toml @@ -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" } @@ -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" diff --git a/crates/ironrdp-agent/Cargo.toml b/crates/ironrdp-agent/Cargo.toml index 9cb8c3b449..a9e3b2d719 100644 --- a/crates/ironrdp-agent/Cargo.toml +++ b/crates/ironrdp-agent/Cargo.toml @@ -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" } @@ -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" diff --git a/crates/ironrdp-capture-replay/Cargo.toml b/crates/ironrdp-capture-replay/Cargo.toml index fdf05cbd74..856b40dcea 100644 --- a/crates/ironrdp-capture-replay/Cargo.toml +++ b/crates/ironrdp-capture-replay/Cargo.toml @@ -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" diff --git a/crates/ironrdp-cfg/CHANGELOG.md b/crates/ironrdp-cfg/CHANGELOG.md index a6a4037b40..f94f6cdd18 100644 --- a/crates/ironrdp-cfg/CHANGELOG.md +++ b/crates/ironrdp-cfg/CHANGELOG.md @@ -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 + +### 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 ### Features diff --git a/crates/ironrdp-cfg/Cargo.toml b/crates/ironrdp-cfg/Cargo.toml index 14b6d52b3b..871143e6b1 100644 --- a/crates/ironrdp-cfg/Cargo.toml +++ b/crates/ironrdp-cfg/Cargo.toml @@ -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 diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index e524e1a5ff..05f0b2a1e8 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -89,11 +89,11 @@ 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 } @@ -101,7 +101,7 @@ 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" } @@ -110,7 +110,7 @@ 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 } @@ -118,7 +118,7 @@ ironrdp-rdpeusb = { path = "../ironrdp-rdpeusb", version = "0.1", optional = tru # 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 diff --git a/crates/ironrdp-daemon/Cargo.toml b/crates/ironrdp-daemon/Cargo.toml index 93d37c55ef..ebe7befca9 100644 --- a/crates/ironrdp-daemon/Cargo.toml +++ b/crates/ironrdp-daemon/Cargo.toml @@ -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" } diff --git a/crates/ironrdp-dvc-com-plugin/CHANGELOG.md b/crates/ironrdp-dvc-com-plugin/CHANGELOG.md index 1f8f4602e6..ef65c5095a 100644 --- a/crates/ironrdp-dvc-com-plugin/CHANGELOG.md +++ b/crates/ironrdp-dvc-com-plugin/CHANGELOG.md @@ -6,6 +6,28 @@ 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.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-com-plugin-v0.2.0...ironrdp-dvc-com-plugin-v0.2.1)] - 2026-10-23 + +### Build + +- Mark exposed dependencies as public ([#2213](https://github.com/Devolutions/IronRDP/issues/2213)) ([405e6576ba](https://github.com/Devolutions/IronRDP/commit/405e6576bac7c64b04efc8a0d00beeecdc648356)) + + These crates expose types or traits from these dependencies in their + public API, so a breaking release of the dependency requires a breaking + release of the crate: + + - `ironrdp-str`: `ironrdp-core`, whose `Encode` and `DecodeOwned` traits + the string types implement. + - `ironrdp-dvc-pipe-proxy`: `ironrdp-dvc`, through `DvcNamedPipeProxy`'s + `DvcProcessor` implementation. + - `ironrdp-dvc-com-plugin`: `ironrdp-dvc`, plus `ironrdp-pdu` and + `ironrdp-svc` through the `PduResult` return type and the `SvcMessage` + callback bound of `load_dvc_plugin`. + - `ironrdp-mstsgu`: `ironrdp-error`, whose `Error` the + `GwClient` methods return. + + + ## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-com-plugin-v0.1.3...ironrdp-dvc-com-plugin-v0.2.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-dvc-com-plugin/Cargo.toml b/crates/ironrdp-dvc-com-plugin/Cargo.toml index c41eb1feed..a53a42ffec 100644 --- a/crates/ironrdp-dvc-com-plugin/Cargo.toml +++ b/crates/ironrdp-dvc-com-plugin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc-com-plugin" -version = "0.2.0" +version = "0.2.1" readme = "README.md" description = "DVC COM client plugin loader for IronRDP (Windows)" edition.workspace = true diff --git a/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md index 6289691dfa..35d8d81c3d 100644 --- a/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md +++ b/crates/ironrdp-dvc-pipe-proxy/CHANGELOG.md @@ -6,6 +6,28 @@ 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.6.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.6.0...ironrdp-dvc-pipe-proxy-v0.6.1)] - 2026-10-23 + +### Build + +- Mark exposed dependencies as public ([#2213](https://github.com/Devolutions/IronRDP/issues/2213)) ([405e6576ba](https://github.com/Devolutions/IronRDP/commit/405e6576bac7c64b04efc8a0d00beeecdc648356)) + + These crates expose types or traits from these dependencies in their + public API, so a breaking release of the dependency requires a breaking + release of the crate: + + - `ironrdp-str`: `ironrdp-core`, whose `Encode` and `DecodeOwned` traits + the string types implement. + - `ironrdp-dvc-pipe-proxy`: `ironrdp-dvc`, through `DvcNamedPipeProxy`'s + `DvcProcessor` implementation. + - `ironrdp-dvc-com-plugin`: `ironrdp-dvc`, plus `ironrdp-pdu` and + `ironrdp-svc` through the `PduResult` return type and the `SvcMessage` + callback bound of `load_dvc_plugin`. + - `ironrdp-mstsgu`: `ironrdp-error`, whose `Error` the + `GwClient` methods return. + + + ## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-pipe-proxy-v0.5.0...ironrdp-dvc-pipe-proxy-v0.6.0)] - 2026-10-23 ### Bug Fixes diff --git a/crates/ironrdp-dvc-pipe-proxy/Cargo.toml b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml index b9232a7bed..7ffe62c517 100644 --- a/crates/ironrdp-dvc-pipe-proxy/Cargo.toml +++ b/crates/ironrdp-dvc-pipe-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc-pipe-proxy" -version = "0.6.0" +version = "0.6.1" readme = "README.md" description = "DVC named pipe proxy for IronRDP" edition.workspace = true diff --git a/crates/ironrdp-dvc/CHANGELOG.md b/crates/ironrdp-dvc/CHANGELOG.md index 04c9ce9f2a..a8789a9220 100644 --- a/crates/ironrdp-dvc/CHANGELOG.md +++ b/crates/ironrdp-dvc/CHANGELOG.md @@ -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.9.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.9.0...ironrdp-dvc-v0.9.1)] - 2026-10-23 + +### 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.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-dvc-v0.8.0...ironrdp-dvc-v0.9.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-dvc/Cargo.toml b/crates/ironrdp-dvc/Cargo.toml index 4063f1b1ae..b2a259831a 100644 --- a/crates/ironrdp-dvc/Cargo.toml +++ b/crates/ironrdp-dvc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-dvc" -version = "0.9.0" +version = "0.9.1" readme = "README.md" description = "DRDYNVC static channel implementation and traits to implement dynamic virtual channels" edition.workspace = true diff --git a/crates/ironrdp-egfx-native/Cargo.toml b/crates/ironrdp-egfx-native/Cargo.toml index 1ebc77d9ac..09eb953326 100644 --- a/crates/ironrdp-egfx-native/Cargo.toml +++ b/crates/ironrdp-egfx-native/Cargo.toml @@ -17,7 +17,7 @@ categories.workspace = true doctest = false [target.'cfg(windows)'.dependencies] -ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4" } # public +ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.5" } # public tracing = { version = "0.1", features = ["log"] } windows = { version = "0.62", features = [ "Win32_Foundation", diff --git a/crates/ironrdp-egfx/CHANGELOG.md b/crates/ironrdp-egfx/CHANGELOG.md index a401cd1165..b91e914cf4 100644 --- a/crates/ironrdp-egfx/CHANGELOG.md +++ b/crates/ironrdp-egfx/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file. 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.5.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-egfx-v0.4.0...ironrdp-egfx-v0.5.0)] - 2026-10-23 + +### Features + +- [**breaking**] Composite surfaces mapped to RemoteApp windows ([#2197](https://github.com/Devolutions/IronRDP/issues/2197)) ([6e54d5e9ec](https://github.com/Devolutions/IronRDP/commit/6e54d5e9ec58c69debaff96fa49bed4343348d3e)) + + With the graphics pipeline in a RemoteApp session, Windows servers + present each application window from its own surface through + MapSurfaceToWindow or MapSurfaceToScaledWindow (MS-RDPEGFX 2.2.2.20 and + 2.2.2.23). The client only forwarded these PDUs to handler callbacks, so + nothing those surfaces drew reached the windows. + + The compositor now records window mappings and, at each EndFrame, + materializes dirty regions of window-mapped surfaces into window + updates: the RAIL window ID, the presented size and the changed region + in window coordinates, scaled when the server asks for a different + target size. GraphicsPipelineClient::drain_window_updates exposes them, + the session emits ActiveStageOutput::WindowSurfaceUpdates and the client + forwards RdpOutputEvent::WindowSurfaceUpdates. A mapping the client + cannot present, such as a mapped area larger than its surface, is + ignored and the surface keeps its previous mapping; FreeRDP also refuses + oversized mappings. + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + + ## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-egfx-v0.3.0...ironrdp-egfx-v0.4.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-egfx/Cargo.toml b/crates/ironrdp-egfx/Cargo.toml index 43132f9a4b..9274d6213b 100644 --- a/crates/ironrdp-egfx/Cargo.toml +++ b/crates/ironrdp-egfx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-egfx" -version = "0.4.0" +version = "0.5.0" readme = "README.md" description = "Graphics pipeline dynamic channel extension implementation" edition.workspace = true diff --git a/crates/ironrdp-graphics/CHANGELOG.md b/crates/ironrdp-graphics/CHANGELOG.md index a3e5fe0ecb..c2ddf92390 100644 --- a/crates/ironrdp-graphics/CHANGELOG.md +++ b/crates/ironrdp-graphics/CHANGELOG.md @@ -6,6 +6,18 @@ 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.10.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.10.0...ironrdp-graphics-v0.10.1)] - 2026-10-23 + +### Bug Fixes + +- Reject RLGR3 values larger than their remainder ([#2132](https://github.com/Devolutions/IronRDP/issues/2132)) ([e31f2b89ad](https://github.com/Devolutions/IronRDP/commit/e31f2b89adb6bd08c3cc240f9e357601323e1976)) + + RLGR3's `code_remainder - val1` (`rlgr.rs`) overflowed when the stream + set `val1` larger: a panic in debug builds, a garbage value in release. + It's now an `InvalidIntegralConversion` error. FreeRDP rejects it too. + + + ## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-graphics-v0.9.0...ironrdp-graphics-v0.10.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-graphics/Cargo.toml b/crates/ironrdp-graphics/Cargo.toml index a65b2ec3a4..6108b97963 100644 --- a/crates/ironrdp-graphics/Cargo.toml +++ b/crates/ironrdp-graphics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-graphics" -version = "0.10.0" +version = "0.10.1" readme = "README.md" description = "RDP image processing primitives" edition.workspace = true diff --git a/crates/ironrdp-mstsgu/CHANGELOG.md b/crates/ironrdp-mstsgu/CHANGELOG.md index 8a36cd1918..723a0cfb5c 100644 --- a/crates/ironrdp-mstsgu/CHANGELOG.md +++ b/crates/ironrdp-mstsgu/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [[0.0.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-mstsgu-v0.0.2...ironrdp-mstsgu-v0.0.3)] - 2026-10-23 + +### Build + +- Mark exposed dependencies as public ([#2213](https://github.com/Devolutions/IronRDP/issues/2213)) ([405e6576ba](https://github.com/Devolutions/IronRDP/commit/405e6576bac7c64b04efc8a0d00beeecdc648356)) + + These crates expose types or traits from these dependencies in their + public API, so a breaking release of the dependency requires a breaking + release of the crate: + + - `ironrdp-str`: `ironrdp-core`, whose `Encode` and `DecodeOwned` traits + the string types implement. + - `ironrdp-dvc-pipe-proxy`: `ironrdp-dvc`, through `DvcNamedPipeProxy`'s + `DvcProcessor` implementation. + - `ironrdp-dvc-com-plugin`: `ironrdp-dvc`, plus `ironrdp-pdu` and + `ironrdp-svc` through the `PduResult` return type and the `SvcMessage` + callback bound of `load_dvc_plugin`. + - `ironrdp-mstsgu`: `ironrdp-error`, whose `Error` the + `GwClient` methods return. + + + ## [[0.0.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-mstsgu-v0.0.1...ironrdp-mstsgu-v0.0.2)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-mstsgu/Cargo.toml b/crates/ironrdp-mstsgu/Cargo.toml index b29aaf1a4f..47ab61ace1 100644 --- a/crates/ironrdp-mstsgu/Cargo.toml +++ b/crates/ironrdp-mstsgu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-mstsgu" -version = "0.0.2" +version = "0.0.3" readme = "README.md" description = "Terminal Services Gateway Server Protocol" edition.workspace = true diff --git a/crates/ironrdp-rdpecam/Cargo.toml b/crates/ironrdp-rdpecam/Cargo.toml index 42176e3ae3..22cf79ee2f 100644 --- a/crates/ironrdp-rdpecam/Cargo.toml +++ b/crates/ironrdp-rdpecam/Cargo.toml @@ -19,7 +19,7 @@ test = false [dependencies] ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } # public spin = { version = "0.9", default-features = false, features = ["spin_mutex"] } diff --git a/crates/ironrdp-rdpegt/Cargo.toml b/crates/ironrdp-rdpegt/Cargo.toml index 5ccc6f1bc5..c863842281 100644 --- a/crates/ironrdp-rdpegt/Cargo.toml +++ b/crates/ironrdp-rdpegt/Cargo.toml @@ -19,7 +19,7 @@ test = false [dependencies] ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-rdpel/CHANGELOG.md b/crates/ironrdp-rdpel/CHANGELOG.md index dc77579485..1716faf222 100644 --- a/crates/ironrdp-rdpel/CHANGELOG.md +++ b/crates/ironrdp-rdpel/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [[0.0.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpel-v0.0.1...ironrdp-rdpel-v0.0.2)] - 2026-10-23 + + + ## [[0.0.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-rdpel-v0.0.0...ironrdp-rdpel-v0.0.1)] - 2026-10-23 ### Build diff --git a/crates/ironrdp-rdpel/Cargo.toml b/crates/ironrdp-rdpel/Cargo.toml index 32ddf051ce..bdc34269a7 100644 --- a/crates/ironrdp-rdpel/Cargo.toml +++ b/crates/ironrdp-rdpel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-rdpel" -version = "0.0.1" +version = "0.0.2" readme = "README.md" description = "Location dynamic channel client as described in MS-RDPEL" edition.workspace = true @@ -18,7 +18,7 @@ test = false [dependencies] ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } # public tracing = { version = "0.1", features = ["log"] } diff --git a/crates/ironrdp-rdpeusb-native/CHANGELOG.md b/crates/ironrdp-rdpeusb-native/CHANGELOG.md new file mode 100644 index 0000000000..fd0ab43bc4 --- /dev/null +++ b/crates/ironrdp-rdpeusb-native/CHANGELOG.md @@ -0,0 +1,178 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +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.1.0](https://github.com/Devolutions/IronRDP/releases/tag/ironrdp-rdpeusb-native-v0.1.0)] - 2026-10-23 + +### 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) + + --------- + + diff --git a/crates/ironrdp-rdpevor/Cargo.toml b/crates/ironrdp-rdpevor/Cargo.toml index f824532a30..af198b7dc7 100644 --- a/crates/ironrdp-rdpevor/Cargo.toml +++ b/crates/ironrdp-rdpevor/Cargo.toml @@ -19,8 +19,8 @@ test = false [dependencies] ironrdp-core = { version = "0.3.0", path = "../ironrdp-core", features = ["alloc"] } # public -ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public -ironrdp-egfx = { version = "0.4", path = "../ironrdp-egfx" } # public +ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public +ironrdp-egfx = { version = "0.5", path = "../ironrdp-egfx" } # public ironrdp-pdu = { version = "0.10.0", path = "../ironrdp-pdu", features = ["alloc"] } # public ironrdp-rdpegt = { path = "../ironrdp-rdpegt" } # public ironrdp-svc = { version = "0.9.0", path = "../ironrdp-svc" } # public diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index 5aac835848..30b84cab1f 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -43,7 +43,7 @@ ironrdp-async = { path = "../ironrdp-async", version = "0.11" } ironrdp-ainput = { path = "../ironrdp-ainput", version = "0.9" } ironrdp-connector = { path = "../ironrdp-connector", version = "0.11" } # public ironrdp-core = { path = "../ironrdp-core", version = "0.3" } -ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.4", optional = true } +ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.5", optional = true } ironrdp-error = { path = "../ironrdp-error", version = "0.2", features = ["std"] } # public ironrdp-nscodec = { path = "../ironrdp-nscodec", version = "0.3", optional = true, features = ["encoder"] } ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public diff --git a/crates/ironrdp-session/CHANGELOG.md b/crates/ironrdp-session/CHANGELOG.md index 42c7f2d83f..e42ce373c8 100644 --- a/crates/ironrdp-session/CHANGELOG.md +++ b/crates/ironrdp-session/CHANGELOG.md @@ -6,6 +6,33 @@ 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.13.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.12.0...ironrdp-session-v0.13.0)] - 2026-10-23 + +### Features + +- [**breaking**] Composite surfaces mapped to RemoteApp windows ([#2197](https://github.com/Devolutions/IronRDP/issues/2197)) ([6e54d5e9ec](https://github.com/Devolutions/IronRDP/commit/6e54d5e9ec58c69debaff96fa49bed4343348d3e)) + + With the graphics pipeline in a RemoteApp session, Windows servers + present each application window from its own surface through + MapSurfaceToWindow or MapSurfaceToScaledWindow (MS-RDPEGFX 2.2.2.20 and + 2.2.2.23). The client only forwarded these PDUs to handler callbacks, so + nothing those surfaces drew reached the windows. + + The compositor now records window mappings and, at each EndFrame, + materializes dirty regions of window-mapped surfaces into window + updates: the RAIL window ID, the presented size and the changed region + in window coordinates, scaled when the server asks for a different + target size. GraphicsPipelineClient::drain_window_updates exposes them, + the session emits ActiveStageOutput::WindowSurfaceUpdates and the client + forwards RdpOutputEvent::WindowSurfaceUpdates. A mapping the client + cannot present, such as a mapped area larger than its surface, is + ignored and the surface keeps its previous mapping; FreeRDP also refuses + oversized mappings. + + 🤖 Generated with [Claude Code](https://claude.com/claude-code) + + + ## [[0.12.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.11.0...ironrdp-session-v0.12.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml index 041a09c1fc..bda4526bf5 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-session" -version = "0.12.0" +version = "0.13.0" readme = "README.md" description = "State machines to drive an RDP session" edition.workspace = true @@ -31,7 +31,7 @@ ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10" } # public ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", features = ["std"] } # public 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-rdpei = { path = "../ironrdp-rdpei", version = "0.2" } # public tracing = { version = "0.1", features = ["log"] } qoicoubeh = { version = "0.5", optional = true } diff --git a/crates/ironrdp-str/CHANGELOG.md b/crates/ironrdp-str/CHANGELOG.md index dd2705ca75..1c9a9f016b 100644 --- a/crates/ironrdp-str/CHANGELOG.md +++ b/crates/ironrdp-str/CHANGELOG.md @@ -6,6 +6,28 @@ 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.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-str-v0.2.0...ironrdp-str-v0.2.1)] - 2026-10-23 + +### Build + +- Mark exposed dependencies as public ([#2213](https://github.com/Devolutions/IronRDP/issues/2213)) ([405e6576ba](https://github.com/Devolutions/IronRDP/commit/405e6576bac7c64b04efc8a0d00beeecdc648356)) + + These crates expose types or traits from these dependencies in their + public API, so a breaking release of the dependency requires a breaking + release of the crate: + + - `ironrdp-str`: `ironrdp-core`, whose `Encode` and `DecodeOwned` traits + the string types implement. + - `ironrdp-dvc-pipe-proxy`: `ironrdp-dvc`, through `DvcNamedPipeProxy`'s + `DvcProcessor` implementation. + - `ironrdp-dvc-com-plugin`: `ironrdp-dvc`, plus `ironrdp-pdu` and + `ironrdp-svc` through the `PduResult` return type and the `SvcMessage` + callback bound of `load_dvc_plugin`. + - `ironrdp-mstsgu`: `ironrdp-error`, whose `Error` the + `GwClient` methods return. + + + ## [[0.2.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-str-v0.1.1...ironrdp-str-v0.2.0)] - 2026-10-23 ### Features diff --git a/crates/ironrdp-str/Cargo.toml b/crates/ironrdp-str/Cargo.toml index 2d871fd6b0..822be91a0f 100644 --- a/crates/ironrdp-str/Cargo.toml +++ b/crates/ironrdp-str/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ironrdp-str" -version = "0.2.0" +version = "0.2.1" description = "Typed wire-aware string primitives for RDP protocol fields" edition.workspace = true rust-version = "1.94" diff --git a/crates/ironrdp-viewer/Cargo.toml b/crates/ironrdp-viewer/Cargo.toml index 3f28e594cc..893e4407ce 100644 --- a/crates/ironrdp-viewer/Cargo.toml +++ b/crates/ironrdp-viewer/Cargo.toml @@ -33,7 +33,7 @@ openh264 = ["ironrdp/client-openh264"] ironrdp-daemon = { path = "../ironrdp-daemon", version = "0.1" } # public ironrdp-rpc = { path = "../ironrdp-rpc", version = "0.1" } ironrdp = { path = "../ironrdp", version = "0.17", features = ["connector", "cliprdr", "input", "pdu", "rdpei", "client", "client-all", "client-vmconnect"] } # public -ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.2" } +ironrdp-cfg = { path = "../ironrdp-cfg", version = "0.3" } ironrdp-propertyset = { path = "../ironrdp-propertyset", version = "0.1" } ironrdp-rdpfile = { path = "../ironrdp-rdpfile", version = "0.1" } ironrdp-rail = { path = "../ironrdp-rail", version = "0.2" } diff --git a/crates/ironrdp-vmconnect/Cargo.toml b/crates/ironrdp-vmconnect/Cargo.toml index c20f3ebfcb..ed11a257a6 100644 --- a/crates/ironrdp-vmconnect/Cargo.toml +++ b/crates/ironrdp-vmconnect/Cargo.toml @@ -29,7 +29,7 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10" } # public tracing = { version = "0.1", features = ["log"] } [target.'cfg(windows)'.dependencies] -ironrdp-dvc = { version = "0.9.0", path = "../ironrdp-dvc" } # public +ironrdp-dvc = { version = "0.9.1", path = "../ironrdp-dvc" } # public sha2 = "0.10" windows = { version = "0.62", features = [ "Win32_Foundation", diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index 6cab00b005..95cbe7239d 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -68,7 +68,7 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.10", optional = true } # p ironrdp-cliprdr = { path = "../ironrdp-cliprdr", version = "0.8", optional = true } # public ironrdp-connector = { path = "../ironrdp-connector", version = "0.11", optional = true } # public ironrdp-acceptor = { path = "../ironrdp-acceptor", version = "0.11", optional = true } # public -ironrdp-session = { path = "../ironrdp-session", version = "0.12", optional = true } # public +ironrdp-session = { path = "../ironrdp-session", version = "0.13", optional = true } # public ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.10", optional = true } # public ironrdp-input = { path = "../ironrdp-input", version = "0.8", optional = true } # public ironrdp-server = { path = "../ironrdp-server", version = "0.13", optional = true, features = ["helper"] } # public @@ -79,7 +79,7 @@ ironrdp-rdpsnd = { path = "../ironrdp-rdpsnd", version = "0.10", optional = true ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.9", optional = true } # public ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.2", optional = true } # public ironrdp-echo = { path = "../ironrdp-echo", version = "0.5", optional = true } # public -ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.2", optional = true } # public +ironrdp-mstsgu = { path = "../ironrdp-mstsgu", version = "0.0.3", optional = true } # public ironrdp-client = { path = "../ironrdp-client", version = "0.1", optional = true } # public ironrdp-vmconnect = { path = "../ironrdp-vmconnect", version = "0.1", optional = true } # public diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index e7e63b42ba..5139c8a471 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "ironrdp-dvc" -version = "0.9.0" +version = "0.9.1" dependencies = [ "ironrdp-bulk", "ironrdp-core", @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "ironrdp-egfx" -version = "0.4.0" +version = "0.5.0" dependencies = [ "arbitrary", "bit_field", @@ -473,7 +473,7 @@ dependencies = [ [[package]] name = "ironrdp-graphics" -version = "0.10.0" +version = "0.10.1" dependencies = [ "bit_field", "bitflags", @@ -525,7 +525,7 @@ dependencies = [ [[package]] name = "ironrdp-rdpel" -version = "0.0.1" +version = "0.0.2" dependencies = [ "ironrdp-core", "ironrdp-dvc", @@ -582,7 +582,7 @@ dependencies = [ [[package]] name = "ironrdp-str" -version = "0.2.0" +version = "0.2.1" dependencies = [ "bytemuck", "ironrdp-core",