From 05ccafa66609448e5123874a9dcdbfce3cbb6d55 Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Sun, 13 Sep 2026 09:46:21 -0500 Subject: [PATCH 1/3] fix(server): recognize the Initiate Multitransport Response on the message channel handle_message_channel_data unconditionally decoded every PDU on the MCS message channel as an AutoDetectRspPdu, per a comment claiming the channel "currently carries only the auto-detect response". That's no longer accurate: once a server sends an Initiate Multitransport Request, the client answers on this same channel with an Initiate Multitransport Response (MS-RDPBCGR 2.2.15.2) if it could not establish the sideband transport. MultitransportResponsePdu already has full Encode/Decode support in ironrdp-pdu, but had zero consumers anywhere in ironrdp-server, so every such response failed to decode (its securityHeader carries SEC_TRANSPORT_RSP, not SEC_AUTODETECT_RSP) and was dropped as an "Unhandled MCS message channel PDU" warning instead of being recognized as the ordinary protocol traffic it is. Extracted the dispatch into decode_message_channel_pdu, tried against both PDU types (auto-detect first, since it is by far the more common one) instead of just one, and given handle_message_channel_data a proper Multitransport arm that logs success/failure at debug level instead of warning on legitimate input. --- crates/ironrdp-server/src/server.rs | 101 ++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index f33f99bae3..f6f504d070 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -13,7 +13,7 @@ use ironrdp_acceptor::{Acceptor, AcceptorResult, BeginResult, DesktopSize}; use ironrdp_async::Framed; use ironrdp_cliprdr::CliprdrServer; use ironrdp_cliprdr::backend::ClipboardMessage; -use ironrdp_core::{decode, encode_vec, impl_as_any}; +use ironrdp_core::{DecodeResult, decode, encode_vec, impl_as_any}; use ironrdp_displaycontrol::pdu::DisplayControlMonitorLayout; use ironrdp_displaycontrol::server::{DisplayControlHandler, DisplayControlServer}; use ironrdp_dvc as dvc; @@ -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_message_channel_pdu(data.user_data.as_ref()) { + Ok(MessageChannelPdu::AutoDetect(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,20 @@ impl RdpServer { } } } + Ok(MessageChannelPdu::Multitransport(pdu)) => { + if pdu.is_success() { + debug!(request_id = pdu.request_id, "Multitransport connection established"); + } else { + // Not a decode error: the client tried the sideband UDP transport and is + // correctly reporting that it could not establish it. The session + // continues on the main transport regardless. + debug!( + request_id = pdu.request_id, + hr_response = format!("{:#x}", pdu.hr_response), + "Multitransport connection failed, continuing on the main transport" + ); + } + } Err(error) => { warn!(error = format!("{error:#}"), "Unhandled MCS message channel PDU"); } @@ -4053,6 +4064,37 @@ impl RdpServer { } } +/// A successfully-recognized MCS message channel PDU. +#[derive(Debug)] +enum MessageChannelPdu { + AutoDetect(rdp::autodetect::AutoDetectRspPdu), + Multitransport(rdp::multitransport::MultitransportResponsePdu), +} + +/// Decode a PDU received on the MCS message channel. +/// +/// The message channel carries the auto-detect response and the Initiate +/// Multitransport Response ([MS-RDPBCGR] 2.2.15.2), both framed by a Basic +/// Security Header whose flags (SEC_AUTODETECT_RSP vs. SEC_TRANSPORT_RSP) +/// distinguish which one follows; neither is framed by a Share Control +/// header. A client only ever sends the latter after the server has sent an +/// Initiate Multitransport Request, so trying the far more common +/// auto-detect response first and falling back to the multitransport +/// response keeps the common case a single decode. On a double failure, the +/// auto-detect error is returned: this channel overwhelmingly carries +/// auto-detect responses, so that error is the more useful one to surface. +/// +/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/ +fn decode_message_channel_pdu(user_data: &[u8]) -> DecodeResult { + match decode::(user_data) { + Ok(pdu) => Ok(MessageChannelPdu::AutoDetect(pdu)), + Err(autodetect_error) => match decode::(user_data) { + Ok(pdu) => Ok(MessageChannelPdu::Multitransport(pdu)), + Err(_) => Err(autodetect_error), + }, + } +} + /// Encode a server-initiated Auto-Detect Request PDU for the MCS message channel. /// /// The request is framed by a Basic Security Header (SEC_AUTODETECT_REQ) per @@ -4870,6 +4912,53 @@ mod tests { use super::*; + #[test] + fn decode_message_channel_pdu_recognizes_autodetect_response() { + let pdu = rdp::autodetect::AutoDetectRspPdu::new(rdp::autodetect::AutoDetectResponse::RttResponse { + sequence_number: 7, + }); + let bytes = encode_vec(&pdu).unwrap(); + + match decode_message_channel_pdu(&bytes).unwrap() { + MessageChannelPdu::AutoDetect(decoded) => assert_eq!(decoded.response.sequence_number(), 7), + MessageChannelPdu::Multitransport(_) => panic!("decoded as the wrong PDU type"), + } + } + + /// Regression test: a real Windows client (mstsc) offering UDP + /// multitransport but failing to establish it answers on this same + /// channel with an Initiate Multitransport Response, not another + /// auto-detect response. Before this fix, `handle_message_channel_data` + /// only ever tried to decode `AutoDetectRspPdu` here, so this PDU failed + /// to decode (`securityHeader`'s flags carry `SEC_TRANSPORT_RSP`, not + /// `SEC_AUTODETECT_RSP`) and was dropped as an "Unhandled MCS message + /// channel PDU" warning instead of being recognized. + #[test] + fn decode_message_channel_pdu_recognizes_multitransport_response() { + let pdu = rdp::multitransport::MultitransportResponsePdu::abort(42); + let bytes = encode_vec(&pdu).unwrap(); + + match decode_message_channel_pdu(&bytes).unwrap() { + MessageChannelPdu::Multitransport(decoded) => { + assert_eq!(decoded.request_id, 42); + assert!(!decoded.is_success()); + } + MessageChannelPdu::AutoDetect(_) => panic!("decoded as the wrong PDU type"), + } + } + + #[test] + fn decode_message_channel_pdu_reports_the_autodetect_error_on_double_failure() { + let garbage = [0xffu8; 12]; + + let error = decode_message_channel_pdu(&garbage).unwrap_err(); + + // Both decode attempts fail on this input; the auto-detect error is + // the one that should surface (see `decode_message_channel_pdu`'s + // doc comment for why). + assert!(format!("{error:#}").contains("AutoDetectResponse")); + } + /// A channel backend that owns a resource, released on drop the way /// `RdpsndServer` stops its handler. #[derive(Debug)] From 55a3e4b66d54d6d728360f2383cb043c93c874e2 Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Tue, 22 Sep 2026 19:42:19 -0500 Subject: [PATCH 2/3] review: dispatch message-channel PDUs on the security header in ironrdp-pdu Moves the MCS message-channel demux into ironrdp-pdu as ClientMessageChannelPdu, which dispatches on the Basic Security Header flags so a malformed Initiate Multitransport Response is reported as one rather than as a failed auto-detect decode. Its tests live in ironrdp-testsuite-core; the ones previously added inline in ironrdp-server never ran there. The server logs the response once, in the acceptor's wording. --- crates/ironrdp-pdu/src/rdp/message_channel.rs | 66 +++++++++++ crates/ironrdp-pdu/src/rdp/mod.rs | 1 + crates/ironrdp-server/src/server.rs | 110 +++--------------- .../tests/pdu/message_channel.rs | 72 ++++++++++++ .../ironrdp-testsuite-core/tests/pdu/mod.rs | 1 + 5 files changed, 156 insertions(+), 94 deletions(-) create mode 100644 crates/ironrdp-pdu/src/rdp/message_channel.rs create mode 100644 crates/ironrdp-testsuite-core/tests/pdu/message_channel.rs 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..3810ee8adf --- /dev/null +++ b/crates/ironrdp-pdu/src/rdp/message_channel.rs @@ -0,0 +1,66 @@ +//! 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)] +#[non_exhaustive] +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 f6f504d070..e72c7a352d 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -13,7 +13,7 @@ use ironrdp_acceptor::{Acceptor, AcceptorResult, BeginResult, DesktopSize}; use ironrdp_async::Framed; use ironrdp_cliprdr::CliprdrServer; use ironrdp_cliprdr::backend::ClipboardMessage; -use ironrdp_core::{DecodeResult, decode, encode_vec, impl_as_any}; +use ironrdp_core::{decode, encode_vec, impl_as_any}; use ironrdp_displaycontrol::pdu::DisplayControlMonitorLayout; use ironrdp_displaycontrol::server::{DisplayControlHandler, DisplayControlServer}; use ironrdp_dvc as dvc; @@ -3847,8 +3847,8 @@ impl RdpServer { } fn handle_message_channel_data(&mut self, data: SendDataRequest<'_>) { - match decode_message_channel_pdu(data.user_data.as_ref()) { - Ok(MessageChannelPdu::AutoDetect(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) => { @@ -3891,19 +3891,19 @@ impl RdpServer { } } } - Ok(MessageChannelPdu::Multitransport(pdu)) => { - if pdu.is_success() { - debug!(request_id = pdu.request_id, "Multitransport connection established"); - } else { - // Not a decode error: the client tried the sideband UDP transport and is - // correctly reporting that it could not establish it. The session - // continues on the main transport regardless. - debug!( - request_id = pdu.request_id, - hr_response = format!("{:#x}", pdu.hr_response), - "Multitransport connection failed, continuing on the main transport" - ); - } + 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" + ); + } + Ok(pdu) => { + warn!(?pdu, "Unhandled MCS message channel PDU"); } Err(error) => { warn!(error = format!("{error:#}"), "Unhandled MCS message channel PDU"); @@ -4064,37 +4064,6 @@ impl RdpServer { } } -/// A successfully-recognized MCS message channel PDU. -#[derive(Debug)] -enum MessageChannelPdu { - AutoDetect(rdp::autodetect::AutoDetectRspPdu), - Multitransport(rdp::multitransport::MultitransportResponsePdu), -} - -/// Decode a PDU received on the MCS message channel. -/// -/// The message channel carries the auto-detect response and the Initiate -/// Multitransport Response ([MS-RDPBCGR] 2.2.15.2), both framed by a Basic -/// Security Header whose flags (SEC_AUTODETECT_RSP vs. SEC_TRANSPORT_RSP) -/// distinguish which one follows; neither is framed by a Share Control -/// header. A client only ever sends the latter after the server has sent an -/// Initiate Multitransport Request, so trying the far more common -/// auto-detect response first and falling back to the multitransport -/// response keeps the common case a single decode. On a double failure, the -/// auto-detect error is returned: this channel overwhelmingly carries -/// auto-detect responses, so that error is the more useful one to surface. -/// -/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/ -fn decode_message_channel_pdu(user_data: &[u8]) -> DecodeResult { - match decode::(user_data) { - Ok(pdu) => Ok(MessageChannelPdu::AutoDetect(pdu)), - Err(autodetect_error) => match decode::(user_data) { - Ok(pdu) => Ok(MessageChannelPdu::Multitransport(pdu)), - Err(_) => Err(autodetect_error), - }, - } -} - /// Encode a server-initiated Auto-Detect Request PDU for the MCS message channel. /// /// The request is framed by a Basic Security Header (SEC_AUTODETECT_REQ) per @@ -4912,53 +4881,6 @@ mod tests { use super::*; - #[test] - fn decode_message_channel_pdu_recognizes_autodetect_response() { - let pdu = rdp::autodetect::AutoDetectRspPdu::new(rdp::autodetect::AutoDetectResponse::RttResponse { - sequence_number: 7, - }); - let bytes = encode_vec(&pdu).unwrap(); - - match decode_message_channel_pdu(&bytes).unwrap() { - MessageChannelPdu::AutoDetect(decoded) => assert_eq!(decoded.response.sequence_number(), 7), - MessageChannelPdu::Multitransport(_) => panic!("decoded as the wrong PDU type"), - } - } - - /// Regression test: a real Windows client (mstsc) offering UDP - /// multitransport but failing to establish it answers on this same - /// channel with an Initiate Multitransport Response, not another - /// auto-detect response. Before this fix, `handle_message_channel_data` - /// only ever tried to decode `AutoDetectRspPdu` here, so this PDU failed - /// to decode (`securityHeader`'s flags carry `SEC_TRANSPORT_RSP`, not - /// `SEC_AUTODETECT_RSP`) and was dropped as an "Unhandled MCS message - /// channel PDU" warning instead of being recognized. - #[test] - fn decode_message_channel_pdu_recognizes_multitransport_response() { - let pdu = rdp::multitransport::MultitransportResponsePdu::abort(42); - let bytes = encode_vec(&pdu).unwrap(); - - match decode_message_channel_pdu(&bytes).unwrap() { - MessageChannelPdu::Multitransport(decoded) => { - assert_eq!(decoded.request_id, 42); - assert!(!decoded.is_success()); - } - MessageChannelPdu::AutoDetect(_) => panic!("decoded as the wrong PDU type"), - } - } - - #[test] - fn decode_message_channel_pdu_reports_the_autodetect_error_on_double_failure() { - let garbage = [0xffu8; 12]; - - let error = decode_message_channel_pdu(&garbage).unwrap_err(); - - // Both decode attempts fail on this input; the auto-detect error is - // the one that should surface (see `decode_message_channel_pdu`'s - // doc comment for why). - assert!(format!("{error:#}").contains("AutoDetectResponse")); - } - /// A channel backend that owns a resource, released on drop the way /// `RdpsndServer` stops its handler. #[derive(Debug)] 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" From 813bead34c799ebf47f429c1d748667ae8df3474 Mon Sep 17 00:00:00 2001 From: Greg Lamberson Date: Wed, 23 Sep 2026 14:31:18 -0500 Subject: [PATCH 3/3] review: make ClientMessageChannelPdu exhaustive MS-RDPBCGR fixes the client-to-server message channel to these two PDUs, and ironrdp-pdu's dispatch enums are exhaustive, so the attribute only forced an unreachable catch-all arm in the server. Both removed. --- crates/ironrdp-pdu/src/rdp/message_channel.rs | 1 - crates/ironrdp-server/src/server.rs | 3 --- 2 files changed, 4 deletions(-) diff --git a/crates/ironrdp-pdu/src/rdp/message_channel.rs b/crates/ironrdp-pdu/src/rdp/message_channel.rs index 3810ee8adf..77b6a33220 100644 --- a/crates/ironrdp-pdu/src/rdp/message_channel.rs +++ b/crates/ironrdp-pdu/src/rdp/message_channel.rs @@ -15,7 +15,6 @@ use crate::rdp::multitransport::MultitransportResponsePdu; /// A PDU received from the client on the MCS message channel. #[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] pub enum ClientMessageChannelPdu { /// An Auto-Detect Response (`SEC_AUTODETECT_RSP`). AutoDetectResponse(AutoDetectRspPdu), diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index e72c7a352d..50c247e474 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -3902,9 +3902,6 @@ impl RdpServer { "Received Initiate Multitransport Response" ); } - Ok(pdu) => { - warn!(?pdu, "Unhandled MCS message channel PDU"); - } Err(error) => { warn!(error = format!("{error:#}"), "Unhandled MCS message channel PDU"); }