diff --git a/crates/ironrdp-pdu/src/rdp/message_channel.rs b/crates/ironrdp-pdu/src/rdp/message_channel.rs new file mode 100644 index 0000000000..77b6a33220 --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/message_channel.rs @@ -0,0 +1,65 @@ +//! PDUs a client sends on the MCS message channel (MS-RDPBCGR 2.2.1.3.7). +//! +//! Two PDUs travel from client to server on this channel, both framed by a +//! Basic Security Header rather than a Share Control header: the Auto-Detect +//! Response (MS-RDPBCGR 2.2.14.4) and the Initiate Multitransport Response +//! (MS-RDPBCGR 2.2.15.2). The header's flags say which one follows +//! (`SEC_AUTODETECT_RSP` or `SEC_TRANSPORT_RSP`), so decoding dispatches on +//! them and reports a malformed PDU against the type its header names. + +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_size}; + +use crate::rdp::autodetect::AutoDetectRspPdu; +use crate::rdp::headers::BasicSecurityHeaderFlags; +use crate::rdp::multitransport::MultitransportResponsePdu; + +/// A PDU received from the client on the MCS message channel. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ClientMessageChannelPdu { + /// An Auto-Detect Response (`SEC_AUTODETECT_RSP`). + AutoDetectResponse(AutoDetectRspPdu), + /// An Initiate Multitransport Response (`SEC_TRANSPORT_RSP`). + MultitransportResponse(MultitransportResponsePdu), +} + +impl ClientMessageChannelPdu { + const NAME: &'static str = "ClientMessageChannelPdu"; + + /// Dispatch only needs the leading `flags` field of the Basic Security + /// Header; the chosen PDU's own decode validates the rest. + const FLAGS_SIZE: usize = 2 /* flags */; +} + +impl Encode for ClientMessageChannelPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + Self::AutoDetectResponse(pdu) => pdu.encode(dst), + Self::MultitransportResponse(pdu) => pdu.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + match self { + Self::AutoDetectResponse(pdu) => pdu.size(), + Self::MultitransportResponse(pdu) => pdu.size(), + } + } +} + +impl<'de> Decode<'de> for ClientMessageChannelPdu { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + ensure_size!(in: src, size: Self::FLAGS_SIZE); + + let flags = BasicSecurityHeaderFlags::from_bits_truncate(src.peek_u16()); + + if flags.contains(BasicSecurityHeaderFlags::TRANSPORT_RSP) { + MultitransportResponsePdu::decode(src).map(Self::MultitransportResponse) + } else { + AutoDetectRspPdu::decode(src).map(Self::AutoDetectResponse) + } + } +} diff --git a/crates/ironrdp-pdu/src/rdp/mod.rs b/crates/ironrdp-pdu/src/rdp/mod.rs index 3aa22d2397..d522ccb52d 100644 --- a/crates/ironrdp-pdu/src/rdp/mod.rs +++ b/crates/ironrdp-pdu/src/rdp/mod.rs @@ -11,6 +11,7 @@ pub mod client_info; pub mod finalization_messages; pub mod headers; pub mod heartbeat; +pub mod message_channel; pub mod multitransport; pub mod refresh_rectangle; pub mod server_error_info; diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index f33f99bae3..50c247e474 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -3847,11 +3847,8 @@ impl RdpServer { } fn handle_message_channel_data(&mut self, data: SendDataRequest<'_>) { - // The MCS message channel currently carries only the auto-detect - // response. It is framed by a Basic Security Header (SEC_AUTODETECT_RSP), - // not a Share Control header. - match decode::(data.user_data.as_ref()) { - Ok(pdu) => { + match decode::(data.user_data.as_ref()) { + Ok(rdp::message_channel::ClientMessageChannelPdu::AutoDetectResponse(pdu)) => { if let Some(ref mut ad) = self.autodetect { match ad.handle_response(&pdu.response, monotonic_now_ms()) { AutoDetectOutcome::Rtt(rtt_ms) => { @@ -3894,6 +3891,17 @@ impl RdpServer { } } } + Ok(rdp::message_channel::ClientMessageChannelPdu::MultitransportResponse(pdu)) => { + // A failure code is not a decode error: the client is correctly reporting + // that the sideband UDP attempt failed, and the session continues on the + // main transport either way. + debug!( + request_id = pdu.request_id, + success = pdu.is_success(), + hr_response = format!("{:#x}", pdu.hr_response), + "Received Initiate Multitransport Response" + ); + } Err(error) => { warn!(error = format!("{error:#}"), "Unhandled MCS message channel PDU"); } diff --git a/crates/ironrdp-testsuite-core/tests/pdu/message_channel.rs b/crates/ironrdp-testsuite-core/tests/pdu/message_channel.rs new file mode 100644 index 0000000000..b17bef686d --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/pdu/message_channel.rs @@ -0,0 +1,72 @@ +//! Decoding of client PDUs on the MCS message channel ([MS-RDPBCGR] 2.2.1.3.7). + +use ironrdp_core::{decode, encode_vec}; +use ironrdp_pdu::rdp::autodetect::{AutoDetectResponse, AutoDetectRspPdu}; +use ironrdp_pdu::rdp::message_channel::ClientMessageChannelPdu; +use ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu; + +#[test] +fn autodetect_response_is_recognized() { + let pdu = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number: 7 }); + let bytes = encode_vec(&pdu).unwrap(); + + let decoded = decode::(&bytes).unwrap(); + + assert_eq!(decoded, ClientMessageChannelPdu::AutoDetectResponse(pdu)); +} + +/// A client that offered UDP multitransport but could not establish it answers +/// on the message channel with an Initiate Multitransport Response (MS-RDPBCGR +/// 2.2.15.2), not an auto-detect response. +#[test] +fn multitransport_response_is_recognized() { + let pdu = MultitransportResponsePdu::abort(42); + let bytes = encode_vec(&pdu).unwrap(); + + let decoded = decode::(&bytes).unwrap(); + + assert_eq!(decoded, ClientMessageChannelPdu::MultitransportResponse(pdu)); +} + +#[test] +fn both_variants_round_trip() { + for pdu in [ + ClientMessageChannelPdu::AutoDetectResponse(AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { + sequence_number: 3, + })), + ClientMessageChannelPdu::MultitransportResponse(MultitransportResponsePdu::abort(9)), + ] { + let bytes = encode_vec(&pdu).unwrap(); + assert_eq!(decode::(&bytes).unwrap(), pdu); + } +} + +/// Dispatch follows the security header: a truncated PDU whose header says +/// SEC_TRANSPORT_RSP is reported as a malformed multitransport response, not +/// as a failed auto-detect decode. +#[test] +fn truncated_multitransport_response_is_reported_as_one() { + let bytes = encode_vec(&MultitransportResponsePdu::abort(42)).unwrap(); + + let error = decode::(&bytes[..8]).unwrap_err(); + + let message = format!("{error:#}"); + assert!(message.contains("MultitransportResponsePdu"), "{message}"); +} + +/// A header without SEC_TRANSPORT_RSP goes to the auto-detect decoder, which +/// reports the flag it expected. +#[test] +fn unrecognized_payload_is_reported_by_the_autodetect_decoder() { + let bytes = [0x00u8; 12]; + + let error = decode::(&bytes).unwrap_err(); + + let message = format!("{error:#}"); + assert!(message.contains("SEC_AUTODETECT_RSP"), "{message}"); +} + +#[test] +fn input_shorter_than_the_flags_field_is_rejected() { + assert!(decode::(&[0x04]).is_err()); +} diff --git a/crates/ironrdp-testsuite-core/tests/pdu/mod.rs b/crates/ironrdp-testsuite-core/tests/pdu/mod.rs index f81a586718..a55c1c047b 100644 --- a/crates/ironrdp-testsuite-core/tests/pdu/mod.rs +++ b/crates/ironrdp-testsuite-core/tests/pdu/mod.rs @@ -4,6 +4,7 @@ mod gcc; mod gfx; mod input; mod mcs; +mod message_channel; #[expect( clippy::needless_raw_strings, reason = "the lint is disable to not interfere with expect! macro"