From d5cbdae06199d045a5e268aa7e0285f91908a1a2 Mon Sep 17 00:00:00 2001 From: Clint Christopher Canada Date: Sat, 13 Jun 2026 02:21:12 +0800 Subject: [PATCH] feat(acceptor): honor the client-requested desktop size An ironrdp-server always serves the desktop size reported by its RdpServerDisplay, regardless of what the client asked for. When that size differs from the client's display resolution, the client rescales every decoded frame -- which on mstsc costs typing latency and, with H.264 over EGFX, contributes to audio drift. The client's requested resolution is carried only in the GCC Client Core Data of the MCS Connect Initial. The size echoed back in the client's Confirm Active is, per MS-RDPBCGR 2.2.1.13.2, the value the client copied from the server's Demand Active, so it cannot reveal what the client asked for (verified: FreeRDP /size:1024x768 echoes the server's size). And the acceptor commits a size in Demand Active before any server-side code runs. So adopt the Core Data size in the acceptor, before Demand Active, behind an opt-in flag. The session is then negotiated at the client's resolution from the start, with no Deactivation-Reactivation resize. The display handler still observes the negotiated size through request_initial_size. - ironrdp-acceptor: add Acceptor::set_honor_client_desktop_size (additive); adopt the Core Data size (when within the protocol-legal 200..=8192 range) in BasicSettingsWaitInitial and write it into the server Bitmap capability set before Demand Active. Factor the existing inline bitmap-capset desktop-size mutation into a shared set_bitmap_desktop_size helper. - ironrdp-server: add RdpServerBuilder::with_honor_client_desktop_size, backed by a new RdpServerOptions::honor_client_desktop_size field, forwarded to the acceptor in run_connection. Opt-in, default off, so existing servers are unchanged. --- crates/ironrdp-acceptor/src/connection.rs | 79 +++++++++++++++++++++-- crates/ironrdp-server/src/builder.rs | 23 +++++++ crates/ironrdp-server/src/server.rs | 7 ++ 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs index 269c3377a..b3e9b52ea 100644 --- a/crates/ironrdp-acceptor/src/connection.rs +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -36,6 +36,37 @@ pub struct Acceptor { pub(crate) creds: Option, received_credentials: Option, reactivation: bool, + honor_client_desktop_size: bool, +} + +/// Minimum and maximum desktop dimension accepted from a client. +/// +/// A desktop dimension in RDP is a `u16`; [MS-RDPBCGR] caps it at 8192, and +/// 200 is a conservative floor below which a request is treated as malformed. +const MIN_DESKTOP_DIM: u16 = 200; +const MAX_DESKTOP_DIM: u16 = 8192; + +/// Returns the client-requested desktop size if both dimensions are within the +/// protocol-legal range, otherwise `None`. +fn validate_desktop_size(width: u16, height: u16) -> Option { + if (MIN_DESKTOP_DIM..=MAX_DESKTOP_DIM).contains(&width) && (MIN_DESKTOP_DIM..=MAX_DESKTOP_DIM).contains(&height) { + Some(DesktopSize { width, height }) + } else { + None + } +} + +/// Writes `size` into every Bitmap capability set in `capabilities`. +/// +/// The server advertises its desktop size in the Bitmap capability set of the +/// Demand Active PDU; this keeps that advertisement in sync with `size`. +fn set_bitmap_desktop_size(capabilities: &mut [CapabilitySet], size: DesktopSize) { + for cap in capabilities.iter_mut() { + if let CapabilitySet::Bitmap(cap) = cap { + cap.desktop_width = size.width; + cap.desktop_height = size.height; + } + } } #[derive(Debug)] @@ -76,9 +107,29 @@ impl Acceptor { creds, received_credentials: None, reactivation: false, + honor_client_desktop_size: false, } } + /// Adopt the desktop size requested by the client in its Client Core Data + /// instead of the size this acceptor was constructed with. + /// + /// The client's requested resolution is only carried in the GCC Client + /// Core Data of the MCS Connect Initial PDU; the desktop size echoed back + /// later in the client's Confirm Active is, per [MS-RDPBCGR] 2.2.1.13.2, + /// the value the client copied from the *server's* Demand Active, so it + /// cannot be used to discover what the client originally asked for. When + /// this is enabled and the client's request is within the protocol-legal + /// range, the acceptor negotiates that size from the start (it is written + /// into the server's Bitmap capability set before Demand Active is sent), + /// avoiding a Deactivation-Reactivation resize round trip. + /// + /// Disabled by default, preserving the previous behavior of always + /// enforcing the server-provided size. + pub fn set_honor_client_desktop_size(&mut self, honor: bool) { + self.honor_client_desktop_size = honor; + } + pub fn new_deactivation_reactivation( mut consumed: Acceptor, static_channels: StaticChannelSet, @@ -92,12 +143,7 @@ impl Acceptor { return Err(general_err!("invalid acceptor state")); }; - for cap in consumed.server_capabilities.iter_mut() { - if let CapabilitySet::Bitmap(cap) = cap { - cap.desktop_width = desktop_size.width; - cap.desktop_height = desktop_size.height; - } - } + set_bitmap_desktop_size(&mut consumed.server_capabilities, desktop_size); let state = AcceptorState::CapabilitiesSendServer { early_capability, channels: channels.clone(), @@ -118,6 +164,7 @@ impl Acceptor { creds: consumed.creds, received_credentials: consumed.received_credentials, reactivation: true, + honor_client_desktop_size: consumed.honor_client_desktop_size, }) } @@ -427,6 +474,26 @@ 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; + // Adopt the client's requested desktop size (from its Client + // Core Data) before Demand Active is sent, so the session is + // negotiated at that size without a Deactivation-Reactivation + // resize. See `set_honor_client_desktop_size`. + if self.honor_client_desktop_size { + if let Some(client_size) = + validate_desktop_size(gcc_blocks.core.desktop_width, gcc_blocks.core.desktop_height) + { + if client_size != self.desktop_size { + debug!( + requested = ?client_size, + previous = ?self.desktop_size, + "Honoring client-requested desktop size" + ); + self.desktop_size = client_size; + set_bitmap_desktop_size(&mut self.server_capabilities, client_size); + } + } + } + let joined: Vec<_> = gcc_blocks .network .map(|network| { diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index fb959830c..c4fbb783a 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -41,6 +41,7 @@ pub struct BuilderDone { #[cfg(feature = "egfx")] gfx_factory: Option>, display_suppressed: Option>, + honor_client_desktop_size: bool, } pub struct RdpServerBuilder { @@ -140,6 +141,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + honor_client_desktop_size: false, }, } } @@ -160,6 +162,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + honor_client_desktop_size: false, }, } } @@ -226,6 +229,25 @@ impl RdpServerBuilder { self } + /// Negotiate each session at the desktop size the client requests in its + /// Client Core Data, rather than the size reported by the display handler. + /// + /// The client's requested resolution is only carried in the GCC Client + /// Core Data of the connection handshake; the size echoed back in the + /// client's Confirm Active is the value it copied from the server's Demand + /// Active (per [MS-RDPBCGR] 2.2.1.13.2) and so cannot reveal what the + /// client asked for. With this enabled the acceptor adopts the requested + /// size (when within the protocol-legal range) before Demand Active is + /// sent, so the session starts at that size with no Deactivation- + /// Reactivation resize. The display handler observes the negotiated size + /// through [`RdpServerDisplay::request_initial_size`]. + /// + /// Defaults to `false`, enforcing the size reported by the display handler. + pub fn with_honor_client_desktop_size(mut self, honor: bool) -> Self { + self.state.honor_client_desktop_size = honor; + self + } + /// Set a credential validator for TLS-mode connections. /// /// When set, credentials received from the client during @@ -248,6 +270,7 @@ impl RdpServerBuilder { security: self.state.security, codecs: self.state.codecs, max_request_size: self.state.max_request_size, + honor_client_desktop_size: self.state.honor_client_desktop_size, }, self.state.handler, self.state.display, diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 0be2a0707..cba560b83 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -223,6 +223,12 @@ pub struct RdpServerOptions { pub security: RdpServerSecurity, pub codecs: BitmapCodecs, pub max_request_size: u32, + /// When `true`, each connection's acceptor adopts the desktop size the + /// client requests in its Client Core Data (instead of the size reported + /// by the display handler), negotiating that size from the start without a + /// Deactivation-Reactivation resize. Defaults to `false`. Set via + /// [`RdpServerBuilder::with_honor_client_desktop_size`](crate::RdpServerBuilder::with_honor_client_desktop_size). + pub honor_client_desktop_size: bool, } impl RdpServerOptions { @@ -692,6 +698,7 @@ impl RdpServer { let size = self.display.lock().await.size().await; let capabilities = capabilities::capabilities(&self.opts, size); let mut acceptor = Acceptor::new(self.opts.security.flag(), size, capabilities, self.creds.clone()); + acceptor.set_honor_client_desktop_size(self.opts.honor_client_desktop_size); self.attach_channels(&mut acceptor);