Skip to content

Commit c790f31

Browse files
committed
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 5663ce2 commit c790f31

2 files changed

Lines changed: 258 additions & 13 deletions

File tree

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

203376
impl Sequence for ClientConnector {
@@ -214,7 +387,8 @@ impl Sequence for ClientConnector {
214387
ClientConnectorState::SecureSettingsExchange { .. } => None,
215388
ClientConnectorState::ConnectTimeAutoDetection { .. } => None,
216389
ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(),
217-
ClientConnectorState::MultitransportBootstrapping { .. } => None,
390+
ClientConnectorState::MultitransportBootstrapping { .. } => Some(&ironrdp_pdu::X224_HINT),
391+
ClientConnectorState::MultitransportPending { .. } => None,
218392
ClientConnectorState::CapabilitiesExchange {
219393
connection_activation, ..
220394
} => connection_activation.next_pdu_hint(),
@@ -519,6 +693,7 @@ impl Sequence for ClientConnector {
519693
ClientConnectorState::MultitransportBootstrapping {
520694
io_channel_id,
521695
user_channel_id,
696+
requests: Vec::new(),
522697
}
523698
} else {
524699
ClientConnectorState::LicensingExchange {
@@ -532,20 +707,88 @@ impl Sequence for ClientConnector {
532707
}
533708

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

550793
//== Capabilities Exchange ==/
551794
// 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)