Skip to content

Commit 642d39b

Browse files
author
AKolenda
committed
fix(client): answer auto-detect requests on the UDP tunnel
The client dropped the sub-headers of every Tunnel Data PDU, so the auto-detect requests Windows sends on the UDP tunnel once it is established (MS-RDPEMT 2.2.1.1.1, MS-RDPBCGR 2.2.14) were never answered. The client now reads the tunnel with `recv_message` and hands those requests to the session, which answers them with the same code as the message channel: - The request handling moves out of the x224 processor into an `AutoDetectResponder`. The message channel and the tunnel each have one, because a continuous measurement counts the data of its own transport. - `ActiveStage::process_tunnel_auto_detect` answers the requests of one Tunnel Data PDU and counts its higher-layer data. MS-RDPBCGR 3.2.5.14 counts only the data after the tunnel PDU header, and that data follows the sub-headers, so the data of the PDU carrying a Start is counted and the data of the one carrying a Stop is not. - The client times the tunnel against its own monotonic clock and sends each set of responses back with `send_message`, in a Tunnel Data PDU with no data. A tunnel sub-header is the auto-detect structure itself: its SubHeaderLength and SubHeaderType are the structure's headerLength and headerTypeId. Requests are decoded from the whole sub-header, and responses are encoded the same way.
1 parent 4172582 commit 642d39b

4 files changed

Lines changed: 247 additions & 14 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/ironrdp-client/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ webauthn = ["dep:ironrdp-rdpewa", "dep:ironrdp-rdpewa-native", "dvc-com-plugin"]
5050
vmconnect = ["dep:ironrdp-vmconnect"]
5151
location = ["dep:ironrdp-rdpel"]
5252
udp = [
53+
"dep:ironrdp-rdpemt",
5354
"dep:ironrdp-rdpeudp",
5455
"dep:ironrdp-rdpeudp-tokio",
5556
"ironrdp-rdpeudp-tokio/rustls-aws-lc-rs",
@@ -82,6 +83,7 @@ ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" }
8283
ironrdp-echo = { path = "../ironrdp-echo", version = "0.4" }
8384
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.3" }
8485
ironrdp-rdpei = { path = "../ironrdp-rdpei", version = "0.1" }
86+
ironrdp-rdpemt = { path = "../ironrdp-rdpemt", version = "0.1", optional = true }
8587
ironrdp-rdpeudp = { path = "../ironrdp-rdpeudp", version = "0.1", optional = true }
8688
ironrdp-rdpeudp-tokio = { path = "../ironrdp-rdpeudp-tokio", version = "0.1", optional = true }
8789
ironrdp-tls = { path = "../ironrdp-tls", version = "0.2" } # public

‎crates/ironrdp-client/src/rdp.rs‎

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::io;
99
use std::sync::Arc;
1010
#[cfg(feature = "location")]
1111
use std::sync::mpsc as std_mpsc;
12-
#[cfg(feature = "location")]
12+
#[cfg(any(feature = "location", feature = "udp"))]
1313
use std::time::Instant;
1414

1515
#[cfg(feature = "clipboard")]
@@ -39,6 +39,7 @@ use ironrdp_pdu::input::mouse::PointerFlags;
3939
all(windows, feature = "webauthn")
4040
))]
4141
use ironrdp_pdu::pdu_other_err;
42+
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
4243
use ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu;
4344
use ironrdp_pdu::rdp::session_info::ServerAutoReconnect;
4445
#[cfg(feature = "rdpdr")]
@@ -2855,6 +2856,56 @@ fn build_ordinary_rdcleanpath_request(
28552856

28562857
// ── Active session ────────────────────────────────────────────────────────────
28572858

2859+
/// Decodes the auto-detect requests among the sub-headers of a Tunnel Data PDU.
2860+
///
2861+
/// Such a sub-header is the request structure itself: its SubHeaderLength and SubHeaderType
2862+
/// are the request's headerLength and headerTypeId ([MS-RDPEMT] 2.2.1.1.1), so the request is
2863+
/// decoded from the whole sub-header. Windows also sends request types the decoder does not
2864+
/// model; those are skipped.
2865+
///
2866+
/// [MS-RDPEMT]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpemt/4f538fd7-3aca-4e7d-a213-13eb5f95c1ad
2867+
#[cfg(feature = "udp")]
2868+
fn tunnel_auto_detect_requests(sub_headers: &[ironrdp_rdpemt::TunnelSubHeader]) -> Vec<AutoDetectRequest> {
2869+
sub_headers
2870+
.iter()
2871+
.filter(|sub_header| sub_header.sub_header_type == ironrdp_rdpemt::SubHeaderType::AutoDetectRequest)
2872+
.filter_map(|sub_header| {
2873+
let decoded = ironrdp_core::encode_vec(sub_header)
2874+
.map_err(|error| error.to_string())
2875+
.and_then(|structure| {
2876+
ironrdp_core::decode::<AutoDetectRequest>(&structure).map_err(|error| error.to_string())
2877+
});
2878+
match decoded {
2879+
Ok(request) => Some(request),
2880+
Err(error) => {
2881+
debug!(%error, data = ?sub_header.data, "Ignoring an undecodable auto-detect request on the tunnel");
2882+
None
2883+
}
2884+
}
2885+
})
2886+
.collect()
2887+
}
2888+
2889+
/// Encodes an auto-detect response as the tunnel sub-header it is on the wire (see
2890+
/// [`tunnel_auto_detect_requests`]).
2891+
#[cfg(feature = "udp")]
2892+
fn tunnel_auto_detect_sub_header(
2893+
response: &ironrdp_pdu::rdp::autodetect::AutoDetectResponse,
2894+
) -> Option<ironrdp_rdpemt::TunnelSubHeader> {
2895+
let encoded = ironrdp_core::encode_vec(response)
2896+
.map_err(|error| error.to_string())
2897+
.and_then(|structure| {
2898+
ironrdp_core::decode::<ironrdp_rdpemt::TunnelSubHeader>(&structure).map_err(|error| error.to_string())
2899+
});
2900+
match encoded {
2901+
Ok(sub_header) => Some(sub_header),
2902+
Err(error) => {
2903+
debug!(%error, ?response, "Could not encode an auto-detect response for the tunnel");
2904+
None
2905+
}
2906+
}
2907+
}
2908+
28582909
enum RdpControlFlow {
28592910
ReconnectWithNewSize {
28602911
width: u16,
@@ -3085,6 +3136,9 @@ async fn active_session(
30853136
let mut graceful_shutdown_sent = false;
30863137
let mut post_logon_redraw_requested = false;
30873138
let mut pending_udp_payload: Option<Vec<u8>> = None;
3139+
// Auto-detect on the tunnel is timed against its own monotonic clock.
3140+
#[cfg(feature = "udp")]
3141+
let tunnel_clock = Instant::now();
30883142
let mut initial_outputs = if *graceful_close_receiver.borrow_and_update() {
30893143
graceful_shutdown_sent = true;
30903144
Some(active_stage.graceful_shutdown()?)
@@ -3228,22 +3282,25 @@ async fn active_session(
32283282
}
32293283
ActiveSessionIteration::outputs(outputs)
32303284
}
3231-
udp_payload = async {
3285+
udp_message = async {
32323286
#[cfg(feature = "udp")]
32333287
{
32343288
match (udp_tunnel.transport.as_mut(), pending_udp_payload.is_none()) {
3235-
(Some(transport), true) => transport.recv().await,
3289+
(Some(transport), true) => transport
3290+
.recv_message()
3291+
.await
3292+
.map(|message| (tunnel_auto_detect_requests(&message.sub_headers), message.data)),
32363293
(Some(_), false) | (None, _) => {
3237-
core::future::pending::<Option<Vec<u8>>>().await
3294+
core::future::pending::<Option<(Vec<AutoDetectRequest>, Vec<u8>)>>().await
32383295
}
32393296
}
32403297
}
32413298
#[cfg(not(feature = "udp"))]
32423299
{
3243-
core::future::pending::<Option<Vec<u8>>>().await
3300+
core::future::pending::<Option<(Vec<AutoDetectRequest>, Vec<u8>)>>().await
32443301
}
32453302
} => {
3246-
match udp_payload {
3303+
match udp_message {
32473304
None => {
32483305
if active_stage.reliable_udp_dvc_tunnel_in_use() {
32493306
return Ok(RdpControlFlow::TransportFailure(
@@ -3258,12 +3315,45 @@ async fn active_session(
32583315
warn!("Reliable UDP tunnel closed before Soft-Sync; continuing with TCP");
32593316
ActiveSessionIteration::outputs(Vec::new())
32603317
}
3261-
Some(payload) if payload.is_empty() => {
3262-
trace!("Ignoring reliable UDP tunnel PDU without higher-layer data");
3263-
ActiveSessionIteration::outputs(Vec::new())
3264-
}
3265-
Some(payload) => {
3266-
if active_stage.reliable_udp_dvc_tunnel_in_use() {
3318+
Some((auto_detect_requests, payload)) => {
3319+
#[cfg(feature = "udp")]
3320+
{
3321+
let received_at = ironrdp_core::MonotonicInstant::from_millis(
3322+
u64::try_from(tunnel_clock.elapsed().as_millis()).unwrap_or(u64::MAX),
3323+
);
3324+
let responses = active_stage.process_tunnel_auto_detect(
3325+
auto_detect_requests,
3326+
payload.len(),
3327+
Some(received_at),
3328+
);
3329+
let sub_headers: Vec<_> = responses.iter().filter_map(tunnel_auto_detect_sub_header).collect();
3330+
if let (false, Some(transport)) = (sub_headers.is_empty(), udp_tunnel.transport.as_ref()) {
3331+
let reply = ironrdp_rdpeudp_tokio::TunnelMessage {
3332+
sub_headers,
3333+
data: Vec::new(),
3334+
};
3335+
let Some(result) =
3336+
cancelable_operation(transport.send_message(reply), close_receiver).await
3337+
else {
3338+
return Ok(RdpControlFlow::TerminatedGracefully(
3339+
GracefulDisconnectReason::UserInitiated,
3340+
));
3341+
};
3342+
if let Err(error) = result {
3343+
return Ok(RdpControlFlow::TransportFailure(ironrdp_session::custom_err!(
3344+
"answer reliable UDP tunnel auto-detect",
3345+
error
3346+
)));
3347+
}
3348+
}
3349+
}
3350+
#[cfg(not(feature = "udp"))]
3351+
let _ = auto_detect_requests;
3352+
3353+
if payload.is_empty() {
3354+
trace!("Reliable UDP tunnel PDU without higher-layer data");
3355+
ActiveSessionIteration::outputs(Vec::new())
3356+
} else if active_stage.reliable_udp_dvc_tunnel_in_use() {
32673357
let batch =
32683358
active_stage.process_dvc_tunnel(SoftSyncTunnelType::RELIABLE_UDP, &payload)?;
32693359
ActiveSessionIteration::dvc(batch)
@@ -4386,12 +4476,51 @@ fn process_clipboard_message(
43864476
mod tests {
43874477
use super::*;
43884478

4479+
/// Windows frames an RTT Measure Request on the tunnel as `06 00 <seq> 01 00`: the
4480+
/// sub-header's own two bytes are the request's header.
4481+
#[cfg(feature = "udp")]
4482+
#[test]
4483+
fn tunnel_sub_headers_decode_as_auto_detect_requests() {
4484+
use ironrdp_rdpemt::{SubHeaderType, TunnelSubHeader};
4485+
4486+
let rtt: TunnelSubHeader = ironrdp_core::decode(&[0x06, 0x00, 0x07, 0x00, 0x01, 0x00]).expect("sub-header");
4487+
let unmodeled = TunnelSubHeader {
4488+
sub_header_type: SubHeaderType::AutoDetectRequest,
4489+
data: vec![0x01, 0x00, 0xFF, 0x7F],
4490+
};
4491+
let response = TunnelSubHeader {
4492+
sub_header_type: SubHeaderType::AutoDetectResponse,
4493+
data: vec![0x07, 0x00, 0x00, 0x00],
4494+
};
4495+
4496+
let requests = tunnel_auto_detect_requests(&[rtt, unmodeled, response]);
4497+
assert!(matches!(
4498+
requests.as_slice(),
4499+
[AutoDetectRequest::RttRequest { sequence_number: 7, .. }]
4500+
));
4501+
}
4502+
4503+
/// The RTT response goes back as `06 01 <seq> 00 00`, alone in a Tunnel Data PDU with no data.
4504+
#[cfg(feature = "udp")]
4505+
#[test]
4506+
fn auto_detect_responses_encode_as_tunnel_sub_headers() {
4507+
let response = ironrdp_pdu::rdp::autodetect::AutoDetectResponse::RttResponse { sequence_number: 7 };
4508+
let sub_header = tunnel_auto_detect_sub_header(&response).expect("sub-header");
4509+
let pdu = encode_vec(&ironrdp_rdpemt::TunnelData {
4510+
sub_headers: vec![sub_header],
4511+
higher_layer_data: Vec::new(),
4512+
})
4513+
.expect("encode");
4514+
// TunnelData: action 2, payloadLength 0, headerLength 4 + 6, then the sub-header.
4515+
assert_eq!(pdu, [0x02, 0x00, 0x00, 0x0A, 0x06, 0x01, 0x07, 0x00, 0x00, 0x00]);
4516+
}
4517+
43894518
#[cfg(feature = "rdpdr")]
43904519
use core::any::TypeId;
43914520
#[cfg(feature = "rdpdr")]
43924521
use core::sync::atomic::AtomicUsize;
43934522

4394-
#[cfg(feature = "rdpdr")]
4523+
#[cfg(any(feature = "rdpdr", feature = "udp"))]
43954524
use ironrdp_core::encode_vec;
43964525
use ironrdp_pdu::input::fast_path::KeyboardFlags;
43974526
#[cfg(feature = "rdpdr")]

‎crates/ironrdp-session/src/active_stage.rs‎

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use ironrdp_pdu::fast_path::FastPathHeader;
1111
use ironrdp_pdu::gcc::{ChannelName, Monitor};
1212
use ironrdp_pdu::geometry::{ExclusiveRectangle, InclusiveRectangle, Rectangle as _};
1313
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
14-
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
14+
use ironrdp_pdu::rdp::autodetect::{AutoDetectRequest, AutoDetectResponse};
1515
use ironrdp_pdu::rdp::capability_sets::WindowSupportLevel;
1616
use ironrdp_pdu::rdp::client_info::CompressionType;
1717
use ironrdp_pdu::rdp::headers::ShareDataPdu;
@@ -28,6 +28,7 @@ use ironrdp_rdpei::RdpeiClient;
2828
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages};
2929
use tracing::{debug, warn};
3030

31+
use crate::autodetect::AutoDetectResponder;
3132
use crate::fast_path::UpdateKind;
3233
use crate::image::DecodedImage;
3334
use crate::{SessionError, SessionErrorExt as _, SessionResult, fast_path, x224};
@@ -50,6 +51,8 @@ pub struct ActiveStage {
5051
window_support_level: Option<WindowSupportLevel>,
5152
graphics_output_needs_full_refresh: bool,
5253
damage_regions: Vec<InclusiveRectangle>,
54+
/// Answers the auto-detect requests carried on the reliable UDP tunnel.
55+
tunnel_auto_detect: AutoDetectResponder,
5356
}
5457

5558
/// Builder for [`ActiveStage`].
@@ -108,6 +111,7 @@ impl ActiveStageBuilder {
108111
window_support_level: None,
109112
graphics_output_needs_full_refresh: false,
110113
damage_regions: Vec::new(),
114+
tunnel_auto_detect: AutoDetectResponder::default(),
111115
}
112116
}
113117
}
@@ -575,6 +579,33 @@ impl ActiveStage {
575579
.is_some_and(|drdynvc| drdynvc.has_channels_on_tunnel(SoftSyncTunnelType::RELIABLE_UDP))
576580
}
577581

582+
/// Answers the auto-detect requests the server sends in the sub-headers of one Tunnel Data
583+
/// PDU ([MS-RDPEMT] 2.2.1.1.1), and counts the `data_len` bytes of higher-layer data the PDU
584+
/// carries for an open bandwidth measurement. Returns the responses to send back on the
585+
/// tunnel.
586+
///
587+
/// The tunnel keeps its own measurement, apart from the message channel's, because a
588+
/// measurement on the tunnel counts only the data that follows the tunnel PDU header
589+
/// ([MS-RDPBCGR] 3.2.5.14). That data follows the sub-headers on the wire, so the data of the
590+
/// PDU that carries a Start is counted and the data of the one that carries a Stop is not.
591+
/// `received_at` is the time the PDU arrived, from one monotonic clock for the whole tunnel.
592+
///
593+
/// [MS-RDPEMT]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpemt/4f538fd7-3aca-4e7d-a213-13eb5f95c1ad
594+
/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/16ffa852-8aa7-481c-99a0-36c1a9a198f6
595+
pub fn process_tunnel_auto_detect(
596+
&mut self,
597+
requests: Vec<AutoDetectRequest>,
598+
data_len: usize,
599+
received_at: Option<MonotonicInstant>,
600+
) -> Vec<AutoDetectResponse> {
601+
let responses = requests
602+
.into_iter()
603+
.filter_map(|request| self.tunnel_auto_detect.respond(request, received_at))
604+
.collect();
605+
self.tunnel_auto_detect.record_bytes(data_len);
606+
responses
607+
}
608+
578609
/// Returns the Soft-Sync tunnel selected for client messages on `channel_id`.
579610
pub fn dvc_tunnel_for_channel(&self, channel_id: u32) -> Option<SoftSyncTunnelType> {
580611
self.x224_processor
@@ -1079,6 +1110,76 @@ mod tests {
10791110
use ironrdp_pdu::pointer::{ColorPointerAttribute, Point16, PointerAttribute, PointerUpdateData};
10801111
use ironrdp_rdpei::pdu::{PenEventPdu, RdpInputProtocolVersion, RdpeiPdu, ScReadyPdu, TouchEventPdu};
10811112

1113+
fn tunnel_test_stage() -> ActiveStage {
1114+
ActiveStageBuilder {
1115+
static_channels: StaticChannelSet::new(),
1116+
user_channel_id: 1001,
1117+
io_channel_id: 1003,
1118+
message_channel_id: None,
1119+
share_id: 1,
1120+
compression_type: None,
1121+
enable_server_pointer: true,
1122+
pointer_software_rendering: false,
1123+
}
1124+
.build()
1125+
}
1126+
1127+
#[test]
1128+
fn tunnel_auto_detect_answers_an_rtt_request() {
1129+
let mut stage = tunnel_test_stage();
1130+
let responses = stage.process_tunnel_auto_detect(
1131+
vec![AutoDetectRequest::rtt_continuous(7)],
1132+
10,
1133+
Some(MonotonicInstant::from_millis(1_000)),
1134+
);
1135+
assert_eq!(responses, [AutoDetectResponse::RttResponse { sequence_number: 7 }]);
1136+
}
1137+
1138+
/// The data of the PDU that carries the Start follows it on the wire and is counted; the data
1139+
/// of the one that carries the Stop is not.
1140+
#[test]
1141+
fn tunnel_auto_detect_counts_the_data_between_start_and_stop() {
1142+
let mut stage = tunnel_test_stage();
1143+
let at = |millis| Some(MonotonicInstant::from_millis(millis));
1144+
1145+
assert!(
1146+
stage
1147+
.process_tunnel_auto_detect(vec![AutoDetectRequest::bw_start_continuous(1)], 7, at(1_000))
1148+
.is_empty()
1149+
);
1150+
assert!(stage.process_tunnel_auto_detect(Vec::new(), 100, at(1_010)).is_empty());
1151+
let responses = stage.process_tunnel_auto_detect(vec![AutoDetectRequest::bw_stop_continuous(3)], 50, at(1_040));
1152+
1153+
let [
1154+
AutoDetectResponse::BandwidthMeasureResults {
1155+
sequence_number,
1156+
time_delta_ms,
1157+
byte_count,
1158+
..
1159+
},
1160+
] = responses.as_slice()
1161+
else {
1162+
panic!("expected one bandwidth result, got {responses:?}");
1163+
};
1164+
assert_eq!((*sequence_number, *time_delta_ms, *byte_count), (3, 40, 7 + 100));
1165+
}
1166+
1167+
/// The tunnel's measurement is separate from the message channel's, so data that arrives over
1168+
/// TCP is not counted in it.
1169+
#[test]
1170+
fn tunnel_auto_detect_keeps_its_own_window() {
1171+
let mut stage = tunnel_test_stage();
1172+
let at = |millis| Some(MonotonicInstant::from_millis(millis));
1173+
1174+
stage.process_tunnel_auto_detect(vec![AutoDetectRequest::bw_start_continuous(1)], 0, at(1_000));
1175+
stage.x224_processor.record_bandwidth_bytes(500);
1176+
let responses = stage.process_tunnel_auto_detect(vec![AutoDetectRequest::bw_stop_continuous(2)], 0, at(1_020));
1177+
assert!(matches!(
1178+
responses.as_slice(),
1179+
[AutoDetectResponse::BandwidthMeasureResults { byte_count: 0, .. }]
1180+
));
1181+
}
1182+
10821183
#[test]
10831184
fn full_redraw_prefers_suppress_output_toggle_when_supported() {
10841185
let stage = ActiveStageBuilder {

0 commit comments

Comments
 (0)