feat(server): wire UDP multitransport into ironrdp-server - #1954
Benoît Cortier (CBenoit) merged 14 commits into
Conversation
|
Potential duplicate detected: #1951. The acceptor portion (MultitransportBootstrapping state, set_multitransport_offer, GCC advertisement, message-channel request, acceptor.rs tests) matches Maintainer review is required. |
There was a problem hiding this comment.
The acceptor-level multitransport work is sound (state machine, message-channel gating, GCC advertisement, response tolerance, tests), but the runtime integration has three significant defects: EGFX Soft-Sync is initiated without advertising SOFTSYNC_TCP_TO_UDP or awaiting a successful Initiate Multitransport Response (violates MS-RDPEDYC/MS-RDPBCGR), UdpTransportHandle::recv holds the tokio Mutex across the idle await and can deadlock sends, and the finalize handler awaits the full 15s UDP accept inline, stalling the handshake. Also published: the routing gate treats a declining SoftSyncResponse as acceptance, the false one-connection-at-a-time premise under preemption, unconditional UDP dependencies for an opt-in feature, and two minor cleanup/test-duplication items. Two optional compression proposals (defensive finalize guard, folding the bootstrapping state) were rejected.
03037a4 to
5f5e9bb
Compare
There was a problem hiding this comment.
PR wires opt-in UDP multitransport through the acceptor (MultitransportBootstrapping state, late-response tolerance, accept_finalize_with_multitransport), a server-side RDPEUDP2+TLS+RDPEMT accept path, and soft-sync-gated EGFX migration, all default-off behind with_udp_transport. The state machine, GCC gating, strict response decoding, and the dvc outgoing-map fix check out. Published: a high-severity silent EGFX black-hole after mid-session tunnel closure (send failures unhandled, no TCP fallback), a migration gate frozen at acceptance that late responses can never enable, an integration test whose scripted client violates the S_OK/SOFTSYNC rule, and low-severity cleanup items (unused accessor, unconditional deps, two duplications). Rejected the speculative auto-detect swallow finding: the response decoder strictly validates SEC_TRANSPORT_RSP, so mismatched message-channel traffic fails decode and falls through.
- [code-compressor] Three new hard dependencies back only an opt-in runtime path — low 🟡 — crates/ironrdp-server/Cargo.toml
ironrdp-rdpemt, ironrdp-rdpeudp, and ironrdp-rdpeudp-tokio are unconditional dependencies yet are consumed solely by src/multitransport.rs, whose runtime path only executes when udp_bind_addr is Some (None by default). Marking them optional behind a dedicated/egfx feature with #[cfg] pairs on the module and its plumbing would shrink the default build with identical runtime behavior; the author acknowledges the mechanical breadth in review.
0be1959 to
5a788d2
Compare
5a788d2 to
4a38668
Compare
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).
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.
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.
The UDP handshake is started with spawn_local, so with_udp_transport makes run, run_connection and run_connection_with panic unless they are driven inside a tokio LocalSet. State that on with_udp_transport and on each entry point, and correct the code comment that said run already documented it.
A client that answers the Initiate Multitransport Request with E_ABORT has given up on the sideband transport (MS-RDPBCGR 2.2.15.2), yet the server kept its UDP accept running and its socket bound until the 15 s timeout, then logged a handshake timeout. Seen with a Windows client. Abort the pending accept once finalization reports the failure; with no response at all it keeps running, since the client may still connect.
Windows clients answer the Initiate Multitransport Request with E_ABORT about 2.7 s after connecting, after finalization has completed, so the earlier check never saw it and the accept still ran to its 15 s timeout. Now built on Devolutions#1964, which decodes that late response on the message channel: keep the pending accept's abort handle and stop the accept when the response reports failure, and report a cancelled accept as a decline rather than a panic.
…eached A socket bound to the unspecified address replies from whichever local address the routing table picks. On a host with several IPv6 addresses that was not the one mstsc sent to, so mstsc dropped the replies and gave up on the handshake with E_ABORT. When the configured UDP address is unspecified, bind to the connection's local address instead: run() records it, and embedders driving run_connection_with pass it through the new RdpServer::set_connection_local_addr.
mstsc finishes its UDP bootstrap after the TCP finalization, so its successful Initiate Multitransport Response arrives on the message channel once the client loop is running. Migration was decided once at finalization and never revisited, so the sideband transport came up and EGFX stayed on TCP for the whole session. Keep the decision on the connection instead: finalization sets it as before, and a later success enables it when Soft-Sync was negotiated.
An embedder that takes a frame handle from its GfxServerFactory has the channel registered as GfxDvcBridge, which the migration lookup did not recognise, so the Soft-Sync Request was never sent and EGFX stayed on TCP with the sideband transport up. Look the channel up as either type. Log the Soft-Sync Request, the client's response with the tunnels and channels it accepted, and the switch of EGFX onto UDP, which were silent.
The server now routes EGFX over the tunnel from the Soft-Sync Request on, since the request carries SOFT_SYNC_TCP_FLUSHED and MS-RDPEDYC 3.3.5.3.1 requires the server to keep using the named tunnel immediately after sending it. The Soft-Sync Response only narrows what the server reads (2.2.5.2), so it no longer changes the outgoing tunnel. Tunnel data that reaches the server before the Response is held and processed once it arrives (3.3.5.3.2) instead of being dropped, and DRDYNVC replies for a tunneled channel go over the tunnel whichever path produced them. The UDP accept is spawned with tokio::spawn, so the server no longer needs a LocalSet. UdpTransportHandle::send returns nothing, and the tunnel loop reads the transport handle once. Adds an end-to-end test that brings up the tunnel with a real client and checks all three routing rules; ironrdp-testsuite-extra now enables ironrdp-server's egfx feature for it.
The server's run future grew past clippy::large_futures' 16 KiB limit on Windows (16,440 bytes) with the DRDYNVC tunnel routing, failing the workspace lint there. Box it in the example rather than keep a future that size in main's stack frame.
Soft-Sync only moves channels onto a tunnel (MS-RDPEDYC 2.2.5.1 has no TCP tunnel type), its request promises no more of their data over TCP, and the tunnel lasts as long as the connection (MS-RDPEMT 1.3.3). Sending EGFX over TCP after the tunnel closed therefore reached a client with nowhere to read it: mstsc froze and reset the connection about 19 s later. With EGFX on the tunnel, the connection now ends when the tunnel does. With nothing moved, the session stays on TCP as before.
|
Thanks for the review. On Soft-Sync being one-shot, that is the design, and the docs now say so. The request declares the TCP path flushed for the channel (SOFT_SYNC_TCP_FLUSHED, MS-RDPEDYC 2.2.5.1) and the server sends its data over the tunnel from then on (3.3.5.3.1), so a decline in the response no longer means EGFX stays on TCP: The response only lists the tunnels the client will write on (2.2.5.2). The spec has no path for taking a Soft-Sync back or offering a second one, so I did not add a re-request. The doc on dispatch_egfx_messages no longer calls the repeat request a no-op. It says the request is made once per connection and that the error from a later attempt is expected and only traced, and request_reliable_udp now says the same. |
|
Thanks for the second look at this. Gating the three UDP crates behind an optional feature is feasible: A stub UdpTransportHandle for the feature-off build would keep the signatures that carry it unchanged, so only the module, the builder method and the accept call become conditional. It also brings a default-on or default-off choice for the feature, so I am leaving it out of this PR. If it is wanted it can be done in another PR. |
The doc on dispatch_egfx_messages called a repeat request a no-op under request_reliable_udp's idempotency guard, but the guard returns an error that the caller only traces. Both docs now say the request is made once per connection, that the state never returns to idle, and that the request declares the TCP path flushed for the channel (SOFT_SYNC_TCP_FLUSHED, MS-RDPEDYC 2.2.5.1) so its data goes over the tunnel from then on (3.3.5.3.1), whatever the client's response lists.
Summary
RdpServerBuilder::with_udp_transport(udp_bind_addr): opt-in,Noneby default, no behavior change unless called.TlsorHybrid, matching the reference client's Enhanced-Security-only gate), the acceptor offers UDP multitransport, andaccept_finalizeusesaccept_finalize_with_multitransportwith a callback that binds a fresh UDP socket per connection, reuses the connection's own TLS certificate (TlsAcceptor::config()) for the sideband transport, and callsaccept_udp().request_reliable_udpis called opportunistically the first time EGFX has data to send (its dynamic channel id is only known once the client opens it). From the request on, every server message on that channel goes over the tunnel, starting with the batch that triggered it: the request carries SOFT_SYNC_TCP_FLUSHED and the server MUST keep using the named tunnel immediately after sending it (MS-RDPEDYC 2.2.5.1, 3.3.5.3.1). DRDYNVC replies for a tunneled channel go over the tunnel too, whichever path produced them. A newclient_loopselect arm feeds incoming tunnel payloads intoDrdynvcServer::process_tunnel(); payloads that arrive before the client's Soft-Sync Response are held and processed once it does (3.3.5.3.2), rather than dropped.tokio::spawntask, so enabling UDP adds no runtime requirement for the caller.run()records the address itself; embedders drivingrun_connection_withpass it with the newRdpServer::set_connection_local_addr.GfxServerFactoryregisters it asGfxDvcBridge, which the migration lookup did not recognise, so it never sent the Soft-Sync Request. The Soft-Sync Request, the client's response (with the tunnels and channels it accepted) and the switch of EGFX onto UDP are now logged at debug level.Validation
cargo xtask check fmt/lints/tests/typos/locksall pass, including--features egfxspecifically (the new code has real feature-gated branches).Two tests in
ironrdp-testsuite-extra. The first confirms that configuring UDP transport on the server does not disturb a client that never advertises support for it (the common case for any client predating this feature). The second,egfx_moves_onto_the_udp_tunnel_with_soft_sync, drives a real client throughconnect_finalize_with_multitransportand the full RDPEUDP2 handshake, then checks that the EGFX batch that triggers the Soft-Sync Request goes over the tunnel, that client tunnel data arriving ahead of the Soft-Sync Response is processed, and that the reply to it comes back over the tunnel. The test enablesironrdp-server'segfxfeature as a dev-dependency, so the EGFX-over-UDP path is now compiled in CI.Notes
One thing caught and fixed while implementing this, not left in the diff:
SvcMessage(the SoftSyncRequest PDU) thatrequest_reliable_udp()returns, meaning the server would decide to migrate but never actually tell the client. Fixed to encode and send it over TCP.