diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index a67d49c9af..f105b342e1 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -200,6 +200,20 @@ pub enum RdpOutputEvent { /// A cookie-based reconnect has completed successfully. AutoReconnected, Terminated(SessionResult), + /// The transport carrying the dynamic channels changed. + /// + /// Sent once the session is active, and again whenever a reliable RDP-UDP tunnel is + /// established, Soft-Sync moves the dynamic channels onto it, or the session falls back + /// to TCP. + #[cfg(feature = "udp")] + Transport { + /// The dynamic channels, the graphics pipeline among them, travel over the reliable + /// RDP-UDP tunnel. + reliable_udp: bool, + /// The RDP-UDP version the tunnel's handshake settled on, while a tunnel is open, + /// whether or not Soft-Sync has moved channels onto it yet. + udp_version: Option, + }, } /// A tightly packed changed region from the composited desktop framebuffer. @@ -3085,6 +3099,8 @@ async fn active_session( let mut graceful_shutdown_sent = false; let mut post_logon_redraw_requested = false; let mut pending_udp_payload: Option> = None; + #[cfg(feature = "udp")] + let mut announced_transport = None; let mut initial_outputs = if *graceful_close_receiver.borrow_and_update() { graceful_shutdown_sent = true; Some(active_stage.graceful_shutdown()?) @@ -4241,6 +4257,30 @@ async fn active_session( } } + #[cfg(feature = "udp")] + { + let transport = ( + active_stage.reliable_udp_dvc_tunnel_in_use(), + udp_tunnel + .transport + .as_ref() + .and_then(ironrdp_rdpeudp_tokio::UdpTransport::negotiated_version), + ); + if announced_transport != Some(transport) { + announced_transport = Some(transport); + let (reliable_udp, udp_version) = transport; + let event = RdpOutputEvent::Transport { + reliable_udp, + udp_version, + }; + if !send_active_output_event(output_event_sender, event, close_receiver).await? { + return Ok(RdpControlFlow::TerminatedGracefully( + GracefulDisconnectReason::UserInitiated, + )); + } + } + } + if resize_queue.in_flight.is_none() && let Some(pending) = resize_queue.pending.as_ref() { diff --git a/crates/ironrdp-rdpeudp-tokio/src/driver.rs b/crates/ironrdp-rdpeudp-tokio/src/driver.rs index 25c55900d4..590568c597 100644 --- a/crates/ironrdp-rdpeudp-tokio/src/driver.rs +++ b/crates/ironrdp-rdpeudp-tokio/src/driver.rs @@ -316,6 +316,9 @@ impl Driver { Event::Connected => { if !self.connected_signaled { self.connected_signaled = true; + if let Ok(mut shared) = self.shared.lock() { + shared.negotiated_version = self.conn.negotiated_version(); + } self.connected_notify.notify_one(); } } diff --git a/crates/ironrdp-rdpeudp-tokio/src/stream.rs b/crates/ironrdp-rdpeudp-tokio/src/stream.rs index 384faf44bd..3b7235e5ac 100644 --- a/crates/ironrdp-rdpeudp-tokio/src/stream.rs +++ b/crates/ironrdp-rdpeudp-tokio/src/stream.rs @@ -79,6 +79,10 @@ pub(crate) struct SharedIo { /// Set when the RDPEUDP2 connection has been cleanly shut down. pub(crate) closed: bool, + + /// The RDP-UDP version the handshake settled on, recorded by the driver once the + /// connection is established. + pub(crate) negotiated_version: Option, } impl SharedIo { @@ -93,6 +97,7 @@ impl SharedIo { write_room_waker: None, error: None, closed: false, + negotiated_version: None, } } diff --git a/crates/ironrdp-rdpeudp-tokio/src/transport.rs b/crates/ironrdp-rdpeudp-tokio/src/transport.rs index 3e53395a47..5e98a0bd0c 100644 --- a/crates/ironrdp-rdpeudp-tokio/src/transport.rs +++ b/crates/ironrdp-rdpeudp-tokio/src/transport.rs @@ -323,6 +323,12 @@ impl UdpTransport { } } + /// The RDP-UDP version the handshake settled on: version 1 or 2 for MS-RDPEUDP, + /// version 3 for MS-RDPEUDP2. + pub fn negotiated_version(&self) -> Option { + self.shared.lock().ok().and_then(|shared| shared.negotiated_version) + } + /// Whether the driver task is still running. pub fn is_alive(&self) -> bool { !self.driver_handle.is_finished() diff --git a/crates/ironrdp-rdpeudp/src/connection.rs b/crates/ironrdp-rdpeudp/src/connection.rs index 5051a44e64..a5453b24fc 100644 --- a/crates/ironrdp-rdpeudp/src/connection.rs +++ b/crates/ironrdp-rdpeudp/src/connection.rs @@ -927,6 +927,15 @@ impl RdpeudpConnection { self.params.as_ref().map(|p| p.mtu) } + /// The protocol version the handshake settled on, once it is complete: version 1 or 2 + /// for MS-RDPEUDP, version 3 for MS-RDPEUDP2. + pub fn negotiated_version(&self) -> Option { + self.params.as_ref().map(|params| match params.wire { + WireFormat::V1 { version } => UdpVersion(version), + WireFormat::V2 => UdpVersion::V3, + }) + } + /// Diagnostics for the MS-RDPEUDP version 1/2 data path; `None` on MS-RDPEUDP2. pub fn v1_stats(&self) -> Option { let params = self.params.as_ref()?; diff --git a/crates/ironrdp-testsuite-core/tests/rdpeudp/connection.rs b/crates/ironrdp-testsuite-core/tests/rdpeudp/connection.rs index c100c37c4f..a73b1d06d8 100644 --- a/crates/ironrdp-testsuite-core/tests/rdpeudp/connection.rs +++ b/crates/ironrdp-testsuite-core/tests/rdpeudp/connection.rs @@ -130,6 +130,7 @@ fn full_handshake_client_server() { .expect("handle SYN+ACK"); assert!(client.is_established()); + assert_eq!(client.negotiated_version(), Some(UdpVersion::V3)); // Client should emit Connected event let event = client.poll_event().expect("should have event"); @@ -1221,11 +1222,13 @@ fn a_client_follows_a_syn_ack_that_settles_on_version_2() { let mut client = RdpeudpConnection::connect(default_config(100), t).expect("connect"); client.poll_transmit(t).expect("SYN"); + assert_eq!(client.negotiated_version(), None); let mut bytes = version_2_syn_ack(); client .handle_datagram(&mut bytes, later(t, 50)) .expect("version 2 is a version both endpoints support"); assert!(client.is_established()); + assert_eq!(client.negotiated_version(), Some(UdpVersion::V2)); // The final handshake ACK acknowledges the SYN+ACK in MS-RDPEUDP framing. let ack = client.poll_transmit(later(t, 50)).expect("final ACK"); diff --git a/crates/ironrdp-viewer/src/app.rs b/crates/ironrdp-viewer/src/app.rs index 9a18f23ced..0d8acaa0ad 100644 --- a/crates/ironrdp-viewer/src/app.rs +++ b/crates/ironrdp-viewer/src/app.rs @@ -620,6 +620,12 @@ impl RpcApp { debug!(?control, "RAIL control received"); } RdpOutputEvent::WindowingOrders(_) => {} + RdpOutputEvent::Transport { + reliable_udp, + udp_version, + } => { + info!(reliable_udp, ?udp_version, "Session transport"); + } // Only produced when the client is built with `.with_desktop_updates()`, which the // viewer does not opt into: it always presents full-frame `Image` snapshots instead. RdpOutputEvent::DesktopUpdate(_) => {}