fix(connector): stay in CapabilitiesExchange when activation handles DeactivateAll#1371
Conversation
…DeactivateAll PR Devolutions#1254 taught the inner ConnectionActivationSequence to skip a Server Deactivate All PDU received before Server Demand Active (sent by e.g. Windows Server and gnome-remote-desktop during a Deactivation-Reactivation Sequence, MS-RDPBCGR 1.3.1.3): it returns Written::Nothing and stays in CapabilitiesExchange. However, the outer ClientConnector::step() only accepted ConnectionFinalization as the resulting inner state, so the same scenario still failed with "invalid state (this is a bug)" during the initial connection sequence. Mirror the inner behavior in the outer state machine and keep waiting for the Demand Active PDU. Fixes Devolutions#1362 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Rocco De Angelis (@rdeangel) thank you for looking into this! I've tried compiling again from your branch and it does get past the |
|
Thanks for testing again, good to see it's getting past ServerDeactivateAll now. From the error location, the connection is getting quite far: it's past NLA and licensing, into capabilities exchange. It handles the ServerDeactivateAll and waits for a ServerDemandActive, but the server sends a disconnectProviderUltimatum and drops instead. So I don't think this PR is causing the disconnect, it just lets the connection get far enough to reach it, where before it stopped at the "invalid state" error, but I'd rather not guess at why the server is dropping without more to go on. A couple of things that would really help narrow it down:
One small note: the UserRequested name is a little misleading, it's just the generic MCS code for a deliberate close, not necessarily anything a user did. Separately, this did make me wonder if it'd be worth surfacing DisconnectProviderUltimatum as a typed error instead of a string, so it's easier to handle distinctly but that's independent of this PR. |
|
That makes sense, to help narrow it down:
|
|
Here's what I see in it: there's a single TCP connection that completes the TLS handshake and NLA, gets through the capabilities exchange, and then the server closes it (it sends the FIN). The client never reconnects. That pattern authenticate, server drops, and mstsc working against the same box is what you'd expect from gnome-remote-desktop's "Remote Login" handover, where the server authenticates you on one endpoint and then redirects the client to the actual session. mstsc follows that redirect automatically; right now IronRDP doesn't, so it just sees the disconnect. I couldn't confirm it from the pcap alone, though, because everything after the handshake is inside TLS, so the redirect PDU (if that's what it is) isn't readable. One thing worth flagging: the branch you built from only has the capabilities-exchange fix. It's missing two capability advertisements that my working setup uses EGFX dynamic-channel support, and redirection support in the cluster data (the old TODO(#139)). The redirection one matters here, because some servers only perform the handover redirect when the client advertises that it supports it. I've put together a test branch that adds those two, plus some temporary logging that prints each PDU during the activation sequence (look for [redir-diag] lines). Could you rebuild from it and share the output? The most useful part is the last [redir-diag] line before the disconnect, and whether any line shows looks_like_redirect=true. Branch: https://github.com/rdeangel/IronRDP/tree/test/gnome-handover-diag If that confirms a redirect, the next step is bigger: IronRDP would need actual server-redirection handling (parse the redirect, then reconnect with the routing token and the session credentials it hands back). That's not in the library yet TODO(#139) only covers advertising support, not following the redirect so it'd be a separate piece of work, but at least we'd know that's the right direction. |
|
I've compiled the new branch, and there are no |
|
Right, Looking at the log, the connection reaches this point: 2026-06-15T16:37:40.326Z INFO ironrdp_web::session: Connected! The canvas initializes and the session starts. What happens next is a separate issue: 2026-06-15T16:37:40.402Z DEBUG ironrdp_web::session: Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence Session error: [General] [ConnectionActivation::CapabilitiesExchange @ /code/src/github.com/rdeangel/IronRDP/crates/ironrdp-connector/src/lib.rs:409] reason: unexpected Share Control PDU during capabilities exchange: got Data PDU (expected Server Demand Active PDU) Gnome sends a Server Deactivate All immediately after connecting, which triggers a Deactivation-Reactivation Sequence. This is a different bug and would need either gnome-side investigation or a follow-up IronRDP fix to tolerate Data PDUs during reactivation the same way the DeactivateAll case is already handled. |
Fixes #1362
Problem
#1254 taught the inner
ConnectionActivationSequenceto skip a Server Deactivate All PDU received before Server Demand Active (sent by e.g. Windows Server and gnome-remote-desktop as part of a Deactivation-Reactivation Sequence, MS-RDPBCGR 1.3.1.3): it returnsWritten::Nothingand remains inCapabilitiesExchange.However, the outer
ClientConnector::step()only acceptsConnectionFinalizationas the resulting inner state after callingconnection_activation.step(), so the same scenario still fails withinvalid state (this is a bug)when it happens during the initial connection sequence — exactly the symptom reported in #1362.Changes
ClientConnector::step(): when the inner sequence stays inCapabilitiesExchange, the outer connector stays inCapabilitiesExchangetoo and waits for the next input.ClientConnectorinstead of the inner sequence (fails withinvalid statebefore the fix).🤖 Generated with Claude Code