Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2868,27 +2868,45 @@ enum RdpControlFlow {
struct ActiveSessionIteration {
outputs: Vec<ActiveStageOutput>,
dvc_batch: Option<DvcMessageBatch>,
/// The tunnel the DVC batch answers a request from. MS-RDPEDYC responses go back on the
/// transport their request arrived on, which the Soft-Sync routing table cannot tell for a
/// Create Request received on the tunnel and declined: that channel is never bound.
reply_tunnel: Option<SoftSyncTunnelType>,
}

impl ActiveSessionIteration {
fn outputs(outputs: Vec<ActiveStageOutput>) -> Self {
Self {
outputs,
dvc_batch: None,
reply_tunnel: None,
}
}

fn dvc(dvc_batch: DvcMessageBatch) -> Self {
Self {
outputs: Vec::new(),
dvc_batch: Some(dvc_batch),
reply_tunnel: None,
}
}

fn with_outputs(dvc_batch: DvcMessageBatch, outputs: Vec<ActiveStageOutput>) -> Self {
Self {
outputs,
dvc_batch: Some(dvc_batch),
reply_tunnel: None,
}
}

fn tunnel(
tunnel_type: SoftSyncTunnelType,
(dvc_batch, outputs): (DvcMessageBatch, Vec<ActiveStageOutput>),
) -> Self {
Self {
outputs,
dvc_batch: Some(dvc_batch),
reply_tunnel: Some(tunnel_type),
}
}
}
Expand Down Expand Up @@ -3122,8 +3140,9 @@ async fn active_session(
};
let buffered_udp_iteration = if initial_outputs.is_none() && active_stage.reliable_udp_dvc_tunnel_in_use() {
match pending_udp_payload.take() {
Some(payload) => Some(ActiveSessionIteration::dvc(
active_stage.process_dvc_tunnel(SoftSyncTunnelType::RELIABLE_UDP, &payload)?,
Some(payload) => Some(ActiveSessionIteration::tunnel(
SoftSyncTunnelType::RELIABLE_UDP,
active_stage.process_dvc_tunnel(&mut image, SoftSyncTunnelType::RELIABLE_UDP, &payload)?,
)),
None => None,
}
Expand Down Expand Up @@ -3263,9 +3282,14 @@ async fn active_session(
}
Some(payload) => {
if active_stage.reliable_udp_dvc_tunnel_in_use() {
let batch =
active_stage.process_dvc_tunnel(SoftSyncTunnelType::RELIABLE_UDP, &payload)?;
ActiveSessionIteration::dvc(batch)
ActiveSessionIteration::tunnel(
SoftSyncTunnelType::RELIABLE_UDP,
active_stage.process_dvc_tunnel(
&mut image,
SoftSyncTunnelType::RELIABLE_UDP,
&payload,
)?,
)
} else {
// The server can send on UDP immediately after its Soft-Sync request,
// before the independently ordered request arrives over TCP. Stop
Expand Down Expand Up @@ -3685,11 +3709,11 @@ async fn active_session(
let channel_id = batch.channel_id();
let messages = batch.into_messages();
#[cfg(feature = "udp")]
let route_over_udp =
active_stage.dvc_tunnel_for_channel(channel_id) == Some(SoftSyncTunnelType::RELIABLE_UDP);
let route_over_udp = iteration.reply_tunnel == Some(SoftSyncTunnelType::RELIABLE_UDP)
|| active_stage.dvc_tunnel_for_channel(channel_id) == Some(SoftSyncTunnelType::RELIABLE_UDP);
#[cfg(not(feature = "udp"))]
let route_over_udp = {
let _ = channel_id;
let _ = (channel_id, iteration.reply_tunnel);
false
};
if route_over_udp {
Expand Down
168 changes: 117 additions & 51 deletions crates/ironrdp-dvc/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ pub struct DrdynvcClient {
cap_handshake_done: bool,
available_tunnels: BTreeSet<SoftSyncTunnelType>,
tunnel_channels: BTreeMap<DynamicChannelId, SoftSyncTunnelType>,
switched_tunnels: BTreeSet<SoftSyncTunnelType>,
soft_sync_complete: bool,
}

Expand Down Expand Up @@ -157,6 +158,7 @@ impl DrdynvcClient {
cap_handshake_done: false,
available_tunnels: BTreeSet::new(),
tunnel_channels: BTreeMap::new(),
switched_tunnels: BTreeSet::new(),
soft_sync_complete: false,
}
}
Expand Down Expand Up @@ -307,8 +309,8 @@ impl DrdynvcClient {
}

pub fn close_channel(&mut self, channel_id: u32) -> Option<SvcMessage> {
self.dynamic_channels.remove_by_channel_id(channel_id)?;
self.tunnel_channels.remove(&channel_id);
self.dynamic_channels.remove_by_channel_id(channel_id)?;
Some(SvcMessage::from(DrdynvcClientPdu::Close(ClosePdu::new(channel_id))))
}

Expand Down Expand Up @@ -344,30 +346,126 @@ impl DrdynvcClient {
self.tunnel_channels.values().any(|selected| *selected == tunnel_type)
}

/// Returns whether the Soft-Sync response switched DVC traffic to `tunnel_type`.
///
/// The tunnel stays in use after every channel bound to it has closed, because the server
/// can open new channels on it at any time.
pub fn switched_to_tunnel(&self, tunnel_type: SoftSyncTunnelType) -> bool {
self.switched_tunnels.contains(&tunnel_type)
}

/// Processes raw DRDYNVC data received through `tunnel_type`.
///
/// The channel's Soft-Sync-selected route is validated before the dynamic channel sees the
/// data, so data arriving on the wrong tunnel is rejected (MS-RDPEDYC 3.1.5.4.3, 3.2.5.3.2).
/// The returned batch carries the channel ID so response messages can be routed back onto
/// the same tunnel.
///
/// Once the Soft-Sync response has switched to a tunnel, the server may also open and close
/// dynamic channels directly on it: Windows creates `AUDIO_PLAYBACK_DVC` and `RDS::Input`
/// on the reliable UDP tunnel. A channel created on a tunnel is bound to that tunnel for the
/// rest of its life, and every response in the returned batch belongs on the tunnel the
/// request arrived on, including the response to a Create Request this client declines.
pub fn process_tunnel(&mut self, tunnel_type: SoftSyncTunnelType, payload: &[u8]) -> PduResult<DvcMessageBatch> {
let pdu = decode_dvc_message(payload).map_err(|e| decode_err!(e))?;
let DrdynvcServerPdu::Data(data) = pdu else {
return Err(pdu_other_err!("only DVC data is permitted on a multitransport tunnel"));
};
let channel_id = data.channel_id();
let selected_tunnel = self
.tunnel_channels
.get(&channel_id)
.copied()
.ok_or_else(|| pdu_other_err!("received tunneled data for a channel not selected by Soft-Sync"))?;
if tunnel_type != selected_tunnel {
if !self.switched_tunnels.contains(&tunnel_type) {
return Err(pdu_other_err!(
"received tunneled data on a tunnel not selected for the dynamic channel"
"received tunneled DVC traffic on a tunnel Soft-Sync did not switch to"
));
}
let messages = self.process_data(data)?;
Ok(DvcMessageBatch::new(channel_id, messages))
let pdu = decode_dvc_message(payload).map_err(|e| decode_err!(e))?;
match pdu {
DrdynvcServerPdu::Data(data) => {
let channel_id = data.channel_id();
let selected_tunnel =
self.tunnel_channels.get(&channel_id).copied().ok_or_else(|| {
pdu_other_err!("received tunneled data for a channel not selected by Soft-Sync")
})?;
if tunnel_type != selected_tunnel {
return Err(pdu_other_err!(
"received tunneled data on a tunnel not selected for the dynamic channel"
));
}
let messages = self.process_data(data)?;
Ok(DvcMessageBatch::new(channel_id, messages))
}
DrdynvcServerPdu::Create(create_request) => {
debug!(
?tunnel_type,
"Got DVC Create Request PDU on a multitransport tunnel: {create_request:?}"
);
// The TCP path answers a Create Request that arrives before the capabilities
// exchange with a Capabilities Response first. Capabilities PDUs are not exchanged
// on a tunnel, so a tunnel refuses the out-of-order request instead.
if !self.cap_handshake_done {
return Err(pdu_other_err!(
"received a DVC Create Request on a multitransport tunnel before the capabilities exchange"
));
}
let channel_id = create_request.channel_id();
let (created, messages) = self.process_create(create_request)?;
if created {
self.tunnel_channels.insert(channel_id, tunnel_type);
}
Ok(DvcMessageBatch::new(channel_id, messages))
}
Comment on lines +391 to +410

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[skeptical] Tunnel Create path skips the capabilities-response recovery the TCP path performs — low 🟡 — The TCP Create arm synthesizes a Capabilities Response when a Create Request arrives before the caps handshake (client.rs:548-554), defending against out-of-order server PDUs. process_tunnel's Create arm calls process_create with no such check, so a server that sends the Soft-Sync Request before capabilities and then a Create Request on the tunnel gets only a Create Response; cap_handshake_done stays false and a later TCP Create would emit a late caps response. Handling the precondition in the tunnel path (or asserting it) keeps the two Create paths consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0c520e8. A Create Request that arrives on a tunnel before the capabilities exchange is now refused. The TCP path recovers by sending a Capabilities Response on DRDYNVC first. Capabilities PDUs are not exchanged on a tunnel, so the tunnel path cannot do the same. Soft-Sync does not depend on the DVC version, so the Soft-Sync request can't rule this case out either. Covered by the new dvc::client::tunnel_refuses_a_create_request_before_the_capabilities_exchange; the two existing tunnel tests now run the capabilities exchange first.

DrdynvcServerPdu::Close(close) => {
debug!(?tunnel_type, "Got DVC Close PDU on a multitransport tunnel: {close:?}");
let channel_id = close.channel_id();
let messages = self.process_close(channel_id);
Ok(DvcMessageBatch::new(channel_id, messages))
}
Comment on lines +411 to +416

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[skeptical] Closing the last tunnel-bound channel stalls tunnel traffic permanently with no recovery path — low 🟡 — The shared process_close removes the tunnel binding, so after the server closes every channel bound to the reliable UDP tunnel, has_channels_on_tunnel (and reliable_udp_dvc_tunnel_in_use) returns false while the UDP transport stays open. In rdp.rs the udp_payload arm then stores tunnel payloads in pending_udp_payload, which the loop top drains only while the tunnel is in use; the later Create Request that could re-bind a channel is itself such a payload, and a duplicate Soft-Sync request is rejected, so the state can never recover and tunnel traffic is silently buffered instead of failing loudly like the pre-change error path. The same unbinding now also happens for server Closes arriving over TCP. The code chain is verified; whether a Windows host actually closes all tunnel channels and reuses the tunnel within one session is unverified, so severity stays low.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 963e3cf. The DRDYNVC client now records the tunnels its Soft-Sync response switched to (DrdynvcClient::switched_to_tunnel), and ActiveStage::reliable_udp_dvc_tunnel_in_use uses that instead of the current channel bindings. The tunnel therefore stays in use after its last channel closes, and a later Create Request on it is processed. process_tunnel now also rejects traffic on a tunnel the response did not switch to. Covered by the new dvc::client::tunnel_stays_in_use_after_its_channels_close and by a check added to active_stage_exposes_and_validates_soft_sync_routing, which fails without the change.

DrdynvcServerPdu::Capabilities(_) | DrdynvcServerPdu::SoftSyncRequest(_) => Err(pdu_other_err!(
"only DVC data, create and close PDUs are permitted on a multitransport tunnel"
)),
}
}

/// Opens the requested dynamic channel and builds its Create Response, followed by any
/// start messages. Also returns whether the channel was opened.
fn process_create(&mut self, create_request: crate::pdu::CreateRequestPdu) -> PduResult<(bool, Vec<SvcMessage>)> {
let channel_id = create_request.channel_id();
let channel_name = create_request.into_channel_name();
let mut responses = Vec::new();

let (creation_status, start_messages) =
if let Some(dvc) = self.dynamic_channels.try_create_channel(&channel_name, channel_id) {
match dvc.start(channel_id) {
Ok(messages) => (CreationStatus::OK, messages),
Err(e) => {
debug!(
?channel_id, error = %e,
"DVC start failed; removing channel and reporting NO_LISTENER"
);
self.dynamic_channels.remove_by_channel_id(channel_id);
(CreationStatus::NO_LISTENER, Vec::new())
}
}
} else {
(CreationStatus::NO_LISTENER, Vec::new())
};

let create_response = DrdynvcClientPdu::Create(CreateResponsePdu::new(channel_id, creation_status));
debug!("Send DVC Create Response PDU: {create_response:?}");
responses.push(SvcMessage::from(create_response));

// If this DVC has start messages, send them.
if !start_messages.is_empty() {
responses.extend(
encode_dvc_messages(channel_id, start_messages, ChannelFlags::empty()).map_err(|e| encode_err!(e))?,
);
}

Ok((creation_status == CreationStatus::OK, responses))
}

/// Closes a dynamic channel at the server's request. The Close response is only sent for a
/// channel that was open.
fn process_close(&mut self, channel_id: DynamicChannelId) -> Vec<SvcMessage> {
let Some(close_response) = self.close_channel(channel_id) else {
return Vec::new();
};
debug!(channel_id, "Send DVC Close Response PDU");
alloc::vec![close_response]
}

fn process_data(&mut self, data: DrdynvcDataPdu) -> PduResult<Vec<SvcMessage>> {
Expand Down Expand Up @@ -415,6 +513,7 @@ impl DrdynvcClient {
tunnels_to_switch.push(list.tunnel_type());
}

self.switched_tunnels = tunnels_to_switch.iter().copied().collect();
let response = SvcMessage::from(DrdynvcClientPdu::SoftSyncResponse(SoftSyncResponsePdu::new(
tunnels_to_switch,
)));
Expand Down Expand Up @@ -452,8 +551,6 @@ impl SvcProcessor for DrdynvcClient {
}
DrdynvcServerPdu::Create(create_request) => {
debug!("Got DVC Create Request PDU: {create_request:?}");
let channel_id = create_request.channel_id();
let channel_name = create_request.into_channel_name();

if !self.cap_handshake_done {
debug!(
Expand All @@ -463,43 +560,12 @@ impl SvcProcessor for DrdynvcClient {
responses.push(self.create_capabilities_response(CapsVersion::V2));
}

let (creation_status, start_messages) =
if let Some(dvc) = self.dynamic_channels.try_create_channel(&channel_name, channel_id) {
match dvc.start(channel_id) {
Ok(messages) => (CreationStatus::OK, messages),
Err(e) => {
debug!(
?channel_id, error = %e,
"DVC start failed; removing channel and reporting NO_LISTENER"
);
self.dynamic_channels.remove_by_channel_id(channel_id);
(CreationStatus::NO_LISTENER, Vec::new())
}
}
} else {
(CreationStatus::NO_LISTENER, Vec::new())
};

let create_response = DrdynvcClientPdu::Create(CreateResponsePdu::new(channel_id, creation_status));
debug!("Send DVC Create Response PDU: {create_response:?}");
responses.push(SvcMessage::from(create_response));

// If this DVC has start messages, send them.
if !start_messages.is_empty() {
responses.extend(
encode_dvc_messages(channel_id, start_messages, ChannelFlags::empty())
.map_err(|e| encode_err!(e))?,
);
}
let (_, messages) = self.process_create(create_request)?;
responses.extend(messages);
}
DrdynvcServerPdu::Close(close) => {
debug!("Got DVC Close PDU: {close:?}");
let channel_id = close.channel_id();
if self.dynamic_channels.remove_by_channel_id(channel_id).is_some() {
let close_response = DrdynvcClientPdu::Close(ClosePdu::new(channel_id));
debug!("Send DVC Close Response PDU: {close_response:?}");
responses.push(SvcMessage::from(close_response));
}
responses.extend(self.process_close(close.channel_id()));
}
DrdynvcServerPdu::Data(data) => {
if self.tunnel_channels.contains_key(&data.channel_id()) {
Expand Down
Loading
Loading