Skip to content

Commit 27c6446

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). Builds on the acceptor message-channel negotiation merged in #1347 (efa5732).
1 parent 45ec1ef commit 27c6446

7 files changed

Lines changed: 443 additions & 151 deletions

File tree

crates/ironrdp-connector/src/connection.rs

Lines changed: 132 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,57 @@ 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 and demux
508+
// by MCS channel: a PDU on the message channel is never a licensing
509+
// PDU, so it must not be handed to the licensing sequence. An
510+
// auto-detect request is answered and we keep listening; any other
511+
// message-channel PDU is not ours to act on in this phase and is
512+
// ignored. The first PDU that is not on the message channel (the
513+
// licensing PDU on the I/O channel) ends the phase. Without a
514+
// message channel nothing is read and we go straight to licensing,
515+
// as before.
516+
let autodetect = self.message_channel_id.and_then(|message_channel_id| {
517+
let mcs = decode::<X224<mcs::McsMessage<'_>>>(input).ok()?;
518+
match mcs.0 {
519+
mcs::McsMessage::SendDataIndication(data) if data.channel_id == message_channel_id => {
520+
decode::<rdp::autodetect::AutoDetectReqPdu>(&data.user_data).ok()
521+
}
522+
_ => None,
523+
}
524+
});
525+
let on_message_channel = self.message_channel_id.is_some_and(|message_channel_id| {
526+
decode::<X224<mcs::McsMessage<'_>>>(input).is_ok_and(|mcs| {
527+
matches!(mcs.0, mcs::McsMessage::SendDataIndication(data) if data.channel_id == message_channel_id)
528+
})
529+
});
530+
531+
if let (Some(message_channel_id), Some(pdu)) = (self.message_channel_id, autodetect) {
532+
let written =
533+
respond_to_connect_time_autodetect(pdu.request, message_channel_id, user_channel_id, output)?;
534+
(
535+
written,
536+
ClientConnectorState::ConnectTimeAutoDetection {
537+
io_channel_id,
538+
user_channel_id,
539+
},
540+
)
541+
} else if on_message_channel {
542+
// A message-channel PDU we do not handle in this phase (per the
543+
// canonical sequence multitransport bootstrap is Phase 8 and
544+
// heartbeat is post-connection, both after licensing). Ignore it
545+
// and keep listening rather than decoding it as a licensing PDU.
546+
(
547+
Written::Nothing,
548+
ClientConnectorState::ConnectTimeAutoDetection {
549+
io_channel_id,
550+
user_channel_id,
551+
},
552+
)
553+
} else {
554+
let mut license_exchange = LicenseExchangeSequence::new(
494555
io_channel_id,
495556
self.config.credentials.username().unwrap_or("").to_owned(),
496557
self.config.domain.clone(),
@@ -499,9 +560,39 @@ impl Sequence for ClientConnector {
499560
.license_cache
500561
.clone()
501562
.unwrap_or_else(|| Arc::new(NoopLicenseCache)),
502-
),
503-
},
504-
),
563+
);
564+
// If a PDU was read (message channel present) it is the first
565+
// licensing PDU; process it here and advance the same way the
566+
// LicensingExchange state would, so it is not stepped twice.
567+
// Otherwise nothing was read and the licensing sequence runs
568+
// from its first step as before.
569+
if self.message_channel_id.is_some() {
570+
let written = license_exchange.step(input, output)?;
571+
let next_state = if license_exchange.state.is_terminal() {
572+
ClientConnectorState::MultitransportBootstrapping {
573+
io_channel_id,
574+
user_channel_id,
575+
}
576+
} else {
577+
ClientConnectorState::LicensingExchange {
578+
io_channel_id,
579+
user_channel_id,
580+
license_exchange,
581+
}
582+
};
583+
(written, next_state)
584+
} else {
585+
(
586+
Written::Nothing,
587+
ClientConnectorState::LicensingExchange {
588+
io_channel_id,
589+
user_channel_id,
590+
license_exchange,
591+
},
592+
)
593+
}
594+
}
595+
}
505596

506597
//== Licensing ==//
507598
// Server is sending information regarding licensing.
@@ -593,6 +684,7 @@ impl Sequence for ClientConnector {
593684
result: ConnectionResult {
594685
io_channel_id,
595686
user_channel_id,
687+
message_channel_id: self.message_channel_id,
596688
share_id,
597689
static_channels: mem::take(&mut self.static_channels),
598690
desktop_size,
@@ -639,6 +731,27 @@ pub fn encode_send_data_request<T: Encode>(
639731
Ok(written)
640732
}
641733

734+
fn respond_to_connect_time_autodetect(
735+
request: rdp::autodetect::AutoDetectRequest,
736+
message_channel_id: u16,
737+
user_channel_id: u16,
738+
output: &mut WriteBuf,
739+
) -> ConnectorResult<Written> {
740+
use ironrdp_pdu::rdp::autodetect::{AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
741+
742+
match request {
743+
AutoDetectRequest::RttRequest { sequence_number, .. } => {
744+
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
745+
let written = encode_send_data_request(user_channel_id, message_channel_id, &response, output)?;
746+
Written::from_size(written)
747+
}
748+
// The Network Characteristics Result is informational, and bandwidth
749+
// measurement is driven implicitly by the connect-time payload exchange.
750+
// Neither requires an immediate reply.
751+
_ => Ok(Written::Nothing),
752+
}
753+
}
754+
642755
#[expect(single_use_lifetimes)] // anonymous lifetimes in `impl Trait` are unstable
643756
fn create_gcc_blocks<'a>(
644757
config: &Config,
@@ -703,6 +816,7 @@ fn create_gcc_blocks<'a>(
703816
let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE
704817
| ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU
705818
| ClientEarlyCapabilityFlags::STRONG_ASYMMETRIC_KEYS
819+
| ClientEarlyCapabilityFlags::SUPPORT_NET_CHAR_AUTODETECT
706820
| ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN;
707821

708822
// TODO(#136): support for ClientEarlyCapabilityFlags::SUPPORT_STATUS_INFO_PDU
@@ -743,8 +857,10 @@ fn create_gcc_blocks<'a>(
743857
// TODO(#139): support for Some(ClientClusterData { flags: RedirectionFlags::REDIRECTION_SUPPORTED, redirection_version: RedirectionVersion::V4, redirected_session_id: 0, }),
744858
cluster: None,
745859
monitor: None,
746-
// TODO(#140): support for Client Message Channel Data (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/f50e791c-de03-4b25-b17e-e914c9020bc3)
747-
message_channel: None,
860+
// Request the MCS message channel, which carries network auto-detect
861+
// ([MS-RDPBCGR] 2.2.14) and the multitransport / heartbeat PDUs. The
862+
// server assigns its ID in Server Message Channel Data.
863+
message_channel: Some(gcc::ClientMessageChannelData),
748864
multi_transport_channel: config
749865
.multitransport_flags
750866
.map(|flags| gcc::MultiTransportChannelData { flags }),

0 commit comments

Comments
 (0)