diff --git a/Cargo.lock b/Cargo.lock index 9cccb13816..538aed934a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2600,6 +2600,7 @@ dependencies = [ "ironrdp-core 0.2.1", "ironrdp-pdu", "ironrdp-svc", + "rand 0.9.4", "tracing", ] diff --git a/crates/ironrdp-acceptor/Cargo.toml b/crates/ironrdp-acceptor/Cargo.toml index 71968d35a4..6f08e9661e 100644 --- a/crates/ironrdp-acceptor/Cargo.toml +++ b/crates/ironrdp-acceptor/Cargo.toml @@ -22,6 +22,7 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9" } # public ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public ironrdp-connector = { path = "../ironrdp-connector", version = "0.10" } # public ironrdp-async = { path = "../ironrdp-async", version = "0.10" } # public +rand = "0.9" tracing = { version = "0.1", features = ["log"] } [lints] diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs index 182595193e..d3f090ca25 100644 --- a/crates/ironrdp-acceptor/src/connection.rs +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -5,7 +5,7 @@ use ironrdp_connector::{ ConnectorError, ConnectorErrorExt as _, ConnectorResult, DesktopSize, MonotonicInstant, Sequence, State, Written, encode_x224_packet, general_err, reason_err, }; -use ironrdp_core::{WriteBuf, decode}; +use ironrdp_core::{ReadCursor, WriteBuf, decode, decode_cursor}; use ironrdp_pdu as pdu; use ironrdp_pdu::nego::SecurityProtocol; use ironrdp_pdu::x224::X224; @@ -16,6 +16,7 @@ use pdu::rdp::headers::ShareControlPdu; use pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; use pdu::rdp::server_license::{LicensePdu, LicensingErrorMessage}; use pdu::{gcc, mcs, nego, rdp}; +use rand::RngCore as _; use tracing::{debug, warn}; use super::channel_connection::ChannelConnectionSequence; @@ -35,7 +36,12 @@ pub struct Acceptor { keyboard_layout: u32, keyboard_type: gcc::KeyboardType, ime_file_name: String, - multitransport_flags: gcc::MultiTransportFlags, + /// The client's MultiTransportChannelData block flags (MS-RDPBCGR + /// 2.2.1.3.8), when it sent one. `None` when the client did not send the + /// block at all, distinct from `Some(empty())` (block present, no flags + /// set): the server's own block MUST be omitted in the former case but + /// not the latter (2.2.1.4). + multitransport_flags: Option, early_capability_flags: gcc::ClientEarlyCapabilityFlags, server_capabilities: Vec, static_channels: StaticChannelSet, @@ -45,6 +51,53 @@ pub struct Acceptor { received_auto_reconnect: Option, reactivation: bool, honor_client_desktop_size: Option, + /// UDP multitransport flags to advertise to the client and, if it + /// reciprocates, to offer. `None` disables the feature entirely: no + /// Server MultiTransportChannelData block is sent, and no Initiate + /// Multitransport Request follows. See `set_multitransport_offer()`. + offer_multitransport: Option, + /// The flags actually written into the Server MultiTransportChannelData + /// block during Basic Settings Exchange, or `None` if no block was sent. + /// Everything after that exchange decides from this snapshot rather than + /// from `offer_multitransport`, so a later `set_multitransport_offer()` + /// call cannot send a request, or report a Soft-Sync result, that the + /// client was never told about. + advertised_multitransport: Option, + /// The Initiate Multitransport Request sent to the client, once + /// `MultitransportBootstrapping` has run. See `multitransport_request()`. + sent_multitransport_request: Option, + /// Source of randomness for the Initiate Multitransport Request's security + /// cookie and request ID. See `set_multitransport_security_rng()`. + multitransport_security_rng: Box, +} + +/// Source of randomness for the security cookie and request ID the acceptor +/// sends in an Initiate Multitransport Request PDU (MS-RDPBCGR 2.2.15.1). +/// +/// [`Acceptor::step`] is otherwise a pure function of its inputs and stored +/// state, which is what makes the sans-I/O sequence deterministic and +/// testable by feeding it bytes; reading a global RNG from inside `step` +/// would be a hidden side channel breaking that. Injected instead via +/// [`Acceptor::set_multitransport_security_rng`], defaulting to an OS-backed +/// implementation. +pub trait MultitransportSecurityRng: Send { + /// Fill `cookie` with random bytes for the security cookie field. + fn fill_security_cookie(&mut self, cookie: &mut [u8; 16]); + /// Produce the request ID. + fn next_request_id(&mut self) -> u32; +} + +/// Default [`MultitransportSecurityRng`], backed by the OS RNG via `rand::rng()`. +struct OsMultitransportSecurityRng; + +impl MultitransportSecurityRng for OsMultitransportSecurityRng { + fn fill_security_cookie(&mut self, cookie: &mut [u8; 16]) { + rand::rng().fill_bytes(cookie); + } + + fn next_request_id(&mut self) -> u32 { + rand::rng().next_u32() + } } /// Minimum and maximum desktop dimension honored from a client. @@ -163,7 +216,7 @@ impl Acceptor { keyboard_layout: 0, keyboard_type: gcc::KeyboardType(0), ime_file_name: String::new(), - multitransport_flags: gcc::MultiTransportFlags::empty(), + multitransport_flags: None, early_capability_flags: gcc::ClientEarlyCapabilityFlags::empty(), server_capabilities: capabilities, static_channels: StaticChannelSet::new(), @@ -173,9 +226,20 @@ impl Acceptor { received_auto_reconnect: None, reactivation: false, honor_client_desktop_size: None, + offer_multitransport: None, + advertised_multitransport: None, + sent_multitransport_request: None, + multitransport_security_rng: Box::new(OsMultitransportSecurityRng), } } + /// Overrides the source of randomness used for the security cookie and + /// request ID in an Initiate Multitransport Request PDU. Defaults to an + /// OS-backed RNG; intended for tests that need deterministic output. + pub fn set_multitransport_security_rng(&mut self, rng: Box) { + self.multitransport_security_rng = rng; + } + /// Adopt the desktop size requested by the client in its Client Core Data /// instead of the size this acceptor was constructed with, clamped to an /// operator-configured maximum. @@ -220,6 +284,137 @@ impl Acceptor { self.honor_client_desktop_size = max; } + /// Advertise UDP multitransport support (MS-RDPBCGR 2.2.1.4.6) and offer + /// it to clients that reciprocate. + /// + /// Pass `Some(flags)` to send a Server MultiTransportChannelData block + /// with these flags during Basic Settings Exchange, and, once licensing + /// completes, an Initiate Multitransport Request for reliable UDP + /// (`TRANSPORT_TYPE_UDP_FECR`) if `flags` includes it and the client's + /// own Client MultiTransportChannelData reciprocated. Lossy UDP + /// (`TRANSPORT_TYPE_UDP_FECL`) is accepted in `flags` for advertisement + /// purposes but this acceptor never requests it; only the reliable + /// transport is implemented. Include `SOFT_SYNC_TCP_TO_UDP` to also + /// support switching dynamic virtual channels from TCP to UDP after the + /// sideband transport is up; see + /// [`multitransport_soft_sync_negotiated()`](Self::multitransport_soft_sync_negotiated). + /// + /// Offering requires the client to have requested an MCS message + /// channel (MS-RDPBCGR 2.2.1.3.7): both the request and, when owed, the + /// client's response travel on it. If the client never requests one, no + /// request is sent regardless of this setting. + /// + /// `None` is the default: no multitransport block is advertised and no + /// request is ever sent. + /// + /// Only a call made before Basic Settings Exchange takes effect. The + /// flags advertised there are what the later request and + /// `multitransport_soft_sync_negotiated()` are based on; changing the + /// offer afterwards does not alter a negotiation already under way. + /// + /// This acceptor only bootstraps and sends the request; it does not + /// itself establish the RDPEUDP2 sideband transport the request + /// promises. Enabling this without a caller that drives that + /// establishment (over `multitransport_request()`) makes every + /// reciprocating client attempt a UDP connection that cannot succeed. + pub fn set_multitransport_offer(&mut self, flags: Option) { + self.offer_multitransport = flags; + } + + /// Returns the Initiate Multitransport Request sent to the client, if + /// [`MultitransportBootstrapping`](AcceptorState::MultitransportBootstrapping) + /// has run and decided to offer UDP multitransport. + /// + /// The caller should treat a `Some` here as the signal to begin + /// establishing the sideband UDP transport (RDPEUDP2 + TLS + RDPEMT) + /// using `request_id` and `security_cookie`, in parallel with (not + /// blocking) the rest of the acceptor sequence: this acceptor does not + /// wait for the client's Initiate Multitransport Response before + /// continuing on to capability negotiation, since MS-RDPBCGR 3.2.5.15.1 + /// only obliges the client to send one when Soft-Sync is negotiated or + /// the attempt failed, never on a plain successful bootstrap. + /// + /// `None` before `MultitransportBootstrapping` has run, or when + /// multitransport was not offered + /// ([`set_multitransport_offer()`](Self::set_multitransport_offer) + /// disabled or the client did not reciprocate). Bootstrapping does not + /// run again on reactivation, so no new request is sent then, but a + /// request from before reactivation carries forward and is still + /// returned here: `CapabilitiesWaitConfirm`'s late-response tolerance + /// needs it to remain visible across reactivation too. + pub fn multitransport_request(&self) -> Option<&rdp::multitransport::MultitransportRequestPdu> { + self.sent_multitransport_request.as_ref() + } + + /// Whether both peers advertised Soft-Sync support for multitransport. + /// + /// `None` before [`multitransport_request()`](Self::multitransport_request) + /// returns `Some`: no request was sent, so nothing was actually + /// negotiated regardless of what the GCC flags alone would suggest. + pub fn multitransport_soft_sync_negotiated(&self) -> Option { + self.sent_multitransport_request.as_ref()?; + Some( + self.advertised_multitransport + .is_some_and(|offer| offer.contains(gcc::MultiTransportFlags::SOFT_SYNC_TCP_TO_UDP)) + && self + .multitransport_flags + .is_some_and(|flags| flags.contains(gcc::MultiTransportFlags::SOFT_SYNC_TCP_TO_UDP)), + ) + } + + /// If `data` (an MCS SendDataRequest already decoded from the wire) is on + /// the message channel while a multitransport request is outstanding AND + /// its payload strictly decodes as an Initiate Multitransport Response, + /// logs it against the outstanding request (matching request IDs) and + /// returns it. MS-RDPBCGR 3.2.5.15.1 gives this response no fixed + /// position relative to the rest of the handshake: it depends on when + /// the client resolves its own bootstrapping and whether the sideband + /// attempt failed, so both `CapabilitiesWaitConfirm` and + /// `ConnectionFinalization` tolerate it landing wherever it actually + /// shows up rather than only where `MultitransportBootstrapping`'s own + /// comment describes as typical. + /// + /// The message channel also carries Auto-Detect Response and Heartbeat + /// PDUs (2.2.1.4.5, 2.2.8.1.1.2.1), so a channel-and-outstanding-request + /// check alone would misclassify that traffic too; requiring the decode + /// to actually succeed here lets callers fall through to their own + /// handling for anything that isn't really a response, mirroring how + /// `ClientConnectorState::ConnectTimeAutoDetection` demuxes the same + /// channel client-side. + fn late_multitransport_response( + &self, + data: &mcs::SendDataRequest<'_>, + ) -> Option { + let sent = self.sent_multitransport_request.as_ref()?; + if Some(data.channel_id) != self.message_channel_id { + return None; + } + // `decode` alone would accept a valid response followed by trailing + // bytes. This acceptor advertises ENCRYPTION_LEVEL_NONE, so the + // response carries the 4-byte Basic Security Header (MS-RDPBCGR + // 2.2.15.2) and is exactly 12 bytes: anything left over means the + // payload is something else. + let mut cursor = ReadCursor::new(data.user_data.as_ref()); + let response = decode_cursor::(&mut cursor).ok()?; + if !cursor.is_empty() { + return None; + } + if response.request_id == sent.request_id { + debug!( + request_id = response.request_id, + success = response.is_success(), + "Received Initiate Multitransport Response" + ); + } else { + warn!( + response.request_id, + expected_request_id = sent.request_id, + "Initiate Multitransport Response request ID does not match the sent request" + ); + } + Some(response) + } + pub fn new_deactivation_reactivation( mut consumed: Acceptor, static_channels: StaticChannelSet, @@ -262,6 +457,10 @@ impl Acceptor { received_auto_reconnect: consumed.received_auto_reconnect, reactivation: true, honor_client_desktop_size: consumed.honor_client_desktop_size, + offer_multitransport: consumed.offer_multitransport, + advertised_multitransport: consumed.advertised_multitransport, + sent_multitransport_request: consumed.sent_multitransport_request, + multitransport_security_rng: consumed.multitransport_security_rng, }) } @@ -352,7 +551,9 @@ impl Acceptor { keyboard_layout: self.keyboard_layout, keyboard_type: self.keyboard_type, ime_file_name: self.ime_file_name.clone(), - multitransport_flags: self.multitransport_flags, + multitransport_flags: self + .multitransport_flags + .unwrap_or_else(gcc::MultiTransportFlags::empty), client_early_capability_flags: self.early_capability_flags, reactivation: self.reactivation, credentials: self.received_credentials.take(), @@ -367,6 +568,7 @@ impl Acceptor { } #[derive(Default, Debug)] +#[non_exhaustive] pub enum AcceptorState { #[default] Consumed, @@ -413,6 +615,40 @@ pub enum AcceptorState { early_capability: Option, channels: Vec<(u16, gcc::ChannelDef)>, }, + /// After licensing, decide whether to offer UDP multitransport + /// (MS-RDPBCGR 2.2.15.1) and, if so, send the Initiate Multitransport + /// Request. + /// + /// Unlike the client's `MultitransportBootstrapping`, which is purely + /// reactive (it waits to read whatever the server sends), this state is + /// where the server actively decides and writes: it is entered with + /// nothing to read, decides based on the client's advertised + /// `multitransport_flags` and the offer the server itself advertised + /// during Basic Settings Exchange, and either sends the request or skips + /// it, either way moving straight on to `CapabilitiesSendServer` in the + /// same step. + /// + /// There is deliberately no state mirroring the client's + /// `MultitransportPending`: MS-RDPBCGR 3.2.5.15.1 only obliges the client + /// to send an Initiate Multitransport Response when Soft-Sync is + /// negotiated or the sideband attempt failed, so on the common + /// successful, non-Soft-Sync path no response is ever sent. Blocking + /// here to read one would stall the handshake forever in exactly that + /// case. Establishing the actual UDP transport (RDPEUDP2 + TLS + RDPEMT) + /// is the caller's responsibility, driven out of band from this request: + /// see [`Acceptor::multitransport_request()`]. Because the client sends + /// its response, if any, before it ever reads the server's Demand + /// Active, IronRDP's own client always sends one (if at all) before the + /// Confirm Active on the wire. That is a client behavior, not a + /// protocol guarantee 3.2.5.15.1 makes: a conforming third-party client + /// could just as legitimately send it later, during finalization. Both + /// `CapabilitiesWaitConfirm` and `ConnectionFinalization` tolerate and + /// consume it wherever it actually lands, rather than this state + /// waiting for it. + MultitransportBootstrapping { + early_capability: Option, + channels: Vec<(u16, gcc::ChannelDef)>, + }, CapabilitiesSendServer { early_capability: Option, channels: Vec<(u16, gcc::ChannelDef)>, @@ -449,6 +685,7 @@ impl State for AcceptorState { Self::RdpSecurityCommencement { .. } => "RdpSecurityCommencement", Self::SecureSettingsExchange { .. } => "SecureSettingsExchange", Self::LicensingExchange { .. } => "LicensingExchange", + Self::MultitransportBootstrapping { .. } => "MultitransportBootstrapping", Self::CapabilitiesSendServer { .. } => "CapabilitiesSendServer", Self::MonitorLayoutSend { .. } => "MonitorLayoutSend", Self::CapabilitiesWaitConfirm { .. } => "CapabilitiesWaitConfirm", @@ -480,6 +717,9 @@ impl Sequence for Acceptor { AcceptorState::RdpSecurityCommencement { .. } => None, AcceptorState::SecureSettingsExchange { .. } => Some(&pdu::X224_HINT), AcceptorState::LicensingExchange { .. } => None, + // Nothing to read: this state decides whether to send a request, + // then moves straight on to CapabilitiesSendServer. + AcceptorState::MultitransportBootstrapping { .. } => None, AcceptorState::CapabilitiesSendServer { .. } => None, AcceptorState::MonitorLayoutSend { .. } => None, AcceptorState::CapabilitiesWaitConfirm { .. } => Some(&pdu::X224_HINT), @@ -621,11 +861,7 @@ impl Sequence for Acceptor { self.keyboard_layout = gcc_blocks.core.keyboard_layout; self.keyboard_type = gcc_blocks.core.keyboard_type; self.ime_file_name.clone_from(&gcc_blocks.core.ime_file_name); - self.multitransport_flags = gcc_blocks - .multi_transport_channel - .as_ref() - .map(|m| m.flags) - .unwrap_or_else(gcc::MultiTransportFlags::empty); + self.multitransport_flags = gcc_blocks.multi_transport_channel.as_ref().map(|m| m.flags); // Adopt the client's requested desktop size (from its Client // Core Data) before Demand Active is sent, so the session is @@ -723,12 +959,18 @@ impl Sequence for Acceptor { let skip_channel_join = early_capability .is_some_and(|client| client.contains(gcc::ClientEarlyCapabilityFlags::SUPPORT_SKIP_CHANNELJOIN)); + self.advertised_multitransport = self + .message_channel_id + .and(self.offer_multitransport) + .filter(|_| self.multitransport_flags.is_some()); + let server_blocks = create_gcc_blocks( self.io_channel_id, channel_ids.clone(), requested_protocol, skip_channel_join, self.message_channel_id, + self.advertised_multitransport, ); let settings_response = mcs::ConnectResponse { @@ -866,6 +1108,10 @@ impl Sequence for Acceptor { let written = util::encode_send_data_indication(self.user_channel_id, self.io_channel_id, &license, output)?; + // Reactivation (Deactivation-Reactivation Sequence, e.g. a + // display resize) re-enters capability negotiation directly: + // the sideband UDP transport, if any, was already bootstrapped + // once for this connection and is not torn down or re-offered. self.saved_for_reactivation = AcceptorState::CapabilitiesSendServer { early_capability, channels: channels.clone(), @@ -873,13 +1119,70 @@ impl Sequence for Acceptor { ( Written::from_size(written)?, - AcceptorState::CapabilitiesSendServer { + AcceptorState::MultitransportBootstrapping { early_capability, channels, }, ) } + AcceptorState::MultitransportBootstrapping { + early_capability, + channels, + } => { + let next_state = AcceptorState::CapabilitiesSendServer { + early_capability, + channels, + }; + + let offer_udp_fecr = self + .advertised_multitransport + .is_some_and(|offer| offer.contains(gcc::MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let client_supports_udp_fecr = self + .multitransport_flags + .is_some_and(|flags| flags.contains(gcc::MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + // 2.2.15.1 requires the request to travel on the MCS message + // channel. A client can in principle advertise UDP support + // without also requesting a message channel; rather than + // failing the whole connection over a mismatch in an optional + // feature's negotiation, that is treated the same as not + // offering. + let message_channel_id = self + .message_channel_id + .filter(|_| offer_udp_fecr && client_supports_udp_fecr); + + if let Some(message_channel_id) = message_channel_id { + let mut security_cookie = [0u8; 16]; + self.multitransport_security_rng + .fill_security_cookie(&mut security_cookie); + let request_id = self.multitransport_security_rng.next_request_id(); + + let request = rdp::multitransport::MultitransportRequestPdu { + security_header: rdp::headers::BasicSecurityHeader { + flags: rdp::headers::BasicSecurityHeaderFlags::TRANSPORT_REQ, + }, + request_id, + requested_protocol: rdp::multitransport::RequestedProtocol::UdpFecR, + security_cookie, + }; + + debug!(message = ?request, "Send"); + + let written = + util::encode_send_data_indication(self.user_channel_id, message_channel_id, &request, output)?; + + self.sent_multitransport_request = Some(request); + + (Written::from_size(written)?, next_state) + } else { + debug!( + offer_udp_fecr, + client_supports_udp_fecr, "Not offering UDP multitransport" + ); + (Written::Nothing, next_state) + } + } + AcceptorState::CapabilitiesSendServer { early_capability, channels, @@ -957,6 +1260,25 @@ impl Sequence for Acceptor { }; match message { mcs::McsMessage::SendDataRequest(data) => { + // An Initiate Multitransport Response can legitimately land + // here: it travels on the message channel, and the client + // sends it (when it sends one at all) while resolving its + // own multitransport bootstrapping, strictly before it + // ever reads the Demand Active that leads to Confirm + // Active. So it is checked for by channel and a + // successful strict decode before assuming the payload is + // a Confirm Active, and simply logged and dropped: this + // acceptor does not gate on it, per the note on + // `AcceptorState::MultitransportBootstrapping`. A decode + // failure here means the message-channel traffic isn't a + // response at all (Auto-Detect Response, Heartbeat), so it + // falls through to the Confirm Active handling below + // instead. + if self.late_multitransport_response(&data).is_some() { + self.state = prev_state; + return Ok(Written::Nothing); + } + let capabilities_confirm = decode::(data.user_data.as_ref()) .map_err(ConnectorError::decode); let capabilities_confirm = match capabilities_confirm { @@ -1006,23 +1328,50 @@ impl Sequence for Acceptor { channels, client_capabilities, } => { - let written = finalization.step(input, received_at, output)?; - - let state = if finalization.is_done() { - AcceptorState::Accepted { - channels, - client_capabilities, - input_events: finalization.into_input_events(), - } - } else { - AcceptorState::ConnectionFinalization { - finalization, - channels, - client_capabilities, + // A late Initiate Multitransport Response can land in any + // finalization sub-state (see `late_multitransport_response`); + // none of FinalizationSequence's own PDU decoders expect it, and + // depending which sub-state is active it would otherwise be + // silently swallowed while advancing a state, propagated as a + // connection-ending decode error, or surfaced to the embedding + // application as a raw input event. Check for it here, before + // finalization ever sees the bytes, mirroring + // `CapabilitiesWaitConfirm`'s handling. + let is_late_multitransport_response = match decode::>>(input) { + Ok(X224(mcs::McsMessage::SendDataRequest(data))) => { + self.late_multitransport_response(&data).is_some() } + _ => false, }; - (written, state) + if is_late_multitransport_response { + ( + Written::Nothing, + AcceptorState::ConnectionFinalization { + finalization, + channels, + client_capabilities, + }, + ) + } else { + let written = finalization.step(input, received_at, output)?; + + let state = if finalization.is_done() { + AcceptorState::Accepted { + channels, + client_capabilities, + input_events: finalization.into_input_events(), + } + } else { + AcceptorState::ConnectionFinalization { + finalization, + channels, + client_capabilities, + } + }; + + (written, state) + } } _ => unreachable!(), @@ -1039,6 +1388,7 @@ fn create_gcc_blocks( requested: SecurityProtocol, skip_channel_join: bool, message_channel_id: Option, + offer_multitransport: Option, ) -> gcc::ServerGccBlocks { gcc::ServerGccBlocks { core: gcc::ServerCoreData { @@ -1057,6 +1407,13 @@ fn create_gcc_blocks( message_channel: message_channel_id.map(|id| gcc::ServerMessageChannelData { mcs_message_channel_id: id, }), - multi_transport_channel: None, + // Only meaningful alongside a message channel: the request and any + // response it draws both travel there (MS-RDPBCGR 2.2.15.1, 2.2.15.2). + // The caller has already filtered the offer to None when + // the client did not populate its own MultiTransportChannelData + // block, per 2.2.1.4's requirement that this block be omitted then. + multi_transport_channel: message_channel_id + .and(offer_multitransport) + .map(|flags| gcc::MultiTransportChannelData { flags }), } } diff --git a/crates/ironrdp-acceptor/src/lib.rs b/crates/ironrdp-acceptor/src/lib.rs index a8a709687e..e7f3636cff 100644 --- a/crates/ironrdp-acceptor/src/lib.rs +++ b/crates/ironrdp-acceptor/src/lib.rs @@ -18,7 +18,7 @@ pub use ironrdp_connector::DesktopSize; use ironrdp_pdu::nego; pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState}; -pub use self::connection::{Acceptor, AcceptorResult, AcceptorState}; +pub use self::connection::{Acceptor, AcceptorResult, AcceptorState, MultitransportSecurityRng}; pub use self::finalization::{FinalizationSequence, FinalizationState}; use crate::credssp::resolve_generator; diff --git a/crates/ironrdp-testsuite-core/tests/server/acceptor.rs b/crates/ironrdp-testsuite-core/tests/server/acceptor.rs index ed95dd60f7..4d1382d3dd 100644 --- a/crates/ironrdp-testsuite-core/tests/server/acceptor.rs +++ b/crates/ironrdp-testsuite-core/tests/server/acceptor.rs @@ -1,11 +1,21 @@ -use ironrdp_acceptor::Acceptor; +use std::borrow::Cow; + +use ironrdp_acceptor::{Acceptor, MultitransportSecurityRng}; use ironrdp_connector::{DesktopSize, Sequence as _, Written, encode_x224_packet}; -use ironrdp_core::{WriteBuf, decode}; -use ironrdp_pdu::gcc::ClientMessageChannelData; +use ironrdp_core::{WriteBuf, decode, encode_vec}; +use ironrdp_pdu::gcc::{ClientMessageChannelData, MultiTransportChannelData, MultiTransportFlags}; use ironrdp_pdu::mcs::{self, ConnectInitial}; use ironrdp_pdu::nego::{self, SecurityProtocol}; +use ironrdp_pdu::rdp::client_info::CompressionType; +use ironrdp_pdu::rdp::finalization_messages::{ControlAction, ControlPdu, SynchronizePdu}; +use ironrdp_pdu::rdp::headers::{ + BasicSecurityHeaderFlags, CompressionFlags, ShareControlHeader, ShareControlPdu, ShareDataHeader, ShareDataPdu, + StreamPriority, +}; +use ironrdp_pdu::rdp::multitransport::{MultitransportRequestPdu, MultitransportResponsePdu, RequestedProtocol}; use ironrdp_pdu::x224::{X224, X224Data}; use ironrdp_testsuite_core::gcc::CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS; +use ironrdp_testsuite_core::rdp::{CLIENT_DEMAND_ACTIVE_PDU_BUFFER, CLIENT_INFO_PDU_BUFFER}; /// Build a minimal ConnectionRequest with the given protocols and encode it. fn encode_connection_request(protocol: SecurityProtocol) -> Vec { @@ -188,3 +198,523 @@ fn neg_failure_hybrid_required() { } } } + +fn encode_send_data_request(initiator_id: u16, channel_id: u16, user_data: &[u8]) -> Vec { + let mut buf = WriteBuf::new(); + ironrdp_core::encode_buf( + &X224(mcs::SendDataRequest { + initiator_id, + channel_id, + user_data: Cow::Borrowed(user_data), + }), + &mut buf, + ) + .unwrap(); + buf.filled().to_vec() +} + +/// Encode a client finalization ShareData PDU (Synchronize, Control, FontList), +/// mirroring the shape `ironrdp_acceptor`'s own `wrap_share_data` uses for the +/// server's confirmations of the same messages. +fn encode_client_share_data(pdu: ShareDataPdu) -> Vec { + let header = ShareControlHeader { + share_id: 0, + pdu_source: 0, + share_control_pdu: ShareControlPdu::Data(ShareDataHeader { + share_data_pdu: pdu, + stream_priority: StreamPriority::Undefined, + compression_flags: CompressionFlags::empty(), + compression_type: CompressionType::K8, + }), + }; + encode_vec(&header).unwrap() +} + +/// Drives `acceptor` from a fresh `InitiationWaitRequest` through +/// `SecureSettingsExchange`, i.e. everything that precedes licensing and is +/// identical regardless of what the multitransport tests below want to +/// exercise: negotiation, TLS upgrade marker, GCC exchange (with the given +/// client blocks) and the full MCS channel join sequence, then the Client +/// Info PDU. Returns `(user_channel_id, io_channel_id, message_channel_id)` +/// as actually assigned by the acceptor, so callers never have to hard-code +/// them. +fn drive_to_secure_settings_exchange( + acceptor: &mut Acceptor, + client_blocks: ironrdp_pdu::gcc::ClientGccBlocks, +) -> (u16, u16, Option, Option) { + let request_bytes = encode_connection_request(SecurityProtocol::SSL); + acceptor.step(&request_bytes, None, &mut WriteBuf::new()).unwrap(); + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + acceptor.mark_security_upgrade_as_done(); + + let connect_initial = ConnectInitial::with_gcc_blocks(client_blocks).unwrap(); + let mut initial_buf = WriteBuf::new(); + encode_x224_packet(&connect_initial, &mut initial_buf).unwrap(); + acceptor.step(initial_buf.filled(), None, &mut WriteBuf::new()).unwrap(); + + let mut output = WriteBuf::new(); + acceptor.step(&[], None, &mut output).unwrap(); + let payload = decode::>>(output.filled()).unwrap().0; + let response = decode::(payload.data.as_ref()).unwrap(); + let server_blocks = response.conference_create_response.gcc_blocks(); + let io_channel_id = server_blocks.network.io_channel; + let message_channel_id = server_blocks.message_channel.as_ref().map(|m| m.mcs_message_channel_id); + let server_multitransport = server_blocks.multi_transport_channel.as_ref().map(|m| m.flags); + + // Erect Domain Request, Attach User Request, then confirm. + let mut buf = WriteBuf::new(); + ironrdp_core::encode_buf( + &X224(mcs::ErectDomainPdu { + sub_height: 0, + sub_interval: 0, + }), + &mut buf, + ) + .unwrap(); + acceptor.step(buf.filled(), None, &mut WriteBuf::new()).unwrap(); + + let mut buf = WriteBuf::new(); + ironrdp_core::encode_buf(&X224(mcs::AttachUserRequest), &mut buf).unwrap(); + acceptor.step(buf.filled(), None, &mut WriteBuf::new()).unwrap(); + + let mut output = WriteBuf::new(); + acceptor.step(&[], None, &mut output).unwrap(); + let attach_user_confirm = decode::>(output.filled()).unwrap().0; + let user_channel_id = attach_user_confirm.initiator_id; + + // Join every channel the server expects: the user and I/O channels, plus + // the message channel when one was negotiated. No other static channels + // are requested by `client_blocks.network` in these tests. + let mut to_join = vec![user_channel_id, io_channel_id]; + to_join.extend(message_channel_id); + for channel_id in to_join { + let mut buf = WriteBuf::new(); + ironrdp_core::encode_buf( + &X224(mcs::ChannelJoinRequest { + initiator_id: user_channel_id, + channel_id, + }), + &mut buf, + ) + .unwrap(); + acceptor.step(buf.filled(), None, &mut WriteBuf::new()).unwrap(); + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + } + + // RdpSecurityCommencement (no input) -> SecureSettingsExchange, then the + // Client Info PDU on the I/O channel. + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + let client_info = encode_send_data_request(user_channel_id, io_channel_id, &CLIENT_INFO_PDU_BUFFER); + acceptor.step(&client_info, None, &mut WriteBuf::new()).unwrap(); + + ( + user_channel_id, + io_channel_id, + message_channel_id, + server_multitransport, + ) +} + +fn client_gcc_with_message_channel_and_multitransport( + offer: Option, +) -> ironrdp_pdu::gcc::ClientGccBlocks { + let mut blocks = CLIENT_GCC_WITHOUT_OPTIONAL_FIELDS.clone(); + blocks.network = None; + blocks.message_channel = Some(ClientMessageChannelData); + blocks.multi_transport_channel = offer.map(|flags| MultiTransportChannelData { flags }); + blocks +} + +/// Builds an `Acceptor` for the multitransport tests below, at a common +/// 1920x1080 desktop size with no static channels or credentials. `offer` is +/// passed to `set_multitransport_offer` when `Some`; pass `None` to exercise +/// the default-disabled path. +fn multitransport_acceptor(offer: Option) -> Acceptor { + let mut acceptor = Acceptor::new( + SecurityProtocol::SSL, + DesktopSize { + width: 1920, + height: 1080, + }, + Vec::new(), + None, + ); + if let Some(offer) = offer { + acceptor.set_multitransport_offer(Some(offer)); + } + acceptor +} + +/// The full happy path: the acceptor offers reliable UDP multitransport, the +/// client reciprocates, so the request goes out on the message channel and +/// `multitransport_request()` surfaces it. A late Initiate Multitransport +/// Response then arrives, interleaved before the mandatory Confirm Active +/// exactly as a real client would send it (MS-RDPBCGR 3.2.5.15.1: sent while +/// resolving its own bootstrapping, strictly before it ever reads Demand +/// Active), and the acceptor must tolerate it rather than erroring out while +/// still reaching capabilities confirmation. +#[test] +fn multitransport_offered_and_client_reciprocates() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let (user_channel_id, _io_channel_id, message_channel_id, server_multitransport) = + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + let message_channel_id = message_channel_id.expect("message channel negotiated"); + assert_eq!( + server_multitransport, + Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR), + "server should advertise the offer once the client reciprocated" + ); + + // LicensingExchange (sends license) -> MultitransportBootstrapping. + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + + assert!( + acceptor.multitransport_request().is_none(), + "no request sent until MultitransportBootstrapping actually runs" + ); + + // MultitransportBootstrapping: decides to offer, sends the request. + let mut output = WriteBuf::new(); + let written = acceptor.step(&[], None, &mut output).unwrap(); + assert!(!matches!(written, Written::Nothing), "expected the request to be sent"); + + let sent_request = acceptor + .multitransport_request() + .expect("request recorded after MultitransportBootstrapping") + .clone(); + assert_eq!(sent_request.requested_protocol, RequestedProtocol::UdpFecR); + assert_eq!( + sent_request.security_header.flags, + BasicSecurityHeaderFlags::TRANSPORT_REQ + ); + + let ctx = mcs::decode_send_data_indication(output.filled()).unwrap(); + assert_eq!( + ctx.channel_id, message_channel_id, + "request must go out on the message channel" + ); + let on_wire_request = decode::(ctx.user_data).unwrap(); + assert_eq!(on_wire_request, sent_request); + + // CapabilitiesSendServer: sends Demand Active, moves on to CapabilitiesWaitConfirm + // (no monitor-layout support was advertised). + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + assert_eq!(acceptor.state().name(), "CapabilitiesWaitConfirm"); + + // The client's Initiate Multitransport Response, arriving before Confirm Active. + // MS-RDPBCGR 2.2.15.2: S_OK MUST only be sent to a server advertising + // SOFTSYNC_TCP_TO_UDP, which this test's offer does not include; the + // legitimate response here is a failure code. + let response = MultitransportResponsePdu::abort(sent_request.request_id); + let response_bytes = encode_send_data_request(user_channel_id, message_channel_id, &encode_vec(&response).unwrap()); + let written = acceptor.step(&response_bytes, None, &mut WriteBuf::new()).unwrap(); + assert!(matches!(written, Written::Nothing)); + assert_eq!( + acceptor.state().name(), + "CapabilitiesWaitConfirm", + "the response must not advance past capabilities waiting" + ); + + // Now the actual Confirm Active. + let confirm_active = encode_send_data_request(user_channel_id, _io_channel_id, &CLIENT_DEMAND_ACTIVE_PDU_BUFFER); + acceptor.step(&confirm_active, None, &mut WriteBuf::new()).unwrap(); + assert_eq!(acceptor.state().name(), "ConnectionFinalization"); +} + +/// A fixed `MultitransportSecurityRng` for deterministic assertions. +struct FixedMultitransportSecurityRng { + cookie: [u8; 16], + request_id: u32, +} + +impl MultitransportSecurityRng for FixedMultitransportSecurityRng { + fn fill_security_cookie(&mut self, cookie: &mut [u8; 16]) { + *cookie = self.cookie; + } + + fn next_request_id(&mut self) -> u32 { + self.request_id + } +} + +/// The security cookie and request ID in the Initiate Multitransport Request +/// come from the injected `MultitransportSecurityRng`, not from a hidden +/// global RNG read inside `step()`: the acceptor is deterministic when its +/// randomness is supplied rather than sourced internally. +#[test] +fn multitransport_request_uses_the_injected_rng() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + acceptor.set_multitransport_security_rng(Box::new(FixedMultitransportSecurityRng { + cookie: [0xAB; 16], + request_id: 0x1234_5678, + })); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + + // LicensingExchange (sends license) -> MultitransportBootstrapping. + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + // MultitransportBootstrapping: sends the request using the injected RNG. + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); + + let sent_request = acceptor + .multitransport_request() + .expect("request recorded after MultitransportBootstrapping"); + assert_eq!(sent_request.security_cookie, [0xAB; 16]); + assert_eq!(sent_request.request_id, 0x1234_5678); +} + +/// The message channel also carries Auto-Detect Response and Heartbeat PDUs +/// (MS-RDPBCGR 2.2.1.4.5, 2.2.8.1.1.2.1), not just the Initiate Multitransport +/// Response. A guard keyed only on channel and outstanding-request, without +/// verifying the payload actually decodes as a response, would misclassify +/// that other traffic and drop it silently instead of letting the caller's +/// own (pre-existing, unrelated to this fix) handling see it. +#[test] +fn non_response_traffic_on_the_message_channel_is_not_misclassified() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let (user_channel_id, _io_channel_id, message_channel_id, _) = + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + let message_channel_id = message_channel_id.expect("message channel negotiated"); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping: sends request + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // CapabilitiesSendServer: sends Demand Active + assert_eq!(acceptor.state().name(), "CapabilitiesWaitConfirm"); + + // Traffic on the message channel that is not a MultitransportResponsePdu + // (its wire format is just requestId/hrResponse, so arbitrary bytes of a + // different shape and length reliably fail that specific decode). Before + // the fix, the old channel-only guard treated this as a response and + // silently dropped it (`Ok(Written::Nothing)`, no change of state, + // forever). After the fix, it falls through to the same handling any + // other unexpected traffic already gets here (a decode error), rather + // than being silently and permanently swallowed. + let not_a_response = encode_send_data_request(user_channel_id, message_channel_id, &[0xAA; 3]); + let result = acceptor.step(¬_a_response, None, &mut WriteBuf::new()); + assert!( + result.is_err(), + "non-response message-channel traffic must not be silently swallowed as a response" + ); +} + +/// With the Basic Security Header that the acceptor's ENCRYPTION_LEVEL_NONE +/// implies, the Initiate Multitransport Response is exactly 12 bytes +/// (MS-RDPBCGR 2.2.15.2). A message-channel payload that starts with a valid +/// response but carries trailing bytes is not one, so it must not be consumed +/// as a late response: it falls through to the same handling as any other +/// unexpected traffic. +#[test] +fn multitransport_response_with_trailing_bytes_is_not_consumed() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let (user_channel_id, _io_channel_id, message_channel_id, _) = + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + let message_channel_id = message_channel_id.expect("message channel negotiated"); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping: sends request + let sent_request = acceptor.multitransport_request().expect("request sent").clone(); + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // CapabilitiesSendServer: sends Demand Active + assert_eq!(acceptor.state().name(), "CapabilitiesWaitConfirm"); + + let mut payload = encode_vec(&MultitransportResponsePdu::abort(sent_request.request_id)).unwrap(); + payload.push(0x00); + let response_with_trailing_byte = encode_send_data_request(user_channel_id, message_channel_id, &payload); + let result = acceptor.step(&response_with_trailing_byte, None, &mut WriteBuf::new()); + assert!( + result.is_err(), + "a response followed by trailing bytes must not be swallowed as a late response" + ); +} + +/// MS-RDPBCGR 3.2.5.15.1 gives the Initiate Multitransport Response no fixed +/// position relative to the rest of the handshake: it depends on when the +/// client resolves its own bootstrapping and whether the sideband attempt +/// failed, so a conforming client can send it after Confirm Active, during +/// finalization, not only before it as `multitransport_offered_and_client_reciprocates` +/// exercises. `WaitRequestControl` is the sharpest of FinalizationSequence's +/// sub-states to hit: unlike `WaitSynchronize`/`WaitControlCooperate` (which +/// discard a decode error and advance anyway) or `WaitFontList` (which retries +/// on a decode error, tolerating one stray message), it propagates a decode +/// failure with `?`, so without tolerance the response's raw bytes (a bare +/// requestId/hrResponse pair, not a ShareControlHeader at all) fail to decode +/// and the connection is dropped outright. +#[test] +fn multitransport_response_arriving_during_finalization_is_tolerated() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let (user_channel_id, io_channel_id, message_channel_id, _) = + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + let message_channel_id = message_channel_id.expect("message channel negotiated"); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping: sends request + let sent_request = acceptor.multitransport_request().expect("request sent").clone(); + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // CapabilitiesSendServer: sends Demand Active + + let confirm_active = encode_send_data_request(user_channel_id, io_channel_id, &CLIENT_DEMAND_ACTIVE_PDU_BUFFER); + acceptor.step(&confirm_active, None, &mut WriteBuf::new()).unwrap(); + assert_eq!(acceptor.state().name(), "ConnectionFinalization"); + + // Drive the real Synchronize and ControlCooperate exchange first, reaching + // WaitRequestControl. + let synchronize = encode_send_data_request( + user_channel_id, + io_channel_id, + &encode_client_share_data(ShareDataPdu::Synchronize(SynchronizePdu { target_user_id: 0 })), + ); + acceptor.step(&synchronize, None, &mut WriteBuf::new()).unwrap(); + + let cooperate = encode_send_data_request( + user_channel_id, + io_channel_id, + &encode_client_share_data(ShareDataPdu::Control(ControlPdu { + action: ControlAction::Cooperate, + grant_id: 0, + control_id: 0, + })), + ); + acceptor.step(&cooperate, None, &mut WriteBuf::new()).unwrap(); + + // The late response, arriving while WaitRequestControl is active. + // MS-RDPBCGR 2.2.15.2: S_OK MUST only be sent to a server advertising + // SOFTSYNC_TCP_TO_UDP, which this test's offer does not include; the + // legitimate response here is a failure code. + let response = MultitransportResponsePdu::abort(sent_request.request_id); + let response_bytes = encode_send_data_request(user_channel_id, message_channel_id, &encode_vec(&response).unwrap()); + let written = acceptor + .step(&response_bytes, None, &mut WriteBuf::new()) + .expect("a late response must not drop the connection"); + assert!(matches!(written, Written::Nothing)); + assert_eq!( + acceptor.state().name(), + "ConnectionFinalization", + "a late response must not be treated as RequestControl" + ); + + // The real remainder of the sequence, undisturbed by the late response above. + let request_control = encode_send_data_request( + user_channel_id, + io_channel_id, + &encode_client_share_data(ShareDataPdu::Control(ControlPdu { + action: ControlAction::RequestControl, + grant_id: 0, + control_id: 0, + })), + ); + acceptor.step(&request_control, None, &mut WriteBuf::new()).unwrap(); + + let font_list = encode_send_data_request( + user_channel_id, + io_channel_id, + &encode_client_share_data(ShareDataPdu::FontList(Default::default())), + ); + acceptor.step(&font_list, None, &mut WriteBuf::new()).unwrap(); + + // The server's four confirmation sends, completing finalization. + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // SendSynchronizeConfirm + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // SendControlCooperateConfirm + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // SendGrantedControlConfirm + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // SendFontMap -> Accepted + + assert_eq!(acceptor.state().name(), "Connected"); +} + +/// Multitransport disabled (the default): no Server MultiTransportChannelData +/// block is advertised even though the client supports it, and no Initiate +/// Multitransport Request is ever sent. +#[test] +fn multitransport_not_offered_by_default() { + let mut acceptor = multitransport_acceptor(None); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + let written = acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping + assert!(matches!(written, Written::Nothing)); + assert!(acceptor.multitransport_request().is_none()); + assert_eq!(acceptor.multitransport_soft_sync_negotiated(), None); +} + +/// The request decision follows what the server advertised during Basic +/// Settings Exchange, not the live offer: enabling multitransport only after +/// the GCC response went out without a MultiTransportChannelData block must +/// not produce a request the client was never told about (MS-RDPBCGR 2.2.1.4). +#[test] +fn multitransport_offer_enabled_after_basic_settings_is_not_requested() { + let mut acceptor = multitransport_acceptor(None); + + let client_blocks = + client_gcc_with_message_channel_and_multitransport(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + let (.., server_multitransport) = drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + assert_eq!(server_multitransport, None); + + acceptor.set_multitransport_offer(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + let written = acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping + assert!(matches!(written, Written::Nothing)); + assert!(acceptor.multitransport_request().is_none()); +} + +/// Likewise for Soft-Sync: adding `SOFT_SYNC_TCP_TO_UDP` to the offer after +/// the GCC response advertised reliable UDP without it must not make +/// `multitransport_soft_sync_negotiated()` report a negotiation that never +/// took place. +#[test] +fn soft_sync_added_after_basic_settings_is_not_negotiated() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = client_gcc_with_message_channel_and_multitransport(Some( + MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR | MultiTransportFlags::SOFT_SYNC_TCP_TO_UDP, + )); + let (.., server_multitransport) = drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + assert_eq!( + server_multitransport, + Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR) + ); + + acceptor.set_multitransport_offer(Some( + MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR | MultiTransportFlags::SOFT_SYNC_TCP_TO_UDP, + )); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping: sends request + assert!(acceptor.multitransport_request().is_some()); + assert_eq!(acceptor.multitransport_soft_sync_negotiated(), Some(false)); +} + +/// The acceptor offers multitransport, but the client's GCC blocks never +/// advertised reliable UDP support: nothing is sent, and per MS-RDPBCGR +/// 2.2.1.4 the server's own GCC response must omit the block entirely +/// rather than echo the offer the client never reciprocated. +#[test] +fn multitransport_not_offered_when_client_does_not_reciprocate() { + let mut acceptor = multitransport_acceptor(Some(MultiTransportFlags::TRANSPORT_TYPE_UDP_FECR)); + + let client_blocks = client_gcc_with_message_channel_and_multitransport(None); + let (.., server_multitransport) = drive_to_secure_settings_exchange(&mut acceptor, client_blocks); + assert_eq!( + server_multitransport, None, + "server must not advertise MultiTransportChannelData when the client didn't send one" + ); + + acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // LicensingExchange + let written = acceptor.step(&[], None, &mut WriteBuf::new()).unwrap(); // MultitransportBootstrapping + assert!(matches!(written, Written::Nothing)); + assert!(acceptor.multitransport_request().is_none()); +}