Skip to content

Commit 6ab896d

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 4ba696c commit 6ab896d

1 file changed

Lines changed: 224 additions & 12 deletions

File tree

crates/ironrdp-connector/src/connection.rs

Lines changed: 224 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,28 @@ pub enum ClientConnectorState {
7171
user_channel_id: u16,
7272
license_exchange: LicenseExchangeSequence,
7373
},
74+
/// Reading the server's optional Initiate Multitransport Request PDU(s).
75+
///
76+
/// The server may send 0, 1, or 2 requests (one per transport protocol).
77+
/// If the first PDU on the IO channel after licensing is a Demand Active
78+
/// (capabilities exchange), the server sent no multitransport requests and
79+
/// the connector transitions directly to `CapabilitiesExchange`.
7480
MultitransportBootstrapping {
7581
io_channel_id: u16,
7682
user_channel_id: u16,
83+
/// Multitransport requests received from the server so far.
84+
requests: Vec<rdp::multitransport::MultitransportRequestPdu>,
85+
},
86+
/// The server sent multitransport request(s) and the connector is paused
87+
/// waiting for the application to establish UDP transport or decline.
88+
///
89+
/// The buffered Demand Active PDU must be re-fed to the connector after
90+
/// the application calls [`ClientConnector::complete_multitransport()`] or
91+
/// [`ClientConnector::skip_multitransport()`].
92+
MultitransportPending {
93+
io_channel_id: u16,
94+
user_channel_id: u16,
95+
requests: Vec<rdp::multitransport::MultitransportRequestPdu>,
7796
},
7897
CapabilitiesExchange {
7998
connection_activation: ConnectionActivationSequence,
@@ -101,6 +120,7 @@ impl State for ClientConnectorState {
101120
Self::ConnectTimeAutoDetection { .. } => "ConnectTimeAutoDetection",
102121
Self::LicensingExchange { .. } => "LicensingExchange",
103122
Self::MultitransportBootstrapping { .. } => "MultitransportBootstrapping",
123+
Self::MultitransportPending { .. } => "MultitransportPending",
104124
Self::CapabilitiesExchange {
105125
connection_activation, ..
106126
} => connection_activation.state().name(),
@@ -200,6 +220,117 @@ impl ClientConnector {
200220
debug_assert!(!self.should_perform_credssp());
201221
assert_eq!(res, Written::Nothing);
202222
}
223+
224+
/// Returns `true` when the connector has collected all multitransport
225+
/// requests from the server and is waiting for the application to either
226+
/// establish the UDP transport(s) or decline them.
227+
///
228+
/// The application should:
229+
///
230+
/// 1. Call [`multitransport_requests()`](Self::multitransport_requests) to
231+
/// get the server's request(s)
232+
/// 2. Establish UDP transport (RDPEUDP2 + TLS + RDPEMT) for each, or decide
233+
/// not to
234+
/// 3. Call [`complete_multitransport()`](Self::complete_multitransport) with
235+
/// the response for each request
236+
pub fn should_perform_multitransport(&self) -> bool {
237+
matches!(self.state, ClientConnectorState::MultitransportPending { .. })
238+
}
239+
240+
/// Returns the multitransport request PDUs received from the server.
241+
///
242+
/// Only meaningful when
243+
/// [`should_perform_multitransport()`](Self::should_perform_multitransport)
244+
/// returns `true`.
245+
pub fn multitransport_requests(&self) -> &[rdp::multitransport::MultitransportRequestPdu] {
246+
match &self.state {
247+
ClientConnectorState::MultitransportPending { requests, .. } => requests,
248+
_ => &[],
249+
}
250+
}
251+
252+
/// Send multitransport response PDU(s) and advance past the bootstrapping
253+
/// phase to capabilities exchange.
254+
///
255+
/// Pass one response per request. Use
256+
/// [`MultitransportResponsePdu::success()`] or
257+
/// [`MultitransportResponsePdu::failure()`] to construct responses.
258+
///
259+
/// After calling this, re-feed the buffered Demand Active PDU to the
260+
/// connector (via `step()`).
261+
///
262+
/// # Panics
263+
///
264+
/// Panics if state is not [`ClientConnectorState::MultitransportPending`].
265+
pub fn complete_multitransport(
266+
&mut self,
267+
responses: &[rdp::multitransport::MultitransportResponsePdu],
268+
output: &mut WriteBuf,
269+
) -> ConnectorResult<Written> {
270+
assert!(self.should_perform_multitransport());
271+
272+
let ClientConnectorState::MultitransportPending {
273+
io_channel_id,
274+
user_channel_id,
275+
..
276+
} = self.state
277+
else {
278+
unreachable!()
279+
};
280+
281+
let mut total_written = 0;
282+
283+
for response in responses {
284+
let written = encode_send_data_request(user_channel_id, io_channel_id, response, output)?;
285+
total_written += written;
286+
}
287+
288+
self.state = ClientConnectorState::CapabilitiesExchange {
289+
connection_activation: ConnectionActivationSequence::new(
290+
self.config.clone(),
291+
io_channel_id,
292+
user_channel_id,
293+
),
294+
};
295+
296+
if total_written > 0 {
297+
Written::from_size(total_written)
298+
} else {
299+
Ok(Written::Nothing)
300+
}
301+
}
302+
303+
/// Skip multitransport bootstrapping without sending any responses.
304+
///
305+
/// Use this when the application doesn't support or doesn't want UDP
306+
/// transport. The server will continue with TCP-only operation.
307+
///
308+
/// After calling this, re-feed the buffered Demand Active PDU to the
309+
/// connector (via `step()`).
310+
///
311+
/// # Panics
312+
///
313+
/// Panics if state is not [`ClientConnectorState::MultitransportPending`].
314+
pub fn skip_multitransport(&mut self) {
315+
assert!(self.should_perform_multitransport());
316+
317+
let ClientConnectorState::MultitransportPending {
318+
io_channel_id,
319+
user_channel_id,
320+
..
321+
} = self.state
322+
else {
323+
unreachable!()
324+
};
325+
326+
self.state = ClientConnectorState::CapabilitiesExchange {
327+
connection_activation: ConnectionActivationSequence::new(
328+
self.config.clone(),
329+
io_channel_id,
330+
user_channel_id,
331+
),
332+
};
333+
}
203334
}
204335

205336
impl Sequence for ClientConnector {
@@ -216,7 +347,8 @@ impl Sequence for ClientConnector {
216347
ClientConnectorState::SecureSettingsExchange { .. } => None,
217348
ClientConnectorState::ConnectTimeAutoDetection { .. } => None,
218349
ClientConnectorState::LicensingExchange { license_exchange, .. } => license_exchange.next_pdu_hint(),
219-
ClientConnectorState::MultitransportBootstrapping { .. } => None,
350+
ClientConnectorState::MultitransportBootstrapping { .. } => Some(&ironrdp_pdu::X224_HINT),
351+
ClientConnectorState::MultitransportPending { .. } => None,
220352
ClientConnectorState::CapabilitiesExchange {
221353
connection_activation, ..
222354
} => connection_activation.next_pdu_hint(),
@@ -521,6 +653,7 @@ impl Sequence for ClientConnector {
521653
ClientConnectorState::MultitransportBootstrapping {
522654
io_channel_id,
523655
user_channel_id,
656+
requests: Vec::new(),
524657
}
525658
} else {
526659
ClientConnectorState::LicensingExchange {
@@ -534,20 +667,99 @@ impl Sequence for ClientConnector {
534667
}
535668

536669
//== Optional Multitransport Bootstrapping ==//
537-
// NOTE: our implementation is not expecting the Auto-Detect Request PDU from server
670+
//
671+
// The server may send 0, 1, or 2 Initiate Multitransport Request PDUs
672+
// after licensing. These use BasicSecurityHeader (SEC_TRANSPORT_REQ)
673+
// rather than ShareControlHeader, so we peek at the first 2 bytes to
674+
// distinguish them from the Demand Active PDU that starts capabilities
675+
// exchange.
538676
ClientConnectorState::MultitransportBootstrapping {
539677
io_channel_id,
540678
user_channel_id,
541-
} => (
542-
Written::Nothing,
543-
ClientConnectorState::CapabilitiesExchange {
544-
connection_activation: ConnectionActivationSequence::new(
545-
self.config.clone(),
546-
io_channel_id,
547-
user_channel_id,
548-
),
549-
},
550-
),
679+
mut requests,
680+
} => {
681+
use ironrdp_pdu::rdp::headers::BasicSecurityHeaderFlags;
682+
683+
let ctx = crate::legacy::decode_send_data_indication(input)?;
684+
685+
// Peek at the BasicSecurityHeader flags field (first 2 bytes of
686+
// user_data) to check for SEC_TRANSPORT_REQ
687+
let is_multitransport = ctx.user_data.len() >= 2 && {
688+
let flags_raw = u16::from_le_bytes([ctx.user_data[0], ctx.user_data[1]]);
689+
BasicSecurityHeaderFlags::from_bits(flags_raw)
690+
.is_some_and(|flags| flags.contains(BasicSecurityHeaderFlags::TRANSPORT_REQ))
691+
};
692+
693+
if is_multitransport {
694+
let pdu = decode::<rdp::multitransport::MultitransportRequestPdu>(ctx.user_data)
695+
.map_err(ConnectorError::decode)?;
696+
697+
debug!(
698+
request_id = pdu.request_id,
699+
protocol = ?pdu.requested_protocol,
700+
"Received Initiate Multitransport Request"
701+
);
702+
703+
requests.push(pdu);
704+
705+
// Stay in this state to read more requests (server may send a second)
706+
(
707+
Written::Nothing,
708+
ClientConnectorState::MultitransportBootstrapping {
709+
io_channel_id,
710+
user_channel_id,
711+
requests,
712+
},
713+
)
714+
} else if !requests.is_empty() {
715+
// Server is done sending multitransport requests. Transition
716+
// to MultitransportPending so the application can establish
717+
// UDP transport(s) via should_perform_multitransport() /
718+
// complete_multitransport().
719+
//
720+
// The Demand Active PDU that triggered this branch must be
721+
// re-fed to the connector after the application calls
722+
// complete_multitransport() or skip_multitransport().
723+
info!(
724+
count = requests.len(),
725+
"Multitransport bootstrapping: pausing for application"
726+
);
727+
728+
(
729+
Written::Nothing,
730+
ClientConnectorState::MultitransportPending {
731+
io_channel_id,
732+
user_channel_id,
733+
requests,
734+
},
735+
)
736+
} else {
737+
// No multitransport requests — server went straight to
738+
// capabilities exchange. Forward the PDU.
739+
let mut connection_activation =
740+
ConnectionActivationSequence::new(self.config.clone(), io_channel_id, user_channel_id);
741+
let written = connection_activation.step(input, output)?;
742+
743+
match connection_activation.connection_activation_state() {
744+
ConnectionActivationState::ConnectionFinalization { .. } => (
745+
written,
746+
ClientConnectorState::ConnectionFinalization { connection_activation },
747+
),
748+
_ => (
749+
written,
750+
ClientConnectorState::CapabilitiesExchange { connection_activation },
751+
),
752+
}
753+
}
754+
}
755+
756+
// MultitransportPending: application should call complete_multitransport()
757+
// or skip_multitransport() instead of step()
758+
ClientConnectorState::MultitransportPending { .. } => {
759+
return Err(general_err!(
760+
"multitransport pending: call complete_multitransport() or skip_multitransport()"
761+
))
762+
}
551763

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

0 commit comments

Comments
 (0)