Skip to content

Commit 89ff5d6

Browse files
glambersonlamco-office
authored andcommitted
feat(connector): implement multitransport bootstrapping handshake
Makes MultitransportBootstrapping functional: reads the server's optional Initiate Multitransport Request PDUs (0, 1, or 2), then pauses in MultitransportPending for the application to establish UDP transport before proceeding to capabilities exchange. Follows the existing should_perform_X() / mark_X_as_done() pattern used by TLS upgrade and CredSSP.
1 parent a6b4109 commit 89ff5d6

File tree

2 files changed

+258
-13
lines changed

2 files changed

+258
-13
lines changed

crates/ironrdp-connector/src/connection.rs

Lines changed: 255 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ use crate::{
1717
NegotiationFailure, Sequence, State, Written, encode_x224_packet, general_err, reason_err,
1818
};
1919

20+
/// Outcome of a single multitransport bootstrapping request, passed to
21+
/// [`ClientConnector::complete_multitransport()`].
22+
///
23+
/// The connector uses this to build the response PDU internally, paired with
24+
/// the request ID and cookie from the server's original request.
25+
#[derive(Debug, Clone, PartialEq, Eq)]
26+
pub enum MultitransportResult {
27+
/// UDP transport was established successfully (`S_OK`).
28+
Success,
29+
/// UDP transport failed. The `u32` is the HRESULT error code (typically
30+
/// [`MultitransportResponsePdu::E_ABORT`](rdp::multitransport::MultitransportResponsePdu::E_ABORT)).
31+
Failure(u32),
32+
}
33+
2034
#[derive(Debug)]
2135
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
2236
pub struct ConnectionResult {
@@ -72,9 +86,32 @@ pub enum ClientConnectorState {
7286
user_channel_id: u16,
7387
license_exchange: LicenseExchangeSequence,
7488
},
89+
/// Reading the server's optional Initiate Multitransport Request PDU(s).
90+
///
91+
/// The server may send 0, 1, or 2 requests (one per transport protocol).
92+
/// If the first PDU on the IO channel after licensing is a Demand Active
93+
/// (capabilities exchange), the server sent no multitransport requests and
94+
/// the connector transitions directly to `CapabilitiesExchange`.
7595
MultitransportBootstrapping {
7696
io_channel_id: u16,
7797
user_channel_id: u16,
98+
/// Multitransport requests received from the server so far.
99+
requests: Vec<rdp::multitransport::MultitransportRequestPdu>,
100+
},
101+
/// The server sent multitransport request(s) and the connector is paused
102+
/// waiting for the application to establish UDP transport or decline.
103+
///
104+
/// Call [`ClientConnector::complete_multitransport()`] or
105+
/// [`ClientConnector::skip_multitransport()`] to advance. The buffered
106+
/// Demand Active PDU is replayed internally — no re-feeding needed.
107+
MultitransportPending {
108+
io_channel_id: u16,
109+
user_channel_id: u16,
110+
requests: Vec<rdp::multitransport::MultitransportRequestPdu>,
111+
/// The raw Demand Active PDU bytes that arrived after the last
112+
/// multitransport request. Replayed through the activation sequence
113+
/// when the application completes or skips multitransport.
114+
buffered_demand_active: Vec<u8>,
78115
},
79116
CapabilitiesExchange {
80117
connection_activation: ConnectionActivationSequence,
@@ -102,6 +139,7 @@ impl State for ClientConnectorState {
102139
Self::ConnectTimeAutoDetection { .. } => "ConnectTimeAutoDetection",
103140
Self::LicensingExchange { .. } => "LicensingExchange",
104141
Self::MultitransportBootstrapping { .. } => "MultitransportBootstrapping",
142+
Self::MultitransportPending { .. } => "MultitransportPending",
105143
Self::CapabilitiesExchange {
106144
connection_activation, ..
107145
} => connection_activation.state().name(),
@@ -201,6 +239,141 @@ impl ClientConnector {
201239
debug_assert!(!self.should_perform_credssp());
202240
assert_eq!(res, Written::Nothing);
203241
}
242+
243+
/// Returns `true` when the connector has collected all multitransport
244+
/// requests from the server and is waiting for the application to either
245+
/// establish the UDP transport(s) or decline them.
246+
///
247+
/// The application should:
248+
///
249+
/// 1. Call [`multitransport_requests()`](Self::multitransport_requests) to
250+
/// get the server's request(s)
251+
/// 2. Establish UDP transport (RDPEUDP2 + TLS + RDPEMT) for each, or decide
252+
/// not to
253+
/// 3. Call [`complete_multitransport()`](Self::complete_multitransport) with
254+
/// a [`MultitransportResult`] for each request, or
255+
/// [`skip_multitransport()`](Self::skip_multitransport) to decline all
256+
pub fn should_perform_multitransport(&self) -> bool {
257+
matches!(self.state, ClientConnectorState::MultitransportPending { .. })
258+
}
259+
260+
/// Returns the multitransport request PDUs received from the server.
261+
///
262+
/// Only meaningful when
263+
/// [`should_perform_multitransport()`](Self::should_perform_multitransport)
264+
/// returns `true`.
265+
pub fn multitransport_requests(&self) -> &[rdp::multitransport::MultitransportRequestPdu] {
266+
match &self.state {
267+
ClientConnectorState::MultitransportPending { requests, .. } => requests,
268+
_ => &[],
269+
}
270+
}
271+
272+
/// Send multitransport response PDU(s) and advance past the bootstrapping
273+
/// phase to capabilities exchange.
274+
///
275+
/// Pass one [`MultitransportResult`] per request (in the same order as
276+
/// [`multitransport_requests()`](Self::multitransport_requests)). The
277+
/// connector builds the response PDUs internally using the stored request
278+
/// IDs and cookies, then replays the buffered Demand Active PDU through
279+
/// the activation sequence.
280+
///
281+
/// Returns an error if the connector is not in `MultitransportPending`
282+
/// state, or if `results.len()` does not match the number of pending
283+
/// requests.
284+
pub fn complete_multitransport(
285+
&mut self,
286+
results: &[MultitransportResult],
287+
output: &mut WriteBuf,
288+
) -> ConnectorResult<Written> {
289+
let ClientConnectorState::MultitransportPending {
290+
io_channel_id,
291+
user_channel_id,
292+
requests,
293+
buffered_demand_active,
294+
} = mem::replace(&mut self.state, ClientConnectorState::Consumed)
295+
else {
296+
return Err(general_err!(
297+
"complete_multitransport called outside MultitransportPending state"
298+
));
299+
};
300+
301+
if results.len() != requests.len() {
302+
return Err(general_err!(
303+
"multitransport results count does not match requests count"
304+
));
305+
}
306+
307+
let mut total_written = 0;
308+
309+
for (request, result) in requests.iter().zip(results) {
310+
let response = match result {
311+
MultitransportResult::Success => {
312+
rdp::multitransport::MultitransportResponsePdu::success(request.request_id)
313+
}
314+
MultitransportResult::Failure(hr) => rdp::multitransport::MultitransportResponsePdu {
315+
security_header: rdp::headers::BasicSecurityHeader {
316+
flags: rdp::headers::BasicSecurityHeaderFlags::TRANSPORT_RSP,
317+
},
318+
request_id: request.request_id,
319+
hr_response: *hr,
320+
},
321+
};
322+
total_written += encode_send_data_request(user_channel_id, io_channel_id, &response, output)?;
323+
}
324+
325+
// Replay the buffered Demand Active through the activation sequence
326+
let mut connection_activation =
327+
ConnectionActivationSequence::new(self.config.clone(), io_channel_id, user_channel_id);
328+
let replay_written = connection_activation.step(&buffered_demand_active, output)?;
329+
total_written += replay_written.size().unwrap_or(0);
330+
331+
self.state = match connection_activation.connection_activation_state() {
332+
ConnectionActivationState::ConnectionFinalization { .. } => {
333+
ClientConnectorState::ConnectionFinalization { connection_activation }
334+
}
335+
_ => ClientConnectorState::CapabilitiesExchange { connection_activation },
336+
};
337+
338+
Written::from_size(total_written)
339+
}
340+
341+
/// Skip multitransport bootstrapping without sending any responses.
342+
///
343+
/// Use this when the application doesn't support or doesn't want UDP
344+
/// transport. The server will continue with TCP-only operation.
345+
///
346+
/// The buffered Demand Active PDU is replayed internally.
347+
///
348+
/// Returns an error if the connector is not in `MultitransportPending`
349+
/// state.
350+
pub fn skip_multitransport(&mut self, output: &mut WriteBuf) -> ConnectorResult<Written> {
351+
let ClientConnectorState::MultitransportPending {
352+
io_channel_id,
353+
user_channel_id,
354+
buffered_demand_active,
355+
..
356+
} = mem::replace(&mut self.state, ClientConnectorState::Consumed)
357+
else {
358+
return Err(general_err!(
359+
"skip_multitransport called outside MultitransportPending state"
360+
));
361+
};
362+
363+
// Replay the buffered Demand Active through the activation sequence
364+
let mut connection_activation =
365+
ConnectionActivationSequence::new(self.config.clone(), io_channel_id, user_channel_id);
366+
let written = connection_activation.step(&buffered_demand_active, output)?;
367+
368+
self.state = match connection_activation.connection_activation_state() {
369+
ConnectionActivationState::ConnectionFinalization { .. } => {
370+
ClientConnectorState::ConnectionFinalization { connection_activation }
371+
}
372+
_ => ClientConnectorState::CapabilitiesExchange { connection_activation },
373+
};
374+
375+
Ok(written)
376+
}
204377
}
205378

206379
impl Sequence for ClientConnector {
@@ -217,7 +390,8 @@ impl Sequence for ClientConnector {
217390
ClientConnectorState::SecureSettingsExchange { .. } => None,
218391
ClientConnectorState::ConnectTimeAutoDetection { .. } => None,
219392
ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(),
220-
ClientConnectorState::MultitransportBootstrapping { .. } => None,
393+
ClientConnectorState::MultitransportBootstrapping { .. } => Some(&ironrdp_pdu::X224_HINT),
394+
ClientConnectorState::MultitransportPending { .. } => None,
221395
ClientConnectorState::CapabilitiesExchange {
222396
connection_activation, ..
223397
} => connection_activation.next_pdu_hint(),
@@ -522,6 +696,7 @@ impl Sequence for ClientConnector {
522696
ClientConnectorState::MultitransportBootstrapping {
523697
io_channel_id,
524698
user_channel_id,
699+
requests: Vec::new(),
525700
}
526701
} else {
527702
ClientConnectorState::LicensingExchange {
@@ -535,20 +710,88 @@ impl Sequence for ClientConnector {
535710
}
536711

537712
//== Optional Multitransport Bootstrapping ==//
538-
// NOTE: our implementation is not expecting the Auto-Detect Request PDU from server
713+
//
714+
// The server may send 0, 1, or 2 Initiate Multitransport Request PDUs
715+
// after licensing. We distinguish them from the Demand Active PDU by
716+
// attempting to decode as MultitransportRequestPdu first — it has a
717+
// distinctive structure (SEC_TRANSPORT_REQ flag + request_id + protocol
718+
// + cookie). If decode fails, this is the Demand Active.
539719
ClientConnectorState::MultitransportBootstrapping {
540720
io_channel_id,
541721
user_channel_id,
542-
} => (
543-
Written::Nothing,
544-
ClientConnectorState::CapabilitiesExchange {
545-
connection_activation: ConnectionActivationSequence::new(
546-
self.config.clone(),
547-
io_channel_id,
548-
user_channel_id,
549-
),
550-
},
551-
),
722+
mut requests,
723+
} => {
724+
let ctx = crate::legacy::decode_send_data_indication(input)?;
725+
726+
// Try decoding as a multitransport request. The decoder validates
727+
// the SEC_TRANSPORT_REQ flag, so a Demand Active PDU will fail
728+
// cleanly without false positives.
729+
match decode::<rdp::multitransport::MultitransportRequestPdu>(ctx.user_data) {
730+
Ok(pdu) => {
731+
debug!(
732+
request_id = pdu.request_id,
733+
protocol = ?pdu.requested_protocol,
734+
"Received Initiate Multitransport Request"
735+
);
736+
737+
requests.push(pdu);
738+
739+
// Stay in this state to read more requests (server may send a second)
740+
(
741+
Written::Nothing,
742+
ClientConnectorState::MultitransportBootstrapping {
743+
io_channel_id,
744+
user_channel_id,
745+
requests,
746+
},
747+
)
748+
}
749+
Err(_) if !requests.is_empty() => {
750+
// Decode failed → this is the Demand Active PDU. Buffer it
751+
// and pause for the application to handle multitransport.
752+
info!(
753+
count = requests.len(),
754+
"Multitransport bootstrapping: pausing for application"
755+
);
756+
757+
(
758+
Written::Nothing,
759+
ClientConnectorState::MultitransportPending {
760+
io_channel_id,
761+
user_channel_id,
762+
requests,
763+
buffered_demand_active: input.to_vec(),
764+
},
765+
)
766+
}
767+
Err(_) => {
768+
// No multitransport requests — server went straight to
769+
// capabilities exchange. Forward the PDU.
770+
let mut connection_activation =
771+
ConnectionActivationSequence::new(self.config.clone(), io_channel_id, user_channel_id);
772+
let written = connection_activation.step(input, output)?;
773+
774+
match connection_activation.connection_activation_state() {
775+
ConnectionActivationState::ConnectionFinalization { .. } => (
776+
written,
777+
ClientConnectorState::ConnectionFinalization { connection_activation },
778+
),
779+
_ => (
780+
written,
781+
ClientConnectorState::CapabilitiesExchange { connection_activation },
782+
),
783+
}
784+
}
785+
}
786+
}
787+
788+
// MultitransportPending: application should call complete_multitransport()
789+
// or skip_multitransport() instead of step()
790+
ClientConnectorState::MultitransportPending { .. } => {
791+
return Err(general_err!(
792+
"multitransport pending: call complete_multitransport() or skip_multitransport()"
793+
));
794+
}
552795

553796
//== Capabilities Exchange ==/
554797
// The server sends the set of capabilities it supports to the client.

crates/ironrdp-connector/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use ironrdp_pdu::{PduHint, gcc, x224};
2626
pub use sspi;
2727

2828
pub use self::channel_connection::{ChannelConnectionSequence, ChannelConnectionState};
29-
pub use self::connection::{ClientConnector, ClientConnectorState, ConnectionResult, encode_send_data_request};
29+
pub use self::connection::{
30+
ClientConnector, ClientConnectorState, ConnectionResult, MultitransportResult, encode_send_data_request,
31+
};
3032
pub use self::connection_finalization::{ConnectionFinalizationSequence, ConnectionFinalizationState};
3133
pub use self::license_exchange::{LicenseExchangeSequence, LicenseExchangeState};
3234
pub use self::server_name::ServerName;

0 commit comments

Comments
 (0)