Skip to content
Open
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
9 changes: 6 additions & 3 deletions crates/ironrdp-async/src/framed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,16 +317,19 @@ where
Ok(())
}

/// Reads the driver-owned monotonic clock.
/// Reads the monotonic clock behind [`Framed::last_read_at`].
///
/// The epoch is the first call; only differences are meaningful.
/// The epoch is the first call; only differences are meaningful. Transports that
/// carry part of the same session elsewhere, such as a UDP sideband, can stamp
/// their reads with this clock so the session compares them with frames read
/// through [`Framed`].
///
/// `web_time::Instant` is `std::time::Instant` everywhere except
/// `wasm32-unknown-unknown`, where `std`'s panics and this one reads
/// `Performance.now()` instead. This crate is reached from `ironrdp-web` through
/// `ironrdp-futures`, so without it the browser build has no clock and every
/// measurement there is lost.
fn monotonic_now() -> MonotonicInstant {
pub fn monotonic_now() -> MonotonicInstant {
static EPOCH: std::sync::LazyLock<web_time::Instant> = std::sync::LazyLock::new(web_time::Instant::now);
MonotonicInstant::from_millis(u64::try_from(EPOCH.elapsed().as_millis()).unwrap_or(u64::MAX))
}
3 changes: 3 additions & 0 deletions crates/ironrdp-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ pub mod rdp;
mod clipboard;

mod ws;

#[cfg(feature = "udp")]
mod udp;
Comment thread
AKolenda marked this conversation as resolved.
94 changes: 74 additions & 20 deletions crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use ironrdp_pdu::input::mouse::PointerFlags;
all(windows, feature = "webauthn")
))]
use ironrdp_pdu::pdu_other_err;
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
use ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu;
use ironrdp_pdu::rdp::session_info::ServerAutoReconnect;
#[cfg(feature = "rdpdr")]
Expand Down Expand Up @@ -83,8 +84,18 @@ use ironrdp_rdpsnd_native::{RdpeaiCaptureBackend, cpal};

use crate::config::{Config, RDCleanPathConfig, Transport};
use crate::rail::{RailClient, RailControlEvent, RailEvent, RailInputEvent};
#[cfg(feature = "udp")]
use crate::udp::disable_failed_tunnel;
use ironrdp_rail::pdu::{ExecutePdu, ExecuteResultPdu};

/// A Tunnel Data PDU received on the reliable UDP tunnel, ready for the session.
struct ReceivedTunnelMessage {
auto_detect_requests: Vec<AutoDetectRequest>,
payload: Vec<u8>,
/// When the transport read the PDU, on the clock of the TCP reader's `last_read_at`.
received_at: Option<ironrdp_core::MonotonicInstant>,
}

// ── Public event types ────────────────────────────────────────────────────────

/// Explains why a dynamic display update must reconnect instead of completing in-session.
Expand Down Expand Up @@ -3189,7 +3200,8 @@ async fn active_session(
Err(error) => return Err(ironrdp_session::custom_err!("read frame", error)),
};
trace!(?action, frame_length = payload.len(), "Frame received");
let mut outputs = active_stage.process(&mut image, action, &payload)?;
let mut outputs =
active_stage.process_with_timestamp(&mut image, action, &payload, reader.last_read_at())?;
#[cfg(feature = "rdpdr")]
if let Some(output) = poll_deferred_rdpdr_output(&mut active_stage)? {
outputs.push(output);
Expand Down Expand Up @@ -3251,42 +3263,84 @@ async fn active_session(
}
ActiveSessionIteration::outputs(outputs)
}
udp_payload = async {
udp_message = async {
#[cfg(feature = "udp")]
{
match (udp_tunnel.transport.as_mut(), pending_udp_payload.is_none()) {
(Some(transport), true) => transport.recv().await,
(Some(transport), true) => match transport.recv_message().await {
// The read time comes from the same clock as the TCP reader's
// `last_read_at`, so a bandwidth window can span both transports.
Some(message) => Some(ReceivedTunnelMessage {
auto_detect_requests: message.auto_detect_requests(),
payload: message.data,
received_at: transport.last_received_at(),
}),
None => None,
},
(Some(_), false) | (None, _) => {
core::future::pending::<Option<Vec<u8>>>().await
core::future::pending::<Option<ReceivedTunnelMessage>>().await
}
}
}
#[cfg(not(feature = "udp"))]
{
core::future::pending::<Option<Vec<u8>>>().await
core::future::pending::<Option<ReceivedTunnelMessage>>().await
}
} => {
match udp_payload {
match udp_message {
None => {
if active_stage.reliable_udp_dvc_tunnel_in_use() {
return Ok(RdpControlFlow::TransportFailure(
ironrdp_session::general_err!("reliable UDP tunnel closed"),
));
}
#[cfg(feature = "udp")]
{
udp_tunnel.transport = None;
if let Err(error) = disable_failed_tunnel(
&mut active_stage,
&mut udp_tunnel.transport,
ironrdp_session::general_err!("reliable UDP tunnel closed"),
) {
return Ok(RdpControlFlow::TransportFailure(error));
}
}
active_stage.disable_reliable_udp_dvc_tunnel()?;
warn!("Reliable UDP tunnel closed before Soft-Sync; continuing with TCP");
ActiveSessionIteration::outputs(Vec::new())
}
Some(payload) if payload.is_empty() => {
trace!("Ignoring reliable UDP tunnel PDU without higher-layer data");
ActiveSessionIteration::outputs(Vec::new())
}
Some(payload) => {
if active_stage.reliable_udp_dvc_tunnel_in_use() {
Some(ReceivedTunnelMessage { auto_detect_requests, payload, received_at }) => {
#[cfg(feature = "udp")]
{
let responses = active_stage.process_tunnel_auto_detect(
auto_detect_requests,
payload.len(),
received_at,
);
let reply = ironrdp_rdpeudp_tokio::TunnelMessage::from_auto_detect_responses(&responses);
if let Some(reply) = reply
&& let Some(transport) = udp_tunnel.transport.as_ref()
{
let Some(result) =
cancelable_operation(transport.send_message(reply), close_receiver).await
else {
return Ok(RdpControlFlow::TerminatedGracefully(
GracefulDisconnectReason::UserInitiated,
));
};
if let Err(error) = result {
if let Err(error) = disable_failed_tunnel(
&mut active_stage,
&mut udp_tunnel.transport,
ironrdp_session::custom_err!("answer reliable UDP tunnel auto-detect", error),
) {
return Ok(RdpControlFlow::TransportFailure(error));
}
// No channel migrated, and the tunnel is now withdrawn, so
// nothing will read this PDU's data.
continue;
}
Comment thread
AKolenda marked this conversation as resolved.
}
}
#[cfg(not(feature = "udp"))]
let _ = (auto_detect_requests, received_at);

if payload.is_empty() {
trace!("Reliable UDP tunnel PDU without higher-layer data");
ActiveSessionIteration::outputs(Vec::new())
} else if active_stage.reliable_udp_dvc_tunnel_in_use() {
ActiveSessionIteration::tunnel(
SoftSyncTunnelType::RELIABLE_UDP,
active_stage.process_dvc_tunnel(
Expand Down
22 changes: 22 additions & 0 deletions crates/ironrdp-client/src/udp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//! Internal client handling for a reliable UDP tunnel.

use ironrdp_rdpeudp_tokio::UdpTransport;
use ironrdp_session::{ActiveStage, SessionError, SessionResult};
use tracing::warn;

/// Drops a failed sideband and withdraws it from future Soft-Sync negotiation.
/// A channel already moved to the tunnel cannot resume over TCP without reconnecting.
/// Both receive closure and auto-detect send failures use this policy.
pub(crate) fn disable_failed_tunnel(
stage: &mut ActiveStage,
transport: &mut Option<UdpTransport>,
error: SessionError,
) -> SessionResult<()> {
if stage.reliable_udp_dvc_tunnel_in_use() {
return Err(error);
}
*transport = None;
stage.disable_reliable_udp_dvc_tunnel()?;
warn!(%error, "Reliable UDP tunnel failed before channel migration; continuing with TCP");
Ok(())
}
64 changes: 64 additions & 0 deletions crates/ironrdp-rdpeudp-tokio/src/autodetect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Auto-detect messages carried in tunnel sub-headers.
//!
//! On a sideband channel in use, the server sends its auto-detect requests in the sub-headers of
//! Tunnel Data PDUs, and the client answers there ([MS-RDPBCGR] 3.2.5.14). Each sub-header is the
//! auto-detect structure itself: SubHeaderLength and SubHeaderType are its headerLength and
//! headerTypeId ([MS-RDPEMT] 2.2.1.1.1).
//!
//! [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/16ffa852-8aa7-481c-99a0-36c1a9a198f6
//! [MS-RDPEMT]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpemt/4f538fd7-3aca-4e7d-a213-13eb5f95c1ad

use ironrdp_core::{Decode, Encode};
use ironrdp_pdu::rdp::autodetect::{AutoDetectRequest, AutoDetectResponse};
use ironrdp_rdpemt::{SubHeaderType, TunnelSubHeader};
use tracing::debug;

use crate::transport::TunnelMessage;

impl TunnelMessage {
/// Decodes the auto-detect requests among this message's sub-headers.
///
/// Sub-headers of other types and requests that do not decode are skipped. RTT requests
/// are returned along with the bandwidth requests MS-RDPEMT lists, because Windows sends
/// them on the tunnel too, although MS-RDPEMT does not specify that encapsulation.
pub fn auto_detect_requests(&self) -> Vec<AutoDetectRequest> {
self.sub_headers
.iter()
.filter(|sub_header| sub_header.sub_header_type == SubHeaderType::AutoDetectRequest)
.filter_map(|sub_header| match redecode(sub_header) {
Ok(request) => Some(request),
Err(error) => {
debug!(%error, data = ?sub_header.data, "Ignoring an undecodable auto-detect request on the tunnel");
None
}
})
.collect()
}

/// Builds a message with no higher-layer data that carries `responses` as sub-headers.
///
/// A response that cannot be encoded is skipped. Returns `None` when nothing is left to
/// send.
pub fn from_auto_detect_responses(responses: &[AutoDetectResponse]) -> Option<Self> {
let sub_headers: Vec<TunnelSubHeader> = responses
.iter()
.filter_map(|response| match redecode(response) {
Ok(sub_header) => Some(sub_header),
Err(error) => {
debug!(%error, ?response, "Could not encode an auto-detect response for the tunnel");
None
}
})
.collect();
(!sub_headers.is_empty()).then_some(Self {
sub_headers,
data: Vec::new(),
})
}
}

/// Re-reads one wire structure as another with the same bytes.
fn redecode<T: for<'de> Decode<'de>>(value: &dyn Encode) -> Result<T, String> {
let bytes = ironrdp_core::encode_vec(value).map_err(|error| error.to_string())?;
ironrdp_core::decode(&bytes).map_err(|error| error.to_string())
}
10 changes: 7 additions & 3 deletions crates/ironrdp-rdpeudp-tokio/src/framed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ mod tests {
use tokio::sync::mpsc;

use super::*;
use crate::transport::TunnelMessage;
use crate::transport::{ReceivedMessage, TunnelMessage};

/// Build a `UdpTransport` backed by test channels (no real network).
fn test_transport() -> (UdpTransport, mpsc::Sender<TunnelMessage>, mpsc::Receiver<TunnelMessage>) {
let (incoming_tx, incoming_rx) = mpsc::channel::<TunnelMessage>(16);
fn test_transport() -> (
UdpTransport,
mpsc::Sender<ReceivedMessage>,
mpsc::Receiver<TunnelMessage>,
) {
let (incoming_tx, incoming_rx) = mpsc::channel::<ReceivedMessage>(16);
let (outgoing_tx, outgoing_rx) = mpsc::channel::<TunnelMessage>(16);

let transport = UdpTransport::from_channels(incoming_rx, outgoing_tx);
Expand Down
1 change: 1 addition & 0 deletions crates/ironrdp-rdpeudp-tokio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod framed;
pub mod multitransport;
pub mod transport;

pub(crate) mod autodetect;
pub(crate) mod clock;
pub(crate) mod driver;
pub(crate) mod stream;
Expand Down
Loading
Loading