feat(rdpeusb): implement urbdrc client#1365
Conversation
6bfdada to
9b3f945
Compare
97479e8 to
acacd98
Compare
|
Marc-Andre Lureau (@elmarco) Benoît Cortier (@CBenoit) [EDIT]: |
c25c053 to
b478576
Compare
There was a problem hiding this comment.
⚠️ Not ready to approve
The client state machine currently diverges from the issue’s required startup sequencing and adds substantial new protocol logic without the acceptance-criteria unit tests.
Pull request overview
Adds an ironrdp-rdpeusb client-side implementation of the URBDRC dynamic virtual channel, including a control-channel processor, per-device channel processor, and backend-neutral device metadata conversion used to construct RDPEUSB PDUs.
Changes:
- Introduces
UrbdrcListener,UrbdrcControlClient, andUrbdrcDeviceClientwith a backend trait surface and pending-IO completion helpers. - Adds backend-neutral
DeviceInfo→ADD_DEVICEconversion (PnP-style IDs, container IDs, capabilities) following observed FreeRDP behavior. - Integrates RDPEUSB PDUs with the DVC layer by implementing
ironrdp_dvc::DvcEncodeand adding anironrdp-dvcdependency.
File summaries
| File | Description |
|---|---|
| crates/ironrdp-rdpeusb/src/pdu/usb_dev/mod.rs | Adds DvcEncode impls and a helper to extract transfer request IDs. |
| crates/ironrdp-rdpeusb/src/pdu/sink.rs | Marks sink PDUs as DVC-encodable (DvcEncode). |
| crates/ironrdp-rdpeusb/src/pdu/notify.rs | Marks CHANNEL_CREATED as DVC-encodable (DvcEncode). |
| crates/ironrdp-rdpeusb/src/pdu/iface_manipulation.rs | Marks interface-manipulation PDUs as DVC-encodable (DvcEncode). |
| crates/ironrdp-rdpeusb/src/pdu/header.rs | Exposes InterfaceId::from_raw within the crate for client allocation. |
| crates/ironrdp-rdpeusb/src/pdu/completion/mod.rs | Marks completion PDUs as DVC-encodable (DvcEncode). |
| crates/ironrdp-rdpeusb/src/pdu/caps.rs | Marks capability PDUs as DVC-encodable (DvcEncode). |
| crates/ironrdp-rdpeusb/src/lib.rs | Adds channel name, exports client module, and introduces InvalidDeviceInterfaceId error type. |
| crates/ironrdp-rdpeusb/src/client/mod.rs | Implements the URBDRC listener + control/device client processors and backend traits. |
| crates/ironrdp-rdpeusb/src/client/device.rs | Implements backend-neutral USB device facts and ADD_DEVICE PDU construction. |
| crates/ironrdp-rdpeusb/Cargo.toml | Adds public dependency on ironrdp-dvc. |
| Cargo.lock | Records the new ironrdp-dvc dependency edge. |
Copilot's findings
- Files reviewed: 11/12 changed files
- Comments generated: 8
Note
Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const fn alloc(&mut self) -> InterfaceId { | ||
| self.id += 1; | ||
| if self.id > 0x3F_FF_FF_FF { | ||
| panic!("USB device amount overflow") | ||
| } | ||
| InterfaceId::from_raw(self.id) | ||
| } | ||
| } | ||
|
|
||
| impl DvcChannelListener for UrbdrcListener { | ||
| fn channel_name(&self) -> &str { | ||
| CHANNEL_NAME | ||
| } | ||
|
|
||
| fn create(&mut self, channel_id: u32) -> Option<Box<dyn DvcProcessor>> { | ||
| if let Some(callback) = self.on_capability_exchanged.take() { | ||
| self.device_man.control_channel_assigned(channel_id); | ||
| Some(Box::new(UrbdrcControlClient::new(callback))) | ||
| } else { | ||
| #[expect(clippy::as_conversions)] | ||
| self.device_man.take_device_for_channel(channel_id).map(|backend| { | ||
| Box::new(UrbdrcDeviceClient::new(self.iface_man.alloc(), backend).expect("invalid interface id")) | ||
| as Box<dyn DvcProcessor> | ||
| }) |
There was a problem hiding this comment.
uchouT (@uchouT) this is intentionally post-poned? You could turn alloc() -> Option and remove all the expect/panic.
8303938 to
06f93df
Compare
Signed-off-by: uchouT <i@uchout.moe>
06f93df to
6c2eaaf
Compare
|
Updated |
`ADD_DEVICE` message is constructed following FreeRDP's pattern. `UrbdrcDeviceClient` maintains a pending IO request map. When the packet is handled asynchronously, the information for the completion message construction (such as output buffer size) will be stored in this map, and `UrbdrcDeviceClient` provides public APIs for completion message construction, which will consume the map items internally. Signed-off-by: uchouT <i@uchout.moe>
Signed-off-by: uchouT <i@uchout.moe> Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
6c2eaaf to
e2cc7f9
Compare
Add test for new device sequence and channel setup sequence. Use hardcoded message id when constructing `ADD_VIRTUAL_CHANNEL`. Signed-off-by: uchouT <i@uchout.moe>
| // Follow FreeRDP use device id as message id | ||
| Ok(Box::new(AddVirtualChannel { | ||
| msg_id: ADD_VIRTUAL_CHANNEL_MSG_ID, | ||
| })) |
| #[derive(Debug, Clone)] | ||
| pub struct DeviceText { | ||
| pub hresult: u32, | ||
| pub description: String, | ||
| } |
| // Ref: [New Device Sequence][1.3.1.2] | ||
| // [1.3.1.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/7e3da218-9cdc-4ebd-bb76-e70202c7f264 | ||
| #[test] | ||
| fn new_device_sequence() { | ||
| let udev_iface = InterfaceId::try_from(4).expect("valid device interface id"); | ||
| let backend = Box::new(TestDeviceBackend::new(simple_device_info())); | ||
| let mut client = UrbdrcDeviceClient::new(udev_iface, backend).expect("device client should be created"); | ||
|
|
||
| assert!(!client.ready_for_io()); | ||
|
|
||
| let resp = client | ||
| .process( | ||
| 99, | ||
| &encode_pdu(&UrbdrcServerDevicePdu::ChanCreated(ChannelCreated { | ||
| msg_id: 21, | ||
| direction: Direction::ToClient, | ||
| })), | ||
| ) | ||
| .expect("channel-created notification should succeed"); | ||
| assert_eq!(resp.len(), 1); | ||
| let UrbdrcClientDevicePdu::ChanCreated(response) = decode_device_msg(&resp[0]) else { | ||
| panic!("expected channel-created response"); | ||
| }; | ||
| assert_eq!(response.msg_id, 21); | ||
| assert_eq!(response.direction, Direction::ToServer); | ||
| assert!(!client.ready_for_io()); | ||
|
|
||
| let resp = client | ||
| .process( | ||
| 99, | ||
| &encode_pdu(&UrbdrcServerDevicePdu::IfaceRelease(InterfaceRelease { | ||
| iface_id: proxy_iface_id(InterfaceId::NOTIFY_CLIENT), | ||
| msg_id: 22, | ||
| })), | ||
| ) | ||
| .expect("notification release should succeed"); | ||
| assert_eq!(resp.len(), 1); | ||
| assert!(client.ready_for_io()); | ||
|
|
||
| let UrbdrcClientDevicePdu::AddDev(add_device) = decode_device_msg(&resp[0]) else { | ||
| panic!("expected add device"); | ||
| }; | ||
| assert_eq!(add_device.usb_device, udev_iface); | ||
| } |
|
I added some tests covering new device add sequence and channel setup sequence, and I plan to add io sequence integration test when server state machine is settled. |
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Hi! Great work!
There is an acceptance criteria from issue #1137 that is not upheld:
Protocol-level unit tests with mock backend
It would be nice to have a "protocol-level unit tests with mock backend" covering the control channel setup sequence, the device channel setup sequence, and IO dispatch/completion round-trips. That being said, I’m happy if this is submitted as a follow up 🙂
Overall, I think it’s a very solid PR, and I’m good with the idea of merging now for velocity, and you could send follow up PRs for anything remaining. How do you want to proceed?
| use crate::{CHANNEL_NAME, InvalidDeviceInterfaceId}; | ||
|
|
||
| pub mod device; | ||
| pub use device::*; |
There was a problem hiding this comment.
style: We tend to prefer explicit re-exports for clarity: pub use device::{DeviceInfo, UsbConnectionSpeed, ...}
| if no_ack { | ||
| self.backend | ||
| .transfer_out(channel_id, request_id.into(), transfer_out_pdu)?; | ||
| Ok(Vec::new()) | ||
| } else { |
There was a problem hiding this comment.
The UrbdrcDeviceBackend::transfer_out contract documents None = pending and Some = immediate completion. In the no-ack path, Some(response) is silently swallowed. Either the contract should explicitly say "returning Some is meaningless in no-ack context" or the backend shouldn't be expected to produce a response at all (different method signature for no-ack vs. ack).
| Ok(Vec::new()) | ||
| } | ||
| } | ||
| IoCtl(io_ctl_pdu) => { |
There was a problem hiding this comment.
thought: The IoCtl and InternalIoCtl arms are nearly identical. A shared helper taking a Pending variant + msg_id + max_output_buf_size could eliminate the repetition cleanly. As you prefer.
Oops, I just saw this after submitting my review I was drafting over several days. Sounds good to me. Thank you! |
|
Benoît Cortier (@CBenoit) Thanks for the review! I've addressed the two inconsistent problems. And all other issues you pointed out make sense to me, but I'd like to address them in the follow-up PR since I've modified them locally when implementing server state machine. |
| const fn alloc(&mut self) -> InterfaceId { | ||
| self.id += 1; | ||
| if self.id > 0x3F_FF_FF_FF { | ||
| panic!("USB device amount overflow") | ||
| } | ||
| InterfaceId::from_raw(self.id) | ||
| } | ||
| } | ||
|
|
||
| impl DvcChannelListener for UrbdrcListener { | ||
| fn channel_name(&self) -> &str { | ||
| CHANNEL_NAME | ||
| } | ||
|
|
||
| fn create(&mut self, channel_id: u32) -> Option<Box<dyn DvcProcessor>> { | ||
| if let Some(callback) = self.on_capability_exchanged.take() { | ||
| self.device_man.control_channel_assigned(channel_id); | ||
| Some(Box::new(UrbdrcControlClient::new(callback))) | ||
| } else { | ||
| #[expect(clippy::as_conversions)] | ||
| self.device_man.take_device_for_channel(channel_id).map(|backend| { | ||
| Box::new(UrbdrcDeviceClient::new(self.iface_man.alloc(), backend).expect("invalid interface id")) | ||
| as Box<dyn DvcProcessor> | ||
| }) |
There was a problem hiding this comment.
uchouT (@uchouT) this is intentionally post-poned? You could turn alloc() -> Option and remove all the expect/panic.
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you Marc-Andre Lureau (@elmarco) uchouT (@uchouT)
I think we got to a pretty decent state, and the PR is big enough, let’s merge and address any remaining concern in follow up PRs 🙂
66c9b5d
into
Devolutions:master
Resolves #1137
The device dvc processor manages a
pending_iomap internally and offers API for upper layer to construct completionDvcMessage.