From 2fd0674d2485d496742e7203157f5a72da7f5df6 Mon Sep 17 00:00:00 2001 From: AKolenda Date: Fri, 25 Sep 2026 23:27:09 -0600 Subject: [PATCH] fix(dvc): switch a Soft-Sync tunnel that also lists declined channels A Soft-Sync request lists every dynamic channel the server intends to move, including channels the client answered with NO_LISTENER. Windows lists CoreInput, MouseCursor, Video and Geometry next to the graphics pipeline. Any unopened channel in a list made the client drop that whole list, so the tunnel was never switched, and the channels the client had opened stayed on TCP while the server was already sending them on the tunnel. Skip unopened channels one by one and switch the tunnel for the rest. --- crates/ironrdp-dvc/src/client.rs | 24 ++++++------ .../tests/dvc/client.rs | 37 ++++++++++++++++++- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/crates/ironrdp-dvc/src/client.rs b/crates/ironrdp-dvc/src/client.rs index b9dab2e50b..9ae1cd35ce 100644 --- a/crates/ironrdp-dvc/src/client.rs +++ b/crates/ironrdp-dvc/src/client.rs @@ -396,19 +396,21 @@ impl DrdynvcClient { return Err(pdu_other_err!("soft-sync request selected an unavailable tunnel")); } - let mut selected_channels = Vec::new(); + // The server lists every channel it intends to move, including channels this client + // declined with NO_LISTENER: Windows lists CoreInput, MouseCursor, Video and Geometry + // next to the graphics pipeline. Those channels carry no data, so they are skipped + // one by one. Refusing the whole tunnel because of them would leave the channels + // this client did open stranded on TCP while the server already sends on the tunnel. for channel_id in list.channel_ids() { - if self.dynamic_channels.get_by_channel_id(*channel_id).is_none() { - selected_channels.clear(); - break; + if self.dynamic_channels.get_by_channel_id(*channel_id).is_some() { + tunnel_channels.insert(*channel_id, list.tunnel_type()); + } else { + debug!( + channel_id, + tunnel_type = ?list.tunnel_type(), + "Soft-Sync lists a dynamic channel this client did not open; ignoring it" + ); } - selected_channels.push(*channel_id); - } - if selected_channels.is_empty() && !list.channel_ids().is_empty() { - continue; - } - for channel_id in selected_channels { - tunnel_channels.insert(channel_id, list.tunnel_type()); } tunnels_to_switch.push(list.tunnel_type()); } diff --git a/crates/ironrdp-testsuite-core/tests/dvc/client.rs b/crates/ironrdp-testsuite-core/tests/dvc/client.rs index e3b90e854c..b4df109189 100644 --- a/crates/ironrdp-testsuite-core/tests/dvc/client.rs +++ b/crates/ironrdp-testsuite-core/tests/dvc/client.rs @@ -1,4 +1,4 @@ -use ironrdp_core::{encode_vec, impl_as_any}; +use ironrdp_core::{Decode as _, ReadCursor, encode_vec, impl_as_any}; use ironrdp_dvc::ironrdp_pdu::{PduResult, pdu_other_err}; use ironrdp_dvc::pdu::{ DataPdu, DrdynvcClientPdu, DrdynvcDataPdu, DrdynvcServerPdu, SoftSyncChannelList, SoftSyncRequestPdu, @@ -134,6 +134,41 @@ fn soft_sync_rejects_a_tunnel_that_became_unavailable_before_migration() { assert!(!client.has_channels_on_tunnel(SoftSyncTunnelType::RELIABLE_UDP)); } +#[test] +fn soft_sync_skips_channels_the_client_did_not_open() { + let mut client = DrdynvcClient::new(); + client + .attach_established_dynamic_channel(7, RecordedDvc::default()) + .expect("recorded channel should attach"); + client.enable_soft_sync_tunnel(SoftSyncTunnelType::RELIABLE_UDP); + + // Windows lists the channels this client answered with NO_LISTENER (2, 6, 8..=12) next to + // the graphics pipeline it accepted (7). The tunnel must still be switched for channel 7. + let request = encode_vec(&DrdynvcServerPdu::SoftSyncRequest(SoftSyncRequestPdu::new(vec![ + SoftSyncChannelList::new(SoftSyncTunnelType::RELIABLE_UDP, vec![2, 6, 7, 8, 9, 10, 11, 12]), + ]))) + .expect("Soft-Sync request should encode"); + + let responses = client.process(&request).expect("Soft-Sync request should be accepted"); + let [response] = responses.as_slice() else { + panic!("expected exactly one Soft-Sync response"); + }; + let encoded = response + .encode_unframed_pdu() + .expect("Soft-Sync response should encode"); + let DrdynvcClientPdu::SoftSyncResponse(response) = + DrdynvcClientPdu::decode(&mut ReadCursor::new(&encoded)).expect("Soft-Sync response should decode") + else { + panic!("expected a Soft-Sync response"); + }; + assert_eq!(response.tunnels_to_switch(), &[SoftSyncTunnelType::RELIABLE_UDP]); + + assert!(client.soft_sync_complete()); + assert_eq!(client.tunnel_for_channel(7), Some(SoftSyncTunnelType::RELIABLE_UDP)); + assert_eq!(client.tunnel_for_channel(2), None); + assert!(client.has_channels_on_tunnel(SoftSyncTunnelType::RELIABLE_UDP)); +} + #[test] fn message_batch_rejects_a_mismatched_channel_id() { let message = SvcMessage::from(DrdynvcClientPdu::Data(DrdynvcDataPdu::Data(DataPdu::new(