Skip to content

feat(server): add accept_finalize_with_multitransport driver - #1953

Merged
Benoît Cortier (CBenoit) merged 4 commits into
Devolutions:masterfrom
lamco-admin:feat/acceptor-multitransport-finalize
Sep 28, 2026
Merged

Benoît Cortier (CBenoit) merged 4 commits into
Devolutions:masterfrom
lamco-admin:feat/acceptor-multitransport-finalize

Conversation

@glamberson

@glamberson Greg Lamberson (glamberson) commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add accept_finalize_with_multitransport, an async driver mirroring the client side's connect_finalize_with_multitransport.
  • It drives the acceptor sequence to completion exactly like accept_finalize already does, and awaits an app-supplied handler once, synchronously, the moment the acceptor sends an Initiate Multitransport Request, so the caller can establish the sideband UDP transport (RDPEUDP2 + TLS + RDPEMT).
  • Unlike the client-side callback, the handler reports nothing back into the sequence: the acceptor has already sent the request and moved on by the time it runs.
  • accept_finalize becomes a thin wrapper around this with a no-op handler, matching the client side's connect_finalize/connect_finalize_with_multitransport relationship.

Validation

cargo xtask check fmt/lints/tests/typos/locks all pass.

Added two integration tests to ironrdp-testsuite-core/tests/server/multitransport_finalize.rs, driving a real Acceptor over a tokio::io::duplex pair with a hand-rolled client script: the handler fires exactly once with the sent request without blocking the handshake, and it does not fire again across a Deactivation-Reactivation round. Writing the first test caught a real bug in the initial implementation (a take_multitransport_request() that cleared the same field CapabilitiesWaitConfirm's response tolerance depends on); fixed before this PR's only commit.

Notes

The driver seeds its "already notified" tracking from whatever request is already present rather than false, since new_deactivation_reactivation carries the original request forward without re-running bootstrapping.

One PR is stacked on this

#1954 (ironrdp-server wiring, consuming accept_finalize_with_multitransport) is stacked on this branch. Its diff is filed against master and is cumulative with this one; see its own body for the incremental compare.

@github-actions github-actions Bot added breaking-change Includes a breaking change, and requires special scrutiny at the boundaries triage/overlap Possible overlap with another pull request; advisory only kind/protocol Affects RDP or related protocol behavior needs-review A human reviewer is the current next actor risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Potential duplicate detected: #1951.

PR #1951 describes the identical feature: a MultitransportBootstrapping acceptor state after licensing, set_multitransport_offer config, GCC Server MultiTransportChannelData advertisement, Initiate Multitransport Request on the MCS message channel, and tests in tests/server/acceptor.rs. This diff matches point-for-point under a different head SHA.

Maintainer review is required.

Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 11, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds opt-in server-side UDP multitransport bootstrapping: a post-licensing MultitransportBootstrapping state that sends an Initiate Multitransport Request on the MCS message channel when configured and reciprocated, GCC advertisement of Server MultiTransportChannelData, tolerance for a late response in CapabilitiesWaitConfirm, and an accept_finalize_with_multitransport driver mirroring the client side, plus sync and duplex-stream tests. The design is coherent, defaults preserve existing behavior, and the renotification seeding for reactivation is correct. Published findings: an unverified response-ordering assumption on the mandatory soft-sync path (raised as a question), advertisement not gated on the client's block per MS-RDPBCGR 2.2.1.4.6, a stale doc claim about reactivation, and test-layer duplication/coverage gaps.

Comment thread crates/ironrdp-acceptor/src/connection.rs Outdated
Comment thread crates/ironrdp-acceptor/src/connection.rs
Comment thread crates/ironrdp-testsuite-core/tests/server/acceptor.rs Outdated
Comment thread crates/ironrdp-testsuite-core/tests/server/multitransport_finalize.rs Outdated
Comment thread crates/ironrdp-testsuite-core/tests/server/multitransport_finalize.rs Outdated
Comment thread crates/ironrdp-testsuite-core/tests/server/acceptor.rs
Comment thread crates/ironrdp-acceptor/src/connection.rs Outdated
@github-actions github-actions Bot added ai-reviewed/1 One automated review completed and removed needs-review A human reviewer is the current next actor labels Sep 11, 2026
@glamberson
Greg Lamberson (glamberson) force-pushed the feat/acceptor-multitransport-finalize branch from 7e7919d to e2925a7 Compare September 11, 2026 23:08
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 11, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
@github-actions github-actions Bot added risk/medium Behavioral change that does not substantially alter a core public API size/XL Size: up to 1299 counted lines and 49 files; exceeds L in either measure and removed risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure breaking-change Includes a breaking change, and requires special scrutiny at the boundaries labels Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

This pull request may overlap with #1954.

This PR adds accept_finalize_with_multitransport plus the acceptor multitransport finalization handling and tests. PR 1954's own description says its cumulative diff includes the acceptor multitransport finalize work (branch feat/acceptor-multitransport-finalize) and that accept_finalize uses accept_finalize_with_multitransport there. Shared scope: the acceptor-side multitransport finalize API and its tests, with 1954 additionally wiring it into ironrdp-server.

This notice is advisory only. Automated review continues as usual, and how these pull requests relate is for maintainers and authors to decide.

Note

LLM-assisted content (no human feedback).

Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 12, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds acceptor-side UDP multitransport bootstrapping: a Server MultiTransportChannelData block gated on the client's own block plus message channel, a MultitransportBootstrapping state sending the Initiate Multitransport Request after licensing, late-response tolerance in CapabilitiesWaitConfirm and ConnectionFinalization, and an accept_finalize_with_multitransport driver notifying a handler once per request, with sync and async integration tests. The implementation is sound: the guard's strict TRANSPORT_RSP decode rules out misclassifying auto-detect/heartbeat traffic, the handler-notified seeding is correct across reactivation, and the GCC block gating matches MS-RDPBCGR 2.2.1.4. Five low-severity findings published: a corrected initiator-channel maintainability nit, a spec-forbidden S_OK in the new async test, and three compression cleanups. No correctness defects.

  1. [code-compressor] Finalization arm re-decodes input on every step before finalization decodes it again — low 🟡 — crates/ironrdp-acceptor/src/connection.rs
    Every ConnectionFinalization step decodes the full X224<mcs::McsMessage> solely to test for a late multitransport response, then finalization.step re-decodes the same bytes internally. For connections where no multitransport request was ever sent (the default configuration) the pre-check is guaranteed false, yet the outstanding-request early-return in late_multitransport_response only runs after the outer decode succeeds, so every client PDU still pays a full TPKT/X.224/MCS parse. Minimal shape with no API churn: gate the pre-check on self.sent_multitransport_request.is_some() before decoding. Full decode deduplication would require widening the exported FinalizationSequence::step byte-slice contract and is not worth the negligible CPU.

Comment thread crates/ironrdp-acceptor/src/connection.rs
Comment thread crates/ironrdp-testsuite-core/tests/server/multitransport_finalize.rs Outdated
Comment thread crates/ironrdp-acceptor/src/connection.rs Outdated
Comment thread crates/ironrdp-testsuite-core/tests/server/acceptor.rs Outdated
@github-actions github-actions Bot added ai-reviewed/2 Final automated review completed and removed ai-reviewed/1 One automated review completed labels Sep 12, 2026
@github-actions github-actions Bot added the needs-review A human reviewer is the current next actor label Sep 12, 2026
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 22, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 22, 2026
Fixes a high-severity bug: after the sideband UDP tunnel closes, the
shared transport handle now gets cleared so dispatch_egfx_messages
actually falls back to TCP instead of silently dropping every
subsequent EGFX batch onto a dead connection.

Documents an accepted timing limitation: a late Initiate
Multitransport Response arriving after finalization completes cannot
retroactively enable Soft-Sync migration, since nothing on the message
channel recognizes it post-handoff. This degrades to TCP-only for the
session rather than causing any correctness issue.

Inherits the S_OK/SOFTSYNC test fix from PR Devolutions#1953 by rebasing onto its
review-response commit, reconciling the resulting connection.rs
conflict between that PR's bool-returning rename and this branch's own
earlier &mut self change for response tracking.

Addresses three low-severity findings: removes an unused accessor,
substitutes an equivalent enum match with the existing tls_acceptor()
helper, and reuses get_svc_processor() instead of inlining its body.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 24, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 24, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 24, 2026
Fixes a high-severity bug: after the sideband UDP tunnel closes, the
shared transport handle now gets cleared so dispatch_egfx_messages
actually falls back to TCP instead of silently dropping every
subsequent EGFX batch onto a dead connection.

Documents an accepted timing limitation: a late Initiate
Multitransport Response arriving after finalization completes cannot
retroactively enable Soft-Sync migration, since nothing on the message
channel recognizes it post-handoff. This degrades to TCP-only for the
session rather than causing any correctness issue.

Inherits the S_OK/SOFTSYNC test fix from PR Devolutions#1953 by rebasing onto its
review-response commit, reconciling the resulting connection.rs
conflict between that PR's bool-returning rename and this branch's own
earlier &mut self change for response tracking.

Addresses three low-severity findings: removes an unused accessor,
substitutes an equivalent enum match with the existing tls_acceptor()
helper, and reuses get_svc_processor() instead of inlining its body.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 25, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 25, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 25, 2026
Fixes a high-severity bug: after the sideband UDP tunnel closes, the
shared transport handle now gets cleared so dispatch_egfx_messages
actually falls back to TCP instead of silently dropping every
subsequent EGFX batch onto a dead connection.

Documents an accepted timing limitation: a late Initiate
Multitransport Response arriving after finalization completes cannot
retroactively enable Soft-Sync migration, since nothing on the message
channel recognizes it post-handoff. This degrades to TCP-only for the
session rather than causing any correctness issue.

Inherits the S_OK/SOFTSYNC test fix from PR Devolutions#1953 by rebasing onto its
review-response commit, reconciling the resulting connection.rs
conflict between that PR's bool-returning rename and this branch's own
earlier &mut self change for response tracking.

Addresses three low-severity findings: removes an unused accessor,
substitutes an equivalent enum match with the existing tls_acceptor()
helper, and reuses get_svc_processor() instead of inlining its body.
Marc-André Moreau (mamoreau-devolutions) pushed a commit that referenced this pull request Sep 28, 2026
## Summary

- Add a `MultitransportBootstrapping` state to the acceptor sequence,
entered right after licensing.
- Advertise UDP multitransport support in the GCC Server
MultiTransportChannelData block via a new `set_multitransport_offer()`
config (previously always `None`, so a server could never enable it),
gated on the client having populated its own Client
MultiTransportChannelData block (MS-RDPBCGR 2.2.1.4).
- When the client reciprocates the reliable-UDP flag, send the Initiate
Multitransport Request (MS-RDPBCGR 2.2.15.1) on the MCS message channel,
then move straight on to capability negotiation.
- `multitransport_request()` surfaces the sent request so the caller can
establish the sideband UDP transport (RDPEUDP2 + TLS + RDPEMT) in
parallel, without the acceptor waiting for the client's response first.

## Validation

`cargo xtask check fmt/lints/tests/typos/locks` all pass.

9 tests in `ironrdp-testsuite-core/tests/server/acceptor.rs` driving the
full handshake through a real MCS channel join sequence:
offered-and-reciprocated (request sent on the message channel, response
tolerated before Confirm Active), disabled by default,
client-does-not-reciprocate, a late response tolerated during
ConnectionFinalization (not just before Confirm Active), non-response
message-channel traffic (Auto-Detect Response, Heartbeat) not
misclassified as a multitransport response, a response with trailing
bytes not consumed, security cookie and request ID taken from the
injected RNG, and an offer changed after Basic Settings Exchange
(request and Soft-Sync) not affecting the negotiation already
advertised.

## Notes

The acceptor does not wait for the client's Initiate Multitransport
Response before continuing: MS-RDPBCGR 3.2.5.15.1 only obliges the
client to send one when Soft-Sync is negotiated or the sideband attempt
failed, so blocking on it would stall the handshake on the plain
successful path. A response can legitimately arrive either before the
mandatory Confirm Active or after it, during ConnectionFinalization:
both `CapabilitiesWaitConfirm` and `ConnectionFinalization` recognize it
(by channel and a successful strict decode of the payload, so other
message-channel traffic like Auto-Detect Response or Heartbeat correctly
falls through instead) and drop it rather than erroring or desyncing the
finalization sequence.

Only reliable UDP (`TRANSPORT_TYPE_UDP_FECR`) is requested; lossy UDP is
accepted in the offered flags for advertisement but never requested.
Server-side wiring (`accept_finalize_with_multitransport` and
`ironrdp-server` integration) is a follow-up.

## One PR is stacked on this

#1953 (`accept_finalize_with_multitransport`, an async driver consuming
this PR's API) is stacked on this branch. Its diff is filed against
`master` and is cumulative with this one; see its own body for the
incremental compare.
@mamoreau-devolutions

Copy link
Copy Markdown
Contributor

Greg Lamberson (@glamberson) this PR needs a rebase

Add an async driver mirroring the client side's
connect_finalize_with_multitransport: it drives the acceptor sequence
to completion the same way accept_finalize already does, and awaits
an app-supplied handler once, synchronously, the moment the acceptor
sends an Initiate Multitransport Request, so the caller can establish
the sideband UDP transport (RDPEUDP2 + TLS + RDPEMT).

Unlike the client-side callback, the handler reports nothing back into
the sequence: the acceptor has already sent the request and moved on
by the time the handler runs, so there is no response to build from an
outcome. The handler should return promptly (e.g. by spawning the
actual work) rather than driving the transport to completion inline,
or the handshake stalls behind it. accept_finalize becomes a thin
wrapper around this with a no-op handler, matching the client side's
connect_finalize/connect_finalize_with_multitransport relationship.

The driver seeds its "already notified" tracking from whatever request
is already present rather than starting at false: a Deactivation-
Reactivation Sequence rebuilds the acceptor via
new_deactivation_reactivation(), which carries the original request
forward without running bootstrapping again, then this function is
called a second time on the rebuilt acceptor. Without the seed, that
second call's first loop iteration would treat the carried-over
request as newly sent and notify the handler again.

Adds integration tests in ironrdp-testsuite-core driving a real
Acceptor over a tokio::io::duplex pair with a hand-rolled client
script: the handler fires exactly once with the sent request and does
not block the handshake, a late Initiate Multitransport Response is
still tolerated ahead of Confirm Active during the async-driven path,
and the handler does not fire again across a reactivation round.
Building the first of these surfaced a real bug: an initial
take_multitransport_request() consumed the same field
CapabilitiesWaitConfirm's response tolerance depends on, breaking that
check the moment the driver read the request. Removed in favor of the
local flag, keeping multitransport_request() a plain borrow.
Four of the eight findings were already resolved by the rebase onto
Devolutions#1951's own review-response commit: the late-response tolerance now
applies uniformly to every FinalizationSequence sub-state, the server
MultiTransportChannelData block is filtered on the client's own block
presence, and two stale doc comments were already corrected.

For the remaining four, deduplicated the MCS SendDataRequest encoder
and the client GCC-block builder between acceptor.rs and
multitransport_finalize.rs (both made pub(super) and reused), extracted
a shared play_confirm_active_and_finalization helper covering the
Confirm Active plus four-PDU finalization exchange that play_client and
play_reactivation_round both repeated, and extracted a recording_handler
factory removing duplicated Arc::clone-into-closure plumbing across the
two handler tests. play_client now also returns the request_id it
decoded so the first handler test can assert the handler received the
exact request the acceptor sent, rather than recording it unread. Added
the missing assertion to multitransport_not_offered_by_default, whose
doc comment claimed the server's GCC advertisement was checked when
nothing actually was.
- Document that USER_CHANNEL_ID doubles as the fixed MCS server channel
  ID (MS-RDPBCGR 3.3.1.5) that every server-to-client Send Data
  Indication in this file relies on.
- Fix the finalize integration test to send an abort response instead
  of S_OK when SOFTSYNC_TCP_TO_UDP was not negotiated (MS-RDPBCGR
  2.2.15.2), matching the existing pattern in acceptor.rs.
- Rename late_multitransport_response to
  is_late_multitransport_response and return bool instead of an
  Option<PDU> neither caller reads.
- Simplify multitransport_acceptor to pass its Option straight through
  to set_multitransport_offer instead of re-wrapping it.
The doc comment still described the earlier Option-returning shape
("logs it ... and returns it"); it now states what true and false mean
for the caller.
@glamberson
Greg Lamberson (glamberson) force-pushed the feat/acceptor-multitransport-finalize branch from e37faf4 to 7b7c30b Compare September 28, 2026 04:32
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Fixes a high-severity bug: after the sideband UDP tunnel closes, the
shared transport handle now gets cleared so dispatch_egfx_messages
actually falls back to TCP instead of silently dropping every
subsequent EGFX batch onto a dead connection.

Documents an accepted timing limitation: a late Initiate
Multitransport Response arriving after finalization completes cannot
retroactively enable Soft-Sync migration, since nothing on the message
channel recognizes it post-handoff. This degrades to TCP-only for the
session rather than causing any correctness issue.

Inherits the S_OK/SOFTSYNC test fix from PR Devolutions#1953 by rebasing onto its
review-response commit, reconciling the resulting connection.rs
conflict between that PR's bool-returning rename and this branch's own
earlier &mut self change for response tracking.

Addresses three low-severity findings: removes an unused accessor,
substitutes an equivalent enum match with the existing tls_acceptor()
helper, and reuses get_svc_processor() instead of inlining its body.
@github-actions github-actions Bot added size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure and removed size/XXL Size: 1300 or more counted lines or 50 or more files breaking-change Includes a breaking change, and requires special scrutiny at the boundaries labels Sep 28, 2026
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Fixes a high-severity bug: after the sideband UDP tunnel closes, the
shared transport handle now gets cleared so dispatch_egfx_messages
actually falls back to TCP instead of silently dropping every
subsequent EGFX batch onto a dead connection.

Documents an accepted timing limitation: a late Initiate
Multitransport Response arriving after finalization completes cannot
retroactively enable Soft-Sync migration, since nothing on the message
channel recognizes it post-handoff. This degrades to TCP-only for the
session rather than causing any correctness issue.

Inherits the S_OK/SOFTSYNC test fix from PR Devolutions#1953 by rebasing onto its
review-response commit, reconciling the resulting connection.rs
conflict between that PR's bool-returning rename and this branch's own
earlier &mut self change for response tracking.

Addresses three low-severity findings: removes an unused accessor,
substitutes an equivalent enum match with the existing tls_acceptor()
helper, and reuses get_svc_processor() instead of inlining its body.

@CBenoit Benoît Cortier (CBenoit) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@CBenoit
Benoît Cortier (CBenoit) merged commit 22006ce into Devolutions:master Sep 28, 2026
41 checks passed
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Depends on Devolutions#1953 (stacked branch, feat/acceptor-multitransport-finalize).

Add RdpServerBuilder::with_udp_transport(udp_bind_addr), opt-in and
None by default. When set (and the security mode is Tls or Hybrid,
matching the reference client's Enhanced-Security-only gate), the
acceptor offers reliable UDP multitransport, and accept_finalize uses
accept_finalize_with_multitransport with a callback that binds a fresh
UDP socket per connection, reuses the connection's own TLS certificate
(TlsAcceptor::config()) for the sideband transport, and calls
accept_udp(). Any failure at any stage falls back to TCP-only, never
fails the connection.

Once established, the transport is used to migrate EGFX graphics
traffic off TCP: request_reliable_udp is called opportunistically the
first time EGFX has data to send (its dynamic channel id is only known
once the client opens it, well after multitransport bootstrapping),
and outgoing EGFX SvcMessages route onto the tunnel via
encode_unframed_pdu() once the client has acknowledged the Soft-Sync
request. A new client_loop select arm feeds incoming tunnel payloads
into DrdynvcServer::process_tunnel(), whose responses go back over
TCP. A closed transport degrades the arm to pending forever rather
than ending the session, matching the non-fatal posture throughout.

Adds a regression test verifying that configuring UDP transport on the
server does not disturb a client that never advertises support for
it (the common case for any client that predates this feature).
Greg Lamberson (glamberson) added a commit to lamco-admin/IronRDP that referenced this pull request Sep 28, 2026
Fixes three high-severity issues: Soft-Sync now requires both a
negotiated SOFT_SYNC_TCP_TO_UDP flag and a successful Initiate
Multitransport Response before migrating any channel, the shared UDP
transport handle exposes a lock-free sender independent of its
receive-side mutex, and the finalize handler no longer blocks the RDP
handshake on the UDP accept, spawning it instead and picking it up
opportunistically from client_loop's own select loop once it resolves.

Fixes a medium-severity bug in ironrdp-dvc's Soft-Sync response handling:
a declined channel stayed routed for outgoing data because the outgoing
tunnel map was never filtered by the response, only the incoming one.

Addresses four low-severity findings: corrects a false single-connection
premise in the UDP accept doc comment, documents the AddrInUse tradeoff
under session preemption, combines a duplicated drdynvc guard into one
failure path, and confirms two findings already resolved by rebasing
onto PR Devolutions#1953's own review-response commit.

This branch was successfully deployed

1 active deployment
llm-providers — 7b7c30b8 Deployed Sep 28, 2026 by glamberson via Classify pull request #815
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/2 Final automated review completed kind/protocol Affects RDP or related protocol behavior needs-review A human reviewer is the current next actor risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure triage/overlap Possible overlap with another pull request; advisory only

Development

Successfully merging this pull request may close these issues.

3 participants