feat(pdu,session,connector,server)!: support session resume via the auto-reconnect cookie - #1501
Merged
Marc-André Moreau (mamoreau-devolutions) merged 1 commit intoAug 3, 2026
Conversation
Greg Lamberson (glamberson)
force-pushed
the
feat/client-auto-reconnect-cookie
branch
from
July 31, 2026 16:57
5427859 to
c87e7cc
Compare
Greg Lamberson (glamberson)
force-pushed
the
feat/client-auto-reconnect-cookie
branch
from
July 31, 2026 19:08
c87e7cc to
76c35a9
Compare
Greg Lamberson (glamberson)
force-pushed
the
feat/client-auto-reconnect-cookie
branch
from
July 31, 2026 23:18
76c35a9 to
0f97db3
Compare
Greg Lamberson (glamberson)
force-pushed
the
feat/client-auto-reconnect-cookie
branch
from
August 2, 2026 23:54
0f97db3 to
62b2abb
Compare
…onnect cookie A client that loses its connection ungracefully can reattach to the session instead of asking the user to log on again, provided it returns the cookie the server issued ([MS-RDPBCGR] 1.3.1.5). Devolutions#1509 built the server half of that: it validates a returning ARC_CS_PRIVATE_PACKET and rotates the random. The client half is still missing. The session layer decodes the cookie and drops it, and the connector has no way to send one back, so ironrdp-client cannot resume a session against ironrdp-server or against mstsc's peer. The wire encoding was already in place. ExtendedClientOptionalInfo carries, encodes and decodes a 28-byte autoReconnectCookie and its builder already had a reconnect_cookie step; ServerAutoReconnect already decoded, and Devolutions#1509 added the ClientAutoReconnect structure and its decode. Nothing connected them. Receive: SaveSessionInfo now surfaces the cookie as ProcessorOutput::AutoReconnectCookie and ActiveStageOutput::AutoReconnectCookie rather than logging it and returning nothing. The server replaces the cookie on every connect and again hourly ([MS-RDPBCGR] 3.3.6.2), so this can arrive more than once per session and the consumer keeps the most recent. Derive: ClientAutoReconnect gains from_server_cookie, implementing [MS-RDPBCGR] 5.5, SecurityVerifier = HMAC(AutoReconnectRandom, ClientRandom), keyed by the server's 16 random bytes with MD5 as the hash. Enhanced RDP Security generates no client random and 5.5 substitutes 32 zero bytes; IronRDP has no Standard RDP Security path, so that is the only case. This moves hmac into ironrdp-pdu, alongside the md-5 it already used. Send: ClientConnector::with_auto_reconnect_cookie takes the cookie last received and makes the connector put the derived Client Auto-Reconnect Packet ([MS-RDPBCGR] 2.2.4.3) in the Client Info PDU. Absent, that PDU is byte-for-byte what it was. Unlike the server packet this structure has no enclosing logon-info field header, so it encodes to exactly the 28 bytes the cookie field expects. to_bytes writes that layout directly rather than through Encode, so filling a fixed-size field has no error path; a test pins the two to agree. One derivation, not two. Putting from_server_cookie in ironrdp-pdu would otherwise duplicate the private HMAC in ironrdp-server that Devolutions#1509 added, so this also gives ClientAutoReconnect a verify method and routes the server through it. verify keeps the constant-time comparison the server had: the verifier is the whole credential, so an early-exit compare would leak it a byte at a time. The server keeps only the policy around it, which cookies are live and whether the security protocol permits auto-reconnect, and drops its hmac and md-5 dependencies. That move also rehomes the known-answer tests @clintcan contributed on Devolutions#1509. They were written as an inline #[cfg(test)] module in ironrdp-server, which sets [lib] test = false, so they never ran. They now live in ironrdp-testsuite-core against the public API, where CI executes them, together with his HMAC-MD5 reference vector and the cases covering a tampered verifier and a mismatched logon ID. ironrdp-client, ironrdp-web and the FFI bindings gain an arm for the new output. None of them reconnect automatically yet, which is the remaining part of Devolutions#271. BREAKING CHANGE: ActiveStageOutput and x224::ProcessorOutput gain a variant and ClientConnector gains a public field, so exhaustive matches and struct literals need updating. Confirmed with cargo-semver-checks; the ironrdp-pdu half is additive.
Greg Lamberson (glamberson)
force-pushed
the
feat/client-auto-reconnect-cookie
branch
from
August 2, 2026 23:59
62b2abb to
e3a67ca
Compare
Contributor
Author
|
Marc-André Moreau (@mamoreau-devolutions) ready here i think. |
Marc-André Moreau (mamoreau-devolutions)
approved these changes
Aug 3, 2026
Marc-André Moreau (mamoreau-devolutions)
merged commit Aug 3, 2026
74b3365
into
Devolutions:master
26 checks passed
Greg Lamberson (glamberson)
deleted the
feat/client-auto-reconnect-cookie
branch
August 3, 2026 00:48
GlassOnTin
added a commit
to GlassOnTin/IronRDP
that referenced
this pull request
Aug 3, 2026
…or EGFX clients Adds an opt-in `Config::support_dyn_vc_gfx_protocol` (default `false`). When set, `SUPPORT_DYN_VC_GFX_PROTOCOL` is OR'd into the client's early capability flags, so the server may negotiate `Microsoft::Windows::RDS::Graphics` for surface-based graphics instead of the legacy slow-path bitmap protocol. BREAKING CHANGE: `Config` is not `#[non_exhaustive]`, so the new field breaks downstream struct literals. Same shape as Devolutions#1501, which added `auto_reconnect_cookie` to this struct. The default is `false` on purpose. A client that advertises the bit without wiring an EGFX-capable `DvcClientProcessor` for that channel gets modern Windows servers to stop sending legacy bitmap updates altogether and route everything over EGFX, leaving the desktop blank — so the doc comment spells out the three steps to enable it properly. Coverage: - create_gcc_blocks unit tests (inline mod at file end, per Devolutions#1527) assert the bit is clear by default, set when enabled, and that the opt-in's symmetric difference against the default flag set is exactly that one bit — so it cannot quietly disturb the others. Verified to fail: gating on `if false` reddens two of the three. - testsuite-extra e2e runs a full client<->server connect with the bit advertised. Scope stated in the test: it asserts negotiation completes, not that the server acted on it, since ironrdp-server does not surface the client's early capability flags.
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.
What
The client half of automatic reconnection. The session layer surfaces the Server Auto-Reconnect Cookie,
ironrdp-pduderives and verifies the client's response to it, and the connector sends that response when resuming a session.Why
A client whose connection drops ungracefully can reattach to its session instead of making the user log on again, provided it returns the cookie the server issued during logon (MS-RDPBCGR 1.3.1.5).
#1509 built the server side of that: it validates a returning
ARC_CS_PRIVATE_PACKETand rotates the random. Nothing answers it.ironrdp-sessiondecodes the cookie and drops it,ironrdp-connectorhas no way to send one back, andTODO(#271)still sits inironrdp-client. Soironrdp-clientcannot resume a session againstironrdp-server, and the validation #1509 added has no in-tree counterpart to exercise it.The wire encoding was already there.
ExtendedClientOptionalInfocarries, encodes and decodes a 28-byteautoReconnectCookieand its builder already had areconnect_cookiestep;ServerAutoReconnectalready decoded; #1509 addedClientAutoReconnectand its decode. Nothing connected them.The three parts
Receive.
SaveSessionInfonow also surfaces the cookie, asProcessorOutput::AutoReconnectCookieandActiveStageOutput::AutoReconnectCookie. #1522 added aSaveSessionInfo { logon_complete }output on that same handler; the two coexist rather than compete, since both are read off one PDU and neither supersedes the other. The handler emits the logon notification unconditionally and appends the cookie when one is present, and a test pins that surfacing the cookie does not suppress the notification. #1509's server replaces the cookie whenever a client connects and again hourly (MS-RDPBCGR 3.3.6.2), so this can arrive more than once in a session and the consumer keeps the most recent.Derive.
ClientAutoReconnect::from_server_cookieimplements MS-RDPBCGR 5.5:IronRDP implements no Standard RDP Security path (there is no Security Exchange PDU), so the zero-client-random case is the only one that arises. As 5.5 notes, that makes the verifier constant for a given cookie, so it proves possession of the cookie and nothing more; session security comes from the outer TLS/CredSSP handshake.
clintcan independently confirmed this construction against real mstsc while validating #1509 (comment): a Windows client's
ARC_CS_PRIVATE_PACKETverifies againstHMAC-MD5(random_bits, [0u8; 32]). That is the same derivation implemented here, so the two halves interoperate with Microsoft's client and not only with each other.Send.
ClientConnector::with_auto_reconnect_cookietakes the cookie last received and makes the connector put the derived Client Auto-Reconnect Packet (MS-RDPBCGR 2.2.4.3) in the Client Info PDU. Absent, that PDU is byte-for-byte what it was.Unlike the server packet, this structure has no enclosing logon-info field header, so it encodes to exactly the 28 bytes the cookie field expects.
to_byteswrites that layout directly rather than going throughEncode, so filling a fixed-size field has no error path a caller must handle; a test pins the two to agree.One derivation, not two
Putting
from_server_cookieinironrdp-pduwould leave the workspace with two implementations of 5.5, since #1509 added a private HMAC toironrdp-server. SoClientAutoReconnectalso gainsverify, and the server routes through it.verifykeeps the constant-time comparison the server had. The verifier is the whole credential, so a comparison returning early on the first differing byte would let a peer recover it a byte at a time from the timing; the session identifier is not secret and is compared normally.ironrdp-serverkeeps the policy around the check, which cookies are live and whether the security protocol permits auto-reconnect, and drops itshmacandmd-5dependencies.hmacmoves toironrdp-pduasdefault-features = false; the crate's full feature powerset still checks clean, including--no-default-features.I would rather not have reached into
ironrdp-serverin apdu,session,connectorchange, but the alternative was shipping the duplicate and filing a follow-up to remove it, which is a worse trade for reviewer time.Tests that were not running
That move also rehomes the known-answer tests clintcan contributed on #1509. They went in as an inline
#[cfg(test)]module incrates/ironrdp-server/src/server.rs, and that crate sets[lib] test = false, so they have never executed in CI. They now live inironrdp-testsuite-coreagainst the public API, where CI runs them: his HMAC-MD5 reference vector is kept as a second vector alongside a differently-keyed one, plus the cases for a tampered verifier and a mismatched logon ID.Worth flagging separately:
ironrdp-serveris not alone.ironrdp-agent,ironrdp-sessionandironrdp-webalso set[lib] test = falseand between them carry 16 files of inline#[cfg(test)]modules that CI never runs. That is out of scope here, but I am happy to open an issue if it would be useful.Breaking changes
ActiveStageOutputandx224::ProcessorOutputgain a variant, andClientConnectorgains a public field, so exhaustive matches and struct literals need updating.Confirmed with
cargo-semver-checksagainst the merge-base: those three are the only findings this branch introduces. The others it reports onmastertoday (ShareDataPdu::Compressedand theShareDataCtxfields from #1518,ProcessorBuilder.bulk_decompressorfrom #1518,ServerEvent::SetAutoReconnectCookiefrom #1509) are present onmasterunchanged. Theironrdp-pduadditions are additive.Scope
This is the library half.
ironrdp-client,ironrdp-weband the FFI bindings gain an arm for the new output but none of them reconnect automatically yet; that is the remaining part of #271, and the existingTODO(#271)inironrdp-clientmarks where it goes.I kept receive, derive and send together deliberately. Split up, none of them is usable on its own: without the receive half there is no way to obtain a cookie, and without the send half there is nothing to do with one.
Tests
Thirteen, all in
ironrdp-testsuite-core.On the packet and the derivation: the
SecurityVerifiermatches two independently computed HMAC-MD5 vectors of 32 zero bytes under different keys, so the tests pin the derivation rather than restating the code; the logon ID carries over from the server cookie; the encoding matches the 2.2.4.3 field layout byte for byte withcbLenfixed at0x1C;to_bytesagrees withEncode; it round-trips; and it rejects both a wrong packet length and an unknown version.On verification: a derived answer is accepted, a single flipped byte in the verifier is rejected, a correct verifier under a different logon ID is rejected, and an answer derived from a different random is rejected.
On the surfacing path: a Save Session Info PDU framed the way a server sends it, through the real x224 processor, yields an
AutoReconnectCookiecarrying the right logon ID and random bits, alongside #1522's logon notification rather than in place of it; and one without a cookie surfaces no cookie.Verification
cargo xtask check fmt/lints/tests/typos/locksall pass on 1.94.1, including afuzz/build before the lock check.Note
#1496 also touches the
ClientAutoReconnectdeclaration. Whichever of the two lands second needs a one-line rebase on the derive attribute; happy to take that in either order.