fix(pdu): tolerate unknown GCC user-data blocks instead of failing#1367
fix(pdu): tolerate unknown GCC user-data blocks instead of failing#1367Hani (mojo17) wants to merge 1 commit into
Conversation
As an RDP server, IronRDP fails to negotiate a connection with the current Windows App (the msrdc client): ClientGccBlocks/ServerGccBlocks return an error on the first GCC user-data block whose blockType is not one of the variants IronRDP models, which aborts the whole decode and drops the connection during the Conference Create exchange. The Windows App trips this in two ways: - It sends a TS_UD_CS_UNUSED1 block (blockType 0xC00C). This block is documented and MS-RDPBCGR 2.2.1.3.9 states it "SHOULD be ignored by the server if sent by the client", but IronRDP did not model it (the ClientGccType set stopped at 0xC00A), so it was rejected rather than ignored. - It also sends blockType 0xC00D, which is undocumented: it is in neither the User Data Header type list in MS-RDPBCGR 2.2.1.3.1 nor FreeRDP's parser, mirroring the historical 0xC009 block sent by the Lync client. GCC user-data blocks are a length-prefixed TLV (TS_UD_HEADER is type + length + value), so a reader can seek past an unknown block and continue; FreeRDP logs and skips unknown blocks rather than aborting. Add UserDataHeader::decode_raw, which returns the raw u16 blockType and the block body without mapping the type to an enum. The block decoders use it and skip user-data blocks they do not recognise, advancing past the body via the block length. The existing generic decode is retained (now a thin wrapper over decode_raw) so its API and behavior are unchanged. Also model TS_UD_CS_UNUSED1 (0xC00C) as an explicit no-op so the one documented ignorable block is handled by name. Add tests covering a skipped unknown block, an ignored CS_UNUSED1 block, and decode_raw returning a raw unrecognised blockType.
|
The GCC user-data section is a length-prefixed block list, designed so a reader can step over a block it does not model, and the table in 2.2.1.3.1 is open: Microsoft has added types over the years without revising the public spec. Rejecting an unrecognized blockType is therefore a conformance bug rather than a safe default. A server that rejects such blocks cannot finish Conference Create with current Windows App and msrdc builds. I maintain an IronRDP-based server downstream, so this is the exact path that matters to me. The shape is right. Applying it symmetrically to both One cheap addition is worth considering. A trace-level log of the skipped |
|
Thanks for the detailed read, and for confirming this from the downstream-server side. That's exactly the path this is meant to unblock. On the trace log: I agree the observability is worth having, but I'd lean against putting it inside the decoder here. |
Summary
As an RDP server, IronRDP currently fails to negotiate a connection with
current Microsoft clients (the Windows App /
msrdc): it aborts thewhole GCC decode the moment it encounters a user-data block whose
blockTypeit does not model, dropping the connection during theConference Create exchange. This change makes the GCC block decoders skip
blocks they do not recognise and continue, which is what the protocol's
wire format is designed for and what other implementations already do.
Problem
When decoding a GCC Conference Create Request/Response,
ClientGccBlocksand
ServerGccBlocksreturn an error on the first user-data block whoseblockTypeis not one of the variants IronRDP models, which aborts theentire decode and fails the connection.
The current Windows App (the
msrdcclient) trips this in two ways:TS_UD_CS_UNUSED1block (blockType0xC00C). This blockis documented, and MS-RDPBCGR 2.2.1.3.9 states it "SHOULD be ignored
by the server if sent by the client", but IronRDP did not model it
(the
ClientGccTypeset stopped at0xC00A), so it was rejectedrather than ignored.
blockType0xC00D, which is undocumented:it is in neither the User Data Header type list in MS-RDPBCGR 2.2.1.3.1
nor FreeRDP's parser. This follows the pattern of the
0xC009blockhistorically sent by the Lync RDP client, also absent from the spec.
Either block causes the decoder to reject the connection, so an
ironrdp-serverpeer cannot complete the connection sequence with thisclient at all.
Why tolerate unknown blocks
GCC user-data blocks are a length-prefixed TLV structure:
TS_UD_HEADERis
type(2 bytes) +length(2 bytes) + value. The length existsprecisely so a reader that does not recognise a
typecan seek past thepayload and continue with the next block, which is the standard
forward-compatibility contract for this wire format. Rejecting on an
unknown type forfeits that.
This is also how the most widely used open-source RDP implementation
behaves. FreeRDP's GCC parser logs the unknown block and continues
rather than aborting the handshake:
It advances by the block's length and proceeds. FreeRDP also models
CS_UNUSED1(0xC00C) as a named case and carries0xC009as a literalcase, matching the approach this PR takes.
Change
UserDataHeader::decode_raw, which returns the rawu16blockTypeand the block body without mapping the type to an enum.The block length is honoured, so the cursor can always advance past a
block the caller does not recognise.
ClientGccBlocks::decodeandServerGccBlocks::decodenow use it andskip user-data blocks they do not model instead of erroring.
TS_UD_CS_UNUSED1(blockType0xC00C) explicitly. PerMS-RDPBCGR 2.2.1.3.9 it is a padding block that "SHOULD be ignored
by the server if sent by the client", so it is handled as a named
no-op rather than going through the generic unknown-block path.
The existing generic
UserDataHeader::decodeis kept as a thin wrapperover
decode_raw, so its signature and behaviour are unchanged. This PRis non-breaking.
Tests
from_buffer_skips_unknown_client_gcc_block_and_parses_known_blocksverifies an undocumented
0xC00Dblock is skipped while the knownblocks still parse.
from_buffer_ignores_cs_unused1_client_gcc_blockverifies aCS_UNUSED1block is recognised and ignored.decode_raw_returns_raw_block_type_for_unknown_typeverifiesdecode_rawsurfaces the raw unrecognisedblockType.