Skip to content

Commit bd23906

Browse files
committed
fix(pdu)!: send NetworkAutoDetect over the MCS message channel
NetworkAutoDetect was modeled as a slow-path Share Data PDU (ShareDataPduType::AutoDetect, a fabricated 0x3B) carried on the I/O channel. Per [MS-RDPBCGR] 2.2.14.3 / 2.2.14.4 the auto-detect PDUs are framed by a Basic Security Header (SEC_AUTODETECT_REQ / SEC_AUTODETECT_RSP) and ride the MCS message channel, so neither mstsc nor xfreerdp answered the requests. ironrdp-pdu: add AutoDetectReqPdu / AutoDetectRspPdu, which wrap the request and response data with the security header (mirroring the multitransport PDUs). Remove the ShareDataPdu::AutoDetectReq / AutoDetectRsp variants and the ShareDataPduType::AutoDetect discriminant, which did not correspond to any real PDUTYPE2. ironrdp-server: send auto-detect requests and receive responses on the message channel surfaced by the acceptor, with the new framing. ironrdp-connector and ironrdp-session: the client now requests the message channel (Client Message Channel Data), advertises SUPPORT_NET_CHAR_AUTODETECT, joins the channel, and answers RTT requests on it. The connector handles connect-time auto-detection (previously a no-op) and the session handles continuous auto-detection. Resolves the client side of the message-channel TODO (#140). Depends on the acceptor message-channel negotiation in the parent PR.
1 parent a81d671 commit bd23906

7 files changed

Lines changed: 418 additions & 145 deletions

File tree

crates/ironrdp-connector/src/connection.rs

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::{
2121
pub struct ConnectionResult {
2222
pub io_channel_id: u16,
2323
pub user_channel_id: u16,
24+
/// MCS channel ID of the message channel, when one was negotiated.
25+
pub message_channel_id: Option<u16>,
2426
pub share_id: u32,
2527
pub static_channels: StaticChannelSet,
2628
pub desktop_size: DesktopSize,
@@ -126,6 +128,8 @@ pub struct ClientConnector {
126128
/// The client address to be used in the Client Info PDU.
127129
pub client_addr: SocketAddr,
128130
pub static_channels: StaticChannelSet,
131+
/// MCS message channel ID assigned by the server, once negotiated.
132+
pub message_channel_id: Option<u16>,
129133
}
130134

131135
impl ClientConnector {
@@ -135,6 +139,7 @@ impl ClientConnector {
135139
state: ClientConnectorState::ConnectionInitiationSendRequest,
136140
client_addr,
137141
static_channels: StaticChannelSet::new(),
142+
message_channel_id: None,
138143
}
139144
}
140145

@@ -212,7 +217,15 @@ impl Sequence for ClientConnector {
212217
ClientConnectorState::BasicSettingsExchangeWaitResponse { .. } => Some(&ironrdp_pdu::X224_HINT),
213218
ClientConnectorState::ChannelConnection { channel_connection, .. } => channel_connection.next_pdu_hint(),
214219
ClientConnectorState::SecureSettingsExchange { .. } => None,
215-
ClientConnectorState::ConnectTimeAutoDetection { .. } => None,
220+
ClientConnectorState::ConnectTimeAutoDetection { .. } => {
221+
// Wait for input only when a message channel was negotiated, so
222+
// we can receive connect-time auto-detect requests there.
223+
if self.message_channel_id.is_some() {
224+
Some(&ironrdp_pdu::X224_HINT)
225+
} else {
226+
None
227+
}
228+
}
216229
ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(),
217230
ClientConnectorState::MultitransportBootstrapping { .. } => None,
218231
ClientConnectorState::CapabilitiesExchange {
@@ -382,9 +395,10 @@ impl Sequence for ClientConnector {
382395
return Err(general_err!("can't satisfy server security settings"));
383396
}
384397

385-
if server_gcc_blocks.message_channel.is_some() {
386-
warn!("Unexpected ServerMessageChannelData GCC block (not supported)");
387-
}
398+
self.message_channel_id = server_gcc_blocks
399+
.message_channel
400+
.as_ref()
401+
.map(|data| data.mcs_message_channel_id);
388402

389403
if server_gcc_blocks.multi_transport_channel.is_some() {
390404
warn!("Unexpected MultiTransportChannelData GCC block (not supported)");
@@ -418,7 +432,9 @@ impl Sequence for ClientConnector {
418432
channel_connection: if skip_channel_join {
419433
ChannelConnectionSequence::skip_channel_join()
420434
} else {
421-
ChannelConnectionSequence::new(io_channel_id, static_channel_ids)
435+
let mut join_channel_ids = static_channel_ids;
436+
join_channel_ids.extend(self.message_channel_id);
437+
ChannelConnectionSequence::new(io_channel_id, join_channel_ids)
422438
},
423439
},
424440
)
@@ -485,12 +501,37 @@ impl Sequence for ClientConnector {
485501
ClientConnectorState::ConnectTimeAutoDetection {
486502
io_channel_id,
487503
user_channel_id,
488-
} => (
489-
Written::Nothing,
490-
ClientConnectorState::LicensingExchange {
491-
io_channel_id,
492-
user_channel_id,
493-
license_exchange: LicenseExchangeSequence::new(
504+
} => {
505+
// The server may run Optional Connect-Time Auto-Detection on the
506+
// message channel before licensing ([MS-RDPBCGR] 1.3.8). When a
507+
// message channel was negotiated we wait for a PDU here: an
508+
// auto-detect request is answered (and we stay to receive more),
509+
// while any other PDU (the first licensing PDU on the I/O channel)
510+
// ends the phase and is forwarded to the licensing sequence.
511+
// Without a message channel nothing is read and we go straight to
512+
// licensing, as before.
513+
let autodetect = self.message_channel_id.and_then(|message_channel_id| {
514+
let mcs = decode::<X224<mcs::McsMessage<'_>>>(input).ok()?;
515+
match mcs.0 {
516+
mcs::McsMessage::SendDataIndication(data) if data.channel_id == message_channel_id => {
517+
decode::<rdp::autodetect::AutoDetectReqPdu>(&data.user_data).ok()
518+
}
519+
_ => None,
520+
}
521+
});
522+
523+
if let (Some(message_channel_id), Some(pdu)) = (self.message_channel_id, autodetect) {
524+
let written =
525+
respond_to_connect_time_autodetect(pdu.request, message_channel_id, user_channel_id, output)?;
526+
(
527+
written,
528+
ClientConnectorState::ConnectTimeAutoDetection {
529+
io_channel_id,
530+
user_channel_id,
531+
},
532+
)
533+
} else {
534+
let mut license_exchange = LicenseExchangeSequence::new(
494535
io_channel_id,
495536
self.config.credentials.username().unwrap_or("").to_owned(),
496537
self.config.domain.clone(),
@@ -499,9 +540,39 @@ impl Sequence for ClientConnector {
499540
.license_cache
500541
.clone()
501542
.unwrap_or_else(|| Arc::new(NoopLicenseCache)),
502-
),
503-
},
504-
),
543+
);
544+
// If a PDU was read (message channel present) it is the first
545+
// licensing PDU; process it here and advance the same way the
546+
// LicensingExchange state would, so it is not stepped twice.
547+
// Otherwise nothing was read and the licensing sequence runs
548+
// from its first step as before.
549+
if self.message_channel_id.is_some() {
550+
let written = license_exchange.step(input, output)?;
551+
let next_state = if license_exchange.state.is_terminal() {
552+
ClientConnectorState::MultitransportBootstrapping {
553+
io_channel_id,
554+
user_channel_id,
555+
}
556+
} else {
557+
ClientConnectorState::LicensingExchange {
558+
io_channel_id,
559+
user_channel_id,
560+
license_exchange,
561+
}
562+
};
563+
(written, next_state)
564+
} else {
565+
(
566+
Written::Nothing,
567+
ClientConnectorState::LicensingExchange {
568+
io_channel_id,
569+
user_channel_id,
570+
license_exchange,
571+
},
572+
)
573+
}
574+
}
575+
}
505576

506577
//== Licensing ==//
507578
// Server is sending information regarding licensing.
@@ -585,6 +656,7 @@ impl Sequence for ClientConnector {
585656
result: ConnectionResult {
586657
io_channel_id,
587658
user_channel_id,
659+
message_channel_id: self.message_channel_id,
588660
share_id,
589661
static_channels: mem::take(&mut self.static_channels),
590662
desktop_size,
@@ -631,6 +703,27 @@ pub fn encode_send_data_request<T: Encode>(
631703
Ok(written)
632704
}
633705

706+
fn respond_to_connect_time_autodetect(
707+
request: rdp::autodetect::AutoDetectRequest,
708+
message_channel_id: u16,
709+
user_channel_id: u16,
710+
output: &mut WriteBuf,
711+
) -> ConnectorResult<Written> {
712+
use ironrdp_pdu::rdp::autodetect::{AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
713+
714+
match request {
715+
AutoDetectRequest::RttRequest { sequence_number, .. } => {
716+
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
717+
let written = encode_send_data_request(user_channel_id, message_channel_id, &response, output)?;
718+
Written::from_size(written)
719+
}
720+
// The Network Characteristics Result is informational, and bandwidth
721+
// measurement is driven implicitly by the connect-time payload exchange.
722+
// Neither requires an immediate reply.
723+
_ => Ok(Written::Nothing),
724+
}
725+
}
726+
634727
#[expect(single_use_lifetimes)] // anonymous lifetimes in `impl Trait` are unstable
635728
fn create_gcc_blocks<'a>(
636729
config: &Config,
@@ -695,6 +788,7 @@ fn create_gcc_blocks<'a>(
695788
let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE
696789
| ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU
697790
| ClientEarlyCapabilityFlags::STRONG_ASYMMETRIC_KEYS
791+
| ClientEarlyCapabilityFlags::SUPPORT_NET_CHAR_AUTODETECT
698792
| ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN;
699793

700794
// TODO(#136): support for ClientEarlyCapabilityFlags::SUPPORT_STATUS_INFO_PDU
@@ -735,8 +829,10 @@ fn create_gcc_blocks<'a>(
735829
// TODO(#139): support for Some(ClientClusterData { flags: RedirectionFlags::REDIRECTION_SUPPORTED, redirection_version: RedirectionVersion::V4, redirected_session_id: 0, }),
736830
cluster: None,
737831
monitor: None,
738-
// TODO(#140): support for Client Message Channel Data (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f50e791c-de03-4b25-b17e-e914c9020bc3)
739-
message_channel: None,
832+
// Request the MCS message channel, which carries network auto-detect
833+
// ([MS-RDPBCGR] 2.2.14) and the multitransport / heartbeat PDUs. The
834+
// server assigns its ID in Server Message Channel Data.
835+
message_channel: Some(gcc::ClientMessageChannelData),
740836
multi_transport_channel: config
741837
.multitransport_flags
742838
.map(|flags| gcc::MultiTransportChannelData { flags }),

crates/ironrdp-pdu/src/rdp/autodetect.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use ironrdp_core::{
1515
Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_size, invalid_field_err,
1616
};
1717

18+
use crate::rdp::headers::{BasicSecurityHeader, BasicSecurityHeaderFlags};
19+
1820
// ============================================================================
1921
// Constants
2022
// ============================================================================
@@ -676,10 +678,170 @@ impl<'de> Decode<'de> for AutoDetectResponse {
676678
}
677679
}
678680

681+
// ============================================================================
682+
// MCS message channel framing
683+
// ============================================================================
684+
//
685+
// Auto-detect is not a Share Data PDU. Per [MS-RDPBCGR] 2.2.14.3 / 2.2.14.4 it
686+
// rides the MCS message channel framed by a Basic Security Header whose
687+
// SEC_AUTODETECT_REQ / SEC_AUTODETECT_RSP flag identifies it, the same dispatch
688+
// mechanism used by multitransport (see `rdp::multitransport`).
689+
690+
/// Server Auto-Detect Request PDU ([MS-RDPBCGR] 2.2.14.3).
691+
///
692+
/// Wraps an [`AutoDetectRequest`] with the `SEC_AUTODETECT_REQ` security header.
693+
#[derive(Debug, Clone, PartialEq, Eq)]
694+
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
695+
pub struct AutoDetectReqPdu {
696+
pub security_header: BasicSecurityHeader,
697+
pub request: AutoDetectRequest,
698+
}
699+
700+
impl AutoDetectReqPdu {
701+
const NAME: &'static str = "AutoDetectReqPdu";
702+
703+
/// Wrap a request with the `SEC_AUTODETECT_REQ` security header.
704+
pub fn new(request: AutoDetectRequest) -> Self {
705+
Self {
706+
security_header: BasicSecurityHeader {
707+
flags: BasicSecurityHeaderFlags::AUTODETECT_REQ,
708+
},
709+
request,
710+
}
711+
}
712+
}
713+
714+
impl Encode for AutoDetectReqPdu {
715+
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
716+
ensure_size!(in: dst, size: self.size());
717+
718+
self.security_header.encode(dst)?;
719+
self.request.encode(dst)?;
720+
721+
Ok(())
722+
}
723+
724+
fn name(&self) -> &'static str {
725+
Self::NAME
726+
}
727+
728+
fn size(&self) -> usize {
729+
BasicSecurityHeader::FIXED_PART_SIZE + self.request.size()
730+
}
731+
}
732+
733+
impl<'de> Decode<'de> for AutoDetectReqPdu {
734+
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
735+
let security_header = BasicSecurityHeader::decode(src)?;
736+
737+
if !security_header.flags.contains(BasicSecurityHeaderFlags::AUTODETECT_REQ) {
738+
return Err(invalid_field_err!("securityHeader", "expected SEC_AUTODETECT_REQ flag"));
739+
}
740+
741+
let request = AutoDetectRequest::decode(src)?;
742+
743+
Ok(Self {
744+
security_header,
745+
request,
746+
})
747+
}
748+
}
749+
750+
/// Client Auto-Detect Response PDU ([MS-RDPBCGR] 2.2.14.4).
751+
///
752+
/// Wraps an [`AutoDetectResponse`] with the `SEC_AUTODETECT_RSP` security header.
753+
#[derive(Debug, Clone, PartialEq, Eq)]
754+
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
755+
pub struct AutoDetectRspPdu {
756+
pub security_header: BasicSecurityHeader,
757+
pub response: AutoDetectResponse,
758+
}
759+
760+
impl AutoDetectRspPdu {
761+
const NAME: &'static str = "AutoDetectRspPdu";
762+
763+
/// Wrap a response with the `SEC_AUTODETECT_RSP` security header.
764+
pub fn new(response: AutoDetectResponse) -> Self {
765+
Self {
766+
security_header: BasicSecurityHeader {
767+
flags: BasicSecurityHeaderFlags::AUTODETECT_RSP,
768+
},
769+
response,
770+
}
771+
}
772+
}
773+
774+
impl Encode for AutoDetectRspPdu {
775+
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
776+
ensure_size!(in: dst, size: self.size());
777+
778+
self.security_header.encode(dst)?;
779+
self.response.encode(dst)?;
780+
781+
Ok(())
782+
}
783+
784+
fn name(&self) -> &'static str {
785+
Self::NAME
786+
}
787+
788+
fn size(&self) -> usize {
789+
BasicSecurityHeader::FIXED_PART_SIZE + self.response.size()
790+
}
791+
}
792+
793+
impl<'de> Decode<'de> for AutoDetectRspPdu {
794+
fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
795+
let security_header = BasicSecurityHeader::decode(src)?;
796+
797+
if !security_header.flags.contains(BasicSecurityHeaderFlags::AUTODETECT_RSP) {
798+
return Err(invalid_field_err!("securityHeader", "expected SEC_AUTODETECT_RSP flag"));
799+
}
800+
801+
let response = AutoDetectResponse::decode(src)?;
802+
803+
Ok(Self {
804+
security_header,
805+
response,
806+
})
807+
}
808+
}
809+
679810
#[cfg(test)]
680811
mod tests {
681812
use super::*;
682813

814+
#[test]
815+
fn req_pdu_round_trip() {
816+
let original = AutoDetectReqPdu::new(AutoDetectRequest::RttRequest {
817+
sequence_number: 7,
818+
request_type: RTT_REQUEST_CONTINUOUS,
819+
});
820+
assert_eq!(original.security_header.flags, BasicSecurityHeaderFlags::AUTODETECT_REQ);
821+
822+
let encoded = ironrdp_core::encode_vec(&original).unwrap();
823+
let decoded = ironrdp_core::decode::<AutoDetectReqPdu>(&encoded).unwrap();
824+
assert_eq!(decoded, original);
825+
}
826+
827+
#[test]
828+
fn rsp_pdu_round_trip() {
829+
let original = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number: 7 });
830+
assert_eq!(original.security_header.flags, BasicSecurityHeaderFlags::AUTODETECT_RSP);
831+
832+
let encoded = ironrdp_core::encode_vec(&original).unwrap();
833+
let decoded = ironrdp_core::decode::<AutoDetectRspPdu>(&encoded).unwrap();
834+
assert_eq!(decoded, original);
835+
}
836+
837+
#[test]
838+
fn req_pdu_rejects_response_flag() {
839+
// A response-flagged frame must not decode as a request PDU.
840+
let rsp = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number: 1 });
841+
let encoded = ironrdp_core::encode_vec(&rsp).unwrap();
842+
assert!(ironrdp_core::decode::<AutoDetectReqPdu>(&encoded).is_err());
843+
}
844+
683845
// ========================================================================
684846
// Request encoding/decoding tests
685847
// ========================================================================

0 commit comments

Comments
 (0)