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(