Files
nexus/rust/vendor/ironrdp-session/src/x224/mod.rs
T
Andy Dixon 50c0104d57 feat(core): RDP client on IronRDP
clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.

IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.

Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
2026-10-12 10:50:24 +01:00

291 lines
14 KiB
Rust

use ironrdp_core::{WriteBuf, decode};
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage, SendDataIndicationCtx};
use ironrdp_pdu::rdp::autodetect::{AutoDetectReqPdu, AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
use ironrdp_pdu::rdp::headers::ShareDataPdu;
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
use ironrdp_pdu::x224::X224;
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages, client_encode_svc_messages};
use tracing::debug;
use crate::{SessionError, SessionErrorExt as _, SessionResult, reason_err};
/// X224 Processor output
#[derive(Debug, Clone)]
pub enum ProcessorOutput {
/// A buffer with encoded data to send to the server.
ResponseFrame(Vec<u8>),
/// A graceful disconnect notification. Client should close the connection upon receiving this.
Disconnect(DisconnectDescription),
/// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the
/// [Deactivation-Reactivation Sequence].
///
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
DeactivateAll,
/// Server Initiate Multitransport Request. The application should establish a
/// sideband UDP transport using the request ID and security cookie, then send
/// a [`MultitransportResponsePdu`] back on the IO channel.
///
/// See [\[MS-RDPBCGR\] 2.2.15.1].
///
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
/// [`MultitransportResponsePdu`]: ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu
MultitransportRequest(MultitransportRequestPdu),
/// Auto-detect network characteristics from server ([\[MS-RDPBCGR\] 2.2.14]).
///
/// Currently only surfaces [`AutoDetectRequest::NetworkCharacteristicsResult`].
/// RTT requests are handled internally with automatic responses.
///
/// [\[MS-RDPBCGR\] 2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dc672839-4f4e-40b1-a71c-cd6a959baa38
AutoDetect(AutoDetectRequest),
/// Slow-path graphics update ([MS-RDPBCGR] 2.2.9.1.1.3).
/// Raw update payload starting with `updateType(u16)`.
GraphicsUpdate(Vec<u8>),
/// Slow-path pointer update ([MS-RDPBCGR] 2.2.9.1.1.4).
/// Raw pointer payload starting with `messageType(u16) + pad(u16)`.
PointerUpdate(Vec<u8>),
}
#[derive(Debug, Clone)]
pub enum DisconnectDescription {
/// Includes the reason from the MCS Disconnect Provider Ultimatum.
/// This is the least-specific disconnect reason and is only used
/// when a more specific disconnect code is not available.
McsDisconnect(DisconnectReason),
/// Includes the error information sent by the RDP server when there
/// is a connection or disconnection failure.
ErrorInfo(ErrorInfo),
}
pub struct Processor {
static_channels: StaticChannelSet,
user_channel_id: u16,
io_channel_id: u16,
message_channel_id: Option<u16>,
share_id: u32,
}
impl Processor {
pub fn new(
static_channels: StaticChannelSet,
user_channel_id: u16,
io_channel_id: u16,
message_channel_id: Option<u16>,
share_id: u32,
) -> Self {
Self {
static_channels,
user_channel_id,
io_channel_id,
message_channel_id,
share_id,
}
}
pub fn set_share_id(&mut self, share_id: u32) {
self.share_id = share_id;
}
pub fn get_svc_processor<T: SvcProcessor + 'static>(&self) -> Option<&T> {
self.static_channels
.get_by_type::<T>()
.and_then(|svc| svc.channel_processor_downcast_ref())
}
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
self.static_channels
.get_by_type_mut::<T>()
.and_then(|svc| svc.channel_processor_downcast_mut())
}
/// Completes user's SVC request with data, required to sent it over the network and returns
/// a buffer with encoded data.
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
&self,
messages: SvcProcessorMessages<C>,
) -> SessionResult<Vec<u8>> {
let channel_id = self
.static_channels
.get_channel_id_by_type::<C>()
.ok_or_else(|| reason_err!("SVC", "channel not found"))?;
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
}
pub fn get_dvc<T: DvcProcessor + 'static>(&self) -> Option<&DynamicVirtualChannel> {
self.get_svc_processor::<DrdynvcClient>()?.get_dvc_by_type_id::<T>()
}
pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
self.get_svc_processor::<DrdynvcClient>()?
.get_dvc_by_channel_id(channel_id)
}
/// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed
/// in the returned order.
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<ProcessorOutput>> {
let data_ctx: SendDataIndicationCtx<'_> =
ironrdp_pdu::mcs::decode_send_data_indication(frame).map_err(SessionError::decode)?;
let channel_id = data_ctx.channel_id;
if channel_id == self.io_channel_id {
self.process_io_channel(data_ctx)
} else if self.message_channel_id == Some(channel_id) {
self.process_message_channel(data_ctx)
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?;
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
} else {
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
}
}
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
let io_channel = ironrdp_pdu::rdp::headers::decode_io_channel(data_ctx).map_err(SessionError::decode)?;
match io_channel {
ironrdp_pdu::rdp::headers::IoChannelPdu::Data(ctx) => {
match ctx.pdu {
ShareDataPdu::SaveSessionInfo(session_info) => {
debug!("Got Session Save Info PDU: {session_info:?}");
Ok(Vec::new())
}
// FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU"
ShareDataPdu::SetKeyboardIndicators(data) => {
debug!("Got Keyboard Indicators PDU: {data:?}");
Ok(Vec::new())
}
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
ProtocolIndependentCode::None,
))) => {
debug!("Received None server error");
Ok(Vec::new())
}
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
// This is a part of server-side graceful disconnect procedure defined
// in [MS-RDPBCGR].
//
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8
let desc = DisconnectDescription::ErrorInfo(e);
Ok(vec![ProcessorOutput::Disconnect(desc)])
}
ShareDataPdu::ShutdownDenied => {
debug!("ShutdownDenied received, session will be closed");
// As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we
// need to send a disconnect ultimatum to the server if we want to proceed with the
// session shutdown.
//
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
let ultimatum = McsMessage::DisconnectProviderUltimatum(
DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested),
);
let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode);
Ok(vec![
ProcessorOutput::ResponseFrame(encoded_pdu?),
ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect(
DisconnectReason::UserRequested,
)),
])
}
// TODO: slow-path payloads may be bulk-compressed when
// ClientInfoFlags::COMPRESSION is negotiated. Decompression
// should happen here before passing data downstream. Currently
// IronRDP does not wire bulk decompression into this path.
// FIXME: until this is wired, the client deliberately defaults to the simple,
// stateless-friendly MPPC 64K (RDP5) compression level rather than XCRUSH; a
// stateful codec would risk silent corruption on slow-path updates.
ShareDataPdu::Update(data) => {
debug!("Got slow-path graphics update ({} bytes)", data.len());
Ok(vec![ProcessorOutput::GraphicsUpdate(data)])
}
ShareDataPdu::Pointer(data) => {
debug!("Got slow-path pointer update ({} bytes)", data.len());
Ok(vec![ProcessorOutput::PointerUpdate(data)])
}
_ => Err(reason_err!(
"IO channel",
"unhandled PDU: {:?}",
ctx.pdu.as_short_name()
)),
}
}
ironrdp_pdu::rdp::headers::IoChannelPdu::MultitransportRequest(pdu) => {
debug!(
"Received Initiate Multitransport Request: request_id={}",
pdu.request_id
);
Ok(vec![ProcessorOutput::MultitransportRequest(pdu)])
}
ironrdp_pdu::rdp::headers::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll]),
}
}
/// Process an auto-detect request received on the MCS message channel.
///
/// During continuous auto-detection ([MS-RDPBCGR] 2.2.14) the server sends
/// RTT (and bandwidth) requests on the message channel; the client answers
/// RTT requests and surfaces the final Network Characteristics Result.
fn process_message_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
let Some(message_channel_id) = self.message_channel_id else {
return Err(reason_err!("message channel", "no message channel negotiated"));
};
let req = decode::<AutoDetectReqPdu>(data_ctx.user_data).map_err(SessionError::decode)?;
match req.request {
AutoDetectRequest::RttRequest { sequence_number, .. } => {
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
let mut frame = WriteBuf::new();
ironrdp_pdu::mcs::encode_send_data_request(
self.user_channel_id,
message_channel_id,
&response,
&mut frame,
)
.map_err(SessionError::encode)?;
debug!(sequence_number, "Responded to auto-detect RTT request");
Ok(vec![ProcessorOutput::ResponseFrame(frame.into_inner())])
}
req @ AutoDetectRequest::NetworkCharacteristicsResult { .. } => {
debug!(?req, "Received network characteristics from server");
Ok(vec![ProcessorOutput::AutoDetect(req)])
}
req => {
debug!(?req, "Auto-detect request not yet implemented");
Ok(Vec::new())
}
}
}
/// Send a pdu on the static global channel. Typically used to send input events
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
let written = ironrdp_pdu::rdp::headers::encode_share_data(
self.user_channel_id,
self.io_channel_id,
self.share_id,
pdu,
output,
)
.map_err(SessionError::encode)?;
Ok(written)
}
}
/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel.
///
/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer.
/// The messages returned here are ready to be sent to the server.
///
/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel.
fn process_svc_messages(messages: Vec<SvcMessage>, channel_id: u16, initiator_id: u16) -> SessionResult<Vec<u8>> {
client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode)
}