From 20200bca231a7629f4d6386b74b59630c4a9c78e Mon Sep 17 00:00:00 2001 From: Anton Isaiev Date: Sun, 10 May 2026 03:07:24 +0300 Subject: [PATCH 1/4] fix(connector): handle ServerDeactivateAll during CapabilitiesExchange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some RDP servers (notably GNOME Remote Desktop / grd) send a ServerDeactivateAll PDU before ServerDemandActive during the initial Capabilities Exchange phase. This is valid per MS-RDPBCGR §1.3.1.3 (Deactivation-Reactivation Sequence). Previously this caused a hard error: "unexpected Share Control Pdu (expected ServerDemandActive)" Now the connector skips the DeactivateAll and waits for the next PDU. Fixes #1253 --- .../ironrdp-connector/src/connection_activation.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs index 1c87b323d..778ac569f 100644 --- a/crates/ironrdp-connector/src/connection_activation.rs +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -118,6 +118,19 @@ impl Sequence for ConnectionActivationSequence { ); } + // Some servers (e.g. GNOME Remote Desktop) send a ServerDeactivateAll PDU + // before ServerDemandActive as part of a Deactivation-Reactivation Sequence + // (MS-RDPBCGR §1.3.1.3). Skip it and stay in the same state to wait for + // the actual DemandActive PDU. + if matches!(share_control_ctx.pdu, rdp::headers::ShareControlPdu::ServerDeactivateAll(_)) { + debug!("Received ServerDeactivateAll during CapabilitiesExchange, waiting for ServerDemandActive"); + self.state = ConnectionActivationState::CapabilitiesExchange { + io_channel_id, + user_channel_id, + }; + return Ok(Written::Nothing); + } + let capability_sets = if let rdp::headers::ShareControlPdu::ServerDemandActive(server_demand_active) = share_control_ctx.pdu { From 76eea5387b5821eef1421fe1d2b82f659b06ecec Mon Sep 17 00:00:00 2001 From: Anton Isaiev Date: Tue, 12 May 2026 22:33:12 +0300 Subject: [PATCH 2/4] address review: add tests and comment for DeactivateAll handling - Add comment noting the decoded PDU is intentionally discarded - Add test: DeactivateAll during CapabilitiesExchange stays in same state - Add test: DemandActive after DeactivateAll transitions to ConnectionFinalization --- .../src/connection_activation.rs | 8 +- .../tests/session/connection_activation.rs | 122 ++++++++++++++++++ .../tests/session/mod.rs | 1 + 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 crates/ironrdp-testsuite-core/tests/session/connection_activation.rs diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs index 778ac569f..d022badef 100644 --- a/crates/ironrdp-connector/src/connection_activation.rs +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -122,7 +122,13 @@ impl Sequence for ConnectionActivationSequence { // before ServerDemandActive as part of a Deactivation-Reactivation Sequence // (MS-RDPBCGR §1.3.1.3). Skip it and stay in the same state to wait for // the actual DemandActive PDU. - if matches!(share_control_ctx.pdu, rdp::headers::ShareControlPdu::ServerDeactivateAll(_)) { + // + // The decoded PDU is intentionally discarded: the DeactivateAll body carries + // no payload we need during initial activation. + if matches!( + share_control_ctx.pdu, + rdp::headers::ShareControlPdu::ServerDeactivateAll(_) + ) { debug!("Received ServerDeactivateAll during CapabilitiesExchange, waiting for ServerDemandActive"); self.state = ConnectionActivationState::CapabilitiesExchange { io_channel_id, diff --git a/crates/ironrdp-testsuite-core/tests/session/connection_activation.rs b/crates/ironrdp-testsuite-core/tests/session/connection_activation.rs new file mode 100644 index 000000000..79887d371 --- /dev/null +++ b/crates/ironrdp-testsuite-core/tests/session/connection_activation.rs @@ -0,0 +1,122 @@ +use std::borrow::Cow; + +use ironrdp_connector::connection_activation::{ConnectionActivationSequence, ConnectionActivationState}; +use ironrdp_connector::{Credentials, DesktopSize, Sequence as _, Written}; +use ironrdp_core::{WriteBuf, encode_vec}; +use ironrdp_pdu::gcc; +use ironrdp_pdu::mcs::{McsMessage, SendDataIndication}; +use ironrdp_pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp_pdu::rdp::headers::{ServerDeactivateAll, ShareControlHeader, ShareControlPdu}; +use ironrdp_pdu::x224::X224; + +use ironrdp_testsuite_core::capsets::SERVER_DEMAND_ACTIVE; + +const USER_CHANNEL_ID: u16 = 1002; +const IO_CHANNEL_ID: u16 = 1003; +const SHARE_ID: u32 = 0x0001_0000; + +fn test_config() -> ironrdp_connector::Config { + ironrdp_connector::Config { + desktop_size: DesktopSize { + width: 1024, + height: 768, + }, + desktop_scale_factor: 0, + enable_tls: true, + enable_credssp: false, + credentials: Credentials::UsernamePassword { + username: "test".into(), + password: "test".into(), + }, + domain: None, + client_build: 0, + client_name: "test".into(), + keyboard_type: gcc::KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + bitmap: None, + dig_product_id: String::new(), + client_dir: String::new(), + platform: MajorPlatformType::UNIX, + hardware_id: None, + request_data: None, + autologon: false, + enable_audio_playback: false, + license_cache: None, + compression_type: None, + enable_server_pointer: false, + pointer_software_rendering: false, + multitransport_flags: None, + performance_flags: Default::default(), + timezone_info: Default::default(), + alternate_shell: String::new(), + work_dir: String::new(), + } +} + +/// Encode a ShareControlPdu as a server-to-client SendDataIndication frame. +fn encode_server_share_control(pdu: ShareControlPdu) -> Vec { + let share_control_header = ShareControlHeader { + share_control_pdu: pdu, + pdu_source: USER_CHANNEL_ID, + share_id: SHARE_ID, + }; + + let user_data = encode_vec(&share_control_header).unwrap(); + + let indication = McsMessage::SendDataIndication(SendDataIndication { + initiator_id: USER_CHANNEL_ID, + channel_id: IO_CHANNEL_ID, + user_data: Cow::Owned(user_data), + }); + + encode_vec(&X224(indication)).unwrap() +} + +#[test] +fn deactivate_all_during_capabilities_exchange_stays_in_same_state() { + let config = test_config(); + let mut seq = ConnectionActivationSequence::new(config, IO_CHANNEL_ID, USER_CHANNEL_ID); + + let frame = encode_server_share_control(ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll)); + let mut output = WriteBuf::new(); + + let written = seq.step(&frame, &mut output).unwrap(); + + assert_eq!(written, Written::Nothing); + assert!( + matches!( + seq.connection_activation_state(), + ConnectionActivationState::CapabilitiesExchange { .. } + ), + "state should remain CapabilitiesExchange after DeactivateAll" + ); +} + +#[test] +fn demand_active_after_deactivate_all_transitions_to_connection_finalization() { + let config = test_config(); + let mut seq = ConnectionActivationSequence::new(config, IO_CHANNEL_ID, USER_CHANNEL_ID); + let mut output = WriteBuf::new(); + + // First: feed DeactivateAll + let deactivate_frame = encode_server_share_control(ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll)); + let written = seq.step(&deactivate_frame, &mut output).unwrap(); + assert_eq!(written, Written::Nothing); + + // Then: feed ServerDemandActive + let demand_active_frame = + encode_server_share_control(ShareControlPdu::ServerDemandActive(SERVER_DEMAND_ACTIVE.clone())); + let written = seq.step(&demand_active_frame, &mut output).unwrap(); + + assert!(written != Written::Nothing, "should have written ClientConfirmActive"); + assert!( + matches!( + seq.connection_activation_state(), + ConnectionActivationState::ConnectionFinalization { .. } + ), + "state should transition to ConnectionFinalization after DemandActive" + ); +} diff --git a/crates/ironrdp-testsuite-core/tests/session/mod.rs b/crates/ironrdp-testsuite-core/tests/session/mod.rs index 917dd2f6a..e9126d910 100644 --- a/crates/ironrdp-testsuite-core/tests/session/mod.rs +++ b/crates/ironrdp-testsuite-core/tests/session/mod.rs @@ -1,4 +1,5 @@ mod autodetect; +mod connection_activation; mod rfx; #[cfg(test)] From 09a8e827ae408847142281e376c3a41416e93eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Cortier?= <3809077+CBenoit@users.noreply.github.com> Date: Fri, 15 May 2026 01:01:37 +0900 Subject: [PATCH 3/4] Apply suggestion from @CBenoit --- crates/ironrdp-connector/src/connection_activation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs index d022badef..cb881e967 100644 --- a/crates/ironrdp-connector/src/connection_activation.rs +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -129,7 +129,7 @@ impl Sequence for ConnectionActivationSequence { share_control_ctx.pdu, rdp::headers::ShareControlPdu::ServerDeactivateAll(_) ) { - debug!("Received ServerDeactivateAll during CapabilitiesExchange, waiting for ServerDemandActive"); + debug!("Skipping Server Deactivate All PDU received during Capabilities Exchange, awaiting Server Demand Active"); self.state = ConnectionActivationState::CapabilitiesExchange { io_channel_id, user_channel_id, From 4051ebc1e57798b9793ca26c9c324f3e080b15e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Cortier?= <3809077+CBenoit@users.noreply.github.com> Date: Fri, 15 May 2026 01:04:10 +0900 Subject: [PATCH 4/4] Apply suggestion from @CBenoit --- crates/ironrdp-connector/src/connection_activation.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs index cb881e967..fa053676e 100644 --- a/crates/ironrdp-connector/src/connection_activation.rs +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -129,7 +129,9 @@ impl Sequence for ConnectionActivationSequence { share_control_ctx.pdu, rdp::headers::ShareControlPdu::ServerDeactivateAll(_) ) { - debug!("Skipping Server Deactivate All PDU received during Capabilities Exchange, awaiting Server Demand Active"); + debug!( + "Skipping Server Deactivate All PDU received during Capabilities Exchange, awaiting Server Demand Active" + ); self.state = ConnectionActivationState::CapabilitiesExchange { io_channel_id, user_channel_id,