Skip to content

fix(connector): stay in CapabilitiesExchange when activation handles DeactivateAll#1371

Open
Rocco De Angelis (rdeangel) wants to merge 1 commit into
Devolutions:masterfrom
rdeangel:fix/connector-capabilities-exchange-same-state
Open

fix(connector): stay in CapabilitiesExchange when activation handles DeactivateAll#1371
Rocco De Angelis (rdeangel) wants to merge 1 commit into
Devolutions:masterfrom
rdeangel:fix/connector-capabilities-exchange-same-state

Conversation

@rdeangel

Copy link
Copy Markdown

Fixes #1362

Problem

#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 as part of a Deactivation-Reactivation Sequence, MS-RDPBCGR 1.3.1.3): it returns Written::Nothing and remains in CapabilitiesExchange.

However, the outer ClientConnector::step() only accepts ConnectionFinalization as the resulting inner state after calling connection_activation.step(), so the same scenario still fails with invalid state (this is a bug) when it happens during the initial connection sequence — exactly the symptom reported in #1362.

Changes

  • Add the missing match arm in ClientConnector::step(): when the inner sequence stays in CapabilitiesExchange, the outer connector stays in CapabilitiesExchange too and waits for the next input.
  • Add a regression test mirroring the existing fix(connector): handle ServerDeactivateAll during CapabilitiesExchange #1254 tests, driving the outer ClientConnector instead of the inner sequence (fails with invalid state before the fix).

🤖 Generated with Claude Code

…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>
@mahcsig

Copy link
Copy Markdown

Rocco De Angelis (@rdeangel) thank you for looking into this!

I've tried compiling again from your branch and it does get past the ServerDectivateAll, but it now errors out saying [decode_send_data_indication @ /code/src/github.com/rdeangel/IronRDP/crates/ironrdp-connector/src/lib.rs:409] reason: received disconnect provider ultimatum: UserRequested

@rdeangel

Copy link
Copy Markdown
Author

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:

  • What server is this (Windows version / GNOME RD / xrdp / other)?
  • Does the same server connect OK with another client like FreeRDP or mstsc? That'd tell us whether it's something specific to IronRDP or a server-side decision.
  • If you can grab a trace log (or Wireshark capture) of the PDUs right after the ServerDeactivateAll, that'd show whether the server sends anything before the disconnect.

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.

@mahcsig

Copy link
Copy Markdown

That makes sense, to help narrow it down:

  • the server is Ubuntu 24.0.4 LTS, using the gnome-remote-desktop service
    • win11 connects properly, so it may be something specific gnome is doing?
  • mstsc is able to connect to the same server
  • I'm not sure how to start the capture after the ServerDeactivateAll, but here is the updated pcap of the connection attempt after the fix: rdp-ironrdp.zip

@rdeangel

Copy link
Copy Markdown
Author

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.

@mahcsig

Copy link
Copy Markdown

I've compiled the new branch, and there are no looks_like_redirect=true, it looks like it's getting a data pdu instead of the ServerDemandActive pdu it expects

console.log

@rdeangel

Copy link
Copy Markdown
Author

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.
Per MS-RDPBCGR 1.3.1.3
(https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432), after a Deactivate All the Capability Exchange phase should be re-executed. Instead, gnome sends a Data PDU, which IronRDP rejects.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Invalid state error using gnome-remote-desktop

2 participants