Repository navigation
feat(pdu): add Initiate Multitransport Request/Response PDU types - #1091
Merged
Benoît Cortier (CBenoit) merged 1 commit intoFeb 12, 2026
Conversation
Benoît Cortier (CBenoit)
merged commit Feb 12, 2026
5a50f40
into
Devolutions:master
10 checks passed
This was referenced Feb 12, 2026
Marc-André Moreau (mamoreau-devolutions)
pushed a commit
that referenced
this pull request
Jul 31, 2026
…1098) ## Summary Makes the `MultitransportBootstrapping` state functional instead of a no-op pass-through. After licensing the server may send 0, 1, or 2 Initiate Multitransport Request PDUs before capabilities exchange. Each one is surfaced to the application, which establishes UDP transport (RDPEUDP2 + TLS + RDPEMT) or declines, and the connector reports the outcome back to the server. ## API Mirrors the existing `should_perform_X()` pause-point pattern used by TLS upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than `mark_X_as_done()` because completion carries result data: - `should_perform_multitransport()`: true while a request awaits an outcome - `multitransport_request()`: the request awaiting an outcome, or `None` - `complete_multitransport(result, output)`: report the outcome, resume - `skip_multitransport(output)`: decline, resume `complete_multitransport` accepts a `MultitransportResult` (a `Success` / `Failure(hresult)` enum) rather than a caller-built response PDU. The connector builds the response internally from the stored request ID. Requests are surfaced one at a time rather than as a batch. There is no end marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act on a request as soon as it decodes one, so waiting to learn how many are coming is not an option the protocol offers. `should_perform_multitransport()` can therefore come round twice; the caller answers reliable and lossy separately. ## Approach **Routing.** Requests arrive on the negotiated MCS message channel (2.2.15.1) and the Demand Active on the I/O channel, so the channel decides which is which. The message channel also carries NetworkAutoDetect since #1348, so a decode still confirms what arrived there, but the I/O channel is never speculatively decoded as multitransport. A PDU on neither channel is an error. For the decode to be a sound confirmation the request decoder must reject a Demand Active, so this PR also tightens `MultitransportRequestPdu` to require the exact `SEC_TRANSPORT_REQ` security-header flag. **Yielding.** Each request is surfaced the moment it decodes. Responding returns the connector to `MultitransportBootstrapping` to read whatever comes next, which may be a second request or the Demand Active. Nothing is buffered and nothing is replayed: when the request is surfaced the Demand Active has not arrived yet. **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync signalling path (2.2.15.2), permitted only when both peers advertised `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The server's block is retained from the GCC exchange and checked against the client's configured flags. One rule covers both paths: - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including on `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and blocking drivers skip automatically, so without this every default client leaves a compliant server waiting. - Not negotiated: never respond. The outcome is reported in band on the new transport, and putting anything on the main channel would be the violation. The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If Soft-Sync was negotiated but no message channel exists the connector errors rather than falling back to the I/O channel, and that check runs before the pending state is taken, so the caller is left with a connector it can still inspect or decline from. ## Wire behaviour On the wire TCP and UDP negotiation happen in parallel: the UDP transport is established alongside the ongoing TCP handshake, and its completion signals the dynamic-channel layer that subsequent channels may migrate to UDP. The connector's API yield point here is a Rust affordance, not a spec-mandated TCP pause. Thanks to @hardening for the correction. ## Tests Connector state-machine tests in `ironrdp-testsuite-core` drive the public API with the shared `SERVER_DEMAND_ACTIVE` fixture: - a request is surfaced on arrival, without waiting for a following PDU (regression test for the stall); - responding returns to bootstrapping so a second request is read normally; - a third request is rejected per the 2.2.15.1 cap; - a Demand Active on the I/O channel ends bootstrapping; - the response targets the message channel, decoded back off the wire; - a `Failure` result is carried through; - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; - `complete` emits nothing without Soft-Sync but still resumes; - a failed response leaves the connector in `MultitransportPending`, still able to report or decline, rather than `Consumed`; - `complete` / `skip` outside `MultitransportPending` error; - a Demand Active's user data does not decode as a `MultitransportRequestPdu` (regression test for the decoder tightening above). Fuzzing: `MultitransportRequestPdu` and `MultitransportResponsePdu` are added to the `pdu_decode` and `pdu_round_trip` oracles, so the decode surface is fuzzed against arbitrary input (auto-discovered into the fuzz CI matrix). Both targets smoke-run clean. Full end-to-end coverage against a live server still awaits server-side `MultitransportRequestPdu` emission, which `ironrdp::server::RdpServer` does not implement yet. ## Breaking changes `cargo semver-checks` reports three, so the title carries the marker: - `ClientConnector` gains the public field `server_multitransport_flags`, so struct literals need updating (`constructible_struct_adds_field`). - `ClientConnectorState::MultitransportBootstrapping` gains `message_channel_id` and `requests_seen` (`enum_struct_variant_field_added`). - Three `ClientConnectorState` discriminants shift, because the new variants are inserted mid-enum (`enum_no_repr_variant_discriminant_changed`). The third I would call a false positive worth naming rather than designing around: `ClientConnectorState` is `#[non_exhaustive]`, has no `repr`, and every variant carries fields, so there is no `as` cast a downstream crate could have depended on. The first two are real. ## Verification - `cargo xtask check fmt -v` green - `cargo xtask check lints -v` green - `cargo xtask check tests -v` green - `cargo xtask check typos -v` green - `cargo xtask check locks -v` green ## References Builds on: #1091 Related: #140, #1348 --------- Signed-off-by: Greg Lamberson <greg@lamco.io>
Marc-André Moreau (mamoreau-devolutions)
pushed a commit
that referenced
this pull request
Aug 2, 2026
…1098) Makes the `MultitransportBootstrapping` state functional instead of a no-op pass-through. After licensing the server may send 0, 1, or 2 Initiate Multitransport Request PDUs before capabilities exchange. Each one is surfaced to the application, which establishes UDP transport (RDPEUDP2 + TLS + RDPEMT) or declines, and the connector reports the outcome back to the server. Mirrors the existing `should_perform_X()` pause-point pattern used by TLS upgrade and CredSSP, but uses `complete_X()` / `skip_X()` rather than `mark_X_as_done()` because completion carries result data: - `should_perform_multitransport()`: true while a request awaits an outcome - `multitransport_request()`: the request awaiting an outcome, or `None` - `complete_multitransport(result, output)`: report the outcome, resume - `skip_multitransport(output)`: decline, resume `complete_multitransport` accepts a `MultitransportResult` (a `Success` / `Failure(hresult)` enum) rather than a caller-built response PDU. The connector builds the response internally from the stored request ID. Requests are surfaced one at a time rather than as a batch. There is no end marker for the set, and MS-RDPBCGR 3.2.5.15.1 requires the client to act on a request as soon as it decodes one, so waiting to learn how many are coming is not an option the protocol offers. `should_perform_multitransport()` can therefore come round twice; the caller answers reliable and lossy separately. **Routing.** Requests arrive on the negotiated MCS message channel (2.2.15.1) and the Demand Active on the I/O channel, so the channel decides which is which. The message channel also carries NetworkAutoDetect since so a decode still confirms what arrived there, but the I/O channel is never speculatively decoded as multitransport. A PDU on neither channel is an error. For the decode to be a sound confirmation the request decoder must reject a Demand Active, so this PR also tightens `MultitransportRequestPdu` to require the exact `SEC_TRANSPORT_REQ` security-header flag. **Yielding.** Each request is surfaced the moment it decodes. Responding returns the connector to `MultitransportBootstrapping` to read whatever comes next, which may be a second request or the Demand Active. Nothing is buffered and nothing is replayed: when the request is surfaced the Demand Active has not arrived yet. **Soft-Sync.** The Initiate Multitransport Response is the Soft-Sync signalling path (2.2.15.2), permitted only when both peers advertised `SOFTSYNC_TCP_TO_UDP` in their GCC `MultiTransportChannelData`. The server's block is retained from the GCC exchange and checked against the client's configured flags. One rule covers both paths: - Soft-Sync negotiated: always respond, `S_OK` or `E_ABORT`, including on `skip_multitransport()`, which 3.2.5.15.1 requires. Both the async and blocking drivers skip automatically, so without this every default client leaves a compliant server waiting. - Not negotiated: never respond. The outcome is reported in band on the new transport, and putting anything on the main channel would be the violation. The response goes on the message channel per 2.2.15.2 and 3.2.5.15.2. If Soft-Sync was negotiated but no message channel exists the connector errors rather than falling back to the I/O channel, and that check runs before the pending state is taken, so the caller is left with a connector it can still inspect or decline from. On the wire TCP and UDP negotiation happen in parallel: the UDP transport is established alongside the ongoing TCP handshake, and its completion signals the dynamic-channel layer that subsequent channels may migrate to UDP. The connector's API yield point here is a Rust affordance, not a spec-mandated TCP pause. Thanks to @hardening for the correction. Connector state-machine tests in `ironrdp-testsuite-core` drive the public API with the shared `SERVER_DEMAND_ACTIVE` fixture: - a request is surfaced on arrival, without waiting for a following PDU (regression test for the stall); - responding returns to bootstrapping so a second request is read normally; - a third request is rejected per the 2.2.15.1 cap; - a Demand Active on the I/O channel ends bootstrapping; - the response targets the message channel, decoded back off the wire; - a `Failure` result is carried through; - `skip` sends `E_ABORT` under Soft-Sync, and nothing without it; - `complete` emits nothing without Soft-Sync but still resumes; - a failed response leaves the connector in `MultitransportPending`, still able to report or decline, rather than `Consumed`; - `complete` / `skip` outside `MultitransportPending` error; - a Demand Active's user data does not decode as a `MultitransportRequestPdu` (regression test for the decoder tightening above). Fuzzing: `MultitransportRequestPdu` and `MultitransportResponsePdu` are added to the `pdu_decode` and `pdu_round_trip` oracles, so the decode surface is fuzzed against arbitrary input (auto-discovered into the fuzz CI matrix). Both targets smoke-run clean. Full end-to-end coverage against a live server still awaits server-side `MultitransportRequestPdu` emission, which `ironrdp::server::RdpServer` does not implement yet. `cargo semver-checks` reports three, so the title carries the marker: - `ClientConnector` gains the public field `server_multitransport_flags`, so struct literals need updating (`constructible_struct_adds_field`). - `ClientConnectorState::MultitransportBootstrapping` gains `message_channel_id` and `requests_seen` (`enum_struct_variant_field_added`). - Three `ClientConnectorState` discriminants shift, because the new variants are inserted mid-enum (`enum_no_repr_variant_discriminant_changed`). The third I would call a false positive worth naming rather than designing around: `ClientConnectorState` is `#[non_exhaustive]`, has no `repr`, and every variant carries fields, so there is no `as` cast a downstream crate could have depended on. The first two are real. - `cargo xtask check fmt -v` green - `cargo xtask check lints -v` green - `cargo xtask check tests -v` green - `cargo xtask check typos -v` green - `cargo xtask check locks -v` green Builds on: #1091 Related: #140, #1348 --------- Signed-off-by: Greg Lamberson <greg@lamco.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add MultitransportRequestPdu and MultitransportResponsePdu types for the
sideband UDP transport bootstrapping PDUs defined in [MS-RDPBCGR] 2.2.15.1
and 2.2.15.2. Needed to decode/encode the IO channel messages that initiate
UDP transport setup.
See #140 for context.