Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions crates/ironrdp-acceptor/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub struct Acceptor {
security: SecurityProtocol,
io_channel_id: u16,
user_channel_id: u16,
message_channel_id: Option<u16>,
desktop_size: DesktopSize,
server_capabilities: Vec<CapabilitySet>,
static_channels: StaticChannelSet,
Expand All @@ -45,6 +46,13 @@ pub struct AcceptorResult {
pub input_events: Vec<Vec<u8>>,
pub user_channel_id: u16,
pub io_channel_id: u16,
/// MCS channel ID of the message channel, present when the client requested
/// one via Client Message Channel Data (section 2.2.1.3.7).
///
/// Server-initiated PDUs that ride the message channel (network auto-detect
/// per section 2.2.14, multitransport bootstrap, heartbeat) are sent on this
/// channel. `None` when the client did not request it.
pub message_channel_id: Option<u16>,
pub reactivation: bool,
/// Credentials received from the client during SecureSettingsExchange.
///
Expand All @@ -69,6 +77,7 @@ impl Acceptor {
state: AcceptorState::InitiationWaitRequest,
user_channel_id: USER_CHANNEL_ID,
io_channel_id: IO_CHANNEL_ID,
message_channel_id: None,
desktop_size,
server_capabilities: capabilities,
static_channels: StaticChannelSet::new(),
Expand Down Expand Up @@ -111,6 +120,7 @@ impl Acceptor {
state,
user_channel_id: consumed.user_channel_id,
io_channel_id: consumed.io_channel_id,
message_channel_id: consumed.message_channel_id,
desktop_size,
server_capabilities: consumed.server_capabilities,
static_channels,
Expand Down Expand Up @@ -170,6 +180,7 @@ impl Acceptor {
input_events,
user_channel_id: self.user_channel_id,
io_channel_id: self.io_channel_id,
message_channel_id: self.message_channel_id,
reactivation: self.reactivation,
credentials: self.received_credentials.take(),
}),
Expand Down Expand Up @@ -364,7 +375,7 @@ impl Sequence for Acceptor {
));
};
let connection_confirm = nego::ConnectionConfirm::Response {
flags: nego::ResponseFlags::empty(),
flags: nego::ResponseFlags::EXTENDED_CLIENT_DATA_SUPPORTED,
protocol,
};

Expand Down Expand Up @@ -426,6 +437,7 @@ impl Sequence for Acceptor {

let gcc_blocks = settings_initial.conference_create_request.into_gcc_blocks();
let early_capability = gcc_blocks.core.optional_data.early_capability_flags;
let client_wants_message_channel = gcc_blocks.message_channel.is_some();

let joined: Vec<_> = gcc_blocks
.network
Expand All @@ -443,7 +455,7 @@ impl Sequence for Acceptor {
.unwrap_or_default();

#[expect(clippy::arithmetic_side_effects)] // IO channel ID is not big enough for overflowing.
let channels = joined
let channels: Vec<_> = joined
.into_iter()
.enumerate()
.map(|(i, channel)| {
Expand All @@ -457,6 +469,15 @@ impl Sequence for Acceptor {
})
.collect();

if client_wants_message_channel {
// Allocate the message channel ID after the I/O channel and
// any static virtual channels. It is advertised in Server
// Message Channel Data and joined alongside the others.
let channel_count = u16::try_from(channels.len()).ok();
self.message_channel_id =
channel_count.and_then(|n| self.io_channel_id.checked_add(n)?.checked_add(1));
}

(
Written::Nothing,
AcceptorState::BasicSettingsSendResponse {
Expand Down Expand Up @@ -484,6 +505,7 @@ impl Sequence for Acceptor {
channel_ids.clone(),
requested_protocol,
skip_channel_join,
self.message_channel_id,
);

let settings_response = mcs::ConnectResponse {
Expand All @@ -507,7 +529,9 @@ impl Sequence for Acceptor {
connection: if skip_channel_join {
ChannelConnectionSequence::skip_channel_join(self.user_channel_id)
} else {
ChannelConnectionSequence::new(self.user_channel_id, self.io_channel_id, channel_ids)
let mut join_channel_ids = channel_ids;
join_channel_ids.extend(self.message_channel_id);
ChannelConnectionSequence::new(self.user_channel_id, self.io_channel_id, join_channel_ids)
},
},
)
Expand Down Expand Up @@ -781,6 +805,7 @@ fn create_gcc_blocks(
channel_ids: Vec<u16>,
requested: SecurityProtocol,
skip_channel_join: bool,
message_channel_id: Option<u16>,
) -> gcc::ServerGccBlocks {
gcc::ServerGccBlocks {
core: gcc::ServerCoreData {
Expand All @@ -796,7 +821,9 @@ fn create_gcc_blocks(
channel_ids,
io_channel,
},
message_channel: None,
message_channel: message_channel_id.map(|id| gcc::ServerMessageChannelData {
mcs_message_channel_id: id,
}),
multi_transport_channel: None,
}
}
148 changes: 132 additions & 16 deletions crates/ironrdp-connector/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use crate::{
pub struct ConnectionResult {
pub io_channel_id: u16,
pub user_channel_id: u16,
/// MCS channel ID of the message channel, when one was negotiated.
pub message_channel_id: Option<u16>,
pub share_id: u32,
pub static_channels: StaticChannelSet,
pub desktop_size: DesktopSize,
Expand Down Expand Up @@ -126,6 +128,8 @@ pub struct ClientConnector {
/// The client address to be used in the Client Info PDU.
pub client_addr: SocketAddr,
pub static_channels: StaticChannelSet,
/// MCS message channel ID assigned by the server, once negotiated.
pub message_channel_id: Option<u16>,
}

impl ClientConnector {
Expand All @@ -135,6 +139,7 @@ impl ClientConnector {
state: ClientConnectorState::ConnectionInitiationSendRequest,
client_addr,
static_channels: StaticChannelSet::new(),
message_channel_id: None,
}
}

Expand Down Expand Up @@ -212,7 +217,15 @@ impl Sequence for ClientConnector {
ClientConnectorState::BasicSettingsExchangeWaitResponse { .. } => Some(&ironrdp_pdu::X224_HINT),
ClientConnectorState::ChannelConnection { channel_connection, .. } => channel_connection.next_pdu_hint(),
ClientConnectorState::SecureSettingsExchange { .. } => None,
ClientConnectorState::ConnectTimeAutoDetection { .. } => None,
ClientConnectorState::ConnectTimeAutoDetection { .. } => {
// Wait for input only when a message channel was negotiated, so
// we can receive connect-time auto-detect requests there.
if self.message_channel_id.is_some() {
Some(&ironrdp_pdu::X224_HINT)
} else {
None
}
}
ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(),
ClientConnectorState::MultitransportBootstrapping { .. } => None,
ClientConnectorState::CapabilitiesExchange {
Expand Down Expand Up @@ -382,9 +395,10 @@ impl Sequence for ClientConnector {
return Err(general_err!("can't satisfy server security settings"));
}

if server_gcc_blocks.message_channel.is_some() {
warn!("Unexpected ServerMessageChannelData GCC block (not supported)");
}
self.message_channel_id = server_gcc_blocks
.message_channel
.as_ref()
.map(|data| data.mcs_message_channel_id);

if server_gcc_blocks.multi_transport_channel.is_some() {
warn!("Unexpected MultiTransportChannelData GCC block (not supported)");
Expand Down Expand Up @@ -418,7 +432,9 @@ impl Sequence for ClientConnector {
channel_connection: if skip_channel_join {
ChannelConnectionSequence::skip_channel_join()
} else {
ChannelConnectionSequence::new(io_channel_id, static_channel_ids)
let mut join_channel_ids = static_channel_ids;
join_channel_ids.extend(self.message_channel_id);
ChannelConnectionSequence::new(io_channel_id, join_channel_ids)
},
},
)
Expand Down Expand Up @@ -485,12 +501,57 @@ impl Sequence for ClientConnector {
ClientConnectorState::ConnectTimeAutoDetection {
io_channel_id,
user_channel_id,
} => (
Written::Nothing,
ClientConnectorState::LicensingExchange {
io_channel_id,
user_channel_id,
license_exchange: LicenseExchangeSequence::new(
} => {
// The server may run Optional Connect-Time Auto-Detection on the
// message channel before licensing ([MS-RDPBCGR] 1.3.8). When a
// message channel was negotiated we wait for a PDU here and demux
// by MCS channel: a PDU on the message channel is never a licensing
// PDU, so it must not be handed to the licensing sequence. An
// auto-detect request is answered and we keep listening; any other
// message-channel PDU is not ours to act on in this phase and is
// ignored. The first PDU that is not on the message channel (the
// licensing PDU on the I/O channel) ends the phase. Without a
// message channel nothing is read and we go straight to licensing,
// as before.
let autodetect = self.message_channel_id.and_then(|message_channel_id| {
let mcs = decode::<X224<mcs::McsMessage<'_>>>(input).ok()?;
match mcs.0 {
mcs::McsMessage::SendDataIndication(data) if data.channel_id == message_channel_id => {
decode::<rdp::autodetect::AutoDetectReqPdu>(&data.user_data).ok()
}
_ => None,
}
});
let on_message_channel = self.message_channel_id.is_some_and(|message_channel_id| {
decode::<X224<mcs::McsMessage<'_>>>(input).is_ok_and(|mcs| {
matches!(mcs.0, mcs::McsMessage::SendDataIndication(data) if data.channel_id == message_channel_id)
})
});

if let (Some(message_channel_id), Some(pdu)) = (self.message_channel_id, autodetect) {
let written =
respond_to_connect_time_autodetect(pdu.request, message_channel_id, user_channel_id, output)?;
(
written,
ClientConnectorState::ConnectTimeAutoDetection {
io_channel_id,
user_channel_id,
},
)
} else if on_message_channel {
// A message-channel PDU we do not handle in this phase (per the
// canonical sequence multitransport bootstrap is Phase 8 and
// heartbeat is post-connection, both after licensing). Ignore it
// and keep listening rather than decoding it as a licensing PDU.
(
Written::Nothing,
ClientConnectorState::ConnectTimeAutoDetection {
io_channel_id,
user_channel_id,
},
)
} else {
let mut license_exchange = LicenseExchangeSequence::new(
io_channel_id,
self.config.credentials.username().unwrap_or("").to_owned(),
self.config.domain.clone(),
Expand All @@ -499,9 +560,39 @@ impl Sequence for ClientConnector {
.license_cache
.clone()
.unwrap_or_else(|| Arc::new(NoopLicenseCache)),
),
},
),
);
// If a PDU was read (message channel present) it is the first
// licensing PDU; process it here and advance the same way the
// LicensingExchange state would, so it is not stepped twice.
// Otherwise nothing was read and the licensing sequence runs
// from its first step as before.
if self.message_channel_id.is_some() {
let written = license_exchange.step(input, output)?;
let next_state = if license_exchange.state.is_terminal() {
ClientConnectorState::MultitransportBootstrapping {
io_channel_id,
user_channel_id,
}
} else {
ClientConnectorState::LicensingExchange {
io_channel_id,
user_channel_id,
license_exchange,
}
};
(written, next_state)
} else {
(
Written::Nothing,
ClientConnectorState::LicensingExchange {
io_channel_id,
user_channel_id,
license_exchange,
},
)
}
}
}

//== Licensing ==//
// Server is sending information regarding licensing.
Expand Down Expand Up @@ -585,6 +676,7 @@ impl Sequence for ClientConnector {
result: ConnectionResult {
io_channel_id,
user_channel_id,
message_channel_id: self.message_channel_id,
share_id,
static_channels: mem::take(&mut self.static_channels),
desktop_size,
Expand Down Expand Up @@ -631,6 +723,27 @@ pub fn encode_send_data_request<T: Encode>(
Ok(written)
}

fn respond_to_connect_time_autodetect(
request: rdp::autodetect::AutoDetectRequest,
message_channel_id: u16,
user_channel_id: u16,
output: &mut WriteBuf,
) -> ConnectorResult<Written> {
use ironrdp_pdu::rdp::autodetect::{AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};

match request {
AutoDetectRequest::RttRequest { sequence_number, .. } => {
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
let written = encode_send_data_request(user_channel_id, message_channel_id, &response, output)?;
Written::from_size(written)
}
// The Network Characteristics Result is informational, and bandwidth
// measurement is driven implicitly by the connect-time payload exchange.
// Neither requires an immediate reply.
_ => Ok(Written::Nothing),
}
}

#[expect(single_use_lifetimes)] // anonymous lifetimes in `impl Trait` are unstable
fn create_gcc_blocks<'a>(
config: &Config,
Expand Down Expand Up @@ -695,6 +808,7 @@ fn create_gcc_blocks<'a>(
let mut early_capability_flags = ClientEarlyCapabilityFlags::VALID_CONNECTION_TYPE
| ClientEarlyCapabilityFlags::SUPPORT_ERR_INFO_PDU
| ClientEarlyCapabilityFlags::STRONG_ASYMMETRIC_KEYS
| ClientEarlyCapabilityFlags::SUPPORT_NET_CHAR_AUTODETECT
| ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN;

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